@crouton-kit/crouter 0.3.70 → 0.3.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +1 -1
  2. package/dist/builtin-memory/00-runtime-base.md +3 -10
  3. package/dist/builtin-memory/04-base-worker.md +18 -0
  4. package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
  5. package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +13 -34
  6. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +622 -724
  7. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +231 -68
  8. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +2 -9
  9. package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +361 -90
  10. package/dist/builtin-pi-packages/pi-crtr-extensions/package-lock.json +2 -2
  11. package/dist/builtin-pi-packages/pi-mode-switch/README.md +11 -39
  12. package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +20 -15
  13. package/dist/builtin-pi-packages/pi-mode-switch/package.json +1 -1
  14. package/dist/builtin-views/canvas/tui.mjs +8 -9
  15. package/dist/builtin-views/chat/tui.mjs +12 -12
  16. package/dist/builtin-views/git-pr/tui.mjs +7 -8
  17. package/dist/builtin-views/inbox/tui.mjs +27 -41
  18. package/dist/builtin-views/linkedin/tui.mjs +23 -37
  19. package/dist/builtin-views/prompt-review/tui.mjs +11 -11
  20. package/dist/builtin-views/settings/tui.mjs +11 -11
  21. package/dist/builtin-views/workspace-sidebar/tui.mjs +8 -9
  22. package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +3 -3
  23. package/dist/clients/attach/__tests__/crtr-output-render.test.js +1 -1
  24. package/dist/clients/attach/__tests__/mermaid-render.test.d.ts +1 -0
  25. package/dist/clients/attach/__tests__/mermaid-render.test.js +20 -0
  26. package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.d.ts +1 -0
  27. package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.js +110 -0
  28. package/dist/clients/attach/attach-cmd.d.ts +1 -1
  29. package/dist/clients/attach/attach-cmd.js +738 -735
  30. package/dist/clients/attach/canvas-panels.js +2 -3
  31. package/dist/clients/attach/chat-view.d.ts +7 -8
  32. package/dist/clients/attach/chat-view.js +119 -79
  33. package/dist/clients/attach/edit-diff-render.d.ts +6 -0
  34. package/dist/clients/attach/edit-diff-render.js +263 -0
  35. package/dist/clients/attach/input-controller.d.ts +28 -2
  36. package/dist/clients/attach/input-controller.js +38 -5
  37. package/dist/clients/attach/mermaid-render.js +5 -2
  38. package/dist/clients/attach/pickers.d.ts +8 -7
  39. package/dist/clients/attach/pickers.js +11 -17
  40. package/dist/clients/attach/slash-commands.d.ts +9 -0
  41. package/dist/clients/attach/slash-commands.js +127 -7
  42. package/dist/clients/attach/titled-editor.d.ts +12 -1
  43. package/dist/clients/attach/titled-editor.js +103 -8
  44. package/dist/commands/canvas-browse.js +2 -2
  45. package/dist/commands/memory/lint.js +39 -5
  46. package/dist/commands/memory/write.js +1 -0
  47. package/dist/commands/node.js +9 -2
  48. package/dist/commands/surface-tmux-spread.js +1 -3
  49. package/dist/commands/sys/__tests__/config-keybindings.test.d.ts +1 -0
  50. package/dist/commands/sys/__tests__/config-keybindings.test.js +55 -0
  51. package/dist/commands/sys/__tests__/config-model-ladders.test.d.ts +1 -0
  52. package/dist/commands/sys/__tests__/config-model-ladders.test.js +121 -0
  53. package/dist/commands/sys/config.js +18 -21
  54. package/dist/commands/sys/setup-core.d.ts +12 -1
  55. package/dist/commands/sys/setup-core.js +23 -3
  56. package/dist/commands/sys/setup.js +42 -12
  57. package/dist/core/__tests__/base-worker-prompt.test.d.ts +1 -0
  58. package/dist/core/__tests__/base-worker-prompt.test.js +24 -0
  59. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +232 -1
  60. package/dist/core/__tests__/fault-classifier.test.js +15 -0
  61. package/dist/core/__tests__/full/broker-dialogs.test.js +7 -2
  62. package/dist/core/__tests__/host-teardown-process-group.test.js +15 -4
  63. package/dist/core/__tests__/stream-watchdog.test.d.ts +1 -0
  64. package/dist/core/__tests__/stream-watchdog.test.js +70 -0
  65. package/dist/core/__tests__/tmux-surface.test.js +72 -0
  66. package/dist/core/canvas/browse/__tests__/model.test.js +23 -5
  67. package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
  68. package/dist/core/canvas/browse/app.js +164 -5
  69. package/dist/core/canvas/browse/model.d.ts +10 -6
  70. package/dist/core/canvas/browse/model.js +20 -12
  71. package/dist/core/canvas/browse/pins.d.ts +4 -0
  72. package/dist/core/canvas/browse/pins.js +29 -0
  73. package/dist/core/canvas/browse/render.d.ts +13 -1
  74. package/dist/core/canvas/browse/render.js +72 -15
  75. package/dist/core/canvas/nav-model.js +5 -11
  76. package/dist/core/canvas/paths.d.ts +5 -0
  77. package/dist/core/canvas/paths.js +7 -0
  78. package/dist/core/config.d.ts +53 -2
  79. package/dist/core/config.js +228 -110
  80. package/dist/core/fault-classifier.js +1 -1
  81. package/dist/core/fs-utils.d.ts +1 -0
  82. package/dist/core/fs-utils.js +15 -1
  83. package/dist/core/keybindings/__tests__/resolve.test.d.ts +1 -0
  84. package/dist/core/keybindings/__tests__/resolve.test.js +224 -0
  85. package/dist/core/keybindings/catalog.d.ts +14 -0
  86. package/dist/core/keybindings/catalog.js +256 -0
  87. package/dist/core/keybindings/index.d.ts +5 -0
  88. package/dist/core/keybindings/index.js +4 -0
  89. package/dist/core/keybindings/match.d.ts +29 -0
  90. package/dist/core/keybindings/match.js +99 -0
  91. package/dist/core/keybindings/persistence.d.ts +14 -0
  92. package/dist/core/keybindings/persistence.js +31 -0
  93. package/dist/core/keybindings/resolve.d.ts +14 -0
  94. package/dist/core/keybindings/resolve.js +305 -0
  95. package/dist/core/keybindings/types.d.ts +37 -0
  96. package/dist/core/keybindings/types.js +1 -0
  97. package/dist/core/memory-resolver.d.ts +1 -1
  98. package/dist/core/memory-resolver.js +10 -3
  99. package/dist/core/predicate.d.ts +5 -3
  100. package/dist/core/predicate.js +5 -3
  101. package/dist/core/runtime/broker-protocol.d.ts +33 -5
  102. package/dist/core/runtime/broker.js +163 -14
  103. package/dist/core/runtime/launch.d.ts +39 -6
  104. package/dist/core/runtime/launch.js +78 -20
  105. package/dist/core/runtime/naming.js +3 -3
  106. package/dist/core/runtime/pi-cli.d.ts +6 -0
  107. package/dist/core/runtime/pi-cli.js +16 -2
  108. package/dist/core/runtime/placement.d.ts +2 -2
  109. package/dist/core/runtime/placement.js +4 -1
  110. package/dist/core/runtime/promote.js +4 -0
  111. package/dist/core/runtime/recap.d.ts +3 -3
  112. package/dist/core/runtime/recap.js +47 -44
  113. package/dist/core/runtime/recycle.js +6 -1
  114. package/dist/core/runtime/reset.js +5 -0
  115. package/dist/core/runtime/session-list-cache.d.ts +23 -0
  116. package/dist/core/runtime/session-list-cache.js +270 -0
  117. package/dist/core/runtime/spawn.js +51 -32
  118. package/dist/core/runtime/stream-watchdog.d.ts +26 -0
  119. package/dist/core/runtime/stream-watchdog.js +75 -0
  120. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  121. package/dist/core/runtime/tmux-chrome.js +2 -2
  122. package/dist/core/runtime/tmux.d.ts +18 -15
  123. package/dist/core/runtime/tmux.js +216 -110
  124. package/dist/core/tui/__tests__/host-keybindings.test.d.ts +1 -0
  125. package/dist/core/tui/__tests__/host-keybindings.test.js +112 -0
  126. package/dist/core/tui/host.d.ts +30 -1
  127. package/dist/core/tui/host.js +148 -34
  128. package/dist/core/view/contract.d.ts +17 -3
  129. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +41 -0
  130. package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.d.ts +1 -0
  131. package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +121 -0
  132. package/dist/pi-extensions/canvas-inbox-watcher.d.ts +1 -1
  133. package/dist/pi-extensions/canvas-inbox-watcher.js +203 -25
  134. package/dist/pi-extensions/canvas-recap.d.ts +4 -0
  135. package/dist/pi-extensions/canvas-recap.js +38 -37
  136. package/dist/pi-extensions/canvas-stophook.d.ts +11 -0
  137. package/dist/pi-extensions/canvas-stophook.js +33 -4
  138. package/dist/types.d.ts +3 -13
  139. package/dist/types.js +2 -26
  140. package/dist/web-client/assets/{index-DiFuLcp6.js → index--md2ylfi.js} +1 -1
  141. package/dist/web-client/index.html +1 -1
  142. package/dist/web-client/sw.js +1 -1
  143. package/package.json +3 -3
  144. package/dist/pi-extensions/canvas-nav.d.ts +0 -43
  145. package/dist/pi-extensions/canvas-nav.js +0 -640
  146. package/dist/pi-extensions/widget-order-bus.d.ts +0 -6
  147. package/dist/pi-extensions/widget-order-bus.js +0 -34
