{"owner":"tw93","repo":"Kaku","hasSkills":true,"totalSkillsCount":11,"totalTokensCount":34258,"categories":["root-instruction","anthropic-skill"],"hasMcp":false,"mcpConfig":null,"found":["kaku-gui/AGENTS.md","AGENTS.md","config/AGENTS.md","crates/AGENTS.md",".agents/skills/bugs/SKILL.md",".agents/skills/release/SKILL.md","term/AGENTS.md","lua-api-crates/AGENTS.md","mux/AGENTS.md",".claude/rules/macos.md","termwiz/AGENTS.md"],"skills":{"kaku-gui/AGENTS.md":"# Kaku GUI Agent Guide\n\n`kaku-gui` owns rendering, window lifecycle, user interaction, AI chat, and the `k` helper binary.\n\n## Scope\n\n`kaku-gui` is responsible for:\n- terminal window rendering\n- input and mouse handling\n- tab/pane UI behavior\n- startup lifecycle and app event flow\n- AI chat overlays, approval UI, inline status, syntax highlighting, and assistant transport\n- the `k` helper binary under `src/bin/k.rs`\n\n## Where to Look\n\n- `src/termwindow/mod.rs`: core window state and action dispatch\n- `src/termwindow/render/`: rendering pipeline\n- `src/termwindow/mouseevent.rs`: mouse and drag behavior\n- `src/termwindow/webgpu.rs`: surface resize and present mode handling\n- `src/frontend.rs`: app lifecycle and macOS integration events\n- `src/overlay/`: launcher and overlays\n- `src/overlay/ai_chat/`: AI chat overlay, markdown rendering, syntax highlighting, and Waza integration\n- `src/ai_chat_engine/`: AI chat engine, approval, and compaction flow\n- `src/ai_client.rs`, `src/ai_remote.rs`, `src/ai_tools/`: provider and tool integration. Keep tool-name strings stable so persisted conversations replay correctly.\n- `src/ai_tools/paths.rs`: sandbox and sensitive-path guards.\n- `src/ai_tools/fs.rs`: read, list, write, patch, mkdir, and delete tools.\n- `src/ai_tools/shell.rs`: exec, background execution, and polling.\n- `src/ai_tools/web.rs`: search, fetch, and URL reading.\n- `src/ai_tools/search.rs`: grep and symbol search.\n- `src/ai_tools/project.rs`: project summaries and file tree helpers.\n- `src/ai_tools/soul.rs`: memory and soul reads.\n- `src/ai_tools/registry.rs`: `ToolDef`, `all_tools`, and `to_api_schema`.\n- `src/cli_chat/`: command-line chat flow\n- `src/thread_util.rs`: macOS worker thread lifetime helpers\n- `src/tabbar.rs`: tab bar behavior\n\n## Practical Rules\n\n- Keep rendering decisions in rendering modules, not scattered across event code.\n- Paint-time Kaku TUI detection must reject non-Kaku foreground executables using the cached leader name before requesting the full process tree. Keep the executable and argument confirmation for `kaku ai` and `kaku config`; cover pane, window and tab-bar consumers together (#583).\n- Keep initial window dimension math in `TermWindow::new_window()` consistent with `get_os_border()` behavior.\n- In-app update must confirm before replacing the running app. User-facing paths (menu Check for Updates, menu Restart to Update, toast click) go through `check_for_updates_from_menu()` / `confirm_and_apply_update()` in `src/frontend.rs`, not straight to `restart_to_update()`. Exploratory menu check opens a tab without `KAKU_UPDATE_AUTO_CONFIRM`; only the overlay-confirmed path may auto-confirm. CLI direct and brew paths confirm in `kaku/src/update.rs`. Guards: `frontend::tests::user_facing_update_events_route_through_confirm` and `update::imp::tests::brew_and_direct_paths_confirm_before_replace`.\n- Keep `KAKU_CONFIG_CHANGED` fast path in `emit_user_var_event()` before pane ownership checks.\n- Pass fullscreen state into `TabBarState::new()` and preserve fullscreen-specific title button spacing behavior.\n- Keep AI overlay state changes cheap enough for live terminal interaction; avoid blocking render/input loops on provider calls.\n- Preserve approval prompts and inline AI status placement when touching AI chat or shell flows.\n- Chat permission summaries retain the complete target and command, with terminal controls escaped. The native review pane scrolls through all details and refuses approval when the pane is too small to read. Notifications may shorten their preview; the approval payload must not. Chat rendering, picking and mouse hit-testing share `msg_area_height`, `picker_height` and `visible_start`. Preserve a scrolled reader's position as output grows. Code Copy uses the original block, never wrapped display rows; Insert only pastes a completed, control-free single shell line into the original shell pane and never sends Enter.\n- AI HTTP transport must preserve both sides of proxy behavior: external API hosts should honor detected system proxy settings, while loopback, private LAN, link-local, CGNAT/Tailscale-style, `.local`, `NO_PROXY`, and macOS ExceptionsList model endpoints should bypass the proxy and connect directly. Do not describe this as full SOCKS support unless reqwest SOCKS support is actually enabled and smoke-tested. Exception: the `http_request` tool always connects directly to the public IP addresses resolved and validated before approval, for both HTTP and HTTPS. It must never pass that hostname to a proxy for independent resolution; users on proxy-only networks receive a connection error instead of weakening the SSRF boundary.\n- Wrap macOS worker thread spawns in the existing autorelease-pool helper so shutdown does not reintroduce use-after-free crashes.\n\n- Remote pane operations in `src/remote_tabs.rs` retain a stable `PaneId`. Do not send a remote split through `spawn_command_impl`, which resolves the active pane again after queuing. Browser-confirmed closes must bypass `AlwaysPrompt` without switching to another pane, and preserve the last-pane tab restore bookkeeping. The isolated remote live smoke covers both paths.\n\n## Known Pitfalls\n\n- During window drag, terminal move/wheel events may need suppression.\n- A tab drag belongs to one press/release gesture. A new press or motion without the left button exclusively held cancels stale state, including when AppKit swallowed the previous release. Preserve the ordinary release handler for settle animation. For unexplained tab reordering, `debug_key_events` records tab press geometry, motion and reorder decisions without terminal text (#584).\n- WebGPU surface reconfigure should happen only on meaningful state changes.\n- Finder \"open with\" behavior is handled in `frontend.rs` event flow.\n- Option+Click cursor movement fires only when: no mouse grab, no alt screen, click is on the same row as the cursor. It sends Left/Right arrow sequences proportional to the column delta; see `mouseevent.rs`.\n- Block cursor height uses `natural_cell_height` (ignoring `line_height`) so Nvim visual selections match WezTerm proportions.\n- Top border clearance for macOS integrated buttons is state-sensitive:\n  - top tab bar visible -> add small gap\n  - top tab bar hidden or bottom tab bar -> add larger clearance for traffic lights\n- Overlay geometry must update when panes are split, resized, closed, or moved.\n- Alternate-screen wheel behavior is config-controlled; do not hard-code one terminal behavior across normal and alternate screens.\n- Inline `#` AI query status belongs below the buffer, not as a toast, so terminal content remains stable.\n- `fast_model` and proxy config are part of the AI UX contract; verify both when changing provider selection or transport.\n- Per-pane overlays (AI chat) render in place of their underlying pane and live in `pane_state`, not the tab's pane tree. They have no window handle, so their output only repaints the window through `PaneOutput` -> `mux_pane_output_event` -> `is_pane_visible` -> `window.invalidate()`. `is_pane_visible` (`termwindow/mod.rs`) MUST treat an active per-pane overlay pane as visible, and the overlay's `render()` MUST `term.flush()`, or streamed output and the loading spinner freeze until an input event forces a repaint. Symptom-to-cause: if `top` refreshes on its own but the AI chat needs a click, the break is one of these two.\n- Scrollback viewport pruning has two policies in `termwindow/mod.rs`: passive pruning during output uses `normalize_viewport` and snaps to the bottom (`None`), while explicit user-driven scroll requests use `normalize_interactive_viewport` and clamp to `scrollback_top`. During a selection drag, only the pane named by `MouseCapture::TerminalPane` may pin a pruned viewport; sibling panes must retain the passive #448 behavior.\n- Tab bar rendering and the tab rename editor must read `resolved_palette.tab_bar`, which includes the selected color scheme and user overrides. Reading `config.colors.tab_bar` alone ignores scheme colors and makes Fancy tabs unreadable in light themes.\n- Tab bar sizing and hit-testing have pure seams in `src/tabbar.rs`: `tab_width_budget` (per-tab column budget that drives truncation) and `is_tab_hover` (clickable span). The truncation and position regressions (#439/#443/#445) came from layout logic with no test seam, so when you change tab sizing, truncation, or click regions, extend their unit tests instead of only eyeballing the running app. `tab_width_budget` locks the invariant that a tab is never squeezed below 1 column.\n- Launcher entries that target a pane must retain its stable `PaneId`, not a tab or topological pane index. The launcher snapshot can outlive pane exits and split-tree reindexing; resolve the `PaneId` against the live mux only after the overlay closes.\n- Session snapshots skip a pane's content while it is in the alternate screen (`f4aca502`), so a vim or htop screen never lands in restored history. Claude Code and Codex are the exception (`foreground_is_agent_cli` in `tabbar.rs`): their last screen is kept, since it is the conversation. Restore never re-runs the program.\n- A partially failed startup session restore must preserve the original session envelope and scrollback sidecars. Only mark a snapshot consumed after every saved window restores successfully; otherwise exit-time saving can overwrite the only complete copy.\n- Session sidecars compact vector-backed lines before checking visible images, since wide-character continuation cells can retain hidden Kitty placements. Strip remaining images and recompress before encoding. Menu restore guards preserve prior dirty state on failure and carry successful changes through overlapping restores; startup-only restores remain pristine. Exit and update checkpoints capture cwd/output even without structural changes, only in the session-owning process and never during an incomplete restore. SIGTERM requests main-thread event-loop shutdown, then uses the normal exit checkpoint; its listener remains registered through repeated TERM signals. Register it only after existing-instance handoff, never in CLI-only commands (#579).\n- The terminal grid's top origin has one source: `TermWindow::terminal_first_row_offset()` (OS top inset plus a shown top tab bar). Pane backgrounds, split lines and mouse row mapping take it from there so they match where text is drawn; a background that skipped the 6px inset under a top tab bar sat above its split line (#562). A source guard in `render/split.rs` fails if `pane.rs`, `split.rs` or `mouseevent.rs` computes its own. `update_text_cursor` (IME candidate rect) still uses a separate origin and sits `border.top` high; that is a known, unfixed gap.\n- Window padding (2026-10-18): with managed padding (no `config.window_padding` in the user's file) every vertical gap derives from the top padding S in `effective_vertical_padding_with_policy`: content keeps S below window chrome and S/2 from the bottom edge or a visible tab bar, top or bottom, fullscreen included; only a maximized window without a bottom bar runs content to the bottom edge. Rows rarely fill the height, and `padding_left_top` splits that leftover evenly between top and bottom (`split_row_slack`, inside the OS borders), so the gap to the bar no longer jumps between 1pt and a full row. Every consumer (pane rendering, split lines, mouse hit testing) reads `padding_left_top`; do not add a second slack policy in `resize.rs` (the old rebalance there only ever reached the resize increments). The tab bar itself stays one terminal row. Explicit user padding is used as written. Hotkey windows are also excluded from `split_row_slack`: their drawer policy owns the top inset, including an explicit zero. Cover hotkey, ordinary and fullscreen states when changing slack allocation.\n- Split geometry follows mux: `split.top = first.rows + gutter / 2` and the pane backgrounds meet at `first.rows + gutter / 2.0`. With the bundled even row gutter (`split_pane_gap = 2`) the line belongs on the top edge of row `split.top`, with an odd gutter on its middle; `split_row_center_offset` places it and `split_drag_row` measures drags against the drawn line so a grab does not jump a row. Left|right splits always have an odd column gutter. In opaque windows the outer panes' backgrounds fill the right and bottom padding (transparent windows leave those strips to the fill strips in `paint.rs`). With `inactive_pane_hsb` dimming on, the split line takes the active pane's background, so the dimming marks focus and the line only separates two dimmed neighbours; do not remove the line, three-pane layouts need it.\n- Touch mode (`termwindow/touch.rs`) is per-window UI state, not a persisted config override. It changes nothing but a drawn close button on each retro tab: tab bar height, desktop tab metrics and terminal font/line height stay the same. Long press works in every mode: `track_long_press` follows a plain left press and, on a release at least 450ms later within 24pt of the press (8pt cancelled ordinary presses over UU) (motion and the release point are both checked), replays it as a right click after the event finishes (on release, so AppKit menu tracking never sees the lift as a pick). The menu it opens draws its items at 18pt (`LONG_PRESS_MENU_FONT_PT`); NSMenu row height barely follows the font, so this is as large as a native menu gets without custom item views, while a mouse right click keeps the system size. A modal clears the pending press, so dismissing the rename editor never opens a menu, and a press that reached a tab through the bottom bar touch band aims its right click at that tab. A press over a pane that grabs the mouse is never converted unless it is on a tab, and the original press and release are handled as usual.\n- The bottom tab bar touch band (`BOTTOM_TAB_BAR_TOUCH_EXTENSION_PT` in `mouseevent.rs`) accepts only a plain left press (`press_takes_touch_extension`) and then owns that gesture's motion and release (`touch_band_press`). A middle press there is a paste into the last row and must never reach the tab bar's middle-click close. Known trade-off: a left click in the lower 12pt of the last row over a tab goes to the tab even when a TUI (tmux status line, htop) grabs the mouse.\n- Agent title (2026-10-18): while Claude Code, Codex or Kimi runs in the active pane (`foreground_agent_cli` in `src/tabbar.rs`), `src/termwindow/render/agent_title.rs` draws the agent's mark and the topic from its OSC 0/2 title at the top right: centered on the traffic-light line in the integrated-buttons strip, or 16pt below the top edge when fullscreen (the same distance as in a window, lifted only to stay above the first row), never under a top tab bar. `show_agent_title = false` (Agent Title in `kaku config`) turns it off; the default is on. Spinner glyphs and a leading agent name are stripped so the text stays still. Strip a name only at a complete boundary (end, whitespace, colon, pipe or middle dot), preserving topics such as `Codexification` and `Claude Codebase`; do not retry a shorter name after a longer partial match. The tab title is untouched on purpose: a tab is too narrow and would change width with every update. The marks in `src/termwindow/render/agent_marks.rs` are generated from Lobe Icons SVGs by `scripts/svg_to_poly.py`; regenerate rather than hand-edit. The remote page shows the same title: `tabbar.rs` publishes each tab's agent and topic through `kaku_remote::publish_agent_title` (honoring `show_agent_title`), and `AGENT_MARKS` in `crates/kaku-remote/assets/web/index.html` holds the same paths as SVG data on the 120 grid, so a regenerated mark goes into both files.\n- Tests that call `config::set_config_file_override` and `config::reload()` can race the config file watcher: `reload()` returns early when another reload starts, leaving the previous config. Write the config as few times as possible inside one test (loop the cheap cases inside one write), and do not split such a test into siblings that run in parallel.\n\n## Cross-References\n\n- [`mux/AGENTS.md`](../mux/AGENTS.md) - Tab and pane abstractions consumed by GUI.\n- [`termwiz/AGENTS.md`](../termwiz/AGENTS.md) - TUI primitives used in overlays.\n- [`config/AGENTS.md`](../config/AGENTS.md) - Config loading and reload signals.\n","AGENTS.md":"# Kaku Agent Guide\n\nKaku is a macOS-native terminal emulator derived from WezTerm and shaped around AI-assisted terminal workflows. This guide is the shared operating context for agents working in this repository.\n\n## Repository Map\n\n- `kaku/` - CLI entry points, command flows, and user-facing configuration commands.\n- `kaku-gui/` - GUI, rendering, window lifecycle, input, mouse handling, AI chat, and the `k` helper binary.\n- `mux/` - tabs, panes, domains, and client/server state.\n- `term/` - terminal emulation and screen buffer behavior.\n- `termwiz/` - terminal UI primitives.\n- `config/` - Lua config loading, schema behavior, proxy settings, and versioned defaults.\n- `window/` - platform windowing layer.\n- `lua-api-crates/` - Rust-to-Lua API bindings.\n- `crates/` - shared utility crates, including Kaku-specific AI helpers.\n- `assets/` - app resources, bundled config, shell integration, and vendor assets.\n- `scripts/` - build, release, and validation helpers.\n- `docs/` - user and developer documentation.\n- `.agents/skills/` - the canonical and tracked home for project skills: `release` and `bugs`. The website skill `product-docs` lives with the site on the `vercel` worktree (`~/www/kaku-site/.agents/skills/product-docs`). `.claude/` is gitignored except for the force-added `.claude/rules/macos.md`; its skill entries may be relative symlinks or ignored private overrides. Do not cite, copy, or edit an ignored `.claude/skills/` directory as project truth. Put durable skill changes in `.agents/skills/` first, then update a symlink when that entry is actually a link.\n- `.github/workflows/checks.yml` - fast correctness gates on pushes to `main` and on pull requests, with `paths-ignore` for `**.md` and assets, so a docs-only change gets no CI at all. Five macOS jobs, the runner's concurrency cap: Format, Unit Tests, Relay Build, Clippy (which also carries the source guards and stands in for `cargo check`, scoped to the lint-opt-in crates, not the workspace), and Scripts and Release Checks (release notes, config version, shell smoke tests, shellcheck). Adding a sixth macOS job makes one queue.\n- `.github/workflows/build-validation.yml` - release-shaped builds with the shipped `release-opt` profile: one job per architecture in parallel (the arm64 one also assembles the bundle), then a `lipo` merge of `kaku`, `kaku-gui` and `k`; runs on build-pipeline changes, daily (skipped when that commit already passed), or on dispatch. `release.sh` preflight requires its latest run green and the latest Security Audit run not failed, and skips its local fmt/check/test when CI Checks already passed on HEAD.\n- `.github/RELEASE_NOTES.md` - source for the GitHub Release title and body.\n\n## Commands\n\n```bash\nmake fmt\nmake fmt-check\nmake check\nmake test\nmake dev\nmake app\n./scripts/build.sh\n./scripts/check_config_release_readiness.sh\n./scripts/check_release_config.sh\n./scripts/check_release_notes.sh\n```\n\n`make build`, `make check`, `make test` and `scripts/build.sh` first run `scripts/prune_target.sh`, which measures `target/` at most once a day and runs `cargo clean` once it reaches `KAKU_TARGET_CAP_GB` (default 40). cargo never drops old artifacts, and the profiles, architectures and remote on/off feature sets here grew it to 92G; a partial, age-based delete was rejected because it can leave fingerprints pointing at missing files. No scheduled job does this, by the maintainer's choice. The dev profile also builds third-party crates without debug info.\n\n`make fmt` and `make fmt-check` both shell out to `cargo +nightly fmt`, so they require the nightly toolchain. Use `make app` for GUI, rendering, windowing, and AI overlay verification because it builds the app bundle that users run.\n\n## Working Rules\n\n- Work on the current branch unless the maintainer asks for a branch or worktree.\n- Keep changes inside one crate or subsystem when the problem allows it.\n- Upstream: Kaku forked WezTerm at `05343b38` (2026-01-17; term/, mux/, the escape parser and wezterm-font matched it byte for byte). On 2026-10-14 upstream main up to `e2a98e1b` was reviewed and its macOS-relevant fixes absorbed (commits citing upstream shas, e.g. DECRQCRA, RIS reset, OSC 8 `;`, tmux -CC input, workspace repaint). Start the next sync from `e2a98e1b`; map `wezterm-gui` to `kaku-gui` and top-level crates to `crates/`, and judge applicability rather than applying patches literally.\n- Kaku is macOS-only. Windows code (cfg(windows) branches, ConPTY/GDI/WSL paths, winapi and the other Windows-only dependencies) was removed on 2026-10-13; do not bring it back when porting upstream WezTerm fixes, port only the macOS-relevant half. `wsl_domains`, `win32_system_backdrop` and `win32_acrylic_accent_color` stay as deprecated config keys so shared configs still load (`windows_only_keys_still_load`). Vendored crates under `deps/` keep their upstream Windows code untouched.\n- Inspect public APIs and cross-crate boundaries before changing shared behavior.\n- Kaku must not automatically inject agent hooks, instructions, or settings. Explicit edits through `kaku ai` may update the selected provider's configuration (including Claude and Codex settings), preserving user-owned fields. Agent-facing help belongs in documentation. The `kaku init` source line in `.zshrc` is managed by `assets/shell-integration/setup_zsh.sh` and checked by `doctor.rs`; `reset.rs` must keep every user-owned line around it.\n- Narrowing a default that existing users already depend on must ship the other camp's explicit path in the same change. Shell integration scopes the Starship prompt to `TERM_PROGRAM=Kaku` OR (`$TMUX` and `KAKU_SESSION`), with `KAKU_PROMPT_EVERYWHERE=1` as the opt-in for every terminal; Smart Tab stays Kaku-only (#503). The predicate is duplicated inside each installer, three times in `setup_zsh.sh` and four in `setup_fish.sh`, so grep both files and change every copy. `assets/shell-integration/tests/prompt_scope_smoke.sh` gates the placements in CI via `.github/workflows/checks.yml`, and `docs/faq.md` is the user-facing half; a change that re-narrows the scope must update both installers, the smoke and the doc together.\n- Tab indicator decision (2026-08-24): the tab bar draws a dot only for `Progress::Paused | Progress::Error` (attention, `kaku-gui/src/tabbar.rs`); a running pane deliberately shows nothing, the running-state dot was dropped in `4f297556`. Do not bring it back.\n- Retro tab bar chrome (2026-09-09, #548): the bundled config (`assets/macos/Kaku.app/Contents/Resources/kaku.lua`) sets `use_fancy_tab_bar = false`, so the shipped bar is the retro one even though the `config/src/config.rs` schema default is true. `kaku-gui/src/tabbar.rs` composes a `Line` of cells, so a control there is a character on the text baseline competing with the title for the one trailing cell the rule above spends on attention; drawn controls belong in the box-model renderers (`make_x_button` in `kaku-gui/src/termwindow/render/fancy_tab_bar.rs`, and the opt-in touch renderer in `kaku-gui/src/termwindow/touch.rs`). The per-tab close mark added in `cef3a371` (three cells, \" X \", over the title end) was reverted in `732d2082` for that reason. On 2026-10-12 it was tried again as a single hover `×` in the status cell, with one and then two cells of padding per side; the maintainer judged it worse than none (a grid glyph cannot sit centered between title and edge) and removed it: the desktop layout closes tabs from the tab's right-click menu, middle-click and Cmd+W, while the web terminal keeps its hover close icon, which is drawn and centered. Answer tab bar chrome requests with the pointers that already exist: the new-tab button is on by default (bundled `show_new_tab_button_in_tab_bar = true`), right-click on a tab opens a tab menu with New Tab, Tab Navigator and Close Tab (`tab_context_menu_actions` in `kaku-gui/src/termwindow/mouseevent.rs`), middle-click closes a tab, and the Shell menu lists New Tab and Close Tab. For touch over remote desktop, a plain left press (a tap) up to `BOTTOM_TAB_BAR_TOUCH_EXTENSION_PT` above a bottom bar still hits tabs and the new-tab button, while middle and right presses there stay with the terminal, so the bar itself stays one row. Do not re-add a native close mark: if one is ever wanted, it has to be a drawn, centered icon like the fancy bar's `make_x_button`, not a grid character. The desktop bottom retro bar is one terminal row with line_height (`tab_bar_pixel_height_impl`), the same as a top bar; the compact natural height read as cramped. Touch mode (2026-10-14) is a per-window toggle with one entry, Window > Touch Mode; the on-bar Touch button and the titlebar entry were removed as clutter. It keeps the normal bar height and look and only adds a drawn, centered close icon to every retro tab, over cells the bar keeps at the tab's end (`close_slots` in `kaku-gui/src/tabbar.rs`). The separate 40pt touch bar, its Menu button and panels were removed; do not bring them back. A long press (left press held 450ms, under 24pt of movement, acted on at release) replays as a right click in every mode, so tab and terminal context menus cover touch; UU was measured to forward a long press as a held left button, never a right click.\n- Line height decision (2026-07): the bundled default stays `line_height = 1.28`. Character-cell graphics (QR codes, `neofetch` logos, TUI bar charts) scale with the row height, so they render about 28% taller than at `line_height = 1.0`, and that trade-off is accepted. Rendering block glyphs at their natural cell height was tried in `d810b13` and reverted in `3f131ea`: block characters must fill the whole cell or box borders and QR codes open horizontal seams. Do not propose lowering the default or re-attempting natural-height block rendering; point users at the `line_height` entries in `docs/faq.md` and `docs/configuration.md`. The block cursor is a separate case and correctly keeps using `natural_cell_height` (see `kaku-gui/AGENTS.md`).\n- Draft issue and PR replies unless the maintainer has already approved the exact public action.\n- Do not modify files outside this repository without showing the intended change and getting explicit confirmation.\n- Do not add instructions for the removed `website/` tree unless that directory exists in the current worktree.\n- The marketing and docs site lives on the `vercel` branch (linked worktree at `~/www/kaku-site`), not on `main`. It follows the design guide at `~/www/kaku-site/DESIGN.md`; verify changes with screenshots at 375px / 1280px and deploy by pushing the `vercel` branch (Vercel serves kaku.fun).\n- Keep private credentials, local keychain paths, and machine-specific release notes out of public repository docs.\n- **Do not propose UI i18n / multi-language menus / a `config.language` setting.** The `rust-i18n` based Chinese UI localization (PR #362, commit `f6cfb4b`) was reverted in `b4d779a` on 2026-05-18; `language` remains in the config schema as a deprecated field for backward compat only. UI strings (menus, confirm dialogs, config TUI copy) stay as English literal strings. New UI surfaces should not introduce translation keys, locale-aware formatting, or \"what if a user wants Chinese\" abstractions. If a user requests a non-English UI, route to the assistant config / AI chat surface; those already accept non-English content.\n- **Kaku stays free, including Kaku Assistant and remote access (maintainer 2026-10-18).** Kaku Assistant runs on the user's own API key or Codex login, so it has no cost to meter. The relay (`crates/kaku-relay`, one Fly machine) is paid for by the maintainer as a thank-you to the community, and `docs/faq.md` says so. Do not propose paid tiers, usage-based billing, accounts or usage metering for the relay; when it nears the concurrency limit in `fly.toml`, load test and move to a larger machine instead.\n- **Do not pre-bake provider abstractions in `kaku/src/ai_config/`.** Kaku Assistant parsing, field presentation, and persistence live in `kaku/src/ai_config/tui/providers/assistant.rs`; the TUI retains its event loop and shared data types. Future extraction should move one concrete provider at a time into that existing directory. Do not add an unused trait, a `ProviderKind` enum, or stub modules. Save Copilot for last because its OAuth flow is the abstraction stress test.\n- CLI Codex readiness and GUI connection parsing both consume `tests/fixtures/codex-connection.json`. Add boundary cases there when either policy changes, and run both crates' tests.\n- Official Homebrew token is `kaku` (`brew install --cask kaku`). `tw93/tap/kakuku` remains the personal tap for people who installed before the official cask. Do not tell users the Homebrew `kaku` token is a different app.\n\n## Maintainer Follow-up\n\n- For current issue and PR sweeps, read live GitHub state first with `gh issue list` and `gh pr list`; refresh once more before final conclusions or public actions.\n- Before commenting on or closing an item, confirm its title, state, and author with `gh issue view` or `gh pr view`.\n- The latest stable tag is `git tag --list 'V*' --sort=-version:refname | head -1`; without the `V*` filter the rolling `nightly` tag sorts first. Map each report to `git log <tag>..HEAD`, and treat a closed issue as a claim until the fix commit or remaining gap is named.\n- Shell integration fixes run the affected `assets/shell-integration/tests/*_smoke.sh`, then the list `.github/workflows/checks.yml` runs under `Shell integration smoke tests`. Watch the pushed `main` run with `gh run watch <id> --exit-status`.\n- Do not close issues or PRs on local green alone. For fixes pushed to `main`, wait for the new GitHub Actions run on `main` to pass before posting fixed/closed replies.\n- The rolling `nightly` release is not rebuilt by push. Before sending users to Nightly, run or verify `./scripts/nightly.sh` and confirm `gh release view nightly --json tagName,targetCommitish,publishedAt,assets,url` points at the fix commit and includes `Kaku-nightly.dmg`.\n- Default issue-closure pipeline once a fix is verified: commit the fix, refresh the `nightly` release assets per the rule above, reply in the reporter's language with the Nightly download or in-app update path, then close if the current request already authorizes closure; otherwise propose it and wait for authorization. Do not promise a specific packaged-release date in replies.\n- Before pushing `main`, run `git fetch origin main` and verify `origin/main` has not moved unexpectedly. If it moved, stop and review `origin/main..HEAD` before pushing.\n- For several unrelated bugs, prefer one commit per issue or behavior, and put the issue number in the subject: `fix(scope): #123 short summary`.\n- Merge contributor PRs with `gh pr merge --merge`, never squash, so the contributor's own commits and authorship stay in history; add no trailer. When `maintainerCanModify` is true, push fix commits to the contributor's branch rather than rewriting their existing commits, and approve a first-time contributor's workflow run with `gh api -X POST repos/tw93/Kaku/actions/runs/<id>/approve` before expecting CI.\n- If an accepted PR's equivalent fix lands outside the contributor branch, acknowledge the contribution and explain which user-visible fix was delivered before closing it; keep commit hashes and internal attribution bookkeeping out of the public reply.\n\n## Investigation Order\n\nWhen scope is incomplete, inspect in this order:\n\n1. User-provided repro, failing command, or failing test.\n2. Entry point for the behavior, usually `kaku/src/main.rs`, `kaku/src/cli/`, or `kaku-gui/src/main.rs`.\n3. Owning subsystem document and target crate.\n4. Immediate cross-crate boundary used by the call path.\n5. Narrow tests, fixtures, snapshots, or scripts that reproduce the behavior.\n\nFor AI-facing behavior, inspect in this order:\n\n1. CLI and assistant configuration under `kaku/src/ai_config/`, `kaku/src/assistant_config.rs`, and `config/src/proxy.rs`.\n2. GUI AI state and transport under `kaku-gui/src/ai_*`, `kaku-gui/src/ai_chat_engine/`, and `kaku-gui/src/cli_chat/`.\n3. Overlay UI under `kaku-gui/src/overlay/ai_chat/`.\n4. Shared helpers in `crates/kaku-ai-utils/`.\n\nFor AI transport bugs around custom `base_url`, keep both proxy paths true: external API hosts should use detected system proxy settings, while loopback, private LAN, link-local, CGNAT/Tailscale-style, `.local`, `NO_PROXY`, and macOS ExceptionsList model endpoints should connect directly. Verify with the macOS system proxy enabled and a loopback or internal OpenAI-compatible smoke before saying it is fixed. Do not claim general SOCKS support unless that transport is actually implemented and verified.\n\nFor `Ctrl+letter` not working in a raw-mode TUI (the most common shape: `Ctrl+C` / `Ctrl+R` works in plain shell but not inside a TUI overlay), inspect in this order:\n\n1. AppKit menu `keyEquivalent` intercepting `keyDown` before the terminal sees it. Enable `config.debug_key_events = true`, restart the app, then `grep 'key_event.*CTRL' ~/.local/share/kaku/kaku-gui-log-<pid>.txt`. If the log shows only `key_is_down: false` and no matching `key_is_down: true`, the AppKit menu absorbed the event; do not chase termwiz or PTY.\n2. Cooked-mode tests (`cat -v` showing `^C`) do **not** rule out menu interception. Reproduce inside a raw-mode TUI before forming a hypothesis.\n3. Only after step 1 rules out menu interception, inspect termwiz encoding (`termwiz/src/input.rs`), then PTY / termios state.\n\nFor TUI display corruption after interactive CLIs re-render prompts or selection lists, first capture a minimal ANSI transcript. Add a terminal-core regression around cursor-up (`CSI n A`), full-line erase (`CSI 2K`), cursor-down (`CSI 1B`), wrapped rows, and styled prompt symbols. If the core transcript passes but the built app differs from Terminal.app, inspect GUI width, cell metrics, resize, and wrapping inputs rather than changing terminal semantics blindly.\n\n## Subsystem Guides\n\n| Subsystem | Guide | Scope |\n|---|---|---|\n| GUI | `kaku-gui/AGENTS.md` | Rendering, window lifecycle, input, mouse |\n| Mux | `mux/AGENTS.md` | Tabs, panes, domains, client/server |\n| Terminal | `term/AGENTS.md` | VT emulation, screen buffer |\n| Config | `config/AGENTS.md` | Lua loading, schema, config reload |\n| Termwiz | `termwiz/AGENTS.md` | TUI primitives and widgets |\n| Lua API | `lua-api-crates/AGENTS.md` | Rust-to-Lua bindings |\n| Crates | `crates/AGENTS.md` | Shared utility crates |\n| macOS platform | `.claude/rules/macos.md` | AppKit menu / keyEquivalent traps, menubar init timing |\n\n## Verification\n\n| Change type | Command |\n|---|---|\n| Rust compile check | `make check` |\n| Rust logic change | `make test` |\n| Formatting | `make fmt-check` |\n| Lints | `cargo clippy --locked --all-targets -p kaku -p kaku-gui -p mux -p config -- -D warnings` (`make check` does not run clippy, so a change that passes locally can still turn CI's Clippy job red) |\n| GUI or rendering change | `make app` |\n| CLI main menu input | `make app`, then `python3 scripts/test_main_menu.py` (real PTY, q/Q/Esc exit) |\n| Config release change | `./scripts/check_config_release_readiness.sh` and `./scripts/check_release_config.sh` |\n| Release note change | `./scripts/check_release_notes.sh` |\n| Release-adjacent change | `make fmt && make check && make test`, then `make app` |\n| `crates/kaku-relay` change | `cargo check --locked --manifest-path crates/kaku-relay/Cargo.toml` (it is in `workspace.exclude`, so `make check` and `make test` never see it; CI's `relay-check` job is the only gate) |\n\nFor GUI or rendering issues, read `kaku-gui/AGENTS.md` first and verify with `make app`, not only `make dev`.\n\nRestarting Kaku (2026-10-14, narrowed 2026-10-21): run `scripts/restart_kaku.sh` only when the maintainer asks for a restart in the current message (重启 Kaku). Swapping a new build into `/Applications`, or an earlier request in the session, is not a request to restart: finish the swap, then ask and wait, since the restart ends the maintainer's own windows and any agent session inside Kaku. It restarts into `/Applications/Kaku.app` the way an in-app update does (TERM, then reopen; tabs return from the periodic session snapshot) and detaches first. It stops every running Kaku GUI, a copy opened from `dist/` included, but sends TERM only to the one that owns `last_session.json` (its pid names the snapshot's `content_dir`) and SIGKILLs the rest after it exits, so a stray copy cannot overwrite the tabs when it quits. An agent session running inside Kaku ends with it, so write the reply first, tell the maintainer to reopen the session with `claude -c` (or the equivalent), and make the script the last action. Its log is `~/Library/Logs/kaku-restart.log`. The detached run is marked with a `--detached` argument, never an environment variable: `/usr/bin/open` hands the caller's environment to the app, so a variable reached every shell in the reopened Kaku and the next restart skipped detaching and died with it (`test_leaked_detach_variable_still_detaches`). An absent snapshot or empty process list must still reach the reopen step; `scripts/test_restart_kaku.py` exercises those branches and owner-only saving with fake processes, never the running app.\n\nStanding authorization (2026-10-19): after a verified change to `crates/kaku-remote/assets/web` is pushed, publish the relay page with `scripts/publish_web_page.sh` without asking. Confirm the deploy reached `t.kaku.fun` by fetching `https://t.kaku.fun/index.html` and finding a string the change added, and check the response's CSP when the change touches the script or the publish script.\n\nWhen a check needs clicks on the built app, hand the maintainer an isolated launch command (`open -na dist/Kaku.app --env HOME=<scratch home with a copy of their kaku.lua>`) and numbered taps with the expected result for each, instead of driving synthetic mouse events: the maintainer's own Kaku takes focus back and the test window may sit on another Space. Static screenshots with `screencapture -l` are still fine when the window is on screen. A shell running inside Kaku carries `KAKU_CONFIG_FILE`, `KAKU_CONFIG_DIR`, `KAKU_SESSION`, `KAKU_UNIX_SOCKET`, `KAKU_ZSH_DIR` and `XDG_CONFIG_HOME`, and `open` hands them to the app, so a test instance launched without `env -u` for each reads the maintainer's real config and session, and may connect to the relay; shell smoke tests run from such a shell need the same unsets. Remote state lives in `$TMPDIR/kaku-remote.json` and `kaku-remote-relay.json`, which a scratch HOME does not isolate: a test instance with remote on overwrites the maintainer's copies, so say that their Kaku needs a restart afterwards for `kaku remote` and `kaku doctor` to read its own state again. Look up the test window by the test instance's pid and capture nothing when that yields no id, since an empty `-l` once captured the maintainer's own window. Native fullscreen does not capture reliably this way; ask the maintainer for that screenshot instead. Four traps from the 2026-10-19 site captures: do not pass `--args start` to `open`, since AppKit hands bare arguments to `application_open_files`, the first pane runs `start ; exit` and the window closes before it is seen; drop `config.remote` from a copied `kaku.lua`, or after a config reload the test instance can attach to the maintainer's mux and `send-text` lands in their pane; `yazi` in a test pane is the shell wrapper, which, like Cmd+Shift+Y, rewrites the maintainer's `~/.config/yazi/theme.toml` from the test config's `color_scheme`, so run the Yazi binary itself with `YAZI_CONFIG_HOME` on a scratch copy; and size or move the test window with one System Events expression per action (`set size of window 1 of (first process whose unix id is <pid>)`), since a process reference kept in a variable resolves by name and reached the maintainer's `kaku-gui` window. Check the result per pid with `CGWindowListCopyWindowInfo`. For a remote capture, launch the test instance with a scratch `HOME` and `TMPDIR` as well: pairing state, the identity and `remote.lock` live under `$HOME/Library/Application Support/kaku` and the relay files under `$TMPDIR`, so only both keep the maintainer's remote untouched; end with `kaku remote off` under the same environment. In Safari, address a window you opened by its AppleScript `id`, never `front window`, which on 2026-10-19 resolved to the maintainer's Start Page window and resized it; editing the address bar opens history and Favorites over the page, so capture with the field unfocused (Tab moves focus into the page).\n\n## Current Risk Areas\n\n- In-app update replace/restart must confirm on every entry path (menu Check for Updates, menu Restart to Update, toast, CLI direct ZIP, brew cask). Do not set `KAKU_UPDATE_AUTO_CONFIRM` for exploratory menu check; only the overlay-confirmed path may auto-confirm. Sibling entry points tend to regress independently; after changing one path, sweep the matrix and keep the guards in `kaku-gui` (`user_facing_update_events_route_through_confirm`) and `kaku` (`brew_and_direct_paths_confirm_before_replace`) green. Details: `kaku-gui/AGENTS.md` and `.agents/skills/bugs/SKILL.md`.\n- AI chat and shell flows are active product surfaces. Preserve `fast_model`, proxy config, inline `#` query status, syntax highlighting, approval flow, and conversation state behavior.\n- `config_version` bumps every release; the source of truth is `assets/shell-integration/config_version.txt` and the gate is `scripts/check_release_config.sh`. Config schema changes must update bundled defaults, docs, release checks, and migration behavior together. Per-version history and the migration rule (only keys that existed in the previous released version need migration code) live in `docs/config-versions.md`. Do not hardcode the current version number in agent guides; it goes stale between releases. A config_version bump is also the only thing that regenerates an existing install's `~/.config/kaku/zsh/kaku.zsh`, so a shell-integration fix reaches stable users through the bump; a Nightly built without one needs `kaku init --update-only`, and replies pointing at that Nightly must say so.\n- GUI regressions can come from overlay resize, pane split/removal, macOS worker thread lifetime, WebGPU surface reconfigure, tab bar spacing, and alternate-screen wheel scroll behavior.\n- Startup performance depends on caching shell user vars, Lua bytecode, early appearance queries, GLSL version, and built-in fonts. Do not invalidate those caches without measurement.\n- `kaku doctor` ends every run by writing `Kaku-Diagnose.zip` (`kaku/src/diagnostics.rs`). It also collects the `kaku-hang-*.txt` stack samples that `kaku-gui/src/stall_watchdog.rs` writes when the GUI thread stays blocked past 1s, so rename that prefix in both files together. Everything in the bundle goes through its redactor, which also hides typed text in `debug_key_events` lines (printable keys, physical key codes, composed and IME text) including committed `KeyCode::Composed` text as well as `DeadKeyStatus::Composing`, while keeping Ctrl and Cmd keys readable for key reports, `assistant.toml` is never read, and large files (session content can be gigabytes, #559) are listed by size, not copied.\n- Notification actions that call back into Kaku should resolve bundled executables relative to the running app, not an assumed system path.\n- Known high-regression zones: theme/config TUI initialization (`kaku/src/ai_config/tui/`), macOS window geometry (`window/src/os/macos/window.rs`, `kaku-gui/src/termwindow/resize.rs`), and menubar initialization timing (`window/src/os/macos/menu.rs`). Changes touching these must ship with a regression test or assertion.\n- `assets/shell-integration` scripts run in the user's shell, not just at build time. Bash heredocs that generate zsh (e.g. `setup_zsh.sh` writing `kaku.zsh`) expand backticks and `$(...)` at generation time, so escape any that must reach the output literally (#450), and never put `local` outside a function (#432/#441). An expired `.zcompdump` needs `compinit && touch` after a successful full check: `compinit` can leave an unchanged dump's timestamp untouched. Keep the failure path stale and run `assets/shell-integration/tests/compinit_cache_smoke.sh`, which checks the next shell returns to `-C`. CI gates this: shellcheck (`--severity=error`, catches SC2168) over the bash scripts plus a `zsh -n` parse check of the generated `kaku.zsh` in the setup smoke.\n- User-owned configuration symlinks must survive updates, and failed writes must leave their targets unchanged. Resolve the final target and finish a sibling temporary file before replacing it; preserve target permissions. The shell `write_through` helper has four copies across the zsh/fish installers and their generated Yazi wrappers. Test all four under a real write failure, not only successful linked-file updates (#581, #582).\n\n## Release Notes\n\nTag format is `V0.x.x`. `scripts/release.sh` is the source of truth for tagged releases. The GitHub Release title comes from the first heading in `.github/RELEASE_NOTES.md`.\n\nBefore drafting release notes, read the previous formal release (`gh release view <latest-tag>`) and treat it as the format template: title `V{version} {Codename}` with a one-word codename, a centered logo header with the product tagline, `### Changelog` as numbered `**Label**: one sentence` items, a `### 更新日志` section whose Chinese items map one-to-one by number, and a `> https://github.com/tw93/Kaku` footer. After publishing, add all six positive reactions (`+1`, `laugh`, `heart`, `hooray`, `rocket`, `eyes`) to the release via `gh api` and read them back to confirm; never add `-1` or `confused`.\n\n## Pre-release Runtime Smoke\n\nCI gates fmt/check/clippy/tests but cannot see visual layout, native AppKit, render timing, or shell-in-user-env behavior, which is where most post-release reports come from. The checklist lives in one place: `.agents/skills/release/SKILL.md` «Pre-release smoke checklist», covering macOS window, tab bar, shell setup, AI chat, and render timing, with the issue numbers each item guards and a note on which slices now have automated coverage. Run it by hand in the built `dist/Kaku.app` before tagging, and add the reproduction there when a release fixes a bug outside the list.\n\nReleases that touch windowing, titlebar coloring, tab bar layout, or transparency also need the config matrix in `docs/release-checklist.md`, which enumerates the tab position / tab style / opacity / window state combinations to check by hand and names the `update_titlebar_background()` regression guard.\n\n## Documentation Maintenance\n\n- Single-crate behavior belongs in that crate's `AGENTS.md`.\n- Cross-crate behavior should update every affected subsystem guide.\n- Build, CI, release, and maintainer workflow changes belong in this root file.\n- Shared agent instructions belong in tracked docs. Personal overrides belong in ignored local files.\n- One-off review reports, scorecards, and diagnostic snapshots are evidence, not durable project docs. Extract stable rules or verification gates into `AGENTS.md`, `CLAUDE.md`, subsystem guides, scripts, or tests, then remove the transient report.\n- Do not hide user-visible behavior changes inside maintainability or cleanup patches. New UI, config fields, defaults, or workflow permissions should be split into their own change unless the maintainer explicitly approved that scope.\n","config/AGENTS.md":"# Config Agent Guide\n\nThe `config` crate owns config loading, Lua integration, schema behavior, proxy settings, versioned defaults, and AI configuration.\n\n## Scope\n\n`config` controls:\n- loading user and bundled configs\n- Lua execution and binding lifecycle\n- schema mapping between Lua and Rust\n- config subscriptions and reload behavior\n- AI model, fast-model, and proxy settings consumed by GUI and CLI flows\n- versioned default config and release readiness checks\n\n## Where to Look\n\n- `config/src/config.rs`: load/parse flow\n- `config/src/lib.rs`: config API and subscriptions\n- `config/src/proxy.rs`: proxy configuration used by AI and network-facing flows\n- `config/src/version.rs`: app version and target-triple accessors (`wezterm_version()`), not `config_version`; that number lives only in `assets/shell-integration/config_version.txt`, read by `kaku/src/init.rs` and the bundled `kaku.lua`\n- `assets/macos/Kaku.app/Contents/Resources/kaku.lua`: bundled fallback config\n\n## Config TUI (`kaku/src/config_tui/` and `kaku/src/tui_core/`)\n\nThe config TUI is the interactive terminal UI for editing Kaku config. It lives in the `kaku` CLI crate, not the `config` crate.\n\n- `kaku/src/config_tui/` - TUI app: `mod.rs` (form state and main loop), `ui.rs` (rendering)\n- `kaku/src/tui_core/` - shared TUI primitives, currently `theme.rs` only (`accent`, `bg`, `muted`, `panel`, `primary`, `red`, `success`, `text_fg`)\n- `tui_core::theme` is consumed by `config_tui/ui.rs`, `ai_config/tui/ui.rs`, and `tui_splash.rs`. New primitives shared by more than one TUI flow belong in `tui_core`, not copied per feature.\n\n## AI Config TUI (`kaku/src/ai_config/`)\n\nThe AI config TUI lives in the `kaku` CLI crate and shares terminal UI primitives with `config_tui`.\n\n- Keep `fast_model`, primary model, and proxy settings aligned between CLI config, GUI AI chat, and documentation.\n- Do not duplicate form widgets or debounce behavior outside `kaku/src/tui_core/`.\n- Verify AI config changes against both config parsing and the visible TUI flow.\n\n## Practical Rules\n\n- Loading priority: user config first, bundled config second.\n- Keep reload-safe behavior for startup hooks and subscriptions.\n- `color::is_light_color` re-exports the terminal core's classification so `COLORFGBG`, window chrome and DEC mode 2031 reports use the same palette policy. Keep that shared policy when changing color classification.\n- After mutating a live `TermConfig` client palette, call `pane.set_config(config)` so the terminal compares its subscribed palette snapshot and emits mode 2031 notifications. Updating only the provider bypasses the terminal's reload boundary.\n- `KAKU_CONFIG_FILE` is an output, not an input. Config loading exports it (`config/src/config.rs`) and `effective_config_file_path()` (`config/src/lib.rs`), the GUI single-instance check (`kaku-gui/src/main.rs`), `assets/shell-integration/setup_{zsh,fish}.sh`, and the bundled `kaku.lua` all read it back. Do not turn it into a config path override; the only supported override is `--config-file` (`CONFIG_FILE_OVERRIDE`).\n- Keep bundled fallback config authoritative at `assets/macos/Kaku.app/Contents/Resources/kaku.lua`.\n- Kaku Dark, Kaku Light, and the Kaku Theme alias must also resolve for standalone user configs that do not load the bundled Lua defaults. Their `src/scheme_data.rs` mirrors are checked against every bundled palette field by `builtin_kaku_scheme_mirrors_match_complete_bundled_palettes`; synchronize both when changing a Kaku palette.\n- Preserve compatibility with runtime reload callers that trigger `config::reload()` from GUI-side signals.\n- `config::reload()` can return without applying: once a config file is loaded, a watcher reloads 100ms after that file changes, and whichever reload started last wins. A test that rewrites its config file and reloads must set `automatically_reload_config = false` in that file, or a slow runner reads the old values (`ssh_pane_titles_keep_the_formatter_on_hover`). CI runs each test in its own process with nextest, so a flaky global-config test is racing a thread, not another test.\n- The current `config_version` is whatever `assets/shell-integration/config_version.txt` says; never trust a number written in a doc. Any version bump must update bundled config, release checks, docs, and migration expectations together, and add a row to `docs/config-versions.md`.\n- New config fields are user-facing behavior. Keep them out of pure cleanup/refactor patches unless the maintainer explicitly approved the product change, and update bundled defaults plus documentation in the same change when they do land.\n- Keep alternate-screen wheel scroll behavior configurable; terminal and GUI defaults must not diverge.\n- Config rewrite guards must validate every assignment the writer can replace, using the same match predicate. A later Lua override can contain user-owned settings even when the first assignment contains only a managed flag; `remote_access_checks_every_assignment_before_rewriting` covers this boundary in `kaku/src/config_tui/mod.rs`. Remote toggles must edit `effective_config_file_path()` through `kaku::utils::write_atomic`, preserving symlinks, permissions and the original bytes on a partial write failure. `kaku/tests/remote_config.rs` exercises the actual CLI with `--config-file`, a stale inherited `KAKU_CONFIG_FILE`, and a real file-size write limit.\n\n## Bundled kaku.lua Pitfalls\n\n- **200-locals hard limit.** LuaJIT caps a chunk at 200 local variables, and the top-level\n  chunk of `assets/macos/Kaku.app/Contents/Resources/kaku.lua` is already at capacity.\n  Adding one more top-level `local` makes the whole config fail to load at startup. New\n  helper functions and values must be nested inside existing functions or tables. After any\n  edit, verify the file still loads:\n\n  ```bash\n  luajit -e \"assert(loadfile('assets/macos/Kaku.app/Contents/Resources/kaku.lua'))\"\n  ```\n\n  `scripts/check_kaku_lua_loads.sh <path>` wraps the same check and doubles as a PostToolUse hook for edits to this file.\n\n- **PaneInformation is fields-only.** In title-formatting paths (`format-tab-title`,\n  `format-window-title`), the `pane` object is a `PaneInformation` userdata that exposes\n  fields only, no methods. Method-style access such as `pane:get_foreground_process_name()`\n  or capability probes like `if pane.get_xxx then` do not error; they silently evaluate to\n  nil, so the guarded branch never runs (#485 shipped dead code this way). Use the\n  documented fields, prefer the `WEZTERM_PROG` user var for the running command, and treat\n  argv-derived titles as untrusted (argv can leak environment values).\n\n## Cross-References\n\n- [`kaku-gui/AGENTS.md`](../kaku-gui/AGENTS.md) - GUI config consumers and reload signals.\n- [`lua-api-crates/AGENTS.md`](../lua-api-crates/AGENTS.md) - Lua APIs that expose config values.\n- [`mux/AGENTS.md`](../mux/AGENTS.md) - Alert propagation for config changes.\n","crates/AGENTS.md":"# Crates Agent Guide\n\n`crates/` contains shared utility crates used across the workspace. Inspect the target crate's `Cargo.toml` before editing because Kaku keeps some WezTerm-derived crates and adds its own helpers over time.\n\n## Scope\n\nKey groups:\n\n| Group | Crates |\n|-------|--------|\n| Terminal core | `codec`, `color-types`, `vtparse`, `wezterm-cell`, `wezterm-escape-parser`, `wezterm-surface` |\n| GUI and input | `wezterm-font`, `wezterm-input-types`, `wezterm-gui-subcommands`, `wezterm-open-url`, `wezterm-toast-notification` |\n| Mux, client, and remote | `wezterm-client`, `wezterm-mux-server-impl`, `wezterm-ssh`, `wezterm-uds`, `kaku-relay`, `kaku-remote` |\n| Platform and process | `pty`, `filedescriptor`, `procinfo`, `env-bootstrap`, `umask`, `async_ossl` |\n| Lua and config support | `luahelper`, `wezterm-dynamic` |\n| Shared utilities | `promise`, `base91`, `bidi`, `bintree`, `frecency`, `lfucache`, `rangeset`, `ratelim`, `tabout`, `wezterm-blob-leases`, `wezterm-char-props`, `wezterm-version` |\n| Kaku-specific utilities | `kaku-ai-utils`, `kaku-unicode-names` (generated Unicode name table lifted out of `kaku-gui/src`; regenerate with `ucd-generate`, do not hand-edit) |\n\n## Practical Rules\n\n- For macOS notification actions that launch Kaku commands, prefer resolving bundled executables relative to the running `kaku-gui` binary.\n- Keep `kaku-ai-utils` provider-agnostic; provider policy belongs in GUI/CLI call sites and config, not in low-level helpers.\n- When shared cache helpers affect startup, verify the GUI consumer and avoid invalidating hot-path caches unnecessarily.\n- `wezterm-escape-parser` ProgramStatus accepts whitespace around key/value pairs and skips lexically malformed optional pairs without losing valid state. Preserve decoded-text control-character and size checks; permissive pair parsing must not weaken those boundaries. Regressions live in `src/osc.rs` (`program_status_trims_pair_whitespace`, `program_status_skips_malformed_optional_pairs`, and `program_status`).\n\n## Web Terminal and Relay\n\n`kaku-remote` serves a browser terminal (`src/web.rs`, page in `assets/web/`) and `kaku-relay` lets it be reached from outside the LAN (`src/share.rs` on the Mac side). The `remote` cargo feature is on by default in `kaku` and `kaku-gui`, so Release and Nightly ship it; the feature itself stays off until the user turns it on. Invariants:\n\n- Launched in testing, free (2026-10-17): every build carries the web terminal, `remote.enabled` defaults to false, and `kaku remote` asks before turning it on, so nothing changes for a user who never runs it. The private-build machinery (`nightly.sh --private`, the `check_no_web_terminal.sh` publish guard, `release.sh` refusing the feature, `kaku remote` hidden from `--help`) was removed at launch. Remote access remains free, including the relay paid for by the maintainer; do not add paid tiers, accounts, usage metering or registration tokens.\n- Off until the user turns it on (2026-10-16): `remote.enabled` defaults to false. `kaku remote` asks before turning it on and writes `config.remote = { enabled = true }` to the user's kaku.lua through the settings screen's line writer (`config_tui::set_remote_enabled`); `kaku remote off` writes false. Neither rewrites a `config.remote` table that holds other keys (a relay of their own), which a whole-line rewrite would drop; they say to edit it instead. The bridge follows config reloads (`follow_config`): it starts when the setting turns on, without restarting Kaku, and when it turns off its runtime is dropped, which ends the relay link and every web session within a second, and the state file is removed so `kaku remote` no longer sees sharing. Verify off/on transitions in the same process; restarting the app does not test release of `remote.lock`.\n- One Kaku per data directory serves remote access (2026-10-19): `launch_when_sole` holds a `flock` on `DATA_DIR/remote.lock`; a second GUI on the same data directory (a build-folder copy, another `kaku start`) waits and takes over when the first quits. Before this, a second copy registered with the relay under the same identity and took the browser over to its own terminals. Drift checks name the frame the page drew (`seq`, `Shown::rows_after`), so they still run while frames are in flight over the relay; checking only with nothing in flight never ran while Claude Code worked.\n- No LAN access (2026-10-16 decision, replacing the 2026-10-15 opt-in): the local server listens on 127.0.0.1 only and `remote.bind` is deprecated and ignored, kept so configs that set it still load. The maintainer has no use for it at home and it was plain HTTP: `http://192.168.x.x` is not a secure context, so `crypto.subtle` is missing (measured in Helium), and the relay's https page cannot open a `ws://` LAN socket (mixed content). Do not bring LAN access back.\n- The local server answers only requests whose `Host` is its own (`127.0.0.1`, `localhost` or `[::1]` with the listener's own port, so a config reload that edits `remote.port` does not lock the page out), so a web page that rebinds its name to 127.0.0.1 cannot read same-origin responses. No route serves the token: `kaku remote` reads it from the 0600 state file. The routes for the old iOS client (`/api/config`, `/api/panes`, `/qr` with the token, `/ws/{pane_id}`, and the `ai_chat` handler in `kaku-gui/src/ai_remote.rs`) were removed on 2026-10-14 with the maintainer's approval: its relay mode had already stopped working, and `/qr` handed the token to any local process, other macOS users included. A future iOS app should speak the web terminal's protocol or wrap the page, not bring these back.\n- A session ends after `SESSION_SILENCE` (180 s) without a message from its browser, so a half-open socket from a sleeping laptop does not hold App Nap off forever.\n- One session per browser, carried either by the local WebSocket (`#t=<token>` link) or by the relay (a paired browser, see below). `run_session` sees transport-neutral input and output queues; keep socket and encryption logic out of it.\n- The page mirrors Kaku's own terminal model as ANSI frames rather than replaying the PTY stream, so terminal queries are answered once, by Kaku. Keys go through `Pane::key_down`, pastes through `send_paste`, and already encoded text through `send_text`. Local panes use the terminal's existing ordered background writer for all three; never send Remote text directly to the blocking PTY writer.\n- Kaku Assistant's chat is a GUI overlay that never reaches the mux, so the page would show and type into the shell under it. The GUI reports it with `set_ai_chat_open` (open, close, and a chat dropped on config reload); while it is open `handle_input` drops typing, pastes and clicks for that pane, an upload is saved without pasting its path, and the page dims the pane with a notice. Chat on the page itself was declined (2026-10-18): it would be a second chat UI, and using it from a browser reads as odd; do not build one.\n- Anything that reads the Lua config (for example `can_close_without_prompting`) must run on the GUI thread via `promise::spawn::spawn_into_main_thread`; calling it from the server's tokio threads panics.\n- Relay ids are the first 128 bits of SHA-256 of a secret that never leaves the Mac, so nobody else can register as that Mac. Through the relay only paired browsers get a session (`src/pairing.rs`, 2026-10-15; the old `#k=<key>` link, one AES key for every browser, is retired and its pages are told to reload). `kaku remote` writes a one-time offer (`remote-pairing.json`, 10 minutes) and prints `/{id}/#p=<secret>&m=<Mac public key>`; the page makes a non-extractable ECDSA P-256 device key and keeps it in IndexedDB once Kaku has admitted it. Each connection: Kaku sends `KCH3` and an ephemeral P-256 key in the clear, the page answers `KCB3` and its own, both derive one AES-256-GCM key per direction by HKDF-SHA256 over the ECDH secret salted with the transcript (SHA-256 of `kaku-remote v3`, the relay id, both ephemeral keys). Kaku's first sealed message signs the transcript with its long-term key (`mac_key` in `remote-share.json`), which the page checks against the key from its pairing link before sending anything, so a relay cannot pose as the Mac; the page's first sealed message signs it with its device key (`auth`), adding an HMAC of the transcript under the offer secret the first time (`pair`). Messages carry their direction label and position as associated data. Close codes: 4403 not a paired browser (the page forgets its key), 4401 pairing link expired or used, 4426 page from before pairing, 4408 no proof in time; the relay must pass each through (`browser_close`), or the page reads it as busy and retries. Pairing shows no dialog, because the Mac is often unattended when a browser is added from elsewhere: the link works once, every new device posts a macOS notification naming it, and so does a paired device connecting again after 30 minutes away (`QUIET_RETURN`; reloads and reconnects stay quiet), which is the Mac-side sign of remote use (no menubar or tab bar mark, by the maintainer's choice on 2026-10-15), and `kaku remote devices` / `kaku remote revoke <id>` list and remove devices (sessions of a removed device end within seconds). ring's ECDH takes only ephemeral keys, which is why long-term identity is a signature, not a static DH key; ring's fixed ECDSA signatures and WebCrypto's are the same r||s form.\n- Each web session holds `awake::Awake`, a latency-critical `NSProcessInfo` activity. Without it, App Nap throttles the backgrounded GUI's main thread, which turns PTY output into the pane change the session renders from, and every echo waited 100-200 ms. Measure remote input latency with the Kaku window hidden, not focused.\n- `Shown::update` turns a screen whose rows moved into `CSI n S` / `CSI n T` plus the new rows. That relies on each row starting with its own `CSI row;1H` and on the page never setting a scroll region; keep both, or the browser's copy drifts from Kaku's.\n- Clipboard writes from programs (OSC 52, `MuxNotification::AssignClipboard`) also go to browsers viewing that pane (`web::publish_clipboard`, 1 MB cap); the Mac clipboard keeps getting them. Find (`Search`) runs on the Mac over the pane's history, so it reaches past what the page shows. Files a browser drops or pastes arrive in chunks, in a bounded 512 KiB window of 16 KiB chunks, each acknowledged (`UploadAck`), so its percentage is what reached the Mac; a session that ends mid-file deletes the partial file (`Uploads` drop) and the page resends after reconnecting. Files are written to `~/Downloads/Kaku` under a sanitized, de-duplicated name (`safe_file_name`, `unused_path`, 50 MB cap), and their path is pasted into the original drop pane with `quote_dropped_files`, as a native drop would be.\n- Scrolling a plain shell (`local_scroll` in each frame: no alternate screen, no mouse grab, and no foreground `less`) never round-trips: the page fetches history rows with `History` in pages of `HISTORY_PAGE` and scrolls them in `#past`, laid exactly over the terminal with each run placed at Kaku's column. Programs that take the mouse or the alternate screen still get the scroll on the Mac; `less -X` also receives scroll keys, as in the native wheel handler, and Find still pins the Mac-side view (`View::pinned_top`).\n- The page names tabs by `tab_id` and reorders them per `window_id`; rename and move go through `TabAction::Rename` / `TabAction::Move` in `kaku-gui/src/remote_tabs.rs`, which keep the Mac's active tab. A split tab lists its `panes`; viewing one sends `Activate`, which also focuses that split on the Mac. A split tab keeps the Mac window's size (`split_claim`): the browser claims nothing for it, shows the viewed split at its own size scaled to fit the page (up to twice Kaku's font size), and a tab a browser had sized is handed back to its window once split (`TabAction::Reclaim`). Growing the tab by the split's share, as before 2026-10-19, pushed the other splits out of the Mac window. Size claims use zoom-aware `iter_panes()` geometry, so a zoomed pane is claimed like a lone one; tab navigation still lists hidden splits. Browser zoom and unzoom carry only the viewed pane's current claim and reapply it synchronously after the GUI toggles zoom; another pane's action cannot borrow that claim, and native or other-client geometry changes do not automatically reclaim the browser viewport. Frame rendering runs on another thread, so the GUI callback is not a cross-thread atomic transaction.\n- Web pane menus retain the selected `pane_id` through clipboard permission prompts and close dialogs. Splits call `Mux::split_pane` with that id rather than the deferred active-pane spawn path. Confirmed pane closes remove that exact pane, or record and remove its tab when it is last; `CloseCurrentPane { confirm: false }` is not sufficient under `AlwaysPrompt`. Middle-click tab close shares the existing browser confirmation path. Optimistic tab closes require a connected transport and a timer that restores the latest authoritative list when the close is not confirmed; the server suppresses identical lists, so expiry must not depend on another list arriving. Keep cached pane snapshots until the authoritative list drops their pane. Keep native phone selection menus intact.\n- Web URL detection stops at CJK sentence punctuation and unmatched closing wrappers, retains balanced path parentheses, and maps UTF-16 regex offsets back to terminal cells, including surrogate pairs and wide end cells. The browser regression fixture includes the reported GitHub comment link followed by `)，并以`.\n- A tab a browser sized keeps that size through relayouts the Mac does on its own (display or scale change, focus, config or theme reload, tab bar visibility): `apply_dimensions` asks `window_relayout_resizes_tab` per tab. Only the Mac's own use takes it back: typing or clicking (`reclaim_active_tab_size`) or a live window drag. Focus and a modifier pressed alone do not count (2026-10-19): switching between a fullscreen browser and Kaku with Cmd+Tab resized the tab on every switch, and Claude Code left a copy of its output behind for each size. Resizing it under the browser made Claude Code and Codex redraw for the Mac's rows and jump up in the page. The desktop view's synthetic input counts as the Mac's, so the page claims its grid again on its next input and when it leaves the desktop view (`macSized`, `takeBack`). After a reload the first frames still carry the Mac's grid until the new claim lands; the page keeps the terminal at opacity 0 (`body.sizing`, still focusable, unlike the pre-first-frame `visibility: hidden`) until a frame has the claimed grid or a split's own, for at most `REVEAL_WAIT` (600 ms). Tests that screenshot the terminal wait for `sizing` to clear.\n- Page fonts: the page draws with the families of Kaku's `config.font` in order (`font_families`; JetBrains Mono maps to the bundled `Kaku Mono`, emoji fonts stay unnamed), then its own fallbacks with `Kaku Symbols`, the bundled Symbols Nerd Font limited by `unicode-range` to its private-use spans so it is fetched only when an icon shows. Fonts the user installed are resolved on the Mac the way Kaku renders (`src/fonts.rs`: `font_dirs`, then CoreText) and listed in `font_files`, text weight only, since the page draws bold at that weight; files under `/System/`, collections and anything over 32 MB are left out. The page fetches a listed file over the download path only when the browser lacks the family, and keeps it in Cache Storage keyed by path, size and date. The WOFF2 files in `assets/web` come from `scripts/build_web_fonts.py`; rerun it when `assets/fonts` changes, and print its `unicode-range` into `index.html`.\n- The relay is public and sheds abuse itself: browser messages are capped at 512 KB and Mac messages at 4 MB, frames too (`max_frame_size`: tungstenite buffers a whole frame, 16 MB by default, before it checks the message cap); bytes queued toward one Mac (`HOST_QUEUE_BYTES`, the flooding browser is dropped) and toward one browser (`BROWSER_QUEUE_BYTES`, the stalled page is dropped) are bounded, and so are all queues together (`GLOBAL_QUEUE_BYTES`): a message count alone let one stalled page hold a gigabyte of 4 MB screens on a 256 MB machine. Every queue gives its unsent bytes back when its socket ends (`Queued`'s `Drop`), or the relay-wide budget leaks until nothing is accepted; at most 2 registrations per id (a third evicts the oldest) and 4 browser sockets per client address per host; when a host's 8 browser slots are full, the oldest socket the Mac has not admitted is let go (`make_room`; the Mac sends `{\"t\":\"ok\",\"c\":c}` once it admits a browser), so sockets that only know the id cannot lock the owner out; devices are matched on their whole public key, never the 8-hex id people see; `remote-devices.json` changes under an `flock` (`update_devices_at`) so a sign-in cannot write a revoked device back; hosts silent for 75 s and browsers for 150 s are closed. The relay serves the page and its files from its own copy, built in from `crates/kaku-remote/assets/web` (`page_file`), and asks no Mac for them; the page itself answers 404 with the offline notice while the Mac is away, which an open page relies on to tell an offline Mac from a network problem. So a relay deploy ships the page: deploy it together with Kakus that speak its protocol (pairing, `KCH3`), never ahead of them. `fly.toml` caps connections at 400 (soft 300), about 200 people online, from a release-build load test (400 Macs plus 200 streaming pages at 88 MB); raise it only with a new load test or a larger machine, and the Docker build uses `Cargo.lock` with `--locked`.\n- The page lives on its own origin, `t.kaku.fun` (2026-10-16), so serving a page that misuses the browser's keys takes a compromise of that origin, not just of the relay. `scripts/publish_web_page.sh` builds it from `crates/kaku-remote/assets/web` and the bundled JetBrains Mono, fills the `kaku-relay` meta tag (empty in the copies Kaku and the relay serve, which then talk to their own origin), writes a `vercel.json` with `/<id>` and `/<id>/` rewrites, a CSP that allows the one inline script by its SHA-256, and no caching, and deploys it with `vercel deploy --prod` to the `kaku-web` Vercel project, which holds nothing else: a path on `kaku.fun` would share an origin, and the keys, with every page of the site. The page must keep exactly one inline script and no inline event handlers, or the script refuses to publish. With `KAKU_RELAY_PAGE_ORIGIN` set, the relay 308s its page addresses there (the fragment survives), serves no page files, admits browser sockets only with that `Origin` (others are upgraded and closed at once with 4426, so a page still open on the relay's origin from before the move says to reload instead of reading as no network), and answers `/<id>/status` (200 or 404, CORS for that origin) for the page's offline notice. Kaku links the default relay's page to `t.kaku.fun`, any other relay's to the relay itself. `crates/kaku-remote/tests/static_page.cjs` runs the published page behind a server applying its `vercel.json` and checks the redirect, pairing, bookmarks with and without the slash, a foreign `Origin` refused, and no CSP refusals; poll with `page.evaluate` there, since `waitForFunction` evaluates a string the CSP blocks. Publish the page before a relay deploy that sets the origin. After each reconnect the page fetches its own address and reloads when the inline script served there differs from its own (at most once per version through sessionStorage, so a stale cache cannot loop, and never while an upload runs), so tabs left open pick up a new page or a Kaku update without the user reloading; that fetch is why the published CSP's `connect-src` includes `'self'`.\n- The Mac-screen view (2026-10-20) is the maintainer's own for now: the `desktop` cargo feature (`kaku-gui/desktop`) is off by default, so Release and Nightly never carry it. It needs a `kaku-desktop` helper, built from its separate repository. Private packages carry it in `Contents/MacOS/kaku-desktop`, preferred over the legacy copy at `config::DATA_DIR/kaku-desktop`; Kaku starts it as a child so macOS checks Kaku's own Screen Recording and Accessibility grants, and `src/desktop.rs` forwards only `desktop_*` messages in each direction, turning the helper's `desktop_frame` lines into binary messages (zero byte, u32 header length, JSON header, H.264 bytes) for a page whose `desktop_start` says `binary`; `run_session` takes them on a separate `frames` channel that both writers (`share.rs` and the local WebSocket) merge. The helper lives as long as the page's session: `desktop_stop` is forwarded, not acted on, because the helper may hold a virtual display shaped to the page's view, and making one again on every visit costs seconds and moves the Mac's windows (`the_helper_outlives_a_stop_but_not_the_session`). The page loads `assets/web/desktop.js` only when the session config says `desktop: true`, so other Kakus and the published page show nothing new. Build the helper in Release first, then build the private package with `PROFILE=release-opt CARGO_FEATURES=kaku-gui/desktop KAKU_DESKTOP_HELPER=<helper-executable> OUT_DIR=dist/private ./scripts/build.sh --app-only --native-arch`. The build checks helper architecture and signs it before the main executable. Public builds omit both the feature and helper input; a public update replaces the private package and removes its desktop capability. Preparing a private package does not authorize replacing or restarting the installed app.\n- Desktop feedback timing names two boundaries: `desktop_input_ack` confirms a successful input post, and a drawn frame with its `input_seq` confirms a capture after that post, not that the target app reacted. Match both even when independent output queues deliver the frame first; retain slow samples and report unposted, unframed and cancelled inputs separately. A new `stream_id` isolates capture generations and decoder callbacks; frame sequence numbers remain monotonic within the helper. ACK old frames with their own stream id, never the current one. Legacy helpers without these fields stay usable without fabricated timing. Desktop wheel, touch and pointer cancellation belong only to the visible desktop, never the hidden terminal. While the Mac is locked the helper sends `desktop_locked` (from `CGSSessionScreenIsLocked` plus the `com.apple.screenIsLocked` / `screenIsUnlocked` notifications, watched only between `desktop_start` and `desktop_stop`) and the page shows a notice over the picture; typing the password into the view unlocks it.\n- Direct path (2026-10-21 for the desktop view, every build and session since 2026-10-22, `src/p2p.rs` and the page's «Direct path» block): beside the relay, the page offers a WebRTC data channel (str0m with `rust-crypto` on the Mac; `apple-crypto` fails to link) once the config says `direct` (or `desktop`, from the private builds that had it first; a Kaku that says neither gets no offer); offer and answer travel inside the sealed session, so the relay cannot sit in the middle. Interactive replies (terminal frames included), the page's messages under 16 KB and desktop pictures take it, files stay on the relay. Each path has a number (`id` in `rtc_answer`). Order across the move is a fence, not a guess: each side's last relay message before it sends on the path is `rtc_ready` (Kaku sends one per lane, text and frames, the frames one as a binary header-only frame), and the receiver holds what arrives on the path until that marker comes through the relay; without it a delta picture or a key-up overtook one still on the relay. Either side ending a path tells the other by the relay (`rtc_closed` from Kaku, `rtc_close` from the page): the page's channel otherwise stayed \"open\" for about 30 s after Kaku dropped it and lost every click. After a path that had opened ends, the page sends `hello` and `attach` (what was on the path is lost, so the screen starts over), asks for a key frame, and offers again after 2, 5, 15, 30, 60 s; a path that never opened is not retried. Kaku tells the helper `desktop_keyframe` with `path_changed` on every move, which drops in-flight pictures and the old path's round trip and rate samples (kept, a 5 ms direct RTT read every 120 ms relay ack as a standing queue). Host candidates from `getifaddrs` minus loopback, link-local and 198.18/15 (TUN fake IP), plus one server-reflexive candidate per socket from a STUN binding to `stun.cloudflare.com` and `stun.l.google.com` (the page asks the same two), sent before the socket's reader starts and given `STUN_WAIT` (800 ms, DNS included) in all. Without the Mac's reflexive address a browser behind a port-restricted NAT only ever sent to the Mac's private addresses and never opened its own NAT to the Mac. Under a TUN proxy the reflexive address is the proxy's exit (measured 2026-10-22: 23.249.x on the maintainer's Mac), a candidate nobody can reach, which costs nothing. No TURN: when hole punching fails, the relay already carries the session. Unit tests keep STUN off (`STUN_SERVERS` is empty under `cfg(test)`); `public_stun` (ignored) asks the real servers. Pictures ride the reliable ordered channel on purpose, since H.264 deltas cannot survive loss.\n- The relay runs in `nrt` (Tokyo, `crates/kaku-relay/fly.toml`), the edge the Mac enters through: from `sin` every keystroke and echo crossed Tokyo to Singapore twice, 375 ms echo against 166 ms now. It keeps hosts in memory, so run one machine; two would split a Mac and its browsers across them. The relay is not where time goes (2026-10-20): a release build forwards 150 Mbps from one Mac to one browser with under a millisecond added, and the live machine sat at load 0.00 while a page streamed. Slowness through it comes from each side's path to Fly: a proxy node can cap the Mac's upload far below its own line, and some direct routes from China enter a far edge such as `fra`. Measure the path (`fly-request-id` names the edge, a timed upload gives the rate) before resizing the machine or moving the region, and make streams adapt to the link rather than assume it, as the desktop helper's rate control does.\n- Only Mac-confirmed text is rendered. Cursor visibility cannot distinguish normal input from passwords, so never predict typed characters. Pages opt into output flow control with `Hello.frame_ack`: at most 12 unacknowledged frames and a 512 KiB JSON byte waterline, including associated images. One complete frame may cross the waterline before rendering pauses; ACK after xterm parses even an ignored pane frame, coalesce later dirty output, and leave legacy hello clients unthrottled.\n- Browser selection copying follows the Mac's existing `copy_on_select` value and runs on selection release without a network hop. Preserve manual copy when disabled, reconstruct history line breaks and virtual gaps, and never queue a denied automatic copy for a later unrelated gesture. Browser clipboard permission and real mobile selection remain browser-specific.\n- `Frame.wrapped` and `History.wrapped` identify physical rows that continue into the next row. Copying, history pagination and pane snapshots preserve these flags; even a delta frame carries the whole array, and a wrap-only change must be sent. Never infer soft wrapping from a full-width row. Older hosts without flags retain hard line breaks.\n- History keeps at most 10,000 row strings but materializes only visible rows and overscan, preserving stable DOM nodes that intersect an active selection. Cached tab attachment must precede resize, and an equal-size claim must still record browser ownership.\n- Paths in the output (`~/…`, `/…`, `./…`, never inside a URL; `PATH_RE` in the page) are links: Cmd+click (Ctrl+click off the Mac) asks Kaku for that file (`Download`), and it comes back as a browser download. Kaku resolves `~` to the home folder and a relative path against the pane's working directory, sends only regular files up to 50 MB, and refuses folders and missing files by name. Chunks of 64 KB go out while less than `DOWNLOAD_WINDOW` (256 KB) is unacknowledged (`DownloadAck`), limiting queued bytes ahead of terminal interaction. The maintainer chose input and scroll responsiveness over bulk throughput on fast, high-latency links (2026-10-18); keep that trade-off explicit when changing the window. The relay never holds much of a file for one browser; the relay drops a browser past 8 MB queued. A download ends with its session, and the page says so on a dropped link. Any paired browser already has the shell, so reading a file grants nothing new.\n- Upload transfer ids bind the file and original pane for ten minutes after completion, across transport reconnects within the same app process. Retry returns the receipt without writing or pasting twice; cancellation and disconnect remove only partial files. Freeze the target once for a multi-file drop.\n- Access code (2026-10-16, the launch gate the maintainer chose over trials): with `KAKU_RELAY_ACCESS_CODE` set (a fly secret the maintainer picks), the relay registers only Macs presenting it in `x-kaku-access`, compared as SHA-256 in constant time; 401 bodies say `access code required` or `wrong access code`, and ten wrong codes from one address lock it for `ACCESS_WINDOW` (429). `kaku remote` reads the relay status, asks for the code without echo when the relay refused it, saves it to `remote-access` (0600, ASCII only since it travels in a header), and waits for the bridge, which polls that file during its backoff and retries within a second. Every recorded attempt stamps `RelayStatus::checked`, so a second wrong code is told apart from a stale one. Per-address limits sit beside it: `CONNECTS_PER_MINUTE` new sockets and `MAX_HOSTS_PER_ADDR` registered Macs per source, which `source_of` takes as the IPv4 address or the IPv6 /64 from `fly-client-ip` (r.kaku.fun has an AAAA record, and one IPv6 client can pick a new address from its /64 for every request); a large limits map is swept at most once a second; local runs have no client address and are not limited. Host admission must enforce both address and global capacity under the same lock as registration; a pre-upgrade count check alone is not a limit under concurrent handshakes. `live.cjs` runs its relay with a code and checks refusal, then acceptance after saving it.\n- No registration tokens (2026-10-17): remote access is free, so the Ed25519 registration tokens built for trials and Pro (`kaku-relay keygen` / `issue`, `KAKU_RELAY_TOKEN_KEY`, `kaku remote --token`, `x-kaku-token` on registration) were removed with their `ring` and `base64` dependencies; the access code and per-address limits are what gate the relay. Measured cost at that point: one heavy user moved 271 MB a day through the relay (about $0.33 a month of Tokyo egress), so about 100 users cost $20 to $40 a month on the 256 MB machine.\n- No pairing QR code (2026-10-17, maintainer's call): the page is made for a desktop browser for now, so `kaku remote` prints the link with how to use it and the manage commands, not a QR code; do not bring one back while phones are not a target. The link is only `#<one-time secret>` (`pairing::pairing_fragment`); `mac_auth` carries the Mac key, and while the offer is fresh a `tag`, HMAC under that secret over the transcript and the key (`pairing::mac_key_tag`), which the page checks before trusting the key. The relay never sees the secret, so it cannot vouch for a key of its own; a missing tag means the link was used or expired. The page still accepts the older `#p=<secret>&m=<key>` form. Publish the page before shipping a build that prints the bare form.\n- Remote input stays ordered within each browser, but a blocked browser must not hold the shared relay receive loop. Each browser has a bounded worker; overload or timeout closes that browser explicitly rather than dropping individual keys. GUI tab snapshots are requested asynchronously, with at most one outstanding request per session, and a later explicit Attach cancels an earlier automatic NewTab follow.\n- Both WebSocket writers prioritize interactive replies before bulk traffic, with one bulk turn after eight interactive messages to prevent starvation. File start, chunks and completion stay in the same bulk FIFO. Assign encrypted sequence numbers only after the final writer selects a message; reordering already encrypted messages breaks authentication. Keep the final bulk queue bounded to one pending message, and discard revoked sessions before encrypting their queued output. Bytes already written to TCP cannot be overtaken by later input.\n- Every relay connect is capped by `CONNECT_TIMEOUT`: a local proxy (Stash TUN) can accept TCP and never finish the handshake, which once left the link offline for hours. Bound socket writes and session inbox waits too, but allow short input bursts to wait rather than discard keys. Wrong-key close 4403 is final; idle unauthenticated sessions expire with 4408. A host replacement closes browsers independently of the old data queue, with late input discarded until the close writer finishes.\n- The default relay uses `r.kaku.fun`; its browser page lives at `t.kaku.fun/{id}/`, and the relay redirects older `/{id}/` and `/t/{id}/` page links there. Never shorten the 128-bit identity or put a pairing secret in a URL path/query. The page erases the fragment at once and saves the device only after Kaku admits it; when IndexedDB is unavailable it pairs for that tab only and says so. Loopback tokens stay tab-only in sessionStorage. Keep pairing instructions in the CLI, page messages and public docs consistent.\n- One identity per Kaku, persisted at `DATA_DIR/remote-share.json` (0600), devices at `remote-devices.json`. `kaku remote --reset` replaces the identity and forgets every device; the running bridge notices within seconds and re-registers.\n\n## Remote Verification\n\n- For an authorized relay deployment, run `fly deploy . --config crates/kaku-relay/fly.toml --dockerfile crates/kaku-relay/Dockerfile --remote-only --depot=false --ha=false --strategy immediate` from the repository root (the build context must reach the page files; the root `.dockerignore` sends only what the relay embeds, about 1 MB, never `target/`). Use `--depot=false` when Depot stalls at builder authentication. Verify the Fly tunnel with `fly doctor` first; do not add `--wg=false` to a working tunnel, since the legacy builder's HTTPS path can fail with an h2c upgrade HTTP 500. This selects a build transport, not a system proxy setting. Verify the deployed image digest and an isolated encrypted session afterward; a dashboard without new releases does not diagnose a failure before image creation. If CLI DNS checks warn while HTTPS works, compare public DNS before changing anything.\n- Run `cargo test --locked -p kaku-remote`, relay tests via its own manifest, and the workspace check/tests/clippy, which build the feature by default as CI does.\n- `node --test crates/kaku-remote/tests/web.test.cjs` uses Playwright Chromium to exercise the shipped HTML and xterm, including input privacy, reconnect, upload pacing, history selection, and viewport boundaries. Set `NODE_PATH` when Playwright is installed outside the checkout; `PLAYWRIGHT_CHROMIUM_EXECUTABLE` optionally selects an existing browser. The page draws with WebGL when it can, so most tests launch with WebGL off and read the DOM fallback, and one test checks the WebGL grid, down to its text pixels. The WebGL tests launch with `GPU_ARGS` (`--use-angle=metal --enable-gpu --ignore-gpu-blocklist`), which give headless Helium WebGL2 on the Apple GPU (2026-10-21; without them it had none and those tests skipped). CI's Chromium can still lack WebGL2 and skip them, so read the reported skips before claiming GPU coverage. Playwright's own Chromium downloads fail code signing on Darwin 27 and its headless shell only has SwiftShader, whose WebGL timings are meaningless. Never emulate `deviceScaleFactor` for WebGL checks or measurements: it changes `devicePixelRatio` but not `devicePixelContentBoxSize`, so xterm sizes its canvas at half the device size and draws nothing or a shrunken grid, a failure real browsers do not have.\n- The bar for the page (maintainer, 2026-10-16) is how typing, scrolling, copying and tab operations feel: no delay, no stutter, like the app. After changing the page, `web.rs` or the relay, run `node crates/kaku-remote/tests/feel.cjs` (same setup as `live.cjs`) and compare: on loopback, keystroke to echo p50 about 5 ms, a 54-character burst intact, no scroll frame over 25 ms, copy-on-select exact within a frame, new tab shown in about 15 ms, switch and close within one frame (16 ms). It times operations in the page from the click event to the screen change, since Playwright waits about two frames before it clicks. It explicitly disables WebGL with `--disable-webgl` and `--disable-3d-apis`; these are DOM timings, not GPU timings. Headless Helium has WebGL2 only with `GPU_ARGS` from `web.test.cjs`, so WebGL timings need those flags or a headed browser; do not read a DOM-only run as WebGL coverage.\n- `node crates/kaku-remote/tests/static_page.cjs` (same app and relay setup as `live.cjs`) also reads DOM rows and explicitly disables WebGL. `--disable-gpu` alone still exposes WebGL2 in some Chromium builds, so it does not guarantee DOM rows exist; use `--disable-webgl` and `--disable-3d-apis` for every DOM-based harness. Keep WebGL off in harnesses that open several tabs for a second reason: headless Helium on Metal stops opening new tabs once a page has drawn with WebGL (navigations abort before any request reaches the server and `newPage` hangs), which looked like a page bug for two rounds.\n- After `make app` and a debug relay build, run `node crates/kaku-remote/tests/live.cjs`. It launches only its own app, temporary HOME, and loopback relay, tests encrypted input, uploads across reconnect, frame flow control, host replacement, stable split targets and AlwaysPrompt pane closing, foreign `Host` refusal, and replayed, misplaced and old-format messages, then removes its own fixtures. Never substitute a real share link. DOM-row assertions in `live.cjs` explicitly disable WebGL; real GPU rendering remains covered by the separate browser tests, which must assert canvas output rather than DOM rows.\n\n## Cross-References\n\n- [`term/AGENTS.md`](../term/AGENTS.md) - Uses codec, vtparse, and color types.\n- [`kaku-gui/AGENTS.md`](../kaku-gui/AGENTS.md) - Uses font, input, and surface crates.\n- [`lua-api-crates/AGENTS.md`](../lua-api-crates/AGENTS.md) - Uses Lua helper and dynamic binding crates.\n",".agents/skills/bugs/SKILL.md":"---\nname: bugs\ndescription: \"Proactively sweep a Kaku area for latent defects and UX traps before users report them, using this repo's own fix history and multi-entry-point archetypes, and confirm every finding with a probe run this turn.\"\nwhen_to_use: \"找找有没有bug, 主动找bug, 有没有隐患, 排查隐患, 举一反三, 上线前扫一遍, 这块可靠吗, 体验问题, latent bug scan, sibling sweep after a fix, /bugs\"\n---\n\n# Bugs: Find It Before the User Does\n\n`/hunt` needs a symptom. `check` / review needs a diff. This one needs neither: it goes looking.\n\nNamed **bugs**, not bug: `/bug` is a Claude Code built-in.\n\n## The Question\n\nAcross Kaku's closed bugs and fix commits, the dominant defect is not always a crash. It is often:\n\n- a **second entry point** that skipped the safety path the first entry already had\n- a **wrong-but-plausible** geometry or selection state after a modal/resize/sleep\n- a **shell-generated script** that is fine on the author's machine and broken in a clean HOME\n- an **AI surface** (Cmd+L, `#`, `k`, toast) that diverges on auth, theme, or cwd\n\nSo the question that finds bugs here is never only \"can this crash?\" It is:\n\n> What does this code do when the user arrives through the **other** path (menu vs toast vs CLI vs shell), when the display **scale or sleep** changes mid-session, when the shell is **not** the developer's zsh, or when the AI pane cwd is **remote**?\n\n## 1. Pick the surface\n\nName the area and the depth. Whole-repo sweeps with no budget produce speculation. Start from the Hotspot Map below (or the area that just shipped a fix) and go deep.\n\n## 2. Read that area's own fix history first\n\n```bash\ngit log --oneline --grep='^fix' -i -- <path>\ngit log --pretty=format:'%h %s%n%b' --grep='^fix' -i -- <path> | head -200\ngh issue list --repo tw93/Kaku --state closed --label bug --limit 40\n```\n\nBugs recur by shape within a module. Two signals worth acting on:\n\n- A fix chain of two or more commits each \"completing\" the previous one means a sibling path is probably still open. Real chain here: toast confirm, then menu confirm, then brew confirm on the same update flow.\n- A commit body that says only one surface was fixed (toast / CLI / overlay / brew) means grep every other surface for the same action.\n\n## 3. Sweep the boundaries, in this order\n\nHighest historical yield first. Subsystem guides live in crate `AGENTS.md` files and root `AGENTS.md` risk areas.\n\n| Boundary | What to ask | Where it usually lives |\n|---|---|---|\n| Multi-entry destructive actions | Menu, toast, CLI, shell, key binding: do they all confirm the same way before quit/replace/kill? | `kaku-gui/src/frontend.rs`, `update.rs`, `overlay/confirm.rs`, `overlay/confirm_close_pane.rs`, `kaku/src/update.rs` |\n| macOS window geometry | Traffic lights vs tab bar, DPI scale, external display drag, sleep/wake, lock screen, fullscreen first frame | `window/src/os/macos/`, `kaku-gui/src/termwindow/resize.rs`, titlebar paint |\n| Tab chrome | Overflow hit targets, rename modal selection, multi-pane title width, top vs bottom bar | `kaku-gui/src/tabbar.rs`, `termwindow/tab_rename.rs` |\n| Selection / mouse state | Survives Ctrl+L, modal cancel, mouse-reporting panes, successful terminal input? | `kaku-gui/src/termwindow/selection.rs`, mouseevent |\n| Shell integration generation | `local` only inside functions; backticks in bash-heredoc that emit zsh; Starship/PATH scoped to Kaku | `assets/shell-integration/`, `kaku/src/init.rs`, `setup_zsh.sh` |\n| Session restore | Split + scrollback both sides, crash backup, stale snapshot resurrecting closed tabs | `kaku-gui/src/session_restore.rs`, paint/quad buffers |\n| AI dual surfaces | Cmd+L vs `#` vs `k` vs chat: same auth, theme, proxy, remote-cwd tool policy? | `kaku-gui/src/ai_*`, `inline_ai.rs`, `cli_chat/`, overlay |\n| Proxy / network | External API uses system proxy; loopback/private/LAN/`.local`/NO_PROXY go direct | `config/src/proxy.rs`, AI transport |\n| AppKit menu intercept | `Ctrl+letter` only `key_is_down: false` in logs? Menu `keyEquivalent` ate it | `window/src/os/macos/menu.rs`, debug_key_events |\n| Render buffer growth | Over-capacity draw after retry budget; sleep invalidates drawable | `renderstate.rs`, paint/draw paths |\n\nFor a named user symptom, switch to hunt. For a diff under review, switch to check / review.\n\n## 4. Confirm before reporting\n\nA candidate is not a finding until it has evidence produced **this turn**.\n\n- **Confirmed**: source trace that names every entry point, a failing test, or a probe (log, `make test` subset, clean-HOME shell smoke).\n- **Plausible**: complete source path, named failure mode, no run yet. Report separately with the probe that would settle it.\n- **Not a finding**: \"looks risky\" without reading the implementation. Grep the call sites or drop it.\n\nBefore flagging unwraps or panics, confirm production code: skip `#[test]`, fixtures, doctests, and build scripts.\n\n## 5. Rank by blast radius\n\n1. Silent data loss or process kill (update/restart, close all, reset) without confirm on some entry path\n2. Crash loop or unrecoverable launch (session restore, paint)\n3. Stuck UI state (selection latch, frozen frame after sleep, window behind menu bar)\n4. Shell/environment pollution outside Kaku (Starship, PATH, generated zsh)\n5. Visual chrome wrong but recoverable (tab clip, traffic-light gap, font weight)\n6. Docs / copy drift\n\n## 6. Guard what gets fixed\n\nAnything confirmed and fixed needs a regression guard when feasible:\n\n- unit/integration test that fails on unfixed code\n- shellcheck / setup smoke for shell-generation bugs\n- entry-point matrix comment or test listing menu + toast + CLI for destructive flows\n\nVerify with the matrix in root `AGENTS.md`. GUI geometry and AppKit need `make app` or a hand smoke from `.agents/skills/release/SKILL.md` «Pre-release smoke checklist».\n\n## Hard Rules\n\n- **No finding without this-turn evidence.** Speculation wastes review time.\n- **One archetype hit means sweep every sibling entry point.** Report `checked N sites, M defective, K n/a`.\n- **Do not fix while sweeping.** Collect, then fix. Interleaving loses the sweep.\n- **Report clean boundaries as clean.** Do not invent findings to justify the run.\n- **Findings outside the named area get listed, not fixed**, unless the maintainer agrees.\n- **Do not propose UI i18n** (see root `AGENTS.md`).\n\n## Hotspot Map (issue-backed)\n\nUse these as default scopes when the maintainer says \"scan for bugs\" without a path:\n\n| Area | Issue / fix examples | Probe |\n|---|---|---|\n| Update confirm matrix | toast + menu overlay, CLI/brew `confirm_apply_update`, guards in `update.rs` / `frontend.rs` tests | Trace every caller of `restart_to_update` / `spawn_update_helper` / `run_brew_upgrade`; run those unit tests |\n| Titlebar / tabs / DPI | #516, #504, #490, #483, #460 | External display + scaled resolution + tab top/bottom |\n| Window drag / menubar | #508, #456, #408, #414, #477 | Drag to menubar, lock screen, fill desktop |\n| Selection latch | #495, #487, #455 | Ctrl+L, cancel rename, mouse-reporting TUI |\n| Shell init generation | #432, #441, #450, #503, #420 | clean HOME `kaku init`, `zsh -n` generated file, non-Kaku shell |\n| Session restore paint | #514, #448, #482 | split + dual scrollback snapshot |\n| AI auth / overlay | #506, #501, #502, #418 | Cmd+L vs `#`, split resize, appearance flip |\n| Proxy split brain | private base_url + system proxy | loopback OpenAI-compatible smoke |\n\n## Output\n\n```\nArea:        [what was swept, at what depth]\nBoundaries:  [N walked, M applicable]\n\nConfirmed (severity order):\n1. [file:line] [the defect in one sentence]\n   Evidence: [probe / call-graph / failing test]\n   Blast:    [what the user sees]\n   Siblings: [N checked, M defective, K n/a]\n\nPlausible (needs a probe):\n1. [file:line] [defect] -> [the probe that would settle it]\n\nSwept clean:\n- [boundary]: [what was checked, why it holds]\n\nSibling sweep: [pattern signature] -> [N checked, M defective, K n/a]\n```\n\nSay whether anything was fixed or whether this was scan-only.\n",".agents/skills/release/SKILL.md":"---\nname: release\ndescription: \"Run the Kaku macOS release flow end-to-end: preflight, build, notarize, tag, upload to GitHub Releases, dispatch the Homebrew tap, or publish the rolling Nightly preview package. Source of truth is scripts/release.sh and scripts/nightly.sh; this skill records release prerequisites and recovery hints without embedding private machine setup.\"\nwhen_to_use: \"发布, 出版本, 打 tag, release Kaku, ship Kaku, V0.x.x, kaku release, prepare release, dry-run release, resume release, notarize, homebrew tap, nightly, Nightly 包, 更新 nightly, 预览包\"\n---\n\n# Kaku Release Runbook\n\n`scripts/release.sh` is authoritative. This skill captures the release prerequisites and the order Tang and the agent rely on each release. Exact credential locations and one-time recovery commands live in the ignored `.claude/release.local.md`, never in this tracked file.\n\nBefore any stable or Nightly release, run the non-secret checks under **Verify**. The release dry-run is the authoritative credential gate: it accepts either the rcodesign path or the notarytool fallback, including environment overrides. If it passes, do not load machine-specific recovery context. If a signing identity or every notarization path is missing or stale, then check for and read `.claude/release.local.md`; on Tang's release machine it is the required recovery layer. If the local runbook is absent, stop and ask for it instead of guessing a path or replacing credentials.\n\n## TL;DR\n\n```bash\n./scripts/release.sh --dry-run                              # preflight only\nKAKU_ASC_API_KEY_PATH=/dev/null ./scripts/release.sh          # known rcodesign compatibility issue\n./scripts/release.sh                                        # full release\n./scripts/nightly.sh                                        # rolling Nightly DMG for fixes already on main\n```\n\n`release.sh` must run on `main`, with a clean tree, in sync with `origin/main`. `nightly.sh` has no branch or clean-tree gate; it only refuses to publish when HEAD is not yet on `tw93/Kaku`. Both auto-detect the signing identity and read notarization creds from the login keychain.\n\nThe `KAKU_ASC_API_KEY_PATH=/dev/null` prefix selects notarytool before submission for a known non-network compatibility issue. Do not switch backends or disable S3 acceleration to bypass a network failure. Submission and timestamp failures allow one retry on the same route; if it fails again, stop, diagnose read-only, and wait for the maintainer to resolve the network before resuming. A stored profile name does not prove that its notarytool login exists; verify the profile or use the API-key recovery below.\n\n## Nightly preview package\n\n`scripts/nightly.sh` is authoritative for the rolling `nightly` GitHub prerelease. It builds a release-grade universal app, signs, notarizes, staples, copies `dist/Kaku.dmg` to `dist/Kaku-nightly.dmg`, and recreates the `nightly` release. It does not bump versions, create a `V*` tag, update Homebrew, or refresh any stable release surface.\n\nRules:\n\n1. Only publish Nightly from the commit users should test. The script checks that HEAD exists on `tw93/Kaku` before deleting the old nightly release.\n2. Do not tell reporters to try Nightly until `./scripts/nightly.sh` has completed and `gh release view nightly -R tw93/Kaku --json tagName,targetCommitish,publishedAt,assets,url` points at the intended commit with `Kaku-nightly.dmg`.\n3. Do not use `--upload-only` unless the existing `dist/Kaku.dmg` is already known to be a signed, notarized, stapled build from the intended commit.\n4. Keep public wording precise: \"available in the latest Nightly\" only after the check above; otherwise say \"fixed on main and will be in the next Nightly or release.\"\n\n## Credential prerequisites\n\nThe ignored `.claude/release.local.md` is the canonical map from Tang's local backup paths to these script inputs. The tracked contract is limited to what the scripts consume: either `KAKU_ASC_API_KEY_PATH` or the `kaku-asc-api-key-path` login-keychain item for rcodesign, and either `KAKU_NOTARYTOOL_PROFILE` or the `kaku-notarytool-profile` login-keychain item for the fallback. Do not delete or rewrite the local recovery map merely because its paths are machine-specific.\n\n### Preferred: rcodesign + ASC API key\n\n`rcodesign` is preferred on macOS 26+ because notarytool can SIGBUS. The scripts also accept the notarytool fallback when its prerequisites are available; use the release dry-run to check eligibility and the actual notarization result to verify success. It is not in homebrew core.\n\n```bash\ncargo install apple-codesign         # binary: rcodesign, lands in ~/.cargo/bin\n```\n\nThe private JSON supplied through the environment or keychain path must use rcodesign's native `issuer_id`, `key_id`, and `private_key` format. Do not record its location in tracked docs.\n\n### Fallback: notarytool profile\n\nCreate the notarytool profile with the maintainer's private Apple ID, team ID, and app-specific password outside the repository. Pass its profile name through `KAKU_NOTARYTOOL_PROFILE` or the expected login-keychain item; never paste the source values or the import command into a tracked runbook.\n\n### Verify\n\n```bash\nsecurity find-identity -v -p codesigning | grep 'Developer ID Application'\ngh auth status\n./scripts/release.sh --dry-run\n```\n\nWhen the dry-run reports a notarization failure, use `command -v rcodesign`, `security find-generic-password -s 'kaku-asc-api-key-path'`, and `security find-generic-password -s 'kaku-notarytool-profile'` as separate diagnostics. They are not an AND gate: one complete notarization backend is sufficient.\n\n## Pre-release content checklist\n\nBefore invoking `release.sh`, confirm in this order:\n\n1. `kaku/Cargo.toml` and `kaku-gui/Cargo.toml` versions match (e.g. `0.10.0`).\n2. `assets/shell-integration/config_version.txt` is the intended schema version, with highlight rows in the matching docs.\n3. `.github/RELEASE_NOTES.md` first heading is `# V<version> <suffix>` (uppercase V, used as the GitHub Release title).\n4. Both English `Changelog` and Chinese `更新日志` sections in `RELEASE_NOTES.md` cover the same items. Register: one full stop per item, declarative middle register, neither literary nor conversational; the item count follows the number of distinct user-visible outcomes (the previous release's count is history, not a quota), and length is measured against the previous release before publishing.\n5. Any pending fixes are committed and pushed to `origin/main`.\n\nUse `./scripts/prep_release.sh <bump>` to draft the version bump and notes when starting from an older tag. Run it with `--no-edit --no-push`: it commits with the checkout's local git identity, which can be the wrong account for the day, so fill in the notes and the `config_update_highlights.tsv` stub rows, then fold them into that unpushed commit with `git -c user.name=… -c user.email=… commit --amend --reset-author` before pushing (V0.25.0). Tang typically edits the resulting `.github/RELEASE_NOTES.md` by hand to apply the announcement-writing style (community first, 2 to 4 highlights, user-experience framing).\n\n## Pre-release smoke checklist (runtime-only hotspots)\n\nThese are the areas that produce the most post-release bug reports and that CI cannot see (visual layout, native AppKit, shell-in-user-env).\n\nHand the maintainer a release-shaped build for this pass, not `make app`: that one is a debug build, and remote-terminal latency and scrolling feel nothing like the shipped app. `PROFILE=release-opt BUILD_ARCH=native ./scripts/build.sh --app-only` (about 20 minutes, signed with the local identity) differs from the release only in notarization and the second architecture; swapping it into `/Applications` and restarting lets the maintainer test in the Kaku they use every day. Run them by hand in the built `dist/Kaku.app` before tagging. Automated coverage already exists for the testable slices: tab width-budget + hover hit-testing have unit tests in `kaku-gui/src/tabbar.rs`, and `local`-outside-function / shell syntax is gated by shellcheck + the setup_zsh smoke. The list below is what still needs a human.\n\n1. **macOS window** (#408, #414): on first launch, a single click on the title bar / top inset must not maximize the window; drag the window while it fills the desktop; flip system light/dark and confirm all windows refresh; enter and exit fullscreen cleanly.\n2. **Tab bar** (#409, #435, #439, #443, #445, #447): check both `tab_bar_at_bottom` true/false; open enough tabs to overflow a narrow window and confirm titles truncate but every tab stays clickable; rename a tab, then click another to switch and confirm no position scramble; confirm a tmux/shell status prompt renders in the bar without clipping. Exercise the status dot through idle, running, pending or attention, and cleared states on active and inactive tabs, with the tab bar at both the top and bottom; idle must not leave a dot behind.\n3. **Shell setup** (#420, #432, #441, #450): from a clean `HOME`, run `kaku init` and confirm z / syntax-highlight / autosuggestions are active in a fresh shell; run `kaku init --update-only` and confirm it exits clean; open a new shell and confirm `~/.config/kaku/zsh/kaku.zsh` sources with no error.\n4. **AI chat** (#418, #431): run `kaku chat`, quit, then run it again in the same window and confirm it reopens.\n5. **Render timing / stale drawable** (#452, #458): on the bundled WebGpu backend, sleep the Mac then wake it and confirm the window repaints instead of freezing on the old frame while keystrokes still reach the shell; connect or disconnect an external display and confirm no frozen frame or geometry jump; open a new window straight into fullscreen and resize it, confirming it fills without a stale first frame.\n6. **Hyperlinks** (#547): Cmd-hover a URL that the terminal soft-wraps and confirm both rows target the full URL. Then print a URL exactly as wide as the terminal, followed by `\\r\\n` and unindented unrelated text; the target must exclude that text. Terminal and surface tests cover the grouping; check the built app hover target too.\n\n7. **Mouse recovery** (#551): in both zsh and fish, run `printf '\\033[?1003;1006h'` and return to the prompt; moving the mouse must not insert escape sequences. Also keep a mouse-enabled TUI running and confirm mouse input still works until it exits.\n\n8. **Settings lifecycle and custom configs** (#545): start the app with a minimal standalone `--config-file`, switch between Kaku Light and Kaku Dark, and confirm Settings edits that file and the main window follows. Save and exit Settings repeatedly; no destroyed-window repaint panic should appear.\n\n9. **Tab bar pointer paths** (V0.22.0): with 2+ tabs and the bar at the bottom, tap just above a tab and confirm it switches; middle-click to paste on the last terminal row and confirm no tab closes; right-click an inactive tab, choose Close Tab several times and confirm the app stays up and closes that tab while you stay on yours.\n10. **Split panes with dimming** (#562): set `inactive_pane_hsb = { brightness = 0.7 }`, split top|bottom with the tab bar at top and at bottom, focus each pane, and confirm the dimmed background stops exactly at the line with no undimmed strip on the right or bottom edge; split into three panes and confirm two dimmed neighbours still show a faint line; drag the line and confirm it does not jump a row on grab.\n\nWhen a release fixes a bug outside this list, add the reproduction here so the next release re-checks it.\n\n## Verification commands\n\n| Scope | Command |\n|---|---|\n| Format | `make fmt-check` |\n| Compile | `make check` |\n| Tests | `make test` |\n| Release notes version match | `./scripts/check_release_notes.sh` |\n| Config schema versioning | `./scripts/check_release_config.sh` |\n| Config release readiness | `./scripts/check_config_release_readiness.sh` |\n| Full preflight | `./scripts/release.sh --dry-run` |\n\n`release.sh --dry-run` stops after preflight and does not run source checks. The normal release reuses successful CI Checks for the current HEAD; otherwise `stage:checks` runs the local checks below, as described in `AGENTS.md`. A passing dry-run alone is not source-check evidence.\n\n## Stage map\n\n`./scripts/release.sh` runs these stages in order. Each is timed and labeled `[stage:<name>]`.\n\n1. **preflight**: clean git on `main` in sync with `origin/main`, version consistency, gh auth, release notes, config, latest `Build Validation` workflow run on `main` green (an in-progress run only warns), profile, signing identity, notarization creds.\n2. **stage:checks**: reuse successful CI Checks on the current HEAD, or run `make fmt-check && make check && make test` locally. `RUN_CLIPPY=1` adds local clippy in either case. `SKIP_TESTS=1` skips local tests (avoid).\n3. **stage:build**: `./scripts/build.sh`, `PROFILE=release-opt`, `BUILD_ARCH=universal`. Output: `dist/Kaku.app`, `dist/Kaku.dmg`, `dist/kaku_for_update.zip`, `dist/kaku_for_update.zip.sha256`.\n4. **stage:notarize**: `./scripts/notarize.sh`. Tries rcodesign first; falls back to notarytool only for a non-network rcodesign failure when a notarytool profile exists.\n5. **stage:tag**: `git tag -a V<version> -m 'Release V<version>'` then `git push origin V<version>`. Idempotent: reuses an existing tag at HEAD instead of dying.\n6. **stage:upload**: `gh release create V<version>` (or `gh release edit` if it already exists) with the dmg, zip, and sha256. Title is taken from the first `# ` line of `RELEASE_NOTES.md`.\n7. **stage:homebrew-tap**: `repository_dispatch` (`kaku_release_published`) to `tw93/homebrew-tap` triggering `bump.yml`. Polls `Casks/kakuku.rb` through the GitHub contents API for the new version (default 12 attempts, 15s apart). The official `homebrew/cask` token is `kaku` and is bumped by Homebrew livecheck from the GitHub release; this stage only updates the personal tap for existing `tw93/tap/kakuku` installs.\n\n## Resume after failure\n\n`release.sh` accepts these flags to skip already-completed stages:\n\n| Flag | Skips | Use when |\n|---|---|---|\n| `--notarize-only` | build | Build succeeded; notarization failed or was interrupted. |\n| `--upload-only` | build, notarize | Notarized `dist/Kaku.app` and `Kaku.dmg` exist on disk; only need to tag + upload + tap. |\n| `--tap-only` | build, notarize, upload | GitHub release exists; only the Homebrew tap dispatch needs to rerun. |\n\nResume flags require the corresponding artifacts in `dist/` to still be present.\n\n## Common blockers\n\n- **notarytool prints `Submission ID received` and then fails**: the submission was created but the DMG upload to Apple's S3 did not finish (there is no `Successfully uploaded file` line), so that submission stays `In Progress` forever and waiting on it is pointless. Once the network is back, resubmit the same artifacts with `./scripts/release.sh --notarize-only`; a good submission came back Accepted in about four minutes (V0.25.0).\n- **Asset upload fails with `HTTP 408: Upload body timed out` or `EOF`**: the proxy cut the long upload; `gh release create` then deletes the new release and keeps the tag. Rerun `env -u HTTPS_PROXY -u HTTP_PROXY -u ALL_PROXY -u https_proxy -u http_proxy -u all_proxy ./scripts/release.sh --upload-only` (gh reads only proxy variables, not the macOS system proxy). Upload failures may be retried up to five times, 30 seconds apart (maintainer 2026-10-20); V0.26.0 went through on the first direct retry even with the route on a TUN interface. Start a full release with the same `env -u` prefix.\n- **`Local main is not synchronized with origin/main`**: push the pending commit. `release.sh` requires the tag to point at a commit that exists on origin.\n- **`rcodesign` not found**: `cargo install apple-codesign`, then verify with `which rcodesign`. Do not try `brew install rcodesign`; it is not a core formula.\n- **`No Developer ID Application certificate found`**: re-import the certificate from the maintainer-approved private backup through Keychain Access, using the password from the private runbook. Never add either location to this file.\n- **`Notarization credentials not found`**: follow the ignored private setup runbook to provision one of the environment/keychain inputs described under Credential prerequisites, then rerun `./scripts/release.sh --dry-run`. The script otherwise prompts interactively, which fails in non-interactive shells.\n- **rcodesign S3 connect timeout**: `s3 upload error: HTTP connect timeout occurred after 3.1s` identifies the failed upload connection, not a credential fault. `notarize.sh` retries once on rcodesign and stops if the network failure persists. Preserve the signed artifact, report the blocked submission, and resume only after the maintainer resolves the network; do not switch to notarytool to retry the same failure.\n- **`No Keychain password item found for profile`**: a profile-name entry alone does not establish a working login. An installed Apple `notarytool` can reuse the existing ASC `.p8` key with `--key`, `--key-id`, and `--issuer`, without creating a Keychain login. Resolve the inputs from the private runbook and keep their values out of displayed commands and logs. For S3 upload failures, apply the same one-retry network budget above and keep the signed artifact fixed. After `Accepted`, retrieve the submission log and verify its SHA-256 against the submitted DMG, then run `./scripts/notarize.sh --staple-only` and `./scripts/nightly.sh --upload-only` from the same source commit. See [Apple's notarization API workflow](https://developer.apple.com/documentation/notaryapi/submitting-software-for-notarization-over-the-web).\n- **Homebrew tap verifier timed out at 12/12**: the tap workflow commits `kakuku <version>` to `tw93/homebrew-tap` main on success; verify with `gh api repos/tw93/homebrew-tap/commits/main --jq .commit.message`. The verifier reads the cask through the GitHub contents API (`Accept: application/vnd.github.raw`), not the Fastly-cached `download_url`. Network errors retry once and stop, and other API errors stop immediately; the 12-poll budget only waits for a version change. A version-poll timeout means the expected version was not observed: open the run URL the script printed. If the tap commit is present the release is complete. Widen the window with `HOMEBREW_TAP_VERIFY_ATTEMPTS` / `HOMEBREW_TAP_VERIFY_SLEEP_SECONDS`, or rerun `./scripts/release.sh --tap-only`; `REQUIRE_HOMEBREW_TAP_UPDATE=0` only downgrades the failure to a warning.\n- **`rcodesign notarization failed` right after the log shows `\"status\": \"Accepted\"`**: Apple accepted; the step died stapling. rcodesign cannot staple an unsigned DMG, which is what `build.sh` produces, and the script used to pass `--staple` (removed 2026-09-10). Do not touch credentials. Recover with `./scripts/notarize.sh --staple-only` then `./scripts/nightly.sh --upload-only`.\n- **`codesign: A timestamp was expected but was not found`** (or `timestamp service is not available`): the timestamp request failed during signing; this message alone does not establish a certificate fault or identify the network cause. `build.sh` retries timestamp failures once before aborting. If that retry fails, stop the release, perform read-only network diagnosis, and report the blocked signing step. Resume only after the maintainer resolves the network problem; do not rerun the build or change proxy settings to retry the same failure.\n- **`codesign: code object is not signed at all, In subcomponent: .../MacOS/k`** while signing `kaku-gui`: `kaku-gui` is the bundle's main executable and codesign validates its siblings as nested code, so every other executable in `Contents/MacOS` must already be signed. `build.sh` signs `kaku` and `k` before `kaku-gui` since 2026-09-10; if a new helper binary is added to the bundle, add it to that loop before `kaku-gui`.\n- **`stapler` fails with CloudKit timeout, exit code 68**: the log reads `The staple and validate action failed! Error 68` right after `Notarization accepted`. Notarization already succeeded; only ticket retrieval from `api.apple-cloudkit.com` timed out. Do not re-notarize. One `./scripts/notarize.sh --staple-only` retry is allowed; if the network failure repeats, stop for read-only diagnosis and wait for the maintainer to resolve it. Only after stapling succeeds, resume with `./scripts/release.sh --upload-only`. The staple-only path regenerates `kaku_for_update.zip` and its sha256 on purpose: attaching the ticket changes the app, so the in-app update archive must be rebuilt after stapling, never before.\n- **Tag already exists on origin at a different SHA**: do not force-push tags. Pick the next patch number, bump versions, and start over.\n\n## Environment variable overrides\n\n| Variable | Default | Purpose |\n|---|---|---|\n| `KAKU_SIGNING_IDENTITY` | auto-detect | Override the Developer ID Application identity. |\n| `KAKU_ASC_API_KEY_PATH` | keychain item `kaku-asc-api-key-path` | Path to rcodesign ASC API key JSON. |\n| `KAKU_NOTARYTOOL_PROFILE` | keychain item `kaku-notarytool-profile` | notarytool keychain profile name. |\n| `HOMEBREW_TAP_TOKEN` | `gh auth token` | GitHub token for tap dispatch. |\n| `REQUIRE_HOMEBREW_TAP_UPDATE` | `1` | Set to `0` to allow release to succeed when tap dispatch fails. |\n| `HOMEBREW_TAP_VERIFY_ATTEMPTS` | `12` | Polls of the tap cask before the verifier gives up. |\n| `HOMEBREW_TAP_VERIFY_SLEEP_SECONDS` | `15` | Seconds between tap cask polls. |\n| `RUN_CLIPPY` | `0` | Set to `1` to run `cargo clippy` during stage:checks. |\n| `SKIP_TESTS` | `0` | Set to `1` to skip `make test` during stage:checks. |\n| `OUT_DIR` | `<repo>/dist` | Override artifact output directory. |\n| `PROFILE` | `release-opt` | Cargo profile. Only `release` and `release-opt` are accepted. |\n| `BUILD_ARCH` | `universal` | Passed through to `build.sh`. |\n\n## After release\n\n- GitHub Release URL: `https://github.com/tw93/Kaku/releases/tag/V<version>`.\n- Official Homebrew cask users get the new version after Homebrew livecheck/bump; verify with `brew update && brew info --cask kaku`. Personal-tap users get it once the tap workflow finishes; verify with `brew info --cask tw93/tap/kakuku`.\n- In-app updates (`kaku-gui/src/update.rs`, Kaku's own updater, not Sparkle) download `kaku_for_update.zip` and its `.sha256` from the latest GitHub Release.\n- Add the six positive reactions to the new release; this is part of shipping. Skip `-1` and `confused`.\n\n  ```bash\n  id=$(gh api repos/tw93/Kaku/releases/tags/V<version> --jq .id)\n  for c in +1 laugh heart hooray rocket eyes; do\n      gh api -X POST \"repos/tw93/Kaku/releases/$id/reactions\" -f content=\"$c\"\n  done\n  ```\n\nFor the announcement post (X / WeChat): community first, 2-4 highlights, user-experience framing, one opinionated sentence. The release notes file is a different artifact; do not paste it as the announcement.\n","term/AGENTS.md":"# Terminal Agent Guide\n\n## Scope\n\n`term/` contains terminal emulation core behavior, including screen state, escape sequence handling, selection, and PTY-facing terminal semantics.\n\n## Where to Look\n\n- `term/src/terminal.rs` - main terminal state and event handling\n- `term/src/terminalstate/` - detailed state transitions\n- `term/src/screen.rs` - screen buffer behavior\n- `term/src/test/` - terminal-core regression tests (`selection.rs`, `csi.rs`, `c0.rs`, `c1.rs`, `core_emulation.rs`)\n- `crates/wezterm-escape-parser/` - escape sequence parsing (CSI/OSC/ESC/APC); `term` consumes it, it does not live under `term/src/`\n- `kaku-gui/src/selection.rs` and `kaku-gui/src/termwindow/selection.rs` - selection and copy behavior (GUI side, not `term`)\n\n## Practical Rules\n\n- Already encoded input uses `send_text`, without paste framing or newline conversion. Local panes share the terminal's existing `ThreadedWriter` with keys and paste, preserving order without blocking on a full PTY.\n- Keep GUI assumptions out of terminal core.\n- Color scheme reporting uses pane-local DEC mode 2031 and DSR 996/997. Notify subscribers after effective configuration palette changes, including same-mode palette changes, and answer subsequent OSC color queries from the new palette. Reloads with an unchanged effective palette and application-issued OSC color writes do not notify; cancellation, RIS and DECSTR clear the subscription. `color::is_light_color` is shared with config/window chrome so the reported scheme stays consistent with Kaku's colors.\n- Keep normal-screen and alternate-screen behavior distinct; wheel scrolling and inline AI status must not corrupt terminal state.\n- Preserve selection and cursor invariants that GUI overlays and inline assistant status depend on.\n- Only the terminal wrap attribute proves that physical rows form one logical line. Full-width output followed by text in column zero can still be independent commands. Keep both indented and unindented hard-newline regressions, alongside soft-wrap coverage, when changing hyperlink grouping (#547).\n- For TUI re-render corruption, reduce the report to an ANSI transcript and cover cursor motion plus erasure in terminal-core tests before changing GUI code. Full-line erase of a physical row must also leave wrap state consistent, especially for styled prompts that previously wrapped.\n\n## Verification\n\n- Compile checks: run `make check`.\n- Terminal logic changes: run `make test` or the narrow cargo nextest filter for the affected behavior.\n- Cross-crate behavior: also inspect `mux/AGENTS.md` and `kaku-gui/AGENTS.md`.\n\n## Cross-References\n\n- `../mux/AGENTS.md` - owns panes and terminal instances.\n- `../kaku-gui/AGENTS.md` - renders terminal surfaces.\n- `../termwiz/AGENTS.md` - terminal UI primitives.\n","lua-api-crates/AGENTS.md":"# Lua API Crates Agent Guide\n\n`lua-api-crates/` contains Rust-to-Lua API bindings for user configuration scripts.\n\n## Scope\n\n`lua-api-crates/` exposes runtime capabilities to Lua, including:\n- mux and pane controls\n- window and spawn helpers\n- ssh and system integration helpers\n- utility modules like logging, time, and filesystem\n\n## Where to Look\n\n- `lua-api-crates/mux/`: pane/tab control APIs\n- `lua-api-crates/window-funcs/`: window-level APIs\n- `lua-api-crates/spawn-funcs/`: process spawn APIs\n- `lua-api-crates/ssh-funcs/`: SSH-related Lua APIs\n\n## Practical Rules\n\n- API behavior here is user-facing and breaking-change sensitive.\n- Be careful with user-var and event-facing APIs, since GUI hooks and config reload signals rely on those event paths.\n- Keep proxy, config reload, and user-var event semantics stable for AI and automation workflows that observe Lua-facing state.\n- `PaneInformation` (used in title-formatting events) is fields-only userdata: method-style calls and `if pane.get_xxx` probes silently no-op instead of erroring. See \"Bundled kaku.lua Pitfalls\" in [`config/AGENTS.md`](../config/AGENTS.md) before extending it or its consumers.\n\n## Cross-References\n\n- [`mux/AGENTS.md`](../mux/AGENTS.md) - Mux controls exposed to Lua.\n- [`config/AGENTS.md`](../config/AGENTS.md) - Lua config execution environment.\n- [`kaku-gui/AGENTS.md`](../kaku-gui/AGENTS.md) - Window functions and GUI integration.\n","mux/AGENTS.md":"# Mux Agent Guide\n\nThe `mux` crate owns tab, pane, domain, and mux state management.\n\n## Scope\n\n`mux` is the central abstraction for:\n- tabs and panes\n- windows and workspaces\n- local and remote domains\n- client/server mux coordination\n\n## Where to Look\n\n- `mux/src/tab.rs`: tab tree and split behavior\n- `mux/src/pane.rs`: pane state and interfaces\n- `mux/src/window.rs`: window-level mux state\n- `mux/src/domain.rs`: domain abstractions\n- `mux/src/client.rs` and `crates/wezterm-mux-server-impl/src/` (`sessionhandler.rs`, `dispatch.rs`, `local.rs`): protocol flow\n\n## Practical Rules\n\n- Already encoded input uses `send_text`, without paste framing or newline conversion. Local panes share the terminal's existing `ThreadedWriter` with keys and paste, preserving order without blocking on a full PTY.\n- A foreground process group can outlive its leader (`seq | less -X`). When the leader has no executable path, identify a live member of that same group from the cached shell tree. A foreground-group change, exec or leader exit invalidates the leader immediately and the fallback tree must be sampled after that leader update; checking only PGID misses a producer that exits after being cached. Do not wait for the normal TTL, substitute an unrelated background job, or scan all processes on every frame. Release the tree cache guard before calling Lua hooks that can query the pane again.\n- Keep alert propagation reliable, including `Alert::SetUserVar`, because GUI reload and user-var hooks depend on this path.\n- Preserve pane and overlay invalidation signals consumed by GUI AI/chat overlays when panes split, move, close, or resize.\n\n## Known Pitfalls\n\n- **Alert propagation**: `Alert::SetUserVar` must reach the GUI side reliably; broken propagation silently breaks config reload and user-var event hooks.\n- **Pane ID stability**: Pane IDs must remain stable across mux round-trips; don't reassign or reuse IDs in ways that confuse client/server sync.\n- **Domain abstraction leakage**: GUI-specific logic (rendering, window handles) must not appear in `mux`; only mux-level state and events.\n- **Split tree correctness**: Tab split tree mutations in `tab.rs` are tricky; always verify with a pane layout sanity check after modifications.\n- **Overlay resize regressions**: GUI overlays derive geometry from pane state. Mux changes that skip resize or removal notifications can leave AI/chat overlays stale.\n\n## Cross-References\n\n- [`kaku-gui/AGENTS.md`](../kaku-gui/AGENTS.md) - GUI rendering for mux state.\n- [`lua-api-crates/AGENTS.md`](../lua-api-crates/AGENTS.md) - Lua APIs for mux control.\n- [`term/AGENTS.md`](../term/AGENTS.md) - Terminal instances owned by panes.\n",".claude/rules/macos.md":"# Kaku macOS Platform Rules\n\n> macOS 26 上 AppKit 自动注入是 PAC-fault / 崩溃的高频来源。改 menubar / window handling 前必读本文。\n\n## 两套像素坐标空间，禁止混算\n\n窗口/屏幕几何存在两套空间，只在\"窗口所在屏 scale == 主屏 scale\"时数值兼容，混用的 bug 只在外接屏暴露（#456 拖动跳变）：\n\n- **Canonical ScreenPoint 空间**：`cartesian_to_screen_point` 定义（全局 point × `screens[0]` 主屏 scale、Y 翻转）。`set_window_position` / `window_position` / `MouseEvent::screen_coords` / `MouseEvent::window_origin` 都在这里。\n- **窗口 backing 空间**：`MouseEvent::coords`、`dimensions.pixel_*`、UI item 布局坐标，用窗口自己屏幕的 backingScaleFactor。\n\n规则：跨空间换算一律在 `window/src/os/macos` 层做；GUI 层需要窗口原点用 `MouseEvent::window_origin`，不要用 `screen_coords - coords` 推算；居中走 `WindowOps::center()`（原生、单屏 point 空间）。`ScreenInfo.rect`（`screens()` / `resolve_geometry` / `--position` / Lua `screens()`）已统一到 canonical 空间（`nsscreen_to_screen_info` 走 `cartesian_to_screen_point`），`Window::new_window`（`window/src/os/macos/window.rs`）不再对 resolve 出的 x/y 除 scale（除一次就是 upstream 继承的 Retina 落点折半 bug，size 的除法保留，那是 px 转 pt 给 NSWindow 用的）。\n\n## 默认渲染后端是 WebGpu，CGL-only 的 wake 修复护不住它\n\nbundled `kaku.lua` 设了 `front_end = 'WebGpu'`（Metal via wgpu）。`window/src/os/macos` 里的睡眠/唤醒防护（`SYSTEM_SLEEPING` flush 闸门、`BackendImpl::update`、display-change present defer）全部只作用于 CGL 路径，对默认用户是 no-op。给 sleep/wake/display-reconfig 加修复时必须同时考虑 WebGpu 路径（#458）。\n\nWebGpu surface 失效的恢复入口在 `TermWindow::do_paint_webgpu`：休眠唤醒会让 Metal drawable 失效但窗口尺寸不变，`WebGpuState::resize` 对相同尺寸是故意 no-op（热路径），所以恢复必须走 `reconfigure_surface()` 强制 `surface.configure`，不能用 `resize(dims)` 代替。wgpu Metal 后端把 nil `nextDrawable` 映射成 `SurfaceError::Timeout`，所以 Timeout 连续出现同样要触发重配，不能永远静默跳帧。症状签名：键盘输入照常进 PTY、画面停在旧帧，说明是渲染管线死了而不是主线程卡死。\n\n## AppKit Menu 自动注入是地雷\n\nNSApplication 默认会向 Window menu 和 Help menu 注入自己的项（Tile / Move & Resize / Bring All to Front / Help Search），并把 NSWindowRepresentingMenuItem 挂在 Windows menu 下做 dangling reference 。在 macOS 26 上，这些注入项可能 PAC-fault：\n\n- `-[NSMenu _performKeyEquivalentWithDelegate:]` 在 Ctrl+Cmd+F 路由时崩\n- Windows menu 关闭窗口后 NSWindowRepresentingMenuItem dangling\n- Help Search field 触发 menu 重新计算时崩\n\n## 必须执行的兜底\n\n- 不要调 `setWindowsMenu:` 让 AppKit 接管 Windows menu。Kaku 有自己的 tab/window 切换器。\n- 不要调 `setHelpMenu:` 让 AppKit 注入 Help Search。Kaku 没有 help book。\n- 窗口创建时调 `setExcludedFromWindowsMenu:` 让 AppKit 看不见我们的 NSWindow，避免它把窗口加到 Windows menu。\n\n参考历史 commit：\n- `1ec3823` - 第一次排查 NSWindowRepresentingMenuItem dangling\n- `4b15bab` - 进一步 stop AppKit from injecting into Window/Help menus\n- `fc2dfa9` - redirect AppKit Spotlight for Help to an orphan menu\n- `6ba7381` - restore synchronous menubar init to prevent key event loss\n\n## Menubar 初始化时序\n\nMenubar **必须同步初始化**。延后到 async block 会丢早期按键事件（Cmd+Q 在启动后立刻按下会被吞）。见 `6ba7381`。\n\n## Key Equivalent\n\n新增 KeyAssignment 前确认走 Kaku 自己的按键分发路径，不要走 AppKit menu 的 `performKeyEquivalent`。后者会经过被 PAC-fault 的注入项。\n\n### menu keyEquivalent 会拦 keyDown，吞掉 raw-mode TUI 的 Ctrl+letter\n\nmacOS 26 上 NSMenu 的 keyEquivalent modifier 匹配并不严格相等。给 menu item 装 `Ctrl+letter` 形式的快捷键，即使带额外 modifier（如 `NSFunctionKeyMask` 想做 `Fn+Ctrl+letter`），仍会拦截 plain `Ctrl+letter` 的 keyDown。后果：\n\n- AppKit 把 keyDown 路由给 menu item action，NSWindow 收不到。\n- keyUp 不走 `performKeyEquivalent:`，所以 keyUp 照常进 Kaku。\n- `termwiz/src/input.rs` 的 `encode` 对 `is_down=false` 直接返回空字符串，PTY 上一个字节都不会到。\n- 普通 shell `cat -v` 测试看到 `^C` 不代表 raw-mode 也工作；启动时机和 cooked vs raw 都会让 menu 拦截表现不一致。**必须**在 raw-mode TUI (claude / codex / vim / htop) 里实测。\n\n兜底：任何\"模仿系统快捷键\"的 menu 项宁可 `keys: vec![]` 不设快捷键，让用户从菜单点。`kaku-gui/src/commands.rs` 里的 keyEquivalent 装配逻辑必须统一从按键候选本身取 modifiers，不要为个别 menu 项走强制 modifier 的特例化路径（历史上的特例化路径已删，不要加回）。\n\n参考历史：`8aba4475` feat(window): add CenterWindow menu without macOS shortcut（Window > Center 刻意不装 keyEquivalent，因为 macOS 26 会把 `Fn+Ctrl+C` 当 plain Ctrl+C 拦了，导致 raw-mode 下 Ctrl+C 不退 claude/codex）。\n\n### 排查 keyDown 丢失\n\n`config.debug_key_events = true` 重启 Kaku，复现按键，看 `~/.local/share/kaku/kaku-gui-log-<pid>.txt`：\n\n- grep `key_event.*CTRL`。\n- 如果只有 `key_is_down: false`、没有 `key_is_down: true`，就是 menu keyEquivalent 拦了 keyDown。\n- 这种情况下不要去查 termwiz / PTY / termios，先找 `set_key_equiv_modifier_mask` 装配点。\n\n## Cocoa 对象所有权：StrongPtr::new 只接 alloc / new / copy\n\n`StrongPtr::new` 接管的是 +1 引用，只能包 `alloc`+`init`、`new`、`copy` 拿到的对象。`+[NSMenuItem separatorItem]` 这类类方法返回的是 autorelease 的 +0 对象，必须用 `StrongPtr::retain`，否则 drop 时多 release 一次。V0.22.0 前 `MenuItem::new_separator()` 就是这么写的：主菜单栏在启动时构建，那时还没有 autorelease pool（从代码推断，未实测），多出的那次 release 被泄漏抵消，所以一直没暴露；第一个在事件处理里构建、带分隔线的菜单（右键 tab 菜单）一弹出就崩（`7b6b6ce2`，带回归测试）。\n\n- 崩溃签名：主线程 `objc_release > AutoreleasePoolPage::releaseUntil > objc_autoreleasePoolPop > -[NSApplication run]`，紧跟在一次原生菜单动作之后。看到它先审 `StrongPtr::new` 包的是不是非 alloc/new/copy 来的对象，再怀疑 AppKit。\n- 拿不准某个 API 返回 +0 还是 +1，用 `clang -fno-objc-arc` 写个十行的 MRC 程序在 `@autoreleasepool` 里打印 `retainCount`，实测，不靠记忆。\n- 新增在事件处理中构建的 `NSMenu`（右键菜单等），必须在 `make app` 的包里真的右键点一次菜单项再下结论，单元测试看不到 autorelease pool 的时序。\n- 配置重载后菜单栏在 macOS 上也会重建（#570）：`frontend.rs` 的 reload 回调在 `spawn_into_main_thread` 任务里调 `recreate_menubar`，不在可能持有 config 锁的回调里直接调。原先挡住它的 TODO 疑似就是上面这个分隔线多释放，修复后放开。外观切换也会触发 `config::reload()`，所以这条重建路径在每次深浅色切换时都会走。`recreate_menubar` 给 Kaku 加的每一项打 `KAKU_MENU_ITEM_TAG`，重建时只删带这个 tag 的项、保留顶层菜单对象，再从每个菜单的开头按 rank 插入新项。不要改成 `remove_all_items` 清空主菜单或换新的 NSMenu：AppKit 只在启动时往 Edit 菜单注入 AutoFill、Start Dictation、Emoji & Symbols，菜单对象一换这些项就再也回不来，再调一次 `setMainMenu:` 也不行（MRC 小程序实测）。也不要换回旧的 mark-sweep：它只扫 `kakuPerformKeyAssignment:` 项，Kaku 菜单的固定项只在子菜单新建时创建、分隔线每次都追加，复用旧菜单会丢固定项、分隔线越积越多、新项排到末尾。`config_reload_rebuilds_menubar_on_macos_in_place` 守这几点。随 mux 状态变化的命令（Attach/Detach Domain、非 local 的 New Tab (Domain X)、Switch to workspace）只进命令面板不进菜单栏：菜单栏只在配置重载时重建，启动时 domain 还没注册，放进去会在一次深浅色切换后突然多出 `~/.ssh/config` 每个 Host 两条 Attach 项，之后又随 attach 状态过时，`mux_state_commands_stay_out_of_the_menubar` 守这一点。\n\n## 调试方式\n\n- 怀疑 menu 注入或菜单对象相关崩溃时：读 `~/Library/Logs/DiagnosticReports/kaku-gui-*.ips` 里最近的 crash report。\n- 看 `NSMenu _performKeyEquivalentWithDelegate:` 栈帧是 AppKit 注入项的标志。\n- 主要复现环境是一台 macOS 26 实机。\n","termwiz/AGENTS.md":"# Termwiz Agent Guide\n\n## Scope\n\n`termwiz/` contains terminal UI primitives, widgets, input handling helpers, and rendering utilities used by Kaku command-line and overlay flows.\n\n## Where to Look\n\n- `termwiz/src/` - primary library code\n- `termwiz/src/widgets/` - reusable widgets\n- `termwiz/src/lineedit/` - line editing behavior\n- `termwiz/src/render/` - rendering helpers\n\n## Practical Rules\n\n- Keep primitives generic; do not leak Kaku-specific policy into reusable widgets.\n\n## Verification\n\n- Compile checks: run `make check`.\n- UI primitive logic changes: run the narrow affected tests, then `make test` if behavior is shared.\n- Overlay or GUI consumers: inspect `kaku-gui/AGENTS.md` and verify the consuming flow.\n\n## Cross-References\n\n- `../kaku-gui/AGENTS.md` - GUI and overlay consumers.\n- `../term/AGENTS.md` - terminal core behavior.\n"},"files":{"kaku-gui/AGENTS.md":"# Kaku GUI Agent Guide\n\n`kaku-gui` owns rendering, window lifecycle, user interaction, AI chat, and the `k` helper binary.\n\n## Scope\n\n`kaku-gui` is responsible for:\n- terminal window rendering\n- input and mouse handling\n- tab/pane UI behavior\n- startup lifecycle and app event flow\n- AI chat overlays, approval UI, inline status, syntax highlighting, and assistant transport\n- the `k` helper binary under `src/bin/k.rs`\n\n## Where to Look\n\n- `src/termwindow/mod.rs`: core window state and action dispatch\n- `src/termwindow/render/`: rendering pipeline\n- `src/termwindow/mouseevent.rs`: mouse and drag behavior\n- `src/termwindow/webgpu.rs`: surface resize and present mode handling\n- `src/frontend.rs`: app lifecycle and macOS integration events\n- `src/overlay/`: launcher and overlays\n- `src/overlay/ai_chat/`: AI chat overlay, markdown rendering, syntax highlighting, and Waza integration\n- `src/ai_chat_engine/`: AI chat engine, approval, and compaction flow\n- `src/ai_client.rs`, `src/ai_remote.rs`, `src/ai_tools/`: provider and tool integration. Keep tool-name strings stable so persisted conversations replay correctly.\n- `src/ai_tools/paths.rs`: sandbox and sensitive-path guards.\n- `src/ai_tools/fs.rs`: read, list, write, patch, mkdir, and delete tools.\n- `src/ai_tools/shell.rs`: exec, background execution, and polling.\n- `src/ai_tools/web.rs`: search, fetch, and URL reading.\n- `src/ai_tools/search.rs`: grep and symbol search.\n- `src/ai_tools/project.rs`: project summaries and file tree helpers.\n- `src/ai_tools/soul.rs`: memory and soul reads.\n- `src/ai_tools/registry.rs`: `ToolDef`, `all_tools`, and `to_api_schema`.\n- `src/cli_chat/`: command-line chat flow\n- `src/thread_util.rs`: macOS worker thread lifetime helpers\n- `src/tabbar.rs`: tab bar behavior\n\n## Practical Rules\n\n- Keep rendering decisions in rendering modules, not scattered across event code.\n- Paint-time Kaku TUI detection must reject non-Kaku foreground executables using the cached leader name before requesting the full process tree. Keep the executable and argument confirmation for `kaku ai` and `kaku config`; cover pane, window and tab-bar consumers together (#583).\n- Keep initial window dimension math in `TermWindow::new_window()` consistent with `get_os_border()` behavior.\n- In-app update must confirm before replacing the running app. User-facing paths (menu Check for Updates, menu Restart to Update, toast click) go through `check_for_updates_from_menu()` / `confirm_and_apply_update()` in `src/frontend.rs`, not straight to `restart_to_update()`. Exploratory menu check opens a tab without `KAKU_UPDATE_AUTO_CONFIRM`; only the overlay-confirmed path may auto-confirm. CLI direct and brew paths confirm in `kaku/src/update.rs`. Guards: `frontend::tests::user_facing_update_events_route_through_confirm` and `update::imp::tests::brew_and_direct_paths_confirm_before_replace`.\n- Keep `KAKU_CONFIG_CHANGED` fast path in `emit_user_var_event()` before pane ownership checks.\n- Pass fullscreen state into `TabBarState::new()` and preserve fullscreen-specific title button spacing behavior.\n- Keep AI overlay state changes cheap enough for live terminal interaction; avoid blocking render/input loops on provider calls.\n- Preserve approval prompts and inline AI status placement when touching AI chat or shell flows.\n- Chat permission summaries retain the complete target and command, with terminal controls escaped. The native review pane scrolls through all details and refuses approval when the pane is too small to read. Notifications may shorten their preview; the approval payload must not. Chat rendering, picking and mouse hit-testing share `msg_area_height`, `picker_height` and `visible_start`. Preserve a scrolled reader's position as output grows. Code Copy uses the original block, never wrapped display rows; Insert only pastes a completed, control-free single shell line into the original shell pane and never sends Enter.\n- AI HTTP transport must preserve both sides of proxy behavior: external API hosts should honor detected system proxy settings, while loopback, private LAN, link-local, CGNAT/Tailscale-style, `.local`, `NO_PROXY`, and macOS ExceptionsList model endpoints should bypass the proxy and connect directly. Do not describe this as full SOCKS support unless reqwest SOCKS support is actually enabled and smoke-tested. Exception: the `http_request` tool always connects directly to the public IP addresses resolved and validated before approval, for both HTTP and HTTPS. It must never pass that hostname to a proxy for independent resolution; users on proxy-only networks receive a connection error instead of weakening the SSRF boundary.\n- Wrap macOS worker thread spawns in the existing autorelease-pool helper so shutdown does not reintroduce use-after-free crashes.\n\n- Remote pane operations in `src/remote_tabs.rs` retain a stable `PaneId`. Do not send a remote split through `spawn_command_impl`, which resolves the active pane again after queuing. Browser-confirmed closes must bypass `AlwaysPrompt` without switching to another pane, and preserve the last-pane tab restore bookkeeping. The isolated remote live smoke covers both paths.\n\n## Known Pitfalls\n\n- During window drag, terminal move/wheel events may need suppression.\n- A tab drag belongs to one press/release gesture. A new press or motion without the left button exclusively held cancels stale state, including when AppKit swallowed the previous release. Preserve the ordinary release handler for settle animation. For unexplained tab reordering, `debug_key_events` records tab press geometry, motion and reorder decisions without terminal text (#584).\n- WebGPU surface reconfigure should happen only on meaningful state changes.\n- Finder \"open with\" behavior is handled in `frontend.rs` event flow.\n- Option+Click cursor movement fires only when: no mouse grab, no alt screen, click is on the same row as the cursor. It sends Left/Right arrow sequences proportional to the column delta; see `mouseevent.rs`.\n- Block cursor height uses `natural_cell_height` (ignoring `line_height`) so Nvim visual selections match WezTerm proportions.\n- Top border clearance for macOS integrated buttons is state-sensitive:\n  - top tab bar visible -> add small gap\n  - top tab bar hidden or bottom tab bar -> add larger clearance for traffic lights\n- Overlay geometry must update when panes are split, resized, closed, or moved.\n- Alternate-screen wheel behavior is config-controlled; do not hard-code one terminal behavior across normal and alternate screens.\n- Inline `#` AI query status belongs below the buffer, not as a toast, so terminal content remains stable.\n- `fast_model` and proxy config are part of the AI UX contract; verify both when changing provider selection or transport.\n- Per-pane overlays (AI chat) render in place of their underlying pane and live in `pane_state`, not the tab's pane tree. They have no window handle, so their output only repaints the window through `PaneOutput` -> `mux_pane_output_event` -> `is_pane_visible` -> `window.invalidate()`. `is_pane_visible` (`termwindow/mod.rs`) MUST treat an active per-pane overlay pane as visible, and the overlay's `render()` MUST `term.flush()`, or streamed output and the loading spinner freeze until an input event forces a repaint. Symptom-to-cause: if `top` refreshes on its own but the AI chat needs a click, the break is one of these two.\n- Scrollback viewport pruning has two policies in `termwindow/mod.rs`: passive pruning during output uses `normalize_viewport` and snaps to the bottom (`None`), while explicit user-driven scroll requests use `normalize_interactive_viewport` and clamp to `scrollback_top`. During a selection drag, only the pane named by `MouseCapture::TerminalPane` may pin a pruned viewport; sibling panes must retain the passive #448 behavior.\n- Tab bar rendering and the tab rename editor must read `resolved_palette.tab_bar`, which includes the selected color scheme and user overrides. Reading `config.colors.tab_bar` alone ignores scheme colors and makes Fancy tabs unreadable in light themes.\n- Tab bar sizing and hit-testing have pure seams in `src/tabbar.rs`: `tab_width_budget` (per-tab column budget that drives truncation) and `is_tab_hover` (clickable span). The truncation and position regressions (#439/#443/#445) came from layout logic with no test seam, so when you change tab sizing, truncation, or click regions, extend their unit tests instead of only eyeballing the running app. `tab_width_budget` locks the invariant that a tab is never squeezed below 1 column.\n- Launcher entries that target a pane must retain its stable `PaneId`, not a tab or topological pane index. The launcher snapshot can outlive pane exits and split-tree reindexing; resolve the `PaneId` against the live mux only after the overlay closes.\n- Session snapshots skip a pane's content while it is in the alternate screen (`f4aca502`), so a vim or htop screen never lands in restored history. Claude Code and Codex are the exception (`foreground_is_agent_cli` in `tabbar.rs`): their last screen is kept, since it is the conversation. Restore never re-runs the program.\n- A partially failed startup session restore must preserve the original session envelope and scrollback sidecars. Only mark a snapshot consumed after every saved window restores successfully; otherwise exit-time saving can overwrite the only complete copy.\n- Session sidecars compact vector-backed lines before checking visible images, since wide-character continuation cells can retain hidden Kitty placements. Strip remaining images and recompress before encoding. Menu restore guards preserve prior dirty state on failure and carry successful changes through overlapping restores; startup-only restores remain pristine. Exit and update checkpoints capture cwd/output even without structural changes, only in the session-owning process and never during an incomplete restore. SIGTERM requests main-thread event-loop shutdown, then uses the normal exit checkpoint; its listener remains registered through repeated TERM signals. Register it only after existing-instance handoff, never in CLI-only commands (#579).\n- The terminal grid's top origin has one source: `TermWindow::terminal_first_row_offset()` (OS top inset plus a shown top tab bar). Pane backgrounds, split lines and mouse row mapping take it from there so they match where text is drawn; a background that skipped the 6px inset under a top tab bar sat above its split line (#562). A source guard in `render/split.rs` fails if `pane.rs`, `split.rs` or `mouseevent.rs` computes its own. `update_text_cursor` (IME candidate rect) still uses a separate origin and sits `border.top` high; that is a known, unfixed gap.\n- Window padding (2026-10-18): with managed padding (no `config.window_padding` in the user's file) every vertical gap derives from the top padding S in `effective_vertical_padding_with_policy`: content keeps S below window chrome and S/2 from the bottom edge or a visible tab bar, top or bottom, fullscreen included; only a maximized window without a bottom bar runs content to the bottom edge. Rows rarely fill the height, and `padding_left_top` splits that leftover evenly between top and bottom (`split_row_slack`, inside the OS borders), so the gap to the bar no longer jumps between 1pt and a full row. Every consumer (pane rendering, split lines, mouse hit testing) reads `padding_left_top`; do not add a second slack policy in `resize.rs` (the old rebalance there only ever reached the resize increments). The tab bar itself stays one terminal row. Explicit user padding is used as written. Hotkey windows are also excluded from `split_row_slack`: their drawer policy owns the top inset, including an explicit zero. Cover hotkey, ordinary and fullscreen states when changing slack allocation.\n- Split geometry follows mux: `split.top = first.rows + gutter / 2` and the pane backgrounds meet at `first.rows + gutter / 2.0`. With the bundled even row gutter (`split_pane_gap = 2`) the line belongs on the top edge of row `split.top`, with an odd gutter on its middle; `split_row_center_offset` places it and `split_drag_row` measures drags against the drawn line so a grab does not jump a row. Left|right splits always have an odd column gutter. In opaque windows the outer panes' backgrounds fill the right and bottom padding (transparent windows leave those strips to the fill strips in `paint.rs`). With `inactive_pane_hsb` dimming on, the split line takes the active pane's background, so the dimming marks focus and the line only separates two dimmed neighbours; do not remove the line, three-pane layouts need it.\n- Touch mode (`termwindow/touch.rs`) is per-window UI state, not a persisted config override. It changes nothing but a drawn close button on each retro tab: tab bar height, desktop tab metrics and terminal font/line height stay the same. Long press works in every mode: `track_long_press` follows a plain left press and, on a release at least 450ms later within 24pt of the press (8pt cancelled ordinary presses over UU) (motion and the release point are both checked), replays it as a right click after the event finishes (on release, so AppKit menu tracking never sees the lift as a pick). The menu it opens draws its items at 18pt (`LONG_PRESS_MENU_FONT_PT`); NSMenu row height barely follows the font, so this is as large as a native menu gets without custom item views, while a mouse right click keeps the system size. A modal clears the pending press, so dismissing the rename editor never opens a menu, and a press that reached a tab through the bottom bar touch band aims its right click at that tab. A press over a pane that grabs the mouse is never converted unless it is on a tab, and the original press and release are handled as usual.\n- The bottom tab bar touch band (`BOTTOM_TAB_BAR_TOUCH_EXTENSION_PT` in `mouseevent.rs`) accepts only a plain left press (`press_takes_touch_extension`) and then owns that gesture's motion and release (`touch_band_press`). A middle press there is a paste into the last row and must never reach the tab bar's middle-click close. Known trade-off: a left click in the lower 12pt of the last row over a tab goes to the tab even when a TUI (tmux status line, htop) grabs the mouse.\n- Agent title (2026-10-18): while Claude Code, Codex or Kimi runs in the active pane (`foreground_agent_cli` in `src/tabbar.rs`), `src/termwindow/render/agent_title.rs` draws the agent's mark and the topic from its OSC 0/2 title at the top right: centered on the traffic-light line in the integrated-buttons strip, or 16pt below the top edge when fullscreen (the same distance as in a window, lifted only to stay above the first row), never under a top tab bar. `show_agent_title = false` (Agent Title in `kaku config`) turns it off; the default is on. Spinner glyphs and a leading agent name are stripped so the text stays still. Strip a name only at a complete boundary (end, whitespace, colon, pipe or middle dot), preserving topics such as `Codexification` and `Claude Codebase`; do not retry a shorter name after a longer partial match. The tab title is untouched on purpose: a tab is too narrow and would change width with every update. The marks in `src/termwindow/render/agent_marks.rs` are generated from Lobe Icons SVGs by `scripts/svg_to_poly.py`; regenerate rather than hand-edit. The remote page shows the same title: `tabbar.rs` publishes each tab's agent and topic through `kaku_remote::publish_agent_title` (honoring `show_agent_title`), and `AGENT_MARKS` in `crates/kaku-remote/assets/web/index.html` holds the same paths as SVG data on the 120 grid, so a regenerated mark goes into both files.\n- Tests that call `config::set_config_file_override` and `config::reload()` can race the config file watcher: `reload()` returns early when another reload starts, leaving the previous config. Write the config as few times as possible inside one test (loop the cheap cases inside one write), and do not split such a test into siblings that run in parallel.\n\n## Cross-References\n\n- [`mux/AGENTS.md`](../mux/AGENTS.md) - Tab and pane abstractions consumed by GUI.\n- [`termwiz/AGENTS.md`](../termwiz/AGENTS.md) - TUI primitives used in overlays.\n- [`config/AGENTS.md`](../config/AGENTS.md) - Config loading and reload signals.\n","AGENTS.md":"# Kaku Agent Guide\n\nKaku is a macOS-native terminal emulator derived from WezTerm and shaped around AI-assisted terminal workflows. This guide is the shared operating context for agents working in this repository.\n\n## Repository Map\n\n- `kaku/` - CLI entry points, command flows, and user-facing configuration commands.\n- `kaku-gui/` - GUI, rendering, window lifecycle, input, mouse handling, AI chat, and the `k` helper binary.\n- `mux/` - tabs, panes, domains, and client/server state.\n- `term/` - terminal emulation and screen buffer behavior.\n- `termwiz/` - terminal UI primitives.\n- `config/` - Lua config loading, schema behavior, proxy settings, and versioned defaults.\n- `window/` - platform windowing layer.\n- `lua-api-crates/` - Rust-to-Lua API bindings.\n- `crates/` - shared utility crates, including Kaku-specific AI helpers.\n- `assets/` - app resources, bundled config, shell integration, and vendor assets.\n- `scripts/` - build, release, and validation helpers.\n- `docs/` - user and developer documentation.\n- `.agents/skills/` - the canonical and tracked home for project skills: `release` and `bugs`. The website skill `product-docs` lives with the site on the `vercel` worktree (`~/www/kaku-site/.agents/skills/product-docs`). `.claude/` is gitignored except for the force-added `.claude/rules/macos.md`; its skill entries may be relative symlinks or ignored private overrides. Do not cite, copy, or edit an ignored `.claude/skills/` directory as project truth. Put durable skill changes in `.agents/skills/` first, then update a symlink when that entry is actually a link.\n- `.github/workflows/checks.yml` - fast correctness gates on pushes to `main` and on pull requests, with `paths-ignore` for `**.md` and assets, so a docs-only change gets no CI at all. Five macOS jobs, the runner's concurrency cap: Format, Unit Tests, Relay Build, Clippy (which also carries the source guards and stands in for `cargo check`, scoped to the lint-opt-in crates, not the workspace), and Scripts and Release Checks (release notes, config version, shell smoke tests, shellcheck). Adding a sixth macOS job makes one queue.\n- `.github/workflows/build-validation.yml` - release-shaped builds with the shipped `release-opt` profile: one job per architecture in parallel (the arm64 one also assembles the bundle), then a `lipo` merge of `kaku`, `kaku-gui` and `k`; runs on build-pipeline changes, daily (skipped when that commit already passed), or on dispatch. `release.sh` preflight requires its latest run green and the latest Security Audit run not failed, and skips its local fmt/check/test when CI Checks already passed on HEAD.\n- `.github/RELEASE_NOTES.md` - source for the GitHub Release title and body.\n\n## Commands\n\n```bash\nmake fmt\nmake fmt-check\nmake check\nmake test\nmake dev\nmake app\n./scripts/build.sh\n./scripts/check_config_release_readiness.sh\n./scripts/check_release_config.sh\n./scripts/check_release_notes.sh\n```\n\n`make build`, `make check`, `make test` and `scripts/build.sh` first run `scripts/prune_target.sh`, which measures `target/` at most once a day and runs `cargo clean` once it reaches `KAKU_TARGET_CAP_GB` (default 40). cargo never drops old artifacts, and the profiles, architectures and remote on/off feature sets here grew it to 92G; a partial, age-based delete was rejected because it can leave fingerprints pointing at missing files. No scheduled job does this, by the maintainer's choice. The dev profile also builds third-party crates without debug info.\n\n`make fmt` and `make fmt-check` both shell out to `cargo +nightly fmt`, so they require the nightly toolchain. Use `make app` for GUI, rendering, windowing, and AI overlay verification because it builds the app bundle that users run.\n\n## Working Rules\n\n- Work on the current branch unless the maintainer asks for a branch or worktree.\n- Keep changes inside one crate or subsystem when the problem allows it.\n- Upstream: Kaku forked WezTerm at `05343b38` (2026-01-17; term/, mux/, the escape parser and wezterm-font matched it byte for byte). On 2026-10-14 upstream main up to `e2a98e1b` was reviewed and its macOS-relevant fixes absorbed (commits citing upstream shas, e.g. DECRQCRA, RIS reset, OSC 8 `;`, tmux -CC input, workspace repaint). Start the next sync from `e2a98e1b`; map `wezterm-gui` to `kaku-gui` and top-level crates to `crates/`, and judge applicability rather than applying patches literally.\n- Kaku is macOS-only. Windows code (cfg(windows) branches, ConPTY/GDI/WSL paths, winapi and the other Windows-only dependencies) was removed on 2026-10-13; do not bring it back when porting upstream WezTerm fixes, port only the macOS-relevant half. `wsl_domains`, `win32_system_backdrop` and `win32_acrylic_accent_color` stay as deprecated config keys so shared configs still load (`windows_only_keys_still_load`). Vendored crates under `deps/` keep their upstream Windows code untouched.\n- Inspect public APIs and cross-crate boundaries before changing shared behavior.\n- Kaku must not automatically inject agent hooks, instructions, or settings. Explicit edits through `kaku ai` may update the selected provider's configuration (including Claude and Codex settings), preserving user-owned fields. Agent-facing help belongs in documentation. The `kaku init` source line in `.zshrc` is managed by `assets/shell-integration/setup_zsh.sh` and checked by `doctor.rs`; `reset.rs` must keep every user-owned line around it.\n- Narrowing a default that existing users already depend on must ship the other camp's explicit path in the same change. Shell integration scopes the Starship prompt to `TERM_PROGRAM=Kaku` OR (`$TMUX` and `KAKU_SESSION`), with `KAKU_PROMPT_EVERYWHERE=1` as the opt-in for every terminal; Smart Tab stays Kaku-only (#503). The predicate is duplicated inside each installer, three times in `setup_zsh.sh` and four in `setup_fish.sh`, so grep both files and change every copy. `assets/shell-integration/tests/prompt_scope_smoke.sh` gates the placements in CI via `.github/workflows/checks.yml`, and `docs/faq.md` is the user-facing half; a change that re-narrows the scope must update both installers, the smoke and the doc together.\n- Tab indicator decision (2026-08-24): the tab bar draws a dot only for `Progress::Paused | Progress::Error` (attention, `kaku-gui/src/tabbar.rs`); a running pane deliberately shows nothing, the running-state dot was dropped in `4f297556`. Do not bring it back.\n- Retro tab bar chrome (2026-09-09, #548): the bundled config (`assets/macos/Kaku.app/Contents/Resources/kaku.lua`) sets `use_fancy_tab_bar = false`, so the shipped bar is the retro one even though the `config/src/config.rs` schema default is true. `kaku-gui/src/tabbar.rs` composes a `Line` of cells, so a control there is a character on the text baseline competing with the title for the one trailing cell the rule above spends on attention; drawn controls belong in the box-model renderers (`make_x_button` in `kaku-gui/src/termwindow/render/fancy_tab_bar.rs`, and the opt-in touch renderer in `kaku-gui/src/termwindow/touch.rs`). The per-tab close mark added in `cef3a371` (three cells, \" X \", over the title end) was reverted in `732d2082` for that reason. On 2026-10-12 it was tried again as a single hover `×` in the status cell, with one and then two cells of padding per side; the maintainer judged it worse than none (a grid glyph cannot sit centered between title and edge) and removed it: the desktop layout closes tabs from the tab's right-click menu, middle-click and Cmd+W, while the web terminal keeps its hover close icon, which is drawn and centered. Answer tab bar chrome requests with the pointers that already exist: the new-tab button is on by default (bundled `show_new_tab_button_in_tab_bar = true`), right-click on a tab opens a tab menu with New Tab, Tab Navigator and Close Tab (`tab_context_menu_actions` in `kaku-gui/src/termwindow/mouseevent.rs`), middle-click closes a tab, and the Shell menu lists New Tab and Close Tab. For touch over remote desktop, a plain left press (a tap) up to `BOTTOM_TAB_BAR_TOUCH_EXTENSION_PT` above a bottom bar still hits tabs and the new-tab button, while middle and right presses there stay with the terminal, so the bar itself stays one row. Do not re-add a native close mark: if one is ever wanted, it has to be a drawn, centered icon like the fancy bar's `make_x_button`, not a grid character. The desktop bottom retro bar is one terminal row with line_height (`tab_bar_pixel_height_impl`), the same as a top bar; the compact natural height read as cramped. Touch mode (2026-10-14) is a per-window toggle with one entry, Window > Touch Mode; the on-bar Touch button and the titlebar entry were removed as clutter. It keeps the normal bar height and look and only adds a drawn, centered close icon to every retro tab, over cells the bar keeps at the tab's end (`close_slots` in `kaku-gui/src/tabbar.rs`). The separate 40pt touch bar, its Menu button and panels were removed; do not bring them back. A long press (left press held 450ms, under 24pt of movement, acted on at release) replays as a right click in every mode, so tab and terminal context menus cover touch; UU was measured to forward a long press as a held left button, never a right click.\n- Line height decision (2026-07): the bundled default stays `line_height = 1.28`. Character-cell graphics (QR codes, `neofetch` logos, TUI bar charts) scale with the row height, so they render about 28% taller than at `line_height = 1.0`, and that trade-off is accepted. Rendering block glyphs at their natural cell height was tried in `d810b13` and reverted in `3f131ea`: block characters must fill the whole cell or box borders and QR codes open horizontal seams. Do not propose lowering the default or re-attempting natural-height block rendering; point users at the `line_height` entries in `docs/faq.md` and `docs/configuration.md`. The block cursor is a separate case and correctly keeps using `natural_cell_height` (see `kaku-gui/AGENTS.md`).\n- Draft issue and PR replies unless the maintainer has already approved the exact public action.\n- Do not modify files outside this repository without showing the intended change and getting explicit confirmation.\n- Do not add instructions for the removed `website/` tree unless that directory exists in the current worktree.\n- The marketing and docs site lives on the `vercel` branch (linked worktree at `~/www/kaku-site`), not on `main`. It follows the design guide at `~/www/kaku-site/DESIGN.md`; verify changes with screenshots at 375px / 1280px and deploy by pushing the `vercel` branch (Vercel serves kaku.fun).\n- Keep private credentials, local keychain paths, and machine-specific release notes out of public repository docs.\n- **Do not propose UI i18n / multi-language menus / a `config.language` setting.** The `rust-i18n` based Chinese UI localization (PR #362, commit `f6cfb4b`) was reverted in `b4d779a` on 2026-05-18; `language` remains in the config schema as a deprecated field for backward compat only. UI strings (menus, confirm dialogs, config TUI copy) stay as English literal strings. New UI surfaces should not introduce translation keys, locale-aware formatting, or \"what if a user wants Chinese\" abstractions. If a user requests a non-English UI, route to the assistant config / AI chat surface; those already accept non-English content.\n- **Kaku stays free, including Kaku Assistant and remote access (maintainer 2026-10-18).** Kaku Assistant runs on the user's own API key or Codex login, so it has no cost to meter. The relay (`crates/kaku-relay`, one Fly machine) is paid for by the maintainer as a thank-you to the community, and `docs/faq.md` says so. Do not propose paid tiers, usage-based billing, accounts or usage metering for the relay; when it nears the concurrency limit in `fly.toml`, load test and move to a larger machine instead.\n- **Do not pre-bake provider abstractions in `kaku/src/ai_config/`.** Kaku Assistant parsing, field presentation, and persistence live in `kaku/src/ai_config/tui/providers/assistant.rs`; the TUI retains its event loop and shared data types. Future extraction should move one concrete provider at a time into that existing directory. Do not add an unused trait, a `ProviderKind` enum, or stub modules. Save Copilot for last because its OAuth flow is the abstraction stress test.\n- CLI Codex readiness and GUI connection parsing both consume `tests/fixtures/codex-connection.json`. Add boundary cases there when either policy changes, and run both crates' tests.\n- Official Homebrew token is `kaku` (`brew install --cask kaku`). `tw93/tap/kakuku` remains the personal tap for people who installed before the official cask. Do not tell users the Homebrew `kaku` token is a different app.\n\n## Maintainer Follow-up\n\n- For current issue and PR sweeps, read live GitHub state first with `gh issue list` and `gh pr list`; refresh once more before final conclusions or public actions.\n- Before commenting on or closing an item, confirm its title, state, and author with `gh issue view` or `gh pr view`.\n- The latest stable tag is `git tag --list 'V*' --sort=-version:refname | head -1`; without the `V*` filter the rolling `nightly` tag sorts first. Map each report to `git log <tag>..HEAD`, and treat a closed issue as a claim until the fix commit or remaining gap is named.\n- Shell integration fixes run the affected `assets/shell-integration/tests/*_smoke.sh`, then the list `.github/workflows/checks.yml` runs under `Shell integration smoke tests`. Watch the pushed `main` run with `gh run watch <id> --exit-status`.\n- Do not close issues or PRs on local green alone. For fixes pushed to `main`, wait for the new GitHub Actions run on `main` to pass before posting fixed/closed replies.\n- The rolling `nightly` release is not rebuilt by push. Before sending users to Nightly, run or verify `./scripts/nightly.sh` and confirm `gh release view nightly --json tagName,targetCommitish,publishedAt,assets,url` points at the fix commit and includes `Kaku-nightly.dmg`.\n- Default issue-closure pipeline once a fix is verified: commit the fix, refresh the `nightly` release assets per the rule above, reply in the reporter's language with the Nightly download or in-app update path, then close if the current request already authorizes closure; otherwise propose it and wait for authorization. Do not promise a specific packaged-release date in replies.\n- Before pushing `main`, run `git fetch origin main` and verify `origin/main` has not moved unexpectedly. If it moved, stop and review `origin/main..HEAD` before pushing.\n- For several unrelated bugs, prefer one commit per issue or behavior, and put the issue number in the subject: `fix(scope): #123 short summary`.\n- Merge contributor PRs with `gh pr merge --merge`, never squash, so the contributor's own commits and authorship stay in history; add no trailer. When `maintainerCanModify` is true, push fix commits to the contributor's branch rather than rewriting their existing commits, and approve a first-time contributor's workflow run with `gh api -X POST repos/tw93/Kaku/actions/runs/<id>/approve` before expecting CI.\n- If an accepted PR's equivalent fix lands outside the contributor branch, acknowledge the contribution and explain which user-visible fix was delivered before closing it; keep commit hashes and internal attribution bookkeeping out of the public reply.\n\n## Investigation Order\n\nWhen scope is incomplete, inspect in this order:\n\n1. User-provided repro, failing command, or failing test.\n2. Entry point for the behavior, usually `kaku/src/main.rs`, `kaku/src/cli/`, or `kaku-gui/src/main.rs`.\n3. Owning subsystem document and target crate.\n4. Immediate cross-crate boundary used by the call path.\n5. Narrow tests, fixtures, snapshots, or scripts that reproduce the behavior.\n\nFor AI-facing behavior, inspect in this order:\n\n1. CLI and assistant configuration under `kaku/src/ai_config/`, `kaku/src/assistant_config.rs`, and `config/src/proxy.rs`.\n2. GUI AI state and transport under `kaku-gui/src/ai_*`, `kaku-gui/src/ai_chat_engine/`, and `kaku-gui/src/cli_chat/`.\n3. Overlay UI under `kaku-gui/src/overlay/ai_chat/`.\n4. Shared helpers in `crates/kaku-ai-utils/`.\n\nFor AI transport bugs around custom `base_url`, keep both proxy paths true: external API hosts should use detected system proxy settings, while loopback, private LAN, link-local, CGNAT/Tailscale-style, `.local`, `NO_PROXY`, and macOS ExceptionsList model endpoints should connect directly. Verify with the macOS system proxy enabled and a loopback or internal OpenAI-compatible smoke before saying it is fixed. Do not claim general SOCKS support unless that transport is actually implemented and verified.\n\nFor `Ctrl+letter` not working in a raw-mode TUI (the most common shape: `Ctrl+C` / `Ctrl+R` works in plain shell but not inside a TUI overlay), inspect in this order:\n\n1. AppKit menu `keyEquivalent` intercepting `keyDown` before the terminal sees it. Enable `config.debug_key_events = true`, restart the app, then `grep 'key_event.*CTRL' ~/.local/share/kaku/kaku-gui-log-<pid>.txt`. If the log shows only `key_is_down: false` and no matching `key_is_down: true`, the AppKit menu absorbed the event; do not chase termwiz or PTY.\n2. Cooked-mode tests (`cat -v` showing `^C`) do **not** rule out menu interception. Reproduce inside a raw-mode TUI before forming a hypothesis.\n3. Only after step 1 rules out menu interception, inspect termwiz encoding (`termwiz/src/input.rs`), then PTY / termios state.\n\nFor TUI display corruption after interactive CLIs re-render prompts or selection lists, first capture a minimal ANSI transcript. Add a terminal-core regression around cursor-up (`CSI n A`), full-line erase (`CSI 2K`), cursor-down (`CSI 1B`), wrapped rows, and styled prompt symbols. If the core transcript passes but the built app differs from Terminal.app, inspect GUI width, cell metrics, resize, and wrapping inputs rather than changing terminal semantics blindly.\n\n## Subsystem Guides\n\n| Subsystem | Guide | Scope |\n|---|---|---|\n| GUI | `kaku-gui/AGENTS.md` | Rendering, window lifecycle, input, mouse |\n| Mux | `mux/AGENTS.md` | Tabs, panes, domains, client/server |\n| Terminal | `term/AGENTS.md` | VT emulation, screen buffer |\n| Config | `config/AGENTS.md` | Lua loading, schema, config reload |\n| Termwiz | `termwiz/AGENTS.md` | TUI primitives and widgets |\n| Lua API | `lua-api-crates/AGENTS.md` | Rust-to-Lua bindings |\n| Crates | `crates/AGENTS.md` | Shared utility crates |\n| macOS platform | `.claude/rules/macos.md` | AppKit menu / keyEquivalent traps, menubar init timing |\n\n## Verification\n\n| Change type | Command |\n|---|---|\n| Rust compile check | `make check` |\n| Rust logic change | `make test` |\n| Formatting | `make fmt-check` |\n| Lints | `cargo clippy --locked --all-targets -p kaku -p kaku-gui -p mux -p config -- -D warnings` (`make check` does not run clippy, so a change that passes locally can still turn CI's Clippy job red) |\n| GUI or rendering change | `make app` |\n| CLI main menu input | `make app`, then `python3 scripts/test_main_menu.py` (real PTY, q/Q/Esc exit) |\n| Config release change | `./scripts/check_config_release_readiness.sh` and `./scripts/check_release_config.sh` |\n| Release note change | `./scripts/check_release_notes.sh` |\n| Release-adjacent change | `make fmt && make check && make test`, then `make app` |\n| `crates/kaku-relay` change | `cargo check --locked --manifest-path crates/kaku-relay/Cargo.toml` (it is in `workspace.exclude`, so `make check` and `make test` never see it; CI's `relay-check` job is the only gate) |\n\nFor GUI or rendering issues, read `kaku-gui/AGENTS.md` first and verify with `make app`, not only `make dev`.\n\nRestarting Kaku (2026-10-14, narrowed 2026-10-21): run `scripts/restart_kaku.sh` only when the maintainer asks for a restart in the current message (重启 Kaku). Swapping a new build into `/Applications`, or an earlier request in the session, is not a request to restart: finish the swap, then ask and wait, since the restart ends the maintainer's own windows and any agent session inside Kaku. It restarts into `/Applications/Kaku.app` the way an in-app update does (TERM, then reopen; tabs return from the periodic session snapshot) and detaches first. It stops every running Kaku GUI, a copy opened from `dist/` included, but sends TERM only to the one that owns `last_session.json` (its pid names the snapshot's `content_dir`) and SIGKILLs the rest after it exits, so a stray copy cannot overwrite the tabs when it quits. An agent session running inside Kaku ends with it, so write the reply first, tell the maintainer to reopen the session with `claude -c` (or the equivalent), and make the script the last action. Its log is `~/Library/Logs/kaku-restart.log`. The detached run is marked with a `--detached` argument, never an environment variable: `/usr/bin/open` hands the caller's environment to the app, so a variable reached every shell in the reopened Kaku and the next restart skipped detaching and died with it (`test_leaked_detach_variable_still_detaches`). An absent snapshot or empty process list must still reach the reopen step; `scripts/test_restart_kaku.py` exercises those branches and owner-only saving with fake processes, never the running app.\n\nStanding authorization (2026-10-19): after a verified change to `crates/kaku-remote/assets/web` is pushed, publish the relay page with `scripts/publish_web_page.sh` without asking. Confirm the deploy reached `t.kaku.fun` by fetching `https://t.kaku.fun/index.html` and finding a string the change added, and check the response's CSP when the change touches the script or the publish script.\n\nWhen a check needs clicks on the built app, hand the maintainer an isolated launch command (`open -na dist/Kaku.app --env HOME=<scratch home with a copy of their kaku.lua>`) and numbered taps with the expected result for each, instead of driving synthetic mouse events: the maintainer's own Kaku takes focus back and the test window may sit on another Space. Static screenshots with `screencapture -l` are still fine when the window is on screen. A shell running inside Kaku carries `KAKU_CONFIG_FILE`, `KAKU_CONFIG_DIR`, `KAKU_SESSION`, `KAKU_UNIX_SOCKET`, `KAKU_ZSH_DIR` and `XDG_CONFIG_HOME`, and `open` hands them to the app, so a test instance launched without `env -u` for each reads the maintainer's real config and session, and may connect to the relay; shell smoke tests run from such a shell need the same unsets. Remote state lives in `$TMPDIR/kaku-remote.json` and `kaku-remote-relay.json`, which a scratch HOME does not isolate: a test instance with remote on overwrites the maintainer's copies, so say that their Kaku needs a restart afterwards for `kaku remote` and `kaku doctor` to read its own state again. Look up the test window by the test instance's pid and capture nothing when that yields no id, since an empty `-l` once captured the maintainer's own window. Native fullscreen does not capture reliably this way; ask the maintainer for that screenshot instead. Four traps from the 2026-10-19 site captures: do not pass `--args start` to `open`, since AppKit hands bare arguments to `application_open_files`, the first pane runs `start ; exit` and the window closes before it is seen; drop `config.remote` from a copied `kaku.lua`, or after a config reload the test instance can attach to the maintainer's mux and `send-text` lands in their pane; `yazi` in a test pane is the shell wrapper, which, like Cmd+Shift+Y, rewrites the maintainer's `~/.config/yazi/theme.toml` from the test config's `color_scheme`, so run the Yazi binary itself with `YAZI_CONFIG_HOME` on a scratch copy; and size or move the test window with one System Events expression per action (`set size of window 1 of (first process whose unix id is <pid>)`), since a process reference kept in a variable resolves by name and reached the maintainer's `kaku-gui` window. Check the result per pid with `CGWindowListCopyWindowInfo`. For a remote capture, launch the test instance with a scratch `HOME` and `TMPDIR` as well: pairing state, the identity and `remote.lock` live under `$HOME/Library/Application Support/kaku` and the relay files under `$TMPDIR`, so only both keep the maintainer's remote untouched; end with `kaku remote off` under the same environment. In Safari, address a window you opened by its AppleScript `id`, never `front window`, which on 2026-10-19 resolved to the maintainer's Start Page window and resized it; editing the address bar opens history and Favorites over the page, so capture with the field unfocused (Tab moves focus into the page).\n\n## Current Risk Areas\n\n- In-app update replace/restart must confirm on every entry path (menu Check for Updates, menu Restart to Update, toast, CLI direct ZIP, brew cask). Do not set `KAKU_UPDATE_AUTO_CONFIRM` for exploratory menu check; only the overlay-confirmed path may auto-confirm. Sibling entry points tend to regress independently; after changing one path, sweep the matrix and keep the guards in `kaku-gui` (`user_facing_update_events_route_through_confirm`) and `kaku` (`brew_and_direct_paths_confirm_before_replace`) green. Details: `kaku-gui/AGENTS.md` and `.agents/skills/bugs/SKILL.md`.\n- AI chat and shell flows are active product surfaces. Preserve `fast_model`, proxy config, inline `#` query status, syntax highlighting, approval flow, and conversation state behavior.\n- `config_version` bumps every release; the source of truth is `assets/shell-integration/config_version.txt` and the gate is `scripts/check_release_config.sh`. Config schema changes must update bundled defaults, docs, release checks, and migration behavior together. Per-version history and the migration rule (only keys that existed in the previous released version need migration code) live in `docs/config-versions.md`. Do not hardcode the current version number in agent guides; it goes stale between releases. A config_version bump is also the only thing that regenerates an existing install's `~/.config/kaku/zsh/kaku.zsh`, so a shell-integration fix reaches stable users through the bump; a Nightly built without one needs `kaku init --update-only`, and replies pointing at that Nightly must say so.\n- GUI regressions can come from overlay resize, pane split/removal, macOS worker thread lifetime, WebGPU surface reconfigure, tab bar spacing, and alternate-screen wheel scroll behavior.\n- Startup performance depends on caching shell user vars, Lua bytecode, early appearance queries, GLSL version, and built-in fonts. Do not invalidate those caches without measurement.\n- `kaku doctor` ends every run by writing `Kaku-Diagnose.zip` (`kaku/src/diagnostics.rs`). It also collects the `kaku-hang-*.txt` stack samples that `kaku-gui/src/stall_watchdog.rs` writes when the GUI thread stays blocked past 1s, so rename that prefix in both files together. Everything in the bundle goes through its redactor, which also hides typed text in `debug_key_events` lines (printable keys, physical key codes, composed and IME text) including committed `KeyCode::Composed` text as well as `DeadKeyStatus::Composing`, while keeping Ctrl and Cmd keys readable for key reports, `assistant.toml` is never read, and large files (session content can be gigabytes, #559) are listed by size, not copied.\n- Notification actions that call back into Kaku should resolve bundled executables relative to the running app, not an assumed system path.\n- Known high-regression zones: theme/config TUI initialization (`kaku/src/ai_config/tui/`), macOS window geometry (`window/src/os/macos/window.rs`, `kaku-gui/src/termwindow/resize.rs`), and menubar initialization timing (`window/src/os/macos/menu.rs`). Changes touching these must ship with a regression test or assertion.\n- `assets/shell-integration` scripts run in the user's shell, not just at build time. Bash heredocs that generate zsh (e.g. `setup_zsh.sh` writing `kaku.zsh`) expand backticks and `$(...)` at generation time, so escape any that must reach the output literally (#450), and never put `local` outside a function (#432/#441). An expired `.zcompdump` needs `compinit && touch` after a successful full check: `compinit` can leave an unchanged dump's timestamp untouched. Keep the failure path stale and run `assets/shell-integration/tests/compinit_cache_smoke.sh`, which checks the next shell returns to `-C`. CI gates this: shellcheck (`--severity=error`, catches SC2168) over the bash scripts plus a `zsh -n` parse check of the generated `kaku.zsh` in the setup smoke.\n- User-owned configuration symlinks must survive updates, and failed writes must leave their targets unchanged. Resolve the final target and finish a sibling temporary file before replacing it; preserve target permissions. The shell `write_through` helper has four copies across the zsh/fish installers and their generated Yazi wrappers. Test all four under a real write failure, not only successful linked-file updates (#581, #582).\n\n## Release Notes\n\nTag format is `V0.x.x`. `scripts/release.sh` is the source of truth for tagged releases. The GitHub Release title comes from the first heading in `.github/RELEASE_NOTES.md`.\n\nBefore drafting release notes, read the previous formal release (`gh release view <latest-tag>`) and treat it as the format template: title `V{version} {Codename}` with a one-word codename, a centered logo header with the product tagline, `### Changelog` as numbered `**Label**: one sentence` items, a `### 更新日志` section whose Chinese items map one-to-one by number, and a `> https://github.com/tw93/Kaku` footer. After publishing, add all six positive reactions (`+1`, `laugh`, `heart`, `hooray`, `rocket`, `eyes`) to the release via `gh api` and read them back to confirm; never add `-1` or `confused`.\n\n## Pre-release Runtime Smoke\n\nCI gates fmt/check/clippy/tests but cannot see visual layout, native AppKit, render timing, or shell-in-user-env behavior, which is where most post-release reports come from. The checklist lives in one place: `.agents/skills/release/SKILL.md` «Pre-release smoke checklist», covering macOS window, tab bar, shell setup, AI chat, and render timing, with the issue numbers each item guards and a note on which slices now have automated coverage. Run it by hand in the built `dist/Kaku.app` before tagging, and add the reproduction there when a release fixes a bug outside the list.\n\nReleases that touch windowing, titlebar coloring, tab bar layout, or transparency also need the config matrix in `docs/release-checklist.md`, which enumerates the tab position / tab style / opacity / window state combinations to check by hand and names the `update_titlebar_background()` regression guard.\n\n## Documentation Maintenance\n\n- Single-crate behavior belongs in that crate's `AGENTS.md`.\n- Cross-crate behavior should update every affected subsystem guide.\n- Build, CI, release, and maintainer workflow changes belong in this root file.\n- Shared agent instructions belong in tracked docs. Personal overrides belong in ignored local files.\n- One-off review reports, scorecards, and diagnostic snapshots are evidence, not durable project docs. Extract stable rules or verification gates into `AGENTS.md`, `CLAUDE.md`, subsystem guides, scripts, or tests, then remove the transient report.\n- Do not hide user-visible behavior changes inside maintainability or cleanup patches. New UI, config fields, defaults, or workflow permissions should be split into their own change unless the maintainer explicitly approved that scope.\n","config/AGENTS.md":"# Config Agent Guide\n\nThe `config` crate owns config loading, Lua integration, schema behavior, proxy settings, versioned defaults, and AI configuration.\n\n## Scope\n\n`config` controls:\n- loading user and bundled configs\n- Lua execution and binding lifecycle\n- schema mapping between Lua and Rust\n- config subscriptions and reload behavior\n- AI model, fast-model, and proxy settings consumed by GUI and CLI flows\n- versioned default config and release readiness checks\n\n## Where to Look\n\n- `config/src/config.rs`: load/parse flow\n- `config/src/lib.rs`: config API and subscriptions\n- `config/src/proxy.rs`: proxy configuration used by AI and network-facing flows\n- `config/src/version.rs`: app version and target-triple accessors (`wezterm_version()`), not `config_version`; that number lives only in `assets/shell-integration/config_version.txt`, read by `kaku/src/init.rs` and the bundled `kaku.lua`\n- `assets/macos/Kaku.app/Contents/Resources/kaku.lua`: bundled fallback config\n\n## Config TUI (`kaku/src/config_tui/` and `kaku/src/tui_core/`)\n\nThe config TUI is the interactive terminal UI for editing Kaku config. It lives in the `kaku` CLI crate, not the `config` crate.\n\n- `kaku/src/config_tui/` - TUI app: `mod.rs` (form state and main loop), `ui.rs` (rendering)\n- `kaku/src/tui_core/` - shared TUI primitives, currently `theme.rs` only (`accent`, `bg`, `muted`, `panel`, `primary`, `red`, `success`, `text_fg`)\n- `tui_core::theme` is consumed by `config_tui/ui.rs`, `ai_config/tui/ui.rs`, and `tui_splash.rs`. New primitives shared by more than one TUI flow belong in `tui_core`, not copied per feature.\n\n## AI Config TUI (`kaku/src/ai_config/`)\n\nThe AI config TUI lives in the `kaku` CLI crate and shares terminal UI primitives with `config_tui`.\n\n- Keep `fast_model`, primary model, and proxy settings aligned between CLI config, GUI AI chat, and documentation.\n- Do not duplicate form widgets or debounce behavior outside `kaku/src/tui_core/`.\n- Verify AI config changes against both config parsing and the visible TUI flow.\n\n## Practical Rules\n\n- Loading priority: user config first, bundled config second.\n- Keep reload-safe behavior for startup hooks and subscriptions.\n- `color::is_light_color` re-exports the terminal core's classification so `COLORFGBG`, window chrome and DEC mode 2031 reports use the same palette policy. Keep that shared policy when changing color classification.\n- After mutating a live `TermConfig` client palette, call `pane.set_config(config)` so the terminal compares its subscribed palette snapshot and emits mode 2031 notifications. Updating only the provider bypasses the terminal's reload boundary.\n- `KAKU_CONFIG_FILE` is an output, not an input. Config loading exports it (`config/src/config.rs`) and `effective_config_file_path()` (`config/src/lib.rs`), the GUI single-instance check (`kaku-gui/src/main.rs`), `assets/shell-integration/setup_{zsh,fish}.sh`, and the bundled `kaku.lua` all read it back. Do not turn it into a config path override; the only supported override is `--config-file` (`CONFIG_FILE_OVERRIDE`).\n- Keep bundled fallback config authoritative at `assets/macos/Kaku.app/Contents/Resources/kaku.lua`.\n- Kaku Dark, Kaku Light, and the Kaku Theme alias must also resolve for standalone user configs that do not load the bundled Lua defaults. Their `src/scheme_data.rs` mirrors are checked against every bundled palette field by `builtin_kaku_scheme_mirrors_match_complete_bundled_palettes`; synchronize both when changing a Kaku palette.\n- Preserve compatibility with runtime reload callers that trigger `config::reload()` from GUI-side signals.\n- `config::reload()` can return without applying: once a config file is loaded, a watcher reloads 100ms after that file changes, and whichever reload started last wins. A test that rewrites its config file and reloads must set `automatically_reload_config = false` in that file, or a slow runner reads the old values (`ssh_pane_titles_keep_the_formatter_on_hover`). CI runs each test in its own process with nextest, so a flaky global-config test is racing a thread, not another test.\n- The current `config_version` is whatever `assets/shell-integration/config_version.txt` says; never trust a number written in a doc. Any version bump must update bundled config, release checks, docs, and migration expectations together, and add a row to `docs/config-versions.md`.\n- New config fields are user-facing behavior. Keep them out of pure cleanup/refactor patches unless the maintainer explicitly approved the product change, and update bundled defaults plus documentation in the same change when they do land.\n- Keep alternate-screen wheel scroll behavior configurable; terminal and GUI defaults must not diverge.\n- Config rewrite guards must validate every assignment the writer can replace, using the same match predicate. A later Lua override can contain user-owned settings even when the first assignment contains only a managed flag; `remote_access_checks_every_assignment_before_rewriting` covers this boundary in `kaku/src/config_tui/mod.rs`. Remote toggles must edit `effective_config_file_path()` through `kaku::utils::write_atomic`, preserving symlinks, permissions and the original bytes on a partial write failure. `kaku/tests/remote_config.rs` exercises the actual CLI with `--config-file`, a stale inherited `KAKU_CONFIG_FILE`, and a real file-size write limit.\n\n## Bundled kaku.lua Pitfalls\n\n- **200-locals hard limit.** LuaJIT caps a chunk at 200 local variables, and the top-level\n  chunk of `assets/macos/Kaku.app/Contents/Resources/kaku.lua` is already at capacity.\n  Adding one more top-level `local` makes the whole config fail to load at startup. New\n  helper functions and values must be nested inside existing functions or tables. After any\n  edit, verify the file still loads:\n\n  ```bash\n  luajit -e \"assert(loadfile('assets/macos/Kaku.app/Contents/Resources/kaku.lua'))\"\n  ```\n\n  `scripts/check_kaku_lua_loads.sh <path>` wraps the same check and doubles as a PostToolUse hook for edits to this file.\n\n- **PaneInformation is fields-only.** In title-formatting paths (`format-tab-title`,\n  `format-window-title`), the `pane` object is a `PaneInformation` userdata that exposes\n  fields only, no methods. Method-style access such as `pane:get_foreground_process_name()`\n  or capability probes like `if pane.get_xxx then` do not error; they silently evaluate to\n  nil, so the guarded branch never runs (#485 shipped dead code this way). Use the\n  documented fields, prefer the `WEZTERM_PROG` user var for the running command, and treat\n  argv-derived titles as untrusted (argv can leak environment values).\n\n## Cross-References\n\n- [`kaku-gui/AGENTS.md`](../kaku-gui/AGENTS.md) - GUI config consumers and reload signals.\n- [`lua-api-crates/AGENTS.md`](../lua-api-crates/AGENTS.md) - Lua APIs that expose config values.\n- [`mux/AGENTS.md`](../mux/AGENTS.md) - Alert propagation for config changes.\n","crates/AGENTS.md":"# Crates Agent Guide\n\n`crates/` contains shared utility crates used across the workspace. Inspect the target crate's `Cargo.toml` before editing because Kaku keeps some WezTerm-derived crates and adds its own helpers over time.\n\n## Scope\n\nKey groups:\n\n| Group | Crates |\n|-------|--------|\n| Terminal core | `codec`, `color-types`, `vtparse`, `wezterm-cell`, `wezterm-escape-parser`, `wezterm-surface` |\n| GUI and input | `wezterm-font`, `wezterm-input-types`, `wezterm-gui-subcommands`, `wezterm-open-url`, `wezterm-toast-notification` |\n| Mux, client, and remote | `wezterm-client`, `wezterm-mux-server-impl`, `wezterm-ssh`, `wezterm-uds`, `kaku-relay`, `kaku-remote` |\n| Platform and process | `pty`, `filedescriptor`, `procinfo`, `env-bootstrap`, `umask`, `async_ossl` |\n| Lua and config support | `luahelper`, `wezterm-dynamic` |\n| Shared utilities | `promise`, `base91`, `bidi`, `bintree`, `frecency`, `lfucache`, `rangeset`, `ratelim`, `tabout`, `wezterm-blob-leases`, `wezterm-char-props`, `wezterm-version` |\n| Kaku-specific utilities | `kaku-ai-utils`, `kaku-unicode-names` (generated Unicode name table lifted out of `kaku-gui/src`; regenerate with `ucd-generate`, do not hand-edit) |\n\n## Practical Rules\n\n- For macOS notification actions that launch Kaku commands, prefer resolving bundled executables relative to the running `kaku-gui` binary.\n- Keep `kaku-ai-utils` provider-agnostic; provider policy belongs in GUI/CLI call sites and config, not in low-level helpers.\n- When shared cache helpers affect startup, verify the GUI consumer and avoid invalidating hot-path caches unnecessarily.\n- `wezterm-escape-parser` ProgramStatus accepts whitespace around key/value pairs and skips lexically malformed optional pairs without losing valid state. Preserve decoded-text control-character and size checks; permissive pair parsing must not weaken those boundaries. Regressions live in `src/osc.rs` (`program_status_trims_pair_whitespace`, `program_status_skips_malformed_optional_pairs`, and `program_status`).\n\n## Web Terminal and Relay\n\n`kaku-remote` serves a browser terminal (`src/web.rs`, page in `assets/web/`) and `kaku-relay` lets it be reached from outside the LAN (`src/share.rs` on the Mac side). The `remote` cargo feature is on by default in `kaku` and `kaku-gui`, so Release and Nightly ship it; the feature itself stays off until the user turns it on. Invariants:\n\n- Launched in testing, free (2026-10-17): every build carries the web terminal, `remote.enabled` defaults to false, and `kaku remote` asks before turning it on, so nothing changes for a user who never runs it. The private-build machinery (`nightly.sh --private`, the `check_no_web_terminal.sh` publish guard, `release.sh` refusing the feature, `kaku remote` hidden from `--help`) was removed at launch. Remote access remains free, including the relay paid for by the maintainer; do not add paid tiers, accounts, usage metering or registration tokens.\n- Off until the user turns it on (2026-10-16): `remote.enabled` defaults to false. `kaku remote` asks before turning it on and writes `config.remote = { enabled = true }` to the user's kaku.lua through the settings screen's line writer (`config_tui::set_remote_enabled`); `kaku remote off` writes false. Neither rewrites a `config.remote` table that holds other keys (a relay of their own), which a whole-line rewrite would drop; they say to edit it instead. The bridge follows config reloads (`follow_config`): it starts when the setting turns on, without restarting Kaku, and when it turns off its runtime is dropped, which ends the relay link and every web session within a second, and the state file is removed so `kaku remote` no longer sees sharing. Verify off/on transitions in the same process; restarting the app does not test release of `remote.lock`.\n- One Kaku per data directory serves remote access (2026-10-19): `launch_when_sole` holds a `flock` on `DATA_DIR/remote.lock`; a second GUI on the same data directory (a build-folder copy, another `kaku start`) waits and takes over when the first quits. Before this, a second copy registered with the relay under the same identity and took the browser over to its own terminals. Drift checks name the frame the page drew (`seq`, `Shown::rows_after`), so they still run while frames are in flight over the relay; checking only with nothing in flight never ran while Claude Code worked.\n- No LAN access (2026-10-16 decision, replacing the 2026-10-15 opt-in): the local server listens on 127.0.0.1 only and `remote.bind` is deprecated and ignored, kept so configs that set it still load. The maintainer has no use for it at home and it was plain HTTP: `http://192.168.x.x` is not a secure context, so `crypto.subtle` is missing (measured in Helium), and the relay's https page cannot open a `ws://` LAN socket (mixed content). Do not bring LAN access back.\n- The local server answers only requests whose `Host` is its own (`127.0.0.1`, `localhost` or `[::1]` with the listener's own port, so a config reload that edits `remote.port` does not lock the page out), so a web page that rebinds its name to 127.0.0.1 cannot read same-origin responses. No route serves the token: `kaku remote` reads it from the 0600 state file. The routes for the old iOS client (`/api/config`, `/api/panes`, `/qr` with the token, `/ws/{pane_id}`, and the `ai_chat` handler in `kaku-gui/src/ai_remote.rs`) were removed on 2026-10-14 with the maintainer's approval: its relay mode had already stopped working, and `/qr` handed the token to any local process, other macOS users included. A future iOS app should speak the web terminal's protocol or wrap the page, not bring these back.\n- A session ends after `SESSION_SILENCE` (180 s) without a message from its browser, so a half-open socket from a sleeping laptop does not hold App Nap off forever.\n- One session per browser, carried either by the local WebSocket (`#t=<token>` link) or by the relay (a paired browser, see below). `run_session` sees transport-neutral input and output queues; keep socket and encryption logic out of it.\n- The page mirrors Kaku's own terminal model as ANSI frames rather than replaying the PTY stream, so terminal queries are answered once, by Kaku. Keys go through `Pane::key_down`, pastes through `send_paste`, and already encoded text through `send_text`. Local panes use the terminal's existing ordered background writer for all three; never send Remote text directly to the blocking PTY writer.\n- Kaku Assistant's chat is a GUI overlay that never reaches the mux, so the page would show and type into the shell under it. The GUI reports it with `set_ai_chat_open` (open, close, and a chat dropped on config reload); while it is open `handle_input` drops typing, pastes and clicks for that pane, an upload is saved without pasting its path, and the page dims the pane with a notice. Chat on the page itself was declined (2026-10-18): it would be a second chat UI, and using it from a browser reads as odd; do not build one.\n- Anything that reads the Lua config (for example `can_close_without_prompting`) must run on the GUI thread via `promise::spawn::spawn_into_main_thread`; calling it from the server's tokio threads panics.\n- Relay ids are the first 128 bits of SHA-256 of a secret that never leaves the Mac, so nobody else can register as that Mac. Through the relay only paired browsers get a session (`src/pairing.rs`, 2026-10-15; the old `#k=<key>` link, one AES key for every browser, is retired and its pages are told to reload). `kaku remote` writes a one-time offer (`remote-pairing.json`, 10 minutes) and prints `/{id}/#p=<secret>&m=<Mac public key>`; the page makes a non-extractable ECDSA P-256 device key and keeps it in IndexedDB once Kaku has admitted it. Each connection: Kaku sends `KCH3` and an ephemeral P-256 key in the clear, the page answers `KCB3` and its own, both derive one AES-256-GCM key per direction by HKDF-SHA256 over the ECDH secret salted with the transcript (SHA-256 of `kaku-remote v3`, the relay id, both ephemeral keys). Kaku's first sealed message signs the transcript with its long-term key (`mac_key` in `remote-share.json`), which the page checks against the key from its pairing link before sending anything, so a relay cannot pose as the Mac; the page's first sealed message signs it with its device key (`auth`), adding an HMAC of the transcript under the offer secret the first time (`pair`). Messages carry their direction label and position as associated data. Close codes: 4403 not a paired browser (the page forgets its key), 4401 pairing link expired or used, 4426 page from before pairing, 4408 no proof in time; the relay must pass each through (`browser_close`), or the page reads it as busy and retries. Pairing shows no dialog, because the Mac is often unattended when a browser is added from elsewhere: the link works once, every new device posts a macOS notification naming it, and so does a paired device connecting again after 30 minutes away (`QUIET_RETURN`; reloads and reconnects stay quiet), which is the Mac-side sign of remote use (no menubar or tab bar mark, by the maintainer's choice on 2026-10-15), and `kaku remote devices` / `kaku remote revoke <id>` list and remove devices (sessions of a removed device end within seconds). ring's ECDH takes only ephemeral keys, which is why long-term identity is a signature, not a static DH key; ring's fixed ECDSA signatures and WebCrypto's are the same r||s form.\n- Each web session holds `awake::Awake`, a latency-critical `NSProcessInfo` activity. Without it, App Nap throttles the backgrounded GUI's main thread, which turns PTY output into the pane change the session renders from, and every echo waited 100-200 ms. Measure remote input latency with the Kaku window hidden, not focused.\n- `Shown::update` turns a screen whose rows moved into `CSI n S` / `CSI n T` plus the new rows. That relies on each row starting with its own `CSI row;1H` and on the page never setting a scroll region; keep both, or the browser's copy drifts from Kaku's.\n- Clipboard writes from programs (OSC 52, `MuxNotification::AssignClipboard`) also go to browsers viewing that pane (`web::publish_clipboard`, 1 MB cap); the Mac clipboard keeps getting them. Find (`Search`) runs on the Mac over the pane's history, so it reaches past what the page shows. Files a browser drops or pastes arrive in chunks, in a bounded 512 KiB window of 16 KiB chunks, each acknowledged (`UploadAck`), so its percentage is what reached the Mac; a session that ends mid-file deletes the partial file (`Uploads` drop) and the page resends after reconnecting. Files are written to `~/Downloads/Kaku` under a sanitized, de-duplicated name (`safe_file_name`, `unused_path`, 50 MB cap), and their path is pasted into the original drop pane with `quote_dropped_files`, as a native drop would be.\n- Scrolling a plain shell (`local_scroll` in each frame: no alternate screen, no mouse grab, and no foreground `less`) never round-trips: the page fetches history rows with `History` in pages of `HISTORY_PAGE` and scrolls them in `#past`, laid exactly over the terminal with each run placed at Kaku's column. Programs that take the mouse or the alternate screen still get the scroll on the Mac; `less -X` also receives scroll keys, as in the native wheel handler, and Find still pins the Mac-side view (`View::pinned_top`).\n- The page names tabs by `tab_id` and reorders them per `window_id`; rename and move go through `TabAction::Rename` / `TabAction::Move` in `kaku-gui/src/remote_tabs.rs`, which keep the Mac's active tab. A split tab lists its `panes`; viewing one sends `Activate`, which also focuses that split on the Mac. A split tab keeps the Mac window's size (`split_claim`): the browser claims nothing for it, shows the viewed split at its own size scaled to fit the page (up to twice Kaku's font size), and a tab a browser had sized is handed back to its window once split (`TabAction::Reclaim`). Growing the tab by the split's share, as before 2026-10-19, pushed the other splits out of the Mac window. Size claims use zoom-aware `iter_panes()` geometry, so a zoomed pane is claimed like a lone one; tab navigation still lists hidden splits. Browser zoom and unzoom carry only the viewed pane's current claim and reapply it synchronously after the GUI toggles zoom; another pane's action cannot borrow that claim, and native or other-client geometry changes do not automatically reclaim the browser viewport. Frame rendering runs on another thread, so the GUI callback is not a cross-thread atomic transaction.\n- Web pane menus retain the selected `pane_id` through clipboard permission prompts and close dialogs. Splits call `Mux::split_pane` with that id rather than the deferred active-pane spawn path. Confirmed pane closes remove that exact pane, or record and remove its tab when it is last; `CloseCurrentPane { confirm: false }` is not sufficient under `AlwaysPrompt`. Middle-click tab close shares the existing browser confirmation path. Optimistic tab closes require a connected transport and a timer that restores the latest authoritative list when the close is not confirmed; the server suppresses identical lists, so expiry must not depend on another list arriving. Keep cached pane snapshots until the authoritative list drops their pane. Keep native phone selection menus intact.\n- Web URL detection stops at CJK sentence punctuation and unmatched closing wrappers, retains balanced path parentheses, and maps UTF-16 regex offsets back to terminal cells, including surrogate pairs and wide end cells. The browser regression fixture includes the reported GitHub comment link followed by `)，并以`.\n- A tab a browser sized keeps that size through relayouts the Mac does on its own (display or scale change, focus, config or theme reload, tab bar visibility): `apply_dimensions` asks `window_relayout_resizes_tab` per tab. Only the Mac's own use takes it back: typing or clicking (`reclaim_active_tab_size`) or a live window drag. Focus and a modifier pressed alone do not count (2026-10-19): switching between a fullscreen browser and Kaku with Cmd+Tab resized the tab on every switch, and Claude Code left a copy of its output behind for each size. Resizing it under the browser made Claude Code and Codex redraw for the Mac's rows and jump up in the page. The desktop view's synthetic input counts as the Mac's, so the page claims its grid again on its next input and when it leaves the desktop view (`macSized`, `takeBack`). After a reload the first frames still carry the Mac's grid until the new claim lands; the page keeps the terminal at opacity 0 (`body.sizing`, still focusable, unlike the pre-first-frame `visibility: hidden`) until a frame has the claimed grid or a split's own, for at most `REVEAL_WAIT` (600 ms). Tests that screenshot the terminal wait for `sizing` to clear.\n- Page fonts: the page draws with the families of Kaku's `config.font` in order (`font_families`; JetBrains Mono maps to the bundled `Kaku Mono`, emoji fonts stay unnamed), then its own fallbacks with `Kaku Symbols`, the bundled Symbols Nerd Font limited by `unicode-range` to its private-use spans so it is fetched only when an icon shows. Fonts the user installed are resolved on the Mac the way Kaku renders (`src/fonts.rs`: `font_dirs`, then CoreText) and listed in `font_files`, text weight only, since the page draws bold at that weight; files under `/System/`, collections and anything over 32 MB are left out. The page fetches a listed file over the download path only when the browser lacks the family, and keeps it in Cache Storage keyed by path, size and date. The WOFF2 files in `assets/web` come from `scripts/build_web_fonts.py`; rerun it when `assets/fonts` changes, and print its `unicode-range` into `index.html`.\n- The relay is public and sheds abuse itself: browser messages are capped at 512 KB and Mac messages at 4 MB, frames too (`max_frame_size`: tungstenite buffers a whole frame, 16 MB by default, before it checks the message cap); bytes queued toward one Mac (`HOST_QUEUE_BYTES`, the flooding browser is dropped) and toward one browser (`BROWSER_QUEUE_BYTES`, the stalled page is dropped) are bounded, and so are all queues together (`GLOBAL_QUEUE_BYTES`): a message count alone let one stalled page hold a gigabyte of 4 MB screens on a 256 MB machine. Every queue gives its unsent bytes back when its socket ends (`Queued`'s `Drop`), or the relay-wide budget leaks until nothing is accepted; at most 2 registrations per id (a third evicts the oldest) and 4 browser sockets per client address per host; when a host's 8 browser slots are full, the oldest socket the Mac has not admitted is let go (`make_room`; the Mac sends `{\"t\":\"ok\",\"c\":c}` once it admits a browser), so sockets that only know the id cannot lock the owner out; devices are matched on their whole public key, never the 8-hex id people see; `remote-devices.json` changes under an `flock` (`update_devices_at`) so a sign-in cannot write a revoked device back; hosts silent for 75 s and browsers for 150 s are closed. The relay serves the page and its files from its own copy, built in from `crates/kaku-remote/assets/web` (`page_file`), and asks no Mac for them; the page itself answers 404 with the offline notice while the Mac is away, which an open page relies on to tell an offline Mac from a network problem. So a relay deploy ships the page: deploy it together with Kakus that speak its protocol (pairing, `KCH3`), never ahead of them. `fly.toml` caps connections at 400 (soft 300), about 200 people online, from a release-build load test (400 Macs plus 200 streaming pages at 88 MB); raise it only with a new load test or a larger machine, and the Docker build uses `Cargo.lock` with `--locked`.\n- The page lives on its own origin, `t.kaku.fun` (2026-10-16), so serving a page that misuses the browser's keys takes a compromise of that origin, not just of the relay. `scripts/publish_web_page.sh` builds it from `crates/kaku-remote/assets/web` and the bundled JetBrains Mono, fills the `kaku-relay` meta tag (empty in the copies Kaku and the relay serve, which then talk to their own origin), writes a `vercel.json` with `/<id>` and `/<id>/` rewrites, a CSP that allows the one inline script by its SHA-256, and no caching, and deploys it with `vercel deploy --prod` to the `kaku-web` Vercel project, which holds nothing else: a path on `kaku.fun` would share an origin, and the keys, with every page of the site. The page must keep exactly one inline script and no inline event handlers, or the script refuses to publish. With `KAKU_RELAY_PAGE_ORIGIN` set, the relay 308s its page addresses there (the fragment survives), serves no page files, admits browser sockets only with that `Origin` (others are upgraded and closed at once with 4426, so a page still open on the relay's origin from before the move says to reload instead of reading as no network), and answers `/<id>/status` (200 or 404, CORS for that origin) for the page's offline notice. Kaku links the default relay's page to `t.kaku.fun`, any other relay's to the relay itself. `crates/kaku-remote/tests/static_page.cjs` runs the published page behind a server applying its `vercel.json` and checks the redirect, pairing, bookmarks with and without the slash, a foreign `Origin` refused, and no CSP refusals; poll with `page.evaluate` there, since `waitForFunction` evaluates a string the CSP blocks. Publish the page before a relay deploy that sets the origin. After each reconnect the page fetches its own address and reloads when the inline script served there differs from its own (at most once per version through sessionStorage, so a stale cache cannot loop, and never while an upload runs), so tabs left open pick up a new page or a Kaku update without the user reloading; that fetch is why the published CSP's `connect-src` includes `'self'`.\n- The Mac-screen view (2026-10-20) is the maintainer's own for now: the `desktop` cargo feature (`kaku-gui/desktop`) is off by default, so Release and Nightly never carry it. It needs a `kaku-desktop` helper, built from its separate repository. Private packages carry it in `Contents/MacOS/kaku-desktop`, preferred over the legacy copy at `config::DATA_DIR/kaku-desktop`; Kaku starts it as a child so macOS checks Kaku's own Screen Recording and Accessibility grants, and `src/desktop.rs` forwards only `desktop_*` messages in each direction, turning the helper's `desktop_frame` lines into binary messages (zero byte, u32 header length, JSON header, H.264 bytes) for a page whose `desktop_start` says `binary`; `run_session` takes them on a separate `frames` channel that both writers (`share.rs` and the local WebSocket) merge. The helper lives as long as the page's session: `desktop_stop` is forwarded, not acted on, because the helper may hold a virtual display shaped to the page's view, and making one again on every visit costs seconds and moves the Mac's windows (`the_helper_outlives_a_stop_but_not_the_session`). The page loads `assets/web/desktop.js` only when the session config says `desktop: true`, so other Kakus and the published page show nothing new. Build the helper in Release first, then build the private package with `PROFILE=release-opt CARGO_FEATURES=kaku-gui/desktop KAKU_DESKTOP_HELPER=<helper-executable> OUT_DIR=dist/private ./scripts/build.sh --app-only --native-arch`. The build checks helper architecture and signs it before the main executable. Public builds omit both the feature and helper input; a public update replaces the private package and removes its desktop capability. Preparing a private package does not authorize replacing or restarting the installed app.\n- Desktop feedback timing names two boundaries: `desktop_input_ack` confirms a successful input post, and a drawn frame with its `input_seq` confirms a capture after that post, not that the target app reacted. Match both even when independent output queues deliver the frame first; retain slow samples and report unposted, unframed and cancelled inputs separately. A new `stream_id` isolates capture generations and decoder callbacks; frame sequence numbers remain monotonic within the helper. ACK old frames with their own stream id, never the current one. Legacy helpers without these fields stay usable without fabricated timing. Desktop wheel, touch and pointer cancellation belong only to the visible desktop, never the hidden terminal. While the Mac is locked the helper sends `desktop_locked` (from `CGSSessionScreenIsLocked` plus the `com.apple.screenIsLocked` / `screenIsUnlocked` notifications, watched only between `desktop_start` and `desktop_stop`) and the page shows a notice over the picture; typing the password into the view unlocks it.\n- Direct path (2026-10-21 for the desktop view, every build and session since 2026-10-22, `src/p2p.rs` and the page's «Direct path» block): beside the relay, the page offers a WebRTC data channel (str0m with `rust-crypto` on the Mac; `apple-crypto` fails to link) once the config says `direct` (or `desktop`, from the private builds that had it first; a Kaku that says neither gets no offer); offer and answer travel inside the sealed session, so the relay cannot sit in the middle. Interactive replies (terminal frames included), the page's messages under 16 KB and desktop pictures take it, files stay on the relay. Each path has a number (`id` in `rtc_answer`). Order across the move is a fence, not a guess: each side's last relay message before it sends on the path is `rtc_ready` (Kaku sends one per lane, text and frames, the frames one as a binary header-only frame), and the receiver holds what arrives on the path until that marker comes through the relay; without it a delta picture or a key-up overtook one still on the relay. Either side ending a path tells the other by the relay (`rtc_closed` from Kaku, `rtc_close` from the page): the page's channel otherwise stayed \"open\" for about 30 s after Kaku dropped it and lost every click. After a path that had opened ends, the page sends `hello` and `attach` (what was on the path is lost, so the screen starts over), asks for a key frame, and offers again after 2, 5, 15, 30, 60 s; a path that never opened is not retried. Kaku tells the helper `desktop_keyframe` with `path_changed` on every move, which drops in-flight pictures and the old path's round trip and rate samples (kept, a 5 ms direct RTT read every 120 ms relay ack as a standing queue). Host candidates from `getifaddrs` minus loopback, link-local and 198.18/15 (TUN fake IP), plus one server-reflexive candidate per socket from a STUN binding to `stun.cloudflare.com` and `stun.l.google.com` (the page asks the same two), sent before the socket's reader starts and given `STUN_WAIT` (800 ms, DNS included) in all. Without the Mac's reflexive address a browser behind a port-restricted NAT only ever sent to the Mac's private addresses and never opened its own NAT to the Mac. Under a TUN proxy the reflexive address is the proxy's exit (measured 2026-10-22: 23.249.x on the maintainer's Mac), a candidate nobody can reach, which costs nothing. No TURN: when hole punching fails, the relay already carries the session. Unit tests keep STUN off (`STUN_SERVERS` is empty under `cfg(test)`); `public_stun` (ignored) asks the real servers. Pictures ride the reliable ordered channel on purpose, since H.264 deltas cannot survive loss.\n- The relay runs in `nrt` (Tokyo, `crates/kaku-relay/fly.toml`), the edge the Mac enters through: from `sin` every keystroke and echo crossed Tokyo to Singapore twice, 375 ms echo against 166 ms now. It keeps hosts in memory, so run one machine; two would split a Mac and its browsers across them. The relay is not where time goes (2026-10-20): a release build forwards 150 Mbps from one Mac to one browser with under a millisecond added, and the live machine sat at load 0.00 while a page streamed. Slowness through it comes from each side's path to Fly: a proxy node can cap the Mac's upload far below its own line, and some direct routes from China enter a far edge such as `fra`. Measure the path (`fly-request-id` names the edge, a timed upload gives the rate) before resizing the machine or moving the region, and make streams adapt to the link rather than assume it, as the desktop helper's rate control does.\n- Only Mac-confirmed text is rendered. Cursor visibility cannot distinguish normal input from passwords, so never predict typed characters. Pages opt into output flow control with `Hello.frame_ack`: at most 12 unacknowledged frames and a 512 KiB JSON byte waterline, including associated images. One complete frame may cross the waterline before rendering pauses; ACK after xterm parses even an ignored pane frame, coalesce later dirty output, and leave legacy hello clients unthrottled.\n- Browser selection copying follows the Mac's existing `copy_on_select` value and runs on selection release without a network hop. Preserve manual copy when disabled, reconstruct history line breaks and virtual gaps, and never queue a denied automatic copy for a later unrelated gesture. Browser clipboard permission and real mobile selection remain browser-specific.\n- `Frame.wrapped` and `History.wrapped` identify physical rows that continue into the next row. Copying, history pagination and pane snapshots preserve these flags; even a delta frame carries the whole array, and a wrap-only change must be sent. Never infer soft wrapping from a full-width row. Older hosts without flags retain hard line breaks.\n- History keeps at most 10,000 row strings but materializes only visible rows and overscan, preserving stable DOM nodes that intersect an active selection. Cached tab attachment must precede resize, and an equal-size claim must still record browser ownership.\n- Paths in the output (`~/…`, `/…`, `./…`, never inside a URL; `PATH_RE` in the page) are links: Cmd+click (Ctrl+click off the Mac) asks Kaku for that file (`Download`), and it comes back as a browser download. Kaku resolves `~` to the home folder and a relative path against the pane's working directory, sends only regular files up to 50 MB, and refuses folders and missing files by name. Chunks of 64 KB go out while less than `DOWNLOAD_WINDOW` (256 KB) is unacknowledged (`DownloadAck`), limiting queued bytes ahead of terminal interaction. The maintainer chose input and scroll responsiveness over bulk throughput on fast, high-latency links (2026-10-18); keep that trade-off explicit when changing the window. The relay never holds much of a file for one browser; the relay drops a browser past 8 MB queued. A download ends with its session, and the page says so on a dropped link. Any paired browser already has the shell, so reading a file grants nothing new.\n- Upload transfer ids bind the file and original pane for ten minutes after completion, across transport reconnects within the same app process. Retry returns the receipt without writing or pasting twice; cancellation and disconnect remove only partial files. Freeze the target once for a multi-file drop.\n- Access code (2026-10-16, the launch gate the maintainer chose over trials): with `KAKU_RELAY_ACCESS_CODE` set (a fly secret the maintainer picks), the relay registers only Macs presenting it in `x-kaku-access`, compared as SHA-256 in constant time; 401 bodies say `access code required` or `wrong access code`, and ten wrong codes from one address lock it for `ACCESS_WINDOW` (429). `kaku remote` reads the relay status, asks for the code without echo when the relay refused it, saves it to `remote-access` (0600, ASCII only since it travels in a header), and waits for the bridge, which polls that file during its backoff and retries within a second. Every recorded attempt stamps `RelayStatus::checked`, so a second wrong code is told apart from a stale one. Per-address limits sit beside it: `CONNECTS_PER_MINUTE` new sockets and `MAX_HOSTS_PER_ADDR` registered Macs per source, which `source_of` takes as the IPv4 address or the IPv6 /64 from `fly-client-ip` (r.kaku.fun has an AAAA record, and one IPv6 client can pick a new address from its /64 for every request); a large limits map is swept at most once a second; local runs have no client address and are not limited. Host admission must enforce both address and global capacity under the same lock as registration; a pre-upgrade count check alone is not a limit under concurrent handshakes. `live.cjs` runs its relay with a code and checks refusal, then acceptance after saving it.\n- No registration tokens (2026-10-17): remote access is free, so the Ed25519 registration tokens built for trials and Pro (`kaku-relay keygen` / `issue`, `KAKU_RELAY_TOKEN_KEY`, `kaku remote --token`, `x-kaku-token` on registration) were removed with their `ring` and `base64` dependencies; the access code and per-address limits are what gate the relay. Measured cost at that point: one heavy user moved 271 MB a day through the relay (about $0.33 a month of Tokyo egress), so about 100 users cost $20 to $40 a month on the 256 MB machine.\n- No pairing QR code (2026-10-17, maintainer's call): the page is made for a desktop browser for now, so `kaku remote` prints the link with how to use it and the manage commands, not a QR code; do not bring one back while phones are not a target. The link is only `#<one-time secret>` (`pairing::pairing_fragment`); `mac_auth` carries the Mac key, and while the offer is fresh a `tag`, HMAC under that secret over the transcript and the key (`pairing::mac_key_tag`), which the page checks before trusting the key. The relay never sees the secret, so it cannot vouch for a key of its own; a missing tag means the link was used or expired. The page still accepts the older `#p=<secret>&m=<key>` form. Publish the page before shipping a build that prints the bare form.\n- Remote input stays ordered within each browser, but a blocked browser must not hold the shared relay receive loop. Each browser has a bounded worker; overload or timeout closes that browser explicitly rather than dropping individual keys. GUI tab snapshots are requested asynchronously, with at most one outstanding request per session, and a later explicit Attach cancels an earlier automatic NewTab follow.\n- Both WebSocket writers prioritize interactive replies before bulk traffic, with one bulk turn after eight interactive messages to prevent starvation. File start, chunks and completion stay in the same bulk FIFO. Assign encrypted sequence numbers only after the final writer selects a message; reordering already encrypted messages breaks authentication. Keep the final bulk queue bounded to one pending message, and discard revoked sessions before encrypting their queued output. Bytes already written to TCP cannot be overtaken by later input.\n- Every relay connect is capped by `CONNECT_TIMEOUT`: a local proxy (Stash TUN) can accept TCP and never finish the handshake, which once left the link offline for hours. Bound socket writes and session inbox waits too, but allow short input bursts to wait rather than discard keys. Wrong-key close 4403 is final; idle unauthenticated sessions expire with 4408. A host replacement closes browsers independently of the old data queue, with late input discarded until the close writer finishes.\n- The default relay uses `r.kaku.fun`; its browser page lives at `t.kaku.fun/{id}/`, and the relay redirects older `/{id}/` and `/t/{id}/` page links there. Never shorten the 128-bit identity or put a pairing secret in a URL path/query. The page erases the fragment at once and saves the device only after Kaku admits it; when IndexedDB is unavailable it pairs for that tab only and says so. Loopback tokens stay tab-only in sessionStorage. Keep pairing instructions in the CLI, page messages and public docs consistent.\n- One identity per Kaku, persisted at `DATA_DIR/remote-share.json` (0600), devices at `remote-devices.json`. `kaku remote --reset` replaces the identity and forgets every device; the running bridge notices within seconds and re-registers.\n\n## Remote Verification\n\n- For an authorized relay deployment, run `fly deploy . --config crates/kaku-relay/fly.toml --dockerfile crates/kaku-relay/Dockerfile --remote-only --depot=false --ha=false --strategy immediate` from the repository root (the build context must reach the page files; the root `.dockerignore` sends only what the relay embeds, about 1 MB, never `target/`). Use `--depot=false` when Depot stalls at builder authentication. Verify the Fly tunnel with `fly doctor` first; do not add `--wg=false` to a working tunnel, since the legacy builder's HTTPS path can fail with an h2c upgrade HTTP 500. This selects a build transport, not a system proxy setting. Verify the deployed image digest and an isolated encrypted session afterward; a dashboard without new releases does not diagnose a failure before image creation. If CLI DNS checks warn while HTTPS works, compare public DNS before changing anything.\n- Run `cargo test --locked -p kaku-remote`, relay tests via its own manifest, and the workspace check/tests/clippy, which build the feature by default as CI does.\n- `node --test crates/kaku-remote/tests/web.test.cjs` uses Playwright Chromium to exercise the shipped HTML and xterm, including input privacy, reconnect, upload pacing, history selection, and viewport boundaries. Set `NODE_PATH` when Playwright is installed outside the checkout; `PLAYWRIGHT_CHROMIUM_EXECUTABLE` optionally selects an existing browser. The page draws with WebGL when it can, so most tests launch with WebGL off and read the DOM fallback, and one test checks the WebGL grid, down to its text pixels. The WebGL tests launch with `GPU_ARGS` (`--use-angle=metal --enable-gpu --ignore-gpu-blocklist`), which give headless Helium WebGL2 on the Apple GPU (2026-10-21; without them it had none and those tests skipped). CI's Chromium can still lack WebGL2 and skip them, so read the reported skips before claiming GPU coverage. Playwright's own Chromium downloads fail code signing on Darwin 27 and its headless shell only has SwiftShader, whose WebGL timings are meaningless. Never emulate `deviceScaleFactor` for WebGL checks or measurements: it changes `devicePixelRatio` but not `devicePixelContentBoxSize`, so xterm sizes its canvas at half the device size and draws nothing or a shrunken grid, a failure real browsers do not have.\n- The bar for the page (maintainer, 2026-10-16) is how typing, scrolling, copying and tab operations feel: no delay, no stutter, like the app. After changing the page, `web.rs` or the relay, run `node crates/kaku-remote/tests/feel.cjs` (same setup as `live.cjs`) and compare: on loopback, keystroke to echo p50 about 5 ms, a 54-character burst intact, no scroll frame over 25 ms, copy-on-select exact within a frame, new tab shown in about 15 ms, switch and close within one frame (16 ms). It times operations in the page from the click event to the screen change, since Playwright waits about two frames before it clicks. It explicitly disables WebGL with `--disable-webgl` and `--disable-3d-apis`; these are DOM timings, not GPU timings. Headless Helium has WebGL2 only with `GPU_ARGS` from `web.test.cjs`, so WebGL timings need those flags or a headed browser; do not read a DOM-only run as WebGL coverage.\n- `node crates/kaku-remote/tests/static_page.cjs` (same app and relay setup as `live.cjs`) also reads DOM rows and explicitly disables WebGL. `--disable-gpu` alone still exposes WebGL2 in some Chromium builds, so it does not guarantee DOM rows exist; use `--disable-webgl` and `--disable-3d-apis` for every DOM-based harness. Keep WebGL off in harnesses that open several tabs for a second reason: headless Helium on Metal stops opening new tabs once a page has drawn with WebGL (navigations abort before any request reaches the server and `newPage` hangs), which looked like a page bug for two rounds.\n- After `make app` and a debug relay build, run `node crates/kaku-remote/tests/live.cjs`. It launches only its own app, temporary HOME, and loopback relay, tests encrypted input, uploads across reconnect, frame flow control, host replacement, stable split targets and AlwaysPrompt pane closing, foreign `Host` refusal, and replayed, misplaced and old-format messages, then removes its own fixtures. Never substitute a real share link. DOM-row assertions in `live.cjs` explicitly disable WebGL; real GPU rendering remains covered by the separate browser tests, which must assert canvas output rather than DOM rows.\n\n## Cross-References\n\n- [`term/AGENTS.md`](../term/AGENTS.md) - Uses codec, vtparse, and color types.\n- [`kaku-gui/AGENTS.md`](../kaku-gui/AGENTS.md) - Uses font, input, and surface crates.\n- [`lua-api-crates/AGENTS.md`](../lua-api-crates/AGENTS.md) - Uses Lua helper and dynamic binding crates.\n",".agents/skills/bugs/SKILL.md":"---\nname: bugs\ndescription: \"Proactively sweep a Kaku area for latent defects and UX traps before users report them, using this repo's own fix history and multi-entry-point archetypes, and confirm every finding with a probe run this turn.\"\nwhen_to_use: \"找找有没有bug, 主动找bug, 有没有隐患, 排查隐患, 举一反三, 上线前扫一遍, 这块可靠吗, 体验问题, latent bug scan, sibling sweep after a fix, /bugs\"\n---\n\n# Bugs: Find It Before the User Does\n\n`/hunt` needs a symptom. `check` / review needs a diff. This one needs neither: it goes looking.\n\nNamed **bugs**, not bug: `/bug` is a Claude Code built-in.\n\n## The Question\n\nAcross Kaku's closed bugs and fix commits, the dominant defect is not always a crash. It is often:\n\n- a **second entry point** that skipped the safety path the first entry already had\n- a **wrong-but-plausible** geometry or selection state after a modal/resize/sleep\n- a **shell-generated script** that is fine on the author's machine and broken in a clean HOME\n- an **AI surface** (Cmd+L, `#`, `k`, toast) that diverges on auth, theme, or cwd\n\nSo the question that finds bugs here is never only \"can this crash?\" It is:\n\n> What does this code do when the user arrives through the **other** path (menu vs toast vs CLI vs shell), when the display **scale or sleep** changes mid-session, when the shell is **not** the developer's zsh, or when the AI pane cwd is **remote**?\n\n## 1. Pick the surface\n\nName the area and the depth. Whole-repo sweeps with no budget produce speculation. Start from the Hotspot Map below (or the area that just shipped a fix) and go deep.\n\n## 2. Read that area's own fix history first\n\n```bash\ngit log --oneline --grep='^fix' -i -- <path>\ngit log --pretty=format:'%h %s%n%b' --grep='^fix' -i -- <path> | head -200\ngh issue list --repo tw93/Kaku --state closed --label bug --limit 40\n```\n\nBugs recur by shape within a module. Two signals worth acting on:\n\n- A fix chain of two or more commits each \"completing\" the previous one means a sibling path is probably still open. Real chain here: toast confirm, then menu confirm, then brew confirm on the same update flow.\n- A commit body that says only one surface was fixed (toast / CLI / overlay / brew) means grep every other surface for the same action.\n\n## 3. Sweep the boundaries, in this order\n\nHighest historical yield first. Subsystem guides live in crate `AGENTS.md` files and root `AGENTS.md` risk areas.\n\n| Boundary | What to ask | Where it usually lives |\n|---|---|---|\n| Multi-entry destructive actions | Menu, toast, CLI, shell, key binding: do they all confirm the same way before quit/replace/kill? | `kaku-gui/src/frontend.rs`, `update.rs`, `overlay/confirm.rs`, `overlay/confirm_close_pane.rs`, `kaku/src/update.rs` |\n| macOS window geometry | Traffic lights vs tab bar, DPI scale, external display drag, sleep/wake, lock screen, fullscreen first frame | `window/src/os/macos/`, `kaku-gui/src/termwindow/resize.rs`, titlebar paint |\n| Tab chrome | Overflow hit targets, rename modal selection, multi-pane title width, top vs bottom bar | `kaku-gui/src/tabbar.rs`, `termwindow/tab_rename.rs` |\n| Selection / mouse state | Survives Ctrl+L, modal cancel, mouse-reporting panes, successful terminal input? | `kaku-gui/src/termwindow/selection.rs`, mouseevent |\n| Shell integration generation | `local` only inside functions; backticks in bash-heredoc that emit zsh; Starship/PATH scoped to Kaku | `assets/shell-integration/`, `kaku/src/init.rs`, `setup_zsh.sh` |\n| Session restore | Split + scrollback both sides, crash backup, stale snapshot resurrecting closed tabs | `kaku-gui/src/session_restore.rs`, paint/quad buffers |\n| AI dual surfaces | Cmd+L vs `#` vs `k` vs chat: same auth, theme, proxy, remote-cwd tool policy? | `kaku-gui/src/ai_*`, `inline_ai.rs`, `cli_chat/`, overlay |\n| Proxy / network | External API uses system proxy; loopback/private/LAN/`.local`/NO_PROXY go direct | `config/src/proxy.rs`, AI transport |\n| AppKit menu intercept | `Ctrl+letter` only `key_is_down: false` in logs? Menu `keyEquivalent` ate it | `window/src/os/macos/menu.rs`, debug_key_events |\n| Render buffer growth | Over-capacity draw after retry budget; sleep invalidates drawable | `renderstate.rs`, paint/draw paths |\n\nFor a named user symptom, switch to hunt. For a diff under review, switch to check / review.\n\n## 4. Confirm before reporting\n\nA candidate is not a finding until it has evidence produced **this turn**.\n\n- **Confirmed**: source trace that names every entry point, a failing test, or a probe (log, `make test` subset, clean-HOME shell smoke).\n- **Plausible**: complete source path, named failure mode, no run yet. Report separately with the probe that would settle it.\n- **Not a finding**: \"looks risky\" without reading the implementation. Grep the call sites or drop it.\n\nBefore flagging unwraps or panics, confirm production code: skip `#[test]`, fixtures, doctests, and build scripts.\n\n## 5. Rank by blast radius\n\n1. Silent data loss or process kill (update/restart, close all, reset) without confirm on some entry path\n2. Crash loop or unrecoverable launch (session restore, paint)\n3. Stuck UI state (selection latch, frozen frame after sleep, window behind menu bar)\n4. Shell/environment pollution outside Kaku (Starship, PATH, generated zsh)\n5. Visual chrome wrong but recoverable (tab clip, traffic-light gap, font weight)\n6. Docs / copy drift\n\n## 6. Guard what gets fixed\n\nAnything confirmed and fixed needs a regression guard when feasible:\n\n- unit/integration test that fails on unfixed code\n- shellcheck / setup smoke for shell-generation bugs\n- entry-point matrix comment or test listing menu + toast + CLI for destructive flows\n\nVerify with the matrix in root `AGENTS.md`. GUI geometry and AppKit need `make app` or a hand smoke from `.agents/skills/release/SKILL.md` «Pre-release smoke checklist».\n\n## Hard Rules\n\n- **No finding without this-turn evidence.** Speculation wastes review time.\n- **One archetype hit means sweep every sibling entry point.** Report `checked N sites, M defective, K n/a`.\n- **Do not fix while sweeping.** Collect, then fix. Interleaving loses the sweep.\n- **Report clean boundaries as clean.** Do not invent findings to justify the run.\n- **Findings outside the named area get listed, not fixed**, unless the maintainer agrees.\n- **Do not propose UI i18n** (see root `AGENTS.md`).\n\n## Hotspot Map (issue-backed)\n\nUse these as default scopes when the maintainer says \"scan for bugs\" without a path:\n\n| Area | Issue / fix examples | Probe |\n|---|---|---|\n| Update confirm matrix | toast + menu overlay, CLI/brew `confirm_apply_update`, guards in `update.rs` / `frontend.rs` tests | Trace every caller of `restart_to_update` / `spawn_update_helper` / `run_brew_upgrade`; run those unit tests |\n| Titlebar / tabs / DPI | #516, #504, #490, #483, #460 | External display + scaled resolution + tab top/bottom |\n| Window drag / menubar | #508, #456, #408, #414, #477 | Drag to menubar, lock screen, fill desktop |\n| Selection latch | #495, #487, #455 | Ctrl+L, cancel rename, mouse-reporting TUI |\n| Shell init generation | #432, #441, #450, #503, #420 | clean HOME `kaku init`, `zsh -n` generated file, non-Kaku shell |\n| Session restore paint | #514, #448, #482 | split + dual scrollback snapshot |\n| AI auth / overlay | #506, #501, #502, #418 | Cmd+L vs `#`, split resize, appearance flip |\n| Proxy split brain | private base_url + system proxy | loopback OpenAI-compatible smoke |\n\n## Output\n\n```\nArea:        [what was swept, at what depth]\nBoundaries:  [N walked, M applicable]\n\nConfirmed (severity order):\n1. [file:line] [the defect in one sentence]\n   Evidence: [probe / call-graph / failing test]\n   Blast:    [what the user sees]\n   Siblings: [N checked, M defective, K n/a]\n\nPlausible (needs a probe):\n1. [file:line] [defect] -> [the probe that would settle it]\n\nSwept clean:\n- [boundary]: [what was checked, why it holds]\n\nSibling sweep: [pattern signature] -> [N checked, M defective, K n/a]\n```\n\nSay whether anything was fixed or whether this was scan-only.\n",".agents/skills/release/SKILL.md":"---\nname: release\ndescription: \"Run the Kaku macOS release flow end-to-end: preflight, build, notarize, tag, upload to GitHub Releases, dispatch the Homebrew tap, or publish the rolling Nightly preview package. Source of truth is scripts/release.sh and scripts/nightly.sh; this skill records release prerequisites and recovery hints without embedding private machine setup.\"\nwhen_to_use: \"发布, 出版本, 打 tag, release Kaku, ship Kaku, V0.x.x, kaku release, prepare release, dry-run release, resume release, notarize, homebrew tap, nightly, Nightly 包, 更新 nightly, 预览包\"\n---\n\n# Kaku Release Runbook\n\n`scripts/release.sh` is authoritative. This skill captures the release prerequisites and the order Tang and the agent rely on each release. Exact credential locations and one-time recovery commands live in the ignored `.claude/release.local.md`, never in this tracked file.\n\nBefore any stable or Nightly release, run the non-secret checks under **Verify**. The release dry-run is the authoritative credential gate: it accepts either the rcodesign path or the notarytool fallback, including environment overrides. If it passes, do not load machine-specific recovery context. If a signing identity or every notarization path is missing or stale, then check for and read `.claude/release.local.md`; on Tang's release machine it is the required recovery layer. If the local runbook is absent, stop and ask for it instead of guessing a path or replacing credentials.\n\n## TL;DR\n\n```bash\n./scripts/release.sh --dry-run                              # preflight only\nKAKU_ASC_API_KEY_PATH=/dev/null ./scripts/release.sh          # known rcodesign compatibility issue\n./scripts/release.sh                                        # full release\n./scripts/nightly.sh                                        # rolling Nightly DMG for fixes already on main\n```\n\n`release.sh` must run on `main`, with a clean tree, in sync with `origin/main`. `nightly.sh` has no branch or clean-tree gate; it only refuses to publish when HEAD is not yet on `tw93/Kaku`. Both auto-detect the signing identity and read notarization creds from the login keychain.\n\nThe `KAKU_ASC_API_KEY_PATH=/dev/null` prefix selects notarytool before submission for a known non-network compatibility issue. Do not switch backends or disable S3 acceleration to bypass a network failure. Submission and timestamp failures allow one retry on the same route; if it fails again, stop, diagnose read-only, and wait for the maintainer to resolve the network before resuming. A stored profile name does not prove that its notarytool login exists; verify the profile or use the API-key recovery below.\n\n## Nightly preview package\n\n`scripts/nightly.sh` is authoritative for the rolling `nightly` GitHub prerelease. It builds a release-grade universal app, signs, notarizes, staples, copies `dist/Kaku.dmg` to `dist/Kaku-nightly.dmg`, and recreates the `nightly` release. It does not bump versions, create a `V*` tag, update Homebrew, or refresh any stable release surface.\n\nRules:\n\n1. Only publish Nightly from the commit users should test. The script checks that HEAD exists on `tw93/Kaku` before deleting the old nightly release.\n2. Do not tell reporters to try Nightly until `./scripts/nightly.sh` has completed and `gh release view nightly -R tw93/Kaku --json tagName,targetCommitish,publishedAt,assets,url` points at the intended commit with `Kaku-nightly.dmg`.\n3. Do not use `--upload-only` unless the existing `dist/Kaku.dmg` is already known to be a signed, notarized, stapled build from the intended commit.\n4. Keep public wording precise: \"available in the latest Nightly\" only after the check above; otherwise say \"fixed on main and will be in the next Nightly or release.\"\n\n## Credential prerequisites\n\nThe ignored `.claude/release.local.md` is the canonical map from Tang's local backup paths to these script inputs. The tracked contract is limited to what the scripts consume: either `KAKU_ASC_API_KEY_PATH` or the `kaku-asc-api-key-path` login-keychain item for rcodesign, and either `KAKU_NOTARYTOOL_PROFILE` or the `kaku-notarytool-profile` login-keychain item for the fallback. Do not delete or rewrite the local recovery map merely because its paths are machine-specific.\n\n### Preferred: rcodesign + ASC API key\n\n`rcodesign` is preferred on macOS 26+ because notarytool can SIGBUS. The scripts also accept the notarytool fallback when its prerequisites are available; use the release dry-run to check eligibility and the actual notarization result to verify success. It is not in homebrew core.\n\n```bash\ncargo install apple-codesign         # binary: rcodesign, lands in ~/.cargo/bin\n```\n\nThe private JSON supplied through the environment or keychain path must use rcodesign's native `issuer_id`, `key_id`, and `private_key` format. Do not record its location in tracked docs.\n\n### Fallback: notarytool profile\n\nCreate the notarytool profile with the maintainer's private Apple ID, team ID, and app-specific password outside the repository. Pass its profile name through `KAKU_NOTARYTOOL_PROFILE` or the expected login-keychain item; never paste the source values or the import command into a tracked runbook.\n\n### Verify\n\n```bash\nsecurity find-identity -v -p codesigning | grep 'Developer ID Application'\ngh auth status\n./scripts/release.sh --dry-run\n```\n\nWhen the dry-run reports a notarization failure, use `command -v rcodesign`, `security find-generic-password -s 'kaku-asc-api-key-path'`, and `security find-generic-password -s 'kaku-notarytool-profile'` as separate diagnostics. They are not an AND gate: one complete notarization backend is sufficient.\n\n## Pre-release content checklist\n\nBefore invoking `release.sh`, confirm in this order:\n\n1. `kaku/Cargo.toml` and `kaku-gui/Cargo.toml` versions match (e.g. `0.10.0`).\n2. `assets/shell-integration/config_version.txt` is the intended schema version, with highlight rows in the matching docs.\n3. `.github/RELEASE_NOTES.md` first heading is `# V<version> <suffix>` (uppercase V, used as the GitHub Release title).\n4. Both English `Changelog` and Chinese `更新日志` sections in `RELEASE_NOTES.md` cover the same items. Register: one full stop per item, declarative middle register, neither literary nor conversational; the item count follows the number of distinct user-visible outcomes (the previous release's count is history, not a quota), and length is measured against the previous release before publishing.\n5. Any pending fixes are committed and pushed to `origin/main`.\n\nUse `./scripts/prep_release.sh <bump>` to draft the version bump and notes when starting from an older tag. Run it with `--no-edit --no-push`: it commits with the checkout's local git identity, which can be the wrong account for the day, so fill in the notes and the `config_update_highlights.tsv` stub rows, then fold them into that unpushed commit with `git -c user.name=… -c user.email=… commit --amend --reset-author` before pushing (V0.25.0). Tang typically edits the resulting `.github/RELEASE_NOTES.md` by hand to apply the announcement-writing style (community first, 2 to 4 highlights, user-experience framing).\n\n## Pre-release smoke checklist (runtime-only hotspots)\n\nThese are the areas that produce the most post-release bug reports and that CI cannot see (visual layout, native AppKit, shell-in-user-env).\n\nHand the maintainer a release-shaped build for this pass, not `make app`: that one is a debug build, and remote-terminal latency and scrolling feel nothing like the shipped app. `PROFILE=release-opt BUILD_ARCH=native ./scripts/build.sh --app-only` (about 20 minutes, signed with the local identity) differs from the release only in notarization and the second architecture; swapping it into `/Applications` and restarting lets the maintainer test in the Kaku they use every day. Run them by hand in the built `dist/Kaku.app` before tagging. Automated coverage already exists for the testable slices: tab width-budget + hover hit-testing have unit tests in `kaku-gui/src/tabbar.rs`, and `local`-outside-function / shell syntax is gated by shellcheck + the setup_zsh smoke. The list below is what still needs a human.\n\n1. **macOS window** (#408, #414): on first launch, a single click on the title bar / top inset must not maximize the window; drag the window while it fills the desktop; flip system light/dark and confirm all windows refresh; enter and exit fullscreen cleanly.\n2. **Tab bar** (#409, #435, #439, #443, #445, #447): check both `tab_bar_at_bottom` true/false; open enough tabs to overflow a narrow window and confirm titles truncate but every tab stays clickable; rename a tab, then click another to switch and confirm no position scramble; confirm a tmux/shell status prompt renders in the bar without clipping. Exercise the status dot through idle, running, pending or attention, and cleared states on active and inactive tabs, with the tab bar at both the top and bottom; idle must not leave a dot behind.\n3. **Shell setup** (#420, #432, #441, #450): from a clean `HOME`, run `kaku init` and confirm z / syntax-highlight / autosuggestions are active in a fresh shell; run `kaku init --update-only` and confirm it exits clean; open a new shell and confirm `~/.config/kaku/zsh/kaku.zsh` sources with no error.\n4. **AI chat** (#418, #431): run `kaku chat`, quit, then run it again in the same window and confirm it reopens.\n5. **Render timing / stale drawable** (#452, #458): on the bundled WebGpu backend, sleep the Mac then wake it and confirm the window repaints instead of freezing on the old frame while keystrokes still reach the shell; connect or disconnect an external display and confirm no frozen frame or geometry jump; open a new window straight into fullscreen and resize it, confirming it fills without a stale first frame.\n6. **Hyperlinks** (#547): Cmd-hover a URL that the terminal soft-wraps and confirm both rows target the full URL. Then print a URL exactly as wide as the terminal, followed by `\\r\\n` and unindented unrelated text; the target must exclude that text. Terminal and surface tests cover the grouping; check the built app hover target too.\n\n7. **Mouse recovery** (#551): in both zsh and fish, run `printf '\\033[?1003;1006h'` and return to the prompt; moving the mouse must not insert escape sequences. Also keep a mouse-enabled TUI running and confirm mouse input still works until it exits.\n\n8. **Settings lifecycle and custom configs** (#545): start the app with a minimal standalone `--config-file`, switch between Kaku Light and Kaku Dark, and confirm Settings edits that file and the main window follows. Save and exit Settings repeatedly; no destroyed-window repaint panic should appear.\n\n9. **Tab bar pointer paths** (V0.22.0): with 2+ tabs and the bar at the bottom, tap just above a tab and confirm it switches; middle-click to paste on the last terminal row and confirm no tab closes; right-click an inactive tab, choose Close Tab several times and confirm the app stays up and closes that tab while you stay on yours.\n10. **Split panes with dimming** (#562): set `inactive_pane_hsb = { brightness = 0.7 }`, split top|bottom with the tab bar at top and at bottom, focus each pane, and confirm the dimmed background stops exactly at the line with no undimmed strip on the right or bottom edge; split into three panes and confirm two dimmed neighbours still show a faint line; drag the line and confirm it does not jump a row on grab.\n\nWhen a release fixes a bug outside this list, add the reproduction here so the next release re-checks it.\n\n## Verification commands\n\n| Scope | Command |\n|---|---|\n| Format | `make fmt-check` |\n| Compile | `make check` |\n| Tests | `make test` |\n| Release notes version match | `./scripts/check_release_notes.sh` |\n| Config schema versioning | `./scripts/check_release_config.sh` |\n| Config release readiness | `./scripts/check_config_release_readiness.sh` |\n| Full preflight | `./scripts/release.sh --dry-run` |\n\n`release.sh --dry-run` stops after preflight and does not run source checks. The normal release reuses successful CI Checks for the current HEAD; otherwise `stage:checks` runs the local checks below, as described in `AGENTS.md`. A passing dry-run alone is not source-check evidence.\n\n## Stage map\n\n`./scripts/release.sh` runs these stages in order. Each is timed and labeled `[stage:<name>]`.\n\n1. **preflight**: clean git on `main` in sync with `origin/main`, version consistency, gh auth, release notes, config, latest `Build Validation` workflow run on `main` green (an in-progress run only warns), profile, signing identity, notarization creds.\n2. **stage:checks**: reuse successful CI Checks on the current HEAD, or run `make fmt-check && make check && make test` locally. `RUN_CLIPPY=1` adds local clippy in either case. `SKIP_TESTS=1` skips local tests (avoid).\n3. **stage:build**: `./scripts/build.sh`, `PROFILE=release-opt`, `BUILD_ARCH=universal`. Output: `dist/Kaku.app`, `dist/Kaku.dmg`, `dist/kaku_for_update.zip`, `dist/kaku_for_update.zip.sha256`.\n4. **stage:notarize**: `./scripts/notarize.sh`. Tries rcodesign first; falls back to notarytool only for a non-network rcodesign failure when a notarytool profile exists.\n5. **stage:tag**: `git tag -a V<version> -m 'Release V<version>'` then `git push origin V<version>`. Idempotent: reuses an existing tag at HEAD instead of dying.\n6. **stage:upload**: `gh release create V<version>` (or `gh release edit` if it already exists) with the dmg, zip, and sha256. Title is taken from the first `# ` line of `RELEASE_NOTES.md`.\n7. **stage:homebrew-tap**: `repository_dispatch` (`kaku_release_published`) to `tw93/homebrew-tap` triggering `bump.yml`. Polls `Casks/kakuku.rb` through the GitHub contents API for the new version (default 12 attempts, 15s apart). The official `homebrew/cask` token is `kaku` and is bumped by Homebrew livecheck from the GitHub release; this stage only updates the personal tap for existing `tw93/tap/kakuku` installs.\n\n## Resume after failure\n\n`release.sh` accepts these flags to skip already-completed stages:\n\n| Flag | Skips | Use when |\n|---|---|---|\n| `--notarize-only` | build | Build succeeded; notarization failed or was interrupted. |\n| `--upload-only` | build, notarize | Notarized `dist/Kaku.app` and `Kaku.dmg` exist on disk; only need to tag + upload + tap. |\n| `--tap-only` | build, notarize, upload | GitHub release exists; only the Homebrew tap dispatch needs to rerun. |\n\nResume flags require the corresponding artifacts in `dist/` to still be present.\n\n## Common blockers\n\n- **notarytool prints `Submission ID received` and then fails**: the submission was created but the DMG upload to Apple's S3 did not finish (there is no `Successfully uploaded file` line), so that submission stays `In Progress` forever and waiting on it is pointless. Once the network is back, resubmit the same artifacts with `./scripts/release.sh --notarize-only`; a good submission came back Accepted in about four minutes (V0.25.0).\n- **Asset upload fails with `HTTP 408: Upload body timed out` or `EOF`**: the proxy cut the long upload; `gh release create` then deletes the new release and keeps the tag. Rerun `env -u HTTPS_PROXY -u HTTP_PROXY -u ALL_PROXY -u https_proxy -u http_proxy -u all_proxy ./scripts/release.sh --upload-only` (gh reads only proxy variables, not the macOS system proxy). Upload failures may be retried up to five times, 30 seconds apart (maintainer 2026-10-20); V0.26.0 went through on the first direct retry even with the route on a TUN interface. Start a full release with the same `env -u` prefix.\n- **`Local main is not synchronized with origin/main`**: push the pending commit. `release.sh` requires the tag to point at a commit that exists on origin.\n- **`rcodesign` not found**: `cargo install apple-codesign`, then verify with `which rcodesign`. Do not try `brew install rcodesign`; it is not a core formula.\n- **`No Developer ID Application certificate found`**: re-import the certificate from the maintainer-approved private backup through Keychain Access, using the password from the private runbook. Never add either location to this file.\n- **`Notarization credentials not found`**: follow the ignored private setup runbook to provision one of the environment/keychain inputs described under Credential prerequisites, then rerun `./scripts/release.sh --dry-run`. The script otherwise prompts interactively, which fails in non-interactive shells.\n- **rcodesign S3 connect timeout**: `s3 upload error: HTTP connect timeout occurred after 3.1s` identifies the failed upload connection, not a credential fault. `notarize.sh` retries once on rcodesign and stops if the network failure persists. Preserve the signed artifact, report the blocked submission, and resume only after the maintainer resolves the network; do not switch to notarytool to retry the same failure.\n- **`No Keychain password item found for profile`**: a profile-name entry alone does not establish a working login. An installed Apple `notarytool` can reuse the existing ASC `.p8` key with `--key`, `--key-id`, and `--issuer`, without creating a Keychain login. Resolve the inputs from the private runbook and keep their values out of displayed commands and logs. For S3 upload failures, apply the same one-retry network budget above and keep the signed artifact fixed. After `Accepted`, retrieve the submission log and verify its SHA-256 against the submitted DMG, then run `./scripts/notarize.sh --staple-only` and `./scripts/nightly.sh --upload-only` from the same source commit. See [Apple's notarization API workflow](https://developer.apple.com/documentation/notaryapi/submitting-software-for-notarization-over-the-web).\n- **Homebrew tap verifier timed out at 12/12**: the tap workflow commits `kakuku <version>` to `tw93/homebrew-tap` main on success; verify with `gh api repos/tw93/homebrew-tap/commits/main --jq .commit.message`. The verifier reads the cask through the GitHub contents API (`Accept: application/vnd.github.raw`), not the Fastly-cached `download_url`. Network errors retry once and stop, and other API errors stop immediately; the 12-poll budget only waits for a version change. A version-poll timeout means the expected version was not observed: open the run URL the script printed. If the tap commit is present the release is complete. Widen the window with `HOMEBREW_TAP_VERIFY_ATTEMPTS` / `HOMEBREW_TAP_VERIFY_SLEEP_SECONDS`, or rerun `./scripts/release.sh --tap-only`; `REQUIRE_HOMEBREW_TAP_UPDATE=0` only downgrades the failure to a warning.\n- **`rcodesign notarization failed` right after the log shows `\"status\": \"Accepted\"`**: Apple accepted; the step died stapling. rcodesign cannot staple an unsigned DMG, which is what `build.sh` produces, and the script used to pass `--staple` (removed 2026-09-10). Do not touch credentials. Recover with `./scripts/notarize.sh --staple-only` then `./scripts/nightly.sh --upload-only`.\n- **`codesign: A timestamp was expected but was not found`** (or `timestamp service is not available`): the timestamp request failed during signing; this message alone does not establish a certificate fault or identify the network cause. `build.sh` retries timestamp failures once before aborting. If that retry fails, stop the release, perform read-only network diagnosis, and report the blocked signing step. Resume only after the maintainer resolves the network problem; do not rerun the build or change proxy settings to retry the same failure.\n- **`codesign: code object is not signed at all, In subcomponent: .../MacOS/k`** while signing `kaku-gui`: `kaku-gui` is the bundle's main executable and codesign validates its siblings as nested code, so every other executable in `Contents/MacOS` must already be signed. `build.sh` signs `kaku` and `k` before `kaku-gui` since 2026-09-10; if a new helper binary is added to the bundle, add it to that loop before `kaku-gui`.\n- **`stapler` fails with CloudKit timeout, exit code 68**: the log reads `The staple and validate action failed! Error 68` right after `Notarization accepted`. Notarization already succeeded; only ticket retrieval from `api.apple-cloudkit.com` timed out. Do not re-notarize. One `./scripts/notarize.sh --staple-only` retry is allowed; if the network failure repeats, stop for read-only diagnosis and wait for the maintainer to resolve it. Only after stapling succeeds, resume with `./scripts/release.sh --upload-only`. The staple-only path regenerates `kaku_for_update.zip` and its sha256 on purpose: attaching the ticket changes the app, so the in-app update archive must be rebuilt after stapling, never before.\n- **Tag already exists on origin at a different SHA**: do not force-push tags. Pick the next patch number, bump versions, and start over.\n\n## Environment variable overrides\n\n| Variable | Default | Purpose |\n|---|---|---|\n| `KAKU_SIGNING_IDENTITY` | auto-detect | Override the Developer ID Application identity. |\n| `KAKU_ASC_API_KEY_PATH` | keychain item `kaku-asc-api-key-path` | Path to rcodesign ASC API key JSON. |\n| `KAKU_NOTARYTOOL_PROFILE` | keychain item `kaku-notarytool-profile` | notarytool keychain profile name. |\n| `HOMEBREW_TAP_TOKEN` | `gh auth token` | GitHub token for tap dispatch. |\n| `REQUIRE_HOMEBREW_TAP_UPDATE` | `1` | Set to `0` to allow release to succeed when tap dispatch fails. |\n| `HOMEBREW_TAP_VERIFY_ATTEMPTS` | `12` | Polls of the tap cask before the verifier gives up. |\n| `HOMEBREW_TAP_VERIFY_SLEEP_SECONDS` | `15` | Seconds between tap cask polls. |\n| `RUN_CLIPPY` | `0` | Set to `1` to run `cargo clippy` during stage:checks. |\n| `SKIP_TESTS` | `0` | Set to `1` to skip `make test` during stage:checks. |\n| `OUT_DIR` | `<repo>/dist` | Override artifact output directory. |\n| `PROFILE` | `release-opt` | Cargo profile. Only `release` and `release-opt` are accepted. |\n| `BUILD_ARCH` | `universal` | Passed through to `build.sh`. |\n\n## After release\n\n- GitHub Release URL: `https://github.com/tw93/Kaku/releases/tag/V<version>`.\n- Official Homebrew cask users get the new version after Homebrew livecheck/bump; verify with `brew update && brew info --cask kaku`. Personal-tap users get it once the tap workflow finishes; verify with `brew info --cask tw93/tap/kakuku`.\n- In-app updates (`kaku-gui/src/update.rs`, Kaku's own updater, not Sparkle) download `kaku_for_update.zip` and its `.sha256` from the latest GitHub Release.\n- Add the six positive reactions to the new release; this is part of shipping. Skip `-1` and `confused`.\n\n  ```bash\n  id=$(gh api repos/tw93/Kaku/releases/tags/V<version> --jq .id)\n  for c in +1 laugh heart hooray rocket eyes; do\n      gh api -X POST \"repos/tw93/Kaku/releases/$id/reactions\" -f content=\"$c\"\n  done\n  ```\n\nFor the announcement post (X / WeChat): community first, 2-4 highlights, user-experience framing, one opinionated sentence. The release notes file is a different artifact; do not paste it as the announcement.\n","term/AGENTS.md":"# Terminal Agent Guide\n\n## Scope\n\n`term/` contains terminal emulation core behavior, including screen state, escape sequence handling, selection, and PTY-facing terminal semantics.\n\n## Where to Look\n\n- `term/src/terminal.rs` - main terminal state and event handling\n- `term/src/terminalstate/` - detailed state transitions\n- `term/src/screen.rs` - screen buffer behavior\n- `term/src/test/` - terminal-core regression tests (`selection.rs`, `csi.rs`, `c0.rs`, `c1.rs`, `core_emulation.rs`)\n- `crates/wezterm-escape-parser/` - escape sequence parsing (CSI/OSC/ESC/APC); `term` consumes it, it does not live under `term/src/`\n- `kaku-gui/src/selection.rs` and `kaku-gui/src/termwindow/selection.rs` - selection and copy behavior (GUI side, not `term`)\n\n## Practical Rules\n\n- Already encoded input uses `send_text`, without paste framing or newline conversion. Local panes share the terminal's existing `ThreadedWriter` with keys and paste, preserving order without blocking on a full PTY.\n- Keep GUI assumptions out of terminal core.\n- Color scheme reporting uses pane-local DEC mode 2031 and DSR 996/997. Notify subscribers after effective configuration palette changes, including same-mode palette changes, and answer subsequent OSC color queries from the new palette. Reloads with an unchanged effective palette and application-issued OSC color writes do not notify; cancellation, RIS and DECSTR clear the subscription. `color::is_light_color` is shared with config/window chrome so the reported scheme stays consistent with Kaku's colors.\n- Keep normal-screen and alternate-screen behavior distinct; wheel scrolling and inline AI status must not corrupt terminal state.\n- Preserve selection and cursor invariants that GUI overlays and inline assistant status depend on.\n- Only the terminal wrap attribute proves that physical rows form one logical line. Full-width output followed by text in column zero can still be independent commands. Keep both indented and unindented hard-newline regressions, alongside soft-wrap coverage, when changing hyperlink grouping (#547).\n- For TUI re-render corruption, reduce the report to an ANSI transcript and cover cursor motion plus erasure in terminal-core tests before changing GUI code. Full-line erase of a physical row must also leave wrap state consistent, especially for styled prompts that previously wrapped.\n\n## Verification\n\n- Compile checks: run `make check`.\n- Terminal logic changes: run `make test` or the narrow cargo nextest filter for the affected behavior.\n- Cross-crate behavior: also inspect `mux/AGENTS.md` and `kaku-gui/AGENTS.md`.\n\n## Cross-References\n\n- `../mux/AGENTS.md` - owns panes and terminal instances.\n- `../kaku-gui/AGENTS.md` - renders terminal surfaces.\n- `../termwiz/AGENTS.md` - terminal UI primitives.\n","lua-api-crates/AGENTS.md":"# Lua API Crates Agent Guide\n\n`lua-api-crates/` contains Rust-to-Lua API bindings for user configuration scripts.\n\n## Scope\n\n`lua-api-crates/` exposes runtime capabilities to Lua, including:\n- mux and pane controls\n- window and spawn helpers\n- ssh and system integration helpers\n- utility modules like logging, time, and filesystem\n\n## Where to Look\n\n- `lua-api-crates/mux/`: pane/tab control APIs\n- `lua-api-crates/window-funcs/`: window-level APIs\n- `lua-api-crates/spawn-funcs/`: process spawn APIs\n- `lua-api-crates/ssh-funcs/`: SSH-related Lua APIs\n\n## Practical Rules\n\n- API behavior here is user-facing and breaking-change sensitive.\n- Be careful with user-var and event-facing APIs, since GUI hooks and config reload signals rely on those event paths.\n- Keep proxy, config reload, and user-var event semantics stable for AI and automation workflows that observe Lua-facing state.\n- `PaneInformation` (used in title-formatting events) is fields-only userdata: method-style calls and `if pane.get_xxx` probes silently no-op instead of erroring. See \"Bundled kaku.lua Pitfalls\" in [`config/AGENTS.md`](../config/AGENTS.md) before extending it or its consumers.\n\n## Cross-References\n\n- [`mux/AGENTS.md`](../mux/AGENTS.md) - Mux controls exposed to Lua.\n- [`config/AGENTS.md`](../config/AGENTS.md) - Lua config execution environment.\n- [`kaku-gui/AGENTS.md`](../kaku-gui/AGENTS.md) - Window functions and GUI integration.\n","mux/AGENTS.md":"# Mux Agent Guide\n\nThe `mux` crate owns tab, pane, domain, and mux state management.\n\n## Scope\n\n`mux` is the central abstraction for:\n- tabs and panes\n- windows and workspaces\n- local and remote domains\n- client/server mux coordination\n\n## Where to Look\n\n- `mux/src/tab.rs`: tab tree and split behavior\n- `mux/src/pane.rs`: pane state and interfaces\n- `mux/src/window.rs`: window-level mux state\n- `mux/src/domain.rs`: domain abstractions\n- `mux/src/client.rs` and `crates/wezterm-mux-server-impl/src/` (`sessionhandler.rs`, `dispatch.rs`, `local.rs`): protocol flow\n\n## Practical Rules\n\n- Already encoded input uses `send_text`, without paste framing or newline conversion. Local panes share the terminal's existing `ThreadedWriter` with keys and paste, preserving order without blocking on a full PTY.\n- A foreground process group can outlive its leader (`seq | less -X`). When the leader has no executable path, identify a live member of that same group from the cached shell tree. A foreground-group change, exec or leader exit invalidates the leader immediately and the fallback tree must be sampled after that leader update; checking only PGID misses a producer that exits after being cached. Do not wait for the normal TTL, substitute an unrelated background job, or scan all processes on every frame. Release the tree cache guard before calling Lua hooks that can query the pane again.\n- Keep alert propagation reliable, including `Alert::SetUserVar`, because GUI reload and user-var hooks depend on this path.\n- Preserve pane and overlay invalidation signals consumed by GUI AI/chat overlays when panes split, move, close, or resize.\n\n## Known Pitfalls\n\n- **Alert propagation**: `Alert::SetUserVar` must reach the GUI side reliably; broken propagation silently breaks config reload and user-var event hooks.\n- **Pane ID stability**: Pane IDs must remain stable across mux round-trips; don't reassign or reuse IDs in ways that confuse client/server sync.\n- **Domain abstraction leakage**: GUI-specific logic (rendering, window handles) must not appear in `mux`; only mux-level state and events.\n- **Split tree correctness**: Tab split tree mutations in `tab.rs` are tricky; always verify with a pane layout sanity check after modifications.\n- **Overlay resize regressions**: GUI overlays derive geometry from pane state. Mux changes that skip resize or removal notifications can leave AI/chat overlays stale.\n\n## Cross-References\n\n- [`kaku-gui/AGENTS.md`](../kaku-gui/AGENTS.md) - GUI rendering for mux state.\n- [`lua-api-crates/AGENTS.md`](../lua-api-crates/AGENTS.md) - Lua APIs for mux control.\n- [`term/AGENTS.md`](../term/AGENTS.md) - Terminal instances owned by panes.\n",".claude/rules/macos.md":"# Kaku macOS Platform Rules\n\n> macOS 26 上 AppKit 自动注入是 PAC-fault / 崩溃的高频来源。改 menubar / window handling 前必读本文。\n\n## 两套像素坐标空间，禁止混算\n\n窗口/屏幕几何存在两套空间，只在\"窗口所在屏 scale == 主屏 scale\"时数值兼容，混用的 bug 只在外接屏暴露（#456 拖动跳变）：\n\n- **Canonical ScreenPoint 空间**：`cartesian_to_screen_point` 定义（全局 point × `screens[0]` 主屏 scale、Y 翻转）。`set_window_position` / `window_position` / `MouseEvent::screen_coords` / `MouseEvent::window_origin` 都在这里。\n- **窗口 backing 空间**：`MouseEvent::coords`、`dimensions.pixel_*`、UI item 布局坐标，用窗口自己屏幕的 backingScaleFactor。\n\n规则：跨空间换算一律在 `window/src/os/macos` 层做；GUI 层需要窗口原点用 `MouseEvent::window_origin`，不要用 `screen_coords - coords` 推算；居中走 `WindowOps::center()`（原生、单屏 point 空间）。`ScreenInfo.rect`（`screens()` / `resolve_geometry` / `--position` / Lua `screens()`）已统一到 canonical 空间（`nsscreen_to_screen_info` 走 `cartesian_to_screen_point`），`Window::new_window`（`window/src/os/macos/window.rs`）不再对 resolve 出的 x/y 除 scale（除一次就是 upstream 继承的 Retina 落点折半 bug，size 的除法保留，那是 px 转 pt 给 NSWindow 用的）。\n\n## 默认渲染后端是 WebGpu，CGL-only 的 wake 修复护不住它\n\nbundled `kaku.lua` 设了 `front_end = 'WebGpu'`（Metal via wgpu）。`window/src/os/macos` 里的睡眠/唤醒防护（`SYSTEM_SLEEPING` flush 闸门、`BackendImpl::update`、display-change present defer）全部只作用于 CGL 路径，对默认用户是 no-op。给 sleep/wake/display-reconfig 加修复时必须同时考虑 WebGpu 路径（#458）。\n\nWebGpu surface 失效的恢复入口在 `TermWindow::do_paint_webgpu`：休眠唤醒会让 Metal drawable 失效但窗口尺寸不变，`WebGpuState::resize` 对相同尺寸是故意 no-op（热路径），所以恢复必须走 `reconfigure_surface()` 强制 `surface.configure`，不能用 `resize(dims)` 代替。wgpu Metal 后端把 nil `nextDrawable` 映射成 `SurfaceError::Timeout`，所以 Timeout 连续出现同样要触发重配，不能永远静默跳帧。症状签名：键盘输入照常进 PTY、画面停在旧帧，说明是渲染管线死了而不是主线程卡死。\n\n## AppKit Menu 自动注入是地雷\n\nNSApplication 默认会向 Window menu 和 Help menu 注入自己的项（Tile / Move & Resize / Bring All to Front / Help Search），并把 NSWindowRepresentingMenuItem 挂在 Windows menu 下做 dangling reference 。在 macOS 26 上，这些注入项可能 PAC-fault：\n\n- `-[NSMenu _performKeyEquivalentWithDelegate:]` 在 Ctrl+Cmd+F 路由时崩\n- Windows menu 关闭窗口后 NSWindowRepresentingMenuItem dangling\n- Help Search field 触发 menu 重新计算时崩\n\n## 必须执行的兜底\n\n- 不要调 `setWindowsMenu:` 让 AppKit 接管 Windows menu。Kaku 有自己的 tab/window 切换器。\n- 不要调 `setHelpMenu:` 让 AppKit 注入 Help Search。Kaku 没有 help book。\n- 窗口创建时调 `setExcludedFromWindowsMenu:` 让 AppKit 看不见我们的 NSWindow，避免它把窗口加到 Windows menu。\n\n参考历史 commit：\n- `1ec3823` - 第一次排查 NSWindowRepresentingMenuItem dangling\n- `4b15bab` - 进一步 stop AppKit from injecting into Window/Help menus\n- `fc2dfa9` - redirect AppKit Spotlight for Help to an orphan menu\n- `6ba7381` - restore synchronous menubar init to prevent key event loss\n\n## Menubar 初始化时序\n\nMenubar **必须同步初始化**。延后到 async block 会丢早期按键事件（Cmd+Q 在启动后立刻按下会被吞）。见 `6ba7381`。\n\n## Key Equivalent\n\n新增 KeyAssignment 前确认走 Kaku 自己的按键分发路径，不要走 AppKit menu 的 `performKeyEquivalent`。后者会经过被 PAC-fault 的注入项。\n\n### menu keyEquivalent 会拦 keyDown，吞掉 raw-mode TUI 的 Ctrl+letter\n\nmacOS 26 上 NSMenu 的 keyEquivalent modifier 匹配并不严格相等。给 menu item 装 `Ctrl+letter` 形式的快捷键，即使带额外 modifier（如 `NSFunctionKeyMask` 想做 `Fn+Ctrl+letter`），仍会拦截 plain `Ctrl+letter` 的 keyDown。后果：\n\n- AppKit 把 keyDown 路由给 menu item action，NSWindow 收不到。\n- keyUp 不走 `performKeyEquivalent:`，所以 keyUp 照常进 Kaku。\n- `termwiz/src/input.rs` 的 `encode` 对 `is_down=false` 直接返回空字符串，PTY 上一个字节都不会到。\n- 普通 shell `cat -v` 测试看到 `^C` 不代表 raw-mode 也工作；启动时机和 cooked vs raw 都会让 menu 拦截表现不一致。**必须**在 raw-mode TUI (claude / codex / vim / htop) 里实测。\n\n兜底：任何\"模仿系统快捷键\"的 menu 项宁可 `keys: vec![]` 不设快捷键，让用户从菜单点。`kaku-gui/src/commands.rs` 里的 keyEquivalent 装配逻辑必须统一从按键候选本身取 modifiers，不要为个别 menu 项走强制 modifier 的特例化路径（历史上的特例化路径已删，不要加回）。\n\n参考历史：`8aba4475` feat(window): add CenterWindow menu without macOS shortcut（Window > Center 刻意不装 keyEquivalent，因为 macOS 26 会把 `Fn+Ctrl+C` 当 plain Ctrl+C 拦了，导致 raw-mode 下 Ctrl+C 不退 claude/codex）。\n\n### 排查 keyDown 丢失\n\n`config.debug_key_events = true` 重启 Kaku，复现按键，看 `~/.local/share/kaku/kaku-gui-log-<pid>.txt`：\n\n- grep `key_event.*CTRL`。\n- 如果只有 `key_is_down: false`、没有 `key_is_down: true`，就是 menu keyEquivalent 拦了 keyDown。\n- 这种情况下不要去查 termwiz / PTY / termios，先找 `set_key_equiv_modifier_mask` 装配点。\n\n## Cocoa 对象所有权：StrongPtr::new 只接 alloc / new / copy\n\n`StrongPtr::new` 接管的是 +1 引用，只能包 `alloc`+`init`、`new`、`copy` 拿到的对象。`+[NSMenuItem separatorItem]` 这类类方法返回的是 autorelease 的 +0 对象，必须用 `StrongPtr::retain`，否则 drop 时多 release 一次。V0.22.0 前 `MenuItem::new_separator()` 就是这么写的：主菜单栏在启动时构建，那时还没有 autorelease pool（从代码推断，未实测），多出的那次 release 被泄漏抵消，所以一直没暴露；第一个在事件处理里构建、带分隔线的菜单（右键 tab 菜单）一弹出就崩（`7b6b6ce2`，带回归测试）。\n\n- 崩溃签名：主线程 `objc_release > AutoreleasePoolPage::releaseUntil > objc_autoreleasePoolPop > -[NSApplication run]`，紧跟在一次原生菜单动作之后。看到它先审 `StrongPtr::new` 包的是不是非 alloc/new/copy 来的对象，再怀疑 AppKit。\n- 拿不准某个 API 返回 +0 还是 +1，用 `clang -fno-objc-arc` 写个十行的 MRC 程序在 `@autoreleasepool` 里打印 `retainCount`，实测，不靠记忆。\n- 新增在事件处理中构建的 `NSMenu`（右键菜单等），必须在 `make app` 的包里真的右键点一次菜单项再下结论，单元测试看不到 autorelease pool 的时序。\n- 配置重载后菜单栏在 macOS 上也会重建（#570）：`frontend.rs` 的 reload 回调在 `spawn_into_main_thread` 任务里调 `recreate_menubar`，不在可能持有 config 锁的回调里直接调。原先挡住它的 TODO 疑似就是上面这个分隔线多释放，修复后放开。外观切换也会触发 `config::reload()`，所以这条重建路径在每次深浅色切换时都会走。`recreate_menubar` 给 Kaku 加的每一项打 `KAKU_MENU_ITEM_TAG`，重建时只删带这个 tag 的项、保留顶层菜单对象，再从每个菜单的开头按 rank 插入新项。不要改成 `remove_all_items` 清空主菜单或换新的 NSMenu：AppKit 只在启动时往 Edit 菜单注入 AutoFill、Start Dictation、Emoji & Symbols，菜单对象一换这些项就再也回不来，再调一次 `setMainMenu:` 也不行（MRC 小程序实测）。也不要换回旧的 mark-sweep：它只扫 `kakuPerformKeyAssignment:` 项，Kaku 菜单的固定项只在子菜单新建时创建、分隔线每次都追加，复用旧菜单会丢固定项、分隔线越积越多、新项排到末尾。`config_reload_rebuilds_menubar_on_macos_in_place` 守这几点。随 mux 状态变化的命令（Attach/Detach Domain、非 local 的 New Tab (Domain X)、Switch to workspace）只进命令面板不进菜单栏：菜单栏只在配置重载时重建，启动时 domain 还没注册，放进去会在一次深浅色切换后突然多出 `~/.ssh/config` 每个 Host 两条 Attach 项，之后又随 attach 状态过时，`mux_state_commands_stay_out_of_the_menubar` 守这一点。\n\n## 调试方式\n\n- 怀疑 menu 注入或菜单对象相关崩溃时：读 `~/Library/Logs/DiagnosticReports/kaku-gui-*.ips` 里最近的 crash report。\n- 看 `NSMenu _performKeyEquivalentWithDelegate:` 栈帧是 AppKit 注入项的标志。\n- 主要复现环境是一台 macOS 26 实机。\n","termwiz/AGENTS.md":"# Termwiz Agent Guide\n\n## Scope\n\n`termwiz/` contains terminal UI primitives, widgets, input handling helpers, and rendering utilities used by Kaku command-line and overlay flows.\n\n## Where to Look\n\n- `termwiz/src/` - primary library code\n- `termwiz/src/widgets/` - reusable widgets\n- `termwiz/src/lineedit/` - line editing behavior\n- `termwiz/src/render/` - rendering helpers\n\n## Practical Rules\n\n- Keep primitives generic; do not leak Kaku-specific policy into reusable widgets.\n\n## Verification\n\n- Compile checks: run `make check`.\n- UI primitive logic changes: run the narrow affected tests, then `make test` if behavior is shared.\n- Overlay or GUI consumers: inspect `kaku-gui/AGENTS.md` and verify the consuming flow.\n\n## Cross-References\n\n- `../kaku-gui/AGENTS.md` - GUI and overlay consumers.\n- `../term/AGENTS.md` - terminal core behavior.\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/tw93/Kaku/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Kaku Agent Guide\n\nKaku is a macOS-native terminal emulator derived from WezTerm and shaped around AI-assisted terminal workflows. This guide is the shared operating context for agents working in this repository.\n\n## Repository Map\n\n- `kaku/` - CLI entry points, command flows, and user-facing configuration commands.\n- `kaku-gui/` - GUI, rendering, window lifecycle, input, mouse handling, AI chat, and the `k` helper binary.\n- `mux/` - tabs, panes, domains, and client/server state.\n- `term/` - terminal emulation and screen buffer behavior.\n- `termwiz/` - terminal UI primitives.\n- `config/` - Lua config loading, schema behavior, proxy settings, and versioned defaults.\n- `window/` - platform windowing layer.\n- `lua-api-crates/` - Rust-to-Lua API bindings.\n- `crates/` - shared utility crates, including Kaku-specific AI helpers.\n- `assets/` - app resources, bundled config, shell integration, and vendor assets.\n- `scripts/` - build, release, and validation helpers.\n- `docs/` - user and developer documentation.\n- `.agents/skills/` - the canonical and tracked home for project skills: `release` and `bugs`. The website skill `product-docs` lives with the site on the `vercel` worktree (`~/www/kaku-site/.agents/skills/product-docs`). `.claude/` is gitignored except for the force-added `.claude/rules/macos.md`; its skill entries may be relative symlinks or ignored private overrides. Do not cite, copy, or edit an ignored `.claude/skills/` directory as project truth. Put durable skill changes in `.agents/skills/` first, then update a symlink when that entry is actually a link.\n- `.github/workflows/checks.yml` - fast correctness gates on pushes to `main` and on pull requests, with `paths-ignore` for `**.md` and assets, so a docs-only change gets no CI at all. Five macOS jobs, the runner's concurrency cap: Format, Unit Tests, Relay Build, Clippy (which also carries the source guards and stands in for `cargo check`, scoped to the lint-opt-in crates, not the workspace), and Scripts and Release Checks (release notes, config version, shell smoke tests, shellcheck). Adding a sixth macOS job makes one queue.\n- `.github/workflows/build-validation.yml` - release-shaped builds with the shipped `release-opt` profile: one job per architecture in parallel (the arm64 one also assembles the bundle), then a `lipo` merge of `kaku`, `kaku-gui` and `k`; runs on build-pipeline changes, daily (skipped when that commit already passed), or on dispatch. `release.sh` preflight requires its latest run green and the latest Security Audit run not failed, and skips its local fmt/check/test when CI Checks already passed on HEAD.\n- `.github/RELEASE_NOTES.md` - source for the GitHub Release title and body.\n\n## Commands\n\n```bash\nmake fmt\nmake fmt-check\nmake check\nmake test\nmake dev\nmake app\n./scripts/build.sh\n./scripts/check_config_release_readiness.sh\n./scripts/check_release_config.sh\n./scripts/check_release_notes.sh\n```\n\n`make build`, `make check`, `make test` and `scripts/build.sh` first run `scripts/prune_target.sh`, which measures `target/` at most once a day and runs `cargo clean` once it reaches `KAKU_TARGET_CAP_GB` (default 40). cargo never drops old artifacts, and the profiles, architectures and remote on/off feature sets here grew it to 92G; a partial, age-based delete was rejected because it can leave fingerprints pointing at missing files. No scheduled job does this, by the maintainer's choice. The dev profile also builds third-party crates without debug info.\n\n`make fmt` and `make fmt-check` both shell out to `cargo +nightly fmt`, so they require the nightly toolchain. Use `make app` for GUI, rendering, windowing, and AI overlay verification because it builds the app bundle that users run.\n\n## Working Rules\n\n- Work on the current branch unless the maintainer asks for a branch or worktree.\n- Keep changes inside one crate or subsystem when the problem allows it.\n- Upstream: Kaku forked WezTerm at `05343b38` (2026-01-17; term/, mux/, the escape parser and wezterm-font matched it byte for byte). On 2026-10-14 upstream main up to `e2a98e1b` was reviewed and its macOS-relevant fixes absorbed (commits citing upstream shas, e.g. DECRQCRA, RIS reset, OSC 8 `;`, tmux -CC input, workspace repaint). Start the next sync from `e2a98e1b`; map `wezterm-gui` to `kaku-gui` and top-level crates to `crates/`, and judge applicability rather than applying patches literally.\n- Kaku is macOS-only. Windows code (cfg(windows) branches, ConPTY/GDI/WSL paths, winapi and the other Windows-only dependencies) was removed on 2026-10-13; do not bring it back when porting upstream WezTerm fixes, port only the macOS-relevant half. `wsl_domains`, `win32_system_backdrop` and `win32_acrylic_accent_color` stay as deprecated config keys so shared configs still load (`windows_only_keys_still_load`). Vendored crates under `deps/` keep their upstream Windows code untouched.\n- Inspect public APIs and cross-crate boundaries before changing shared behavior.\n- Kaku must not automatically inject agent hooks, instructions, or settings. Explicit edits through `kaku ai` may update the selected provider's configuration (including Claude and Codex settings), preserving user-owned fields. Agent-facing help belongs in documentation. The `kaku init` source line in `.zshrc` is managed by `assets/shell-integration/setup_zsh.sh` and checked by `doctor.rs`; `reset.rs` must keep every user-owned line around it.\n- Narrowing a default that existing users already depend on must ship the other camp's explicit path in the same change. Shell integration scopes the Starship prompt to `TERM_PROGRAM=Kaku` OR (`$TMUX` and `KAKU_SESSION`), with `KAKU_PROMPT_EVERYWHERE=1` as the opt-in for every terminal; Smart Tab stays Kaku-only (#503). The predicate is duplicated inside each installer, three times in `setup_zsh.sh` and four in `setup_fish.sh`, so grep both files and change every copy. `assets/shell-integration/tests/prompt_scope_smoke.sh` gates the placements in CI via `.github/workflows/checks.yml`, and `docs/faq.md` is the user-facing half; a change that re-narrows the scope must update both installers, the smoke and the doc together.\n- Tab indicator decision (2026-08-24): the tab bar draws a dot only for `Progress::Paused | Progress::Error` (attention, `kaku-gui/src/tabbar.rs`); a running pane deliberately shows nothing, the running-state dot was dropped in `4f297556`. Do not bring it back.\n- Retro tab bar chrome (2026-09-09, #548): the bundled config (`assets/macos/Kaku.app/Contents/Resources/kaku.lua`) sets `use_fancy_tab_bar = false`, so the shipped bar is the retro one even though the `config/src/config.rs` schema default is true. `kaku-gui/src/tabbar.rs` composes a `Line` of cells, so a control there is a character on the text baseline competing with the title for the one trailing cell the rule above spends on attention; drawn controls belong in the box-model renderers (`make_x_button` in `kaku-gui/src/termwindow/render/fancy_tab_bar.rs`, and the opt-in touch renderer in `kaku-gui/src/termwindow/touch.rs`). The per-tab close mark added in `cef3a371` (three cells, \" X \", over the title end) was reverted in `732d2082` for that reason. On 2026-10-12 it was tried again as a single hover `×` in the status cell, with one and then two cells of padding per side; the maintainer judged it worse than none (a grid glyph cannot sit centered between title and edge) and removed it: the desktop layout closes tabs from the tab's right-click menu, middle-click and Cmd+W, while the web terminal keeps its hover close icon, which is drawn and centered. Answer tab bar chrome requests with the pointers that already exist: the new-tab button is on by default (bundled `show_new_tab_button_in_tab_bar = true`), right-click on a tab opens a tab menu with New Tab, Tab Navigator and Close Tab (`tab_context_menu_actions` in `kaku-gui/src/termwindow/mouseevent.rs`), middle-click closes a tab, and the Shell menu lists New Tab and Close Tab. For touch over remote desktop, a plain left press (a tap) up to `BOTTOM_TAB_BAR_TOUCH_EXTENSION_PT` above a bottom bar still hits tabs and the new-tab button, while middle and right presses there stay with the terminal, so the bar itself stays one row. Do not re-add a native close mark: if one is ever wanted, it has to be a drawn, centered icon like the fancy bar's `make_x_button`, not a grid character. The desktop bottom retro bar is one terminal row with line_height (`tab_bar_pixel_height_impl`), the same as a top bar; the compact natural height read as cramped. Touch mode (2026-10-14) is a per-window toggle with one entry, Window > Touch Mode; the on-bar Touch button and the titlebar entry were removed as clutter. It keeps the normal bar height and look and only adds a drawn, centered close icon to every retro tab, over cells the bar keeps at the tab's end (`close_slots` in `kaku-gui/src/tabbar.rs`). The separate 40pt touch bar, its Menu button and panels were removed; do not bring them back. A long press (left press held 450ms, under 24pt of movement, acted on at release) replays as a right click in every mode, so tab and terminal context menus cover touch; UU was measured to forward a long press as a held left button, never a right click.\n- Line height decision (2026-07): the bundled default stays `line_height = 1.28`. Character-cell graphics (QR codes, `neofetch` logos, TUI bar charts) scale with the row height, so they render about 28% taller than at `line_height = 1.0`, and that trade-off is accepted. Rendering block glyphs at their natural cell height was tried in `d810b13` and reverted in `3f131ea`: block characters must fill the whole cell or box borders and QR codes open horizontal seams. Do not propose lowering the default or re-attempting natural-height block rendering; point users at the `line_height` entries in `docs/faq.md` and `docs/configuration.md`. The block cursor is a separate case and correctly keeps using `natural_cell_height` (see `kaku-gui/AGENTS.md`).\n- Draft issue and PR replies unless the maintainer has already approved the exact public action.\n- Do not modify files outside this repository without showing the intended change and getting explicit confirmation.\n- Do not add instructions for the removed `website/` tree unless that directory exists in the current worktree.\n- The marketing and docs site lives on the `vercel` branch (linked worktree at `~/www/kaku-site`), not on `main`. It follows the design guide at `~/www/kaku-site/DESIGN.md`; verify changes with screenshots at 375px / 1280px and deploy by pushing the `vercel` branch (Vercel serves kaku.fun).\n- Keep private credentials, local keychain paths, and machine-specific release notes out of public repository docs.\n- **Do not propose UI i18n / multi-language menus / a `config.language` setting.** The `rust-i18n` based Chinese UI localization (PR #362, commit `f6cfb4b`) was reverted in `b4d779a` on 2026-05-18; `language` remains in the config schema as a deprecated field for backward compat only. UI strings (menus, confirm dialogs, config TUI copy) stay as English literal strings. New UI surfaces should not introduce translation keys, locale-aware formatting, or \"what if a user wants Chinese\" abstractions. If a user requests a non-English UI, route to the assistant config / AI chat surface; those already accept non-English content.\n- **Kaku stays free, including Kaku Assistant and remote access (maintainer 2026-10-18).** Kaku Assistant runs on the user's own API key or Codex login, so it has no cost to meter. The relay (`crates/kaku-relay`, one Fly machine) is paid for by the maintainer as a thank-you to the community, and `docs/faq.md` says so. Do not propose paid tiers, usage-based billing, accounts or usage metering for the relay; when it nears the concurrency limit in `fly.toml`, load test and move to a larger machine instead.\n- **Do not pre-bake provider abstractions in `kaku/src/ai_config/`.** Kaku Assistant parsing, field presentation, and persistence live in `kaku/src/ai_config/tui/providers/assistant.rs`; the TUI retains its event loop and shared data types. Future extraction should move one concrete provider at a time into that existing directory. Do not add an unused trait, a `ProviderKind` enum, or stub modules. Save Copilot for last because its OAuth flow is the abstraction stress test.\n- CLI Codex readiness and GUI connection parsing both consume `tests/fixtures/codex-connection.json`. Add boundary cases there when either policy changes, and run both crates' tests.\n- Official Homebrew token is `kaku` (`brew install --cask kaku`). `tw93/tap/kakuku` remains the personal tap for people who installed before the official cask. Do not tell users the Homebrew `kaku` token is a different app.\n\n## Maintainer Follow-up\n\n- For current issue and PR sweeps, read live GitHub state first with `gh issue list` and `gh pr list`; refresh once more before final conclusions or public actions.\n- Before commenting on or closing an item, confirm its title, state, and author with `gh issue view` or `gh pr view`.\n- The latest stable tag is `git tag --list 'V*' --sort=-version:refname | head -1`; without the `V*` filter the rolling `nightly` tag sorts first. Map each report to `git log <tag>..HEAD`, and treat a closed issue as a claim until the fix commit or remaining gap is named.\n- Shell integration fixes run the affected `assets/shell-integration/tests/*_smoke.sh`, then the list `.github/workflows/checks.yml` runs under `Shell integration smoke tests`. Watch the pushed `main` run with `gh run watch <id> --exit-status`.\n- Do not close issues or PRs on local green alone. For fixes pushed to `main`, wait for the new GitHub Actions run on `main` to pass before posting fixed/closed replies.\n- The rolling `nightly` release is not rebuilt by push. Before sending users to Nightly, run or verify `./scripts/nightly.sh` and confirm `gh release view nightly --json tagName,targetCommitish,publishedAt,assets,url` points at the fix commit and includes `Kaku-nightly.dmg`.\n- Default issue-closure pipeline once a fix is verified: commit the fix, refresh the `nightly` release assets per the rule above, reply in the reporter's language with the Nightly download or in-app update path, then close if the current request already authorizes closure; otherwise propose it and wait for authorization. Do not promise a specific packaged-release date in replies.\n- Before pushing `main`, run `git fetch origin main` and verify `origin/main` has not moved unexpectedly. If it moved, stop and review `origin/main..HEAD` before pushing.\n- For several unrelated bugs, prefer one commit per issue or behavior, and put the issue number in the subject: `fix(scope): #123 short summary`.\n- Merge contributor PRs with `gh pr merge --merge`, never squash, so the contributor's own commits and authorship stay in history; add no trailer. When `maintainerCanModify` is true, push fix commits to the contributor's branch rather than rewriting their existing commits, and approve a first-time contributor's workflow run with `gh api -X POST repos/tw93/Kaku/actions/runs/<id>/approve` before expecting CI.\n- If an accepted PR's equivalent fix lands outside the contributor branch, acknowledge the contribution and explain which user-visible fix was delivered before closing it; keep commit hashes and internal attribution bookkeeping out of the public reply.\n\n## Investigation Order\n\nWhen scope is incomplete, inspect in this order:\n\n1. User-provided repro, failing command, or failing test.\n2. Entry point for the behavior, usually `kaku/src/main.rs`, `kaku/src/cli/`, or `kaku-gui/src/main.rs`.\n3. Owning subsystem document and target crate.\n4. Immediate cross-crate boundary used by the call path.\n5. Narrow tests, fixtures, snapshots, or scripts that reproduce the behavior.\n\nFor AI-facing behavior, inspect in this order:\n\n1. CLI and assistant configuration under `kaku/src/ai_config/`, `kaku/src/assistant_config.rs`, and `config/src/proxy.rs`.\n2. GUI AI state and transport under `kaku-gui/src/ai_*`, `kaku-gui/src/ai_chat_engine/`, and `kaku-gui/src/cli_chat/`.\n3. Overlay UI under `kaku-gui/src/overlay/ai_chat/`.\n4. Shared helpers in `crates/kaku-ai-utils/`.\n\nFor AI transport bugs around custom `base_url`, keep both proxy paths true: external API hosts should use detected system proxy settings, while loopback, private LAN, link-local, CGNAT/Tailscale-style, `.local`, `NO_PROXY`, and macOS ExceptionsList model endpoints should connect directly. Verify with the macOS system proxy enabled and a loopback or internal OpenAI-compatible smoke before saying it is fixed. Do not claim general SOCKS support unless that transport is actually implemented and verified.\n\nFor `Ctrl+letter` not working in a raw-mode TUI (the most common shape: `Ctrl+C` / `Ctrl+R` works in plain shell but not inside a TUI overlay), inspect in this order:\n\n1. AppKit menu `keyEquivalent` intercepting `keyDown` before the terminal sees it. Enable `config.debug_key_events = true`, restart the app, then `grep 'key_event.*CTRL' ~/.local/share/kaku/kaku-gui-log-<pid>.txt`. If the log shows only `key_is_down: false` and no matching `key_is_down: true`, the AppKit menu absorbed the event; do not chase termwiz or PTY.\n2. Cooked-mode tests (`cat -v` showing `^C`) do **not** rule out menu interception. Reproduce inside a raw-mode TUI before forming a hypothesis.\n3. Only after step 1 rules out menu interception, inspect termwiz encoding (`termwiz/src/input.rs`), then PTY / termios state.\n\nFor TUI display corruption after interactive CLIs re-render prompts or selection lists, first capture a minimal ANSI transcript. Add a terminal-core regression around cursor-up (`CSI n A`), full-line erase (`CSI 2K`), cursor-down (`CSI 1B`), wrapped rows, and styled prompt symbols. If the core transcript passes but the built app differs from Terminal.app, inspect GUI width, cell metrics, resize, and wrapping inputs rather than changing terminal semantics blindly.\n\n## Subsystem Guides\n\n| Subsystem | Guide | Scope |\n|---|---|---|\n| GUI | `kaku-gui/AGENTS.md` | Rendering, window lifecycle, input, mouse |\n| Mux | `mux/AGENTS.md` | Tabs, panes, domains, client/server |\n| Terminal | `term/AGENTS.md` | VT emulation, screen buffer |\n| Config | `config/AGENTS.md` | Lua loading, schema, config reload |\n| Termwiz | `termwiz/AGENTS.md` | TUI primitives and widgets |\n| Lua API | `lua-api-crates/AGENTS.md` | Rust-to-Lua bindings |\n| Crates | `crates/AGENTS.md` | Shared utility crates |\n| macOS platform | `.claude/rules/macos.md` | AppKit menu / keyEquivalent traps, menubar init timing |\n\n## Verification\n\n| Change type | Command |\n|---|---|\n| Rust compile check | `make check` |\n| Rust logic change | `make test` |\n| Formatting | `make fmt-check` |\n| Lints | `cargo clippy --locked --all-targets -p kaku -p kaku-gui -p mux -p config -- -D warnings` (`make check` does not run clippy, so a change that passes locally can still turn CI's Clippy job red) |\n| GUI or rendering change | `make app` |\n| CLI main menu input | `make app`, then `python3 scripts/test_main_menu.py` (real PTY, q/Q/Esc exit) |\n| Config release change | `./scripts/check_config_release_readiness.sh` and `./scripts/check_release_config.sh` |\n| Release note change | `./scripts/check_release_notes.sh` |\n| Release-adjacent change | `make fmt && make check && make test`, then `make app` |\n| `crates/kaku-relay` change | `cargo check --locked --manifest-path crates/kaku-relay/Cargo.toml` (it is in `workspace.exclude`, so `make check` and `make test` never see it; CI's `relay-check` job is the only gate) |\n\nFor GUI or rendering issues, read `kaku-gui/AGENTS.md` first and verify with `make app`, not only `make dev`.\n\nRestarting Kaku (2026-10-14, narrowed 2026-10-21): run `scripts/restart_kaku.sh` only when the maintainer asks for a restart in the current message (重启 Kaku). Swapping a new build into `/Applications`, or an earlier request in the session, is not a request to restart: finish the swap, then ask and wait, since the restart ends the maintainer's own windows and any agent session inside Kaku. It restarts into `/Applications/Kaku.app` the way an in-app update does (TERM, then reopen; tabs return from the periodic session snapshot) and detaches first. It stops every running Kaku GUI, a copy opened from `dist/` included, but sends TERM only to the one that owns `last_session.json` (its pid names the snapshot's `content_dir`) and SIGKILLs the rest after it exits, so a stray copy cannot overwrite the tabs when it quits. An agent session running inside Kaku ends with it, so write the reply first, tell the maintainer to reopen the session with `claude -c` (or the equivalent), and make the script the last action. Its log is `~/Library/Logs/kaku-restart.log`. The detached run is marked with a `--detached` argument, never an environment variable: `/usr/bin/open` hands the caller's environment to the app, so a variable reached every shell in the reopened Kaku and the next restart skipped detaching and died with it (`test_leaked_detach_variable_still_detaches`). An absent snapshot or empty process list must still reach the reopen step; `scripts/test_restart_kaku.py` exercises those branches and owner-only saving with fake processes, never the running app.\n\nStanding authorization (2026-10-19): after a verified change to `crates/kaku-remote/assets/web` is pushed, publish the relay page with `scripts/publish_web_page.sh` without asking. Confirm the deploy reached `t.kaku.fun` by fetching `https://t.kaku.fun/index.html` and finding a string the change added, and check the response's CSP when the change touches the script or the publish script.\n\nWhen a check needs clicks on the built app, hand the maintainer an isolated launch command (`open -na dist/Kaku.app --env HOME=<scratch home with a copy of their kaku.lua>`) and numbered taps with the expected result for each, instead of driving synthetic mouse events: the maintainer's own Kaku takes focus back and the test window may sit on another Space. Static screenshots with `screencapture -l` are still fine when the window is on screen. A shell running inside Kaku carries `KAKU_CONFIG_FILE`, `KAKU_CONFIG_DIR`, `KAKU_SESSION`, `KAKU_UNIX_SOCKET`, `KAKU_ZSH_DIR` and `XDG_CONFIG_HOME`, and `open` hands them to the app, so a test instance launched without `env -u` for each reads the maintainer's real config and session, and may connect to the relay; shell smoke tests run from such a shell need the same unsets. Remote state lives in `$TMPDIR/kaku-remote.json` and `kaku-remote-relay.json`, which a scratch HOME does not isolate: a test instance with remote on overwrites the maintainer's copies, so say that their Kaku needs a restart afterwards for `kaku remote` and `kaku doctor` to read its own state again. Look up the test window by the test instance's pid and capture nothing when that yields no id, since an empty `-l` once captured the maintainer's own window. Native fullscreen does not capture reliably this way; ask the maintainer for that screenshot instead. Four traps from the 2026-10-19 site captures: do not pass `--args start` to `open`, since AppKit hands bare arguments to `application_open_files`, the first pane runs `start ; exit` and the window closes before it is seen; drop `config.remote` from a copied `kaku.lua`, or after a config reload the test instance can attach to the maintainer's mux and `send-text` lands in their pane; `yazi` in a test pane is the shell wrapper, which, like Cmd+Shift+Y, rewrites the maintainer's `~/.config/yazi/theme.toml` from the test config's `color_scheme`, so run the Yazi binary itself with `YAZI_CONFIG_HOME` on a scratch copy; and size or move the test window with one System Events expression per action (`set size of window 1 of (first process whose unix id is <pid>)`), since a process reference kept in a variable resolves by name and reached the maintainer's `kaku-gui` window. Check the result per pid with `CGWindowListCopyWindowInfo`. For a remote capture, launch the test instance with a scratch `HOME` and `TMPDIR` as well: pairing state, the identity and `remote.lock` live under `$HOME/Library/Application Support/kaku` and the relay files under `$TMPDIR`, so only both keep the maintainer's remote untouched; end with `kaku remote off` under the same environment. In Safari, address a window you opened by its AppleScript `id`, never `front window`, which on 2026-10-19 resolved to the maintainer's Start Page window and resized it; editing the address bar opens history and Favorites over the page, so capture with the field unfocused (Tab moves focus into the page).\n\n## Current Risk Areas\n\n- In-app update replace/restart must confirm on every entry path (menu Check for Updates, menu Restart to Update, toast, CLI direct ZIP, brew cask). Do not set `KAKU_UPDATE_AUTO_CONFIRM` for exploratory menu check; only the overlay-confirmed path may auto-confirm. Sibling entry points tend to regress independently; after changing one path, sweep the matrix and keep the guards in `kaku-gui` (`user_facing_update_events_route_through_confirm`) and `kaku` (`brew_and_direct_paths_confirm_before_replace`) green. Details: `kaku-gui/AGENTS.md` and `.agents/skills/bugs/SKILL.md`.\n- AI chat and shell flows are active product surfaces. Preserve `fast_model`, proxy config, inline `#` query status, syntax highlighting, approval flow, and conversation state behavior.\n- `config_version` bumps every release; the source of truth is `assets/shell-integration/config_version.txt` and the gate is `scripts/check_release_config.sh`. Config schema changes must update bundled defaults, docs, release checks, and migration behavior together. Per-version history and the migration rule (only keys that existed in the previous released version need migration code) live in `docs/config-versions.md`. Do not hardcode the current version number in agent guides; it goes stale between releases. A config_version bump is also the only thing that regenerates an existing install's `~/.config/kaku/zsh/kaku.zsh`, so a shell-integration fix reaches stable users through the bump; a Nightly built without one needs `kaku init --update-only`, and replies pointing at that Nightly must say so.\n- GUI regressions can come from overlay resize, pane split/removal, macOS worker thread lifetime, WebGPU surface reconfigure, tab bar spacing, and alternate-screen wheel scroll behavior.\n- Startup performance depends on caching shell user vars, Lua bytecode, early appearance queries, GLSL version, and built-in fonts. Do not invalidate those caches without measurement.\n- `kaku doctor` ends every run by writing `Kaku-Diagnose.zip` (`kaku/src/diagnostics.rs`). It also collects the `kaku-hang-*.txt` stack samples that `kaku-gui/src/stall_watchdog.rs` writes when the GUI thread stays blocked past 1s, so rename that prefix in both files together. Everything in the bundle goes through its redactor, which also hides typed text in `debug_key_events` lines (printable keys, physical key codes, composed and IME text) including committed `KeyCode::Composed` text as well as `DeadKeyStatus::Composing`, while keeping Ctrl and Cmd keys readable for key reports, `assistant.toml` is never read, and large files (session content can be gigabytes, #559) are listed by size, not copied.\n- Notification actions that call back into Kaku should resolve bundled executables relative to the running app, not an assumed system path.\n- Known high-regression zones: theme/config TUI initialization (`kaku/src/ai_config/tui/`), macOS window geometry (`window/src/os/macos/window.rs`, `kaku-gui/src/termwindow/resize.rs`), and menubar initialization timing (`window/src/os/macos/menu.rs`). Changes touching these must ship with a regression test or assertion.\n- `assets/shell-integration` scripts run in the user's shell, not just at build time. Bash heredocs that generate zsh (e.g. `setup_zsh.sh` writing `kaku.zsh`) expand backticks and `$(...)` at generation time, so escape any that must reach the output literally (#450), and never put `local` outside a function (#432/#441). An expired `.zcompdump` needs `compinit && touch` after a successful full check: `compinit` can leave an unchanged dump's timestamp untouched. Keep the failure path stale and run `assets/shell-integration/tests/compinit_cache_smoke.sh`, which checks the next shell returns to `-C`. CI gates this: shellcheck (`--severity=error`, catches SC2168) over the bash scripts plus a `zsh -n` parse check of the generated `kaku.zsh` in the setup smoke.\n- User-owned configuration symlinks must survive updates, and failed writes must leave their targets unchanged. Resolve the final target and finish a sibling temporary file before replacing it; preserve target permissions. The shell `write_through` helper has four copies across the zsh/fish installers and their generated Yazi wrappers. Test all four under a real write failure, not only successful linked-file updates (#581, #582).\n\n## Release Notes\n\nTag format is `V0.x.x`. `scripts/release.sh` is the source of truth for tagged releases. The GitHub Release title comes from the first heading in `.github/RELEASE_NOTES.md`.\n\nBefore drafting release notes, read the previous formal release (`gh release view <latest-tag>`) and treat it as the format template: title `V{version} {Codename}` with a one-word codename, a centered logo header with the product tagline, `### Changelog` as numbered `**Label**: one sentence` items, a `### 更新日志` section whose Chinese items map one-to-one by number, and a `> https://github.com/tw93/Kaku` footer. After publishing, add all six positive reactions (`+1`, `laugh`, `heart`, `hooray`, `rocket`, `eyes`) to the release via `gh api` and read them back to confirm; never add `-1` or `confused`.\n\n## Pre-release Runtime Smoke\n\nCI gates fmt/check/clippy/tests but cannot see visual layout, native AppKit, render timing, or shell-in-user-env behavior, which is where most post-release reports come from. The checklist lives in one place: `.agents/skills/release/SKILL.md` «Pre-release smoke checklist», covering macOS window, tab bar, shell setup, AI chat, and render timing, with the issue numbers each item guards and a note on which slices now have automated coverage. Run it by hand in the built `dist/Kaku.app` before tagging, and add the reproduction there when a release fixes a bug outside the list.\n\nReleases that touch windowing, titlebar coloring, tab bar layout, or transparency also need the config matrix in `docs/release-checklist.md`, which enumerates the tab position / tab style / opacity / window state combinations to check by hand and names the `update_titlebar_background()` regression guard.\n\n## Documentation Maintenance\n\n- Single-crate behavior belongs in that crate's `AGENTS.md`.\n- Cross-crate behavior should update every affected subsystem guide.\n- Build, CI, release, and maintainer workflow changes belong in this root file.\n- Shared agent instructions belong in tracked docs. Personal overrides belong in ignored local files.\n- One-off review reports, scorecards, and diagnostic snapshots are evidence, not durable project docs. Extract stable rules or verification gates into `AGENTS.md`, `CLAUDE.md`, subsystem guides, scripts, or tests, then remove the transient report.\n- Do not hide user-visible behavior changes inside maintainability or cleanup patches. New UI, config fields, defaults, or workflow permissions should be split into their own change unless the maintainer explicitly approved that scope.\n","isInternal":false,"tokens":7507,"sizeBytes":31496},{"name":"AGENTS.md","path":"config/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/tw93/Kaku/HEAD/config/AGENTS.md","title":"config - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Config Agent Guide\n\nThe `config` crate owns config loading, Lua integration, schema behavior, proxy settings, versioned defaults, and AI configuration.\n\n## Scope\n\n`config` controls:\n- loading user and bundled configs\n- Lua execution and binding lifecycle\n- schema mapping between Lua and Rust\n- config subscriptions and reload behavior\n- AI model, fast-model, and proxy settings consumed by GUI and CLI flows\n- versioned default config and release readiness checks\n\n## Where to Look\n\n- `config/src/config.rs`: load/parse flow\n- `config/src/lib.rs`: config API and subscriptions\n- `config/src/proxy.rs`: proxy configuration used by AI and network-facing flows\n- `config/src/version.rs`: app version and target-triple accessors (`wezterm_version()`), not `config_version`; that number lives only in `assets/shell-integration/config_version.txt`, read by `kaku/src/init.rs` and the bundled `kaku.lua`\n- `assets/macos/Kaku.app/Contents/Resources/kaku.lua`: bundled fallback config\n\n## Config TUI (`kaku/src/config_tui/` and `kaku/src/tui_core/`)\n\nThe config TUI is the interactive terminal UI for editing Kaku config. It lives in the `kaku` CLI crate, not the `config` crate.\n\n- `kaku/src/config_tui/` - TUI app: `mod.rs` (form state and main loop), `ui.rs` (rendering)\n- `kaku/src/tui_core/` - shared TUI primitives, currently `theme.rs` only (`accent`, `bg`, `muted`, `panel`, `primary`, `red`, `success`, `text_fg`)\n- `tui_core::theme` is consumed by `config_tui/ui.rs`, `ai_config/tui/ui.rs`, and `tui_splash.rs`. New primitives shared by more than one TUI flow belong in `tui_core`, not copied per feature.\n\n## AI Config TUI (`kaku/src/ai_config/`)\n\nThe AI config TUI lives in the `kaku` CLI crate and shares terminal UI primitives with `config_tui`.\n\n- Keep `fast_model`, primary model, and proxy settings aligned between CLI config, GUI AI chat, and documentation.\n- Do not duplicate form widgets or debounce behavior outside `kaku/src/tui_core/`.\n- Verify AI config changes against both config parsing and the visible TUI flow.\n\n## Practical Rules\n\n- Loading priority: user config first, bundled config second.\n- Keep reload-safe behavior for startup hooks and subscriptions.\n- `color::is_light_color` re-exports the terminal core's classification so `COLORFGBG`, window chrome and DEC mode 2031 reports use the same palette policy. Keep that shared policy when changing color classification.\n- After mutating a live `TermConfig` client palette, call `pane.set_config(config)` so the terminal compares its subscribed palette snapshot and emits mode 2031 notifications. Updating only the provider bypasses the terminal's reload boundary.\n- `KAKU_CONFIG_FILE` is an output, not an input. Config loading exports it (`config/src/config.rs`) and `effective_config_file_path()` (`config/src/lib.rs`), the GUI single-instance check (`kaku-gui/src/main.rs`), `assets/shell-integration/setup_{zsh,fish}.sh`, and the bundled `kaku.lua` all read it back. Do not turn it into a config path override; the only supported override is `--config-file` (`CONFIG_FILE_OVERRIDE`).\n- Keep bundled fallback config authoritative at `assets/macos/Kaku.app/Contents/Resources/kaku.lua`.\n- Kaku Dark, Kaku Light, and the Kaku Theme alias must also resolve for standalone user configs that do not load the bundled Lua defaults. Their `src/scheme_data.rs` mirrors are checked against every bundled palette field by `builtin_kaku_scheme_mirrors_match_complete_bundled_palettes`; synchronize both when changing a Kaku palette.\n- Preserve compatibility with runtime reload callers that trigger `config::reload()` from GUI-side signals.\n- `config::reload()` can return without applying: once a config file is loaded, a watcher reloads 100ms after that file changes, and whichever reload started last wins. A test that rewrites its config file and reloads must set `automatically_reload_config = false` in that file, or a slow runner reads the old values (`ssh_pane_titles_keep_the_formatter_on_hover`). CI runs each test in its own process with nextest, so a flaky global-config test is racing a thread, not another test.\n- The current `config_version` is whatever `assets/shell-integration/config_version.txt` says; never trust a number written in a doc. Any version bump must update bundled config, release checks, docs, and migration expectations together, and add a row to `docs/config-versions.md`.\n- New config fields are user-facing behavior. Keep them out of pure cleanup/refactor patches unless the maintainer explicitly approved the product change, and update bundled defaults plus documentation in the same change when they do land.\n- Keep alternate-screen wheel scroll behavior configurable; terminal and GUI defaults must not diverge.\n- Config rewrite guards must validate every assignment the writer can replace, using the same match predicate. A later Lua override can contain user-owned settings even when the first assignment contains only a managed flag; `remote_access_checks_every_assignment_before_rewriting` covers this boundary in `kaku/src/config_tui/mod.rs`. Remote toggles must edit `effective_config_file_path()` through `kaku::utils::write_atomic`, preserving symlinks, permissions and the original bytes on a partial write failure. `kaku/tests/remote_config.rs` exercises the actual CLI with `--config-file`, a stale inherited `KAKU_CONFIG_FILE`, and a real file-size write limit.\n\n## Bundled kaku.lua Pitfalls\n\n- **200-locals hard limit.** LuaJIT caps a chunk at 200 local variables, and the top-level\n  chunk of `assets/macos/Kaku.app/Contents/Resources/kaku.lua` is already at capacity.\n  Adding one more top-level `local` makes the whole config fail to load at startup. New\n  helper functions and values must be nested inside existing functions or tables. After any\n  edit, verify the file still loads:\n\n  ```bash\n  luajit -e \"assert(loadfile('assets/macos/Kaku.app/Contents/Resources/kaku.lua'))\"\n  ```\n\n  `scripts/check_kaku_lua_loads.sh <path>` wraps the same check and doubles as a PostToolUse hook for edits to this file.\n\n- **PaneInformation is fields-only.** In title-formatting paths (`format-tab-title`,\n  `format-window-title`), the `pane` object is a `PaneInformation` userdata that exposes\n  fields only, no methods. Method-style access such as `pane:get_foreground_process_name()`\n  or capability probes like `if pane.get_xxx then` do not error; they silently evaluate to\n  nil, so the guarded branch never runs (#485 shipped dead code this way). Use the\n  documented fields, prefer the `WEZTERM_PROG` user var for the running command, and treat\n  argv-derived titles as untrusted (argv can leak environment values).\n\n## Cross-References\n\n- [`kaku-gui/AGENTS.md`](../kaku-gui/AGENTS.md) - GUI config consumers and reload signals.\n- [`lua-api-crates/AGENTS.md`](../lua-api-crates/AGENTS.md) - Lua APIs that expose config values.\n- [`mux/AGENTS.md`](../mux/AGENTS.md) - Alert propagation for config changes.\n","isInternal":false,"tokens":1617,"sizeBytes":6926},{"name":"AGENTS.md","path":"crates/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/tw93/Kaku/HEAD/crates/AGENTS.md","title":"crates - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Crates Agent Guide\n\n`crates/` contains shared utility crates used across the workspace. Inspect the target crate's `Cargo.toml` before editing because Kaku keeps some WezTerm-derived crates and adds its own helpers over time.\n\n## Scope\n\nKey groups:\n\n| Group | Crates |\n|-------|--------|\n| Terminal core | `codec`, `color-types`, `vtparse`, `wezterm-cell`, `wezterm-escape-parser`, `wezterm-surface` |\n| GUI and input | `wezterm-font`, `wezterm-input-types`, `wezterm-gui-subcommands`, `wezterm-open-url`, `wezterm-toast-notification` |\n| Mux, client, and remote | `wezterm-client`, `wezterm-mux-server-impl`, `wezterm-ssh`, `wezterm-uds`, `kaku-relay`, `kaku-remote` |\n| Platform and process | `pty`, `filedescriptor`, `procinfo`, `env-bootstrap`, `umask`, `async_ossl` |\n| Lua and config support | `luahelper`, `wezterm-dynamic` |\n| Shared utilities | `promise`, `base91`, `bidi`, `bintree`, `frecency`, `lfucache`, `rangeset`, `ratelim`, `tabout`, `wezterm-blob-leases`, `wezterm-char-props`, `wezterm-version` |\n| Kaku-specific utilities | `kaku-ai-utils`, `kaku-unicode-names` (generated Unicode name table lifted out of `kaku-gui/src`; regenerate with `ucd-generate`, do not hand-edit) |\n\n## Practical Rules\n\n- For macOS notification actions that launch Kaku commands, prefer resolving bundled executables relative to the running `kaku-gui` binary.\n- Keep `kaku-ai-utils` provider-agnostic; provider policy belongs in GUI/CLI call sites and config, not in low-level helpers.\n- When shared cache helpers affect startup, verify the GUI consumer and avoid invalidating hot-path caches unnecessarily.\n- `wezterm-escape-parser` ProgramStatus accepts whitespace around key/value pairs and skips lexically malformed optional pairs without losing valid state. Preserve decoded-text control-character and size checks; permissive pair parsing must not weaken those boundaries. Regressions live in `src/osc.rs` (`program_status_trims_pair_whitespace`, `program_status_skips_malformed_optional_pairs`, and `program_status`).\n\n## Web Terminal and Relay\n\n`kaku-remote` serves a browser terminal (`src/web.rs`, page in `assets/web/`) and `kaku-relay` lets it be reached from outside the LAN (`src/share.rs` on the Mac side). The `remote` cargo feature is on by default in `kaku` and `kaku-gui`, so Release and Nightly ship it; the feature itself stays off until the user turns it on. Invariants:\n\n- Launched in testing, free (2026-10-17): every build carries the web terminal, `remote.enabled` defaults to false, and `kaku remote` asks before turning it on, so nothing changes for a user who never runs it. The private-build machinery (`nightly.sh --private`, the `check_no_web_terminal.sh` publish guard, `release.sh` refusing the feature, `kaku remote` hidden from `--help`) was removed at launch. Remote access remains free, including the relay paid for by the maintainer; do not add paid tiers, accounts, usage metering or registration tokens.\n- Off until the user turns it on (2026-10-16): `remote.enabled` defaults to false. `kaku remote` asks before turning it on and writes `config.remote = { enabled = true }` to the user's kaku.lua through the settings screen's line writer (`config_tui::set_remote_enabled`); `kaku remote off` writes false. Neither rewrites a `config.remote` table that holds other keys (a relay of their own), which a whole-line rewrite would drop; they say to edit it instead. The bridge follows config reloads (`follow_config`): it starts when the setting turns on, without restarting Kaku, and when it turns off its runtime is dropped, which ends the relay link and every web session within a second, and the state file is removed so `kaku remote` no longer sees sharing. Verify off/on transitions in the same process; restarting the app does not test release of `remote.lock`.\n- One Kaku per data directory serves remote access (2026-10-19): `launch_when_sole` holds a `flock` on `DATA_DIR/remote.lock`; a second GUI on the same data directory (a build-folder copy, another `kaku start`) waits and takes over when the first quits. Before this, a second copy registered with the relay under the same identity and took the browser over to its own terminals. Drift checks name the frame the page drew (`seq`, `Shown::rows_after`), so they still run while frames are in flight over the relay; checking only with nothing in flight never ran while Claude Code worked.\n- No LAN access (2026-10-16 decision, replacing the 2026-10-15 opt-in): the local server listens on 127.0.0.1 only and `remote.bind` is deprecated and ignored, kept so configs that set it still load. The maintainer has no use for it at home and it was plain HTTP: `http://192.168.x.x` is not a secure context, so `crypto.subtle` is missing (measured in Helium), and the relay's https page cannot open a `ws://` LAN socket (mixed content). Do not bring LAN access back.\n- The local server answers only requests whose `Host` is its own (`127.0.0.1`, `localhost` or `[::1]` with the listener's own port, so a config reload that edits `remote.port` does not lock the page out), so a web page that rebinds its name to 127.0.0.1 cannot read same-origin responses. No route serves the token: `kaku remote` reads it from the 0600 state file. The routes for the old iOS client (`/api/config`, `/api/panes`, `/qr` with the token, `/ws/{pane_id}`, and the `ai_chat` handler in `kaku-gui/src/ai_remote.rs`) were removed on 2026-10-14 with the maintainer's approval: its relay mode had already stopped working, and `/qr` handed the token to any local process, other macOS users included. A future iOS app should speak the web terminal's protocol or wrap the page, not bring these back.\n- A session ends after `SESSION_SILENCE` (180 s) without a message from its browser, so a half-open socket from a sleeping laptop does not hold App Nap off forever.\n- One session per browser, carried either by the local WebSocket (`#t=<token>` link) or by the relay (a paired browser, see below). `run_session` sees transport-neutral input and output queues; keep socket and encryption logic out of it.\n- The page mirrors Kaku's own terminal model as ANSI frames rather than replaying the PTY stream, so terminal queries are answered once, by Kaku. Keys go through `Pane::key_down`, pastes through `send_paste`, and already encoded text through `send_text`. Local panes use the terminal's existing ordered background writer for all three; never send Remote text directly to the blocking PTY writer.\n- Kaku Assistant's chat is a GUI overlay that never reaches the mux, so the page would show and type into the shell under it. The GUI reports it with `set_ai_chat_open` (open, close, and a chat dropped on config reload); while it is open `handle_input` drops typing, pastes and clicks for that pane, an upload is saved without pasting its path, and the page dims the pane with a notice. Chat on the page itself was declined (2026-10-18): it would be a second chat UI, and using it from a browser reads as odd; do not build one.\n- Anything that reads the Lua config (for example `can_close_without_prompting`) must run on the GUI thread via `promise::spawn::spawn_into_main_thread`; calling it from the server's tokio threads panics.\n- Relay ids are the first 128 bits of SHA-256 of a secret that never leaves the Mac, so nobody else can register as that Mac. Through the relay only paired browsers get a session (`src/pairing.rs`, 2026-10-15; the old `#k=<key>` link, one AES key for every browser, is retired and its pages are told to reload). `kaku remote` writes a one-time offer (`remote-pairing.json`, 10 minutes) and prints `/{id}/#p=<secret>&m=<Mac public key>`; the page makes a non-extractable ECDSA P-256 device key and keeps it in IndexedDB once Kaku has admitted it. Each connection: Kaku sends `KCH3` and an ephemeral P-256 key in the clear, the page answers `KCB3` and its own, both derive one AES-256-GCM key per direction by HKDF-SHA256 over the ECDH secret salted with the transcript (SHA-256 of `kaku-remote v3`, the relay id, both ephemeral keys). Kaku's first sealed message signs the transcript with its long-term key (`mac_key` in `remote-share.json`), which the page checks against the key from its pairing link before sending anything, so a relay cannot pose as the Mac; the page's first sealed message signs it with its device key (`auth`), adding an HMAC of the transcript under the offer secret the first time (`pair`). Messages carry their direction label and position as associated data. Close codes: 4403 not a paired browser (the page forgets its key), 4401 pairing link expired or used, 4426 page from before pairing, 4408 no proof in time; the relay must pass each through (`browser_close`), or the page reads it as busy and retries. Pairing shows no dialog, because the Mac is often unattended when a browser is added from elsewhere: the link works once, every new device posts a macOS notification naming it, and so does a paired device connecting again after 30 minutes away (`QUIET_RETURN`; reloads and reconnects stay quiet), which is the Mac-side sign of remote use (no menubar or tab bar mark, by the maintainer's choice on 2026-10-15), and `kaku remote devices` / `kaku remote revoke <id>` list and remove devices (sessions of a removed device end within seconds). ring's ECDH takes only ephemeral keys, which is why long-term identity is a signature, not a static DH key; ring's fixed ECDSA signatures and WebCrypto's are the same r||s form.\n- Each web session holds `awake::Awake`, a latency-critical `NSProcessInfo` activity. Without it, App Nap throttles the backgrounded GUI's main thread, which turns PTY output into the pane change the session renders from, and every echo waited 100-200 ms. Measure remote input latency with the Kaku window hidden, not focused.\n- `Shown::update` turns a screen whose rows moved into `CSI n S` / `CSI n T` plus the new rows. That relies on each row starting with its own `CSI row;1H` and on the page never setting a scroll region; keep both, or the browser's copy drifts from Kaku's.\n- Clipboard writes from programs (OSC 52, `MuxNotification::AssignClipboard`) also go to browsers viewing that pane (`web::publish_clipboard`, 1 MB cap); the Mac clipboard keeps getting them. Find (`Search`) runs on the Mac over the pane's history, so it reaches past what the page shows. Files a browser drops or pastes arrive in chunks, in a bounded 512 KiB window of 16 KiB chunks, each acknowledged (`UploadAck`), so its percentage is what reached the Mac; a session that ends mid-file deletes the partial file (`Uploads` drop) and the page resends after reconnecting. Files are written to `~/Downloads/Kaku` under a sanitized, de-duplicated name (`safe_file_name`, `unused_path`, 50 MB cap), and their path is pasted into the original drop pane with `quote_dropped_files`, as a native drop would be.\n- Scrolling a plain shell (`local_scroll` in each frame: no alternate screen, no mouse grab, and no foreground `less`) never round-trips: the page fetches history rows with `History` in pages of `HISTORY_PAGE` and scrolls them in `#past`, laid exactly over the terminal with each run placed at Kaku's column. Programs that take the mouse or the alternate screen still get the scroll on the Mac; `less -X` also receives scroll keys, as in the native wheel handler, and Find still pins the Mac-side view (`View::pinned_top`).\n- The page names tabs by `tab_id` and reorders them per `window_id`; rename and move go through `TabAction::Rename` / `TabAction::Move` in `kaku-gui/src/remote_tabs.rs`, which keep the Mac's active tab. A split tab lists its `panes`; viewing one sends `Activate`, which also focuses that split on the Mac. A split tab keeps the Mac window's size (`split_claim`): the browser claims nothing for it, shows the viewed split at its own size scaled to fit the page (up to twice Kaku's font size), and a tab a browser had sized is handed back to its window once split (`TabAction::Reclaim`). Growing the tab by the split's share, as before 2026-10-19, pushed the other splits out of the Mac window. Size claims use zoom-aware `iter_panes()` geometry, so a zoomed pane is claimed like a lone one; tab navigation still lists hidden splits. Browser zoom and unzoom carry only the viewed pane's current claim and reapply it synchronously after the GUI toggles zoom; another pane's action cannot borrow that claim, and native or other-client geometry changes do not automatically reclaim the browser viewport. Frame rendering runs on another thread, so the GUI callback is not a cross-thread atomic transaction.\n- Web pane menus retain the selected `pane_id` through clipboard permission prompts and close dialogs. Splits call `Mux::split_pane` with that id rather than the deferred active-pane spawn path. Confirmed pane closes remove that exact pane, or record and remove its tab when it is last; `CloseCurrentPane { confirm: false }` is not sufficient under `AlwaysPrompt`. Middle-click tab close shares the existing browser confirmation path. Optimistic tab closes require a connected transport and a timer that restores the latest authoritative list when the close is not confirmed; the server suppresses identical lists, so expiry must not depend on another list arriving. Keep cached pane snapshots until the authoritative list drops their pane. Keep native phone selection menus intact.\n- Web URL detection stops at CJK sentence punctuation and unmatched closing wrappers, retains balanced path parentheses, and maps UTF-16 regex offsets back to terminal cells, including surrogate pairs and wide end cells. The browser regression fixture includes the reported GitHub comment link followed by `)，并以`.\n- A tab a browser sized keeps that size through relayouts the Mac does on its own (display or scale change, focus, config or theme reload, tab bar visibility): `apply_dimensions` asks `window_relayout_resizes_tab` per tab. Only the Mac's own use takes it back: typing or clicking (`reclaim_active_tab_size`) or a live window drag. Focus and a modifier pressed alone do not count (2026-10-19): switching between a fullscreen browser and Kaku with Cmd+Tab resized the tab on every switch, and Claude Code left a copy of its output behind for each size. Resizing it under the browser made Claude Code and Codex redraw for the Mac's rows and jump up in the page. The desktop view's synthetic input counts as the Mac's, so the page claims its grid again on its next input and when it leaves the desktop view (`macSized`, `takeBack`). After a reload the first frames still carry the Mac's grid until the new claim lands; the page keeps the terminal at opacity 0 (`body.sizing`, still focusable, unlike the pre-first-frame `visibility: hidden`) until a frame has the claimed grid or a split's own, for at most `REVEAL_WAIT` (600 ms). Tests that screenshot the terminal wait for `sizing` to clear.\n- Page fonts: the page draws with the families of Kaku's `config.font` in order (`font_families`; JetBrains Mono maps to the bundled `Kaku Mono`, emoji fonts stay unnamed), then its own fallbacks with `Kaku Symbols`, the bundled Symbols Nerd Font limited by `unicode-range` to its private-use spans so it is fetched only when an icon shows. Fonts the user installed are resolved on the Mac the way Kaku renders (`src/fonts.rs`: `font_dirs`, then CoreText) and listed in `font_files`, text weight only, since the page draws bold at that weight; files under `/System/`, collections and anything over 32 MB are left out. The page fetches a listed file over the download path only when the browser lacks the family, and keeps it in Cache Storage keyed by path, size and date. The WOFF2 files in `assets/web` come from `scripts/build_web_fonts.py`; rerun it when `assets/fonts` changes, and print its `unicode-range` into `index.html`.\n- The relay is public and sheds abuse itself: browser messages are capped at 512 KB and Mac messages at 4 MB, frames too (`max_frame_size`: tungstenite buffers a whole frame, 16 MB by default, before it checks the message cap); bytes queued toward one Mac (`HOST_QUEUE_BYTES`, the flooding browser is dropped) and toward one browser (`BROWSER_QUEUE_BYTES`, the stalled page is dropped) are bounded, and so are all queues together (`GLOBAL_QUEUE_BYTES`): a message count alone let one stalled page hold a gigabyte of 4 MB screens on a 256 MB machine. Every queue gives its unsent bytes back when its socket ends (`Queued`'s `Drop`), or the relay-wide budget leaks until nothing is accepted; at most 2 registrations per id (a third evicts the oldest) and 4 browser sockets per client address per host; when a host's 8 browser slots are full, the oldest socket the Mac has not admitted is let go (`make_room`; the Mac sends `{\"t\":\"ok\",\"c\":c}` once it admits a browser), so sockets that only know the id cannot lock the owner out; devices are matched on their whole public key, never the 8-hex id people see; `remote-devices.json` changes under an `flock` (`update_devices_at`) so a sign-in cannot write a revoked device back; hosts silent for 75 s and browsers for 150 s are closed. The relay serves the page and its files from its own copy, built in from `crates/kaku-remote/assets/web` (`page_file`), and asks no Mac for them; the page itself answers 404 with the offline notice while the Mac is away, which an open page relies on to tell an offline Mac from a network problem. So a relay deploy ships the page: deploy it together with Kakus that speak its protocol (pairing, `KCH3`), never ahead of them. `fly.toml` caps connections at 400 (soft 300), about 200 people online, from a release-build load test (400 Macs plus 200 streaming pages at 88 MB); raise it only with a new load test or a larger machine, and the Docker build uses `Cargo.lock` with `--locked`.\n- The page lives on its own origin, `t.kaku.fun` (2026-10-16), so serving a page that misuses the browser's keys takes a compromise of that origin, not just of the relay. `scripts/publish_web_page.sh` builds it from `crates/kaku-remote/assets/web` and the bundled JetBrains Mono, fills the `kaku-relay` meta tag (empty in the copies Kaku and the relay serve, which then talk to their own origin), writes a `vercel.json` with `/<id>` and `/<id>/` rewrites, a CSP that allows the one inline script by its SHA-256, and no caching, and deploys it with `vercel deploy --prod` to the `kaku-web` Vercel project, which holds nothing else: a path on `kaku.fun` would share an origin, and the keys, with every page of the site. The page must keep exactly one inline script and no inline event handlers, or the script refuses to publish. With `KAKU_RELAY_PAGE_ORIGIN` set, the relay 308s its page addresses there (the fragment survives), serves no page files, admits browser sockets only with that `Origin` (others are upgraded and closed at once with 4426, so a page still open on the relay's origin from before the move says to reload instead of reading as no network), and answers `/<id>/status` (200 or 404, CORS for that origin) for the page's offline notice. Kaku links the default relay's page to `t.kaku.fun`, any other relay's to the relay itself. `crates/kaku-remote/tests/static_page.cjs` runs the published page behind a server applying its `vercel.json` and checks the redirect, pairing, bookmarks with and without the slash, a foreign `Origin` refused, and no CSP refusals; poll with `page.evaluate` there, since `waitForFunction` evaluates a string the CSP blocks. Publish the page before a relay deploy that sets the origin. After each reconnect the page fetches its own address and reloads when the inline script served there differs from its own (at most once per version through sessionStorage, so a stale cache cannot loop, and never while an upload runs), so tabs left open pick up a new page or a Kaku update without the user reloading; that fetch is why the published CSP's `connect-src` includes `'self'`.\n- The Mac-screen view (2026-10-20) is the maintainer's own for now: the `desktop` cargo feature (`kaku-gui/desktop`) is off by default, so Release and Nightly never carry it. It needs a `kaku-desktop` helper, built from its separate repository. Private packages carry it in `Contents/MacOS/kaku-desktop`, preferred over the legacy copy at `config::DATA_DIR/kaku-desktop`; Kaku starts it as a child so macOS checks Kaku's own Screen Recording and Accessibility grants, and `src/desktop.rs` forwards only `desktop_*` messages in each direction, turning the helper's `desktop_frame` lines into binary messages (zero byte, u32 header length, JSON header, H.264 bytes) for a page whose `desktop_start` says `binary`; `run_session` takes them on a separate `frames` channel that both writers (`share.rs` and the local WebSocket) merge. The helper lives as long as the page's session: `desktop_stop` is forwarded, not acted on, because the helper may hold a virtual display shaped to the page's view, and making one again on every visit costs seconds and moves the Mac's windows (`the_helper_outlives_a_stop_but_not_the_session`). The page loads `assets/web/desktop.js` only when the session config says `desktop: true`, so other Kakus and the published page show nothing new. Build the helper in Release first, then build the private package with `PROFILE=release-opt CARGO_FEATURES=kaku-gui/desktop KAKU_DESKTOP_HELPER=<helper-executable> OUT_DIR=dist/private ./scripts/build.sh --app-only --native-arch`. The build checks helper architecture and signs it before the main executable. Public builds omit both the feature and helper input; a public update replaces the private package and removes its desktop capability. Preparing a private package does not authorize replacing or restarting the installed app.\n- Desktop feedback timing names two boundaries: `desktop_input_ack` confirms a successful input post, and a drawn frame with its `input_seq` confirms a capture after that post, not that the target app reacted. Match both even when independent output queues deliver the frame first; retain slow samples and report unposted, unframed and cancelled inputs separately. A new `stream_id` isolates capture generations and decoder callbacks; frame sequence numbers remain monotonic within the helper. ACK old frames with their own stream id, never the current one. Legacy helpers without these fields stay usable without fabricated timing. Desktop wheel, touch and pointer cancellation belong only to the visible desktop, never the hidden terminal. While the Mac is locked the helper sends `desktop_locked` (from `CGSSessionScreenIsLocked` plus the `com.apple.screenIsLocked` / `screenIsUnlocked` notifications, watched only between `desktop_start` and `desktop_stop`) and the page shows a notice over the picture; typing the password into the view unlocks it.\n- Direct path (2026-10-21 for the desktop view, every build and session since 2026-10-22, `src/p2p.rs` and the page's «Direct path» block): beside the relay, the page offers a WebRTC data channel (str0m with `rust-crypto` on the Mac; `apple-crypto` fails to link) once the config says `direct` (or `desktop`, from the private builds that had it first; a Kaku that says neither gets no offer); offer and answer travel inside the sealed session, so the relay cannot sit in the middle. Interactive replies (terminal frames included), the page's messages under 16 KB and desktop pictures take it, files stay on the relay. Each path has a number (`id` in `rtc_answer`). Order across the move is a fence, not a guess: each side's last relay message before it sends on the path is `rtc_ready` (Kaku sends one per lane, text and frames, the frames one as a binary header-only frame), and the receiver holds what arrives on the path until that marker comes through the relay; without it a delta picture or a key-up overtook one still on the relay. Either side ending a path tells the other by the relay (`rtc_closed` from Kaku, `rtc_close` from the page): the page's channel otherwise stayed \"open\" for about 30 s after Kaku dropped it and lost every click. After a path that had opened ends, the page sends `hello` and `attach` (what was on the path is lost, so the screen starts over), asks for a key frame, and offers again after 2, 5, 15, 30, 60 s; a path that never opened is not retried. Kaku tells the helper `desktop_keyframe` with `path_changed` on every move, which drops in-flight pictures and the old path's round trip and rate samples (kept, a 5 ms direct RTT read every 120 ms relay ack as a standing queue). Host candidates from `getifaddrs` minus loopback, link-local and 198.18/15 (TUN fake IP), plus one server-reflexive candidate per socket from a STUN binding to `stun.cloudflare.com` and `stun.l.google.com` (the page asks the same two), sent before the socket's reader starts and given `STUN_WAIT` (800 ms, DNS included) in all. Without the Mac's reflexive address a browser behind a port-restricted NAT only ever sent to the Mac's private addresses and never opened its own NAT to the Mac. Under a TUN proxy the reflexive address is the proxy's exit (measured 2026-10-22: 23.249.x on the maintainer's Mac), a candidate nobody can reach, which costs nothing. No TURN: when hole punching fails, the relay already carries the session. Unit tests keep STUN off (`STUN_SERVERS` is empty under `cfg(test)`); `public_stun` (ignored) asks the real servers. Pictures ride the reliable ordered channel on purpose, since H.264 deltas cannot survive loss.\n- The relay runs in `nrt` (Tokyo, `crates/kaku-relay/fly.toml`), the edge the Mac enters through: from `sin` every keystroke and echo crossed Tokyo to Singapore twice, 375 ms echo against 166 ms now. It keeps hosts in memory, so run one machine; two would split a Mac and its browsers across them. The relay is not where time goes (2026-10-20): a release build forwards 150 Mbps from one Mac to one browser with under a millisecond added, and the live machine sat at load 0.00 while a page streamed. Slowness through it comes from each side's path to Fly: a proxy node can cap the Mac's upload far below its own line, and some direct routes from China enter a far edge such as `fra`. Measure the path (`fly-request-id` names the edge, a timed upload gives the rate) before resizing the machine or moving the region, and make streams adapt to the link rather than assume it, as the desktop helper's rate control does.\n- Only Mac-confirmed text is rendered. Cursor visibility cannot distinguish normal input from passwords, so never predict typed characters. Pages opt into output flow control with `Hello.frame_ack`: at most 12 unacknowledged frames and a 512 KiB JSON byte waterline, including associated images. One complete frame may cross the waterline before rendering pauses; ACK after xterm parses even an ignored pane frame, coalesce later dirty output, and leave legacy hello clients unthrottled.\n- Browser selection copying follows the Mac's existing `copy_on_select` value and runs on selection release without a network hop. Preserve manual copy when disabled, reconstruct history line breaks and virtual gaps, and never queue a denied automatic copy for a later unrelated gesture. Browser clipboard permission and real mobile selection remain browser-specific.\n- `Frame.wrapped` and `History.wrapped` identify physical rows that continue into the next row. Copying, history pagination and pane snapshots preserve these flags; even a delta frame carries the whole array, and a wrap-only change must be sent. Never infer soft wrapping from a full-width row. Older hosts without flags retain hard line breaks.\n- History keeps at most 10,000 row strings but materializes only visible rows and overscan, preserving stable DOM nodes that intersect an active selection. Cached tab attachment must precede resize, and an equal-size claim must still record browser ownership.\n- Paths in the output (`~/…`, `/…`, `./…`, never inside a URL; `PATH_RE` in the page) are links: Cmd+click (Ctrl+click off the Mac) asks Kaku for that file (`Download`), and it comes back as a browser download. Kaku resolves `~` to the home folder and a relative path against the pane's working directory, sends only regular files up to 50 MB, and refuses folders and missing files by name. Chunks of 64 KB go out while less than `DOWNLOAD_WINDOW` (256 KB) is unacknowledged (`DownloadAck`), limiting queued bytes ahead of terminal interaction. The maintainer chose input and scroll responsiveness over bulk throughput on fast, high-latency links (2026-10-18); keep that trade-off explicit when changing the window. The relay never holds much of a file for one browser; the relay drops a browser past 8 MB queued. A download ends with its session, and the page says so on a dropped link. Any paired browser already has the shell, so reading a file grants nothing new.\n- Upload transfer ids bind the file and original pane for ten minutes after completion, across transport reconnects within the same app process. Retry returns the receipt without writing or pasting twice; cancellation and disconnect remove only partial files. Freeze the target once for a multi-file drop.\n- Access code (2026-10-16, the launch gate the maintainer chose over trials): with `KAKU_RELAY_ACCESS_CODE` set (a fly secret the maintainer picks), the relay registers only Macs presenting it in `x-kaku-access`, compared as SHA-256 in constant time; 401 bodies say `access code required` or `wrong access code`, and ten wrong codes from one address lock it for `ACCESS_WINDOW` (429). `kaku remote` reads the relay status, asks for the code without echo when the relay refused it, saves it to `remote-access` (0600, ASCII only since it travels in a header), and waits for the bridge, which polls that file during its backoff and retries within a second. Every recorded attempt stamps `RelayStatus::checked`, so a second wrong code is told apart from a stale one. Per-address limits sit beside it: `CONNECTS_PER_MINUTE` new sockets and `MAX_HOSTS_PER_ADDR` registered Macs per source, which `source_of` takes as the IPv4 address or the IPv6 /64 from `fly-client-ip` (r.kaku.fun has an AAAA record, and one IPv6 client can pick a new address from its /64 for every request); a large limits map is swept at most once a second; local runs have no client address and are not limited. Host admission must enforce both address and global capacity under the same lock as registration; a pre-upgrade count check alone is not a limit under concurrent handshakes. `live.cjs` runs its relay with a code and checks refusal, then acceptance after saving it.\n- No registration tokens (2026-10-17): remote access is free, so the Ed25519 registration tokens built for trials and Pro (`kaku-relay keygen` / `issue`, `KAKU_RELAY_TOKEN_KEY`, `kaku remote --token`, `x-kaku-token` on registration) were removed with their `ring` and `base64` dependencies; the access code and per-address limits are what gate the relay. Measured cost at that point: one heavy user moved 271 MB a day through the relay (about $0.33 a month of Tokyo egress), so about 100 users cost $20 to $40 a month on the 256 MB machine.\n- No pairing QR code (2026-10-17, maintainer's call): the page is made for a desktop browser for now, so `kaku remote` prints the link with how to use it and the manage commands, not a QR code; do not bring one back while phones are not a target. The link is only `#<one-time secret>` (`pairing::pairing_fragment`); `mac_auth` carries the Mac key, and while the offer is fresh a `tag`, HMAC under that secret over the transcript and the key (`pairing::mac_key_tag`), which the page checks before trusting the key. The relay never sees the secret, so it cannot vouch for a key of its own; a missing tag means the link was used or expired. The page still accepts the older `#p=<secret>&m=<key>` form. Publish the page before shipping a build that prints the bare form.\n- Remote input stays ordered within each browser, but a blocked browser must not hold the shared relay receive loop. Each browser has a bounded worker; overload or timeout closes that browser explicitly rather than dropping individual keys. GUI tab snapshots are requested asynchronously, with at most one outstanding request per session, and a later explicit Attach cancels an earlier automatic NewTab follow.\n- Both WebSocket writers prioritize interactive replies before bulk traffic, with one bulk turn after eight interactive messages to prevent starvation. File start, chunks and completion stay in the same bulk FIFO. Assign encrypted sequence numbers only after the final writer selects a message; reordering already encrypted messages breaks authentication. Keep the final bulk queue bounded to one pending message, and discard revoked sessions before encrypting their queued output. Bytes already written to TCP cannot be overtaken by later input.\n- Every relay connect is capped by `CONNECT_TIMEOUT`: a local proxy (Stash TUN) can accept TCP and never finish the handshake, which once left the link offline for hours. Bound socket writes and session inbox waits too, but allow short input bursts to wait rather than discard keys. Wrong-key close 4403 is final; idle unauthenticated sessions expire with 4408. A host replacement closes browsers independently of the old data queue, with late input discarded until the close writer finishes.\n- The default relay uses `r.kaku.fun`; its browser page lives at `t.kaku.fun/{id}/`, and the relay redirects older `/{id}/` and `/t/{id}/` page links there. Never shorten the 128-bit identity or put a pairing secret in a URL path/query. The page erases the fragment at once and saves the device only after Kaku admits it; when IndexedDB is unavailable it pairs for that tab only and says so. Loopback tokens stay tab-only in sessionStorage. Keep pairing instructions in the CLI, page messages and public docs consistent.\n- One identity per Kaku, persisted at `DATA_DIR/remote-share.json` (0600), devices at `remote-devices.json`. `kaku remote --reset` replaces the identity and forgets every device; the running bridge notices within seconds and re-registers.\n\n## Remote Verification\n\n- For an authorized relay deployment, run `fly deploy . --config crates/kaku-relay/fly.toml --dockerfile crates/kaku-relay/Dockerfile --remote-only --depot=false --ha=false --strategy immediate` from the repository root (the build context must reach the page files; the root `.dockerignore` sends only what the relay embeds, about 1 MB, never `target/`). Use `--depot=false` when Depot stalls at builder authentication. Verify the Fly tunnel with `fly doctor` first; do not add `--wg=false` to a working tunnel, since the legacy builder's HTTPS path can fail with an h2c upgrade HTTP 500. This selects a build transport, not a system proxy setting. Verify the deployed image digest and an isolated encrypted session afterward; a dashboard without new releases does not diagnose a failure before image creation. If CLI DNS checks warn while HTTPS works, compare public DNS before changing anything.\n- Run `cargo test --locked -p kaku-remote`, relay tests via its own manifest, and the workspace check/tests/clippy, which build the feature by default as CI does.\n- `node --test crates/kaku-remote/tests/web.test.cjs` uses Playwright Chromium to exercise the shipped HTML and xterm, including input privacy, reconnect, upload pacing, history selection, and viewport boundaries. Set `NODE_PATH` when Playwright is installed outside the checkout; `PLAYWRIGHT_CHROMIUM_EXECUTABLE` optionally selects an existing browser. The page draws with WebGL when it can, so most tests launch with WebGL off and read the DOM fallback, and one test checks the WebGL grid, down to its text pixels. The WebGL tests launch with `GPU_ARGS` (`--use-angle=metal --enable-gpu --ignore-gpu-blocklist`), which give headless Helium WebGL2 on the Apple GPU (2026-10-21; without them it had none and those tests skipped). CI's Chromium can still lack WebGL2 and skip them, so read the reported skips before claiming GPU coverage. Playwright's own Chromium downloads fail code signing on Darwin 27 and its headless shell only has SwiftShader, whose WebGL timings are meaningless. Never emulate `deviceScaleFactor` for WebGL checks or measurements: it changes `devicePixelRatio` but not `devicePixelContentBoxSize`, so xterm sizes its canvas at half the device size and draws nothing or a shrunken grid, a failure real browsers do not have.\n- The bar for the page (maintainer, 2026-10-16) is how typing, scrolling, copying and tab operations feel: no delay, no stutter, like the app. After changing the page, `web.rs` or the relay, run `node crates/kaku-remote/tests/feel.cjs` (same setup as `live.cjs`) and compare: on loopback, keystroke to echo p50 about 5 ms, a 54-character burst intact, no scroll frame over 25 ms, copy-on-select exact within a frame, new tab shown in about 15 ms, switch and close within one frame (16 ms). It times operations in the page from the click event to the screen change, since Playwright waits about two frames before it clicks. It explicitly disables WebGL with `--disable-webgl` and `--disable-3d-apis`; these are DOM timings, not GPU timings. Headless Helium has WebGL2 only with `GPU_ARGS` from `web.test.cjs`, so WebGL timings need those flags or a headed browser; do not read a DOM-only run as WebGL coverage.\n- `node crates/kaku-remote/tests/static_page.cjs` (same app and relay setup as `live.cjs`) also reads DOM rows and explicitly disables WebGL. `--disable-gpu` alone still exposes WebGL2 in some Chromium builds, so it does not guarantee DOM rows exist; use `--disable-webgl` and `--disable-3d-apis` for every DOM-based harness. Keep WebGL off in harnesses that open several tabs for a second reason: headless Helium on Metal stops opening new tabs once a page has drawn with WebGL (navigations abort before any request reaches the server and `newPage` hangs), which looked like a page bug for two rounds.\n- After `make app` and a debug relay build, run `node crates/kaku-remote/tests/live.cjs`. It launches only its own app, temporary HOME, and loopback relay, tests encrypted input, uploads across reconnect, frame flow control, host replacement, stable split targets and AlwaysPrompt pane closing, foreign `Host` refusal, and replayed, misplaced and old-format messages, then removes its own fixtures. Never substitute a real share link. DOM-row assertions in `live.cjs` explicitly disable WebGL; real GPU rendering remains covered by the separate browser tests, which must assert canvas output rather than DOM rows.\n\n## Cross-References\n\n- [`term/AGENTS.md`](../term/AGENTS.md) - Uses codec, vtparse, and color types.\n- [`kaku-gui/AGENTS.md`](../kaku-gui/AGENTS.md) - Uses font, input, and surface crates.\n- [`lua-api-crates/AGENTS.md`](../lua-api-crates/AGENTS.md) - Uses Lua helper and dynamic binding crates.\n","isInternal":false,"tokens":9091,"sizeBytes":38761},{"name":"AGENTS.md","path":"kaku-gui/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/tw93/Kaku/HEAD/kaku-gui/AGENTS.md","title":"kaku-gui - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Kaku GUI Agent Guide\n\n`kaku-gui` owns rendering, window lifecycle, user interaction, AI chat, and the `k` helper binary.\n\n## Scope\n\n`kaku-gui` is responsible for:\n- terminal window rendering\n- input and mouse handling\n- tab/pane UI behavior\n- startup lifecycle and app event flow\n- AI chat overlays, approval UI, inline status, syntax highlighting, and assistant transport\n- the `k` helper binary under `src/bin/k.rs`\n\n## Where to Look\n\n- `src/termwindow/mod.rs`: core window state and action dispatch\n- `src/termwindow/render/`: rendering pipeline\n- `src/termwindow/mouseevent.rs`: mouse and drag behavior\n- `src/termwindow/webgpu.rs`: surface resize and present mode handling\n- `src/frontend.rs`: app lifecycle and macOS integration events\n- `src/overlay/`: launcher and overlays\n- `src/overlay/ai_chat/`: AI chat overlay, markdown rendering, syntax highlighting, and Waza integration\n- `src/ai_chat_engine/`: AI chat engine, approval, and compaction flow\n- `src/ai_client.rs`, `src/ai_remote.rs`, `src/ai_tools/`: provider and tool integration. Keep tool-name strings stable so persisted conversations replay correctly.\n- `src/ai_tools/paths.rs`: sandbox and sensitive-path guards.\n- `src/ai_tools/fs.rs`: read, list, write, patch, mkdir, and delete tools.\n- `src/ai_tools/shell.rs`: exec, background execution, and polling.\n- `src/ai_tools/web.rs`: search, fetch, and URL reading.\n- `src/ai_tools/search.rs`: grep and symbol search.\n- `src/ai_tools/project.rs`: project summaries and file tree helpers.\n- `src/ai_tools/soul.rs`: memory and soul reads.\n- `src/ai_tools/registry.rs`: `ToolDef`, `all_tools`, and `to_api_schema`.\n- `src/cli_chat/`: command-line chat flow\n- `src/thread_util.rs`: macOS worker thread lifetime helpers\n- `src/tabbar.rs`: tab bar behavior\n\n## Practical Rules\n\n- Keep rendering decisions in rendering modules, not scattered across event code.\n- Paint-time Kaku TUI detection must reject non-Kaku foreground executables using the cached leader name before requesting the full process tree. Keep the executable and argument confirmation for `kaku ai` and `kaku config`; cover pane, window and tab-bar consumers together (#583).\n- Keep initial window dimension math in `TermWindow::new_window()` consistent with `get_os_border()` behavior.\n- In-app update must confirm before replacing the running app. User-facing paths (menu Check for Updates, menu Restart to Update, toast click) go through `check_for_updates_from_menu()` / `confirm_and_apply_update()` in `src/frontend.rs`, not straight to `restart_to_update()`. Exploratory menu check opens a tab without `KAKU_UPDATE_AUTO_CONFIRM`; only the overlay-confirmed path may auto-confirm. CLI direct and brew paths confirm in `kaku/src/update.rs`. Guards: `frontend::tests::user_facing_update_events_route_through_confirm` and `update::imp::tests::brew_and_direct_paths_confirm_before_replace`.\n- Keep `KAKU_CONFIG_CHANGED` fast path in `emit_user_var_event()` before pane ownership checks.\n- Pass fullscreen state into `TabBarState::new()` and preserve fullscreen-specific title button spacing behavior.\n- Keep AI overlay state changes cheap enough for live terminal interaction; avoid blocking render/input loops on provider calls.\n- Preserve approval prompts and inline AI status placement when touching AI chat or shell flows.\n- Chat permission summaries retain the complete target and command, with terminal controls escaped. The native review pane scrolls through all details and refuses approval when the pane is too small to read. Notifications may shorten their preview; the approval payload must not. Chat rendering, picking and mouse hit-testing share `msg_area_height`, `picker_height` and `visible_start`. Preserve a scrolled reader's position as output grows. Code Copy uses the original block, never wrapped display rows; Insert only pastes a completed, control-free single shell line into the original shell pane and never sends Enter.\n- AI HTTP transport must preserve both sides of proxy behavior: external API hosts should honor detected system proxy settings, while loopback, private LAN, link-local, CGNAT/Tailscale-style, `.local`, `NO_PROXY`, and macOS ExceptionsList model endpoints should bypass the proxy and connect directly. Do not describe this as full SOCKS support unless reqwest SOCKS support is actually enabled and smoke-tested. Exception: the `http_request` tool always connects directly to the public IP addresses resolved and validated before approval, for both HTTP and HTTPS. It must never pass that hostname to a proxy for independent resolution; users on proxy-only networks receive a connection error instead of weakening the SSRF boundary.\n- Wrap macOS worker thread spawns in the existing autorelease-pool helper so shutdown does not reintroduce use-after-free crashes.\n\n- Remote pane operations in `src/remote_tabs.rs` retain a stable `PaneId`. Do not send a remote split through `spawn_command_impl`, which resolves the active pane again after queuing. Browser-confirmed closes must bypass `AlwaysPrompt` without switching to another pane, and preserve the last-pane tab restore bookkeeping. The isolated remote live smoke covers both paths.\n\n## Known Pitfalls\n\n- During window drag, terminal move/wheel events may need suppression.\n- A tab drag belongs to one press/release gesture. A new press or motion without the left button exclusively held cancels stale state, including when AppKit swallowed the previous release. Preserve the ordinary release handler for settle animation. For unexplained tab reordering, `debug_key_events` records tab press geometry, motion and reorder decisions without terminal text (#584).\n- WebGPU surface reconfigure should happen only on meaningful state changes.\n- Finder \"open with\" behavior is handled in `frontend.rs` event flow.\n- Option+Click cursor movement fires only when: no mouse grab, no alt screen, click is on the same row as the cursor. It sends Left/Right arrow sequences proportional to the column delta; see `mouseevent.rs`.\n- Block cursor height uses `natural_cell_height` (ignoring `line_height`) so Nvim visual selections match WezTerm proportions.\n- Top border clearance for macOS integrated buttons is state-sensitive:\n  - top tab bar visible -> add small gap\n  - top tab bar hidden or bottom tab bar -> add larger clearance for traffic lights\n- Overlay geometry must update when panes are split, resized, closed, or moved.\n- Alternate-screen wheel behavior is config-controlled; do not hard-code one terminal behavior across normal and alternate screens.\n- Inline `#` AI query status belongs below the buffer, not as a toast, so terminal content remains stable.\n- `fast_model` and proxy config are part of the AI UX contract; verify both when changing provider selection or transport.\n- Per-pane overlays (AI chat) render in place of their underlying pane and live in `pane_state`, not the tab's pane tree. They have no window handle, so their output only repaints the window through `PaneOutput` -> `mux_pane_output_event` -> `is_pane_visible` -> `window.invalidate()`. `is_pane_visible` (`termwindow/mod.rs`) MUST treat an active per-pane overlay pane as visible, and the overlay's `render()` MUST `term.flush()`, or streamed output and the loading spinner freeze until an input event forces a repaint. Symptom-to-cause: if `top` refreshes on its own but the AI chat needs a click, the break is one of these two.\n- Scrollback viewport pruning has two policies in `termwindow/mod.rs`: passive pruning during output uses `normalize_viewport` and snaps to the bottom (`None`), while explicit user-driven scroll requests use `normalize_interactive_viewport` and clamp to `scrollback_top`. During a selection drag, only the pane named by `MouseCapture::TerminalPane` may pin a pruned viewport; sibling panes must retain the passive #448 behavior.\n- Tab bar rendering and the tab rename editor must read `resolved_palette.tab_bar`, which includes the selected color scheme and user overrides. Reading `config.colors.tab_bar` alone ignores scheme colors and makes Fancy tabs unreadable in light themes.\n- Tab bar sizing and hit-testing have pure seams in `src/tabbar.rs`: `tab_width_budget` (per-tab column budget that drives truncation) and `is_tab_hover` (clickable span). The truncation and position regressions (#439/#443/#445) came from layout logic with no test seam, so when you change tab sizing, truncation, or click regions, extend their unit tests instead of only eyeballing the running app. `tab_width_budget` locks the invariant that a tab is never squeezed below 1 column.\n- Launcher entries that target a pane must retain its stable `PaneId`, not a tab or topological pane index. The launcher snapshot can outlive pane exits and split-tree reindexing; resolve the `PaneId` against the live mux only after the overlay closes.\n- Session snapshots skip a pane's content while it is in the alternate screen (`f4aca502`), so a vim or htop screen never lands in restored history. Claude Code and Codex are the exception (`foreground_is_agent_cli` in `tabbar.rs`): their last screen is kept, since it is the conversation. Restore never re-runs the program.\n- A partially failed startup session restore must preserve the original session envelope and scrollback sidecars. Only mark a snapshot consumed after every saved window restores successfully; otherwise exit-time saving can overwrite the only complete copy.\n- Session sidecars compact vector-backed lines before checking visible images, since wide-character continuation cells can retain hidden Kitty placements. Strip remaining images and recompress before encoding. Menu restore guards preserve prior dirty state on failure and carry successful changes through overlapping restores; startup-only restores remain pristine. Exit and update checkpoints capture cwd/output even without structural changes, only in the session-owning process and never during an incomplete restore. SIGTERM requests main-thread event-loop shutdown, then uses the normal exit checkpoint; its listener remains registered through repeated TERM signals. Register it only after existing-instance handoff, never in CLI-only commands (#579).\n- The terminal grid's top origin has one source: `TermWindow::terminal_first_row_offset()` (OS top inset plus a shown top tab bar). Pane backgrounds, split lines and mouse row mapping take it from there so they match where text is drawn; a background that skipped the 6px inset under a top tab bar sat above its split line (#562). A source guard in `render/split.rs` fails if `pane.rs`, `split.rs` or `mouseevent.rs` computes its own. `update_text_cursor` (IME candidate rect) still uses a separate origin and sits `border.top` high; that is a known, unfixed gap.\n- Window padding (2026-10-18): with managed padding (no `config.window_padding` in the user's file) every vertical gap derives from the top padding S in `effective_vertical_padding_with_policy`: content keeps S below window chrome and S/2 from the bottom edge or a visible tab bar, top or bottom, fullscreen included; only a maximized window without a bottom bar runs content to the bottom edge. Rows rarely fill the height, and `padding_left_top` splits that leftover evenly between top and bottom (`split_row_slack`, inside the OS borders), so the gap to the bar no longer jumps between 1pt and a full row. Every consumer (pane rendering, split lines, mouse hit testing) reads `padding_left_top`; do not add a second slack policy in `resize.rs` (the old rebalance there only ever reached the resize increments). The tab bar itself stays one terminal row. Explicit user padding is used as written. Hotkey windows are also excluded from `split_row_slack`: their drawer policy owns the top inset, including an explicit zero. Cover hotkey, ordinary and fullscreen states when changing slack allocation.\n- Split geometry follows mux: `split.top = first.rows + gutter / 2` and the pane backgrounds meet at `first.rows + gutter / 2.0`. With the bundled even row gutter (`split_pane_gap = 2`) the line belongs on the top edge of row `split.top`, with an odd gutter on its middle; `split_row_center_offset` places it and `split_drag_row` measures drags against the drawn line so a grab does not jump a row. Left|right splits always have an odd column gutter. In opaque windows the outer panes' backgrounds fill the right and bottom padding (transparent windows leave those strips to the fill strips in `paint.rs`). With `inactive_pane_hsb` dimming on, the split line takes the active pane's background, so the dimming marks focus and the line only separates two dimmed neighbours; do not remove the line, three-pane layouts need it.\n- Touch mode (`termwindow/touch.rs`) is per-window UI state, not a persisted config override. It changes nothing but a drawn close button on each retro tab: tab bar height, desktop tab metrics and terminal font/line height stay the same. Long press works in every mode: `track_long_press` follows a plain left press and, on a release at least 450ms later within 24pt of the press (8pt cancelled ordinary presses over UU) (motion and the release point are both checked), replays it as a right click after the event finishes (on release, so AppKit menu tracking never sees the lift as a pick). The menu it opens draws its items at 18pt (`LONG_PRESS_MENU_FONT_PT`); NSMenu row height barely follows the font, so this is as large as a native menu gets without custom item views, while a mouse right click keeps the system size. A modal clears the pending press, so dismissing the rename editor never opens a menu, and a press that reached a tab through the bottom bar touch band aims its right click at that tab. A press over a pane that grabs the mouse is never converted unless it is on a tab, and the original press and release are handled as usual.\n- The bottom tab bar touch band (`BOTTOM_TAB_BAR_TOUCH_EXTENSION_PT` in `mouseevent.rs`) accepts only a plain left press (`press_takes_touch_extension`) and then owns that gesture's motion and release (`touch_band_press`). A middle press there is a paste into the last row and must never reach the tab bar's middle-click close. Known trade-off: a left click in the lower 12pt of the last row over a tab goes to the tab even when a TUI (tmux status line, htop) grabs the mouse.\n- Agent title (2026-10-18): while Claude Code, Codex or Kimi runs in the active pane (`foreground_agent_cli` in `src/tabbar.rs`), `src/termwindow/render/agent_title.rs` draws the agent's mark and the topic from its OSC 0/2 title at the top right: centered on the traffic-light line in the integrated-buttons strip, or 16pt below the top edge when fullscreen (the same distance as in a window, lifted only to stay above the first row), never under a top tab bar. `show_agent_title = false` (Agent Title in `kaku config`) turns it off; the default is on. Spinner glyphs and a leading agent name are stripped so the text stays still. Strip a name only at a complete boundary (end, whitespace, colon, pipe or middle dot), preserving topics such as `Codexification` and `Claude Codebase`; do not retry a shorter name after a longer partial match. The tab title is untouched on purpose: a tab is too narrow and would change width with every update. The marks in `src/termwindow/render/agent_marks.rs` are generated from Lobe Icons SVGs by `scripts/svg_to_poly.py`; regenerate rather than hand-edit. The remote page shows the same title: `tabbar.rs` publishes each tab's agent and topic through `kaku_remote::publish_agent_title` (honoring `show_agent_title`), and `AGENT_MARKS` in `crates/kaku-remote/assets/web/index.html` holds the same paths as SVG data on the 120 grid, so a regenerated mark goes into both files.\n- Tests that call `config::set_config_file_override` and `config::reload()` can race the config file watcher: `reload()` returns early when another reload starts, leaving the previous config. Write the config as few times as possible inside one test (loop the cheap cases inside one write), and do not split such a test into siblings that run in parallel.\n\n## Cross-References\n\n- [`mux/AGENTS.md`](../mux/AGENTS.md) - Tab and pane abstractions consumed by GUI.\n- [`termwiz/AGENTS.md`](../termwiz/AGENTS.md) - TUI primitives used in overlays.\n- [`config/AGENTS.md`](../config/AGENTS.md) - Config loading and reload signals.\n","isInternal":false,"tokens":3583,"sizeBytes":16307},{"name":"AGENTS.md","path":"lua-api-crates/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/tw93/Kaku/HEAD/lua-api-crates/AGENTS.md","title":"lua-api-crates - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Lua API Crates Agent Guide\n\n`lua-api-crates/` contains Rust-to-Lua API bindings for user configuration scripts.\n\n## Scope\n\n`lua-api-crates/` exposes runtime capabilities to Lua, including:\n- mux and pane controls\n- window and spawn helpers\n- ssh and system integration helpers\n- utility modules like logging, time, and filesystem\n\n## Where to Look\n\n- `lua-api-crates/mux/`: pane/tab control APIs\n- `lua-api-crates/window-funcs/`: window-level APIs\n- `lua-api-crates/spawn-funcs/`: process spawn APIs\n- `lua-api-crates/ssh-funcs/`: SSH-related Lua APIs\n\n## Practical Rules\n\n- API behavior here is user-facing and breaking-change sensitive.\n- Be careful with user-var and event-facing APIs, since GUI hooks and config reload signals rely on those event paths.\n- Keep proxy, config reload, and user-var event semantics stable for AI and automation workflows that observe Lua-facing state.\n- `PaneInformation` (used in title-formatting events) is fields-only userdata: method-style calls and `if pane.get_xxx` probes silently no-op instead of erroring. See \"Bundled kaku.lua Pitfalls\" in [`config/AGENTS.md`](../config/AGENTS.md) before extending it or its consumers.\n\n## Cross-References\n\n- [`mux/AGENTS.md`](../mux/AGENTS.md) - Mux controls exposed to Lua.\n- [`config/AGENTS.md`](../config/AGENTS.md) - Lua config execution environment.\n- [`kaku-gui/AGENTS.md`](../kaku-gui/AGENTS.md) - Window functions and GUI integration.\n","isInternal":false,"tokens":358,"sizeBytes":1425},{"name":"AGENTS.md","path":"mux/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/tw93/Kaku/HEAD/mux/AGENTS.md","title":"mux - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Mux Agent Guide\n\nThe `mux` crate owns tab, pane, domain, and mux state management.\n\n## Scope\n\n`mux` is the central abstraction for:\n- tabs and panes\n- windows and workspaces\n- local and remote domains\n- client/server mux coordination\n\n## Where to Look\n\n- `mux/src/tab.rs`: tab tree and split behavior\n- `mux/src/pane.rs`: pane state and interfaces\n- `mux/src/window.rs`: window-level mux state\n- `mux/src/domain.rs`: domain abstractions\n- `mux/src/client.rs` and `crates/wezterm-mux-server-impl/src/` (`sessionhandler.rs`, `dispatch.rs`, `local.rs`): protocol flow\n\n## Practical Rules\n\n- Already encoded input uses `send_text`, without paste framing or newline conversion. Local panes share the terminal's existing `ThreadedWriter` with keys and paste, preserving order without blocking on a full PTY.\n- A foreground process group can outlive its leader (`seq | less -X`). When the leader has no executable path, identify a live member of that same group from the cached shell tree. A foreground-group change, exec or leader exit invalidates the leader immediately and the fallback tree must be sampled after that leader update; checking only PGID misses a producer that exits after being cached. Do not wait for the normal TTL, substitute an unrelated background job, or scan all processes on every frame. Release the tree cache guard before calling Lua hooks that can query the pane again.\n- Keep alert propagation reliable, including `Alert::SetUserVar`, because GUI reload and user-var hooks depend on this path.\n- Preserve pane and overlay invalidation signals consumed by GUI AI/chat overlays when panes split, move, close, or resize.\n\n## Known Pitfalls\n\n- **Alert propagation**: `Alert::SetUserVar` must reach the GUI side reliably; broken propagation silently breaks config reload and user-var event hooks.\n- **Pane ID stability**: Pane IDs must remain stable across mux round-trips; don't reassign or reuse IDs in ways that confuse client/server sync.\n- **Domain abstraction leakage**: GUI-specific logic (rendering, window handles) must not appear in `mux`; only mux-level state and events.\n- **Split tree correctness**: Tab split tree mutations in `tab.rs` are tricky; always verify with a pane layout sanity check after modifications.\n- **Overlay resize regressions**: GUI overlays derive geometry from pane state. Mux changes that skip resize or removal notifications can leave AI/chat overlays stale.\n\n## Cross-References\n\n- [`kaku-gui/AGENTS.md`](../kaku-gui/AGENTS.md) - GUI rendering for mux state.\n- [`lua-api-crates/AGENTS.md`](../lua-api-crates/AGENTS.md) - Lua APIs for mux control.\n- [`term/AGENTS.md`](../term/AGENTS.md) - Terminal instances owned by panes.\n","isInternal":false,"tokens":610,"sizeBytes":2683},{"name":"AGENTS.md","path":"term/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/tw93/Kaku/HEAD/term/AGENTS.md","title":"term - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Terminal Agent Guide\n\n## Scope\n\n`term/` contains terminal emulation core behavior, including screen state, escape sequence handling, selection, and PTY-facing terminal semantics.\n\n## Where to Look\n\n- `term/src/terminal.rs` - main terminal state and event handling\n- `term/src/terminalstate/` - detailed state transitions\n- `term/src/screen.rs` - screen buffer behavior\n- `term/src/test/` - terminal-core regression tests (`selection.rs`, `csi.rs`, `c0.rs`, `c1.rs`, `core_emulation.rs`)\n- `crates/wezterm-escape-parser/` - escape sequence parsing (CSI/OSC/ESC/APC); `term` consumes it, it does not live under `term/src/`\n- `kaku-gui/src/selection.rs` and `kaku-gui/src/termwindow/selection.rs` - selection and copy behavior (GUI side, not `term`)\n\n## Practical Rules\n\n- Already encoded input uses `send_text`, without paste framing or newline conversion. Local panes share the terminal's existing `ThreadedWriter` with keys and paste, preserving order without blocking on a full PTY.\n- Keep GUI assumptions out of terminal core.\n- Color scheme reporting uses pane-local DEC mode 2031 and DSR 996/997. Notify subscribers after effective configuration palette changes, including same-mode palette changes, and answer subsequent OSC color queries from the new palette. Reloads with an unchanged effective palette and application-issued OSC color writes do not notify; cancellation, RIS and DECSTR clear the subscription. `color::is_light_color` is shared with config/window chrome so the reported scheme stays consistent with Kaku's colors.\n- Keep normal-screen and alternate-screen behavior distinct; wheel scrolling and inline AI status must not corrupt terminal state.\n- Preserve selection and cursor invariants that GUI overlays and inline assistant status depend on.\n- Only the terminal wrap attribute proves that physical rows form one logical line. Full-width output followed by text in column zero can still be independent commands. Keep both indented and unindented hard-newline regressions, alongside soft-wrap coverage, when changing hyperlink grouping (#547).\n- For TUI re-render corruption, reduce the report to an ANSI transcript and cover cursor motion plus erasure in terminal-core tests before changing GUI code. Full-line erase of a physical row must also leave wrap state consistent, especially for styled prompts that previously wrapped.\n\n## Verification\n\n- Compile checks: run `make check`.\n- Terminal logic changes: run `make test` or the narrow cargo nextest filter for the affected behavior.\n- Cross-crate behavior: also inspect `mux/AGENTS.md` and `kaku-gui/AGENTS.md`.\n\n## Cross-References\n\n- `../mux/AGENTS.md` - owns panes and terminal instances.\n- `../kaku-gui/AGENTS.md` - renders terminal surfaces.\n- `../termwiz/AGENTS.md` - terminal UI primitives.\n","isInternal":false,"tokens":617,"sizeBytes":2780},{"name":"AGENTS.md","path":"termwiz/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/tw93/Kaku/HEAD/termwiz/AGENTS.md","title":"termwiz - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Termwiz Agent Guide\n\n## Scope\n\n`termwiz/` contains terminal UI primitives, widgets, input handling helpers, and rendering utilities used by Kaku command-line and overlay flows.\n\n## Where to Look\n\n- `termwiz/src/` - primary library code\n- `termwiz/src/widgets/` - reusable widgets\n- `termwiz/src/lineedit/` - line editing behavior\n- `termwiz/src/render/` - rendering helpers\n\n## Practical Rules\n\n- Keep primitives generic; do not leak Kaku-specific policy into reusable widgets.\n\n## Verification\n\n- Compile checks: run `make check`.\n- UI primitive logic changes: run the narrow affected tests, then `make test` if behavior is shared.\n- Overlay or GUI consumers: inspect `kaku-gui/AGENTS.md` and verify the consuming flow.\n\n## Cross-References\n\n- `../kaku-gui/AGENTS.md` - GUI and overlay consumers.\n- `../term/AGENTS.md` - terminal core behavior.\n","isInternal":false,"tokens":214,"sizeBytes":848},{"name":"SKILL.md","path":".agents/skills/bugs/SKILL.md","rawUrl":"https://raw.githubusercontent.com/tw93/Kaku/HEAD/.agents/skills/bugs/SKILL.md","title":"bugs","category":"anthropic-skill","format":"markdown","content":"---\nname: bugs\ndescription: \"Proactively sweep a Kaku area for latent defects and UX traps before users report them, using this repo's own fix history and multi-entry-point archetypes, and confirm every finding with a probe run this turn.\"\nwhen_to_use: \"找找有没有bug, 主动找bug, 有没有隐患, 排查隐患, 举一反三, 上线前扫一遍, 这块可靠吗, 体验问题, latent bug scan, sibling sweep after a fix, /bugs\"\n---\n\n# Bugs: Find It Before the User Does\n\n`/hunt` needs a symptom. `check` / review needs a diff. This one needs neither: it goes looking.\n\nNamed **bugs**, not bug: `/bug` is a Claude Code built-in.\n\n## The Question\n\nAcross Kaku's closed bugs and fix commits, the dominant defect is not always a crash. It is often:\n\n- a **second entry point** that skipped the safety path the first entry already had\n- a **wrong-but-plausible** geometry or selection state after a modal/resize/sleep\n- a **shell-generated script** that is fine on the author's machine and broken in a clean HOME\n- an **AI surface** (Cmd+L, `#`, `k`, toast) that diverges on auth, theme, or cwd\n\nSo the question that finds bugs here is never only \"can this crash?\" It is:\n\n> What does this code do when the user arrives through the **other** path (menu vs toast vs CLI vs shell), when the display **scale or sleep** changes mid-session, when the shell is **not** the developer's zsh, or when the AI pane cwd is **remote**?\n\n## 1. Pick the surface\n\nName the area and the depth. Whole-repo sweeps with no budget produce speculation. Start from the Hotspot Map below (or the area that just shipped a fix) and go deep.\n\n## 2. Read that area's own fix history first\n\n```bash\ngit log --oneline --grep='^fix' -i -- <path>\ngit log --pretty=format:'%h %s%n%b' --grep='^fix' -i -- <path> | head -200\ngh issue list --repo tw93/Kaku --state closed --label bug --limit 40\n```\n\nBugs recur by shape within a module. Two signals worth acting on:\n\n- A fix chain of two or more commits each \"completing\" the previous one means a sibling path is probably still open. Real chain here: toast confirm, then menu confirm, then brew confirm on the same update flow.\n- A commit body that says only one surface was fixed (toast / CLI / overlay / brew) means grep every other surface for the same action.\n\n## 3. Sweep the boundaries, in this order\n\nHighest historical yield first. Subsystem guides live in crate `AGENTS.md` files and root `AGENTS.md` risk areas.\n\n| Boundary | What to ask | Where it usually lives |\n|---|---|---|\n| Multi-entry destructive actions | Menu, toast, CLI, shell, key binding: do they all confirm the same way before quit/replace/kill? | `kaku-gui/src/frontend.rs`, `update.rs`, `overlay/confirm.rs`, `overlay/confirm_close_pane.rs`, `kaku/src/update.rs` |\n| macOS window geometry | Traffic lights vs tab bar, DPI scale, external display drag, sleep/wake, lock screen, fullscreen first frame | `window/src/os/macos/`, `kaku-gui/src/termwindow/resize.rs`, titlebar paint |\n| Tab chrome | Overflow hit targets, rename modal selection, multi-pane title width, top vs bottom bar | `kaku-gui/src/tabbar.rs`, `termwindow/tab_rename.rs` |\n| Selection / mouse state | Survives Ctrl+L, modal cancel, mouse-reporting panes, successful terminal input? | `kaku-gui/src/termwindow/selection.rs`, mouseevent |\n| Shell integration generation | `local` only inside functions; backticks in bash-heredoc that emit zsh; Starship/PATH scoped to Kaku | `assets/shell-integration/`, `kaku/src/init.rs`, `setup_zsh.sh` |\n| Session restore | Split + scrollback both sides, crash backup, stale snapshot resurrecting closed tabs | `kaku-gui/src/session_restore.rs`, paint/quad buffers |\n| AI dual surfaces | Cmd+L vs `#` vs `k` vs chat: same auth, theme, proxy, remote-cwd tool policy? | `kaku-gui/src/ai_*`, `inline_ai.rs`, `cli_chat/`, overlay |\n| Proxy / network | External API uses system proxy; loopback/private/LAN/`.local`/NO_PROXY go direct | `config/src/proxy.rs`, AI transport |\n| AppKit menu intercept | `Ctrl+letter` only `key_is_down: false` in logs? Menu `keyEquivalent` ate it | `window/src/os/macos/menu.rs`, debug_key_events |\n| Render buffer growth | Over-capacity draw after retry budget; sleep invalidates drawable | `renderstate.rs`, paint/draw paths |\n\nFor a named user symptom, switch to hunt. For a diff under review, switch to check / review.\n\n## 4. Confirm before reporting\n\nA candidate is not a finding until it has evidence produced **this turn**.\n\n- **Confirmed**: source trace that names every entry point, a failing test, or a probe (log, `make test` subset, clean-HOME shell smoke).\n- **Plausible**: complete source path, named failure mode, no run yet. Report separately with the probe that would settle it.\n- **Not a finding**: \"looks risky\" without reading the implementation. Grep the call sites or drop it.\n\nBefore flagging unwraps or panics, confirm production code: skip `#[test]`, fixtures, doctests, and build scripts.\n\n## 5. Rank by blast radius\n\n1. Silent data loss or process kill (update/restart, close all, reset) without confirm on some entry path\n2. Crash loop or unrecoverable launch (session restore, paint)\n3. Stuck UI state (selection latch, frozen frame after sleep, window behind menu bar)\n4. Shell/environment pollution outside Kaku (Starship, PATH, generated zsh)\n5. Visual chrome wrong but recoverable (tab clip, traffic-light gap, font weight)\n6. Docs / copy drift\n\n## 6. Guard what gets fixed\n\nAnything confirmed and fixed needs a regression guard when feasible:\n\n- unit/integration test that fails on unfixed code\n- shellcheck / setup smoke for shell-generation bugs\n- entry-point matrix comment or test listing menu + toast + CLI for destructive flows\n\nVerify with the matrix in root `AGENTS.md`. GUI geometry and AppKit need `make app` or a hand smoke from `.agents/skills/release/SKILL.md` «Pre-release smoke checklist».\n\n## Hard Rules\n\n- **No finding without this-turn evidence.** Speculation wastes review time.\n- **One archetype hit means sweep every sibling entry point.** Report `checked N sites, M defective, K n/a`.\n- **Do not fix while sweeping.** Collect, then fix. Interleaving loses the sweep.\n- **Report clean boundaries as clean.** Do not invent findings to justify the run.\n- **Findings outside the named area get listed, not fixed**, unless the maintainer agrees.\n- **Do not propose UI i18n** (see root `AGENTS.md`).\n\n## Hotspot Map (issue-backed)\n\nUse these as default scopes when the maintainer says \"scan for bugs\" without a path:\n\n| Area | Issue / fix examples | Probe |\n|---|---|---|\n| Update confirm matrix | toast + menu overlay, CLI/brew `confirm_apply_update`, guards in `update.rs` / `frontend.rs` tests | Trace every caller of `restart_to_update` / `spawn_update_helper` / `run_brew_upgrade`; run those unit tests |\n| Titlebar / tabs / DPI | #516, #504, #490, #483, #460 | External display + scaled resolution + tab top/bottom |\n| Window drag / menubar | #508, #456, #408, #414, #477 | Drag to menubar, lock screen, fill desktop |\n| Selection latch | #495, #487, #455 | Ctrl+L, cancel rename, mouse-reporting TUI |\n| Shell init generation | #432, #441, #450, #503, #420 | clean HOME `kaku init`, `zsh -n` generated file, non-Kaku shell |\n| Session restore paint | #514, #448, #482 | split + dual scrollback snapshot |\n| AI auth / overlay | #506, #501, #502, #418 | Cmd+L vs `#`, split resize, appearance flip |\n| Proxy split brain | private base_url + system proxy | loopback OpenAI-compatible smoke |\n\n## Output\n\n```\nArea:        [what was swept, at what depth]\nBoundaries:  [N walked, M applicable]\n\nConfirmed (severity order):\n1. [file:line] [the defect in one sentence]\n   Evidence: [probe / call-graph / failing test]\n   Blast:    [what the user sees]\n   Siblings: [N checked, M defective, K n/a]\n\nPlausible (needs a probe):\n1. [file:line] [defect] -> [the probe that would settle it]\n\nSwept clean:\n- [boundary]: [what was checked, why it holds]\n\nSibling sweep: [pattern signature] -> [N checked, M defective, K n/a]\n```\n\nSay whether anything was fixed or whether this was scan-only.\n","frontmatter":{"name":"bugs","description":"Proactively sweep a Kaku area for latent defects and UX traps before users report them, using this repo's own fix history and multi-entry-point archetypes, and confirm every finding with a probe run this turn.","when_to_use":"找找有没有bug, 主动找bug, 有没有隐患, 排查隐患, 举一反三, 上线前扫一遍, 这块可靠吗, 体验问题, latent bug scan, sibling sweep after a fix, /bugs"},"isInternal":false,"tokens":2089,"sizeBytes":8056},{"name":"SKILL.md","path":".agents/skills/release/SKILL.md","rawUrl":"https://raw.githubusercontent.com/tw93/Kaku/HEAD/.agents/skills/release/SKILL.md","title":"release","category":"anthropic-skill","format":"markdown","content":"---\nname: release\ndescription: \"Run the Kaku macOS release flow end-to-end: preflight, build, notarize, tag, upload to GitHub Releases, dispatch the Homebrew tap, or publish the rolling Nightly preview package. Source of truth is scripts/release.sh and scripts/nightly.sh; this skill records release prerequisites and recovery hints without embedding private machine setup.\"\nwhen_to_use: \"发布, 出版本, 打 tag, release Kaku, ship Kaku, V0.x.x, kaku release, prepare release, dry-run release, resume release, notarize, homebrew tap, nightly, Nightly 包, 更新 nightly, 预览包\"\n---\n\n# Kaku Release Runbook\n\n`scripts/release.sh` is authoritative. This skill captures the release prerequisites and the order Tang and the agent rely on each release. Exact credential locations and one-time recovery commands live in the ignored `.claude/release.local.md`, never in this tracked file.\n\nBefore any stable or Nightly release, run the non-secret checks under **Verify**. The release dry-run is the authoritative credential gate: it accepts either the rcodesign path or the notarytool fallback, including environment overrides. If it passes, do not load machine-specific recovery context. If a signing identity or every notarization path is missing or stale, then check for and read `.claude/release.local.md`; on Tang's release machine it is the required recovery layer. If the local runbook is absent, stop and ask for it instead of guessing a path or replacing credentials.\n\n## TL;DR\n\n```bash\n./scripts/release.sh --dry-run                              # preflight only\nKAKU_ASC_API_KEY_PATH=/dev/null ./scripts/release.sh          # known rcodesign compatibility issue\n./scripts/release.sh                                        # full release\n./scripts/nightly.sh                                        # rolling Nightly DMG for fixes already on main\n```\n\n`release.sh` must run on `main`, with a clean tree, in sync with `origin/main`. `nightly.sh` has no branch or clean-tree gate; it only refuses to publish when HEAD is not yet on `tw93/Kaku`. Both auto-detect the signing identity and read notarization creds from the login keychain.\n\nThe `KAKU_ASC_API_KEY_PATH=/dev/null` prefix selects notarytool before submission for a known non-network compatibility issue. Do not switch backends or disable S3 acceleration to bypass a network failure. Submission and timestamp failures allow one retry on the same route; if it fails again, stop, diagnose read-only, and wait for the maintainer to resolve the network before resuming. A stored profile name does not prove that its notarytool login exists; verify the profile or use the API-key recovery below.\n\n## Nightly preview package\n\n`scripts/nightly.sh` is authoritative for the rolling `nightly` GitHub prerelease. It builds a release-grade universal app, signs, notarizes, staples, copies `dist/Kaku.dmg` to `dist/Kaku-nightly.dmg`, and recreates the `nightly` release. It does not bump versions, create a `V*` tag, update Homebrew, or refresh any stable release surface.\n\nRules:\n\n1. Only publish Nightly from the commit users should test. The script checks that HEAD exists on `tw93/Kaku` before deleting the old nightly release.\n2. Do not tell reporters to try Nightly until `./scripts/nightly.sh` has completed and `gh release view nightly -R tw93/Kaku --json tagName,targetCommitish,publishedAt,assets,url` points at the intended commit with `Kaku-nightly.dmg`.\n3. Do not use `--upload-only` unless the existing `dist/Kaku.dmg` is already known to be a signed, notarized, stapled build from the intended commit.\n4. Keep public wording precise: \"available in the latest Nightly\" only after the check above; otherwise say \"fixed on main and will be in the next Nightly or release.\"\n\n## Credential prerequisites\n\nThe ignored `.claude/release.local.md` is the canonical map from Tang's local backup paths to these script inputs. The tracked contract is limited to what the scripts consume: either `KAKU_ASC_API_KEY_PATH` or the `kaku-asc-api-key-path` login-keychain item for rcodesign, and either `KAKU_NOTARYTOOL_PROFILE` or the `kaku-notarytool-profile` login-keychain item for the fallback. Do not delete or rewrite the local recovery map merely because its paths are machine-specific.\n\n### Preferred: rcodesign + ASC API key\n\n`rcodesign` is preferred on macOS 26+ because notarytool can SIGBUS. The scripts also accept the notarytool fallback when its prerequisites are available; use the release dry-run to check eligibility and the actual notarization result to verify success. It is not in homebrew core.\n\n```bash\ncargo install apple-codesign         # binary: rcodesign, lands in ~/.cargo/bin\n```\n\nThe private JSON supplied through the environment or keychain path must use rcodesign's native `issuer_id`, `key_id`, and `private_key` format. Do not record its location in tracked docs.\n\n### Fallback: notarytool profile\n\nCreate the notarytool profile with the maintainer's private Apple ID, team ID, and app-specific password outside the repository. Pass its profile name through `KAKU_NOTARYTOOL_PROFILE` or the expected login-keychain item; never paste the source values or the import command into a tracked runbook.\n\n### Verify\n\n```bash\nsecurity find-identity -v -p codesigning | grep 'Developer ID Application'\ngh auth status\n./scripts/release.sh --dry-run\n```\n\nWhen the dry-run reports a notarization failure, use `command -v rcodesign`, `security find-generic-password -s 'kaku-asc-api-key-path'`, and `security find-generic-password -s 'kaku-notarytool-profile'` as separate diagnostics. They are not an AND gate: one complete notarization backend is sufficient.\n\n## Pre-release content checklist\n\nBefore invoking `release.sh`, confirm in this order:\n\n1. `kaku/Cargo.toml` and `kaku-gui/Cargo.toml` versions match (e.g. `0.10.0`).\n2. `assets/shell-integration/config_version.txt` is the intended schema version, with highlight rows in the matching docs.\n3. `.github/RELEASE_NOTES.md` first heading is `# V<version> <suffix>` (uppercase V, used as the GitHub Release title).\n4. Both English `Changelog` and Chinese `更新日志` sections in `RELEASE_NOTES.md` cover the same items. Register: one full stop per item, declarative middle register, neither literary nor conversational; the item count follows the number of distinct user-visible outcomes (the previous release's count is history, not a quota), and length is measured against the previous release before publishing.\n5. Any pending fixes are committed and pushed to `origin/main`.\n\nUse `./scripts/prep_release.sh <bump>` to draft the version bump and notes when starting from an older tag. Run it with `--no-edit --no-push`: it commits with the checkout's local git identity, which can be the wrong account for the day, so fill in the notes and the `config_update_highlights.tsv` stub rows, then fold them into that unpushed commit with `git -c user.name=… -c user.email=… commit --amend --reset-author` before pushing (V0.25.0). Tang typically edits the resulting `.github/RELEASE_NOTES.md` by hand to apply the announcement-writing style (community first, 2 to 4 highlights, user-experience framing).\n\n## Pre-release smoke checklist (runtime-only hotspots)\n\nThese are the areas that produce the most post-release bug reports and that CI cannot see (visual layout, native AppKit, shell-in-user-env).\n\nHand the maintainer a release-shaped build for this pass, not `make app`: that one is a debug build, and remote-terminal latency and scrolling feel nothing like the shipped app. `PROFILE=release-opt BUILD_ARCH=native ./scripts/build.sh --app-only` (about 20 minutes, signed with the local identity) differs from the release only in notarization and the second architecture; swapping it into `/Applications` and restarting lets the maintainer test in the Kaku they use every day. Run them by hand in the built `dist/Kaku.app` before tagging. Automated coverage already exists for the testable slices: tab width-budget + hover hit-testing have unit tests in `kaku-gui/src/tabbar.rs`, and `local`-outside-function / shell syntax is gated by shellcheck + the setup_zsh smoke. The list below is what still needs a human.\n\n1. **macOS window** (#408, #414): on first launch, a single click on the title bar / top inset must not maximize the window; drag the window while it fills the desktop; flip system light/dark and confirm all windows refresh; enter and exit fullscreen cleanly.\n2. **Tab bar** (#409, #435, #439, #443, #445, #447): check both `tab_bar_at_bottom` true/false; open enough tabs to overflow a narrow window and confirm titles truncate but every tab stays clickable; rename a tab, then click another to switch and confirm no position scramble; confirm a tmux/shell status prompt renders in the bar without clipping. Exercise the status dot through idle, running, pending or attention, and cleared states on active and inactive tabs, with the tab bar at both the top and bottom; idle must not leave a dot behind.\n3. **Shell setup** (#420, #432, #441, #450): from a clean `HOME`, run `kaku init` and confirm z / syntax-highlight / autosuggestions are active in a fresh shell; run `kaku init --update-only` and confirm it exits clean; open a new shell and confirm `~/.config/kaku/zsh/kaku.zsh` sources with no error.\n4. **AI chat** (#418, #431): run `kaku chat`, quit, then run it again in the same window and confirm it reopens.\n5. **Render timing / stale drawable** (#452, #458): on the bundled WebGpu backend, sleep the Mac then wake it and confirm the window repaints instead of freezing on the old frame while keystrokes still reach the shell; connect or disconnect an external display and confirm no frozen frame or geometry jump; open a new window straight into fullscreen and resize it, confirming it fills without a stale first frame.\n6. **Hyperlinks** (#547): Cmd-hover a URL that the terminal soft-wraps and confirm both rows target the full URL. Then print a URL exactly as wide as the terminal, followed by `\\r\\n` and unindented unrelated text; the target must exclude that text. Terminal and surface tests cover the grouping; check the built app hover target too.\n\n7. **Mouse recovery** (#551): in both zsh and fish, run `printf '\\033[?1003;1006h'` and return to the prompt; moving the mouse must not insert escape sequences. Also keep a mouse-enabled TUI running and confirm mouse input still works until it exits.\n\n8. **Settings lifecycle and custom configs** (#545): start the app with a minimal standalone `--config-file`, switch between Kaku Light and Kaku Dark, and confirm Settings edits that file and the main window follows. Save and exit Settings repeatedly; no destroyed-window repaint panic should appear.\n\n9. **Tab bar pointer paths** (V0.22.0): with 2+ tabs and the bar at the bottom, tap just above a tab and confirm it switches; middle-click to paste on the last terminal row and confirm no tab closes; right-click an inactive tab, choose Close Tab several times and confirm the app stays up and closes that tab while you stay on yours.\n10. **Split panes with dimming** (#562): set `inactive_pane_hsb = { brightness = 0.7 }`, split top|bottom with the tab bar at top and at bottom, focus each pane, and confirm the dimmed background stops exactly at the line with no undimmed strip on the right or bottom edge; split into three panes and confirm two dimmed neighbours still show a faint line; drag the line and confirm it does not jump a row on grab.\n\nWhen a release fixes a bug outside this list, add the reproduction here so the next release re-checks it.\n\n## Verification commands\n\n| Scope | Command |\n|---|---|\n| Format | `make fmt-check` |\n| Compile | `make check` |\n| Tests | `make test` |\n| Release notes version match | `./scripts/check_release_notes.sh` |\n| Config schema versioning | `./scripts/check_release_config.sh` |\n| Config release readiness | `./scripts/check_config_release_readiness.sh` |\n| Full preflight | `./scripts/release.sh --dry-run` |\n\n`release.sh --dry-run` stops after preflight and does not run source checks. The normal release reuses successful CI Checks for the current HEAD; otherwise `stage:checks` runs the local checks below, as described in `AGENTS.md`. A passing dry-run alone is not source-check evidence.\n\n## Stage map\n\n`./scripts/release.sh` runs these stages in order. Each is timed and labeled `[stage:<name>]`.\n\n1. **preflight**: clean git on `main` in sync with `origin/main`, version consistency, gh auth, release notes, config, latest `Build Validation` workflow run on `main` green (an in-progress run only warns), profile, signing identity, notarization creds.\n2. **stage:checks**: reuse successful CI Checks on the current HEAD, or run `make fmt-check && make check && make test` locally. `RUN_CLIPPY=1` adds local clippy in either case. `SKIP_TESTS=1` skips local tests (avoid).\n3. **stage:build**: `./scripts/build.sh`, `PROFILE=release-opt`, `BUILD_ARCH=universal`. Output: `dist/Kaku.app`, `dist/Kaku.dmg`, `dist/kaku_for_update.zip`, `dist/kaku_for_update.zip.sha256`.\n4. **stage:notarize**: `./scripts/notarize.sh`. Tries rcodesign first; falls back to notarytool only for a non-network rcodesign failure when a notarytool profile exists.\n5. **stage:tag**: `git tag -a V<version> -m 'Release V<version>'` then `git push origin V<version>`. Idempotent: reuses an existing tag at HEAD instead of dying.\n6. **stage:upload**: `gh release create V<version>` (or `gh release edit` if it already exists) with the dmg, zip, and sha256. Title is taken from the first `# ` line of `RELEASE_NOTES.md`.\n7. **stage:homebrew-tap**: `repository_dispatch` (`kaku_release_published`) to `tw93/homebrew-tap` triggering `bump.yml`. Polls `Casks/kakuku.rb` through the GitHub contents API for the new version (default 12 attempts, 15s apart). The official `homebrew/cask` token is `kaku` and is bumped by Homebrew livecheck from the GitHub release; this stage only updates the personal tap for existing `tw93/tap/kakuku` installs.\n\n## Resume after failure\n\n`release.sh` accepts these flags to skip already-completed stages:\n\n| Flag | Skips | Use when |\n|---|---|---|\n| `--notarize-only` | build | Build succeeded; notarization failed or was interrupted. |\n| `--upload-only` | build, notarize | Notarized `dist/Kaku.app` and `Kaku.dmg` exist on disk; only need to tag + upload + tap. |\n| `--tap-only` | build, notarize, upload | GitHub release exists; only the Homebrew tap dispatch needs to rerun. |\n\nResume flags require the corresponding artifacts in `dist/` to still be present.\n\n## Common blockers\n\n- **notarytool prints `Submission ID received` and then fails**: the submission was created but the DMG upload to Apple's S3 did not finish (there is no `Successfully uploaded file` line), so that submission stays `In Progress` forever and waiting on it is pointless. Once the network is back, resubmit the same artifacts with `./scripts/release.sh --notarize-only`; a good submission came back Accepted in about four minutes (V0.25.0).\n- **Asset upload fails with `HTTP 408: Upload body timed out` or `EOF`**: the proxy cut the long upload; `gh release create` then deletes the new release and keeps the tag. Rerun `env -u HTTPS_PROXY -u HTTP_PROXY -u ALL_PROXY -u https_proxy -u http_proxy -u all_proxy ./scripts/release.sh --upload-only` (gh reads only proxy variables, not the macOS system proxy). Upload failures may be retried up to five times, 30 seconds apart (maintainer 2026-10-20); V0.26.0 went through on the first direct retry even with the route on a TUN interface. Start a full release with the same `env -u` prefix.\n- **`Local main is not synchronized with origin/main`**: push the pending commit. `release.sh` requires the tag to point at a commit that exists on origin.\n- **`rcodesign` not found**: `cargo install apple-codesign`, then verify with `which rcodesign`. Do not try `brew install rcodesign`; it is not a core formula.\n- **`No Developer ID Application certificate found`**: re-import the certificate from the maintainer-approved private backup through Keychain Access, using the password from the private runbook. Never add either location to this file.\n- **`Notarization credentials not found`**: follow the ignored private setup runbook to provision one of the environment/keychain inputs described under Credential prerequisites, then rerun `./scripts/release.sh --dry-run`. The script otherwise prompts interactively, which fails in non-interactive shells.\n- **rcodesign S3 connect timeout**: `s3 upload error: HTTP connect timeout occurred after 3.1s` identifies the failed upload connection, not a credential fault. `notarize.sh` retries once on rcodesign and stops if the network failure persists. Preserve the signed artifact, report the blocked submission, and resume only after the maintainer resolves the network; do not switch to notarytool to retry the same failure.\n- **`No Keychain password item found for profile`**: a profile-name entry alone does not establish a working login. An installed Apple `notarytool` can reuse the existing ASC `.p8` key with `--key`, `--key-id`, and `--issuer`, without creating a Keychain login. Resolve the inputs from the private runbook and keep their values out of displayed commands and logs. For S3 upload failures, apply the same one-retry network budget above and keep the signed artifact fixed. After `Accepted`, retrieve the submission log and verify its SHA-256 against the submitted DMG, then run `./scripts/notarize.sh --staple-only` and `./scripts/nightly.sh --upload-only` from the same source commit. See [Apple's notarization API workflow](https://developer.apple.com/documentation/notaryapi/submitting-software-for-notarization-over-the-web).\n- **Homebrew tap verifier timed out at 12/12**: the tap workflow commits `kakuku <version>` to `tw93/homebrew-tap` main on success; verify with `gh api repos/tw93/homebrew-tap/commits/main --jq .commit.message`. The verifier reads the cask through the GitHub contents API (`Accept: application/vnd.github.raw`), not the Fastly-cached `download_url`. Network errors retry once and stop, and other API errors stop immediately; the 12-poll budget only waits for a version change. A version-poll timeout means the expected version was not observed: open the run URL the script printed. If the tap commit is present the release is complete. Widen the window with `HOMEBREW_TAP_VERIFY_ATTEMPTS` / `HOMEBREW_TAP_VERIFY_SLEEP_SECONDS`, or rerun `./scripts/release.sh --tap-only`; `REQUIRE_HOMEBREW_TAP_UPDATE=0` only downgrades the failure to a warning.\n- **`rcodesign notarization failed` right after the log shows `\"status\": \"Accepted\"`**: Apple accepted; the step died stapling. rcodesign cannot staple an unsigned DMG, which is what `build.sh` produces, and the script used to pass `--staple` (removed 2026-09-10). Do not touch credentials. Recover with `./scripts/notarize.sh --staple-only` then `./scripts/nightly.sh --upload-only`.\n- **`codesign: A timestamp was expected but was not found`** (or `timestamp service is not available`): the timestamp request failed during signing; this message alone does not establish a certificate fault or identify the network cause. `build.sh` retries timestamp failures once before aborting. If that retry fails, stop the release, perform read-only network diagnosis, and report the blocked signing step. Resume only after the maintainer resolves the network problem; do not rerun the build or change proxy settings to retry the same failure.\n- **`codesign: code object is not signed at all, In subcomponent: .../MacOS/k`** while signing `kaku-gui`: `kaku-gui` is the bundle's main executable and codesign validates its siblings as nested code, so every other executable in `Contents/MacOS` must already be signed. `build.sh` signs `kaku` and `k` before `kaku-gui` since 2026-09-10; if a new helper binary is added to the bundle, add it to that loop before `kaku-gui`.\n- **`stapler` fails with CloudKit timeout, exit code 68**: the log reads `The staple and validate action failed! Error 68` right after `Notarization accepted`. Notarization already succeeded; only ticket retrieval from `api.apple-cloudkit.com` timed out. Do not re-notarize. One `./scripts/notarize.sh --staple-only` retry is allowed; if the network failure repeats, stop for read-only diagnosis and wait for the maintainer to resolve it. Only after stapling succeeds, resume with `./scripts/release.sh --upload-only`. The staple-only path regenerates `kaku_for_update.zip` and its sha256 on purpose: attaching the ticket changes the app, so the in-app update archive must be rebuilt after stapling, never before.\n- **Tag already exists on origin at a different SHA**: do not force-push tags. Pick the next patch number, bump versions, and start over.\n\n## Environment variable overrides\n\n| Variable | Default | Purpose |\n|---|---|---|\n| `KAKU_SIGNING_IDENTITY` | auto-detect | Override the Developer ID Application identity. |\n| `KAKU_ASC_API_KEY_PATH` | keychain item `kaku-asc-api-key-path` | Path to rcodesign ASC API key JSON. |\n| `KAKU_NOTARYTOOL_PROFILE` | keychain item `kaku-notarytool-profile` | notarytool keychain profile name. |\n| `HOMEBREW_TAP_TOKEN` | `gh auth token` | GitHub token for tap dispatch. |\n| `REQUIRE_HOMEBREW_TAP_UPDATE` | `1` | Set to `0` to allow release to succeed when tap dispatch fails. |\n| `HOMEBREW_TAP_VERIFY_ATTEMPTS` | `12` | Polls of the tap cask before the verifier gives up. |\n| `HOMEBREW_TAP_VERIFY_SLEEP_SECONDS` | `15` | Seconds between tap cask polls. |\n| `RUN_CLIPPY` | `0` | Set to `1` to run `cargo clippy` during stage:checks. |\n| `SKIP_TESTS` | `0` | Set to `1` to skip `make test` during stage:checks. |\n| `OUT_DIR` | `<repo>/dist` | Override artifact output directory. |\n| `PROFILE` | `release-opt` | Cargo profile. Only `release` and `release-opt` are accepted. |\n| `BUILD_ARCH` | `universal` | Passed through to `build.sh`. |\n\n## After release\n\n- GitHub Release URL: `https://github.com/tw93/Kaku/releases/tag/V<version>`.\n- Official Homebrew cask users get the new version after Homebrew livecheck/bump; verify with `brew update && brew info --cask kaku`. Personal-tap users get it once the tap workflow finishes; verify with `brew info --cask tw93/tap/kakuku`.\n- In-app updates (`kaku-gui/src/update.rs`, Kaku's own updater, not Sparkle) download `kaku_for_update.zip` and its `.sha256` from the latest GitHub Release.\n- Add the six positive reactions to the new release; this is part of shipping. Skip `-1` and `confused`.\n\n  ```bash\n  id=$(gh api repos/tw93/Kaku/releases/tags/V<version> --jq .id)\n  for c in +1 laugh heart hooray rocket eyes; do\n      gh api -X POST \"repos/tw93/Kaku/releases/$id/reactions\" -f content=\"$c\"\n  done\n  ```\n\nFor the announcement post (X / WeChat): community first, 2-4 highlights, user-experience framing, one opinionated sentence. The release notes file is a different artifact; do not paste it as the announcement.\n","frontmatter":{"name":"release","description":"Run the Kaku macOS release flow end-to-end: preflight, build, notarize, tag, upload to GitHub Releases, dispatch the Homebrew tap, or publish the rolling Nightly preview package. Source of truth is scripts/release.sh and scripts/nightly.sh; this skill records release prerequisites and recovery hints without embedding private machine setup.","when_to_use":"发布, 出版本, 打 tag, release Kaku, ship Kaku, V0.x.x, kaku release, prepare release, dry-run release, resume release, notarize, homebrew tap, nightly, Nightly 包, 更新 nightly, 预览包"},"isInternal":false,"tokens":5529,"sizeBytes":22865},{"name":"macos.md","path":".claude/rules/macos.md","rawUrl":"https://raw.githubusercontent.com/tw93/Kaku/HEAD/.claude/rules/macos.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"# Kaku macOS Platform Rules\n\n> macOS 26 上 AppKit 自动注入是 PAC-fault / 崩溃的高频来源。改 menubar / window handling 前必读本文。\n\n## 两套像素坐标空间，禁止混算\n\n窗口/屏幕几何存在两套空间，只在\"窗口所在屏 scale == 主屏 scale\"时数值兼容，混用的 bug 只在外接屏暴露（#456 拖动跳变）：\n\n- **Canonical ScreenPoint 空间**：`cartesian_to_screen_point` 定义（全局 point × `screens[0]` 主屏 scale、Y 翻转）。`set_window_position` / `window_position` / `MouseEvent::screen_coords` / `MouseEvent::window_origin` 都在这里。\n- **窗口 backing 空间**：`MouseEvent::coords`、`dimensions.pixel_*`、UI item 布局坐标，用窗口自己屏幕的 backingScaleFactor。\n\n规则：跨空间换算一律在 `window/src/os/macos` 层做；GUI 层需要窗口原点用 `MouseEvent::window_origin`，不要用 `screen_coords - coords` 推算；居中走 `WindowOps::center()`（原生、单屏 point 空间）。`ScreenInfo.rect`（`screens()` / `resolve_geometry` / `--position` / Lua `screens()`）已统一到 canonical 空间（`nsscreen_to_screen_info` 走 `cartesian_to_screen_point`），`Window::new_window`（`window/src/os/macos/window.rs`）不再对 resolve 出的 x/y 除 scale（除一次就是 upstream 继承的 Retina 落点折半 bug，size 的除法保留，那是 px 转 pt 给 NSWindow 用的）。\n\n## 默认渲染后端是 WebGpu，CGL-only 的 wake 修复护不住它\n\nbundled `kaku.lua` 设了 `front_end = 'WebGpu'`（Metal via wgpu）。`window/src/os/macos` 里的睡眠/唤醒防护（`SYSTEM_SLEEPING` flush 闸门、`BackendImpl::update`、display-change present defer）全部只作用于 CGL 路径，对默认用户是 no-op。给 sleep/wake/display-reconfig 加修复时必须同时考虑 WebGpu 路径（#458）。\n\nWebGpu surface 失效的恢复入口在 `TermWindow::do_paint_webgpu`：休眠唤醒会让 Metal drawable 失效但窗口尺寸不变，`WebGpuState::resize` 对相同尺寸是故意 no-op（热路径），所以恢复必须走 `reconfigure_surface()` 强制 `surface.configure`，不能用 `resize(dims)` 代替。wgpu Metal 后端把 nil `nextDrawable` 映射成 `SurfaceError::Timeout`，所以 Timeout 连续出现同样要触发重配，不能永远静默跳帧。症状签名：键盘输入照常进 PTY、画面停在旧帧，说明是渲染管线死了而不是主线程卡死。\n\n## AppKit Menu 自动注入是地雷\n\nNSApplication 默认会向 Window menu 和 Help menu 注入自己的项（Tile / Move & Resize / Bring All to Front / Help Search），并把 NSWindowRepresentingMenuItem 挂在 Windows menu 下做 dangling reference 。在 macOS 26 上，这些注入项可能 PAC-fault：\n\n- `-[NSMenu _performKeyEquivalentWithDelegate:]` 在 Ctrl+Cmd+F 路由时崩\n- Windows menu 关闭窗口后 NSWindowRepresentingMenuItem dangling\n- Help Search field 触发 menu 重新计算时崩\n\n## 必须执行的兜底\n\n- 不要调 `setWindowsMenu:` 让 AppKit 接管 Windows menu。Kaku 有自己的 tab/window 切换器。\n- 不要调 `setHelpMenu:` 让 AppKit 注入 Help Search。Kaku 没有 help book。\n- 窗口创建时调 `setExcludedFromWindowsMenu:` 让 AppKit 看不见我们的 NSWindow，避免它把窗口加到 Windows menu。\n\n参考历史 commit：\n- `1ec3823` - 第一次排查 NSWindowRepresentingMenuItem dangling\n- `4b15bab` - 进一步 stop AppKit from injecting into Window/Help menus\n- `fc2dfa9` - redirect AppKit Spotlight for Help to an orphan menu\n- `6ba7381` - restore synchronous menubar init to prevent key event loss\n\n## Menubar 初始化时序\n\nMenubar **必须同步初始化**。延后到 async block 会丢早期按键事件（Cmd+Q 在启动后立刻按下会被吞）。见 `6ba7381`。\n\n## Key Equivalent\n\n新增 KeyAssignment 前确认走 Kaku 自己的按键分发路径，不要走 AppKit menu 的 `performKeyEquivalent`。后者会经过被 PAC-fault 的注入项。\n\n### menu keyEquivalent 会拦 keyDown，吞掉 raw-mode TUI 的 Ctrl+letter\n\nmacOS 26 上 NSMenu 的 keyEquivalent modifier 匹配并不严格相等。给 menu item 装 `Ctrl+letter` 形式的快捷键，即使带额外 modifier（如 `NSFunctionKeyMask` 想做 `Fn+Ctrl+letter`），仍会拦截 plain `Ctrl+letter` 的 keyDown。后果：\n\n- AppKit 把 keyDown 路由给 menu item action，NSWindow 收不到。\n- keyUp 不走 `performKeyEquivalent:`，所以 keyUp 照常进 Kaku。\n- `termwiz/src/input.rs` 的 `encode` 对 `is_down=false` 直接返回空字符串，PTY 上一个字节都不会到。\n- 普通 shell `cat -v` 测试看到 `^C` 不代表 raw-mode 也工作；启动时机和 cooked vs raw 都会让 menu 拦截表现不一致。**必须**在 raw-mode TUI (claude / codex / vim / htop) 里实测。\n\n兜底：任何\"模仿系统快捷键\"的 menu 项宁可 `keys: vec![]` 不设快捷键，让用户从菜单点。`kaku-gui/src/commands.rs` 里的 keyEquivalent 装配逻辑必须统一从按键候选本身取 modifiers，不要为个别 menu 项走强制 modifier 的特例化路径（历史上的特例化路径已删，不要加回）。\n\n参考历史：`8aba4475` feat(window): add CenterWindow menu without macOS shortcut（Window > Center 刻意不装 keyEquivalent，因为 macOS 26 会把 `Fn+Ctrl+C` 当 plain Ctrl+C 拦了，导致 raw-mode 下 Ctrl+C 不退 claude/codex）。\n\n### 排查 keyDown 丢失\n\n`config.debug_key_events = true` 重启 Kaku，复现按键，看 `~/.local/share/kaku/kaku-gui-log-<pid>.txt`：\n\n- grep `key_event.*CTRL`。\n- 如果只有 `key_is_down: false`、没有 `key_is_down: true`，就是 menu keyEquivalent 拦了 keyDown。\n- 这种情况下不要去查 termwiz / PTY / termios，先找 `set_key_equiv_modifier_mask` 装配点。\n\n## Cocoa 对象所有权：StrongPtr::new 只接 alloc / new / copy\n\n`StrongPtr::new` 接管的是 +1 引用，只能包 `alloc`+`init`、`new`、`copy` 拿到的对象。`+[NSMenuItem separatorItem]` 这类类方法返回的是 autorelease 的 +0 对象，必须用 `StrongPtr::retain`，否则 drop 时多 release 一次。V0.22.0 前 `MenuItem::new_separator()` 就是这么写的：主菜单栏在启动时构建，那时还没有 autorelease pool（从代码推断，未实测），多出的那次 release 被泄漏抵消，所以一直没暴露；第一个在事件处理里构建、带分隔线的菜单（右键 tab 菜单）一弹出就崩（`7b6b6ce2`，带回归测试）。\n\n- 崩溃签名：主线程 `objc_release > AutoreleasePoolPage::releaseUntil > objc_autoreleasePoolPop > -[NSApplication run]`，紧跟在一次原生菜单动作之后。看到它先审 `StrongPtr::new` 包的是不是非 alloc/new/copy 来的对象，再怀疑 AppKit。\n- 拿不准某个 API 返回 +0 还是 +1，用 `clang -fno-objc-arc` 写个十行的 MRC 程序在 `@autoreleasepool` 里打印 `retainCount`，实测，不靠记忆。\n- 新增在事件处理中构建的 `NSMenu`（右键菜单等），必须在 `make app` 的包里真的右键点一次菜单项再下结论，单元测试看不到 autorelease pool 的时序。\n- 配置重载后菜单栏在 macOS 上也会重建（#570）：`frontend.rs` 的 reload 回调在 `spawn_into_main_thread` 任务里调 `recreate_menubar`，不在可能持有 config 锁的回调里直接调。原先挡住它的 TODO 疑似就是上面这个分隔线多释放，修复后放开。外观切换也会触发 `config::reload()`，所以这条重建路径在每次深浅色切换时都会走。`recreate_menubar` 给 Kaku 加的每一项打 `KAKU_MENU_ITEM_TAG`，重建时只删带这个 tag 的项、保留顶层菜单对象，再从每个菜单的开头按 rank 插入新项。不要改成 `remove_all_items` 清空主菜单或换新的 NSMenu：AppKit 只在启动时往 Edit 菜单注入 AutoFill、Start Dictation、Emoji & Symbols，菜单对象一换这些项就再也回不来，再调一次 `setMainMenu:` 也不行（MRC 小程序实测）。也不要换回旧的 mark-sweep：它只扫 `kakuPerformKeyAssignment:` 项，Kaku 菜单的固定项只在子菜单新建时创建、分隔线每次都追加，复用旧菜单会丢固定项、分隔线越积越多、新项排到末尾。`config_reload_rebuilds_menubar_on_macos_in_place` 守这几点。随 mux 状态变化的命令（Attach/Detach Domain、非 local 的 New Tab (Domain X)、Switch to workspace）只进命令面板不进菜单栏：菜单栏只在配置重载时重建，启动时 domain 还没注册，放进去会在一次深浅色切换后突然多出 `~/.ssh/config` 每个 Host 两条 Attach 项，之后又随 attach 状态过时，`mux_state_commands_stay_out_of_the_menubar` 守这一点。\n\n## 调试方式\n\n- 怀疑 menu 注入或菜单对象相关崩溃时：读 `~/Library/Logs/DiagnosticReports/kaku-gui-*.ips` 里最近的 crash report。\n- 看 `NSMenu _performKeyEquivalentWithDelegate:` 栈帧是 AppKit 注入项的标志。\n- 主要复现环境是一台 macOS 26 实机。\n","isInternal":false,"tokens":3043,"sizeBytes":8953}],"systemPromptSnippet":"<yakaai_skills repo=\"tw93/Kaku\">\n<!-- File: AGENTS.md (Tokens: ~7507 | Category: root-instruction) -->\n# Kaku Agent Guide\n\nKaku is a macOS-native terminal emulator derived from WezTerm and shaped around AI-assisted terminal workflows. This guide is the shared operating context for agents working in this repository.\n\n## Repository Map\n\n- `kaku/` - CLI entry points, command flows, and user-facing configuration commands.\n- `kaku-gui/` - GUI, rendering, window lifecycle, input, mouse handling, AI chat, and the `k` helper binary.\n- `mux/` - tabs, panes, domains, and client/server state.\n- `term/` - terminal emulation and screen buffer behavior.\n- `termwiz/` - terminal UI primitives.\n- `config/` - Lua config loading, schema behavior, proxy settings, and versioned defaults.\n- `window/` - platform windowing layer.\n- `lua-api-crates/` - Rust-to-Lua API bindings.\n- `crates/` - shared utility crates, including Kaku-specific AI helpers.\n- `assets/` - app resources, bundled config, shell integration, and vendor assets.\n- `scripts/` - build, release, and validation helpers.\n- `docs/` - user and developer documentation.\n- `.agents/skills/` - the canonical and tracked home for project skills: `release` and `bugs`. The website skill `product-docs` lives with the site on the `vercel` worktree (`~/www/kaku-site/.agents/skills/product-docs`). `.claude/` is gitignored except for the force-added `.claude/rules/macos.md`; its skill entries may be relative symlinks or ignored private overrides. Do not cite, copy, or edit an ignored `.claude/skills/` directory as project truth. Put durable skill changes in `.agents/skills/` first, then update a symlink when that entry is actually a link.\n- `.github/workflows/checks.yml` - fast correctness gates on pushes to `main` and on pull requests, with `paths-ignore` for `**.md` and assets, so a docs-only change gets no CI at all. Five macOS jobs, the runner's concurrency cap: Format, Unit Tests, Relay Build, Clippy (which also carries the source guards and stands in for `cargo check`, scoped to the lint-opt-in crates, not the workspace), and Scripts and Release Checks (release notes, config version, shell smoke tests, shellcheck). Adding a sixth macOS job makes one queue.\n- `.github/workflows/build-validation.yml` - release-shaped builds with the shipped `release-opt` profile: one job per architecture in parallel (the arm64 one also assembles the bundle), then a `lipo` merge of `kaku`, `kaku-gui` and `k`; runs on build-pipeline changes, daily (skipped when that commit already passed), or on dispatch. `release.sh` preflight requires its latest run green and the latest Security Audit run not failed, and skips its local fmt/check/test when CI Checks already passed on HEAD.\n- `.github/RELEASE_NOTES.md` - source for the GitHub Release title and body.\n\n## Commands\n\n```bash\nmake fmt\nmake fmt-check\nmake check\nmake test\nmake dev\nmake app\n./scripts/build.sh\n./scripts/check_config_release_readiness.sh\n./scripts/check_release_config.sh\n./scripts/check_release_notes.sh\n```\n\n`make build`, `make check`, `make test` and `scripts/build.sh` first run `scripts/prune_target.sh`, which measures `target/` at most once a day and runs `cargo clean` once it reaches `KAKU_TARGET_CAP_GB` (default 40). cargo never drops old artifacts, and the profiles, architectures and remote on/off feature sets here grew it to 92G; a partial, age-based delete was rejected because it can leave fingerprints pointing at missing files. No scheduled job does this, by the maintainer's choice. The dev profile also builds third-party crates without debug info.\n\n`make fmt` and `make fmt-check` both shell out to `cargo +nightly fmt`, so they require the nightly toolchain. Use `make app` for GUI, rendering, windowing, and AI overlay verification because it builds the app bundle that users run.\n\n## Working Rules\n\n- Work on the current branch unless the maintainer asks for a branch or worktree.\n- Keep changes inside one crate or subsystem when the problem allows it.\n- Upstream: Kaku forked WezTerm at `05343b38` (2026-01-17; term/, mux/, the escape parser and wezterm-font matched it byte for byte). On 2026-10-14 upstream main up to `e2a98e1b` was reviewed and its macOS-relevant fixes absorbed (commits citing upstream shas, e.g. DECRQCRA, RIS reset, OSC 8 `;`, tmux -CC input, workspace repaint). Start the next sync from `e2a98e1b`; map `wezterm-gui` to `kaku-gui` and top-level crates to `crates/`, and judge applicability rather than applying patches literally.\n- Kaku is macOS-only. Windows code (cfg(windows) branches, ConPTY/GDI/WSL paths, winapi and the other Windows-only dependencies) was removed on 2026-10-13; do not bring it back when porting upstream WezTerm fixes, port only the macOS-relevant half. `wsl_domains`, `win32_system_backdrop` and `win32_acrylic_accent_color` stay as deprecated config keys so shared configs still load (`windows_only_keys_still_load`). Vendored crates under `deps/` keep their upstream Windows code untouched.\n- Inspect public APIs and cross-crate boundaries before changing shared behavior.\n- Kaku must not automatically inject agent hooks, instructions, or settings. Explicit edits through `kaku ai` may update the selected provider's configuration (including Claude and Codex settings), preserving user-owned fields. Agent-facing help belongs in documentation. The `kaku init` source line in `.zshrc` is managed by `assets/shell-integration/setup_zsh.sh` and checked by `doctor.rs`; `reset.rs` must keep every user-owned line around it.\n- Narrowing a default that existing users already depend on must ship the other camp's explicit path in the same change. Shell integration scopes the Starship prompt to `TERM_PROGRAM=Kaku` OR (`$TMUX` and `KAKU_SESSION`), with `KAKU_PROMPT_EVERYWHERE=1` as the opt-in for every terminal; Smart Tab stays Kaku-only (#503). The predicate is duplicated inside each installer, three times in `setup_zsh.sh` and four in `setup_fish.sh`, so grep both files and change every copy. `assets/shell-integration/tests/prompt_scope_smoke.sh` gates the placements in CI via `.github/workflows/checks.yml`, and `docs/faq.md` is the user-facing half; a change that re-narrows the scope must update both installers, the smoke and the doc together.\n- Tab indicator decision (2026-08-24): the tab bar draws a dot only for `Progress::Paused | Progress::Error` (attention, `kaku-gui/src/tabbar.rs`); a running pane deliberately shows nothing, the running-state dot was dropped in `4f297556`. Do not bring it back.\n- Retro tab bar chrome (2026-09-09, #548): the bundled config (`assets/macos/Kaku.app/Contents/Resources/kaku.lua`) sets `use_fancy_tab_bar = false`, so the shipped bar is the retro one even though the `config/src/config.rs` schema default is true. `kaku-gui/src/tabbar.rs` composes a `Line` of cells, so a control there is a character on the text baseline competing with the title for the one trailing cell the rule above spends on attention; drawn controls belong in the box-model renderers (`make_x_button` in `kaku-gui/src/termwindow/render/fancy_tab_bar.rs`, and the opt-in touch renderer in `kaku-gui/src/termwindow/touch.rs`). The per-tab close mark added in `cef3a371` (three cells, \" X \", over the title end) was reverted in `732d2082` for that reason. On 2026-10-12 it was tried again as a single hover `×` in the status cell, with one and then two cells of padding per side; the maintainer judged it worse than none (a grid glyph cannot sit centered between title and edge) and removed it: the desktop layout closes tabs from the tab's right-click menu, middle-click and Cmd+W, while the web terminal keeps its hover close icon, which is drawn and centered. Answer tab bar chrome requests with the pointers that already exist: the new-tab button is on by default (bundled `show_new_tab_button_in_tab_bar = true`), right-click on a tab opens a tab menu with New Tab, Tab Navigator and Close Tab (`tab_context_menu_actions` in `kaku-gui/src/termwindow/mouseevent.rs`), middle-click closes a tab, and the Shell menu lists New Tab and Close Tab. For touch over remote desktop, a plain left press (a tap) up to `BOTTOM_TAB_BAR_TOUCH_EXTENSION_PT` above a bottom bar still hits tabs and the new-tab button, while middle and right presses there stay with the terminal, so the bar itself stays one row. Do not re-add a native close mark: if one is ever wanted, it has to be a drawn, centered icon like the fancy bar's `make_x_button`, not a grid character. The desktop bottom retro bar is one terminal row with line_height (`tab_bar_pixel_height_impl`), the same as a top bar; the compact natural height read as cramped. Touch mode (2026-10-14) is a per-window toggle with one entry, Window > Touch Mode; the on-bar Touch button and the titlebar entry were removed as clutter. It keeps the normal bar height and look and only adds a drawn, centered close icon to every retro tab, over cells the bar keeps at the tab's end (`close_slots` in `kaku-gui/src/tabbar.rs`). The separate 40pt touch bar, its Menu button and panels were removed; do not bring them back. A long press (left press held 450ms, under 24pt of movement, acted on at release) replays as a right click in every mode, so tab and terminal context menus cover touch; UU was measured to forward a long press as a held left button, never a right click.\n- Line height decision (2026-07): the bundled default stays `line_height = 1.28`. Character-cell graphics (QR codes, `neofetch` logos, TUI bar charts) scale with the row height, so they render about 28% taller than at `line_height = 1.0`, and that trade-off is accepted. Rendering block glyphs at their natural cell height was tried in `d810b13` and reverted in `3f131ea`: block characters must fill the whole cell or box borders and QR codes open horizontal seams. Do not propose lowering the default or re-attempting natural-height block rendering; point users at the `line_height` entries in `docs/faq.md` and `docs/configuration.md`. The block cursor is a separate case and correctly keeps using `natural_cell_height` (see `kaku-gui/AGENTS.md`).\n- Draft issue and PR replies unless the maintainer has already approved the exact public action.\n- Do not modify files outside this repository without showing the intended change and getting explicit confirmation.\n- Do not add instructions for the removed `website/` tree unless that directory exists in the current worktree.\n- The marketing and docs site lives on the `vercel` branch (linked worktree at `~/www/kaku-site`), not on `main`. It follows the design guide at `~/www/kaku-site/DESIGN.md`; verify changes with screenshots at 375px / 1280px and deploy by pushing the `vercel` branch (Vercel serves kaku.fun).\n- Keep private credentials, local keychain paths, and machine-specific release notes out of public repository docs.\n- **Do not propose UI i18n / multi-language menus / a `config.language` setting.** The `rust-i18n` based Chinese UI localization (PR #362, commit `f6cfb4b`) was reverted in `b4d779a` on 2026-05-18; `language` remains in the config schema as a deprecated field for backward compat only. UI strings (menus, confirm dialogs, config TUI copy) stay as English literal strings. New UI surfaces should not introduce translation keys, locale-aware formatting, or \"what if a user wants Chinese\" abstractions. If a user requests a non-English UI, route to the assistant config / AI chat surface; those already accept non-English content.\n- **Kaku stays free, including Kaku Assistant and remote access (maintainer 2026-10-18).** Kaku Assistant runs on the user's own API key or Codex login, so it has no cost to meter. The relay (`crates/kaku-relay`, one Fly machine) is paid for by the maintainer as a thank-you to the community, and `docs/faq.md` says so. Do not propose paid tiers, usage-based billing, accounts or usage metering for the relay; when it nears the concurrency limit in `fly.toml`, load test and move to a larger machine instead.\n- **Do not pre-bake provider abstractions in `kaku/src/ai_config/`.** Kaku Assistant parsing, field presentation, and persistence live in `kaku/src/ai_config/tui/providers/assistant.rs`; the TUI retains its event loop and shared data types. Future extraction should move one concrete provider at a time into that existing directory. Do not add an unused trait, a `ProviderKind` enum, or stub modules. Save Copilot for last because its OAuth flow is the abstraction stress test.\n- CLI Codex readiness and GUI connection parsing both consume `tests/fixtures/codex-connection.json`. Add boundary cases there when either policy changes, and run both crates' tests.\n- Official Homebrew token is `kaku` (`brew install --cask kaku`). `tw93/tap/kakuku` remains the personal tap for people who installed before the official cask. Do not tell users the Homebrew `kaku` token is a different app.\n\n## Maintainer Follow-up\n\n- For current issue and PR sweeps, read live GitHub state first with `gh issue list` and `gh pr list`; refresh once more before final conclusions or public actions.\n- Before commenting on or closing an item, confirm its title, state, and author with `gh issue view` or `gh pr view`.\n- The latest stable tag is `git tag --list 'V*' --sort=-version:refname | head -1`; without the `V*` filter the rolling `nightly` tag sorts first. Map each report to `git log <tag>..HEAD`, and treat a closed issue as a claim until the fix commit or remaining gap is named.\n- Shell integration fixes run the affected `assets/shell-integration/tests/*_smoke.sh`, then the list `.github/workflows/checks.yml` runs under `Shell integration smoke tests`. Watch the pushed `main` run with `gh run watch <id> --exit-status`.\n- Do not close issues or PRs on local green alone. For fixes pushed to `main`, wait for the new GitHub Actions run on `main` to pass before posting fixed/closed replies.\n- The rolling `nightly` release is not rebuilt by push. Before sending users to Nightly, run or verify `./scripts/nightly.sh` and confirm `gh release view nightly --json tagName,targetCommitish,publishedAt,assets,url` points at the fix commit and includes `Kaku-nightly.dmg`.\n- Default issue-closure pipeline once a fix is verified: commit the fix, refresh the `nightly` release assets per the rule above, reply in the reporter's language with the Nightly download or in-app update path, then close if the current request already authorizes closure; otherwise propose it and wait for authorization. Do not promise a specific packaged-release date in replies.\n- Before pushing `main`, run `git fetch origin main` and verify `origin/main` has not moved unexpectedly. If it moved, stop and review `origin/main..HEAD` before pushing.\n- For several unrelated bugs, prefer one commit per issue or behavior, and put the issue number in the subject: `fix(scope): #123 short summary`.\n- Merge contributor PRs with `gh pr merge --merge`, never squash, so the contributor's own commits and authorship stay in history; add no trailer. When `maintainerCanModify` is true, push fix commits to the contributor's branch rather than rewriting their existing commits, and approve a first-time contributor's workflow run with `gh api -X POST repos/tw93/Kaku/actions/runs/<id>/approve` before expecting CI.\n- If an accepted PR's equivalent fix lands outside the contributor branch, acknowledge the contribution and explain which user-visible fix was delivered before closing it; keep commit hashes and internal attribution bookkeeping out of the public reply.\n\n## Investigation Order\n\nWhen scope is incomplete, inspect in this order:\n\n1. User-provided repro, failing command, or failing test.\n2. Entry point for the behavior, usually `kaku/src/main.rs`, `kaku/src/cli/`, or `kaku-gui/src/main.rs`.\n3. Owning subsystem document and target crate.\n4. Immediate cross-crate boundary used by the call path.\n5. Narrow tests, fixtures, snapshots, or scripts that reproduce the behavior.\n\nFor AI-facing behavior, inspect in this order:\n\n1. CLI and assistant configuration under `kaku/src/ai_config/`, `kaku/src/assistant_config.rs`, and `config/src/proxy.rs`.\n2. GUI AI state and transport under `kaku-gui/src/ai_*`, `kaku-gui/src/ai_chat_engine/`, and `kaku-gui/src/cli_chat/`.\n3. Overlay UI under `kaku-gui/src/overlay/ai_chat/`.\n4. Shared helpers in `crates/kaku-ai-utils/`.\n\nFor AI transport bugs around custom `base_url`, keep both proxy paths true: external API hosts should use detected system proxy settings, while loopback, private LAN, link-local, CGNAT/Tailscale-style, `.local`, `NO_PROXY`, and macOS ExceptionsList model endpoints should connect directly. Verify with the macOS system proxy enabled and a loopback or internal OpenAI-compatible smoke before saying it is fixed. Do not claim general SOCKS support unless that transport is actually implemented and verified.\n\nFor `Ctrl+letter` not working in a raw-mode TUI (the most common shape: `Ctrl+C` / `Ctrl+R` works in plain shell but not inside a TUI overlay), inspect in this order:\n\n1. AppKit menu `keyEquivalent` intercepting `keyDown` before the terminal sees it. Enable `config.debug_key_events = true`, restart the app, then `grep 'key_event.*CTRL' ~/.local/share/kaku/kaku-gui-log-<pid>.txt`. If the log shows only `key_is_down: false` and no matching `key_is_down: true`, the AppKit menu absorbed the event; do not chase termwiz or PTY.\n2. Cooked-mode tests (`cat -v` showing `^C`) do **not** rule out menu interception. Reproduce inside a raw-mode TUI before forming a hypothesis.\n3. Only after step 1 rules out menu interception, inspect termwiz encoding (`termwiz/src/input.rs`), then PTY / termios state.\n\nFor TUI display corruption after interactive CLIs re-render prompts or selection lists, first capture a minimal ANSI transcript. Add a terminal-core regression around cursor-up (`CSI n A`), full-line erase (`CSI 2K`), cursor-down (`CSI 1B`), wrapped rows, and styled prompt symbols. If the core transcript passes but the built app differs from Terminal.app, inspect GUI width, cell metrics, resize, and wrapping inputs rather than changing terminal semantics blindly.\n\n## Subsystem Guides\n\n| Subsystem | Guide | Scope |\n|---|---|---|\n| GUI | `kaku-gui/AGENTS.md` | Rendering, window lifecycle, input, mouse |\n| Mux | `mux/AGENTS.md` | Tabs, panes, domains, client/server |\n| Terminal | `term/AGENTS.md` | VT emulation, screen buffer |\n| Config | `config/AGENTS.md` | Lua loading, schema, config reload |\n| Termwiz | `termwiz/AGENTS.md` | TUI primitives and widgets |\n| Lua API | `lua-api-crates/AGENTS.md` | Rust-to-Lua bindings |\n| Crates | `crates/AGENTS.md` | Shared utility crates |\n| macOS platform | `.claude/rules/macos.md` | AppKit menu / keyEquivalent traps, menubar init timing |\n\n## Verification\n\n| Change type | Command |\n|---|---|\n| Rust compile check | `make check` |\n| Rust logic change | `make test` |\n| Formatting | `make fmt-check` |\n| Lints | `cargo clippy --locked --all-targets -p kaku -p kaku-gui -p mux -p config -- -D warnings` (`make check` does not run clippy, so a change that passes locally can still turn CI's Clippy job red) |\n| GUI or rendering change | `make app` |\n| CLI main menu input | `make app`, then `python3 scripts/test_main_menu.py` (real PTY, q/Q/Esc exit) |\n| Config release change | `./scripts/check_config_release_readiness.sh` and `./scripts/check_release_config.sh` |\n| Release note change | `./scripts/check_release_notes.sh` |\n| Release-adjacent change | `make fmt && make check && make test`, then `make app` |\n| `crates/kaku-relay` change | `cargo check --locked --manifest-path crates/kaku-relay/Cargo.toml` (it is in `workspace.exclude`, so `make check` and `make test` never see it; CI's `relay-check` job is the only gate) |\n\nFor GUI or rendering issues, read `kaku-gui/AGENTS.md` first and verify with `make app`, not only `make dev`.\n\nRestarting Kaku (2026-10-14, narrowed 2026-10-21): run `scripts/restart_kaku.sh` only when the maintainer asks for a restart in the current message (重启 Kaku). Swapping a new build into `/Applications`, or an earlier request in the session, is not a request to restart: finish the swap, then ask and wait, since the restart ends the maintainer's own windows and any agent session inside Kaku. It restarts into `/Applications/Kaku.app` the way an in-app update does (TERM, then reopen; tabs return from the periodic session snapshot) and detaches first. It stops every running Kaku GUI, a copy opened from `dist/` included, but sends TERM only to the one that owns `last_session.json` (its pid names the snapshot's `content_dir`) and SIGKILLs the rest after it exits, so a stray copy cannot overwrite the tabs when it quits. An agent session running inside Kaku ends with it, so write the reply first, tell the maintainer to reopen the session with `claude -c` (or the equivalent), and make the script the last action. Its log is `~/Library/Logs/kaku-restart.log`. The detached run is marked with a `--detached` argument, never an environment variable: `/usr/bin/open` hands the caller's environment to the app, so a variable reached every shell in the reopened Kaku and the next restart skipped detaching and died with it (`test_leaked_detach_variable_still_detaches`). An absent snapshot or empty process list must still reach the reopen step; `scripts/test_restart_kaku.py` exercises those branches and owner-only saving with fake processes, never the running app.\n\nStanding authorization (2026-10-19): after a verified change to `crates/kaku-remote/assets/web` is pushed, publish the relay page with `scripts/publish_web_page.sh` without asking. Confirm the deploy reached `t.kaku.fun` by fetching `https://t.kaku.fun/index.html` and finding a string the change added, and check the response's CSP when the change touches the script or the publish script.\n\nWhen a check needs clicks on the built app, hand the maintainer an isolated launch command (`open -na dist/Kaku.app --env HOME=<scratch home with a copy of their kaku.lua>`) and numbered taps with the expected result for each, instead of driving synthetic mouse events: the maintainer's own Kaku takes focus back and the test window may sit on another Space. Static screenshots with `screencapture -l` are still fine when the window is on screen. A shell running inside Kaku carries `KAKU_CONFIG_FILE`, `KAKU_CONFIG_DIR`, `KAKU_SESSION`, `KAKU_UNIX_SOCKET`, `KAKU_ZSH_DIR` and `XDG_CONFIG_HOME`, and `open` hands them to the app, so a test instance launched without `env -u` for each reads the maintainer's real config and session, and may connect to the relay; shell smoke tests run from such a shell need the same unsets. Remote state lives in `$TMPDIR/kaku-remote.json` and `kaku-remote-relay.json`, which a scratch HOME does not isolate: a test instance with remote on overwrites the maintainer's copies, so say that their Kaku needs a restart afterwards for `kaku remote` and `kaku doctor` to read its own state again. Look up the test window by the test instance's pid and capture nothing when that yields no id, since an empty `-l` once captured the maintainer's own window. Native fullscreen does not capture reliably this way; ask the maintainer for that screenshot instead. Four traps from the 2026-10-19 site captures: do not pass `--args start` to `open`, since AppKit hands bare arguments to `application_open_files`, the first pane runs `start ; exit` and the window closes before it is seen; drop `config.remote` from a copied `kaku.lua`, or after a config reload the test instance can attach to the maintainer's mux and `send-text` lands in their pane; `yazi` in a test pane is the shell wrapper, which, like Cmd+Shift+Y, rewrites the maintainer's `~/.config/yazi/theme.toml` from the test config's `color_scheme`, so run the Yazi binary itself with `YAZI_CONFIG_HOME` on a scratch copy; and size or move the test window with one System Events expression per action (`set size of window 1 of (first process whose unix id is <pid>)`), since a process reference kept in a variable resolves by name and reached the maintainer's `kaku-gui` window. Check the result per pid with `CGWindowListCopyWindowInfo`. For a remote capture, launch the test instance with a scratch `HOME` and `TMPDIR` as well: pairing state, the identity and `remote.lock` live under `$HOME/Library/Application Support/kaku` and the relay files under `$TMPDIR`, so only both keep the maintainer's remote untouched; end with `kaku remote off` under the same environment. In Safari, address a window you opened by its AppleScript `id`, never `front window`, which on 2026-10-19 resolved to the maintainer's Start Page window and resized it; editing the address bar opens history and Favorites over the page, so capture with the field unfocused (Tab moves focus into the page).\n\n## Current Risk Areas\n\n- In-app update replace/restart must confirm on every entry path (menu Check for Updates, menu Restart to Update, toast, CLI direct ZIP, brew cask). Do not set `KAKU_UPDATE_AUTO_CONFIRM` for exploratory menu check; only the overlay-confirmed path may auto-confirm. Sibling entry points tend to regress independently; after changing one path, sweep the matrix and keep the guards in `kaku-gui` (`user_facing_update_events_route_through_confirm`) and `kaku` (`brew_and_direct_paths_confirm_before_replace`) green. Details: `kaku-gui/AGENTS.md` and `.agents/skills/bugs/SKILL.md`.\n- AI chat and shell flows are active product surfaces. Preserve `fast_model`, proxy config, inline `#` query status, syntax highlighting, approval flow, and conversation state behavior.\n- `config_version` bumps every release; the source of truth is `assets/shell-integration/config_version.txt` and the gate is `scripts/check_release_config.sh`. Config schema changes must update bundled defaults, docs, release checks, and migration behavior together. Per-version history and the migration rule (only keys that existed in the previous released version need migration code) live in `docs/config-versions.md`. Do not hardcode the current version number in agent guides; it goes stale between releases. A config_version bump is also the only thing that regenerates an existing install's `~/.config/kaku/zsh/kaku.zsh`, so a shell-integration fix reaches stable users through the bump; a Nightly built without one needs `kaku init --update-only`, and replies pointing at that Nightly must say so.\n- GUI regressions can come from overlay resize, pane split/removal, macOS worker thread lifetime, WebGPU surface reconfigure, tab bar spacing, and alternate-screen wheel scroll behavior.\n- Startup performance depends on caching shell user vars, Lua bytecode, early appearance queries, GLSL version, and built-in fonts. Do not invalidate those caches without measurement.\n- `kaku doctor` ends every run by writing `Kaku-Diagnose.zip` (`kaku/src/diagnostics.rs`). It also collects the `kaku-hang-*.txt` stack samples that `kaku-gui/src/stall_watchdog.rs` writes when the GUI thread stays blocked past 1s, so rename that prefix in both files together. Everything in the bundle goes through its redactor, which also hides typed text in `debug_key_events` lines (printable keys, physical key codes, composed and IME text) including committed `KeyCode::Composed` text as well as `DeadKeyStatus::Composing`, while keeping Ctrl and Cmd keys readable for key reports, `assistant.toml` is never read, and large files (session content can be gigabytes, #559) are listed by size, not copied.\n- Notification actions that call back into Kaku should resolve bundled executables relative to the running app, not an assumed system path.\n- Known high-regression zones: theme/config TUI initialization (`kaku/src/ai_config/tui/`), macOS window geometry (`window/src/os/macos/window.rs`, `kaku-gui/src/termwindow/resize.rs`), and menubar initialization timing (`window/src/os/macos/menu.rs`). Changes touching these must ship with a regression test or assertion.\n- `assets/shell-integration` scripts run in the user's shell, not just at build time. Bash heredocs that generate zsh (e.g. `setup_zsh.sh` writing `kaku.zsh`) expand backticks and `$(...)` at generation time, so escape any that must reach the output literally (#450), and never put `local` outside a function (#432/#441). An expired `.zcompdump` needs `compinit && touch` after a successful full check: `compinit` can leave an unchanged dump's timestamp untouched. Keep the failure path stale and run `assets/shell-integration/tests/compinit_cache_smoke.sh`, which checks the next shell returns to `-C`. CI gates this: shellcheck (`--severity=error`, catches SC2168) over the bash scripts plus a `zsh -n` parse check of the generated `kaku.zsh` in the setup smoke.\n- User-owned configuration symlinks must survive updates, and failed writes must leave their targets unchanged. Resolve the final target and finish a sibling temporary file before replacing it; preserve target permissions. The shell `write_through` helper has four copies across the zsh/fish installers and their generated Yazi wrappers. Test all four under a real write failure, not only successful linked-file updates (#581, #582).\n\n## Release Notes\n\nTag format is `V0.x.x`. `scripts/release.sh` is the source of truth for tagged releases. The GitHub Release title comes from the first heading in `.github/RELEASE_NOTES.md`.\n\nBefore drafting release notes, read the previous formal release (`gh release view <latest-tag>`) and treat it as the format template: title `V{version} {Codename}` with a one-word codename, a centered logo header with the product tagline, `### Changelog` as numbered `**Label**: one sentence` items, a `### 更新日志` section whose Chinese items map one-to-one by number, and a `> https://github.com/tw93/Kaku` footer. After publishing, add all six positive reactions (`+1`, `laugh`, `heart`, `hooray`, `rocket`, `eyes`) to the release via `gh api` and read them back to confirm; never add `-1` or `confused`.\n\n## Pre-release Runtime Smoke\n\nCI gates fmt/check/clippy/tests but cannot see visual layout, native AppKit, render timing, or shell-in-user-env behavior, which is where most post-release reports come from. The checklist lives in one place: `.agents/skills/release/SKILL.md` «Pre-release smoke checklist», covering macOS window, tab bar, shell setup, AI chat, and render timing, with the issue numbers each item guards and a note on which slices now have automated coverage. Run it by hand in the built `dist/Kaku.app` before tagging, and add the reproduction there when a release fixes a bug outside the list.\n\nReleases that touch windowing, titlebar coloring, tab bar layout, or transparency also need the config matrix in `docs/release-checklist.md`, which enumerates the tab position / tab style / opacity / window state combinations to check by hand and names the `update_titlebar_background()` regression guard.\n\n## Documentation Maintenance\n\n- Single-crate behavior belongs in that crate's `AGENTS.md`.\n- Cross-crate behavior should update every affected subsystem guide.\n- Build, CI, release, and maintainer workflow changes belong in this root file.\n- Shared agent instructions belong in tracked docs. Personal overrides belong in ignored local files.\n- One-off review reports, scorecards, and diagnostic snapshots are evidence, not durable project docs. Extract stable rules or verification gates into `AGENTS.md`, `CLAUDE.md`, subsystem guides, scripts, or tests, then remove the transient report.\n- Do not hide user-visible behavior changes inside maintainability or cleanup patches. New UI, config fields, defaults, or workflow permissions should be split into their own change unless the maintainer explicitly approved that scope.\n\n<!-- File: config/AGENTS.md (Tokens: ~1617 | Category: root-instruction) -->\n# Config Agent Guide\n\nThe `config` crate owns config loading, Lua integration, schema behavior, proxy settings, versioned defaults, and AI configuration.\n\n## Scope\n\n`config` controls:\n- loading user and bundled configs\n- Lua execution and binding lifecycle\n- schema mapping between Lua and Rust\n- config subscriptions and reload behavior\n- AI model, fast-model, and proxy settings consumed by GUI and CLI flows\n- versioned default config and release readiness checks\n\n## Where to Look\n\n- `config/src/config.rs`: load/parse flow\n- `config/src/lib.rs`: config API and subscriptions\n- `config/src/proxy.rs`: proxy configuration used by AI and network-facing flows\n- `config/src/version.rs`: app version and target-triple accessors (`wezterm_version()`), not `config_version`; that number lives only in `assets/shell-integration/config_version.txt`, read by `kaku/src/init.rs` and the bundled `kaku.lua`\n- `assets/macos/Kaku.app/Contents/Resources/kaku.lua`: bundled fallback config\n\n## Config TUI (`kaku/src/config_tui/` and `kaku/src/tui_core/`)\n\nThe config TUI is the interactive terminal UI for editing Kaku config. It lives in the `kaku` CLI crate, not the `config` crate.\n\n- `kaku/src/config_tui/` - TUI app: `mod.rs` (form state and main loop), `ui.rs` (rendering)\n- `kaku/src/tui_core/` - shared TUI primitives, currently `theme.rs` only (`accent`, `bg`, `muted`, `panel`, `primary`, `red`, `success`, `text_fg`)\n- `tui_core::theme` is consumed by `config_tui/ui.rs`, `ai_config/tui/ui.rs`, and `tui_splash.rs`. New primitives shared by more than one TUI flow belong in `tui_core`, not copied per feature.\n\n## AI Config TUI (`kaku/src/ai_config/`)\n\nThe AI config TUI lives in the `kaku` CLI crate and shares terminal UI primitives with `config_tui`.\n\n- Keep `fast_model`, primary model, and proxy settings aligned between CLI config, GUI AI chat, and documentation.\n- Do not duplicate form widgets or debounce behavior outside `kaku/src/tui_core/`.\n- Verify AI config changes against both config parsing and the visible TUI flow.\n\n## Practical Rules\n\n- Loading priority: user config first, bundled config second.\n- Keep reload-safe behavior for startup hooks and subscriptions.\n- `color::is_light_color` re-exports the terminal core's classification so `COLORFGBG`, window chrome and DEC mode 2031 reports use the same palette policy. Keep that shared policy when changing color classification.\n- After mutating a live `TermConfig` client palette, call `pane.set_config(config)` so the terminal compares its subscribed palette snapshot and emits mode 2031 notifications. Updating only the provider bypasses the terminal's reload boundary.\n- `KAKU_CONFIG_FILE` is an output, not an input. Config loading exports it (`config/src/config.rs`) and `effective_config_file_path()` (`config/src/lib.rs`), the GUI single-instance check (`kaku-gui/src/main.rs`), `assets/shell-integration/setup_{zsh,fish}.sh`, and the bundled `kaku.lua` all read it back. Do not turn it into a config path override; the only supported override is `--config-file` (`CONFIG_FILE_OVERRIDE`).\n- Keep bundled fallback config authoritative at `assets/macos/Kaku.app/Contents/Resources/kaku.lua`.\n- Kaku Dark, Kaku Light, and the Kaku Theme alias must also resolve for standalone user configs that do not load the bundled Lua defaults. Their `src/scheme_data.rs` mirrors are checked against every bundled palette field by `builtin_kaku_scheme_mirrors_match_complete_bundled_palettes`; synchronize both when changing a Kaku palette.\n- Preserve compatibility with runtime reload callers that trigger `config::reload()` from GUI-side signals.\n- `config::reload()` can return without applying: once a config file is loaded, a watcher reloads 100ms after that file changes, and whichever reload started last wins. A test that rewrites its config file and reloads must set `automatically_reload_config = false` in that file, or a slow runner reads the old values (`ssh_pane_titles_keep_the_formatter_on_hover`). CI runs each test in its own process with nextest, so a flaky global-config test is racing a thread, not another test.\n- The current `config_version` is whatever `assets/shell-integration/config_version.txt` says; never trust a number written in a doc. Any version bump must update bundled config, release checks, docs, and migration expectations together, and add a row to `docs/config-versions.md`.\n- New config fields are user-facing behavior. Keep them out of pure cleanup/refactor patches unless the maintainer explicitly approved the product change, and update bundled defaults plus documentation in the same change when they do land.\n- Keep alternate-screen wheel scroll behavior configurable; terminal and GUI defaults must not diverge.\n- Config rewrite guards must validate every assignment the writer can replace, using the same match predicate. A later Lua override can contain user-owned settings even when the first assignment contains only a managed flag; `remote_access_checks_every_assignment_before_rewriting` covers this boundary in `kaku/src/config_tui/mod.rs`. Remote toggles must edit `effective_config_file_path()` through `kaku::utils::write_atomic`, preserving symlinks, permissions and the original bytes on a partial write failure. `kaku/tests/remote_config.rs` exercises the actual CLI with `--config-file`, a stale inherited `KAKU_CONFIG_FILE`, and a real file-size write limit.\n\n## Bundled kaku.lua Pitfalls\n\n- **200-locals hard limit.** LuaJIT caps a chunk at 200 local variables, and the top-level\n  chunk of `assets/macos/Kaku.app/Contents/Resources/kaku.lua` is already at capacity.\n  Adding one more top-level `local` makes the whole config fail to load at startup. New\n  helper functions and values must be nested inside existing functions or tables. After any\n  edit, verify the file still loads:\n\n  ```bash\n  luajit -e \"assert(loadfile('assets/macos/Kaku.app/Contents/Resources/kaku.lua'))\"\n  ```\n\n  `scripts/check_kaku_lua_loads.sh <path>` wraps the same check and doubles as a PostToolUse hook for edits to this file.\n\n- **PaneInformation is fields-only.** In title-formatting paths (`format-tab-title`,\n  `format-window-title`), the `pane` object is a `PaneInformation` userdata that exposes\n  fields only, no methods. Method-style access such as `pane:get_foreground_process_name()`\n  or capability probes like `if pane.get_xxx then` do not error; they silently evaluate to\n  nil, so the guarded branch never runs (#485 shipped dead code this way). Use the\n  documented fields, prefer the `WEZTERM_PROG` user var for the running command, and treat\n  argv-derived titles as untrusted (argv can leak environment values).\n\n## Cross-References\n\n- [`kaku-gui/AGENTS.md`](../kaku-gui/AGENTS.md) - GUI config consumers and reload signals.\n- [`lua-api-crates/AGENTS.md`](../lua-api-crates/AGENTS.md) - Lua APIs that expose config values.\n- [`mux/AGENTS.md`](../mux/AGENTS.md) - Alert propagation for config changes.\n\n<!-- File: crates/AGENTS.md (Tokens: ~9091 | Category: root-instruction) -->\n# Crates Agent Guide\n\n`crates/` contains shared utility crates used across the workspace. Inspect the target crate's `Cargo.toml` before editing because Kaku keeps some WezTerm-derived crates and adds its own helpers over time.\n\n## Scope\n\nKey groups:\n\n| Group | Crates |\n|-------|--------|\n| Terminal core | `codec`, `color-types`, `vtparse`, `wezterm-cell`, `wezterm-escape-parser`, `wezterm-surface` |\n| GUI and input | `wezterm-font`, `wezterm-input-types`, `wezterm-gui-subcommands`, `wezterm-open-url`, `wezterm-toast-notification` |\n| Mux, client, and remote | `wezterm-client`, `wezterm-mux-server-impl`, `wezterm-ssh`, `wezterm-uds`, `kaku-relay`, `kaku-remote` |\n| Platform and process | `pty`, `filedescriptor`, `procinfo`, `env-bootstrap`, `umask`, `async_ossl` |\n| Lua and config support | `luahelper`, `wezterm-dynamic` |\n| Shared utilities | `promise`, `base91`, `bidi`, `bintree`, `frecency`, `lfucache`, `rangeset`, `ratelim`, `tabout`, `wezterm-blob-leases`, `wezterm-char-props`, `wezterm-version` |\n| Kaku-specific utilities | `kaku-ai-utils`, `kaku-unicode-names` (generated Unicode name table lifted out of `kaku-gui/src`; regenerate with `ucd-generate`, do not hand-edit) |\n\n## Practical Rules\n\n- For macOS notification actions that launch Kaku commands, prefer resolving bundled executables relative to the running `kaku-gui` binary.\n- Keep `kaku-ai-utils` provider-agnostic; provider policy belongs in GUI/CLI call sites and config, not in low-level helpers.\n- When shared cache helpers affect startup, verify the GUI consumer and avoid invalidating hot-path caches unnecessarily.\n- `wezterm-escape-parser` ProgramStatus accepts whitespace around key/value pairs and skips lexically malformed optional pairs without losing valid state. Preserve decoded-text control-character and size checks; permissive pair parsing must not weaken those boundaries. Regressions live in `src/osc.rs` (`program_status_trims_pair_whitespace`, `program_status_skips_malformed_optional_pairs`, and `program_status`).\n\n## Web Terminal and Relay\n\n`kaku-remote` serves a browser terminal (`src/web.rs`, page in `assets/web/`) and `kaku-relay` lets it be reached from outside the LAN (`src/share.rs` on the Mac side). The `remote` cargo feature is on by default in `kaku` and `kaku-gui`, so Release and Nightly ship it; the feature itself stays off until the user turns it on. Invariants:\n\n- Launched in testing, free (2026-10-17): every build carries the web terminal, `remote.enabled` defaults to false, and `kaku remote` asks before turning it on, so nothing changes for a user who never runs it. The private-build machinery (`nightly.sh --private`, the `check_no_web_terminal.sh` publish guard, `release.sh` refusing the feature, `kaku remote` hidden from `--help`) was removed at launch. Remote access remains free, including the relay paid for by the maintainer; do not add paid tiers, accounts, usage metering or registration tokens.\n- Off until the user turns it on (2026-10-16): `remote.enabled` defaults to false. `kaku remote` asks before turning it on and writes `config.remote = { enabled = true }` to the user's kaku.lua through the settings screen's line writer (`config_tui::set_remote_enabled`); `kaku remote off` writes false. Neither rewrites a `config.remote` table that holds other keys (a relay of their own), which a whole-line rewrite would drop; they say to edit it instead. The bridge follows config reloads (`follow_config`): it starts when the setting turns on, without restarting Kaku, and when it turns off its runtime is dropped, which ends the relay link and every web session within a second, and the state file is removed so `kaku remote` no longer sees sharing. Verify off/on transitions in the same process; restarting the app does not test release of `remote.lock`.\n- One Kaku per data directory serves remote access (2026-10-19): `launch_when_sole` holds a `flock` on `DATA_DIR/remote.lock`; a second GUI on the same data directory (a build-folder copy, another `kaku start`) waits and takes over when the first quits. Before this, a second copy registered with the relay under the same identity and took the browser over to its own terminals. Drift checks name the frame the page drew (`seq`, `Shown::rows_after`), so they still run while frames are in flight over the relay; checking only with nothing in flight never ran while Claude Code worked.\n- No LAN access (2026-10-16 decision, replacing the 2026-10-15 opt-in): the local server listens on 127.0.0.1 only and `remote.bind` is deprecated and ignored, kept so configs that set it still load. The maintainer has no use for it at home and it was plain HTTP: `http://192.168.x.x` is not a secure context, so `crypto.subtle` is missing (measured in Helium), and the relay's https page cannot open a `ws://` LAN socket (mixed content). Do not bring LAN access back.\n- The local server answers only requests whose `Host` is its own (`127.0.0.1`, `localhost` or `[::1]` with the listener's own port, so a config reload that edits `remote.port` does not lock the page out), so a web page that rebinds its name to 127.0.0.1 cannot read same-origin responses. No route serves the token: `kaku remote` reads it from the 0600 state file. The routes for the old iOS client (`/api/config`, `/api/panes`, `/qr` with the token, `/ws/{pane_id}`, and the `ai_chat` handler in `kaku-gui/src/ai_remote.rs`) were removed on 2026-10-14 with the maintainer's approval: its relay mode had already stopped working, and `/qr` handed the token to any local process, other macOS users included. A future iOS app should speak the web terminal's protocol or wrap the page, not bring these back.\n- A session ends after `SESSION_SILENCE` (180 s) without a message from its browser, so a half-open socket from a sleeping laptop does not hold App Nap off forever.\n- One session per browser, carried either by the local WebSocket (`#t=<token>` link) or by the relay (a paired browser, see below). `run_session` sees transport-neutral input and output queues; keep socket and encryption logic out of it.\n- The page mirrors Kaku's own terminal model as ANSI frames rather than replaying the PTY stream, so terminal queries are answered once, by Kaku. Keys go through `Pane::key_down`, pastes through `send_paste`, and already encoded text through `send_text`. Local panes use the terminal's existing ordered background writer for all three; never send Remote text directly to the blocking PTY writer.\n- Kaku Assistant's chat is a GUI overlay that never reaches the mux, so the page would show and type into the shell under it. The GUI reports it with `set_ai_chat_open` (open, close, and a chat dropped on config reload); while it is open `handle_input` drops typing, pastes and clicks for that pane, an upload is saved without pasting its path, and the page dims the pane with a notice. Chat on the page itself was declined (2026-10-18): it would be a second chat UI, and using it from a browser reads as odd; do not build one.\n- Anything that reads the Lua config (for example `can_close_without_prompting`) must run on the GUI thread via `promise::spawn::spawn_into_main_thread`; calling it from the server's tokio threads panics.\n- Relay ids are the first 128 bits of SHA-256 of a secret that never leaves the Mac, so nobody else can register as that Mac. Through the relay only paired browsers get a session (`src/pairing.rs`, 2026-10-15; the old `#k=<key>` link, one AES key for every browser, is retired and its pages are told to reload). `kaku remote` writes a one-time offer (`remote-pairing.json`, 10 minutes) and prints `/{id}/#p=<secret>&m=<Mac public key>`; the page makes a non-extractable ECDSA P-256 device key and keeps it in IndexedDB once Kaku has admitted it. Each connection: Kaku sends `KCH3` and an ephemeral P-256 key in the clear, the page answers `KCB3` and its own, both derive one AES-256-GCM key per direction by HKDF-SHA256 over the ECDH secret salted with the transcript (SHA-256 of `kaku-remote v3`, the relay id, both ephemeral keys). Kaku's first sealed message signs the transcript with its long-term key (`mac_key` in `remote-share.json`), which the page checks against the key from its pairing link before sending anything, so a relay cannot pose as the Mac; the page's first sealed message signs it with its device key (`auth`), adding an HMAC of the transcript under the offer secret the first time (`pair`). Messages carry their direction label and position as associated data. Close codes: 4403 not a paired browser (the page forgets its key), 4401 pairing link expired or used, 4426 page from before pairing, 4408 no proof in time; the relay must pass each through (`browser_close`), or the page reads it as busy and retries. Pairing shows no dialog, because the Mac is often unattended when a browser is added from elsewhere: the link works once, every new device posts a macOS notification naming it, and so does a paired device connecting again after 30 minutes away (`QUIET_RETURN`; reloads and reconnects stay quiet), which is the Mac-side sign of remote use (no menubar or tab bar mark, by the maintainer's choice on 2026-10-15), and `kaku remote devices` / `kaku remote revoke <id>` list and remove devices (sessions of a removed device end within seconds). ring's ECDH takes only ephemeral keys, which is why long-term identity is a signature, not a static DH key; ring's fixed ECDSA signatures and WebCrypto's are the same r||s form.\n- Each web session holds `awake::Awake`, a latency-critical `NSProcessInfo` activity. Without it, App Nap throttles the backgrounded GUI's main thread, which turns PTY output into the pane change the session renders from, and every echo waited 100-200 ms. Measure remote input latency with the Kaku window hidden, not focused.\n- `Shown::update` turns a screen whose rows moved into `CSI n S` / `CSI n T` plus the new rows. That relies on each row starting with its own `CSI row;1H` and on the page never setting a scroll region; keep both, or the browser's copy drifts from Kaku's.\n- Clipboard writes from programs (OSC 52, `MuxNotification::AssignClipboard`) also go to browsers viewing that pane (`web::publish_clipboard`, 1 MB cap); the Mac clipboard keeps getting them. Find (`Search`) runs on the Mac over the pane's history, so it reaches past what the page shows. Files a browser drops or pastes arrive in chunks, in a bounded 512 KiB window of 16 KiB chunks, each acknowledged (`UploadAck`), so its percentage is what reached the Mac; a session that ends mid-file deletes the partial file (`Uploads` drop) and the page resends after reconnecting. Files are written to `~/Downloads/Kaku` under a sanitized, de-duplicated name (`safe_file_name`, `unused_path`, 50 MB cap), and their path is pasted into the original drop pane with `quote_dropped_files`, as a native drop would be.\n- Scrolling a plain shell (`local_scroll` in each frame: no alternate screen, no mouse grab, and no foreground `less`) never round-trips: the page fetches history rows with `History` in pages of `HISTORY_PAGE` and scrolls them in `#past`, laid exactly over the terminal with each run placed at Kaku's column. Programs that take the mouse or the alternate screen still get the scroll on the Mac; `less -X` also receives scroll keys, as in the native wheel handler, and Find still pins the Mac-side view (`View::pinned_top`).\n- The page names tabs by `tab_id` and reorders them per `window_id`; rename and move go through `TabAction::Rename` / `TabAction::Move` in `kaku-gui/src/remote_tabs.rs`, which keep the Mac's active tab. A split tab lists its `panes`; viewing one sends `Activate`, which also focuses that split on the Mac. A split tab keeps the Mac window's size (`split_claim`): the browser claims nothing for it, shows the viewed split at its own size scaled to fit the page (up to twice Kaku's font size), and a tab a browser had sized is handed back to its window once split (`TabAction::Reclaim`). Growing the tab by the split's share, as before 2026-10-19, pushed the other splits out of the Mac window. Size claims use zoom-aware `iter_panes()` geometry, so a zoomed pane is claimed like a lone one; tab navigation still lists hidden splits. Browser zoom and unzoom carry only the viewed pane's current claim and reapply it synchronously after the GUI toggles zoom; another pane's action cannot borrow that claim, and native or other-client geometry changes do not automatically reclaim the browser viewport. Frame rendering runs on another thread, so the GUI callback is not a cross-thread atomic transaction.\n- Web pane menus retain the selected `pane_id` through clipboard permission prompts and close dialogs. Splits call `Mux::split_pane` with that id rather than the deferred active-pane spawn path. Confirmed pane closes remove that exact pane, or record and remove its tab when it is last; `CloseCurrentPane { confirm: false }` is not sufficient under `AlwaysPrompt`. Middle-click tab close shares the existing browser confirmation path. Optimistic tab closes require a connected transport and a timer that restores the latest authoritative list when the close is not confirmed; the server suppresses identical lists, so expiry must not depend on another list arriving. Keep cached pane snapshots until the authoritative list drops their pane. Keep native phone selection menus intact.\n- Web URL detection stops at CJK sentence punctuation and unmatched closing wrappers, retains balanced path parentheses, and maps UTF-16 regex offsets back to terminal cells, including surrogate pairs and wide end cells. The browser regression fixture includes the reported GitHub comment link followed by `)，并以`.\n- A tab a browser sized keeps that size through relayouts the Mac does on its own (display or scale change, focus, config or theme reload, tab bar visibility): `apply_dimensions` asks `window_relayout_resizes_tab` per tab. Only the Mac's own use takes it back: typing or clicking (`reclaim_active_tab_size`) or a live window drag. Focus and a modifier pressed alone do not count (2026-10-19): switching between a fullscreen browser and Kaku with Cmd+Tab resized the tab on every switch, and Claude Code left a copy of its output behind for each size. Resizing it under the browser made Claude Code and Codex redraw for the Mac's rows and jump up in the page. The desktop view's synthetic input counts as the Mac's, so the page claims its grid again on its next input and when it leaves the desktop view (`macSized`, `takeBack`). After a reload the first frames still carry the Mac's grid until the new claim lands; the page keeps the terminal at opacity 0 (`body.sizing`, still focusable, unlike the pre-first-frame `visibility: hidden`) until a frame has the claimed grid or a split's own, for at most `REVEAL_WAIT` (600 ms). Tests that screenshot the terminal wait for `sizing` to clear.\n- Page fonts: the page draws with the families of Kaku's `config.font` in order (`font_families`; JetBrains Mono maps to the bundled `Kaku Mono`, emoji fonts stay unnamed), then its own fallbacks with `Kaku Symbols`, the bundled Symbols Nerd Font limited by `unicode-range` to its private-use spans so it is fetched only when an icon shows. Fonts the user installed are resolved on the Mac the way Kaku renders (`src/fonts.rs`: `font_dirs`, then CoreText) and listed in `font_files`, text weight only, since the page draws bold at that weight; files under `/System/`, collections and anything over 32 MB are left out. The page fetches a listed file over the download path only when the browser lacks the family, and keeps it in Cache Storage keyed by path, size and date. The WOFF2 files in `assets/web` come from `scripts/build_web_fonts.py`; rerun it when `assets/fonts` changes, and print its `unicode-range` into `index.html`.\n- The relay is public and sheds abuse itself: browser messages are capped at 512 KB and Mac messages at 4 MB, frames too (`max_frame_size`: tungstenite buffers a whole frame, 16 MB by default, before it checks the message cap); bytes queued toward one Mac (`HOST_QUEUE_BYTES`, the flooding browser is dropped) and toward one browser (`BROWSER_QUEUE_BYTES`, the stalled page is dropped) are bounded, and so are all queues together (`GLOBAL_QUEUE_BYTES`): a message count alone let one stalled page hold a gigabyte of 4 MB screens on a 256 MB machine. Every queue gives its unsent bytes back when its socket ends (`Queued`'s `Drop`), or the relay-wide budget leaks until nothing is accepted; at most 2 registrations per id (a third evicts the oldest) and 4 browser sockets per client address per host; when a host's 8 browser slots are full, the oldest socket the Mac has not admitted is let go (`make_room`; the Mac sends `{\"t\":\"ok\",\"c\":c}` once it admits a browser), so sockets that only know the id cannot lock the owner out; devices are matched on their whole public key, never the 8-hex id people see; `remote-devices.json` changes under an `flock` (`update_devices_at`) so a sign-in cannot write a revoked device back; hosts silent for 75 s and browsers for 150 s are closed. The relay serves the page and its files from its own copy, built in from `crates/kaku-remote/assets/web` (`page_file`), and asks no Mac for them; the page itself answers 404 with the offline notice while the Mac is away, which an open page relies on to tell an offline Mac from a network problem. So a relay deploy ships the page: deploy it together with Kakus that speak its protocol (pairing, `KCH3`), never ahead of them. `fly.toml` caps connections at 400 (soft 300), about 200 people online, from a release-build load test (400 Macs plus 200 streaming pages at 88 MB); raise it only with a new load test or a larger machine, and the Docker build uses `Cargo.lock` with `--locked`.\n- The page lives on its own origin, `t.kaku.fun` (2026-10-16), so serving a page that misuses the browser's keys takes a compromise of that origin, not just of the relay. `scripts/publish_web_page.sh` builds it from `crates/kaku-remote/assets/web` and the bundled JetBrains Mono, fills the `kaku-relay` meta tag (empty in the copies Kaku and the relay serve, which then talk to their own origin), writes a `vercel.json` with `/<id>` and `/<id>/` rewrites, a CSP that allows the one inline script by its SHA-256, and no caching, and deploys it with `vercel deploy --prod` to the `kaku-web` Vercel project, which holds nothing else: a path on `kaku.fun` would share an origin, and the keys, with every page of the site. The page must keep exactly one inline script and no inline event handlers, or the script refuses to publish. With `KAKU_RELAY_PAGE_ORIGIN` set, the relay 308s its page addresses there (the fragment survives), serves no page files, admits browser sockets only with that `Origin` (others are upgraded and closed at once with 4426, so a page still open on the relay's origin from before the move says to reload instead of reading as no network), and answers `/<id>/status` (200 or 404, CORS for that origin) for the page's offline notice. Kaku links the default relay's page to `t.kaku.fun`, any other relay's to the relay itself. `crates/kaku-remote/tests/static_page.cjs` runs the published page behind a server applying its `vercel.json` and checks the redirect, pairing, bookmarks with and without the slash, a foreign `Origin` refused, and no CSP refusals; poll with `page.evaluate` there, since `waitForFunction` evaluates a string the CSP blocks. Publish the page before a relay deploy that sets the origin. After each reconnect the page fetches its own address and reloads when the inline script served there differs from its own (at most once per version through sessionStorage, so a stale cache cannot loop, and never while an upload runs), so tabs left open pick up a new page or a Kaku update without the user reloading; that fetch is why the published CSP's `connect-src` includes `'self'`.\n- The Mac-screen view (2026-10-20) is the maintainer's own for now: the `desktop` cargo feature (`kaku-gui/desktop`) is off by default, so Release and Nightly never carry it. It needs a `kaku-desktop` helper, built from its separate repository. Private packages carry it in `Contents/MacOS/kaku-desktop`, preferred over the legacy copy at `config::DATA_DIR/kaku-desktop`; Kaku starts it as a child so macOS checks Kaku's own Screen Recording and Accessibility grants, and `src/desktop.rs` forwards only `desktop_*` messages in each direction, turning the helper's `desktop_frame` lines into binary messages (zero byte, u32 header length, JSON header, H.264 bytes) for a page whose `desktop_start` says `binary`; `run_session` takes them on a separate `frames` channel that both writers (`share.rs` and the local WebSocket) merge. The helper lives as long as the page's session: `desktop_stop` is forwarded, not acted on, because the helper may hold a virtual display shaped to the page's view, and making one again on every visit costs seconds and moves the Mac's windows (`the_helper_outlives_a_stop_but_not_the_session`). The page loads `assets/web/desktop.js` only when the session config says `desktop: true`, so other Kakus and the published page show nothing new. Build the helper in Release first, then build the private package with `PROFILE=release-opt CARGO_FEATURES=kaku-gui/desktop KAKU_DESKTOP_HELPER=<helper-executable> OUT_DIR=dist/private ./scripts/build.sh --app-only --native-arch`. The build checks helper architecture and signs it before the main executable. Public builds omit both the feature and helper input; a public update replaces the private package and removes its desktop capability. Preparing a private package does not authorize replacing or restarting the installed app.\n- Desktop feedback timing names two boundaries: `desktop_input_ack` confirms a successful input post, and a drawn frame with its `input_seq` confirms a capture after that post, not that the target app reacted. Match both even when independent output queues deliver the frame first; retain slow samples and report unposted, unframed and cancelled inputs separately. A new `stream_id` isolates capture generations and decoder callbacks; frame sequence numbers remain monotonic within the helper. ACK old frames with their own stream id, never the current one. Legacy helpers without these fields stay usable without fabricated timing. Desktop wheel, touch and pointer cancellation belong only to the visible desktop, never the hidden terminal. While the Mac is locked the helper sends `desktop_locked` (from `CGSSessionScreenIsLocked` plus the `com.apple.screenIsLocked` / `screenIsUnlocked` notifications, watched only between `desktop_start` and `desktop_stop`) and the page shows a notice over the picture; typing the password into the view unlocks it.\n- Direct path (2026-10-21 for the desktop view, every build and session since 2026-10-22, `src/p2p.rs` and the page's «Direct path» block): beside the relay, the page offers a WebRTC data channel (str0m with `rust-crypto` on the Mac; `apple-crypto` fails to link) once the config says `direct` (or `desktop`, from the private builds that had it first; a Kaku that says neither gets no offer); offer and answer travel inside the sealed session, so the relay cannot sit in the middle. Interactive replies (terminal frames included), the page's messages under 16 KB and desktop pictures take it, files stay on the relay. Each path has a number (`id` in `rtc_answer`). Order across the move is a fence, not a guess: each side's last relay message before it sends on the path is `rtc_ready` (Kaku sends one per lane, text and frames, the frames one as a binary header-only frame), and the receiver holds what arrives on the path until that marker comes through the relay; without it a delta picture or a key-up overtook one still on the relay. Either side ending a path tells the other by the relay (`rtc_closed` from Kaku, `rtc_close` from the page): the page's channel otherwise stayed \"open\" for about 30 s after Kaku dropped it and lost every click. After a path that had opened ends, the page sends `hello` and `attach` (what was on the path is lost, so the screen starts over), asks for a key frame, and offers again after 2, 5, 15, 30, 60 s; a path that never opened is not retried. Kaku tells the helper `desktop_keyframe` with `path_changed` on every move, which drops in-flight pictures and the old path's round trip and rate samples (kept, a 5 ms direct RTT read every 120 ms relay ack as a standing queue). Host candidates from `getifaddrs` minus loopback, link-local and 198.18/15 (TUN fake IP), plus one server-reflexive candidate per socket from a STUN binding to `stun.cloudflare.com` and `stun.l.google.com` (the page asks the same two), sent before the socket's reader starts and given `STUN_WAIT` (800 ms, DNS included) in all. Without the Mac's reflexive address a browser behind a port-restricted NAT only ever sent to the Mac's private addresses and never opened its own NAT to the Mac. Under a TUN proxy the reflexive address is the proxy's exit (measured 2026-10-22: 23.249.x on the maintainer's Mac), a candidate nobody can reach, which costs nothing. No TURN: when hole punching fails, the relay already carries the session. Unit tests keep STUN off (`STUN_SERVERS` is empty under `cfg(test)`); `public_stun` (ignored) asks the real servers. Pictures ride the reliable ordered channel on purpose, since H.264 deltas cannot survive loss.\n- The relay runs in `nrt` (Tokyo, `crates/kaku-relay/fly.toml`), the edge the Mac enters through: from `sin` every keystroke and echo crossed Tokyo to Singapore twice, 375 ms echo against 166 ms now. It keeps hosts in memory, so run one machine; two would split a Mac and its browsers across them. The relay is not where time goes (2026-10-20): a release build forwards 150 Mbps from one Mac to one browser with under a millisecond added, and the live machine sat at load 0.00 while a page streamed. Slowness through it comes from each side's path to Fly: a proxy node can cap the Mac's upload far below its own line, and some direct routes from China enter a far edge such as `fra`. Measure the path (`fly-request-id` names the edge, a timed upload gives the rate) before resizing the machine or moving the region, and make streams adapt to the link rather than assume it, as the desktop helper's rate control does.\n- Only Mac-confirmed text is rendered. Cursor visibility cannot distinguish normal input from passwords, so never predict typed characters. Pages opt into output flow control with `Hello.frame_ack`: at most 12 unacknowledged frames and a 512 KiB JSON byte waterline, including associated images. One complete frame may cross the waterline before rendering pauses; ACK after xterm parses even an ignored pane frame, coalesce later dirty output, and leave legacy hello clients unthrottled.\n- Browser selection copying follows the Mac's existing `copy_on_select` value and runs on selection release without a network hop. Preserve manual copy when disabled, reconstruct history line breaks and virtual gaps, and never queue a denied automatic copy for a later unrelated gesture. Browser clipboard permission and real mobile selection remain browser-specific.\n- `Frame.wrapped` and `History.wrapped` identify physical rows that continue into the next row. Copying, history pagination and pane snapshots preserve these flags; even a delta frame carries the whole array, and a wrap-only change must be sent. Never infer soft wrapping from a full-width row. Older hosts without flags retain hard line breaks.\n- History keeps at most 10,000 row strings but materializes only visible rows and overscan, preserving stable DOM nodes that intersect an active selection. Cached tab attachment must precede resize, and an equal-size claim must still record browser ownership.\n- Paths in the output (`~/…`, `/…`, `./…`, never inside a URL; `PATH_RE` in the page) are links: Cmd+click (Ctrl+click off the Mac) asks Kaku for that file (`Download`), and it comes back as a browser download. Kaku resolves `~` to the home folder and a relative path against the pane's working directory, sends only regular files up to 50 MB, and refuses folders and missing files by name. Chunks of 64 KB go out while less than `DOWNLOAD_WINDOW` (256 KB) is unacknowledged (`DownloadAck`), limiting queued bytes ahead of terminal interaction. The maintainer chose input and scroll responsiveness over bulk throughput on fast, high-latency links (2026-10-18); keep that trade-off explicit when changing the window. The relay never holds much of a file for one browser; the relay drops a browser past 8 MB queued. A download ends with its session, and the page says so on a dropped link. Any paired browser already has the shell, so reading a file grants nothing new.\n- Upload transfer ids bind the file and original pane for ten minutes after completion, across transport reconnects within the same app process. Retry returns the receipt without writing or pasting twice; cancellation and disconnect remove only partial files. Freeze the target once for a multi-file drop.\n- Access code (2026-10-16, the launch gate the maintainer chose over trials): with `KAKU_RELAY_ACCESS_CODE` set (a fly secret the maintainer picks), the relay registers only Macs presenting it in `x-kaku-access`, compared as SHA-256 in constant time; 401 bodies say `access code required` or `wrong access code`, and ten wrong codes from one address lock it for `ACCESS_WINDOW` (429). `kaku remote` reads the relay status, asks for the code without echo when the relay refused it, saves it to `remote-access` (0600, ASCII only since it travels in a header), and waits for the bridge, which polls that file during its backoff and retries within a second. Every recorded attempt stamps `RelayStatus::checked`, so a second wrong code is told apart from a stale one. Per-address limits sit beside it: `CONNECTS_PER_MINUTE` new sockets and `MAX_HOSTS_PER_ADDR` registered Macs per source, which `source_of` takes as the IPv4 address or the IPv6 /64 from `fly-client-ip` (r.kaku.fun has an AAAA record, and one IPv6 client can pick a new address from its /64 for every request); a large limits map is swept at most once a second; local runs have no client address and are not limited. Host admission must enforce both address and global capacity under the same lock as registration; a pre-upgrade count check alone is not a limit under concurrent handshakes. `live.cjs` runs its relay with a code and checks refusal, then acceptance after saving it.\n- No registration tokens (2026-10-17): remote access is free, so the Ed25519 registration tokens built for trials and Pro (`kaku-relay keygen` / `issue`, `KAKU_RELAY_TOKEN_KEY`, `kaku remote --token`, `x-kaku-token` on registration) were removed with their `ring` and `base64` dependencies; the access code and per-address limits are what gate the relay. Measured cost at that point: one heavy user moved 271 MB a day through the relay (about $0.33 a month of Tokyo egress), so about 100 users cost $20 to $40 a month on the 256 MB machine.\n- No pairing QR code (2026-10-17, maintainer's call): the page is made for a desktop browser for now, so `kaku remote` prints the link with how to use it and the manage commands, not a QR code; do not bring one back while phones are not a target. The link is only `#<one-time secret>` (`pairing::pairing_fragment`); `mac_auth` carries the Mac key, and while the offer is fresh a `tag`, HMAC under that secret over the transcript and the key (`pairing::mac_key_tag`), which the page checks before trusting the key. The relay never sees the secret, so it cannot vouch for a key of its own; a missing tag means the link was used or expired. The page still accepts the older `#p=<secret>&m=<key>` form. Publish the page before shipping a build that prints the bare form.\n- Remote input stays ordered within each browser, but a blocked browser must not hold the shared relay receive loop. Each browser has a bounded worker; overload or timeout closes that browser explicitly rather than dropping individual keys. GUI tab snapshots are requested asynchronously, with at most one outstanding request per session, and a later explicit Attach cancels an earlier automatic NewTab follow.\n- Both WebSocket writers prioritize interactive replies before bulk traffic, with one bulk turn after eight interactive messages to prevent starvation. File start, chunks and completion stay in the same bulk FIFO. Assign encrypted sequence numbers only after the final writer selects a message; reordering already encrypted messages breaks authentication. Keep the final bulk queue bounded to one pending message, and discard revoked sessions before encrypting their queued output. Bytes already written to TCP cannot be overtaken by later input.\n- Every relay connect is capped by `CONNECT_TIMEOUT`: a local proxy (Stash TUN) can accept TCP and never finish the handshake, which once left the link offline for hours. Bound socket writes and session inbox waits too, but allow short input bursts to wait rather than discard keys. Wrong-key close 4403 is final; idle unauthenticated sessions expire with 4408. A host replacement closes browsers independently of the old data queue, with late input discarded until the close writer finishes.\n- The default relay uses `r.kaku.fun`; its browser page lives at `t.kaku.fun/{id}/`, and the relay redirects older `/{id}/` and `/t/{id}/` page links there. Never shorten the 128-bit identity or put a pairing secret in a URL path/query. The page erases the fragment at once and saves the device only after Kaku admits it; when IndexedDB is unavailable it pairs for that tab only and says so. Loopback tokens stay tab-only in sessionStorage. Keep pairing instructions in the CLI, page messages and public docs consistent.\n- One identity per Kaku, persisted at `DATA_DIR/remote-share.json` (0600), devices at `remote-devices.json`. `kaku remote --reset` replaces the identity and forgets every device; the running bridge notices within seconds and re-registers.\n\n## Remote Verification\n\n- For an authorized relay deployment, run `fly deploy . --config crates/kaku-relay/fly.toml --dockerfile crates/kaku-relay/Dockerfile --remote-only --depot=false --ha=false --strategy immediate` from the repository root (the build context must reach the page files; the root `.dockerignore` sends only what the relay embeds, about 1 MB, never `target/`). Use `--depot=false` when Depot stalls at builder authentication. Verify the Fly tunnel with `fly doctor` first; do not add `--wg=false` to a working tunnel, since the legacy builder's HTTPS path can fail with an h2c upgrade HTTP 500. This selects a build transport, not a system proxy setting. Verify the deployed image digest and an isolated encrypted session afterward; a dashboard without new releases does not diagnose a failure before image creation. If CLI DNS checks warn while HTTPS works, compare public DNS before changing anything.\n- Run `cargo test --locked -p kaku-remote`, relay tests via its own manifest, and the workspace check/tests/clippy, which build the feature by default as CI does.\n- `node --test crates/kaku-remote/tests/web.test.cjs` uses Playwright Chromium to exercise the shipped HTML and xterm, including input privacy, reconnect, upload pacing, history selection, and viewport boundaries. Set `NODE_PATH` when Playwright is installed outside the checkout; `PLAYWRIGHT_CHROMIUM_EXECUTABLE` optionally selects an existing browser. The page draws with WebGL when it can, so most tests launch with WebGL off and read the DOM fallback, and one test checks the WebGL grid, down to its text pixels. The WebGL tests launch with `GPU_ARGS` (`--use-angle=metal --enable-gpu --ignore-gpu-blocklist`), which give headless Helium WebGL2 on the Apple GPU (2026-10-21; without them it had none and those tests skipped). CI's Chromium can still lack WebGL2 and skip them, so read the reported skips before claiming GPU coverage. Playwright's own Chromium downloads fail code signing on Darwin 27 and its headless shell only has SwiftShader, whose WebGL timings are meaningless. Never emulate `deviceScaleFactor` for WebGL checks or measurements: it changes `devicePixelRatio` but not `devicePixelContentBoxSize`, so xterm sizes its canvas at half the device size and draws nothing or a shrunken grid, a failure real browsers do not have.\n- The bar for the page (maintainer, 2026-10-16) is how typing, scrolling, copying and tab operations feel: no delay, no stutter, like the app. After changing the page, `web.rs` or the relay, run `node crates/kaku-remote/tests/feel.cjs` (same setup as `live.cjs`) and compare: on loopback, keystroke to echo p50 about 5 ms, a 54-character burst intact, no scroll frame over 25 ms, copy-on-select exact within a frame, new tab shown in about 15 ms, switch and close within one frame (16 ms). It times operations in the page from the click event to the screen change, since Playwright waits about two frames before it clicks. It explicitly disables WebGL with `--disable-webgl` and `--disable-3d-apis`; these are DOM timings, not GPU timings. Headless Helium has WebGL2 only with `GPU_ARGS` from `web.test.cjs`, so WebGL timings need those flags or a headed browser; do not read a DOM-only run as WebGL coverage.\n- `node crates/kaku-remote/tests/static_page.cjs` (same app and relay setup as `live.cjs`) also reads DOM rows and explicitly disables WebGL. `--disable-gpu` alone still exposes WebGL2 in some Chromium builds, so it does not guarantee DOM rows exist; use `--disable-webgl` and `--disable-3d-apis` for every DOM-based harness. Keep WebGL off in harnesses that open several tabs for a second reason: headless Helium on Metal stops opening new tabs once a page has drawn with WebGL (navigations abort before any request reaches the server and `newPage` hangs), which looked like a page bug for two rounds.\n- After `make app` and a debug relay build, run `node crates/kaku-remote/tests/live.cjs`. It launches only its own app, temporary HOME, and loopback relay, tests encrypted input, uploads across reconnect, frame flow control, host replacement, stable split targets and AlwaysPrompt pane closing, foreign `Host` refusal, and replayed, misplaced and old-format messages, then removes its own fixtures. Never substitute a real share link. DOM-row assertions in `live.cjs` explicitly disable WebGL; real GPU rendering remains covered by the separate browser tests, which must assert canvas output rather than DOM rows.\n\n## Cross-References\n\n- [`term/AGENTS.md`](../term/AGENTS.md) - Uses codec, vtparse, and color types.\n- [`kaku-gui/AGENTS.md`](../kaku-gui/AGENTS.md) - Uses font, input, and surface crates.\n- [`lua-api-crates/AGENTS.md`](../lua-api-crates/AGENTS.md) - Uses Lua helper and dynamic binding crates.\n\n<!-- File: kaku-gui/AGENTS.md (Tokens: ~3583 | Category: root-instruction) -->\n# Kaku GUI Agent Guide\n\n`kaku-gui` owns rendering, window lifecycle, user interaction, AI chat, and the `k` helper binary.\n\n## Scope\n\n`kaku-gui` is responsible for:\n- terminal window rendering\n- input and mouse handling\n- tab/pane UI behavior\n- startup lifecycle and app event flow\n- AI chat overlays, approval UI, inline status, syntax highlighting, and assistant transport\n- the `k` helper binary under `src/bin/k.rs`\n\n## Where to Look\n\n- `src/termwindow/mod.rs`: core window state and action dispatch\n- `src/termwindow/render/`: rendering pipeline\n- `src/termwindow/mouseevent.rs`: mouse and drag behavior\n- `src/termwindow/webgpu.rs`: surface resize and present mode handling\n- `src/frontend.rs`: app lifecycle and macOS integration events\n- `src/overlay/`: launcher and overlays\n- `src/overlay/ai_chat/`: AI chat overlay, markdown rendering, syntax highlighting, and Waza integration\n- `src/ai_chat_engine/`: AI chat engine, approval, and compaction flow\n- `src/ai_client.rs`, `src/ai_remote.rs`, `src/ai_tools/`: provider and tool integration. Keep tool-name strings stable so persisted conversations replay correctly.\n- `src/ai_tools/paths.rs`: sandbox and sensitive-path guards.\n- `src/ai_tools/fs.rs`: read, list, write, patch, mkdir, and delete tools.\n- `src/ai_tools/shell.rs`: exec, background execution, and polling.\n- `src/ai_tools/web.rs`: search, fetch, and URL reading.\n- `src/ai_tools/search.rs`: grep and symbol search.\n- `src/ai_tools/project.rs`: project summaries and file tree helpers.\n- `src/ai_tools/soul.rs`: memory and soul reads.\n- `src/ai_tools/registry.rs`: `ToolDef`, `all_tools`, and `to_api_schema`.\n- `src/cli_chat/`: command-line chat flow\n- `src/thread_util.rs`: macOS worker thread lifetime helpers\n- `src/tabbar.rs`: tab bar behavior\n\n## Practical Rules\n\n- Keep rendering decisions in rendering modules, not scattered across event code.\n- Paint-time Kaku TUI detection must reject non-Kaku foreground executables using the cached leader name before requesting the full process tree. Keep the executable and argument confirmation for `kaku ai` and `kaku config`; cover pane, window and tab-bar consumers together (#583).\n- Keep initial window dimension math in `TermWindow::new_window()` consistent with `get_os_border()` behavior.\n- In-app update must confirm before replacing the running app. User-facing paths (menu Check for Updates, menu Restart to Update, toast click) go through `check_for_updates_from_menu()` / `confirm_and_apply_update()` in `src/frontend.rs`, not straight to `restart_to_update()`. Exploratory menu check opens a tab without `KAKU_UPDATE_AUTO_CONFIRM`; only the overlay-confirmed path may auto-confirm. CLI direct and brew paths confirm in `kaku/src/update.rs`. Guards: `frontend::tests::user_facing_update_events_route_through_confirm` and `update::imp::tests::brew_and_direct_paths_confirm_before_replace`.\n- Keep `KAKU_CONFIG_CHANGED` fast path in `emit_user_var_event()` before pane ownership checks.\n- Pass fullscreen state into `TabBarState::new()` and preserve fullscreen-specific title button spacing behavior.\n- Keep AI overlay state changes cheap enough for live terminal interaction; avoid blocking render/input loops on provider calls.\n- Preserve approval prompts and inline AI status placement when touching AI chat or shell flows.\n- Chat permission summaries retain the complete target and command, with terminal controls escaped. The native review pane scrolls through all details and refuses approval when the pane is too small to read. Notifications may shorten their preview; the approval payload must not. Chat rendering, picking and mouse hit-testing share `msg_area_height`, `picker_height` and `visible_start`. Preserve a scrolled reader's position as output grows. Code Copy uses the original block, never wrapped display rows; Insert only pastes a completed, control-free single shell line into the original shell pane and never sends Enter.\n- AI HTTP transport must preserve both sides of proxy behavior: external API hosts should honor detected system proxy settings, while loopback, private LAN, link-local, CGNAT/Tailscale-style, `.local`, `NO_PROXY`, and macOS ExceptionsList model endpoints should bypass the proxy and connect directly. Do not describe this as full SOCKS support unless reqwest SOCKS support is actually enabled and smoke-tested. Exception: the `http_request` tool always connects directly to the public IP addresses resolved and validated before approval, for both HTTP and HTTPS. It must never pass that hostname to a proxy for independent resolution; users on proxy-only networks receive a connection error instead of weakening the SSRF boundary.\n- Wrap macOS worker thread spawns in the existing autorelease-pool helper so shutdown does not reintroduce use-after-free crashes.\n\n- Remote pane operations in `src/remote_tabs.rs` retain a stable `PaneId`. Do not send a remote split through `spawn_command_impl`, which resolves the active pane again after queuing. Browser-confirmed closes must bypass `AlwaysPrompt` without switching to another pane, and preserve the last-pane tab restore bookkeeping. The isolated remote live smoke covers both paths.\n\n## Known Pitfalls\n\n- During window drag, terminal move/wheel events may need suppression.\n- A tab drag belongs to one press/release gesture. A new press or motion without the left button exclusively held cancels stale state, including when AppKit swallowed the previous release. Preserve the ordinary release handler for settle animation. For unexplained tab reordering, `debug_key_events` records tab press geometry, motion and reorder decisions without terminal text (#584).\n- WebGPU surface reconfigure should happen only on meaningful state changes.\n- Finder \"open with\" behavior is handled in `frontend.rs` event flow.\n- Option+Click cursor movement fires only when: no mouse grab, no alt screen, click is on the same row as the cursor. It sends Left/Right arrow sequences proportional to the column delta; see `mouseevent.rs`.\n- Block cursor height uses `natural_cell_height` (ignoring `line_height`) so Nvim visual selections match WezTerm proportions.\n- Top border clearance for macOS integrated buttons is state-sensitive:\n  - top tab bar visible -> add small gap\n  - top tab bar hidden or bottom tab bar -> add larger clearance for traffic lights\n- Overlay geometry must update when panes are split, resized, closed, or moved.\n- Alternate-screen wheel behavior is config-controlled; do not hard-code one terminal behavior across normal and alternate screens.\n- Inline `#` AI query status belongs below the buffer, not as a toast, so terminal content remains stable.\n- `fast_model` and proxy config are part of the AI UX contract; verify both when changing provider selection or transport.\n- Per-pane overlays (AI chat) render in place of their underlying pane and live in `pane_state`, not the tab's pane tree. They have no window handle, so their output only repaints the window through `PaneOutput` -> `mux_pane_output_event` -> `is_pane_visible` -> `window.invalidate()`. `is_pane_visible` (`termwindow/mod.rs`) MUST treat an active per-pane overlay pane as visible, and the overlay's `render()` MUST `term.flush()`, or streamed output and the loading spinner freeze until an input event forces a repaint. Symptom-to-cause: if `top` refreshes on its own but the AI chat needs a click, the break is one of these two.\n- Scrollback viewport pruning has two policies in `termwindow/mod.rs`: passive pruning during output uses `normalize_viewport` and snaps to the bottom (`None`), while explicit user-driven scroll requests use `normalize_interactive_viewport` and clamp to `scrollback_top`. During a selection drag, only the pane named by `MouseCapture::TerminalPane` may pin a pruned viewport; sibling panes must retain the passive #448 behavior.\n- Tab bar rendering and the tab rename editor must read `resolved_palette.tab_bar`, which includes the selected color scheme and user overrides. Reading `config.colors.tab_bar` alone ignores scheme colors and makes Fancy tabs unreadable in light themes.\n- Tab bar sizing and hit-testing have pure seams in `src/tabbar.rs`: `tab_width_budget` (per-tab column budget that drives truncation) and `is_tab_hover` (clickable span). The truncation and position regressions (#439/#443/#445) came from layout logic with no test seam, so when you change tab sizing, truncation, or click regions, extend their unit tests instead of only eyeballing the running app. `tab_width_budget` locks the invariant that a tab is never squeezed below 1 column.\n- Launcher entries that target a pane must retain its stable `PaneId`, not a tab or topological pane index. The launcher snapshot can outlive pane exits and split-tree reindexing; resolve the `PaneId` against the live mux only after the overlay closes.\n- Session snapshots skip a pane's content while it is in the alternate screen (`f4aca502`), so a vim or htop screen never lands in restored history. Claude Code and Codex are the exception (`foreground_is_agent_cli` in `tabbar.rs`): their last screen is kept, since it is the conversation. Restore never re-runs the program.\n- A partially failed startup session restore must preserve the original session envelope and scrollback sidecars. Only mark a snapshot consumed after every saved window restores successfully; otherwise exit-time saving can overwrite the only complete copy.\n- Session sidecars compact vector-backed lines before checking visible images, since wide-character continuation cells can retain hidden Kitty placements. Strip remaining images and recompress before encoding. Menu restore guards preserve prior dirty state on failure and carry successful changes through overlapping restores; startup-only restores remain pristine. Exit and update checkpoints capture cwd/output even without structural changes, only in the session-owning process and never during an incomplete restore. SIGTERM requests main-thread event-loop shutdown, then uses the normal exit checkpoint; its listener remains registered through repeated TERM signals. Register it only after existing-instance handoff, never in CLI-only commands (#579).\n- The terminal grid's top origin has one source: `TermWindow::terminal_first_row_offset()` (OS top inset plus a shown top tab bar). Pane backgrounds, split lines and mouse row mapping take it from there so they match where text is drawn; a background that skipped the 6px inset under a top tab bar sat above its split line (#562). A source guard in `render/split.rs` fails if `pane.rs`, `split.rs` or `mouseevent.rs` computes its own. `update_text_cursor` (IME candidate rect) still uses a separate origin and sits `border.top` high; that is a known, unfixed gap.\n- Window padding (2026-10-18): with managed padding (no `config.window_padding` in the user's file) every vertical gap derives from the top padding S in `effective_vertical_padding_with_policy`: content keeps S below window chrome and S/2 from the bottom edge or a visible tab bar, top or bottom, fullscreen included; only a maximized window without a bottom bar runs content to the bottom edge. Rows rarely fill the height, and `padding_left_top` splits that leftover evenly between top and bottom (`split_row_slack`, inside the OS borders), so the gap to the bar no longer jumps between 1pt and a full row. Every consumer (pane rendering, split lines, mouse hit testing) reads `padding_left_top`; do not add a second slack policy in `resize.rs` (the old rebalance there only ever reached the resize increments). The tab bar itself stays one terminal row. Explicit user padding is used as written. Hotkey windows are also excluded from `split_row_slack`: their drawer policy owns the top inset, including an explicit zero. Cover hotkey, ordinary and fullscreen states when changing slack allocation.\n- Split geometry follows mux: `split.top = first.rows + gutter / 2` and the pane backgrounds meet at `first.rows + gutter / 2.0`. With the bundled even row gutter (`split_pane_gap = 2`) the line belongs on the top edge of row `split.top`, with an odd gutter on its middle; `split_row_center_offset` places it and `split_drag_row` measures drags against the drawn line so a grab does not jump a row. Left|right splits always have an odd column gutter. In opaque windows the outer panes' backgrounds fill the right and bottom padding (transparent windows leave those strips to the fill strips in `paint.rs`). With `inactive_pane_hsb` dimming on, the split line takes the active pane's background, so the dimming marks focus and the line only separates two dimmed neighbours; do not remove the line, three-pane layouts need it.\n- Touch mode (`termwindow/touch.rs`) is per-window UI state, not a persisted config override. It changes nothing but a drawn close button on each retro tab: tab bar height, desktop tab metrics and terminal font/line height stay the same. Long press works in every mode: `track_long_press` follows a plain left press and, on a release at least 450ms later within 24pt of the press (8pt cancelled ordinary presses over UU) (motion and the release point are both checked), replays it as a right click after the event finishes (on release, so AppKit menu tracking never sees the lift as a pick). The menu it opens draws its items at 18pt (`LONG_PRESS_MENU_FONT_PT`); NSMenu row height barely follows the font, so this is as large as a native menu gets without custom item views, while a mouse right click keeps the system size. A modal clears the pending press, so dismissing the rename editor never opens a menu, and a press that reached a tab through the bottom bar touch band aims its right click at that tab. A press over a pane that grabs the mouse is never converted unless it is on a tab, and the original press and release are handled as usual.\n- The bottom tab bar touch band (`BOTTOM_TAB_BAR_TOUCH_EXTENSION_PT` in `mouseevent.rs`) accepts only a plain left press (`press_takes_touch_extension`) and then owns that gesture's motion and release (`touch_band_press`). A middle press there is a paste into the last row and must never reach the tab bar's middle-click close. Known trade-off: a left click in the lower 12pt of the last row over a tab goes to the tab even when a TUI (tmux status line, htop) grabs the mouse.\n- Agent title (2026-10-18): while Claude Code, Codex or Kimi runs in the active pane (`foreground_agent_cli` in `src/tabbar.rs`), `src/termwindow/render/agent_title.rs` draws the agent's mark and the topic from its OSC 0/2 title at the top right: centered on the traffic-light line in the integrated-buttons strip, or 16pt below the top edge when fullscreen (the same distance as in a window, lifted only to stay above the first row), never under a top tab bar. `show_agent_title = false` (Agent Title in `kaku config`) turns it off; the default is on. Spinner glyphs and a leading agent name are stripped so the text stays still. Strip a name only at a complete boundary (end, whitespace, colon, pipe or middle dot), preserving topics such as `Codexification` and `Claude Codebase`; do not retry a shorter name after a longer partial match. The tab title is untouched on purpose: a tab is too narrow and would change width with every update. The marks in `src/termwindow/render/agent_marks.rs` are generated from Lobe Icons SVGs by `scripts/svg_to_poly.py`; regenerate rather than hand-edit. The remote page shows the same title: `tabbar.rs` publishes each tab's agent and topic through `kaku_remote::publish_agent_title` (honoring `show_agent_title`), and `AGENT_MARKS` in `crates/kaku-remote/assets/web/index.html` holds the same paths as SVG data on the 120 grid, so a regenerated mark goes into both files.\n- Tests that call `config::set_config_file_override` and `config::reload()` can race the config file watcher: `reload()` returns early when another reload starts, leaving the previous config. Write the config as few times as possible inside one test (loop the cheap cases inside one write), and do not split such a test into siblings that run in parallel.\n\n## Cross-References\n\n- [`mux/AGENTS.md`](../mux/AGENTS.md) - Tab and pane abstractions consumed by GUI.\n- [`termwiz/AGENTS.md`](../termwiz/AGENTS.md) - TUI primitives used in overlays.\n- [`config/AGENTS.md`](../config/AGENTS.md) - Config loading and reload signals.\n\n<!-- File: lua-api-crates/AGENTS.md (Tokens: ~358 | Category: root-instruction) -->\n# Lua API Crates Agent Guide\n\n`lua-api-crates/` contains Rust-to-Lua API bindings for user configuration scripts.\n\n## Scope\n\n`lua-api-crates/` exposes runtime capabilities to Lua, including:\n- mux and pane controls\n- window and spawn helpers\n- ssh and system integration helpers\n- utility modules like logging, time, and filesystem\n\n## Where to Look\n\n- `lua-api-crates/mux/`: pane/tab control APIs\n- `lua-api-crates/window-funcs/`: window-level APIs\n- `lua-api-crates/spawn-funcs/`: process spawn APIs\n- `lua-api-crates/ssh-funcs/`: SSH-related Lua APIs\n\n## Practical Rules\n\n- API behavior here is user-facing and breaking-change sensitive.\n- Be careful with user-var and event-facing APIs, since GUI hooks and config reload signals rely on those event paths.\n- Keep proxy, config reload, and user-var event semantics stable for AI and automation workflows that observe Lua-facing state.\n- `PaneInformation` (used in title-formatting events) is fields-only userdata: method-style calls and `if pane.get_xxx` probes silently no-op instead of erroring. See \"Bundled kaku.lua Pitfalls\" in [`config/AGENTS.md`](../config/AGENTS.md) before extending it or its consumers.\n\n## Cross-References\n\n- [`mux/AGENTS.md`](../mux/AGENTS.md) - Mux controls exposed to Lua.\n- [`config/AGENTS.md`](../config/AGENTS.md) - Lua config execution environment.\n- [`kaku-gui/AGENTS.md`](../kaku-gui/AGENTS.md) - Window functions and GUI integration.\n\n<!-- File: mux/AGENTS.md (Tokens: ~610 | Category: root-instruction) -->\n# Mux Agent Guide\n\nThe `mux` crate owns tab, pane, domain, and mux state management.\n\n## Scope\n\n`mux` is the central abstraction for:\n- tabs and panes\n- windows and workspaces\n- local and remote domains\n- client/server mux coordination\n\n## Where to Look\n\n- `mux/src/tab.rs`: tab tree and split behavior\n- `mux/src/pane.rs`: pane state and interfaces\n- `mux/src/window.rs`: window-level mux state\n- `mux/src/domain.rs`: domain abstractions\n- `mux/src/client.rs` and `crates/wezterm-mux-server-impl/src/` (`sessionhandler.rs`, `dispatch.rs`, `local.rs`): protocol flow\n\n## Practical Rules\n\n- Already encoded input uses `send_text`, without paste framing or newline conversion. Local panes share the terminal's existing `ThreadedWriter` with keys and paste, preserving order without blocking on a full PTY.\n- A foreground process group can outlive its leader (`seq | less -X`). When the leader has no executable path, identify a live member of that same group from the cached shell tree. A foreground-group change, exec or leader exit invalidates the leader immediately and the fallback tree must be sampled after that leader update; checking only PGID misses a producer that exits after being cached. Do not wait for the normal TTL, substitute an unrelated background job, or scan all processes on every frame. Release the tree cache guard before calling Lua hooks that can query the pane again.\n- Keep alert propagation reliable, including `Alert::SetUserVar`, because GUI reload and user-var hooks depend on this path.\n- Preserve pane and overlay invalidation signals consumed by GUI AI/chat overlays when panes split, move, close, or resize.\n\n## Known Pitfalls\n\n- **Alert propagation**: `Alert::SetUserVar` must reach the GUI side reliably; broken propagation silently breaks config reload and user-var event hooks.\n- **Pane ID stability**: Pane IDs must remain stable across mux round-trips; don't reassign or reuse IDs in ways that confuse client/server sync.\n- **Domain abstraction leakage**: GUI-specific logic (rendering, window handles) must not appear in `mux`; only mux-level state and events.\n- **Split tree correctness**: Tab split tree mutations in `tab.rs` are tricky; always verify with a pane layout sanity check after modifications.\n- **Overlay resize regressions**: GUI overlays derive geometry from pane state. Mux changes that skip resize or removal notifications can leave AI/chat overlays stale.\n\n## Cross-References\n\n- [`kaku-gui/AGENTS.md`](../kaku-gui/AGENTS.md) - GUI rendering for mux state.\n- [`lua-api-crates/AGENTS.md`](../lua-api-crates/AGENTS.md) - Lua APIs for mux control.\n- [`term/AGENTS.md`](../term/AGENTS.md) - Terminal instances owned by panes.\n\n<!-- File: term/AGENTS.md (Tokens: ~617 | Category: root-instruction) -->\n# Terminal Agent Guide\n\n## Scope\n\n`term/` contains terminal emulation core behavior, including screen state, escape sequence handling, selection, and PTY-facing terminal semantics.\n\n## Where to Look\n\n- `term/src/terminal.rs` - main terminal state and event handling\n- `term/src/terminalstate/` - detailed state transitions\n- `term/src/screen.rs` - screen buffer behavior\n- `term/src/test/` - terminal-core regression tests (`selection.rs`, `csi.rs`, `c0.rs`, `c1.rs`, `core_emulation.rs`)\n- `crates/wezterm-escape-parser/` - escape sequence parsing (CSI/OSC/ESC/APC); `term` consumes it, it does not live under `term/src/`\n- `kaku-gui/src/selection.rs` and `kaku-gui/src/termwindow/selection.rs` - selection and copy behavior (GUI side, not `term`)\n\n## Practical Rules\n\n- Already encoded input uses `send_text`, without paste framing or newline conversion. Local panes share the terminal's existing `ThreadedWriter` with keys and paste, preserving order without blocking on a full PTY.\n- Keep GUI assumptions out of terminal core.\n- Color scheme reporting uses pane-local DEC mode 2031 and DSR 996/997. Notify subscribers after effective configuration palette changes, including same-mode palette changes, and answer subsequent OSC color queries from the new palette. Reloads with an unchanged effective palette and application-issued OSC color writes do not notify; cancellation, RIS and DECSTR clear the subscription. `color::is_light_color` is shared with config/window chrome so the reported scheme stays consistent with Kaku's colors.\n- Keep normal-screen and alternate-screen behavior distinct; wheel scrolling and inline AI status must not corrupt terminal state.\n- Preserve selection and cursor invariants that GUI overlays and inline assistant status depend on.\n- Only the terminal wrap attribute proves that physical rows form one logical line. Full-width output followed by text in column zero can still be independent commands. Keep both indented and unindented hard-newline regressions, alongside soft-wrap coverage, when changing hyperlink grouping (#547).\n- For TUI re-render corruption, reduce the report to an ANSI transcript and cover cursor motion plus erasure in terminal-core tests before changing GUI code. Full-line erase of a physical row must also leave wrap state consistent, especially for styled prompts that previously wrapped.\n\n## Verification\n\n- Compile checks: run `make check`.\n- Terminal logic changes: run `make test` or the narrow cargo nextest filter for the affected behavior.\n- Cross-crate behavior: also inspect `mux/AGENTS.md` and `kaku-gui/AGENTS.md`.\n\n## Cross-References\n\n- `../mux/AGENTS.md` - owns panes and terminal instances.\n- `../kaku-gui/AGENTS.md` - renders terminal surfaces.\n- `../termwiz/AGENTS.md` - terminal UI primitives.\n\n<!-- File: termwiz/AGENTS.md (Tokens: ~214 | Category: root-instruction) -->\n# Termwiz Agent Guide\n\n## Scope\n\n`termwiz/` contains terminal UI primitives, widgets, input handling helpers, and rendering utilities used by Kaku command-line and overlay flows.\n\n## Where to Look\n\n- `termwiz/src/` - primary library code\n- `termwiz/src/widgets/` - reusable widgets\n- `termwiz/src/lineedit/` - line editing behavior\n- `termwiz/src/render/` - rendering helpers\n\n## Practical Rules\n\n- Keep primitives generic; do not leak Kaku-specific policy into reusable widgets.\n\n## Verification\n\n- Compile checks: run `make check`.\n- UI primitive logic changes: run the narrow affected tests, then `make test` if behavior is shared.\n- Overlay or GUI consumers: inspect `kaku-gui/AGENTS.md` and verify the consuming flow.\n\n## Cross-References\n\n- `../kaku-gui/AGENTS.md` - GUI and overlay consumers.\n- `../term/AGENTS.md` - terminal core behavior.\n\n<!-- File: .agents/skills/bugs/SKILL.md (Tokens: ~2089 | Category: anthropic-skill) -->\n---\nname: bugs\ndescription: \"Proactively sweep a Kaku area for latent defects and UX traps before users report them, using this repo's own fix history and multi-entry-point archetypes, and confirm every finding with a probe run this turn.\"\nwhen_to_use: \"找找有没有bug, 主动找bug, 有没有隐患, 排查隐患, 举一反三, 上线前扫一遍, 这块可靠吗, 体验问题, latent bug scan, sibling sweep after a fix, /bugs\"\n---\n\n# Bugs: Find It Before the User Does\n\n`/hunt` needs a symptom. `check` / review needs a diff. This one needs neither: it goes looking.\n\nNamed **bugs**, not bug: `/bug` is a Claude Code built-in.\n\n## The Question\n\nAcross Kaku's closed bugs and fix commits, the dominant defect is not always a crash. It is often:\n\n- a **second entry point** that skipped the safety path the first entry already had\n- a **wrong-but-plausible** geometry or selection state after a modal/resize/sleep\n- a **shell-generated script** that is fine on the author's machine and broken in a clean HOME\n- an **AI surface** (Cmd+L, `#`, `k`, toast) that diverges on auth, theme, or cwd\n\nSo the question that finds bugs here is never only \"can this crash?\" It is:\n\n> What does this code do when the user arrives through the **other** path (menu vs toast vs CLI vs shell), when the display **scale or sleep** changes mid-session, when the shell is **not** the developer's zsh, or when the AI pane cwd is **remote**?\n\n## 1. Pick the surface\n\nName the area and the depth. Whole-repo sweeps with no budget produce speculation. Start from the Hotspot Map below (or the area that just shipped a fix) and go deep.\n\n## 2. Read that area's own fix history first\n\n```bash\ngit log --oneline --grep='^fix' -i -- <path>\ngit log --pretty=format:'%h %s%n%b' --grep='^fix' -i -- <path> | head -200\ngh issue list --repo tw93/Kaku --state closed --label bug --limit 40\n```\n\nBugs recur by shape within a module. Two signals worth acting on:\n\n- A fix chain of two or more commits each \"completing\" the previous one means a sibling path is probably still open. Real chain here: toast confirm, then menu confirm, then brew confirm on the same update flow.\n- A commit body that says only one surface was fixed (toast / CLI / overlay / brew) means grep every other surface for the same action.\n\n## 3. Sweep the boundaries, in this order\n\nHighest historical yield first. Subsystem guides live in crate `AGENTS.md` files and root `AGENTS.md` risk areas.\n\n| Boundary | What to ask | Where it usually lives |\n|---|---|---|\n| Multi-entry destructive actions | Menu, toast, CLI, shell, key binding: do they all confirm the same way before quit/replace/kill? | `kaku-gui/src/frontend.rs`, `update.rs`, `overlay/confirm.rs`, `overlay/confirm_close_pane.rs`, `kaku/src/update.rs` |\n| macOS window geometry | Traffic lights vs tab bar, DPI scale, external display drag, sleep/wake, lock screen, fullscreen first frame | `window/src/os/macos/`, `kaku-gui/src/termwindow/resize.rs`, titlebar paint |\n| Tab chrome | Overflow hit targets, rename modal selection, multi-pane title width, top vs bottom bar | `kaku-gui/src/tabbar.rs`, `termwindow/tab_rename.rs` |\n| Selection / mouse state | Survives Ctrl+L, modal cancel, mouse-reporting panes, successful terminal input? | `kaku-gui/src/termwindow/selection.rs`, mouseevent |\n| Shell integration generation | `local` only inside functions; backticks in bash-heredoc that emit zsh; Starship/PATH scoped to Kaku | `assets/shell-integration/`, `kaku/src/init.rs`, `setup_zsh.sh` |\n| Session restore | Split + scrollback both sides, crash backup, stale snapshot resurrecting closed tabs | `kaku-gui/src/session_restore.rs`, paint/quad buffers |\n| AI dual surfaces | Cmd+L vs `#` vs `k` vs chat: same auth, theme, proxy, remote-cwd tool policy? | `kaku-gui/src/ai_*`, `inline_ai.rs`, `cli_chat/`, overlay |\n| Proxy / network | External API uses system proxy; loopback/private/LAN/`.local`/NO_PROXY go direct | `config/src/proxy.rs`, AI transport |\n| AppKit menu intercept | `Ctrl+letter` only `key_is_down: false` in logs? Menu `keyEquivalent` ate it | `window/src/os/macos/menu.rs`, debug_key_events |\n| Render buffer growth | Over-capacity draw after retry budget; sleep invalidates drawable | `renderstate.rs`, paint/draw paths |\n\nFor a named user symptom, switch to hunt. For a diff under review, switch to check / review.\n\n## 4. Confirm before reporting\n\nA candidate is not a finding until it has evidence produced **this turn**.\n\n- **Confirmed**: source trace that names every entry point, a failing test, or a probe (log, `make test` subset, clean-HOME shell smoke).\n- **Plausible**: complete source path, named failure mode, no run yet. Report separately with the probe that would settle it.\n- **Not a finding**: \"looks risky\" without reading the implementation. Grep the call sites or drop it.\n\nBefore flagging unwraps or panics, confirm production code: skip `#[test]`, fixtures, doctests, and build scripts.\n\n## 5. Rank by blast radius\n\n1. Silent data loss or process kill (update/restart, close all, reset) without confirm on some entry path\n2. Crash loop or unrecoverable launch (session restore, paint)\n3. Stuck UI state (selection latch, frozen frame after sleep, window behind menu bar)\n4. Shell/environment pollution outside Kaku (Starship, PATH, generated zsh)\n5. Visual chrome wrong but recoverable (tab clip, traffic-light gap, font weight)\n6. Docs / copy drift\n\n## 6. Guard what gets fixed\n\nAnything confirmed and fixed needs a regression guard when feasible:\n\n- unit/integration test that fails on unfixed code\n- shellcheck / setup smoke for shell-generation bugs\n- entry-point matrix comment or test listing menu + toast + CLI for destructive flows\n\nVerify with the matrix in root `AGENTS.md`. GUI geometry and AppKit need `make app` or a hand smoke from `.agents/skills/release/SKILL.md` «Pre-release smoke checklist».\n\n## Hard Rules\n\n- **No finding without this-turn evidence.** Speculation wastes review time.\n- **One archetype hit means sweep every sibling entry point.** Report `checked N sites, M defective, K n/a`.\n- **Do not fix while sweeping.** Collect, then fix. Interleaving loses the sweep.\n- **Report clean boundaries as clean.** Do not invent findings to justify the run.\n- **Findings outside the named area get listed, not fixed**, unless the maintainer agrees.\n- **Do not propose UI i18n** (see root `AGENTS.md`).\n\n## Hotspot Map (issue-backed)\n\nUse these as default scopes when the maintainer says \"scan for bugs\" without a path:\n\n| Area | Issue / fix examples | Probe |\n|---|---|---|\n| Update confirm matrix | toast + menu overlay, CLI/brew `confirm_apply_update`, guards in `update.rs` / `frontend.rs` tests | Trace every caller of `restart_to_update` / `spawn_update_helper` / `run_brew_upgrade`; run those unit tests |\n| Titlebar / tabs / DPI | #516, #504, #490, #483, #460 | External display + scaled resolution + tab top/bottom |\n| Window drag / menubar | #508, #456, #408, #414, #477 | Drag to menubar, lock screen, fill desktop |\n| Selection latch | #495, #487, #455 | Ctrl+L, cancel rename, mouse-reporting TUI |\n| Shell init generation | #432, #441, #450, #503, #420 | clean HOME `kaku init`, `zsh -n` generated file, non-Kaku shell |\n| Session restore paint | #514, #448, #482 | split + dual scrollback snapshot |\n| AI auth / overlay | #506, #501, #502, #418 | Cmd+L vs `#`, split resize, appearance flip |\n| Proxy split brain | private base_url + system proxy | loopback OpenAI-compatible smoke |\n\n## Output\n\n```\nArea:        [what was swept, at what depth]\nBoundaries:  [N walked, M applicable]\n\nConfirmed (severity order):\n1. [file:line] [the defect in one sentence]\n   Evidence: [probe / call-graph / failing test]\n   Blast:    [what the user sees]\n   Siblings: [N checked, M defective, K n/a]\n\nPlausible (needs a probe):\n1. [file:line] [defect] -> [the probe that would settle it]\n\nSwept clean:\n- [boundary]: [what was checked, why it holds]\n\nSibling sweep: [pattern signature] -> [N checked, M defective, K n/a]\n```\n\nSay whether anything was fixed or whether this was scan-only.\n\n<!-- File: .agents/skills/release/SKILL.md (Tokens: ~5529 | Category: anthropic-skill) -->\n---\nname: release\ndescription: \"Run the Kaku macOS release flow end-to-end: preflight, build, notarize, tag, upload to GitHub Releases, dispatch the Homebrew tap, or publish the rolling Nightly preview package. Source of truth is scripts/release.sh and scripts/nightly.sh; this skill records release prerequisites and recovery hints without embedding private machine setup.\"\nwhen_to_use: \"发布, 出版本, 打 tag, release Kaku, ship Kaku, V0.x.x, kaku release, prepare release, dry-run release, resume release, notarize, homebrew tap, nightly, Nightly 包, 更新 nightly, 预览包\"\n---\n\n# Kaku Release Runbook\n\n`scripts/release.sh` is authoritative. This skill captures the release prerequisites and the order Tang and the agent rely on each release. Exact credential locations and one-time recovery commands live in the ignored `.claude/release.local.md`, never in this tracked file.\n\nBefore any stable or Nightly release, run the non-secret checks under **Verify**. The release dry-run is the authoritative credential gate: it accepts either the rcodesign path or the notarytool fallback, including environment overrides. If it passes, do not load machine-specific recovery context. If a signing identity or every notarization path is missing or stale, then check for and read `.claude/release.local.md`; on Tang's release machine it is the required recovery layer. If the local runbook is absent, stop and ask for it instead of guessing a path or replacing credentials.\n\n## TL;DR\n\n```bash\n./scripts/release.sh --dry-run                              # preflight only\nKAKU_ASC_API_KEY_PATH=/dev/null ./scripts/release.sh          # known rcodesign compatibility issue\n./scripts/release.sh                                        # full release\n./scripts/nightly.sh                                        # rolling Nightly DMG for fixes already on main\n```\n\n`release.sh` must run on `main`, with a clean tree, in sync with `origin/main`. `nightly.sh` has no branch or clean-tree gate; it only refuses to publish when HEAD is not yet on `tw93/Kaku`. Both auto-detect the signing identity and read notarization creds from the login keychain.\n\nThe `KAKU_ASC_API_KEY_PATH=/dev/null` prefix selects notarytool before submission for a known non-network compatibility issue. Do not switch backends or disable S3 acceleration to bypass a network failure. Submission and timestamp failures allow one retry on the same route; if it fails again, stop, diagnose read-only, and wait for the maintainer to resolve the network before resuming. A stored profile name does not prove that its notarytool login exists; verify the profile or use the API-key recovery below.\n\n## Nightly preview package\n\n`scripts/nightly.sh` is authoritative for the rolling `nightly` GitHub prerelease. It builds a release-grade universal app, signs, notarizes, staples, copies `dist/Kaku.dmg` to `dist/Kaku-nightly.dmg`, and recreates the `nightly` release. It does not bump versions, create a `V*` tag, update Homebrew, or refresh any stable release surface.\n\nRules:\n\n1. Only publish Nightly from the commit users should test. The script checks that HEAD exists on `tw93/Kaku` before deleting the old nightly release.\n2. Do not tell reporters to try Nightly until `./scripts/nightly.sh` has completed and `gh release view nightly -R tw93/Kaku --json tagName,targetCommitish,publishedAt,assets,url` points at the intended commit with `Kaku-nightly.dmg`.\n3. Do not use `--upload-only` unless the existing `dist/Kaku.dmg` is already known to be a signed, notarized, stapled build from the intended commit.\n4. Keep public wording precise: \"available in the latest Nightly\" only after the check above; otherwise say \"fixed on main and will be in the next Nightly or release.\"\n\n## Credential prerequisites\n\nThe ignored `.claude/release.local.md` is the canonical map from Tang's local backup paths to these script inputs. The tracked contract is limited to what the scripts consume: either `KAKU_ASC_API_KEY_PATH` or the `kaku-asc-api-key-path` login-keychain item for rcodesign, and either `KAKU_NOTARYTOOL_PROFILE` or the `kaku-notarytool-profile` login-keychain item for the fallback. Do not delete or rewrite the local recovery map merely because its paths are machine-specific.\n\n### Preferred: rcodesign + ASC API key\n\n`rcodesign` is preferred on macOS 26+ because notarytool can SIGBUS. The scripts also accept the notarytool fallback when its prerequisites are available; use the release dry-run to check eligibility and the actual notarization result to verify success. It is not in homebrew core.\n\n```bash\ncargo install apple-codesign         # binary: rcodesign, lands in ~/.cargo/bin\n```\n\nThe private JSON supplied through the environment or keychain path must use rcodesign's native `issuer_id`, `key_id`, and `private_key` format. Do not record its location in tracked docs.\n\n### Fallback: notarytool profile\n\nCreate the notarytool profile with the maintainer's private Apple ID, team ID, and app-specific password outside the repository. Pass its profile name through `KAKU_NOTARYTOOL_PROFILE` or the expected login-keychain item; never paste the source values or the import command into a tracked runbook.\n\n### Verify\n\n```bash\nsecurity find-identity -v -p codesigning | grep 'Developer ID Application'\ngh auth status\n./scripts/release.sh --dry-run\n```\n\nWhen the dry-run reports a notarization failure, use `command -v rcodesign`, `security find-generic-password -s 'kaku-asc-api-key-path'`, and `security find-generic-password -s 'kaku-notarytool-profile'` as separate diagnostics. They are not an AND gate: one complete notarization backend is sufficient.\n\n## Pre-release content checklist\n\nBefore invoking `release.sh`, confirm in this order:\n\n1. `kaku/Cargo.toml` and `kaku-gui/Cargo.toml` versions match (e.g. `0.10.0`).\n2. `assets/shell-integration/config_version.txt` is the intended schema version, with highlight rows in the matching docs.\n3. `.github/RELEASE_NOTES.md` first heading is `# V<version> <suffix>` (uppercase V, used as the GitHub Release title).\n4. Both English `Changelog` and Chinese `更新日志` sections in `RELEASE_NOTES.md` cover the same items. Register: one full stop per item, declarative middle register, neither literary nor conversational; the item count follows the number of distinct user-visible outcomes (the previous release's count is history, not a quota), and length is measured against the previous release before publishing.\n5. Any pending fixes are committed and pushed to `origin/main`.\n\nUse `./scripts/prep_release.sh <bump>` to draft the version bump and notes when starting from an older tag. Run it with `--no-edit --no-push`: it commits with the checkout's local git identity, which can be the wrong account for the day, so fill in the notes and the `config_update_highlights.tsv` stub rows, then fold them into that unpushed commit with `git -c user.name=… -c user.email=… commit --amend --reset-author` before pushing (V0.25.0). Tang typically edits the resulting `.github/RELEASE_NOTES.md` by hand to apply the announcement-writing style (community first, 2 to 4 highlights, user-experience framing).\n\n## Pre-release smoke checklist (runtime-only hotspots)\n\nThese are the areas that produce the most post-release bug reports and that CI cannot see (visual layout, native AppKit, shell-in-user-env).\n\nHand the maintainer a release-shaped build for this pass, not `make app`: that one is a debug build, and remote-terminal latency and scrolling feel nothing like the shipped app. `PROFILE=release-opt BUILD_ARCH=native ./scripts/build.sh --app-only` (about 20 minutes, signed with the local identity) differs from the release only in notarization and the second architecture; swapping it into `/Applications` and restarting lets the maintainer test in the Kaku they use every day. Run them by hand in the built `dist/Kaku.app` before tagging. Automated coverage already exists for the testable slices: tab width-budget + hover hit-testing have unit tests in `kaku-gui/src/tabbar.rs`, and `local`-outside-function / shell syntax is gated by shellcheck + the setup_zsh smoke. The list below is what still needs a human.\n\n1. **macOS window** (#408, #414): on first launch, a single click on the title bar / top inset must not maximize the window; drag the window while it fills the desktop; flip system light/dark and confirm all windows refresh; enter and exit fullscreen cleanly.\n2. **Tab bar** (#409, #435, #439, #443, #445, #447): check both `tab_bar_at_bottom` true/false; open enough tabs to overflow a narrow window and confirm titles truncate but every tab stays clickable; rename a tab, then click another to switch and confirm no position scramble; confirm a tmux/shell status prompt renders in the bar without clipping. Exercise the status dot through idle, running, pending or attention, and cleared states on active and inactive tabs, with the tab bar at both the top and bottom; idle must not leave a dot behind.\n3. **Shell setup** (#420, #432, #441, #450): from a clean `HOME`, run `kaku init` and confirm z / syntax-highlight / autosuggestions are active in a fresh shell; run `kaku init --update-only` and confirm it exits clean; open a new shell and confirm `~/.config/kaku/zsh/kaku.zsh` sources with no error.\n4. **AI chat** (#418, #431): run `kaku chat`, quit, then run it again in the same window and confirm it reopens.\n5. **Render timing / stale drawable** (#452, #458): on the bundled WebGpu backend, sleep the Mac then wake it and confirm the window repaints instead of freezing on the old frame while keystrokes still reach the shell; connect or disconnect an external display and confirm no frozen frame or geometry jump; open a new window straight into fullscreen and resize it, confirming it fills without a stale first frame.\n6. **Hyperlinks** (#547): Cmd-hover a URL that the terminal soft-wraps and confirm both rows target the full URL. Then print a URL exactly as wide as the terminal, followed by `\\r\\n` and unindented unrelated text; the target must exclude that text. Terminal and surface tests cover the grouping; check the built app hover target too.\n\n7. **Mouse recovery** (#551): in both zsh and fish, run `printf '\\033[?1003;1006h'` and return to the prompt; moving the mouse must not insert escape sequences. Also keep a mouse-enabled TUI running and confirm mouse input still works until it exits.\n\n8. **Settings lifecycle and custom configs** (#545): start the app with a minimal standalone `--config-file`, switch between Kaku Light and Kaku Dark, and confirm Settings edits that file and the main window follows. Save and exit Settings repeatedly; no destroyed-window repaint panic should appear.\n\n9. **Tab bar pointer paths** (V0.22.0): with 2+ tabs and the bar at the bottom, tap just above a tab and confirm it switches; middle-click to paste on the last terminal row and confirm no tab closes; right-click an inactive tab, choose Close Tab several times and confirm the app stays up and closes that tab while you stay on yours.\n10. **Split panes with dimming** (#562): set `inactive_pane_hsb = { brightness = 0.7 }`, split top|bottom with the tab bar at top and at bottom, focus each pane, and confirm the dimmed background stops exactly at the line with no undimmed strip on the right or bottom edge; split into three panes and confirm two dimmed neighbours still show a faint line; drag the line and confirm it does not jump a row on grab.\n\nWhen a release fixes a bug outside this list, add the reproduction here so the next release re-checks it.\n\n## Verification commands\n\n| Scope | Command |\n|---|---|\n| Format | `make fmt-check` |\n| Compile | `make check` |\n| Tests | `make test` |\n| Release notes version match | `./scripts/check_release_notes.sh` |\n| Config schema versioning | `./scripts/check_release_config.sh` |\n| Config release readiness | `./scripts/check_config_release_readiness.sh` |\n| Full preflight | `./scripts/release.sh --dry-run` |\n\n`release.sh --dry-run` stops after preflight and does not run source checks. The normal release reuses successful CI Checks for the current HEAD; otherwise `stage:checks` runs the local checks below, as described in `AGENTS.md`. A passing dry-run alone is not source-check evidence.\n\n## Stage map\n\n`./scripts/release.sh` runs these stages in order. Each is timed and labeled `[stage:<name>]`.\n\n1. **preflight**: clean git on `main` in sync with `origin/main`, version consistency, gh auth, release notes, config, latest `Build Validation` workflow run on `main` green (an in-progress run only warns), profile, signing identity, notarization creds.\n2. **stage:checks**: reuse successful CI Checks on the current HEAD, or run `make fmt-check && make check && make test` locally. `RUN_CLIPPY=1` adds local clippy in either case. `SKIP_TESTS=1` skips local tests (avoid).\n3. **stage:build**: `./scripts/build.sh`, `PROFILE=release-opt`, `BUILD_ARCH=universal`. Output: `dist/Kaku.app`, `dist/Kaku.dmg`, `dist/kaku_for_update.zip`, `dist/kaku_for_update.zip.sha256`.\n4. **stage:notarize**: `./scripts/notarize.sh`. Tries rcodesign first; falls back to notarytool only for a non-network rcodesign failure when a notarytool profile exists.\n5. **stage:tag**: `git tag -a V<version> -m 'Release V<version>'` then `git push origin V<version>`. Idempotent: reuses an existing tag at HEAD instead of dying.\n6. **stage:upload**: `gh release create V<version>` (or `gh release edit` if it already exists) with the dmg, zip, and sha256. Title is taken from the first `# ` line of `RELEASE_NOTES.md`.\n7. **stage:homebrew-tap**: `repository_dispatch` (`kaku_release_published`) to `tw93/homebrew-tap` triggering `bump.yml`. Polls `Casks/kakuku.rb` through the GitHub contents API for the new version (default 12 attempts, 15s apart). The official `homebrew/cask` token is `kaku` and is bumped by Homebrew livecheck from the GitHub release; this stage only updates the personal tap for existing `tw93/tap/kakuku` installs.\n\n## Resume after failure\n\n`release.sh` accepts these flags to skip already-completed stages:\n\n| Flag | Skips | Use when |\n|---|---|---|\n| `--notarize-only` | build | Build succeeded; notarization failed or was interrupted. |\n| `--upload-only` | build, notarize | Notarized `dist/Kaku.app` and `Kaku.dmg` exist on disk; only need to tag + upload + tap. |\n| `--tap-only` | build, notarize, upload | GitHub release exists; only the Homebrew tap dispatch needs to rerun. |\n\nResume flags require the corresponding artifacts in `dist/` to still be present.\n\n## Common blockers\n\n- **notarytool prints `Submission ID received` and then fails**: the submission was created but the DMG upload to Apple's S3 did not finish (there is no `Successfully uploaded file` line), so that submission stays `In Progress` forever and waiting on it is pointless. Once the network is back, resubmit the same artifacts with `./scripts/release.sh --notarize-only`; a good submission came back Accepted in about four minutes (V0.25.0).\n- **Asset upload fails with `HTTP 408: Upload body timed out` or `EOF`**: the proxy cut the long upload; `gh release create` then deletes the new release and keeps the tag. Rerun `env -u HTTPS_PROXY -u HTTP_PROXY -u ALL_PROXY -u https_proxy -u http_proxy -u all_proxy ./scripts/release.sh --upload-only` (gh reads only proxy variables, not the macOS system proxy). Upload failures may be retried up to five times, 30 seconds apart (maintainer 2026-10-20); V0.26.0 went through on the first direct retry even with the route on a TUN interface. Start a full release with the same `env -u` prefix.\n- **`Local main is not synchronized with origin/main`**: push the pending commit. `release.sh` requires the tag to point at a commit that exists on origin.\n- **`rcodesign` not found**: `cargo install apple-codesign`, then verify with `which rcodesign`. Do not try `brew install rcodesign`; it is not a core formula.\n- **`No Developer ID Application certificate found`**: re-import the certificate from the maintainer-approved private backup through Keychain Access, using the password from the private runbook. Never add either location to this file.\n- **`Notarization credentials not found`**: follow the ignored private setup runbook to provision one of the environment/keychain inputs described under Credential prerequisites, then rerun `./scripts/release.sh --dry-run`. The script otherwise prompts interactively, which fails in non-interactive shells.\n- **rcodesign S3 connect timeout**: `s3 upload error: HTTP connect timeout occurred after 3.1s` identifies the failed upload connection, not a credential fault. `notarize.sh` retries once on rcodesign and stops if the network failure persists. Preserve the signed artifact, report the blocked submission, and resume only after the maintainer resolves the network; do not switch to notarytool to retry the same failure.\n- **`No Keychain password item found for profile`**: a profile-name entry alone does not establish a working login. An installed Apple `notarytool` can reuse the existing ASC `.p8` key with `--key`, `--key-id`, and `--issuer`, without creating a Keychain login. Resolve the inputs from the private runbook and keep their values out of displayed commands and logs. For S3 upload failures, apply the same one-retry network budget above and keep the signed artifact fixed. After `Accepted`, retrieve the submission log and verify its SHA-256 against the submitted DMG, then run `./scripts/notarize.sh --staple-only` and `./scripts/nightly.sh --upload-only` from the same source commit. See [Apple's notarization API workflow](https://developer.apple.com/documentation/notaryapi/submitting-software-for-notarization-over-the-web).\n- **Homebrew tap verifier timed out at 12/12**: the tap workflow commits `kakuku <version>` to `tw93/homebrew-tap` main on success; verify with `gh api repos/tw93/homebrew-tap/commits/main --jq .commit.message`. The verifier reads the cask through the GitHub contents API (`Accept: application/vnd.github.raw`), not the Fastly-cached `download_url`. Network errors retry once and stop, and other API errors stop immediately; the 12-poll budget only waits for a version change. A version-poll timeout means the expected version was not observed: open the run URL the script printed. If the tap commit is present the release is complete. Widen the window with `HOMEBREW_TAP_VERIFY_ATTEMPTS` / `HOMEBREW_TAP_VERIFY_SLEEP_SECONDS`, or rerun `./scripts/release.sh --tap-only`; `REQUIRE_HOMEBREW_TAP_UPDATE=0` only downgrades the failure to a warning.\n- **`rcodesign notarization failed` right after the log shows `\"status\": \"Accepted\"`**: Apple accepted; the step died stapling. rcodesign cannot staple an unsigned DMG, which is what `build.sh` produces, and the script used to pass `--staple` (removed 2026-09-10). Do not touch credentials. Recover with `./scripts/notarize.sh --staple-only` then `./scripts/nightly.sh --upload-only`.\n- **`codesign: A timestamp was expected but was not found`** (or `timestamp service is not available`): the timestamp request failed during signing; this message alone does not establish a certificate fault or identify the network cause. `build.sh` retries timestamp failures once before aborting. If that retry fails, stop the release, perform read-only network diagnosis, and report the blocked signing step. Resume only after the maintainer resolves the network problem; do not rerun the build or change proxy settings to retry the same failure.\n- **`codesign: code object is not signed at all, In subcomponent: .../MacOS/k`** while signing `kaku-gui`: `kaku-gui` is the bundle's main executable and codesign validates its siblings as nested code, so every other executable in `Contents/MacOS` must already be signed. `build.sh` signs `kaku` and `k` before `kaku-gui` since 2026-09-10; if a new helper binary is added to the bundle, add it to that loop before `kaku-gui`.\n- **`stapler` fails with CloudKit timeout, exit code 68**: the log reads `The staple and validate action failed! Error 68` right after `Notarization accepted`. Notarization already succeeded; only ticket retrieval from `api.apple-cloudkit.com` timed out. Do not re-notarize. One `./scripts/notarize.sh --staple-only` retry is allowed; if the network failure repeats, stop for read-only diagnosis and wait for the maintainer to resolve it. Only after stapling succeeds, resume with `./scripts/release.sh --upload-only`. The staple-only path regenerates `kaku_for_update.zip` and its sha256 on purpose: attaching the ticket changes the app, so the in-app update archive must be rebuilt after stapling, never before.\n- **Tag already exists on origin at a different SHA**: do not force-push tags. Pick the next patch number, bump versions, and start over.\n\n## Environment variable overrides\n\n| Variable | Default | Purpose |\n|---|---|---|\n| `KAKU_SIGNING_IDENTITY` | auto-detect | Override the Developer ID Application identity. |\n| `KAKU_ASC_API_KEY_PATH` | keychain item `kaku-asc-api-key-path` | Path to rcodesign ASC API key JSON. |\n| `KAKU_NOTARYTOOL_PROFILE` | keychain item `kaku-notarytool-profile` | notarytool keychain profile name. |\n| `HOMEBREW_TAP_TOKEN` | `gh auth token` | GitHub token for tap dispatch. |\n| `REQUIRE_HOMEBREW_TAP_UPDATE` | `1` | Set to `0` to allow release to succeed when tap dispatch fails. |\n| `HOMEBREW_TAP_VERIFY_ATTEMPTS` | `12` | Polls of the tap cask before the verifier gives up. |\n| `HOMEBREW_TAP_VERIFY_SLEEP_SECONDS` | `15` | Seconds between tap cask polls. |\n| `RUN_CLIPPY` | `0` | Set to `1` to run `cargo clippy` during stage:checks. |\n| `SKIP_TESTS` | `0` | Set to `1` to skip `make test` during stage:checks. |\n| `OUT_DIR` | `<repo>/dist` | Override artifact output directory. |\n| `PROFILE` | `release-opt` | Cargo profile. Only `release` and `release-opt` are accepted. |\n| `BUILD_ARCH` | `universal` | Passed through to `build.sh`. |\n\n## After release\n\n- GitHub Release URL: `https://github.com/tw93/Kaku/releases/tag/V<version>`.\n- Official Homebrew cask users get the new version after Homebrew livecheck/bump; verify with `brew update && brew info --cask kaku`. Personal-tap users get it once the tap workflow finishes; verify with `brew info --cask tw93/tap/kakuku`.\n- In-app updates (`kaku-gui/src/update.rs`, Kaku's own updater, not Sparkle) download `kaku_for_update.zip` and its `.sha256` from the latest GitHub Release.\n- Add the six positive reactions to the new release; this is part of shipping. Skip `-1` and `confused`.\n\n  ```bash\n  id=$(gh api repos/tw93/Kaku/releases/tags/V<version> --jq .id)\n  for c in +1 laugh heart hooray rocket eyes; do\n      gh api -X POST \"repos/tw93/Kaku/releases/$id/reactions\" -f content=\"$c\"\n  done\n  ```\n\nFor the announcement post (X / WeChat): community first, 2-4 highlights, user-experience framing, one opinionated sentence. The release notes file is a different artifact; do not paste it as the announcement.\n\n<!-- File: .claude/rules/macos.md (Tokens: ~3043 | Category: anthropic-skill) -->\n# Kaku macOS Platform Rules\n\n> macOS 26 上 AppKit 自动注入是 PAC-fault / 崩溃的高频来源。改 menubar / window handling 前必读本文。\n\n## 两套像素坐标空间，禁止混算\n\n窗口/屏幕几何存在两套空间，只在\"窗口所在屏 scale == 主屏 scale\"时数值兼容，混用的 bug 只在外接屏暴露（#456 拖动跳变）：\n\n- **Canonical ScreenPoint 空间**：`cartesian_to_screen_point` 定义（全局 point × `screens[0]` 主屏 scale、Y 翻转）。`set_window_position` / `window_position` / `MouseEvent::screen_coords` / `MouseEvent::window_origin` 都在这里。\n- **窗口 backing 空间**：`MouseEvent::coords`、`dimensions.pixel_*`、UI item 布局坐标，用窗口自己屏幕的 backingScaleFactor。\n\n规则：跨空间换算一律在 `window/src/os/macos` 层做；GUI 层需要窗口原点用 `MouseEvent::window_origin`，不要用 `screen_coords - coords` 推算；居中走 `WindowOps::center()`（原生、单屏 point 空间）。`ScreenInfo.rect`（`screens()` / `resolve_geometry` / `--position` / Lua `screens()`）已统一到 canonical 空间（`nsscreen_to_screen_info` 走 `cartesian_to_screen_point`），`Window::new_window`（`window/src/os/macos/window.rs`）不再对 resolve 出的 x/y 除 scale（除一次就是 upstream 继承的 Retina 落点折半 bug，size 的除法保留，那是 px 转 pt 给 NSWindow 用的）。\n\n## 默认渲染后端是 WebGpu，CGL-only 的 wake 修复护不住它\n\nbundled `kaku.lua` 设了 `front_end = 'WebGpu'`（Metal via wgpu）。`window/src/os/macos` 里的睡眠/唤醒防护（`SYSTEM_SLEEPING` flush 闸门、`BackendImpl::update`、display-change present defer）全部只作用于 CGL 路径，对默认用户是 no-op。给 sleep/wake/display-reconfig 加修复时必须同时考虑 WebGpu 路径（#458）。\n\nWebGpu surface 失效的恢复入口在 `TermWindow::do_paint_webgpu`：休眠唤醒会让 Metal drawable 失效但窗口尺寸不变，`WebGpuState::resize` 对相同尺寸是故意 no-op（热路径），所以恢复必须走 `reconfigure_surface()` 强制 `surface.configure`，不能用 `resize(dims)` 代替。wgpu Metal 后端把 nil `nextDrawable` 映射成 `SurfaceError::Timeout`，所以 Timeout 连续出现同样要触发重配，不能永远静默跳帧。症状签名：键盘输入照常进 PTY、画面停在旧帧，说明是渲染管线死了而不是主线程卡死。\n\n## AppKit Menu 自动注入是地雷\n\nNSApplication 默认会向 Window menu 和 Help menu 注入自己的项（Tile / Move & Resize / Bring All to Front / Help Search），并把 NSWindowRepresentingMenuItem 挂在 Windows menu 下做 dangling reference 。在 macOS 26 上，这些注入项可能 PAC-fault：\n\n- `-[NSMenu _performKeyEquivalentWithDelegate:]` 在 Ctrl+Cmd+F 路由时崩\n- Windows menu 关闭窗口后 NSWindowRepresentingMenuItem dangling\n- Help Search field 触发 menu 重新计算时崩\n\n## 必须执行的兜底\n\n- 不要调 `setWindowsMenu:` 让 AppKit 接管 Windows menu。Kaku 有自己的 tab/window 切换器。\n- 不要调 `setHelpMenu:` 让 AppKit 注入 Help Search。Kaku 没有 help book。\n- 窗口创建时调 `setExcludedFromWindowsMenu:` 让 AppKit 看不见我们的 NSWindow，避免它把窗口加到 Windows menu。\n\n参考历史 commit：\n- `1ec3823` - 第一次排查 NSWindowRepresentingMenuItem dangling\n- `4b15bab` - 进一步 stop AppKit from injecting into Window/Help menus\n- `fc2dfa9` - redirect AppKit Spotlight for Help to an orphan menu\n- `6ba7381` - restore synchronous menubar init to prevent key event loss\n\n## Menubar 初始化时序\n\nMenubar **必须同步初始化**。延后到 async block 会丢早期按键事件（Cmd+Q 在启动后立刻按下会被吞）。见 `6ba7381`。\n\n## Key Equivalent\n\n新增 KeyAssignment 前确认走 Kaku 自己的按键分发路径，不要走 AppKit menu 的 `performKeyEquivalent`。后者会经过被 PAC-fault 的注入项。\n\n### menu keyEquivalent 会拦 keyDown，吞掉 raw-mode TUI 的 Ctrl+letter\n\nmacOS 26 上 NSMenu 的 keyEquivalent modifier 匹配并不严格相等。给 menu item 装 `Ctrl+letter` 形式的快捷键，即使带额外 modifier（如 `NSFunctionKeyMask` 想做 `Fn+Ctrl+letter`），仍会拦截 plain `Ctrl+letter` 的 keyDown。后果：\n\n- AppKit 把 keyDown 路由给 menu item action，NSWindow 收不到。\n- keyUp 不走 `performKeyEquivalent:`，所以 keyUp 照常进 Kaku。\n- `termwiz/src/input.rs` 的 `encode` 对 `is_down=false` 直接返回空字符串，PTY 上一个字节都不会到。\n- 普通 shell `cat -v` 测试看到 `^C` 不代表 raw-mode 也工作；启动时机和 cooked vs raw 都会让 menu 拦截表现不一致。**必须**在 raw-mode TUI (claude / codex / vim / htop) 里实测。\n\n兜底：任何\"模仿系统快捷键\"的 menu 项宁可 `keys: vec![]` 不设快捷键，让用户从菜单点。`kaku-gui/src/commands.rs` 里的 keyEquivalent 装配逻辑必须统一从按键候选本身取 modifiers，不要为个别 menu 项走强制 modifier 的特例化路径（历史上的特例化路径已删，不要加回）。\n\n参考历史：`8aba4475` feat(window): add CenterWindow menu without macOS shortcut（Window > Center 刻意不装 keyEquivalent，因为 macOS 26 会把 `Fn+Ctrl+C` 当 plain Ctrl+C 拦了，导致 raw-mode 下 Ctrl+C 不退 claude/codex）。\n\n### 排查 keyDown 丢失\n\n`config.debug_key_events = true` 重启 Kaku，复现按键，看 `~/.local/share/kaku/kaku-gui-log-<pid>.txt`：\n\n- grep `key_event.*CTRL`。\n- 如果只有 `key_is_down: false`、没有 `key_is_down: true`，就是 menu keyEquivalent 拦了 keyDown。\n- 这种情况下不要去查 termwiz / PTY / termios，先找 `set_key_equiv_modifier_mask` 装配点。\n\n## Cocoa 对象所有权：StrongPtr::new 只接 alloc / new / copy\n\n`StrongPtr::new` 接管的是 +1 引用，只能包 `alloc`+`init`、`new`、`copy` 拿到的对象。`+[NSMenuItem separatorItem]` 这类类方法返回的是 autorelease 的 +0 对象，必须用 `StrongPtr::retain`，否则 drop 时多 release 一次。V0.22.0 前 `MenuItem::new_separator()` 就是这么写的：主菜单栏在启动时构建，那时还没有 autorelease pool（从代码推断，未实测），多出的那次 release 被泄漏抵消，所以一直没暴露；第一个在事件处理里构建、带分隔线的菜单（右键 tab 菜单）一弹出就崩（`7b6b6ce2`，带回归测试）。\n\n- 崩溃签名：主线程 `objc_release > AutoreleasePoolPage::releaseUntil > objc_autoreleasePoolPop > -[NSApplication run]`，紧跟在一次原生菜单动作之后。看到它先审 `StrongPtr::new` 包的是不是非 alloc/new/copy 来的对象，再怀疑 AppKit。\n- 拿不准某个 API 返回 +0 还是 +1，用 `clang -fno-objc-arc` 写个十行的 MRC 程序在 `@autoreleasepool` 里打印 `retainCount`，实测，不靠记忆。\n- 新增在事件处理中构建的 `NSMenu`（右键菜单等），必须在 `make app` 的包里真的右键点一次菜单项再下结论，单元测试看不到 autorelease pool 的时序。\n- 配置重载后菜单栏在 macOS 上也会重建（#570）：`frontend.rs` 的 reload 回调在 `spawn_into_main_thread` 任务里调 `recreate_menubar`，不在可能持有 config 锁的回调里直接调。原先挡住它的 TODO 疑似就是上面这个分隔线多释放，修复后放开。外观切换也会触发 `config::reload()`，所以这条重建路径在每次深浅色切换时都会走。`recreate_menubar` 给 Kaku 加的每一项打 `KAKU_MENU_ITEM_TAG`，重建时只删带这个 tag 的项、保留顶层菜单对象，再从每个菜单的开头按 rank 插入新项。不要改成 `remove_all_items` 清空主菜单或换新的 NSMenu：AppKit 只在启动时往 Edit 菜单注入 AutoFill、Start Dictation、Emoji & Symbols，菜单对象一换这些项就再也回不来，再调一次 `setMainMenu:` 也不行（MRC 小程序实测）。也不要换回旧的 mark-sweep：它只扫 `kakuPerformKeyAssignment:` 项，Kaku 菜单的固定项只在子菜单新建时创建、分隔线每次都追加，复用旧菜单会丢固定项、分隔线越积越多、新项排到末尾。`config_reload_rebuilds_menubar_on_macos_in_place` 守这几点。随 mux 状态变化的命令（Attach/Detach Domain、非 local 的 New Tab (Domain X)、Switch to workspace）只进命令面板不进菜单栏：菜单栏只在配置重载时重建，启动时 domain 还没注册，放进去会在一次深浅色切换后突然多出 `~/.ssh/config` 每个 Host 两条 Attach 项，之后又随 attach 状态过时，`mux_state_commands_stay_out_of_the_menubar` 守这一点。\n\n## 调试方式\n\n- 怀疑 menu 注入或菜单对象相关崩溃时：读 `~/Library/Logs/DiagnosticReports/kaku-gui-*.ips` 里最近的 crash report。\n- 看 `NSMenu _performKeyEquivalentWithDelegate:` 栈帧是 AppKit 注入项的标志。\n- 主要复现环境是一台 macOS 26 实机。\n</yakaai_skills>"}