{"version":3,"file":"keybindings.d.ts","sourceRoot":"","sources":["../../src/core/keybindings.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype Keybinding,\n\ttype KeybindingDefinitions,\n\ttype KeybindingsConfig,\n\ttype KeyId,\n\tTUI_KEYBINDINGS,\n\tKeybindingsManager as TuiKeybindingsManager,\n} from \"@kolisachint/hoocode-tui\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.js\";\n\ninterface AppKeybindings {\n\t\"app.interrupt\": true;\n\t\"app.clear\": true;\n\t\"app.exit\": true;\n\t\"app.suspend\": true;\n\t\"app.thinking.cycleForward\": true;\n\t\"app.thinking.cycleBackward\": true;\n\t\"app.model.cycleForward\": true;\n\t\"app.model.cycleBackward\": true;\n\t\"app.model.select\": true;\n\t\"app.tools.expand\": true;\n\t\"app.view.cycleForward\": true;\n\t\"app.view.cycleBackward\": true;\n\t\"app.scroll.pageUp\": true;\n\t\"app.scroll.pageDown\": true;\n\t\"app.scroll.top\": true;\n\t\"app.scroll.bottom\": true;\n\t\"app.scroll.lineUp\": true;\n\t\"app.scroll.lineDown\": true;\n\t\"app.scroll.exit\": true;\n\t\"app.scroll.search\": true;\n\t\"app.scroll.searchInView\": true;\n\t\"app.scroll.searchNext\": true;\n\t\"app.scroll.searchPrevious\": true;\n\t\"app.scroll.previousMessage\": true;\n\t\"app.scroll.nextMessage\": true;\n\t\"app.clipboard.copyMessage\": true;\n\t\"app.thinking.toggle\": true;\n\t\"app.chrome.cycleForward\": true;\n\t\"app.chrome.cycleBackward\": true;\n\t\"app.tasks.cycleForward\": true;\n\t\"app.tasks.cycleBackward\": true;\n\t\"app.team.focus\": true;\n\t\"app.team.nudge\": true;\n\t\"app.team.attach\": true;\n\t\"app.session.toggleNamedFilter\": true;\n\t\"app.editor.external\": true;\n\t\"app.message.followUp\": true;\n\t\"app.message.dequeue\": true;\n\t\"app.clipboard.pasteImage\": true;\n\t\"app.input.voiceTranscribe\": true;\n\t\"app.session.new\": true;\n\t\"app.session.tree\": true;\n\t\"app.session.fork\": true;\n\t\"app.session.resume\": true;\n\t\"app.session.changeDirectory\": true;\n\t\"app.session.color.cycleForward\": true;\n\t\"app.session.color.cycleBackward\": true;\n\t\"app.settings.open\": true;\n\t\"app.hotkeys.open\": true;\n\t\"app.mode.cycleForward\": true;\n\t\"app.mode.cycleBackward\": true;\n\t\"app.options.next\": true;\n\t\"app.options.back\": true;\n\t\"app.tree.foldOrUp\": true;\n\t\"app.tree.unfoldOrDown\": true;\n\t\"app.tree.editLabel\": true;\n\t\"app.tree.toggleLabelTimestamp\": true;\n\t\"app.session.togglePath\": true;\n\t\"app.session.toggleSort\": true;\n\t\"app.session.rename\": true;\n\t\"app.session.delete\": true;\n\t\"app.session.deleteNoninvasive\": true;\n\t\"app.models.save\": true;\n\t\"app.models.enableAll\": true;\n\t\"app.models.clearAll\": true;\n\t\"app.models.toggleProvider\": true;\n\t\"app.models.reorderUp\": true;\n\t\"app.models.reorderDown\": true;\n\t\"app.tree.filter.default\": true;\n\t\"app.tree.filter.noTools\": true;\n\t\"app.tree.filter.userOnly\": true;\n\t\"app.tree.filter.labeledOnly\": true;\n\t\"app.tree.filter.all\": true;\n\t\"app.tree.filter.cycleForward\": true;\n\t\"app.tree.filter.cycleBackward\": true;\n}\n\nexport type AppKeybinding = keyof AppKeybindings;\n\ndeclare module \"@kolisachint/hoocode-tui\" {\n\tinterface Keybindings extends AppKeybindings {}\n}\n\n/**\n * The keyboard map.\n *\n * ## Why it is grouped the way it is\n *\n * There are around sixty bindings here. Nobody holds sixty of anything, and the\n * usual answer — sort them by mechanism, so everything that cycles sits together\n * — makes a list that is tidy on the page and useless at the keyboard. \"It\n * cycles\" is a fact about the widget. It is not what anyone is thinking when\n * they reach for the key.\n *\n * So the grouping is by **intention**: the five things a person is ever doing\n * here, in the order the loop runs.\n *\n *   1. **Flow** — get out, get back. esc, ctrl+c, ctrl+d, ctrl+z.\n *   2. **Compose** — the message in your hands. alt+e, alt+r, alt+enter, alt+↑.\n *   3. **Steer** — what the agent is before it runs. alt+a, alt+m, alt+t.\n *   4. **Read** — what you see of what it did. alt+o, alt+l, ctrl+o, ctrl+t.\n *   5. **Screen** — how much room there is to see it in. alt+z.\n *   6. **Scroll** — where in it you are looking. pageUp/pageDown, ctrl+home/end.\n *   7. **Go** — sessions and places. alt+h, alt+w, alt+s, alt+k, alt+c.\n *\n * Seven groups, none of the learned ones larger than five, which is the size a\n * person can actually hold. Three cost nothing: **Flow** and **Scroll** are the\n * sets every terminal program and every pager already taught you, and the\n * **overlays** (pickers, the tree, the options pane, the pinned scroll view)\n * print their own keys on their own hint lines — recognised, never recalled.\n * **Screen** is one key. That leaves three groups to genuinely know.\n *\n * Screen is its own group rather than part of Read on purpose: every other\n * family changes what the screen *says*, and that one changes how much screen\n * there is to say it in. Folding it into Read pushed that family to six\n * subjects, which is the one thing this grouping exists to prevent.\n *\n * The declaration order below *is* the grouping, and it is not cosmetic:\n * `orderKeybindingsConfig` writes `keybindings.json` in this order, so the file\n * a user opens to rebind something is grouped the same way this one is.\n *\n * ## What the chord tells you\n *\n * The modifier says what kind of thing will happen; the letter says to what.\n *\n * - **alt+<letter> sets a value.** Nothing takes the screen, nothing loses\n *   focus, you keep typing. Seven of these are dials — an ordered set of stops\n *   with the current one painted where you can see it — and `shift+alt+<letter>`\n *   always steps back:\n *\n *       **a**gent mode · **m**odel · **t**hinking · tool **o**utput\n *       task **l**ist · session **c**olour · chrome (**z**)\n *\n *   Reversibility is the point. A control you can undo invites you to try it; a\n *   one-way control makes you stop and think first, which is the wrong tax on a\n *   key you press all day.\n *\n * - **ctrl+<letter> acts on what is drawn right now**, and shares its letter\n *   with the alt key for the same subject. `alt+o` sets how much tool output\n *   there ever is, `ctrl+o` jumps to all of it and back. `alt+t` sets how much\n *   thinking there ever is, `ctrl+t` shows or hides what you have. Two subjects,\n *   two letters, four keys — half of what four unrelated chords would cost.\n *\n * - **A bare navigation key moves the view.** pageUp, pageDown, ctrl+home and\n *   ctrl+end scroll the transcript and nothing else, which is what they do\n *   everywhere else too. They are the one family that took a key off another\n *   binding — the prompt editor's page keys, which had one to three lines to\n *   page through and so did nothing on nearly every press.\n *\n * - **A slash command chooses a stop outright.** `/mode`, `/model`, `/color`,\n *   `/chrome`, `/tree`. The key steps, the command picks; that is why\n *   `app.model.select` and `app.session.tree` ship unbound, having given their\n *   letters to dials. It is also the only way a dial stays reachable on a\n *   terminal that composes characters instead of sending alt — macOS\n *   Terminal.app — which is why any dial that can strand a setting has one.\n *\n * Two letters in the whole set name nothing, and both survive on the same\n * fallback — what they do is legible on screen, so the letter is read off rather\n * than remembered. `alt+n` focuses the team roster, and the task panel prints it\n * in its own header. `alt+z` steps the chrome dial, whose stop *is* the shape of\n * the screen you are looking at. Every letter that could have named the second\n * one is already taken by the thing it controls.\n *\n * ## The constraints that shaped it\n *\n * - No binding takes a key the editor or the terminal already owns\n *   (ctrl+a/e/b/f/k/u/w/y/d/l/r/g, ctrl+s XOFF, ctrl+m == enter, ctrl+i == tab).\n * - Inside an overlay the query line is a text field, so every `ctrl+<letter>`\n *   there belongs to the *text*. Overlay verbs are all on `alt+<letter>`.\n * - Only `alt+<letter>` and `alt+<digit>` survive a terminal without the Kitty\n *   keyboard protocol; `alt+[` and `alt+]` arrive as the CSI and OSC introducers.\n * - `shift+<non-letter>` needs Kitty too, so a `shift+…` default is a\n *   convenience on top of a key that works everywhere. `shift+<letter>` is\n *   banned outright: without Kitty it *is* the uppercase letter, which no scope\n *   with a query line can tell from typing.\n * - Legacy `alt+p` and `alt+n` are also read as `alt+up` and `alt+down`, so no\n *   scope may bind both halves of either pair.\n *\n * `test/keybinding-layout.test.ts` holds all of it, including that every\n * binding belongs to exactly one family.\n */\nexport const KEYBINDINGS = {\n\t...TUI_KEYBINDINGS,\n\n\t// ── Flow — getting out, getting back ────────────────────────────────────\n\t// Every terminal program you already use binds these, so they cost nothing to\n\t// learn and must never move: whatever else is misconfigured, you can still\n\t// stop the agent, clear the line, and leave.\n\t\"app.interrupt\": { defaultKeys: \"escape\", description: \"Cancel or abort\" },\n\t\"app.clear\": { defaultKeys: \"ctrl+c\", description: \"Clear editor\" },\n\t\"app.exit\": { defaultKeys: \"ctrl+d\", description: \"Exit when editor is empty\" },\n\t\"app.suspend\": {\n\t\tdefaultKeys: process.platform === \"win32\" ? [] : \"ctrl+z\",\n\t\tdescription: \"Suspend to background\",\n\t},\n\n\t// ── Compose — the message in your hands ─────────────────────────────────\n\t// Everything here acts on the text you are writing. Highest-frequency group,\n\t// and the one your hands are already on.\n\t// alt+e, not ctrl+g: ctrl+g is emacs/readline \"abort\" — the key you hit to\n\t// get out of something. Having it launch $EDITOR inverts that reflex.\n\t\"app.editor.external\": {\n\t\tdefaultKeys: \"alt+e\",\n\t\tdescription: \"Open external editor\",\n\t},\n\t// alt+r, not ctrl+r: ctrl+r is reverse history search in every shell, and it\n\t// was also the session picker's rename key — one chord, two meanings.\n\t\"app.input.voiceTranscribe\": {\n\t\tdefaultKeys: \"alt+r\",\n\t\tdescription: \"Record voice and transcribe into the editor\",\n\t},\n\t// The clipboard, both directions — one subject, the way a dial's two halves\n\t// are one subject. In is the image paste; out is the agent's last message.\n\t\"app.clipboard.pasteImage\": {\n\t\tdefaultKeys: process.platform === \"win32\" ? \"alt+v\" : \"ctrl+v\",\n\t\tdescription: \"Paste image from clipboard\",\n\t},\n\t// The one key in the set with no mnemonic left to offer: copy's own letters\n\t// are all spent (ctrl+c clears, ctrl+y yanks, alt+c is the colour dial) and\n\t// the letters still free name nothing at all. It earns its slot on a\n\t// fallback of its own — the action reports itself on the status line, so a\n\t// wrong guess is legible and costs nothing — and `/copy` is there for anyone\n\t// who would rather read it than remember it.\n\t\"app.clipboard.copyMessage\": {\n\t\tdefaultKeys: \"alt+q\",\n\t\tdescription: \"Copy the agent's last message to the clipboard\",\n\t},\n\t\"app.message.followUp\": {\n\t\tdefaultKeys: \"alt+enter\",\n\t\tdescription: \"Queue follow-up message\",\n\t},\n\t\"app.message.dequeue\": {\n\t\tdefaultKeys: \"alt+up\",\n\t\tdescription: \"Restore queued messages\",\n\t},\n\n\t// ── Steer — what the agent is before it runs ────────────────────────────\n\t// The only three keys that change what happens next, and the only three that\n\t// cost anything: latency, money, behaviour. The footer shows all three, which\n\t// is why they are worth one deliberate chunk of memory.\n\t// alt+a for \"agent mode\", and it is the dial the footer leads with. Not alt+m:\n\t// mode and model are one letter apart and the model has the better claim on\n\t// m, so this one takes the letter of what it selects — the agent's stance.\n\t\"app.mode.cycleForward\": {\n\t\tdefaultKeys: \"alt+a\",\n\t\tdescription: \"Cycle agent mode (ask → plan → build → debug)\",\n\t},\n\t\"app.mode.cycleBackward\": {\n\t\tdefaultKeys: \"shift+alt+a\",\n\t\tdescription: \"Cycle agent mode backward\",\n\t},\n\t// alt+m, not ctrl+p: the model is a cockpit dial — it is what the agent *is*,\n\t// not what is on screen — and p named nothing. The letter names the dial now,\n\t// which is the whole rule for the six of them. `/model` opens the picker that\n\t// used to be on this key, exactly as `/color` backs alt+c.\n\t\"app.model.cycleForward\": {\n\t\tdefaultKeys: \"alt+m\",\n\t\tdescription: \"Cycle to next model\",\n\t},\n\t\"app.model.cycleBackward\": {\n\t\tdefaultKeys: \"shift+alt+m\",\n\t\tdescription: \"Cycle to previous model\",\n\t},\n\t// Unbound by default: a dial's key steps it, and its slash command chooses.\n\t// alt+m stepping the model is worth more than alt+m opening a list of them,\n\t// and `/model` is one keystroke further with completion on the name.\n\t\"app.model.select\": { defaultKeys: [], description: \"Open model selector\" },\n\t// alt+t pairs with ctrl+t the way alt+o pairs with ctrl+o: same letter, same\n\t// subject, ctrl acting on what is drawn right now and alt on how much there\n\t// ever is. shift+tab stays as a second key rather than the first — it is the\n\t// cycle key every terminal agent has taught, and with no slash command for\n\t// the thinking level it is the only way to reach this dial on a terminal\n\t// that eats alt. Hints and /hotkeys name alt+t, which is the taught key.\n\t\"app.thinking.cycleForward\": {\n\t\tdefaultKeys: [\"alt+t\", \"shift+tab\"],\n\t\tdescription: \"Cycle thinking level (off → … → high)\",\n\t},\n\t\"app.thinking.cycleBackward\": {\n\t\tdefaultKeys: \"shift+alt+t\",\n\t\tdescription: \"Cycle thinking level backward\",\n\t},\n\n\t// ── Read — what you see of what it did ──────────────────────────────────\n\t// Free and reversible, every one of them: nothing here touches the work, only\n\t// the window onto it. Press again or add shift and you are back where you\n\t// were, which is what makes poking at them safe.\n\t// The view dial pairs with ctrl+o on purpose: same letter, different ring.\n\t// alt+o walks the dial a stop at a time and saves where it lands; ctrl+o\n\t// jumps to the far end and back without moving your home stop. One value\n\t// between them, so there is no second \"expanded\" state to keep in sync.\n\t\"app.view.cycleForward\": {\n\t\tdefaultKeys: \"alt+o\",\n\t\tdescription: \"Cycle tool output view (radar → peek → full)\",\n\t},\n\t\"app.view.cycleBackward\": {\n\t\tdefaultKeys: \"shift+alt+o\",\n\t\tdescription: \"Cycle tool output view backward\",\n\t},\n\t\"app.tools.expand\": {\n\t\tdefaultKeys: \"ctrl+o\",\n\t\tdescription: \"Jump to the full view from wherever you are, and back again\",\n\t},\n\t\"app.thinking.toggle\": {\n\t\tdefaultKeys: \"ctrl+t\",\n\t\tdescription: \"Toggle thinking blocks\",\n\t},\n\t\"app.tasks.cycleForward\": {\n\t\tdefaultKeys: \"alt+l\",\n\t\tdescription: \"Cycle task panel view (tasks → subagents → teams, skips empty lenses)\",\n\t},\n\t\"app.tasks.cycleBackward\": {\n\t\tdefaultKeys: \"shift+alt+l\",\n\t\tdescription: \"Cycle task panel view backward\",\n\t},\n\t// The one letter in the set that names nothing. It survives on the fallback\n\t// every unmemorable key needs: the task panel prints it in its own header\n\t// when the teams lens is up, so it is read off the screen rather than\n\t// remembered. alt+n steps INTO that lens and focuses the roster (--team only).\n\t\"app.team.focus\": {\n\t\tdefaultKeys: \"alt+n\",\n\t\tdescription: \"Focus the team roster (navigate roles, n nudge, a attach)\",\n\t},\n\n\t// alt+z for the amount of screen the chrome gets — the seventh dial, and the\n\t// only one whose letter names nothing. It earns the slot the way alt+n does:\n\t// its stop is not a label you have to recall, it is the shape of the screen\n\t// in front of you, so pressing it once tells you everything the name would.\n\t// Every other letter that could have named it is taken by what it controls\n\t// (alt+o the view, alt+l the ledger) or by a dial that outranks it.\n\t\"app.chrome.cycleForward\": {\n\t\tdefaultKeys: \"alt+z\",\n\t\tdescription: \"Cycle chrome density (full → compact → bare)\",\n\t},\n\t\"app.chrome.cycleBackward\": {\n\t\tdefaultKeys: \"shift+alt+z\",\n\t\tdescription: \"Cycle chrome density backward\",\n\t},\n\n\t// ── Scrolling the transcript ────────────────────────────────────────────\n\t// These are the only keys in the set that need no mnemonic at all: pageUp,\n\t// pageDown, ctrl+home and ctrl+end mean exactly here what they mean in every\n\t// pager, editor and browser anyone has ever used. That is worth more than\n\t// any letter, and it is why they took the page keys off the prompt editor.\n\t//\n\t// The prompt is one to three lines nine times out of ten, so\n\t// `tui.editor.pageUp` had nothing to scroll and the most universally known\n\t// \"show me what scrolled past\" gesture in computing did nothing at all. It\n\t// is unbound by default rather than sharing the key, because a key that\n\t// moves the transcript sometimes and a text cursor other times is precisely\n\t// the unpredictability this whole family exists to remove.\n\t//\n\t// Only these four are live at the prompt. The rest — line steps, the ends,\n\t// and escape — belong to the pinned view, which captures keys the way a\n\t// picker does, so they may reuse keys the prompt owns.\n\t\"app.scroll.pageUp\": {\n\t\tdefaultKeys: \"pageUp\",\n\t\tdescription: \"Scroll the transcript up a page (pins the view)\",\n\t},\n\t\"app.scroll.pageDown\": {\n\t\tdefaultKeys: \"pageDown\",\n\t\tdescription: \"Scroll the transcript down a page\",\n\t},\n\t\"app.scroll.top\": {\n\t\tdefaultKeys: \"ctrl+home\",\n\t\tdescription: \"Jump to the start of the transcript\",\n\t},\n\t\"app.scroll.bottom\": {\n\t\tdefaultKeys: \"ctrl+end\",\n\t\tdescription: \"Jump back to live output\",\n\t},\n\t// ctrl+up / ctrl+down, which every editor already means \"move by the bigger\n\t// unit\" with — and the bigger unit in a session is a turn. They stop only on\n\t// your own messages: those are the landmarks anyone actually navigates by,\n\t// and they are sparse enough that a few presses cross a long transcript.\n\t// Both the legacy `\\x1bOa` form and the CSI `\\x1b[1;5A` form parse, so this\n\t// needs no Kitty.\n\t// ctrl+r, which is reverse-search in every shell — and this searches\n\t// backwards for the same reason: what you are looking for in a session is\n\t// behind you, and the most recent hit is nearly always the one you meant.\n\t// It was kept free of the voice key for exactly this.\n\t\"app.scroll.search\": {\n\t\tdefaultKeys: \"ctrl+r\",\n\t\tdescription: \"Search the transcript\",\n\t},\n\t\"app.scroll.previousMessage\": {\n\t\tdefaultKeys: \"ctrl+up\",\n\t\tdescription: \"Jump to your previous message\",\n\t},\n\t\"app.scroll.nextMessage\": {\n\t\tdefaultKeys: \"ctrl+down\",\n\t\tdescription: \"Jump to your next message\",\n\t},\n\t// alt+l for the task *ledger* — the pane's own name for itself. It was\n\t// ctrl+n, which named nothing and was the last dial off the alt ring; moving\n\t// it also gives the lens the reverse it could never have on ctrl, where\n\t// shift+ctrl+n is Windows Terminal's \"new window\". ctrl+n is free now, so an\n\t// emacs config can take it for cursorDown without colliding.\n\n\t// ── Go — sessions and places ────────────────────────────────────────────\n\t// These take the screen and hand it back on escape. Each has a slash command\n\t// that does the same thing, so none of them has to be remembered as a key.\n\t\"app.session.resume\": { defaultKeys: \"alt+h\", description: \"Resume a session from history\" },\n\t// Unbound since alt+t became the thinking dial: the letter is worth more to a\n\t// dial pressed through the day than to a surface `/tree` opens with\n\t// completion, the same trade `app.model.select` makes for `/model`.\n\t\"app.session.tree\": { defaultKeys: [], description: \"Open session tree\" },\n\t// The two destructive-ish session moves stay unbound by default. /new\n\t// replaces the transcript and /fork needs a message picked out of it, so\n\t// neither wants to be one stray chord away; both remain bindable by hand.\n\t\"app.session.new\": { defaultKeys: [], description: \"Start a new session\" },\n\t\"app.session.fork\": { defaultKeys: [], description: \"Fork current session\" },\n\t\"app.session.changeDirectory\": {\n\t\tdefaultKeys: \"alt+w\",\n\t\tdescription: \"Change working directory (move to another repo without quitting)\",\n\t},\n\t// alt+c (\"colour\"), and the tree's filter cycle has the same letter on\n\t// purpose: the tree captures keys while it is open, so the two are never live\n\t// at once, and both mean \"step to the next one\" wherever you press them. The\n\t// backward half needs Kitty, like every other shift-modified default here.\n\t\"app.session.color.cycleForward\": {\n\t\tdefaultKeys: \"alt+c\",\n\t\tdescription: \"Cycle the session chip's color\",\n\t},\n\t\"app.session.color.cycleBackward\": {\n\t\tdefaultKeys: \"shift+alt+c\",\n\t\tdescription: \"Cycle the session chip's color backward\",\n\t},\n\t\"app.settings.open\": { defaultKeys: \"alt+s\", description: \"Open settings\" },\n\t\"app.hotkeys.open\": { defaultKeys: \"alt+k\", description: \"Show keyboard shortcuts\" },\n\n\t// ── Overlays — live only while their surface is open ────────────────────\n\t// Never a memory burden: each surface prints its own keys on its own hint\n\t// line, so these are recognised, not recalled. They may reuse a global\n\t// letter, because the surface captures keys while it is up.\n\t\"app.team.nudge\": {\n\t\t// Plain letters are safe here: these fire only while the task panel holds\n\t\t// focus (team-focus mode), never while typing in the editor.\n\t\tdefaultKeys: \"n\",\n\t\tdescription: \"Nudge the selected team role (team focus mode)\",\n\t},\n\t\"app.team.attach\": {\n\t\tdefaultKeys: \"a\",\n\t\tdescription: \"Attach to the selected team role (team focus mode)\",\n\t},\n\t// The pinned transcript view. Live only while the view is pinned, which it\n\t// captures keys during, so the arrows are free to mean here what the prompt\n\t// means by them elsewhere — up and down move the view rather than walking\n\t// prompt history, because there is no prompt in front of you to walk.\n\t// `app.scroll.top` / `app.scroll.bottom` keep their global keys inside the\n\t// view as well, so the ends are the same chord wherever you press them.\n\t\"app.scroll.lineUp\": {\n\t\tdefaultKeys: \"up\",\n\t\tdescription: \"Scroll up a line (pinned view)\",\n\t},\n\t\"app.scroll.lineDown\": {\n\t\tdefaultKeys: \"down\",\n\t\tdescription: \"Scroll down a line (pinned view)\",\n\t},\n\t// escape is the Flow key, and it keeps its meaning — get out of the thing\n\t// you are in. While pinned, the thing you are in is the scrolled-back view,\n\t// so the first press returns you to live output and a second one reaches\n\t// `app.interrupt` as it always did. That ordering is deliberate: it puts you\n\t// back where you can see what the agent is doing *before* you stop it.\n\t\"app.scroll.exit\": {\n\t\tdefaultKeys: \"escape\",\n\t\tdescription: \"Leave the pinned view and follow live output\",\n\t},\n\t// `/` opens the search once you are already reading, the way every pager\n\t// does — and it is a separate id from `app.scroll.search` precisely because\n\t// it cannot be global: at the prompt `/` starts a slash command, and that is\n\t// not a key this feature gets to take. Inside the pinned view the prompt is\n\t// not taking input, so the character is free.\n\t\"app.scroll.searchInView\": {\n\t\tdefaultKeys: \"/\",\n\t\tdescription: \"Search the transcript (pinned view)\",\n\t},\n\t// Plain letters step the matches, which is safe here and only here:\n\t// stepping is live only after the query line is committed, so no letter is\n\t// competing with typing.\n\t//\n\t// n/p rather than the pager's n/N — a bare `shift+<letter>` is banned\n\t// outright, because a terminal without the Kitty protocol sends it as the\n\t// plain uppercase letter. n/p is also the emacs next/previous pair this map\n\t// already leans on elsewhere.\n\t\"app.scroll.searchNext\": {\n\t\tdefaultKeys: \"n\",\n\t\tdescription: \"Next search match (pinned view)\",\n\t},\n\t\"app.scroll.searchPrevious\": {\n\t\tdefaultKeys: \"p\",\n\t\tdescription: \"Previous search match (pinned view)\",\n\t},\n\t// The options pane reads as a horizontal wizard, so the arrows point the way\n\t// the steps run: → commits the highlighted answer and moves on, ← goes back.\n\t// On the free-text row they only mean that while the field is empty —\n\t// otherwise they move the text cursor, and enter is what commits.\n\t\"app.options.next\": { defaultKeys: \"right\", description: \"Confirm and advance to the next question\" },\n\t\"app.options.back\": { defaultKeys: \"left\", description: \"Go back to the previous question\" },\n\t// Session picker verbs. All on alt so the picker's query line keeps the\n\t// emacs editing keys it used to lose: ctrl+a jumped to the start of the\n\t// query *and* enabled every model, ctrl+d deleted a character *and* deleted\n\t// the highlighted session.\n\t\"app.session.togglePath\": {\n\t\tdefaultKeys: \"alt+p\",\n\t\tdescription: \"Toggle session path display\",\n\t},\n\t\"app.session.toggleSort\": {\n\t\tdefaultKeys: \"alt+o\",\n\t\tdescription: \"Toggle session sort order\",\n\t},\n\t\"app.session.toggleNamedFilter\": {\n\t\tdefaultKeys: \"alt+n\",\n\t\tdescription: \"Toggle named session filter\",\n\t},\n\t\"app.session.rename\": {\n\t\tdefaultKeys: \"alt+r\",\n\t\tdescription: \"Rename session\",\n\t},\n\t\"app.session.delete\": {\n\t\tdefaultKeys: \"alt+x\",\n\t\tdescription: \"Delete session\",\n\t},\n\t\"app.session.deleteNoninvasive\": {\n\t\tdefaultKeys: \"ctrl+backspace\",\n\t\tdescription: \"Delete session when query is empty\",\n\t},\n\t// Model picker verbs, same rule. ctrl+s in particular was XOFF: on a terminal\n\t// with flow control still on, \"save\" froze the session instead.\n\t\"app.models.save\": {\n\t\tdefaultKeys: \"alt+s\",\n\t\tdescription: \"Save model selection\",\n\t},\n\t\"app.models.enableAll\": {\n\t\tdefaultKeys: \"alt+a\",\n\t\tdescription: \"Enable all models\",\n\t},\n\t\"app.models.clearAll\": {\n\t\tdefaultKeys: \"alt+x\",\n\t\tdescription: \"Clear all models\",\n\t},\n\t// alt+g (\"group\"), not alt+p: on a terminal without the Kitty protocol, alt+p\n\t// arrives as ESC-p, which the key parser also accepts as alt+up — and alt+up\n\t// is this same picker's reorder key.\n\t\"app.models.toggleProvider\": {\n\t\tdefaultKeys: \"alt+g\",\n\t\tdescription: \"Toggle all models for provider\",\n\t},\n\t\"app.models.reorderUp\": {\n\t\tdefaultKeys: \"alt+up\",\n\t\tdescription: \"Move model up in order\",\n\t},\n\t\"app.models.reorderDown\": {\n\t\tdefaultKeys: \"alt+down\",\n\t\tdescription: \"Move model down in order\",\n\t},\n\t\"app.tree.foldOrUp\": {\n\t\tdefaultKeys: [\"ctrl+left\", \"alt+left\"],\n\t\tdescription: \"Fold tree branch or move up\",\n\t},\n\t\"app.tree.unfoldOrDown\": {\n\t\tdefaultKeys: [\"ctrl+right\", \"alt+right\"],\n\t\tdescription: \"Unfold tree branch or move down\",\n\t},\n\t// alt+l (\"label\") and alt+t (\"time\"), not shift+l / shift+t. The tree has a\n\t// search query that takes every printable key, and outside the Kitty protocol\n\t// shift+<letter> *is* the plain uppercase letter — so typing \"TODO\" or\n\t// \"Logger\" into the query opened the label editor instead of searching. Same\n\t// rule as every other picker verb: the query owns the letters, the verbs take\n\t// alt. Global alt+t opens the tree; inside the tree it toggles timestamps,\n\t// which is unambiguous because the tree captures keys while it is open.\n\t\"app.tree.editLabel\": {\n\t\tdefaultKeys: \"alt+l\",\n\t\tdescription: \"Edit tree label\",\n\t},\n\t\"app.tree.toggleLabelTimestamp\": {\n\t\tdefaultKeys: \"alt+t\",\n\t\tdescription: \"Toggle tree label timestamps\",\n\t},\n\t// Five lenses in a fixed order, so they are numbered rather than lettered:\n\t// alt+1..alt+5 needs no mnemonic and collides with nothing. The old set\n\t// (ctrl+d/t/u/l/a) collided with delete-char, thinking, kill-to-start,\n\t// clear-screen and start-of-line respectively.\n\t\"app.tree.filter.default\": {\n\t\tdefaultKeys: \"alt+1\",\n\t\tdescription: \"Tree filter: default view\",\n\t},\n\t\"app.tree.filter.noTools\": {\n\t\tdefaultKeys: \"alt+2\",\n\t\tdescription: \"Tree filter: hide tool results\",\n\t},\n\t\"app.tree.filter.userOnly\": {\n\t\tdefaultKeys: \"alt+3\",\n\t\tdescription: \"Tree filter: user messages only\",\n\t},\n\t\"app.tree.filter.labeledOnly\": {\n\t\tdefaultKeys: \"alt+4\",\n\t\tdescription: \"Tree filter: labeled entries only\",\n\t},\n\t\"app.tree.filter.all\": {\n\t\tdefaultKeys: \"alt+5\",\n\t\tdescription: \"Tree filter: show all entries\",\n\t},\n\t// alt+c for \"cycle\". Not a bracket pair, tempting as `alt+[` / `alt+]` are:\n\t// without the Kitty protocol those arrive as ESC-[ and ESC-], which are the\n\t// CSI and OSC introducers and so are not parsed as keys at all. The backward\n\t// half needs Kitty, like every other shift-modified default here.\n\t\"app.tree.filter.cycleForward\": {\n\t\tdefaultKeys: \"alt+c\",\n\t\tdescription: \"Tree filter: cycle forward\",\n\t},\n\t\"app.tree.filter.cycleBackward\": {\n\t\tdefaultKeys: \"shift+alt+c\",\n\t\tdescription: \"Tree filter: cycle backward\",\n\t},\n} as const satisfies KeybindingDefinitions;\n\nconst KEYBINDING_NAME_MIGRATIONS = {\n\tcursorUp: \"tui.editor.cursorUp\",\n\tcursorDown: \"tui.editor.cursorDown\",\n\tcursorLeft: \"tui.editor.cursorLeft\",\n\tcursorRight: \"tui.editor.cursorRight\",\n\tcursorWordLeft: \"tui.editor.cursorWordLeft\",\n\tcursorWordRight: \"tui.editor.cursorWordRight\",\n\tcursorLineStart: \"tui.editor.cursorLineStart\",\n\tcursorLineEnd: \"tui.editor.cursorLineEnd\",\n\tjumpForward: \"tui.editor.jumpForward\",\n\tjumpBackward: \"tui.editor.jumpBackward\",\n\tpageUp: \"tui.editor.pageUp\",\n\tpageDown: \"tui.editor.pageDown\",\n\tdeleteCharBackward: \"tui.editor.deleteCharBackward\",\n\tdeleteCharForward: \"tui.editor.deleteCharForward\",\n\tdeleteWordBackward: \"tui.editor.deleteWordBackward\",\n\tdeleteWordForward: \"tui.editor.deleteWordForward\",\n\tdeleteToLineStart: \"tui.editor.deleteToLineStart\",\n\tdeleteToLineEnd: \"tui.editor.deleteToLineEnd\",\n\tyank: \"tui.editor.yank\",\n\tyankPop: \"tui.editor.yankPop\",\n\tundo: \"tui.editor.undo\",\n\tnewLine: \"tui.input.newLine\",\n\tsubmit: \"tui.input.submit\",\n\ttab: \"tui.input.tab\",\n\tcopy: \"tui.input.copy\",\n\tselectUp: \"tui.select.up\",\n\tselectDown: \"tui.select.down\",\n\tselectPageUp: \"tui.select.pageUp\",\n\tselectPageDown: \"tui.select.pageDown\",\n\tselectConfirm: \"tui.select.confirm\",\n\tselectCancel: \"tui.select.cancel\",\n\t// Renamed when each gained a backward half, so that every two-direction dial\n\t// reads <dial>.cycleForward / <dial>.cycleBackward.\n\t\"app.thinking.cycle\": \"app.thinking.cycleForward\",\n\t\"app.mode.cycle\": \"app.mode.cycleForward\",\n\t\"app.tasks.cycleView\": \"app.tasks.cycleForward\",\n\tinterrupt: \"app.interrupt\",\n\tclear: \"app.clear\",\n\texit: \"app.exit\",\n\tsuspend: \"app.suspend\",\n\tcycleThinkingLevel: \"app.thinking.cycleForward\",\n\tcycleModelForward: \"app.model.cycleForward\",\n\tcycleModelBackward: \"app.model.cycleBackward\",\n\tselectModel: \"app.model.select\",\n\texpandTools: \"app.tools.expand\",\n\ttoggleThinking: \"app.thinking.toggle\",\n\ttoggleSessionNamedFilter: \"app.session.toggleNamedFilter\",\n\texternalEditor: \"app.editor.external\",\n\tfollowUp: \"app.message.followUp\",\n\tdequeue: \"app.message.dequeue\",\n\tpasteImage: \"app.clipboard.pasteImage\",\n\tnewSession: \"app.session.new\",\n\ttree: \"app.session.tree\",\n\tfork: \"app.session.fork\",\n\tresume: \"app.session.resume\",\n\ttreeFoldOrUp: \"app.tree.foldOrUp\",\n\ttreeUnfoldOrDown: \"app.tree.unfoldOrDown\",\n\ttreeEditLabel: \"app.tree.editLabel\",\n\ttreeToggleLabelTimestamp: \"app.tree.toggleLabelTimestamp\",\n\ttoggleSessionPath: \"app.session.togglePath\",\n\ttoggleSessionSort: \"app.session.toggleSort\",\n\trenameSession: \"app.session.rename\",\n\tdeleteSession: \"app.session.delete\",\n\tdeleteSessionNoninvasive: \"app.session.deleteNoninvasive\",\n} as const satisfies Record<string, Keybinding>;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isLegacyKeybindingName(key: string): key is keyof typeof KEYBINDING_NAME_MIGRATIONS {\n\treturn key in KEYBINDING_NAME_MIGRATIONS;\n}\n\nfunction toKeybindingsConfig(value: unknown): KeybindingsConfig {\n\tif (!isRecord(value)) return {};\n\n\tconst config: KeybindingsConfig = {};\n\tfor (const [key, binding] of Object.entries(value)) {\n\t\tif (typeof binding === \"string\") {\n\t\t\tconfig[key] = binding as KeyId;\n\t\t\tcontinue;\n\t\t}\n\t\tif (Array.isArray(binding) && binding.every((entry) => typeof entry === \"string\")) {\n\t\t\tconfig[key] = binding as KeyId[];\n\t\t}\n\t}\n\treturn config;\n}\n\nexport function migrateKeybindingsConfig(rawConfig: Record<string, unknown>): {\n\tconfig: Record<string, unknown>;\n\tmigrated: boolean;\n} {\n\tconst config: Record<string, unknown> = {};\n\tlet migrated = false;\n\n\tfor (const [key, value] of Object.entries(rawConfig)) {\n\t\tconst nextKey = isLegacyKeybindingName(key) ? KEYBINDING_NAME_MIGRATIONS[key] : key;\n\t\tif (nextKey !== key) {\n\t\t\tmigrated = true;\n\t\t}\n\t\tif (key !== nextKey && Object.hasOwn(rawConfig, nextKey)) {\n\t\t\tmigrated = true;\n\t\t\tcontinue;\n\t\t}\n\t\tconfig[nextKey] = value;\n\t}\n\n\treturn { config: orderKeybindingsConfig(config), migrated };\n}\n\nfunction orderKeybindingsConfig(config: Record<string, unknown>): Record<string, unknown> {\n\tconst ordered: Record<string, unknown> = {};\n\tfor (const keybinding of Object.keys(KEYBINDINGS)) {\n\t\tif (Object.hasOwn(config, keybinding)) {\n\t\t\tordered[keybinding] = config[keybinding];\n\t\t}\n\t}\n\n\tconst extras = Object.keys(config)\n\t\t.filter((key) => !Object.hasOwn(ordered, key))\n\t\t.sort();\n\tfor (const key of extras) {\n\t\tordered[key] = config[key];\n\t}\n\n\treturn ordered;\n}\n\nfunction loadRawConfig(path: string): Record<string, unknown> | undefined {\n\tif (!existsSync(path)) return undefined;\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\")) as unknown;\n\t\treturn isRecord(parsed) ? parsed : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport class KeybindingsManager extends TuiKeybindingsManager {\n\tprivate configPath: string | undefined;\n\n\tconstructor(userBindings: KeybindingsConfig = {}, configPath?: string) {\n\t\tsuper(KEYBINDINGS, userBindings);\n\t\tthis.configPath = configPath;\n\t}\n\n\tstatic create(agentDir: string = getAgentDir()): KeybindingsManager {\n\t\tconst configPath = join(agentDir, \"keybindings.json\");\n\t\tconst userBindings = KeybindingsManager.loadFromFile(configPath);\n\t\treturn new KeybindingsManager(userBindings, configPath);\n\t}\n\n\treload(): void {\n\t\tif (!this.configPath) return;\n\t\tthis.setUserBindings(KeybindingsManager.loadFromFile(this.configPath));\n\t}\n\n\tgetEffectiveConfig(): KeybindingsConfig {\n\t\treturn this.getResolvedBindings();\n\t}\n\n\tprivate static loadFromFile(path: string): KeybindingsConfig {\n\t\tconst rawConfig = loadRawConfig(path);\n\t\tif (!rawConfig) return {};\n\t\treturn toKeybindingsConfig(migrateKeybindingsConfig(rawConfig).config);\n\t}\n}\n\nexport type { KeyId, KeybindingsConfig };\n"]}