@@ -1,8 +1,6 @@
1
1
  # pi-mode-switch
2
2
 
3
- Press **Alt+M** to cycle between operating modes — the way Claude's Shift+Tab
4
- cycles plan / accept-edits. Whenever the mode changes, the new mode's operating
5
- instructions are injected (invisibly) before your next message.
3
+ Cycle between operating modes with crouter's configurable **Cycle mode** binding (default **Alt+M**) — the way Claude's Shift+Tab cycles plan / accept-edits. Whenever the mode changes, the new mode's operating instructions are injected invisibly before your next message.
6
4
 
7
5
  ## Modes
8
6
 
@@ -14,51 +12,25 @@ instructions are injected (invisibly) before your next message.
14
12
 
15
13
  ## Usage
16
14
 
17
- - `Alt+M` — cycle `normal → spec → plan → normal`
18
- - `/mode` — same as Alt+M
15
+ - the configured `crtr.mode.cycle` binding (default `Alt+M`) — cycle `normal → spec → plan → normal`
16
+ - `/mode` — cycle without using the shortcut
19
17
  - `/mode <normal\|spec\|plan>` — jump to a specific mode
20
- - the agent can switch its own mode by running the bundled `bin/mode` CLI through
21
- its bash tool — e.g. `bash <plugin>/bin/mode normal` after a spec/plan is
22
- approved. The command prints that mode's guidance, so the text only enters
23
- context at the moment of the switch (no always-on tool)
18
+ - the agent can switch its own mode by running the bundled `bin/mode` CLI through its bash tool — e.g. `bash <plugin>/bin/mode normal` after a spec/plan is approved. The command prints that mode's guidance, so the text only enters context at the moment of the switch (no always-on tool)
24
19
 
25
- The current mode renders as a badge on the **bottom-right of the input box**,
26
- diagonally opposite the session-name badge (top-left). Mode persists across
27
- `--resume`.
20
+ The current mode renders as a badge on the **bottom-right of the input box**, diagonally opposite the session-name badge (top-left). Mode persists across `--resume`.
28
21
 
29
22
  ## How it works
30
23
 
31
- The extension tracks the active mode and the mode whose guidance was last
32
- injected. On `before_agent_start` it compares the two: if they differ, it
33
- prepends that mode's guidance to the turn (as a hidden `mode-switch-context`
34
- message) and records the injection. Stay in the same mode and nothing extra is
35
- added. Returning to `normal` also strips stale guidance from context.
24
+ The extension tracks the active mode and the mode whose guidance was last injected. On `before_agent_start` it compares the two: if they differ, it prepends that mode's guidance to the turn as a hidden `mode-switch-context` message and records the injection. Stay in the same mode and nothing extra is added. Returning to `normal` also strips stale guidance from context.
36
25
 
