Skip to content

plan: change leader prefix from Ctrl+Shift+Space to Ctrl+I #761

Description

@btipling

Plan header

Field Value
Status IMPLEMENTED
Date 2026-08-22
Type single
Parent N/A
Source issue #755 — harness: change leader prefix from Ctrl+Shift+Space to Ctrl+I
Branch plan/leader-ctrl-i
Layers harness (Wasm)
Reusability impact none — a keymap preference in the Wasm harness; no host/env coupling
Production mutate? no — Wasm keymap only
Cloud ops path N/A — no Production mutate
Living docs docs/harness-limits.md (Keyboard & focus row); AGENTS / feature-divide N/A (they reference "leader" generically, no chord string)

Summary

Change the harness leader prefix from Ctrl+Shift+Space to Ctrl+I (Control on
both platforms — never Cmd+I), tmux/zellij-style. The leader is still a prefix, not a
held chord: press Ctrl+I, let go, then ? / Escape / t (the existing thinking
default-collapse chord, #742) / any future leader command within LEADER_WINDOW_MS
(800). Ctrl+Shift+Space stops arming; Cmd+I and Ctrl+Shift+I (Inspect) are left
to the browser, never handled. Pure Wasm change — no bridge export, no protocol bump, no
new caps.

Goals

# Goal Success signal
1 Leader prefix is Ctrl+I (Control, both platforms — not Cmd+I) match(.i, .down, .{ .control = true }, …)Action.leader. In-canvas help + docs say Ctrl+I
2 Ctrl+Shift+Space no longer arms space + ctrl_shift → .none in the normal (non-leader) context
3 Cmd+I does not arm (Mac italic / Get Info / not our prefix) command + i → not Action.leader; not marked handled (.none)
4 Ctrl+Shift+I stays with the browser (Inspect) reserved deny-list entry .i+.ctrl_shiftOutcome.browser; never marked handled, even while a leader is pending
5 IME / Spotlight still safe Ctrl+Space / Cmd+Space still do not arm (existing test unchanged)
6 Leader commands unchanged ?, Escape, t, re-arm on the new prefix all behave identically

Non-goals / out of scope

  • Do not change LEADER_WINDOW_MS (800) or KEYMAP_MAX (64)
  • Do not add a DOM window keydown / host keyboard UI
  • Do not steal Ctrl+Shift+I (devtools) or Ctrl+B (leave to tmux)
  • Do not make Cmd+I the Mac leader — Control on both platforms, same as today's leader
  • Do not add or remove a leader command — the existing thinking-toggle t chord
    (row thinking_default_toggle, keymap.zig line 294, plan plan: thinking default-collapsed toggle via keyboard shortcut #742.key=.t,
    when_true.leader_pending, wired to the toggleThinkingDefault seam) is kept
    and only its on-screen chord string re-worded to the new prefix; no new leader
    command ships
  • No new caps, no bridge protocol change, no build.zig export_symlink_names change (no new exports)
  • Forbidden wiring: dual DOM chat · secrets in Wasm · laptop-only Production ops

Architectural decisions

Decision Options considered Choice Why
Modifier prereq for the leader reuse ctrl_or_cmd / reuse ctrl_shift / new ModPrereq.control new ModPrereq.control (control AND NOT command/shift/alt) ctrl_or_cmd would arm on Cmd+I (violates goal 3); ctrl_shift requires Shift (wrong). A control-only prereq is the only option that makes Cmd+I, Ctrl+Shift+I, and Cmd+Space all non-arming
.i in RESERVED keep .i+.ctrl_or_cmd / remove it remove .i+.ctrl_or_cmd The reserved deny-list runs before table match, so keeping it would make Ctrl+I Outcome.browser and the leader could never fire. Removal is required for the prefix to work
Explicit Inspect deny leave Ctrl+Shift+I to fall-through .none / add .i+.ctrl_shift add .i+.ctrl_shift to RESERVED While a leader is pending, a fall-through Ctrl+Shift+I would otherwise be .swallow_leader (marked handled + disarms). Reserving it keeps Outcome.browser (never handled) with no behavior difference outside the window — matches goal 4 exactly
Leader row + re-arm keep space+ctrl_shift / i+.control .key = .i, .prereq = .control in the leader row and the match leader_pending re-arm branch Table and re-arm must move together so the prefix and its re-arm stay coherent
Protocol / bridge bump / none no change Keymap is a pure Wasm concern; no new inv_* export, no host change. No build.zig export_symlink_names update needed

Layer placement

Concern Layer Path(s) Rationale
Modifier prereq + leader row + reserve list + re-arm harness (Wasm) native/harness/src/keymap.zig Pure, host-unit-tested; ownership per module header (#741)
Chord glyph + help copy harness (Wasm) native/harness/src/ui/help_overlay.zig Overlay IS the table; chord strings live here
Dispatcher comment harness (Wasm) native/harness/src/ui/keymap_dispatch.zig Doc comment only (arming copy); one per-frame walk unchanged
Keyboard & focus row docs docs/harness-limits.md Durable operator behavior table
DOM shell none No host key handling, no window keydown (per AGENTS/feature-divide contract)

Current baseline (live code)

Claim Path / symbol Notes
Leader chord native/harness/src/keymap.zig KEY_TABLE id leader, lines 266–274 .key = .space, .prereq = .ctrl_shift, .help = "Leader: Ctrl+Shift+Space"
ModPrereq has no control-only keymap.zig ModPrereq, lines 71–82 today only none, ctrl_or_cmd, ctrl_shift, shift, alt; ctrl_or_cmd requires !mods.shift (so Ctrl+Shift+I already passes through today)
.i reserved keymap.zig RESERVED, line 323 { .key = .i, .prereq = .ctrl_or_cmd } — must be removed for Ctrl+I to arm
Re-arm branch keymap.zig match, lines 374–419 leader_pending block; space+ctrl_shift re-arms (lines 400–402)
Thinking-toggle chord keymap.zig KEY_TABLE id thinking_default_toggle, lines 293–301 .key = .t, .prereq = .none, when_true.leader_pending — ships in the live table (#742); kept, string only
Dispatch key map keymap.zig Key.i (line 48) + ui/keymap_dispatch.zig fromDvui (line 79) .i => .i already mapped; Mods already has control
Overlay chord ui/help_overlay.zig rowChord (lines 20–35) .leader => "Ctrl+Shift+Space", .help_toggle_leader => "Leader Space, then ?", .thinking_default_toggle => "Leader Space, then t"
Docs row docs/harness-limits.md line 63 Ctrl+Shift+Space
AGENTS / feature-divide AGENTS.md line 299, docs/feature-divide.md line 49 reference "leader / keymap table" generically — no chord string to change

Design

  1. keymap.zig — add ModPrereq.control; extend modsMatch with
    .control => mods.control and !mods.command and !mods.shift and !mods.alt.
  2. Leader row (lines 266–274): .key = .i, .prereq = .control,
    .help = "Leader: Ctrl+I".
  3. Re-arm branch (lines 400–402): if (key == .i and modsMatch(mods, .control))
    Action.leader (re-arm), replacing space+ctrl_shift.
  4. RESERVED (line 323): remove { .key = .i, .prereq = .ctrl_or_cmd };
    add { .key = .i, .prereq = .ctrl_shift } so Ctrl+Shift+I (Inspect) is an
    explicit Outcome.browser — never handled, including during the leader window.
  5. Comments in keymap.zig (lines 17, 106, 308, 366) and
    ui/keymap_dispatch.zig (line 46): point at Ctrl+I, not Ctrl+Shift+Space.
  6. help_overlay.zig rowChord: .leader => "Ctrl+I",
    .help_toggle_leader => "Leader I, then ?",
    .thinking_default_toggle => "Leader I, then t" (keeps the plan: thinking default-collapsed toggle via keyboard shortcut #742 chord,
    re-worded) (and leader_cancel stays).
  7. docs/harness-limits.md line 63: **Ctrl+I** row — "Arm the leader
    prefix (800 ms). Within the window press ? to toggle help; t
    toggles thinking default-collapsed; Escape cancels; an unmatched key
    swallows (never lands in the prompt); a reserved browser chord still yields
    to the browser."
  8. Update the existing "leader arms on Ctrl+Shift+Space" test to Ctrl+I; keep
    the IME/Spotlight (Ctrl+Space / Cmd+Space) test; add/adjust the cases below.

Edge cases & invariants

  • Cmd+I (no ctrl): no row matches .control (fails !mods.command), no
    longer reserved → Outcome.none, never handled → browser/app keeps Mac italic.
  • Ctrl+Shift+I: reserved .browser; outside the window it passes through
    (same as today); inside a leader window it is .browser (not .swallow_leader),
    so Inspect is never swallowed — matches goal 4.
  • Ctrl+Shift+Space (goal 2): removed from both the leader row and the re-arm
    branch; no row matches → Outcome.none in the normal context.
  • Leader + Ctrl+I re-arms (identical to today's Space+ctrl_shift re-arm).
  • Leader + t (thinking-toggle, plan: thinking default-collapsed toggle via keyboard shortcut #742) behavior unchanged — only the on-screen
    chord string changes to Leader I, then t.
  • Bare i (no modifier) still types the letter in the composer — .control
    is strict, .none outside the window.
  • No cap, budget, or frame-loop change: one extra ModPrereq switch arm and
    a same-shaped re-arm branch; LEADER_WINDOW_MS/KEYMAP_MAX untouched.
  • No Wasm artifact ABI change: no new inv_* export, so build.zig
    export_symlink_names and the protocol row are untouched.

Cloud ops path

N/A — no Production mutate. Wasm keymap only; no environment, data, GHA, or
deploy cutover. The rebuild of public/harness/harness.wasm is the normal
harness artifact path (build-harness GHA is the release-wasm gate; the agent
rebuilds locally for the debug/wasm gates).

Living docs plan

Surface Change Notes
docs/harness-limits.md Keyboard & focus table — replace the Ctrl+Shift+Space row with Ctrl+I and the matching leader-window copy (incl. keeping the t thinking row) timeless; no phase/issue artifacts
AGENTS.md N/A — line 299 references the keymap table/leader generically, no chord string verified
docs/feature-divide.md N/A — line 49 references the keymap/leader generically, no chord string verified
README.md N/A — no keyboard docs
SECURITY.md N/A — no secrets/trust-boundary change
.env.example N/A — no env change

Implementation order

  1. native/harness/src/keymap.zig — add ModPrereq.control + modsMatch arm
  2. Same file — leader row → .i+.control, help string, re-arm branch, and the
    RESERVED .i swap (remove ctrl_or_cmd, add ctrl_shift); update comments
  3. native/harness/src/ui/help_overlay.zigrowChord strings (all three:
    .leader, .help_toggle_leader, .thinking_default_toggle)
  4. native/harness/src/ui/keymap_dispatch.zig — doc comment copy only
  5. docs/harness-limits.md — Keyboard & focus row
  6. Update existing leader test + add the new cases in keymap.zig
  7. Rebuild the Debug Wasm artifact and run the Zig gates (below)

Testing

# Case Layer Type Command / method
1 Ctrl+I → Action.leader harness unit zig build test-rich
2 Cmd+I → not leader, .none harness unit zig build test-rich
3 Ctrl+Shift+I → Outcome.browser (never handled), incl. while leader pending harness unit zig build test-rich
4 Ctrl+Shift+Space → .none (no longer arms) harness unit zig build test-rich
5 Ctrl+Space / Cmd+Space → .none (IME / Spotlight — existing test unchanged) harness unit zig build test-rich
6 leader pending + Ctrl+I → Action.leader (re-arm) harness unit zig build test-rich
7 leader + ?help_toggle_leader; leader + Escape → leader_cancel (with help open → help_close); leader + unmatched → swallow_leader (unchanged) harness unit zig build test-rich
8 Ctrl+I no longer Outcome.browser (regression on RESERVED .i removal) harness unit zig build test-rich
9 bare i (no modifier) → .none (letter still types) harness unit zig build test-rich
10 ModPrereq.control strictness: control only, Cmd/Alt/Shift each excluded harness unit zig build test-rich
11 leader + tthinking_default_toggle behavior unchanged (existing #742 tests stay green) harness unit zig build test-rich
12 help overlay paints Ctrl+I / Leader I, then ? / Leader I, then t chords harness operator smoke canvas help (Ctrl/Cmd+/ then leader)

Minimum locked (DoD): cases 1–11 in keymap.zig pass; zig fmt --check clean
on touched files; zig build test-rich and zig build test-rich-invariants pass;
zig build harness -Doptimize=Debug (full Wasm compile) succeeds; artifact
native/harness/zig-out/bin/harness.wasm rebuilt so it can be copied to
public/harness/ for the wasm-int suite if bridge signatures changed (they do
not here). CI build-harness remains the release-wasm gate.

Definition of done

  • native/harness/src/keymap.zigModPrereq.control + .i/.control leader (row + re-arm) + .i reserve swap; tests updated/added (cases 1–11)
  • ui/help_overlay.zig chords read Ctrl+I / Leader I, then ? / Leader I, then t (no Space/Ctrl+Shift+Space string remains in the overlay)
  • ui/keymap_dispatch.zig + keymap inline comments no longer reference Ctrl+Shift+Space (incl. the RESERVED doc comment, line 308, and the leader-row doc lines 106/366)
  • docs/harness-limits.md Keyboard & focus row reads Ctrl+I (and keeps the t thinking chord)
  • zig fmt --check clean on touched .zig files
  • zig build test-rich + zig build test-rich-invariants green
  • zig build harness -Doptimize=Debug (full Wasm compile) green
  • Cloud ops: N/A (no Production mutate) — explicitly
  • Living docs: docs/harness-limits.md updated; AGENTS/feature-divide/README/SECURITY/.env.example N/A justified
  • No bridge/protocol/build.zig export_symlink_names change (additive-not-needed verified)

Caps table

Cap / ceiling Value Rationale Code location Verdict
LEADER_WINDOW_MS 800 (unchanged) leader window; plan explicitly non-goal keymap.zig line 18 no change
KEYMAP_MAX 64 (unchanged) static table cap; plan explicitly non-goal keymap.zig line 15 no change

No new or changed cap — no human-gate decision required. (The ModPrereq enum
addition is a code artifact, not a cap.)

Risks & mitigations

Risk Mitigation
Ctrl+I collides with a browser/OS shortcut on some platform and never reaches the canvas Deliberate product decision (the source issue's premise). Emits Action.leader when the canvas receives the chord; Cmd+I / Ctrl+Shift+I explicitly left to the browser so no Inspect/italic regression
Forgetting the re-arm branch while changing the table row leaves the leader stuck Re-arm must change with the row (decision #4); covered by test 6
Regressing the reserved deny-list and re-marking Ctrl+I .browser .i removed from RESERVED; covered by test 8
Reserved Inspect chord swallowed during a pending leader .i+.ctrl_shift added to RESERVED (runs before leader handling); covered by test 3
Missing the thinking-toggle chord string (keeps "Leader Space, then t" on-screen) Design step 6 re-words .thinking_default_toggle too; covered by test 11 (behavior) + test 12 (overlay string); DoD asserts no Space/Ctrl+Shift+Space string remains in the overlay
Docs/AGENTS/feature-divide drift to old chord Only docs/harness-limits.md carries the chord string (verified); the other surfaces reference "leader" generically

Open questions

None — all in-scope engineering choices are locked above (no human decisions
remain; no existing cap is changed).

References


Implemented as PR #773 (plan/leader-ctrl-i, commit 1c4a965). Status
flipped HANDOFF-READY → IMPLEMENTED.

Metadata

Metadata

Assignees

No one assigned

    Labels

    harnessHarness / agent UIui

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions