@crouton-kit/crouter 0.3.70 → 0.3.79

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 (193) hide show
  1. package/README.md +1 -1
  2. package/dist/build-root.d.ts +12 -4
  3. package/dist/build-root.js +25 -6
  4. package/dist/builtin-memory/00-runtime-base.md +3 -10
  5. package/dist/builtin-memory/04-base-worker.md +18 -0
  6. package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
  7. package/dist/builtin-memory/crouter-development/plugins.md +82 -5
  8. package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +13 -34
  9. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +1115 -1
  10. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +234 -71
  11. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +2 -9
  12. package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +2 -516
  13. package/dist/builtin-pi-packages/pi-crtr-extensions/package-lock.json +2 -2
  14. package/dist/builtin-pi-packages/pi-mode-switch/README.md +11 -39
  15. package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +20 -15
  16. package/dist/builtin-pi-packages/pi-mode-switch/package.json +1 -1
  17. package/dist/builtin-views/canvas/tui.mjs +8 -9
  18. package/dist/builtin-views/chat/tui.mjs +12 -12
  19. package/dist/builtin-views/git-pr/tui.mjs +7 -8
  20. package/dist/builtin-views/inbox/tui.mjs +27 -41
  21. package/dist/builtin-views/linkedin/tui.mjs +23 -37
  22. package/dist/builtin-views/prompt-review/tui.mjs +11 -11
  23. package/dist/builtin-views/settings/tui.mjs +11 -11
  24. package/dist/builtin-views/workspace-sidebar/tui.mjs +8 -9
  25. package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +11 -6
  26. package/dist/clients/attach/__tests__/attach-keybindings.test.d.ts +1 -0
  27. package/dist/clients/attach/__tests__/attach-keybindings.test.js +113 -0
  28. package/dist/clients/attach/__tests__/crtr-output-render.test.js +1 -1
  29. package/dist/clients/attach/__tests__/mermaid-render.test.d.ts +1 -0
  30. package/dist/clients/attach/__tests__/mermaid-render.test.js +28 -0
  31. package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.d.ts +1 -0
  32. package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.js +110 -0
  33. package/dist/clients/attach/attach-cmd.d.ts +9 -1
  34. package/dist/clients/attach/attach-cmd.js +841 -792
  35. package/dist/clients/attach/auth-pickers.d.ts +0 -12
  36. package/dist/clients/attach/auth-pickers.js +64 -15
  37. package/dist/clients/attach/canvas-panels.js +2 -3
  38. package/dist/clients/attach/chat-view.d.ts +7 -8
  39. package/dist/clients/attach/chat-view.js +119 -79
  40. package/dist/clients/attach/edit-diff-render.d.ts +6 -0
  41. package/dist/clients/attach/edit-diff-render.js +263 -0
  42. package/dist/clients/attach/graph-overlay.d.ts +12 -2
  43. package/dist/clients/attach/graph-overlay.js +83 -33
  44. package/dist/clients/attach/input-controller.d.ts +28 -2
  45. package/dist/clients/attach/input-controller.js +38 -5
  46. package/dist/clients/attach/mermaid-render.js +5 -2
  47. package/dist/clients/attach/pickers.d.ts +8 -7
  48. package/dist/clients/attach/pickers.js +11 -17
  49. package/dist/clients/attach/slash-commands.d.ts +9 -0
  50. package/dist/clients/attach/slash-commands.js +127 -7
  51. package/dist/clients/attach/titled-editor.d.ts +12 -1
  52. package/dist/clients/attach/titled-editor.js +103 -8
  53. package/dist/commands/canvas-browse.js +2 -2
  54. package/dist/commands/human/queue.js +3 -4
  55. package/dist/commands/memory/lint.js +39 -5
  56. package/dist/commands/memory/write.js +1 -0
  57. package/dist/commands/node.js +9 -2
  58. package/dist/commands/pkg/plugin-inspect.js +22 -1
  59. package/dist/commands/pkg/plugin-manage.js +31 -9
  60. package/dist/commands/surface-tmux-spread.js +1 -3
  61. package/dist/commands/sys/__tests__/config-keybindings.test.d.ts +1 -0
  62. package/dist/commands/sys/__tests__/config-keybindings.test.js +55 -0
  63. package/dist/commands/sys/__tests__/config-model-ladders.test.d.ts +1 -0
  64. package/dist/commands/sys/__tests__/config-model-ladders.test.js +121 -0
  65. package/dist/commands/sys/__tests__/setup-core.test.js +158 -1
  66. package/dist/commands/sys/config.js +18 -21
  67. package/dist/commands/sys/doctor.js +42 -4
  68. package/dist/commands/sys/setup-core.d.ts +49 -1
  69. package/dist/commands/sys/setup-core.js +161 -4
  70. package/dist/commands/sys/setup.d.ts +88 -0
  71. package/dist/commands/sys/setup.js +943 -169
  72. package/dist/commands/view-pick.d.ts +4 -0
  73. package/dist/commands/view-pick.js +17 -7
  74. package/dist/core/__tests__/base-worker-prompt.test.d.ts +1 -0
  75. package/dist/core/__tests__/base-worker-prompt.test.js +24 -0
  76. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +232 -1
  77. package/dist/core/__tests__/canvas-inbox-watcher.test.js +34 -9
  78. package/dist/core/__tests__/command-plugins-surfaces.test.d.ts +1 -0
  79. package/dist/core/__tests__/command-plugins-surfaces.test.js +298 -0
  80. package/dist/core/__tests__/command-plugins.test.d.ts +1 -0
  81. package/dist/core/__tests__/command-plugins.test.js +444 -0
  82. package/dist/core/__tests__/fault-classifier.test.js +15 -0
  83. package/dist/core/__tests__/fixtures/fake-engine.d.ts +6 -0
  84. package/dist/core/__tests__/fixtures/fake-engine.js +9 -1
  85. package/dist/core/__tests__/full/broker-dialogs.test.js +7 -2
  86. package/dist/core/__tests__/host-teardown-process-group.test.js +15 -4
  87. package/dist/core/__tests__/preview-registry-sync.test.js +30 -1
  88. package/dist/core/__tests__/scope-crouter-home-fence.test.d.ts +1 -0
  89. package/dist/core/__tests__/scope-crouter-home-fence.test.js +55 -0
  90. package/dist/core/__tests__/stream-watchdog.test.d.ts +1 -0
  91. package/dist/core/__tests__/stream-watchdog.test.js +70 -0
  92. package/dist/core/__tests__/tmux-surface.test.js +72 -0
  93. package/dist/core/canvas/browse/__tests__/model.test.js +23 -5
  94. package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
  95. package/dist/core/canvas/browse/app.d.ts +6 -0
  96. package/dist/core/canvas/browse/app.js +230 -41
  97. package/dist/core/canvas/browse/model.d.ts +10 -6
  98. package/dist/core/canvas/browse/model.js +20 -12
  99. package/dist/core/canvas/browse/pins.d.ts +4 -0
  100. package/dist/core/canvas/browse/pins.js +29 -0
  101. package/dist/core/canvas/browse/render.d.ts +13 -1
  102. package/dist/core/canvas/browse/render.js +72 -15
  103. package/dist/core/canvas/nav-model.js +5 -11
  104. package/dist/core/canvas/paths.d.ts +5 -0
  105. package/dist/core/canvas/paths.js +7 -0
  106. package/dist/core/command-plugins/adapter.d.ts +15 -0
  107. package/dist/core/command-plugins/adapter.js +145 -0
  108. package/dist/core/command-plugins/compose.d.ts +5 -0
  109. package/dist/core/command-plugins/compose.js +56 -0
  110. package/dist/core/command-plugins/discovery.d.ts +104 -0
  111. package/dist/core/command-plugins/discovery.js +565 -0
  112. package/dist/core/config.d.ts +48 -0
  113. package/dist/core/config.js +228 -110
  114. package/dist/core/fault-classifier.js +1 -1
  115. package/dist/core/fs-utils.d.ts +1 -0
  116. package/dist/core/fs-utils.js +15 -1
  117. package/dist/core/keybindings/__tests__/bespoke-consumers.test.d.ts +1 -0
  118. package/dist/core/keybindings/__tests__/bespoke-consumers.test.js +40 -0
  119. package/dist/core/keybindings/__tests__/resolve.test.d.ts +1 -0
  120. package/dist/core/keybindings/__tests__/resolve.test.js +224 -0
  121. package/dist/core/keybindings/catalog.d.ts +14 -0
  122. package/dist/core/keybindings/catalog.js +257 -0
  123. package/dist/core/keybindings/index.d.ts +5 -0
  124. package/dist/core/keybindings/index.js +4 -0
  125. package/dist/core/keybindings/match.d.ts +29 -0
  126. package/dist/core/keybindings/match.js +99 -0
  127. package/dist/core/keybindings/persistence.d.ts +14 -0
  128. package/dist/core/keybindings/persistence.js +31 -0
  129. package/dist/core/keybindings/resolve.d.ts +14 -0
  130. package/dist/core/keybindings/resolve.js +305 -0
  131. package/dist/core/keybindings/types.d.ts +37 -0
  132. package/dist/core/keybindings/types.js +1 -0
  133. package/dist/core/memory-resolver.d.ts +1 -1
  134. package/dist/core/memory-resolver.js +10 -3
  135. package/dist/core/predicate.d.ts +5 -3
  136. package/dist/core/predicate.js +5 -3
  137. package/dist/core/profiles/select.d.ts +6 -0
  138. package/dist/core/profiles/select.js +86 -52
  139. package/dist/core/provider-management.d.ts +12 -0
  140. package/dist/core/provider-management.js +24 -0
  141. package/dist/core/runtime/broker-protocol.d.ts +33 -5
  142. package/dist/core/runtime/broker.js +164 -14
  143. package/dist/core/runtime/launch.d.ts +39 -6
  144. package/dist/core/runtime/launch.js +78 -20
  145. package/dist/core/runtime/naming.js +3 -3
  146. package/dist/core/runtime/pi-cli.d.ts +6 -0
  147. package/dist/core/runtime/pi-cli.js +16 -2
  148. package/dist/core/runtime/pi-vendored.d.ts +8 -0
  149. package/dist/core/runtime/pi-vendored.js +14 -0
  150. package/dist/core/runtime/placement.d.ts +2 -2
  151. package/dist/core/runtime/placement.js +4 -1
  152. package/dist/core/runtime/promote.js +4 -0
  153. package/dist/core/runtime/recap.d.ts +3 -3
  154. package/dist/core/runtime/recap.js +75 -47
  155. package/dist/core/runtime/recycle.js +6 -1
  156. package/dist/core/runtime/reset.js +5 -0
  157. package/dist/core/runtime/session-list-cache.d.ts +33 -0
  158. package/dist/core/runtime/session-list-cache.js +338 -0
  159. package/dist/core/runtime/spawn.js +52 -45
  160. package/dist/core/runtime/stream-watchdog.d.ts +26 -0
  161. package/dist/core/runtime/stream-watchdog.js +75 -0
  162. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  163. package/dist/core/runtime/tmux-chrome.js +2 -2
  164. package/dist/core/runtime/tmux.d.ts +18 -15
  165. package/dist/core/runtime/tmux.js +217 -110
  166. package/dist/core/scope.js +27 -4
  167. package/dist/core/subscription-state.d.ts +90 -0
  168. package/dist/core/subscription-state.js +762 -0
  169. package/dist/core/tui/__tests__/host-keybindings.test.d.ts +1 -0
  170. package/dist/core/tui/__tests__/host-keybindings.test.js +112 -0
  171. package/dist/core/tui/host.d.ts +30 -1
  172. package/dist/core/tui/host.js +148 -34
  173. package/dist/core/view/contract.d.ts +17 -3
  174. package/dist/daemon/crtrd.js +253 -12
  175. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +41 -0
  176. package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.d.ts +1 -0
  177. package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +121 -0
  178. package/dist/pi-extensions/canvas-inbox-watcher.d.ts +1 -1
  179. package/dist/pi-extensions/canvas-inbox-watcher.js +203 -25
  180. package/dist/pi-extensions/canvas-recap.d.ts +4 -0
  181. package/dist/pi-extensions/canvas-recap.js +73 -46
  182. package/dist/pi-extensions/canvas-stophook.d.ts +11 -0
  183. package/dist/pi-extensions/canvas-stophook.js +33 -4
  184. package/dist/types.d.ts +9 -26
  185. package/dist/types.js +1 -28
  186. package/dist/web-client/assets/{index-DiFuLcp6.js → index--md2ylfi.js} +1 -1
  187. package/dist/web-client/index.html +1 -1
  188. package/dist/web-client/sw.js +1 -1
  189. package/package.json +8 -4
  190. package/dist/pi-extensions/canvas-nav.d.ts +0 -43
  191. package/dist/pi-extensions/canvas-nav.js +0 -640
  192. package/dist/pi-extensions/widget-order-bus.d.ts +0 -6
  193. package/dist/pi-extensions/widget-order-bus.js +0 -34
@@ -15,6 +15,37 @@ import { emitKeypressEvents } from 'node:readline';
15
15
  import { listProfiles, loadProfileManifest, updateProfileLastUsed, createProfile, addProfileProject, ensureRootProfile, ROOT_PROFILE_ID, } from './manifest.js';
16
16
  import { getDefaultProfileId, setDefaultProfileId, clearDefaultProfile, } from './default-binding.js';
17
17
  import { stdoutColor } from '../output.js';
18
+ import { formatBinding, matchesTerminalInput, resolveUserKeybindings, } from '../keybindings/index.js';
19
+ const PROFILE_MENU_ACTIONS = [
20
+ 'crtr.profile.menu.abort', 'crtr.profile.menu.up', 'crtr.profile.menu.down',
21
+ 'crtr.profile.menu.select', 'crtr.profile.menu.accept-default',
22
+ 'crtr.profile.menu.slot.1', 'crtr.profile.menu.slot.2', 'crtr.profile.menu.slot.3',
23
+ 'crtr.profile.menu.slot.4', 'crtr.profile.menu.slot.5', 'crtr.profile.menu.slot.6',
24
+ 'crtr.profile.menu.slot.7', 'crtr.profile.menu.slot.8', 'crtr.profile.menu.slot.9',
25
+ 'crtr.profile.menu.create', 'crtr.profile.menu.search-existing', 'crtr.profile.menu.use-root',
26
+ 'crtr.profile.menu.set-default', 'crtr.profile.menu.add-directory', 'crtr.profile.menu.unpin',
27
+ ];
28
+ const PROFILE_SEARCH_ACTIONS = [
29
+ 'crtr.profile.search.abort', 'crtr.profile.search.back', 'crtr.profile.search.pick',
30
+ 'crtr.profile.search.up', 'crtr.profile.search.down', 'crtr.profile.search.delete',
31
+ ];
32
+ function profileRawInput(str, k) {
33
+ return {
34
+ input: k.ctrl === true || k.meta === true ? (k.name ?? str) : str,
35
+ upArrow: k.name === 'up', downArrow: k.name === 'down',
36
+ return: k.name === 'return' || k.name === 'enter', escape: k.name === 'escape',
37
+ backspace: k.name === 'backspace', ctrl: k.ctrl, meta: k.meta, shift: k.shift,
38
+ };
39
+ }
40
+ function firstProfileMatch(bindings, ids, raw) {
41
+ return ids.find((id) => matchesTerminalInput(bindings, id, raw)) ?? null;
42
+ }
43
+ export function resolveProfileMenuAction(bindings, raw) {
44
+ return firstProfileMatch(bindings, PROFILE_MENU_ACTIONS, raw);
45
+ }
46
+ export function resolveProfileSearchAction(bindings, raw) {
47
+ return firstProfileMatch(bindings, PROFILE_SEARCH_ACTIONS, raw);
48
+ }
18
49
  /** Resolve cwd to an absolute, realpath'd form so it compares against the
19
50
  * realpath'd project dirs `createProfile`/`addProfileProject` already store
20
51
  * (manifest.ts resolves every project dir through `realpathSync`). Falls back
@@ -154,14 +185,17 @@ function blockRows(block) {
154
185
  }
155
186
  return newlines + extra;
156
187
  }
157
- function renderMenu(title, subtitle, rows, sel, hint) {
188
+ function renderMenu(bindings, title, subtitle, rows, sel, hint) {
158
189
  let out = `\n ${bold(title)}\n`;
159
190
  if (subtitle !== undefined)
160
191
  out += ` ${dim(subtitle)}\n`;
161
192
  out += '\n';
162
193
  let num = 0;
163
194
  for (const [i, r] of rows.entries()) {
164
- const tag = r.hotkey ?? String(++num);
195
+ const slot = r.bindingId === undefined ? ++num : 0;
196
+ const tag = r.bindingId !== undefined
197
+ ? formatBinding(bindings, r.bindingId)
198
+ : slot <= 9 ? formatBinding(bindings, `crtr.profile.menu.slot.${slot}`) : '';
165
199
  const mark = i === sel ? HILITE : ' ';
166
200
  const det = r.detail !== '' ? ' ' + dim(r.detail) : '';
167
201
  const def = r.isDefault ? ' ' + accent('\u00b7 ' + (r.defaultText ?? 'default')) : '';
@@ -169,23 +203,22 @@ function renderMenu(title, subtitle, rows, sel, hint) {
169
203
  }
170
204
  if (hint !== undefined)
171
205
  out += `\n ${dim(hint)}\n`;
172
- out += `\n ${dim('\u2191\u2193/jk move \u21b5 select esc default ^C quit')}\n`;
206
+ out += `\n ${dim(`${formatBinding(bindings, 'crtr.profile.menu.up')}/${formatBinding(bindings, 'crtr.profile.menu.down')} move ${formatBinding(bindings, 'crtr.profile.menu.select')} select ${formatBinding(bindings, 'crtr.profile.menu.accept-default')} default ${formatBinding(bindings, 'crtr.profile.menu.abort')} quit`)}\n`;
173
207
  return out;
174
208
  }
175
209
  /** Drive a highlighted keypress menu to a resolution. Returns the selected row
176
- * index, plus the action letter when one of `actionKeys` was pressed on the
177
- * highlighted row. */
178
- function runMenu(title, subtitle, rows, opts = {}) {
210
+ * index, plus the semantic action when a row-scoped action was invoked. */
211
+ function runMenu(bindings, title, subtitle, rows, opts = {}) {
179
212
  const numbered = [];
180
213
  rows.forEach((r, i) => {
181
- if (r.hotkey === undefined)
214
+ if (r.bindingId === undefined)
182
215
  numbered.push(i);
183
216
  });
184
217
  let sel = opts.initialIndex ?? 0;
185
218
  return new Promise((resolve) => {
186
219
  let prevLines = 0;
187
220
  const draw = () => {
188
- const block = renderMenu(title, subtitle, rows, sel, opts.hint);
221
+ const block = renderMenu(bindings, title, subtitle, rows, sel, opts.hint);
189
222
  process.stdout.write((prevLines > 0 ? cursorUp(prevLines) + CLEAR_DOWN : '') + block);
190
223
  prevLines = blockRows(block);
191
224
  };
@@ -196,22 +229,23 @@ function runMenu(title, subtitle, rows, opts = {}) {
196
229
  process.stdin.pause();
197
230
  };
198
231
  const onKey = (str, k) => {
199
- if (k.ctrl === true && k.name === 'c') {
232
+ const action = resolveProfileMenuAction(bindings, profileRawInput(str, k));
233
+ if (action === 'crtr.profile.menu.abort') {
200
234
  cleanup();
201
235
  process.stdout.write('\n');
202
236
  process.exit(130);
203
237
  }
204
- if (k.name === 'up' || k.name === 'k' || str === 'k') {
238
+ if (action === 'crtr.profile.menu.up') {
205
239
  sel = (sel - 1 + rows.length) % rows.length;
206
240
  draw();
207
241
  return;
208
242
  }
209
- if (k.name === 'down' || k.name === 'j' || str === 'j') {
243
+ if (action === 'crtr.profile.menu.down') {
210
244
  sel = (sel + 1) % rows.length;
211
245
  draw();
212
246
  return;
213
247
  }
214
- if (k.name === 'return' || k.name === 'enter') {
248
+ if (action === 'crtr.profile.menu.select') {
215
249
  cleanup();
216
250
  resolve({ index: sel });
217
251
  return;
@@ -219,28 +253,28 @@ function runMenu(title, subtitle, rows, opts = {}) {
219
253
  // Esc cancels navigation and accepts the default row (the MRU/pinned
220
254
  // profile, or the root row on the no-coverage menu) — a safe exit, never
221
255
  // a dead key. Ctrl-C still aborts crtr entirely.
222
- if (k.name === 'escape') {
256
+ if (action === 'crtr.profile.menu.accept-default') {
223
257
  cleanup();
224
258
  resolve({ index: opts.initialIndex ?? 0 });
225
259
  return;
226
260
  }
227
- if (str !== undefined && /^[0-9]$/.test(str)) {
228
- const d = Number.parseInt(str, 10);
261
+ if (action?.startsWith('crtr.profile.menu.slot.')) {
262
+ const d = Number.parseInt(action.slice(-1), 10);
229
263
  if (d >= 1 && d <= numbered.length) {
230
264
  sel = numbered[d - 1];
231
265
  draw();
232
266
  }
233
267
  return;
234
268
  }
235
- const hk = rows.findIndex((r) => r.hotkey !== undefined && r.hotkey === str);
269
+ const hk = rows.findIndex((r) => r.bindingId !== undefined && r.bindingId === action);
236
270
  if (hk >= 0) {
237
271
  cleanup();
238
272
  resolve({ index: hk });
239
273
  return;
240
274
  }
241
- if (str !== undefined && opts.actionKeys?.includes(str) === true) {
275
+ if (action !== null && opts.actionBindingIds?.includes(action) === true) {
242
276
  cleanup();
243
- resolve({ index: sel, action: str });
277
+ resolve({ index: sel, action });
244
278
  }
245
279
  };
246
280
  emitKeypressEvents(process.stdin);
@@ -308,7 +342,7 @@ async function offerAddDirToProfile(rl, entry, cwd) {
308
342
  * out (returns null → caller falls back to root). Because typing filters,
309
343
  * navigation here is arrows-only — j/k are literal filter characters (fzf
310
344
  * convention), unlike the non-search menus where j/k move. */
311
- async function promptSelectExistingProfile() {
345
+ async function promptSelectExistingProfile(bindings) {
312
346
  const all = [...listProfiles()]
313
347
  .filter((e) => e.profileId !== ROOT_PROFILE_ID)
314
348
  .sort((a, b) => (isMoreRecent(a.manifest.last_used_at, b.manifest.last_used_at) ? -1 : 1));
@@ -341,7 +375,7 @@ async function promptSelectExistingProfile() {
341
375
  });
342
376
  }
343
377
  block += `\n ${accent('\u203a')} ${filter}${dim('\u2588')}\n`;
344
- block += ` ${dim('type to filter \u2191\u2193 move \u21b5 pick esc back')}\n`;
378
+ block += ` ${dim(`type to filter ${formatBinding(bindings, 'crtr.profile.search.up')}/${formatBinding(bindings, 'crtr.profile.search.down')} move ${formatBinding(bindings, 'crtr.profile.search.pick')} pick ${formatBinding(bindings, 'crtr.profile.search.back')} back`)}\n`;
345
379
  process.stdout.write((prevLines > 0 ? cursorUp(prevLines) + CLEAR_DOWN : '') + block);
346
380
  prevLines = blockRows(block);
347
381
  };
@@ -352,39 +386,40 @@ async function promptSelectExistingProfile() {
352
386
  process.stdin.pause();
353
387
  };
354
388
  const onKey = (str, k) => {
355
- if (k.ctrl === true && k.name === 'c') {
389
+ const action = resolveProfileSearchAction(bindings, profileRawInput(str, k));
390
+ if (action === 'crtr.profile.search.abort') {
356
391
  cleanup();
357
392
  process.stdout.write('\n');
358
393
  process.exit(130);
359
394
  }
360
- if (k.name === 'escape') {
395
+ if (action === 'crtr.profile.search.back') {
361
396
  cleanup();
362
397
  resolve(null);
363
398
  return;
364
399
  }
365
400
  const matches = matchesOf();
366
- if (k.name === 'return' || k.name === 'enter') {
401
+ if (action === 'crtr.profile.search.pick') {
367
402
  if (matches.length > 0) {
368
403
  cleanup();
369
404
  resolve(matches[sel].profileId);
370
405
  }
371
406
  return;
372
407
  }
373
- if (k.name === 'up' || (k.ctrl === true && k.name === 'p')) {
408
+ if (action === 'crtr.profile.search.up') {
374
409
  if (matches.length > 0) {
375
410
  sel = (sel - 1 + matches.length) % matches.length;
376
411
  draw();
377
412
  }
378
413
  return;
379
414
  }
380
- if (k.name === 'down' || (k.ctrl === true && k.name === 'n')) {
415
+ if (action === 'crtr.profile.search.down') {
381
416
  if (matches.length > 0) {
382
417
  sel = (sel + 1) % matches.length;
383
418
  draw();
384
419
  }
385
420
  return;
386
421
  }
387
- if (k.name === 'backspace') {
422
+ if (action === 'crtr.profile.search.delete') {
388
423
  filter = filter.slice(0, -1);
389
424
  sel = 0;
390
425
  draw();
@@ -416,18 +451,18 @@ async function createProfileHere(rl, cwd) {
416
451
  process.stdout.write(` ${accent('\u2713')} ${dim(`created profile "${name}"`)}\n`);
417
452
  return profileId;
418
453
  }
419
- async function promptCreateOrRoot(cwd) {
454
+ async function promptCreateOrRoot(cwd, bindings) {
420
455
  // Root is the highlighted default so a bare Enter (or Esc) still lands on the
421
456
  // safe no-op, exactly as the old `[c/s/r]` prompt defaulted. Looped so Esc
422
457
  // out of the search sub-list returns HERE rather than silently dropping to
423
458
  // root — the create/select/root choice is always the fallback, never skipped.
424
459
  const rows = [
425
- { label: 'create a profile here', detail: '', hotkey: 'c' },
426
- { label: 'select an existing profile', detail: '', hotkey: 's' },
427
- { label: dim('use the root profile'), detail: '', hotkey: 'r', isDefault: true },
460
+ { label: 'create a profile here', detail: '', bindingId: 'crtr.profile.menu.create' },
461
+ { label: 'select an existing profile', detail: '', bindingId: 'crtr.profile.menu.search-existing' },
462
+ { label: dim('use the root profile'), detail: '', bindingId: 'crtr.profile.menu.use-root', isDefault: true },
428
463
  ];
429
464
  for (;;) {
430
- const res = await runMenu('No profile covers this directory', undefined, rows, {
465
+ const res = await runMenu(bindings, 'No profile covers this directory', undefined, rows, {
431
466
  initialIndex: 2,
432
467
  });
433
468
  if (res.index === 0) {
@@ -440,7 +475,7 @@ async function promptCreateOrRoot(cwd) {
440
475
  }
441
476
  }
442
477
  if (res.index === 1) {
443
- const selected = await promptSelectExistingProfile();
478
+ const selected = await promptSelectExistingProfile(bindings);
444
479
  // Esc/back out of the search list (or no profiles to pick) → re-show this
445
480
  // create/select/root menu, not a silent fall-through to root.
446
481
  if (selected === null)
@@ -469,7 +504,7 @@ async function promptCreateOrRoot(cwd) {
469
504
  * here: a claimed/covered directory always has at least a covering profile as a
470
505
  * sane identity, so `[r]oot` belongs only to the no-coverage prompt
471
506
  * (`promptCreateOrRoot`). Same raw-readline / boot-gating rationale. */
472
- async function promptPickProfileOrCreate(candidates, cwd, exact, pinnedId) {
507
+ async function promptPickProfileOrCreate(candidates, cwd, exact, pinnedId, bindings) {
473
508
  // MRU-first so the most-recently-used candidate is the bare-Enter default —
474
509
  // the same profile the non-interactive path auto-picks. A per-cwd pin (set
475
510
  // here with `d`) overrides that: it sorts to the top and becomes the
@@ -501,24 +536,22 @@ async function promptPickProfileOrCreate(candidates, cwd, exact, pinnedId) {
501
536
  defaultText,
502
537
  };
503
538
  });
504
- rows.push({ label: dim('create a new profile here'), detail: '', hotkey: 'c' });
505
- // Action letters resolve on the highlighted row: `d` pins it as this
506
- // directory's default; `a` widens its purview to include cwd (meaningless
507
- // when the candidates already claim cwd exactly, so hidden there); `u` clears
508
- // an existing pin.
509
- const actionKeys = ['d'];
539
+ rows.push({ label: dim('create a new profile here'), detail: '', bindingId: 'crtr.profile.menu.create' });
540
+ // Row-scoped actions pin the highlighted profile, widen its purview (hidden
541
+ // when candidates already claim cwd exactly), or clear an existing pin.
542
+ const actionBindingIds = ['crtr.profile.menu.set-default'];
510
543
  if (!exact)
511
- actionKeys.push('a');
544
+ actionBindingIds.push('crtr.profile.menu.add-directory');
512
545
  if (pinned !== null)
513
- actionKeys.push('u');
514
- const hintParts = ['d=set default'];
546
+ actionBindingIds.push('crtr.profile.menu.unpin');
547
+ const hintParts = [`${formatBinding(bindings, 'crtr.profile.menu.set-default')}=set default`];
515
548
  if (!exact)
516
- hintParts.push('a=add dir here');
549
+ hintParts.push(`${formatBinding(bindings, 'crtr.profile.menu.add-directory')}=add dir here`);
517
550
  if (pinned !== null)
518
- hintParts.push('u=unpin');
519
- const res = await runMenu(exact ? 'Choose a profile' : 'Select a profile', exact
551
+ hintParts.push(`${formatBinding(bindings, 'crtr.profile.menu.unpin')}=unpin`);
552
+ const res = await runMenu(bindings, exact ? 'Choose a profile' : 'Select a profile', exact
520
553
  ? 'Several profiles claim this directory as their project'
521
- : 'This directory falls under existing profile(s)', rows, { actionKeys, hint: hintParts.join(' ') });
554
+ : 'This directory falls under existing profile(s)', rows, { actionBindingIds, hint: hintParts.join(' ') });
522
555
  // The create row is last; selecting it (Enter on it, or `c`) creates here.
523
556
  if (res.index >= ordered.length) {
524
557
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -530,16 +563,16 @@ async function promptPickProfileOrCreate(candidates, cwd, exact, pinnedId) {
530
563
  }
531
564
  }
532
565
  const picked = ordered[res.index];
533
- if (res.action === 'u') {
566
+ if (res.action === 'crtr.profile.menu.unpin') {
534
567
  clearDefaultProfile(cwd);
535
568
  process.stdout.write(` ${accent('\u2713')} ${dim('cleared the default for this directory')}\n`);
536
569
  return picked.profileId;
537
570
  }
538
- if (res.action === 'd') {
571
+ if (res.action === 'crtr.profile.menu.set-default') {
539
572
  setDefaultProfileId(cwd, picked.profileId);
540
573
  process.stdout.write(` ${accent('\u2713')} ${dim(`"${picked.manifest.name}" is now the default here`)}\n`);
541
574
  }
542
- else if (res.action === 'a') {
575
+ else if (res.action === 'crtr.profile.menu.add-directory') {
543
576
  addProfileProject(picked.profileId, cwd);
544
577
  process.stdout.write(` ${accent('\u2713')} ${dim(`added this directory to "${picked.manifest.name}"`)}\n`);
545
578
  }
@@ -576,6 +609,7 @@ async function promptPickProfileOrCreate(candidates, cwd, exact, pinnedId) {
576
609
  * never stdout, which the caller may be piping. */
577
610
  export async function selectProfileForCwd(cwd, explicitProfile, forcePicker = false) {
578
611
  const resolvedCwd = resolveCwd(cwd);
612
+ const bindings = resolveUserKeybindings();
579
613
  if (explicitProfile !== undefined && explicitProfile !== null && explicitProfile !== '') {
580
614
  const entry = loadProfileManifest(explicitProfile);
581
615
  // Selecting a profile from a directory it does not yet cover: offer to
@@ -633,10 +667,10 @@ export async function selectProfileForCwd(cwd, explicitProfile, forcePicker = fa
633
667
  // the create-or-root prompt (root is only ever offered there).
634
668
  const validPinnedId = pinned !== null ? pinned.profileId : null;
635
669
  const chosen = exact.length > 1
636
- ? await promptPickProfileOrCreate(exact, resolvedCwd, true, validPinnedId)
670
+ ? await promptPickProfileOrCreate(exact, resolvedCwd, true, validPinnedId, bindings)
637
671
  : covering.length > 0
638
- ? await promptPickProfileOrCreate(covering, resolvedCwd, false, validPinnedId)
639
- : await promptCreateOrRoot(resolvedCwd);
672
+ ? await promptPickProfileOrCreate(covering, resolvedCwd, false, validPinnedId, bindings)
673
+ : await promptCreateOrRoot(resolvedCwd, bindings);
640
674
  updateProfileLastUsed(chosen);
641
675
  return chosen;
642
676
  }
@@ -0,0 +1,12 @@
1
+ /** The minimal shape of a Providers-panel row the removal router needs. */
2
+ export interface RemovableProviderRow {
3
+ providerId: string;
4
+ name: string;
5
+ managed: boolean;
6
+ configured?: boolean;
7
+ label?: string;
8
+ }
9
+ /** Route a row's removal to the coordinated service (managed) or AuthStorage (non-managed) and
10
+ * return the truthful panel notice. May throw (e.g. an unknown-subscription error) — callers
11
+ * surface that as the notice. */
12
+ export declare function removeProviderRow(agentDir: string, row: RemovableProviderRow): string;
@@ -0,0 +1,24 @@
1
+ // Removal routing for a Providers-panel row, kept in core (out of the TUI module) so both the
2
+ // `crtr sys setup` panel and the regression suite exercise the exact same guard.
3
+ //
4
+ // Final-review finding #1: a MANAGED provider — whether an account row (has a label) or the
5
+ // provider HEADER row (no label, the default cursor position) — MUST go through the coordinated
6
+ // removeManagedProvider so auth.json AND the rotation pool are torn down together (a header removes
7
+ // every account). Only a NON-managed configured provider uses the raw AuthStorage.logout(). Routing
8
+ // a managed header to logout() (the original bug) strips only auth.json, leaves the pool configured,
9
+ // and makes the panel claim "Removed" while a reload still shows the provider — the split-ownership
10
+ // bug this feature exists to eliminate.
11
+ import { join } from 'node:path';
12
+ import { AuthStorage } from '@earendil-works/pi-coding-agent';
13
+ import { removeManagedProvider } from './subscription-state.js';
14
+ /** Route a row's removal to the coordinated service (managed) or AuthStorage (non-managed) and
15
+ * return the truthful panel notice. May throw (e.g. an unknown-subscription error) — callers
16
+ * surface that as the notice. */
17
+ export function removeProviderRow(agentDir, row) {
18
+ if (row.managed) {
19
+ removeManagedProvider(row.providerId, row.label === undefined ? undefined : row.label === 'Default account' ? row.providerId : row.label);
20
+ return row.label === undefined ? `Removed ${row.name}` : `Removed ${row.name} account`;
21
+ }
22
+ AuthStorage.create(join(agentDir, 'auth.json')).logout(row.providerId);
23
+ return `Removed ${row.name}`;
24
+ }
@@ -169,6 +169,15 @@ export interface CycleModelFrame {
169
169
  type: 'cycle_model';
170
170
  direction?: 'forward' | 'backward';
171
171
  }
172
+ /** `cycleLadder(direction)` — alt+m forward / alt+shift+m backward. Walks the INTERLEAVED model ladder
173
+ * (anthropic/ultra → openai/ultra → anthropic/strong → openai/strong → …),
174
+ * starting from the current model, applying each rung's own thinking level.
175
+ * Distinct from `cycle_model`, which steps the flat registry list. `direction`
176
+ * defaults to `'forward'` when absent. */
177
+ export interface CycleLadderFrame {
178
+ type: 'cycle_ladder';
179
+ direction?: 'forward' | 'backward';
180
+ }
172
181
  /** `cycleThinkingLevel()` — shift+tab. Cycles to the next thinking level; the new
173
182
  * level reaches viewers via the relayed `thinking_level_changed` event, so the
174
183
  * broker replies a plain `ack` (no payload). Distinct from `set_thinking_level`,
@@ -303,7 +312,7 @@ export interface ShareFrame {
303
312
  export interface ReloadAuthFrame {
304
313
  type: 'reload_auth';
305
314
  }
306
- export type ClientToBroker = HelloFrame | PromptFrame | SteerFrame | FollowUpFrame | AbortFrame | BashFrame | RequestControlFrame | ReleaseControlFrame | ByeFrame | ShutdownFrame | SetModelFrame | CycleModelFrame | CycleThinkingFrame | SetThinkingLevelFrame | DequeueFrame | SetAutoRetryFrame | SetAutoCompactionFrame | CompactFrame | NewSessionFrame | SwitchSessionFrame | ForkFrame | SetSessionNameFrame | GetCommandsFrame | NavigateTreeFrame | ReloadFrame | ExportFrame | ListModelsFrame | ListSessionsFrame | GetTreeFrame | GetSettingsFrame | ListScopedModelsFrame | ExtensionUIResponseFrame | CloneFrame | ShareFrame | ReloadAuthFrame;
315
+ export type ClientToBroker = HelloFrame | PromptFrame | SteerFrame | FollowUpFrame | AbortFrame | BashFrame | RequestControlFrame | ReleaseControlFrame | ByeFrame | ShutdownFrame | SetModelFrame | CycleModelFrame | CycleLadderFrame | CycleThinkingFrame | SetThinkingLevelFrame | DequeueFrame | SetAutoRetryFrame | SetAutoCompactionFrame | CompactFrame | NewSessionFrame | SwitchSessionFrame | ForkFrame | SetSessionNameFrame | GetCommandsFrame | NavigateTreeFrame | ReloadFrame | ExportFrame | ListModelsFrame | ListSessionsFrame | GetTreeFrame | GetSettingsFrame | ListScopedModelsFrame | ExtensionUIResponseFrame | CloneFrame | ShareFrame | ReloadAuthFrame;
307
316
  export interface WelcomeFrame {
308
317
  type: 'welcome';
309
318
  snapshot: BrokerSnapshot;
@@ -330,6 +339,13 @@ export interface ControlChangedFrame {
330
339
  export interface ModelChangedFrame {
331
340
  type: 'model_changed';
332
341
  model: string | undefined;
342
+ /** The full resolved `provider/id[:thinking]` spec (`formatModelSpec`) — what
343
+ * the broker persists into the node's own launch recipe. Viewers use it to
344
+ * persist a non-portable explicit pick as a kind DEFAULT
345
+ * (`persistDefaultKindModel`) without guessing the provider from the bare
346
+ * id. Null when the engine has no model. Optional so older brokers'
347
+ * frames still parse. */
348
+ spec?: string | null;
333
349
  }
334
350
  export interface ErrorFrame {
335
351
  type: 'error';
@@ -380,9 +396,10 @@ export interface WireScopedModel {
380
396
  model: Model<Api>;
381
397
  thinkingLevel?: AgentSession['thinkingLevel'];
382
398
  }
383
- /** Wire form of pi's `SessionInfo`: identical EXCEPT `created`/`modified` are ISO
384
- * strings (pi's are `Date`, which JSON cannot round-trip) the viewer revives
385
- * them with `new Date(...)` before handing them to `SessionSelectorComponent`. */
399
+ /** Wire form of pi's `SessionInfo`: `created`/`modified` are ISO strings (pi's
400
+ * are `Date`, which JSON cannot round-trip), and `allMessagesText` is a bounded
401
+ * opening/latest search excerpt rather than the unbounded transcript text. The
402
+ * viewer revives the dates before handing them to `SessionSelectorComponent`. */
386
403
  export interface WireSessionInfo extends Omit<SessionInfo, 'created' | 'modified'> {
387
404
  created: string;
388
405
  modified: string;
@@ -494,10 +511,21 @@ export interface DisplayTitleFrame {
494
511
  }
495
512
  /** A blocking dialog routed to the controller — pi's public RPC request type. */
496
513
  export type ExtensionUIRequestFrame = RpcExtensionUIRequest;
514
+ /** Broker → client: dismiss a previously-forwarded blocking dialog by request id,
515
+ * WITHOUT a client answer. The broker sends this exactly when it resolves a pending
516
+ * dialog ITSELF rather than the client answering it — the extension aborted the
517
+ * request out-of-band (e.g. a local OAuth loopback callback won the race against a
518
+ * still-open manual-paste dialog) or the broker-side timeout fired. The client tears
519
+ * down ONLY the overlay whose `id` matches; every other dialog stays exactly as it
520
+ * was, so an unrelated request can never dismiss an unrelated blocking dialog. */
521
+ export interface ExtensionUIDismissFrame {
522
+ type: 'extension_ui_dismiss';
523
+ id: string;
524
+ }
497
525
  /** Everything the broker can send. Live `AgentSessionEvent`s are relayed
498
526
  * verbatim (the broker adds nothing); the broker's own control frames carry
499
527
  * non-colliding `type` discriminants. */
500
- export type BrokerToClient = WelcomeFrame | ControlChangedFrame | ModelChangedFrame | ErrorFrame | AckFrame | BrokerDataFrame | DisplayStatusFrame | DisplayWidgetFrame | DisplayTitleFrame | BashStartFrame | BashOutputFrame | BashEndFrame | ExtensionUIRequestFrame | AgentSessionEvent;
528
+ export type BrokerToClient = WelcomeFrame | ControlChangedFrame | ModelChangedFrame | ErrorFrame | AckFrame | BrokerDataFrame | DisplayStatusFrame | DisplayWidgetFrame | DisplayTitleFrame | BashStartFrame | BashOutputFrame | BashEndFrame | ExtensionUIRequestFrame | ExtensionUIDismissFrame | AgentSessionEvent;
501
529
  /** Encode one frame as a single newline-terminated JSON line. */
502
530
  export declare function encodeFrame(frame: ClientToBroker | BrokerToClient): string;
503
531
  /** Byte bounds for a {@link FrameDecoder} (C5). */