37
- The agent can also switch its own mode without a permanent tool. The bundled
38
- `bin/mode` CLI, run through the normal bash tool, writes the requested mode to
39
- `<artifact-root>/mode-switch/request.json` and prints that mode's operating
40
- instructions to stdout. The extension mirrors each mode's guidance to
41
- `mode-switch/guidance/<mode>.txt` at session start (single source of truth stays
42
- in the extension), watches the request file to keep the badge and session state in
43
- sync, and consumes each request on read so a later Alt+M isn't clobbered. The
44
- guidance reaches the agent through the command's own output, so it only enters
45
- context at the moment of the switch — no tool sits in the context window, and
46
- there's no coupling to any external runtime, just files.
26
+ The agent can also switch its own mode without a permanent tool. The bundled `bin/mode` CLI, run through the normal bash tool, writes the requested mode to `<artifact-root>/mode-switch/request.json` and prints that mode's operating instructions to stdout. The extension mirrors each mode's guidance to `mode-switch/guidance/<mode>.txt` at session start, watches the request file to keep the badge and session state in sync, and applies each tokenized request once. The guidance reaches the agent through the command's own output, so it only enters context at the moment of the switch — no tool sits in the context window, and there's no coupling to an external runtime beyond the shared files.
47
27
 
48
- This is guidance-only — it does not hard-restrict tools. The instructions tell
49
- the agent not to edit in spec/plan mode; combine with a tool gate if you want
50
- enforcement.
28
+ This is guidance-only — it does not hard-restrict tools. The instructions tell the agent not to edit in spec/plan mode; combine with a tool gate if you want enforcement.
51
29
 
52
- The input badge is drawn by wrapping whatever editor is already installed (e.g. a
53
- session-name badge extension) in a transparent Proxy that overrides only
54
- `render()`. It composes instead of replacing, so other editor decorations keep
55
- working.
30
+ The input badge is drawn by wrapping whatever editor is already installed (for example, a session-name badge extension) in a transparent Proxy that overrides only `render()`. It composes instead of replacing, so other editor decorations keep working.
56
31
 
57
32
  ## Install
58
33
 
59
- ```bash
60
- pi install /Users/silasrhyneer/Code/cli/pi-mode-switch # user scope
61
- pi install -l /Users/silasrhyneer/Code/cli/pi-mode-switch # project scope
62
- ```
34
+ This package ships inside crouter. `crtr sys setup` registers its built package path with pi; there is no separate repository or package to install.
63
35
 
64
- Run `/reload` in pi after install, then press Alt+M.
36
+ Run `/reload` in pi after setup. The extension resolves `crtr.mode.cycle` from the sparse `keybindings` object in `~/.crouter/config.json`; missing overrides use Alt+M, multiple gestures all register, and an empty list disables the shortcut. `/mode` remains available when the shortcut is disabled. Pi's stock actions and `~/.pi/agent/keybindings.json` remain untouched.
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Mode Switch Extension
3
3
  *
4
- * Press Alt+M to cycle between operating modes, the way Claude's Shift+Tab
5
- * cycles plan / accept-edits. Modes here:
4
+ * Use the configured crouter mode-cycle binding to cycle between operating
5
+ * modes, the way Claude's Shift+Tab cycles plan / accept-edits. Modes here:
6
6
  *
7
7
  * normal → full access (default)
8
8
  * spec → write a specification before any code
@@ -13,7 +13,7 @@
13
13
  * instructions are prepended (invisibly) to the next agent turn. Stay in the
14
14
  * same mode across turns and nothing extra is injected.
15
15
  *
16
- * - Alt+M or /mode to cycle; /mode <name> to jump to a specific mode
16
+ * - The configured shortcut or /mode cycles; /mode <name> jumps to a mode
17
17
  * - The agent can switch its own mode by running the bundled `bin/mode` CLI via
18
18
  * its bash tool; the command prints the new mode's guidance to stdout, so that
19
19
  * text only enters context at the moment of the switch (no always-on tool)
@@ -31,6 +31,7 @@ import { type FSWatcher, mkdirSync, readFileSync, rmSync, watch, writeFileSync }
31
31
  import { dirname, join } from "node:path";
32
32
  import { fileURLToPath } from "node:url";
33
33
  import { workspaceRoot } from "../../../core/artifact.js";
34
+ import { resolveUserKeybindings } from "../../../core/keybindings/index.js";
34
35
 
35
36
  type Mode = "normal" | "spec" | "plan";
36
37
 
@@ -62,7 +63,7 @@ function modeSwitchDir(cwd: string = process.cwd(), nodeId: string = process.env
62
63
  return join(workspaceRoot(cwd), "mode-switch", nodeId);
63
64
  }
64
65
 
65
- // The cycle order Alt+M walks through.
66
+ // The order used by the shortcut and /mode command.
66
67
  const CYCLE: Mode[] = ["normal", "spec", "plan"];
67
68
 
68
69
  interface ModeDef {
@@ -214,9 +215,8 @@ export default function modeSwitchExtension(pi: ExtensionAPI): void {
214
215
  }
215
216
 
216
217
  // Apply a mode change to state and persistence. Shared by every path: the
217
- // Alt+M / command path (which adds a UI toast and injects guidance on the
218
- // next turn) and the CLI path (which already handed the agent its guidance via
219
- // stdout).
218
+ // Shortcut/command changes add a UI toast and inject guidance on the next
219
+ // turn; the CLI path has already handed the agent its guidance via stdout.
220
220
  function applyMode(next: Mode): void {
221
221
  currentMode = next;
222
222
  repaint();
@@ -241,8 +241,8 @@ export default function modeSwitchExtension(pi: ExtensionAPI): void {
241
241
  // requested mode to <root>/mode-switch/request.json. We pick that up live via
242
242
  // fs.watch (with a before_agent_start reconcile as a backstop) only to sync the
243
243
  // badge and persisted state — never to inject guidance, since the CLI already
244
- // printed it to the agent's bash output. The request is consumed on read so a
245
- // later Alt+M can't be clobbered by a stale request.
244
+ // printed it to the agent's bash output. Token gating ensures a later
245
+ // shortcut-driven change can't be clobbered by a stale request.
246
246
  function applyFromRequest(next: Mode, source: "cli" | "attach"): void {
247
247
  if (next === currentMode) return;
248
248
  applyMode(next);
@@ -266,7 +266,7 @@ export default function modeSwitchExtension(pi: ExtensionAPI): void {
266
266
  // Apply each CLI request at most once (token-gated) and never delete it, so
267
267
  // every live instance — including the one actually on screen — converges to the
268
268
  // requested mode regardless of /reload leftovers. A lingering request whose
269
- // token we already applied can't clobber a later Alt+M.
269
+ // token we already applied can't clobber a later shortcut-driven change.
270
270
  if (parsed.token && parsed.token === lastAppliedToken) return;
271
271
  lastAppliedToken = parsed.token;
272
272
  if (parsed.mode && (CYCLE as string[]).includes(parsed.mode)) {
@@ -291,11 +291,16 @@ export default function modeSwitchExtension(pi: ExtensionAPI): void {
291
291
  setMode(CYCLE[(idx + 1) % CYCLE.length], ctx);
292
292
  }
293
293
 
294
- // Alt+M cycles modes.
295
- pi.registerShortcut("alt+m", {
296
- description: "Cycle mode (normal spec plan)",
297
- handler: async (ctx) => cycleMode(ctx),
298
- });
294
+ // Resolve once when the extension loads. Crouter's sparse user config owns
295
+ // this semantic action; pi's stock action bindings remain independently owned
296
+ // by pi and are neither read nor rewritten here.
297
+ const bindings = resolveUserKeybindings();
298
+ for (const gesture of bindings.gestures("crtr.mode.cycle")) {
299
+ pi.registerShortcut(gesture, {
300
+ description: "Cycle mode (normal → spec → plan)",
301
+ handler: async (ctx) => cycleMode(ctx),
302
+ });
303
+ }
299
304
 
300
305
  // /mode cycles; /mode <name> jumps to a specific mode.
301
306
  pi.registerCommand("mode", {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-mode-switch",
3
3
  "version": "0.1.0",
4
- "description": "Press Alt+M to cycle between spec, plan, and normal modes. Injects mode-specific operating guidance whenever the mode changes.",
4
+ "description": "Cycle between spec, plan, and normal modes with crouter's configurable mode binding. Injects mode-specific guidance whenever the mode changes.",
5
5
  "keywords": ["pi-package", "modes", "spec", "plan"],
6
6
  "license": "MIT",
7
7
  "peerDependencies": {
@@ -127,12 +127,12 @@ export function render(state, draw, content) {
127
127
  if (state.totalNodes === 0) {
128
128
  emptyState(draw, content, {
129
129
  headline: 'No nodes on the canvas',
130
- secondary: ['Spawn one with `crtr node new`.', 'Press g to refresh.'],
130
+ secondary: ['Spawn one with `crtr node new`.', 'Use refresh to update.'],
131
131
  });
132
132
  } else {
133
133
  emptyState(draw, content, {
134
134
  headline: 'All caught up',
135
- secondary: [`${state.totalNodes} node${state.totalNodes === 1 ? '' : 's'} finished — none active.`, 'Press g to refresh.'],
135
+ secondary: [`${state.totalNodes} node${state.totalNodes === 1 ? '' : 's'} finished — none active.`, 'Use refresh to update.'],
136
136
  });
137
137
  }
138
138
  return;
@@ -156,14 +156,13 @@ export function render(state, draw, content) {
156
156
  // ── keymap ───────────────────────────────────────────────────────────────
157
157
 
158
158
  /**
159
- * Read-only navigation: j/k move the cursor, g refreshes, q quits. No async
160
- * actions from input this is a monitor, not a controller. Footer hints come
161
- * from these bindings' `hint` fields (the single source of truth).
159
+ * Read-only navigation. No async actions from input this is a monitor, not a
160
+ * controller. Footer hints come from these bindings' `hint` fields.
162
161
  * @type {import('../../core/view/contract.js').KeyBinding<CanvasState>[]}
163
162
  */
164
163
  export const keymap = [
165
- { keys: ['j', 'down'], intent: 'cursorDown', hint: { keys: 'j/k', label: 'move' } },
166
- { keys: ['k', 'up'], intent: 'cursorUp' },
167
- { keys: ['g'], intent: 'refresh', hint: { keys: 'g', label: 'refresh' } },
168
- { keys: ['q'], intent: 'quit', hint: { keys: 'q', label: 'quit' } },
164
+ { bindingId: 'crtr.view.canvas.down', intent: 'cursorDown', hint: { label: 'move' } },
165
+ { bindingId: 'crtr.view.canvas.up', intent: 'cursorUp' },
166
+ { bindingId: 'crtr.view.canvas.refresh', intent: 'refresh', hint: { label: 'refresh' } },
167
+ { bindingId: 'crtr.view.canvas.quit', intent: 'quit', hint: { label: 'quit' } },
169
168
  ];
@@ -275,7 +275,7 @@ function renderTakeover(state, draw, content) {
275
275
  errorState(draw, content, {
276
276
  headline: 'Missing target',
277
277
  cause: 'run crtr view run chat --target <node-id>',
278
- hint: 'Press q to quit.',
278
+ hint: 'Use the view quit control.',
279
279
  });
280
280
  return;
281
281
  }
@@ -285,7 +285,7 @@ function renderTakeover(state, draw, content) {
285
285
  glyphFg: '33',
286
286
  headline: 'Node asleep',
287
287
  explanation: 'node is asleep / focus or revive it first',
288
- nextStep: 'Press r after revive.',
288
+ nextStep: 'Reconnect after revive.',
289
289
  });
290
290
  return;
291
291
  }
@@ -293,7 +293,7 @@ function renderTakeover(state, draw, content) {
293
293
  errorState(draw, content, {
294
294
  headline: 'Node not found',
295
295
  cause: 'check the target',
296
- hint: 'Press q to quit.',
296
+ hint: 'Use the view quit control.',
297
297
  });
298
298
  return;
299
299
  }
@@ -354,15 +354,15 @@ const canCompose = (state) => state.target != null && state.conn !== 'no-broker'
354
354
 
355
355
  /** @type {import('../../core/view/contract.js').KeyBinding<ChatState>[]} */
356
356
  export const keymap = [
357
- { capture: 'setDraft', when: canCompose, hint: { keys: 'type', label: 'compose' } },
358
- { keys: ['return', 'enter'], intent: 'submitDraft', when: canCompose, hint: { keys: 'enter', label: 'send' } },
359
- { keys: ['escape', 'esc'], intent: 'abort', when: isControllerStreaming },
360
- { keys: ['escape', 'esc'], intent: 'clearNotice', when: hasNotice, payload: (state) => state.notices[state.notices.length - 1]?.id },
361
- { keys: ['escape', 'esc'], intent: 'setDraft', when: hasDraft, payload: () => '' },
362
- { keys: ['r'], intent: 'reconnect', when: canCompose, hint: { keys: 'r', label: 'reconnect' } },
363
- { keys: ['c'], intent: 'requestControl', when: isObserver, hint: { keys: 'c', label: 'control' } },
364
- { keys: ['C', 'ctrl+r'], intent: 'releaseControl', when: isController, hint: { keys: 'C', label: 'yield' } },
365
- { keys: ['q'], intent: 'quit', hint: { keys: 'q', label: 'quit' } },
357
+ { capture: 'setDraft', when: canCompose, hint: { label: 'compose' } },
358
+ { bindingId: 'crtr.view.chat.submit', intent: 'submitDraft', when: canCompose, hint: { label: 'send' } },
359
+ { bindingId: 'crtr.view.chat.contextual-cancel', intent: 'abort', when: isControllerStreaming },
360
+ { bindingId: 'crtr.view.chat.contextual-cancel', intent: 'clearNotice', when: hasNotice, payload: (state) => state.notices[state.notices.length - 1]?.id },
361
+ { bindingId: 'crtr.view.chat.contextual-cancel', intent: 'setDraft', when: hasDraft, payload: () => '' },
362
+ { bindingId: 'crtr.view.chat.reconnect', intent: 'reconnect', when: canCompose, hint: { label: 'reconnect' } },
363
+ { bindingId: 'crtr.view.chat.request-control', intent: 'requestControl', when: isObserver, hint: { label: 'control' } },
364
+ { bindingId: 'crtr.view.chat.release-control', intent: 'releaseControl', when: isController, hint: { label: 'yield' } },
365
+ { bindingId: 'crtr.view.chat.quit', intent: 'quit', hint: { label: 'quit' } },
366
366
  ];
367
367
 
368
368
  export default { render, keymap };
@@ -275,7 +275,7 @@ export function render(state, draw, content) {
275
275
  if (g.files.length === 0 && state.prs.length === 0 && !state.prNote) {
276
276
  emptyState(draw, bodyRect, {
277
277
  headline: 'All clear',
278
- secondary: [`${g.branch} — clean working tree, no open PRs.`, 'Press g to refresh.'],
278
+ secondary: [`${g.branch} — clean working tree, no open PRs.`, 'Use refresh to update.'],
279
279
  });
280
280
  return;
281
281
  }
@@ -289,14 +289,13 @@ export function render(state, draw, content) {
289
289
  // ── keymap ───────────────────────────────────────────────────────────────
290
290
 
291
291
  /**
292
- * Read-only navigation: j/k move the cursor, g refreshes, q quits. No async
293
- * actions from input this is a monitor, not a controller. Footer hints come
294
- * from these bindings' `hint` fields (the single source of truth).
292
+ * Read-only navigation. No async actions from input this is a monitor, not a
293
+ * controller. Footer hints come from these bindings' `hint` fields.
295
294
  * @type {import('../../core/view/contract.js').KeyBinding<GitPrState>[]}
296
295
  */
297
296
  export const keymap = [
298
- { keys: ['j', 'down'], intent: 'cursorDown', hint: { keys: 'j/k', label: 'move' } },
299
- { keys: ['k', 'up'], intent: 'cursorUp' },
300
- { keys: ['g'], intent: 'refresh', hint: { keys: 'g', label: 'refresh' } },
301
- { keys: ['q'], intent: 'quit', hint: { keys: 'q', label: 'quit' } },
297
+ { bindingId: 'crtr.view.git-pr.down', intent: 'cursorDown', hint: { label: 'move' } },
298
+ { bindingId: 'crtr.view.git-pr.up', intent: 'cursorUp' },
299
+ { bindingId: 'crtr.view.git-pr.refresh', intent: 'refresh', hint: { label: 'refresh' } },
300
+ { bindingId: 'crtr.view.git-pr.quit', intent: 'quit', hint: { label: 'quit' } },
302
301
  ];
@@ -33,7 +33,7 @@ function renderGuided(draw, content, guide) {
33
33
  errorState(draw, content, {
34
34
  headline,
35
35
  cause: d.explanation,
36
- hint: d.nextStep || 'Press g to retry.',
36
+ hint: d.nextStep || 'Refresh to retry.',
37
37
  });
38
38
  return;
39
39
  }
@@ -149,11 +149,10 @@ function renderThreadBody(state, draw, rect, reactTarget) {
149
149
  }
150
150
  }
151
151
 
152
- /** The compose bar (reply mode): hairline + label + draft + cursor + hint. */
152
+ /** The compose bar (reply mode): hairline + label + draft + cursor. */
153
153
  function renderComposer(state, draw, right) {
154
- const hairRow = right.row + right.height - 3;
155
- const barRow = right.row + right.height - 2;
156
- const hintRow = right.row + right.height - 1;
154
+ const hairRow = right.row + right.height - 2;
155
+ const barRow = right.row + right.height - 1;
157
156
  draw.hline(hairRow, right.col, right.col + right.width);
158
157
  const label = '✎ Reply ';
159
158
  const labelW = Array.from(label).length;
@@ -166,18 +165,12 @@ function renderComposer(state, draw, right) {
166
165
  { text: shown },
167
166
  { text: '█', style: { fg: '33' } },
168
167
  ], right.width);
169
- draw.spans(hintRow, right.col, [
170
- { text: 'enter', style: { bold: true } }, { text: ' send', style: { dim: true } },
171
- { text: ' · ', style: { dim: true } },
172
- { text: 'esc', style: { bold: true } }, { text: ' cancel', style: { dim: true } },
173
- ], right.width);
174
168
  }
175
169
 
176
- /** The react picker bar (react mode): hairline + emoji chip row + hint. */
170
+ /** The react picker bar (react mode): hairline + emoji chip row. */
177
171
  function renderReactBar(state, draw, right) {
178
- const hairRow = right.row + right.height - 3;
179
- const barRow = right.row + right.height - 2;
180
- const hintRow = right.row + right.height - 1;
172
+ const hairRow = right.row + right.height - 2;
173
+ const barRow = right.row + right.height - 1;
181
174
  draw.hline(hairRow, right.col, right.col + right.width);
182
175
  /** @type {object[]} */
183
176
  const spans = [{ text: '☺ React ', style: { fg: '33', bold: true } }];
@@ -190,13 +183,6 @@ function renderReactBar(state, draw, right) {
190
183
  }
191
184
  });
192
185
  draw.spans(barRow, right.col, spans, right.width);
193
- draw.spans(hintRow, right.col, [
194
- { text: '←/→', style: { bold: true } }, { text: ' pick', style: { dim: true } },
195
- { text: ' · ', style: { dim: true } },
196
- { text: 'enter', style: { bold: true } }, { text: ' react', style: { dim: true } },
197
- { text: ' · ', style: { dim: true } },
198
- { text: 'esc', style: { bold: true } }, { text: ' cancel', style: { dim: true } },
199
- ], right.width);
200
186
  }
201
187
 
202
188
  /**
@@ -210,7 +196,7 @@ function renderDetail(state, draw, right) {
210
196
  centeredStack(draw, right, [
211
197
  [{ text: '✉ ', style: { dim: true } }, { text: 'No conversation open', style: { dim: true } }],
212
198
  [{ text: '' }],
213
- [{ text: 'Press ' }, { text: 'Enter', style: { bold: true } }, { text: ' to open a conversation' }],
199
+ [{ text: 'Open a conversation to view it.', style: { dim: true } }],
214
200
  ]);
215
201
  return;
216
202
  }
@@ -233,7 +219,7 @@ function renderDetail(state, draw, right) {
233
219
 
234
220
  const composing = state.mode === 'reply';
235
221
  const reacting = state.mode === 'react';
236
- const composerRows = composing || reacting ? 3 : 0;
222
+ const composerRows = composing || reacting ? 2 : 0;
237
223
  const bodyTop = right.row + headerRows + 1;
238
224
  const bodyBottom = right.row + right.height - 1 - composerRows;
239
225
  const bodyRect = { row: bodyTop, col: right.col, width: right.width, height: Math.max(0, bodyBottom - bodyTop + 1) };
@@ -269,7 +255,7 @@ export function render(state, draw, content) {
269
255
  }
270
256
  emptyState(draw, content, {
271
257
  headline: 'All caught up',
272
- secondary: ['No messages in your inbox.', 'Press g to refresh.'],
258
+ secondary: ['No messages in your inbox.', 'Refresh to check again.'],
273
259
  });
274
260
  return;
275
261
  }
@@ -290,23 +276,23 @@ const reactMode = (s) => s.mode === 'react';
290
276
  /** @type {import('../../core/view/contract.js').KeyBinding<InboxState>[]} */
291
277
  export const keymap = [
292
278
  // list
293
- { keys: ['j', 'down'], intent: 'cursorDown', when: listMode, hint: { keys: 'j/k', label: 'move' } },
294
- { keys: ['k', 'up'], intent: 'cursorUp', when: listMode },
295
- { keys: ['return'], intent: 'open', payload: (s) => s.cursor, when: listMode, hint: { keys: 'enter', label: 'open' } },
296
- { keys: ['r'], intent: 'startReply', when: listMode, hint: { keys: 'r', label: 'reply' } },
297
- { keys: ['e'], intent: 'startReact', when: listMode, hint: { keys: 'e', label: 'react' } },
298
- { keys: ['f'], intent: 'cycleFilter', when: listMode, hint: { keys: 'f', label: 'filter' } },
299
- { keys: ['g'], intent: 'refresh', when: listMode, hint: { keys: 'g', label: 'refresh' } },
300
- { keys: ['L'], intent: 'connectLinkedin', when: listMode },
301
- { keys: ['G'], intent: 'connectGmail', when: listMode, hint: { keys: 'G', label: 'connect' } },
302
- { keys: ['q'], intent: 'quit', when: listMode, hint: { keys: 'q', label: 'quit' } },
279
+ { bindingId: 'crtr.view.inbox.down', intent: 'cursorDown', when: listMode, hint: { label: 'move' } },
280
+ { bindingId: 'crtr.view.inbox.up', intent: 'cursorUp', when: listMode },
281
+ { bindingId: 'crtr.view.inbox.open', intent: 'open', payload: (s) => s.cursor, when: listMode, hint: { label: 'open' } },
282
+ { bindingId: 'crtr.view.inbox.reply', intent: 'startReply', when: listMode, hint: { label: 'reply' } },
283
+ { bindingId: 'crtr.view.inbox.react', intent: 'startReact', when: listMode, hint: { label: 'react' } },
284
+ { bindingId: 'crtr.view.inbox.filter', intent: 'cycleFilter', when: listMode, hint: { label: 'filter' } },
285
+ { bindingId: 'crtr.view.inbox.refresh', intent: 'refresh', when: listMode, hint: { label: 'refresh' } },
286
+ { bindingId: 'crtr.view.inbox.connect-linkedin', intent: 'connectLinkedin', when: listMode },
287
+ { bindingId: 'crtr.view.inbox.connect-gmail', intent: 'connectGmail', when: listMode, hint: { label: 'connect' } },
288
+ { bindingId: 'crtr.view.inbox.quit', intent: 'quit', when: listMode, hint: { label: 'quit' } },
303
289
  // reply
304
- { capture: 'setDraft', when: replyMode, hint: { keys: 'type', label: 'reply' } },
305
- { keys: ['return'], intent: 'submitReply', when: replyMode },
306
- { keys: ['escape'], intent: 'cancelCompose', when: replyMode },
290
+ { capture: 'setDraft', when: replyMode, hint: { label: 'reply' } },
291
+ { bindingId: 'crtr.view.inbox.submit-reply', intent: 'submitReply', when: replyMode, hint: { label: 'send' } },
292
+ { bindingId: 'crtr.view.inbox.cancel', intent: 'cancelCompose', when: replyMode, hint: { label: 'cancel' } },
307
293
  // react
308
- { keys: ['h', 'left'], intent: 'reactPrev', when: reactMode },
309
- { keys: ['l', 'right'], intent: 'reactNext', when: reactMode },
310
- { keys: ['return'], intent: 'submitReact', when: reactMode },
311
- { keys: ['escape'], intent: 'cancelCompose', when: reactMode },
294
+ { bindingId: 'crtr.view.inbox.reaction-previous', intent: 'reactPrev', when: reactMode, hint: { label: 'pick' } },
295
+ { bindingId: 'crtr.view.inbox.reaction-next', intent: 'reactNext', when: reactMode },
296
+ { bindingId: 'crtr.view.inbox.submit-reaction', intent: 'submitReact', when: reactMode, hint: { label: 'react' } },
297
+ { bindingId: 'crtr.view.inbox.cancel', intent: 'cancelCompose', when: reactMode, hint: { label: 'cancel' } },
312
298
  ];
@@ -274,9 +274,8 @@ function renderThreadBody(state, draw, rect, reactTarget) {
274
274
 
275
275
  /** The compose bar (reply mode). @param {LiState} state @param {Draw} draw @param {Rect} right */
276
276
  function renderComposer(state, draw, right) {
277
- const hairRow = right.row + right.height - 3;
278
- const barRow = right.row + right.height - 2;
279
- const hintRow = right.row + right.height - 1;
277
+ const hairRow = right.row + right.height - 2;
278
+ const barRow = right.row + right.height - 1;
280
279
  draw.hline(hairRow, right.col, right.col + right.width);
281
280
  const label = '✎ Reply ';
282
281
  const labelW = Array.from(label).length;
@@ -289,18 +288,12 @@ function renderComposer(state, draw, right) {
289
288
  { text: shown },
290
289
  { text: '█', style: { fg: '33' } },
291
290
  ], right.width);
292
- draw.spans(hintRow, right.col, [
293
- { text: 'enter', style: { bold: true } }, { text: ' send', style: { dim: true } },
294
- { text: ' · ', style: { dim: true } },
295
- { text: 'esc', style: { bold: true } }, { text: ' cancel', style: { dim: true } },
296
- ], right.width);
297
291
  }
298
292
 
299
293
  /** The react picker bar (react mode). @param {LiState} state @param {Draw} draw @param {Rect} right */
300
294
  function renderReactBar(state, draw, right) {
301
- const hairRow = right.row + right.height - 3;
302
- const barRow = right.row + right.height - 2;
303
- const hintRow = right.row + right.height - 1;
295
+ const hairRow = right.row + right.height - 2;
296
+ const barRow = right.row + right.height - 1;
304
297
  draw.hline(hairRow, right.col, right.col + right.width);
305
298
  /** @type {Span[]} */
306
299
  const spans = [{ text: '☺ React ', style: { fg: '33', bold: true } }];
@@ -313,13 +306,6 @@ function renderReactBar(state, draw, right) {
313
306
  }
314
307
  });
315
308
  draw.spans(barRow, right.col, spans, right.width);
316
- draw.spans(hintRow, right.col, [
317
- { text: '←/→', style: { bold: true } }, { text: ' pick', style: { dim: true } },
318
- { text: ' · ', style: { dim: true } },
319
- { text: 'enter', style: { bold: true } }, { text: ' react', style: { dim: true } },
320
- { text: ' · ', style: { dim: true } },
321
- { text: 'esc', style: { bold: true } }, { text: ' cancel', style: { dim: true } },
322
- ], right.width);
323
309
  }
324
310
 
325
311
  /** Right pane — the open thread + the compose/react bar when in a mode. @param {LiState} state @param {Draw} draw @param {Rect} right */
@@ -331,7 +317,7 @@ function renderThread(state, draw, right) {
331
317
  centeredStack(draw, right, [
332
318
  [{ text: '✉ ', style: { dim: true } }, { text: 'No conversation open', style: { dim: true } }],
333
319
  [{ text: '' }],
334
- [{ text: 'Press ' }, { text: 'Enter', style: { bold: true } }, { text: ' to open a conversation' }],
320
+ [{ text: 'Open a conversation to view it.', style: { dim: true } }],
335
321
  ]);
336
322
  return;
337
323
  }
@@ -345,7 +331,7 @@ function renderThread(state, draw, right) {
345
331
 
346
332
  const composing = state.mode === 'reply';
347
333
  const reacting = state.mode === 'react';
348
- const composerRows = composing || reacting ? 3 : 0;
334
+ const composerRows = composing || reacting ? 2 : 0;
349
335
  const bodyTop = right.row + 2;
350
336
  const bodyBottom = right.row + right.height - 1 - composerRows;
351
337
  const bodyRect = { row: bodyTop, col: right.col, width: right.width, height: Math.max(0, bodyBottom - bodyTop + 1) };
@@ -380,7 +366,7 @@ export function render(state, draw, content) {
380
366
  }
381
367
  emptyState(draw, content, {
382
368
  headline: 'All caught up',
383
- secondary: ['No conversations in your inbox.', 'Press g to refresh.'],
369
+ secondary: ['No conversations in your inbox.', 'Refresh to check again.'],
384
370
  });
385
371
  return;
386
372
  }
@@ -403,25 +389,25 @@ const isComposeOrReact = (s) => s.mode === 'reply' || s.mode === 'react';
403
389
  /**
404
390
  * Input→intent map. List-mode navigation + entry into compose/react; the compose
405
391
  * bar uses a `capture` binding (the host's line-editor dispatches setDraft with
406
- * the next draft); react uses h/l + arrows. Footer hints come from these `hint`
407
- * fields (single source of truth).
392
+ * the next draft). Footer hints come from these `hint` fields (single source of
393
+ * truth).
408
394
  * @type {import('../../core/view/contract.js').KeyBinding<LiState>[]}
409
395
  */
410
396
  export const keymap = [
411
397
  // ── List mode ──
412
- { keys: ['j', 'down'], intent: 'cursorDown', when: isList, hint: { keys: 'j/k', label: 'move' } },
413
- { keys: ['k', 'up'], intent: 'cursorUp', when: isList },
414
- { keys: ['enter', 'return'], intent: 'openThread', payload: (s) => s.convCursor, when: isList, hint: { keys: 'enter', label: 'open' } },
415
- { keys: ['r'], intent: 'startReply', when: isList, hint: { keys: 'r', label: 'reply' } },
416
- { keys: ['e'], intent: 'startReact', when: isList, hint: { keys: 'e', label: 'react' } },
417
- { keys: ['g'], intent: 'refresh', when: isList, hint: { keys: 'g', label: 'refresh' } },
418
- { keys: ['q'], intent: 'quit', when: isList, hint: { keys: 'q', label: 'quit' } },
419
- // ── Reply mode ── (capture → setDraft; enter sends; esc cancels)
420
- { capture: 'setDraft', when: isReply },
421
- { keys: ['enter', 'return'], intent: 'submitReply', when: isReply },
422
- { keys: ['escape', 'esc'], intent: 'cancelCompose', when: isComposeOrReact },
398
+ { bindingId: 'crtr.view.linkedin.down', intent: 'cursorDown', when: isList, hint: { label: 'move' } },
399
+ { bindingId: 'crtr.view.linkedin.up', intent: 'cursorUp', when: isList },
400
+ { bindingId: 'crtr.view.linkedin.open', intent: 'openThread', payload: (s) => s.convCursor, when: isList, hint: { label: 'open' } },
401
+ { bindingId: 'crtr.view.linkedin.reply', intent: 'startReply', when: isList, hint: { label: 'reply' } },
402
+ { bindingId: 'crtr.view.linkedin.react', intent: 'startReact', when: isList, hint: { label: 'react' } },
403
+ { bindingId: 'crtr.view.linkedin.refresh', intent: 'refresh', when: isList, hint: { label: 'refresh' } },
404
+ { bindingId: 'crtr.view.linkedin.quit', intent: 'quit', when: isList, hint: { label: 'quit' } },
405
+ // ── Reply mode ──
406
+ { capture: 'setDraft', when: isReply, hint: { label: 'reply' } },
407
+ { bindingId: 'crtr.view.linkedin.submit-reply', intent: 'submitReply', when: isReply, hint: { label: 'send' } },
408
+ { bindingId: 'crtr.view.linkedin.cancel', intent: 'cancelCompose', when: isComposeOrReact, hint: { label: 'cancel' } },
423
409
  // ── React mode ──
424
- { keys: ['left', 'h'], intent: 'reactPrev', when: isReact },
425
- { keys: ['right', 'l'], intent: 'reactNext', when: isReact },
426
- { keys: ['enter', 'return'], intent: 'submitReact', when: isReact },
410
+ { bindingId: 'crtr.view.linkedin.reaction-previous', intent: 'reactPrev', when: isReact, hint: { label: 'pick' } },
411
+ { bindingId: 'crtr.view.linkedin.reaction-next', intent: 'reactNext', when: isReact },
412
+ { bindingId: 'crtr.view.linkedin.submit-reaction', intent: 'submitReact', when: isReact, hint: { label: 'react' } },
427
413
  ];
@@ -107,14 +107,14 @@ export function render(state, draw, content) {
107
107
  return;
108
108
  }
109
109
  if (state.review.length === 0) {
110
- emptyState(draw, content, { headline: 'No prompt layers', secondary: ['Press g to refresh.'] });
110
+ emptyState(draw, content, { headline: 'No prompt layers', secondary: ['Refresh to try again.'] });
111
111
  return;
112
112
  }
113
113
 
114
114
  const headerRows = 3;
115
115
  draw.text(content.row, content.col, 'Prompt Studio', { bold: true });
116
116
  draw.text(content.row + 1, content.col, configHeadline(state.config), { dim: true });
117
- draw.text(content.row + 2, content.col, `j/k layer · h/l compare · tab raw/layered · e export · g refresh${state.lastExportPath ? ` · ${state.lastExportPath}` : ''}`, { dim: true });
117
+ draw.text(content.row + 2, content.col, `Layer navigation · comparison · raw/layered · export · refresh${state.lastExportPath ? ` · ${state.lastExportPath}` : ''}`, { dim: true });
118
118
 
119
119
  const body = { row: content.row + headerRows + 1, col: content.col, width: content.width, height: Math.max(0, content.height - headerRows - 1) };
120
120
  const cols = draw.columns(body, [3, 5, 4]);
@@ -184,13 +184,13 @@ export function render(state, draw, content) {
184
184
  }
185
185
 
186
186
  export const keymap = [
187
- { keys: ['j', 'down'], intent: 'cursorDown', hint: { keys: 'j/k', label: 'layer' } },
188
- { keys: ['k', 'up'], intent: 'cursorUp' },
189
- { keys: ['h', 'left'], intent: 'compareCursorUp', hint: { keys: 'h/l', label: 'compare' } },
190
- { keys: ['l', 'right'], intent: 'compareCursorDown' },
191
- { keys: ['tab'], intent: 'setViewMode', payload: (s) => (s.viewMode === 'raw' ? 'layered' : 'raw'), hint: { keys: 'tab', label: 'raw/layered' } },
192
- { keys: ['enter', 'return'], intent: 'toggleLayerExpanded', payload: (s) => s.selectedLayerId || (s.review && s.review[s.cursor] && s.review[s.cursor].id) || '', hint: { keys: 'enter', label: 'expand' } },
193
- { keys: ['e'], intent: 'exportComments', hint: { keys: 'e', label: 'export' } },
194
- { keys: ['g'], intent: 'refresh', hint: { keys: 'g', label: 'refresh' } },
195
- { keys: ['q'], intent: 'quit', hint: { keys: 'q', label: 'quit' } },
187
+ { bindingId: 'crtr.view.prompt-review.down', intent: 'cursorDown', hint: { label: 'layer' } },
188
+ { bindingId: 'crtr.view.prompt-review.up', intent: 'cursorUp' },
189
+ { bindingId: 'crtr.view.prompt-review.compare-previous', intent: 'compareCursorUp', hint: { label: 'compare' } },
190
+ { bindingId: 'crtr.view.prompt-review.compare-next', intent: 'compareCursorDown' },
191
+ { bindingId: 'crtr.view.prompt-review.toggle-mode', intent: 'setViewMode', payload: (s) => (s.viewMode === 'raw' ? 'layered' : 'raw'), hint: { label: 'raw/layered' } },
192
+ { bindingId: 'crtr.view.prompt-review.expand', intent: 'toggleLayerExpanded', payload: (s) => s.selectedLayerId || (s.review && s.review[s.cursor] && s.review[s.cursor].id) || '', hint: { label: 'expand' } },
193
+ { bindingId: 'crtr.view.prompt-review.export', intent: 'exportComments', hint: { label: 'export' } },
194
+ { bindingId: 'crtr.view.prompt-review.refresh', intent: 'refresh', hint: { label: 'refresh' } },
195
+ { bindingId: 'crtr.view.prompt-review.quit', intent: 'quit', hint: { label: 'quit' } },
196
196
  ];