-
Notifications
You must be signed in to change notification settings - Fork 0
fix: IME切り替えを物理かな/英数キー発行方式に変更(+ claude-lmstudio / prettier整理) #996
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
90c0fb9
a26d507
ea70f7a
b10b686
e62876c
ce3ccdc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,3 +6,6 @@ CLAUDE.md | |
|
|
||
| # Auto-generated context files | ||
| .context/ | ||
|
|
||
| # Playwright MCP session artifacts | ||
| .playwright-mcp/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,20 @@ | ||
| # Expose selected shared local secrets to CLI tools such as Codex MCP servers. | ||
| # NOTE: This list is duplicated in nix/home/zsh.nix (home.file). Keep both in sync. | ||
| # Clear previously exported tokens so removed entries in ~/.devcontainer.env don't linger. | ||
| unset SUPABASE_ACCESS_TOKEN VERCEL_TOKEN LINEAR_API_KEY DOPPLER_TOKEN | ||
| if [[ -r "$HOME/.devcontainer.env" ]]; then | ||
| while IFS='=' read -r _codex_env_key _codex_env_value || [[ -n $_codex_env_key ]]; do | ||
| # Strip trailing CR so Windows-style CRLF files work correctly. | ||
| _codex_env_value="${_codex_env_value%$'\r'}" | ||
| case "$_codex_env_key" in | ||
| # Allowed keys: SUPABASE_ACCESS_TOKEN | VERCEL_TOKEN | LINEAR_API_KEY | DOPPLER_TOKEN | ||
| SUPABASE_ACCESS_TOKEN|VERCEL_TOKEN|LINEAR_API_KEY|DOPPLER_TOKEN) | ||
| # NOTE: This loader is duplicated in nix/home/zsh.nix (home.file). Keep both in sync. | ||
| # 許可キー一覧は組織名を含むため private-config 管理の外部ファイルに置く | ||
| _codex_env_allowlist="$HOME/.config/devcontainer-env-keys.txt" | ||
| if [[ -r "$_codex_env_allowlist" ]]; then | ||
| # Clear previously exported tokens so removed entries in ~/.devcontainer.env don't linger. | ||
| while IFS= read -r _codex_env_key || [[ -n $_codex_env_key ]]; do | ||
| [[ -n $_codex_env_key && $_codex_env_key != \#* ]] && unset "$_codex_env_key" | ||
| done < "$_codex_env_allowlist" | ||
| if [[ -r "$HOME/.devcontainer.env" ]]; then | ||
| while IFS='=' read -r _codex_env_key _codex_env_value || [[ -n $_codex_env_key ]]; do | ||
| # Strip trailing CR so Windows-style CRLF files work correctly. | ||
| _codex_env_value="${_codex_env_value%$'\r'}" | ||
| if [[ -n $_codex_env_key && $_codex_env_key != \#* ]] && grep -qxF -- "$_codex_env_key" "$_codex_env_allowlist"; then | ||
| export "$_codex_env_key=$_codex_env_value" | ||
| ;; | ||
| esac | ||
| done < "$HOME/.devcontainer.env" | ||
| unset _codex_env_key _codex_env_value | ||
| fi | ||
| done < "$HOME/.devcontainer.env" | ||
| fi | ||
| fi | ||
| unset _codex_env_key _codex_env_value _codex_env_allowlist |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| #!/usr/bin/env bash | ||
| # Launch Claude Code against a local LM Studio server (Anthropic-compatible API). | ||
| # LM Studio 0.4.1+ serves POST /v1/messages natively, so no proxy is needed. | ||
| # | ||
| # IMPORTANT: use an MLX build of the model, not GGUF. Claude Code's tool schemas | ||
| # contain JSON-schema `pattern` regexes with `\w`, which llama.cpp's GBNF grammar | ||
| # parser rejects ("failed to parse grammar"). The MLX runtime handles these, so | ||
| # GGUF models fail on tool use while MLX models work end-to-end (verified 2026-07-15). | ||
| # | ||
| # The MLX runtime loads a vendored CPython from | ||
| # ~/.lmstudio/extensions/backends/vendor/_amphibian/cpython3.11-mac-arm64@*. If that | ||
| # directory goes missing, every MLX model fails to load with | ||
| # "Library not loaded: @rpath/libpython3.11.dylib"; repair it from the LM Studio app's | ||
| # Runtime tab (`lms runtime update`/`get` report it as already installed and do not fix it). | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| BASE_URL="${LMSTUDIO_BASE_URL:-http://localhost:1234}" | ||
| MODEL="${LMSTUDIO_MODEL:-qwen/qwen3-coder-next}" # MLX build; override for another MLX model | ||
| AUTH_TOKEN="${LMSTUDIO_AUTH_TOKEN:-lmstudio}" | ||
| CONTEXT_LENGTH="${LMSTUDIO_CONTEXT_LENGTH:-262144}" | ||
|
|
||
| usage() { | ||
| cat <<'EOF' | ||
| Usage: claude-lmstudio [claude-args...] | ||
|
|
||
| Runs `claude` with ANTHROPIC_BASE_URL pointed at a local LM Studio server. | ||
| Any extra arguments are passed through to the claude CLI. | ||
|
|
||
| Environment overrides: | ||
| LMSTUDIO_BASE_URL LM Studio endpoint (default: http://localhost:1234) | ||
| LMSTUDIO_MODEL Model id to request (default: qwen/qwen3-coder-next, MLX build) | ||
| LMSTUDIO_AUTH_TOKEN Auth token if LM Studio requires one (default: lmstudio) | ||
| LMSTUDIO_CONTEXT_LENGTH Context window to load the model with (default: 262144) | ||
|
|
||
| Use an MLX build of the model. GGUF models fail on Claude Code tool use because | ||
| llama.cpp's grammar parser rejects the `\w` regex in tool JSON schemas. | ||
| EOF | ||
| } | ||
|
|
||
| if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then | ||
| usage | ||
| exit 0 | ||
| fi | ||
|
|
||
| for cli in claude lms; do | ||
| if ! command -v "$cli" >/dev/null 2>&1; then | ||
| echo "claude-lmstudio: '${cli}' CLI not found in PATH" >&2 | ||
| exit 1 | ||
| fi | ||
| done | ||
|
|
||
| # Fail fast if the local server is unreachable, with a clear hint. | ||
| if ! curl -fsS -m 3 "${BASE_URL}/v1/models" >/dev/null 2>&1; then | ||
| echo "claude-lmstudio: cannot reach LM Studio at ${BASE_URL}." >&2 | ||
| echo " Start it with: lms server start --port 1234 (or launch the LM Studio app)." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| # LM Studio's JIT loader picks an 8k context, which cannot even hold Claude Code's | ||
| # system prompt ("number of tokens to keep from the initial prompt is greater than | ||
| # the context length"). Report the resident copies of $MODEL, splitting them by | ||
| # whether their window is big enough: `usable` on the first line, then one | ||
| # too-small identifier per line. | ||
| resident_copies() { | ||
| lms ps --json 2>/dev/null | | ||
| MODEL="$MODEL" CONTEXT_LENGTH="$CONTEXT_LENGTH" node -e ' | ||
| let raw = ""; | ||
| process.stdin.on("data", (c) => (raw += c)); | ||
| process.stdin.on("end", () => { | ||
| const want = Number(process.env.CONTEXT_LENGTH); | ||
| const copies = JSON.parse(raw || "[]").filter((m) => m.modelKey === process.env.MODEL); | ||
| console.log(copies.filter((m) => m.contextLength >= want).length); | ||
| for (const m of copies.filter((m) => m.contextLength < want)) console.log(m.identifier); | ||
| }); | ||
| ' | ||
| } | ||
|
|
||
| # Loading a second copy alongside a small one is not enough: LM Studio routes by model | ||
| # key and keeps serving the small copy, so drop those before loading a usable one. | ||
| usable_count="" | ||
| while read -r line; do | ||
| if [[ -z "$usable_count" ]]; then | ||
| usable_count="$line" | ||
| else | ||
| echo "claude-lmstudio: unloading ${line} (context below ${CONTEXT_LENGTH})..." >&2 | ||
| lms unload "$line" >&2 | ||
| fi | ||
| done < <(resident_copies) | ||
|
|
||
| if [[ "${usable_count:-0}" -eq 0 ]]; then | ||
| echo "claude-lmstudio: loading ${MODEL} with a ${CONTEXT_LENGTH}-token context..." >&2 | ||
| lms load "$MODEL" --context-length "$CONTEXT_LENGTH" -y >&2 | ||
| fi | ||
|
|
||
| export ANTHROPIC_BASE_URL="$BASE_URL" | ||
| export ANTHROPIC_AUTH_TOKEN="$AUTH_TOKEN" | ||
| export CLAUDE_CODE_ATTRIBUTION_HEADER=0 | ||
|
|
||
| exec claude --model "$MODEL" "$@" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| #!/bin/sh | ||
| set -eu | ||
|
|
||
| data_home="${XDG_DATA_HOME:-${HOME}/.local/share}" | ||
| src="${data_home}/input-source/send-ime-key.swift" | ||
|
|
||
| exec /usr/bin/xcrun swift "$src" "$@" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win Pre-compile the Swift script to eliminate execution latency. Invoking Consider pre-compiling the Swift script into a binary (e.g., using 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import CoreGraphics | ||
| import Foundation | ||
|
|
||
| // Emit the physical Japanese IME keys (かな / 英数) via CGEvent so the active | ||
| // input method reliably switches its conversion mode. | ||
| // | ||
| // TISSelectInputSource on an input *mode* (base <-> Roman) of the same input | ||
| // method only updates the menu-bar indicator; it does not reliably notify the | ||
| // already-running Google Japanese IME to change its conversion mode. That leaves | ||
| // the tooltip showing Hiragana while typing still produces alphanumeric. | ||
| // The physical かな/英数 keys are handled by macOS at the HID level and switch | ||
| // the IME (and the input source) reliably, which is exactly what Kanary's | ||
| // Command-tap mappings rely on. | ||
|
|
||
| func keyCode(for name: String) -> CGKeyCode? { | ||
| switch name { | ||
| case "kana", "hiragana", "japanese": | ||
| return 104 // かな key | ||
| case "eisuu", "eisu", "alphanumeric", "roman": | ||
| return 102 // 英数 key | ||
| default: | ||
| if let raw = UInt16(name) { | ||
| return CGKeyCode(raw) | ||
| } | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| guard CommandLine.arguments.count == 2, let key = keyCode(for: CommandLine.arguments[1]) else { | ||
| fputs("usage: send-ime-key <kana|eisuu|keycode>\n", stderr) | ||
| exit(64) | ||
| } | ||
|
|
||
| let source = CGEventSource(stateID: .hidSystemState) | ||
| guard | ||
| let keyDown = CGEvent(keyboardEventSource: source, virtualKey: key, keyDown: true), | ||
| let keyUp = CGEvent(keyboardEventSource: source, virtualKey: key, keyDown: false) | ||
| else { | ||
| fputs("failed to create key events\n", stderr) | ||
| exit(1) | ||
| } | ||
|
|
||
| keyDown.post(tap: .cghidEventTap) | ||
| keyUp.post(tap: .cghidEventTap) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Add
nodeto the required CLI check.The script uses
nodeto parse the output oflms ps --json(on line 67). Addingnodeto the fail-fast check ensures the script produces a clear error if Node.js is missing, rather than failing opaquely during execution.💻 Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents