@crouton-kit/crouter 0.3.78 → 0.3.80

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 (80) hide show
  1. package/dist/build-root.d.ts +12 -4
  2. package/dist/build-root.js +25 -6
  3. package/dist/builtin-memory/crouter-development/plugins.md +82 -5
  4. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +1228 -12
  5. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +3 -3
  6. package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +2 -787
  7. package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +8 -3
  8. package/dist/clients/attach/__tests__/attach-keybindings.test.d.ts +1 -0
  9. package/dist/clients/attach/__tests__/attach-keybindings.test.js +113 -0
  10. package/dist/clients/attach/__tests__/mermaid-render.test.js +9 -1
  11. package/dist/clients/attach/attach-cmd.d.ts +9 -1
  12. package/dist/clients/attach/attach-cmd.js +847 -801
  13. package/dist/clients/attach/auth-pickers.d.ts +0 -12
  14. package/dist/clients/attach/auth-pickers.js +64 -15
  15. package/dist/clients/attach/chat-view.d.ts +4 -0
  16. package/dist/clients/attach/chat-view.js +10 -0
  17. package/dist/clients/attach/graph-overlay.d.ts +12 -2
  18. package/dist/clients/attach/graph-overlay.js +83 -33
  19. package/dist/clients/attach/input-controller.d.ts +19 -0
  20. package/dist/clients/attach/input-controller.js +55 -11
  21. package/dist/clients/attach/titled-editor.d.ts +21 -0
  22. package/dist/clients/attach/titled-editor.js +101 -0
  23. package/dist/commands/human/queue.js +3 -4
  24. package/dist/commands/pkg/plugin-inspect.js +22 -1
  25. package/dist/commands/pkg/plugin-manage.js +31 -9
  26. package/dist/commands/sys/__tests__/setup-core.test.js +158 -1
  27. package/dist/commands/sys/doctor.js +42 -4
  28. package/dist/commands/sys/setup-core.d.ts +37 -0
  29. package/dist/commands/sys/setup-core.js +138 -1
  30. package/dist/commands/sys/setup.d.ts +88 -0
  31. package/dist/commands/sys/setup.js +915 -171
  32. package/dist/commands/view-pick.d.ts +4 -0
  33. package/dist/commands/view-pick.js +17 -7
  34. package/dist/core/__tests__/canvas-inbox-watcher.test.js +34 -9
  35. package/dist/core/__tests__/command-plugins-surfaces.test.d.ts +1 -0
  36. package/dist/core/__tests__/command-plugins-surfaces.test.js +298 -0
  37. package/dist/core/__tests__/command-plugins.test.d.ts +1 -0
  38. package/dist/core/__tests__/command-plugins.test.js +444 -0
  39. package/dist/core/__tests__/fixtures/fake-engine.d.ts +6 -0
  40. package/dist/core/__tests__/fixtures/fake-engine.js +9 -1
  41. package/dist/core/__tests__/preview-registry-sync.test.js +30 -1
  42. package/dist/core/__tests__/scope-crouter-home-fence.test.d.ts +1 -0
  43. package/dist/core/__tests__/scope-crouter-home-fence.test.js +55 -0
  44. package/dist/core/canvas/browse/app.d.ts +6 -0
  45. package/dist/core/canvas/browse/app.js +71 -41
  46. package/dist/core/command-plugins/adapter.d.ts +15 -0
  47. package/dist/core/command-plugins/adapter.js +145 -0
  48. package/dist/core/command-plugins/compose.d.ts +5 -0
  49. package/dist/core/command-plugins/compose.js +56 -0
  50. package/dist/core/command-plugins/discovery.d.ts +104 -0
  51. package/dist/core/command-plugins/discovery.js +565 -0
  52. package/dist/core/config.d.ts +2 -5
  53. package/dist/core/keybindings/__tests__/bespoke-consumers.test.d.ts +1 -0
  54. package/dist/core/keybindings/__tests__/bespoke-consumers.test.js +40 -0
  55. package/dist/core/keybindings/__tests__/resolve.test.js +2 -2
  56. package/dist/core/keybindings/catalog.d.ts +3 -3
  57. package/dist/core/keybindings/catalog.js +2 -1
  58. package/dist/core/profiles/select.d.ts +6 -0
  59. package/dist/core/profiles/select.js +90 -59
  60. package/dist/core/provider-management.d.ts +12 -0
  61. package/dist/core/provider-management.js +24 -0
  62. package/dist/core/runtime/banner.d.ts +13 -0
  63. package/dist/core/runtime/banner.js +51 -0
  64. package/dist/core/runtime/broker.js +7 -6
  65. package/dist/core/runtime/pi-vendored.d.ts +8 -0
  66. package/dist/core/runtime/pi-vendored.js +14 -0
  67. package/dist/core/runtime/recap.d.ts +1 -1
  68. package/dist/core/runtime/recap.js +50 -25
  69. package/dist/core/runtime/session-list-cache.d.ts +10 -0
  70. package/dist/core/runtime/session-list-cache.js +94 -26
  71. package/dist/core/runtime/spawn.js +5 -17
  72. package/dist/core/runtime/tmux.js +2 -1
  73. package/dist/core/scope.js +27 -4
  74. package/dist/core/subscription-state.d.ts +90 -0
  75. package/dist/core/subscription-state.js +762 -0
  76. package/dist/daemon/crtrd.js +253 -12
  77. package/dist/pi-extensions/canvas-recap.js +43 -17
  78. package/dist/types.d.ts +6 -13
  79. package/dist/types.js +0 -3
  80. package/package.json +7 -3
@@ -1,14 +1,14 @@
1
1
  import type { BindingDefinition, SafetyRequirement } from './types.js';
2
- type TmuxMenuAction = 'open' | 'promote' | 'resume' | 'demote' | 'detach' | 'close-subtree' | 'context' | 'graph' | 'focus-manager' | 'issues';
2
+ type TmuxMenuAction = 'open' | 'promote' | 'resume' | 'demote' | 'detach' | 'close-subtree' | 'context' | 'providers' | 'graph' | 'focus-manager' | 'issues';
3
3
  type GraphAction = 'down' | 'up' | 'first' | 'last' | 'collapse-or-manager' | 'expand-or-child' | 'focus' | 'focus-manager' | 'close-subtree' | 'dismiss';
4
4
  type InboxAction = 'down' | 'up' | 'open' | 'reply' | 'react' | 'filter' | 'refresh' | 'connect-linkedin' | 'connect-gmail' | 'quit' | 'submit-reply' | 'cancel' | 'reaction-previous' | 'reaction-next' | 'submit-reaction';
5
5
  type LinkedinAction = Exclude<InboxAction, 'filter' | 'connect-linkedin' | 'connect-gmail'>;
6
- type BrowseAction = 'quit' | 'down' | 'up' | 'first' | 'last' | 'expand-or-child' | 'collapse-or-parent' | 'toggle-fold' | 'tab-next' | 'tab-previous' | `tab.${1 | 2 | 3 | 4}` | 'close-subtree' | 'confirm-close' | 'sort' | 'scope' | 'residents' | 'preview' | 'search' | 'revive-now' | 'focus';
6
+ type BrowseAction = 'quit' | 'down' | 'up' | 'first' | 'last' | 'expand-or-child' | 'collapse-or-parent' | 'toggle-fold' | 'tab-next' | 'tab-previous' | `tab.${1 | 2 | 3 | 4 | 5}` | 'close-subtree' | 'confirm-close' | 'sort' | 'scope' | 'residents' | 'preview' | 'search' | 'revive-now' | 'focus';
7
7
  type ProfileMenuAction = 'up' | 'down' | 'select' | 'accept-default' | 'abort' | `slot.${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}` | 'create' | 'search-existing' | 'use-root' | 'set-default' | 'add-directory' | 'unpin';
8
8
  type SetupAction = 'cancel' | 'tab-next' | 'tab-previous' | 'open' | 'list-down' | 'list-up' | 'toggle' | 'details' | 'advance';
9
9
  type SetupKeybindingAction = 'edit' | 'commit-edit' | 'cancel-edit' | 'unbind' | 'reset-row' | 'reset-all' | 'save' | 'discard' | 'page-up' | 'page-down' | 'filter' | 'filter-delete' | 'filter-clear';
10
10
  export type BindingId = `crtr.tmux.menu.${TmuxMenuAction}` | `crtr.tmux.menu.issue.${'idea' | 'bug'}` | `crtr.tmux.node.${'next' | 'previous'}` | `crtr.tmux.view.${'next' | 'previous'}` | `crtr.attach.${'detach' | 'clear-or-detach' | 'graph.toggle' | 'model-ladder.next'}` | 'crtr.mode.cycle' | `crtr.graph.${GraphAction}` | `crtr.graph.confirm.${'accept' | 'cancel'}` | `crtr.view.host.${'force-quit' | 'editor.backspace'}` | `crtr.view.canvas.${'down' | 'up' | 'refresh' | 'quit'}` | `crtr.view.chat.${'submit' | 'contextual-cancel' | 'reconnect' | 'request-control' | 'release-control' | 'quit'}` | `crtr.view.git-pr.${'down' | 'up' | 'refresh' | 'quit'}` | `crtr.view.inbox.${InboxAction}` | `crtr.view.linkedin.${LinkedinAction}` | `crtr.view.prompt-review.${'down' | 'up' | 'compare-previous' | 'compare-next' | 'toggle-mode' | 'expand' | 'export' | 'refresh' | 'quit'}` | `crtr.view.settings.${'down' | 'up' | 'cell-previous' | 'cell-next' | 'refresh' | 'quit' | 'edit-or-submit' | 'cancel-edit'}` | `crtr.view.workspace-sidebar.${'down' | 'up' | 'open' | 'refresh' | 'quit'}` | `crtr.view-picker.${'down' | 'up' | 'run' | 'quit'}` | `crtr.browse.${BrowseAction}` | `crtr.browse.search.${'cancel' | 'commit' | 'delete' | 'quit'}` | `crtr.profile.menu.${ProfileMenuAction}` | `crtr.profile.search.${'up' | 'down' | 'pick' | 'back' | 'delete' | 'abort'}` | `crtr.setup.${SetupAction}` | `crtr.setup.confirm.${'accept' | 'back'}` | `crtr.setup.keybindings.${SetupKeybindingAction}`;
11
- export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, BindingDefinition<"crtr.tmux.menu.promote">, BindingDefinition<"crtr.tmux.menu.resume">, BindingDefinition<"crtr.tmux.menu.demote">, BindingDefinition<"crtr.tmux.menu.detach">, BindingDefinition<"crtr.tmux.menu.close-subtree">, BindingDefinition<"crtr.tmux.menu.context">, BindingDefinition<"crtr.tmux.menu.graph">, BindingDefinition<"crtr.tmux.menu.focus-manager">, BindingDefinition<"crtr.tmux.menu.issues">, BindingDefinition<"crtr.tmux.menu.issue.idea">, BindingDefinition<"crtr.tmux.menu.issue.bug">, BindingDefinition<"crtr.tmux.node.next">, BindingDefinition<"crtr.tmux.node.previous">, BindingDefinition<"crtr.tmux.view.next">, BindingDefinition<"crtr.tmux.view.previous">, BindingDefinition<"crtr.attach.detach">, BindingDefinition<"crtr.attach.clear-or-detach">, BindingDefinition<"crtr.attach.graph.toggle">, BindingDefinition<"crtr.attach.model-ladder.next">, BindingDefinition<"crtr.mode.cycle">, BindingDefinition<"crtr.graph.down">, BindingDefinition<"crtr.graph.up">, BindingDefinition<"crtr.graph.first">, BindingDefinition<"crtr.graph.last">, BindingDefinition<"crtr.graph.collapse-or-manager">, BindingDefinition<"crtr.graph.expand-or-child">, BindingDefinition<"crtr.graph.focus">, BindingDefinition<"crtr.graph.focus-manager">, BindingDefinition<"crtr.graph.close-subtree">, BindingDefinition<"crtr.graph.dismiss">, BindingDefinition<"crtr.graph.confirm.accept">, BindingDefinition<"crtr.graph.confirm.cancel">, BindingDefinition<"crtr.view.host.force-quit">, BindingDefinition<"crtr.view.host.editor.backspace">, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<"crtr.view.chat.submit">, BindingDefinition<"crtr.view.chat.contextual-cancel">, BindingDefinition<"crtr.view.chat.reconnect">, BindingDefinition<"crtr.view.chat.request-control">, BindingDefinition<"crtr.view.chat.release-control">, BindingDefinition<"crtr.view.chat.quit">, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.view.prompt-review.down"> | BindingDefinition<"crtr.view.prompt-review.up"> | BindingDefinition<"crtr.view.prompt-review.compare-previous"> | BindingDefinition<"crtr.view.prompt-review.compare-next"> | BindingDefinition<"crtr.view.prompt-review.toggle-mode"> | BindingDefinition<"crtr.view.prompt-review.expand"> | BindingDefinition<"crtr.view.prompt-review.export"> | BindingDefinition<"crtr.view.prompt-review.refresh"> | BindingDefinition<"crtr.view.prompt-review.quit"> | BindingDefinition<"crtr.view.settings.down"> | BindingDefinition<"crtr.view.settings.up"> | BindingDefinition<"crtr.view.settings.cell-previous"> | BindingDefinition<"crtr.view.settings.cell-next"> | BindingDefinition<"crtr.view.settings.refresh"> | BindingDefinition<"crtr.view.settings.quit"> | BindingDefinition<"crtr.view.settings.edit-or-submit"> | BindingDefinition<"crtr.view.settings.cancel-edit"> | BindingDefinition<"crtr.view.workspace-sidebar.down"> | BindingDefinition<"crtr.view.workspace-sidebar.up"> | BindingDefinition<"crtr.view.workspace-sidebar.open"> | BindingDefinition<"crtr.view.workspace-sidebar.refresh"> | BindingDefinition<"crtr.view.workspace-sidebar.quit"> | BindingDefinition<"crtr.view-picker.down"> | BindingDefinition<"crtr.view-picker.up"> | BindingDefinition<"crtr.view-picker.run"> | BindingDefinition<"crtr.view-picker.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.6" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.9">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.search.up">, BindingDefinition<"crtr.profile.search.down">, BindingDefinition<"crtr.profile.search.pick">, BindingDefinition<"crtr.profile.search.back">, BindingDefinition<"crtr.profile.search.delete">, BindingDefinition<"crtr.profile.search.abort">, BindingDefinition<"crtr.setup.cancel">, BindingDefinition<"crtr.setup.tab-next">, BindingDefinition<"crtr.setup.tab-previous">, BindingDefinition<"crtr.setup.open">, BindingDefinition<"crtr.setup.list-down">, BindingDefinition<"crtr.setup.list-up">, BindingDefinition<"crtr.setup.toggle">, BindingDefinition<"crtr.setup.details">, BindingDefinition<"crtr.setup.advance">, BindingDefinition<"crtr.setup.confirm.accept">, BindingDefinition<"crtr.setup.confirm.back">, BindingDefinition<"crtr.setup.keybindings.edit">, BindingDefinition<"crtr.setup.keybindings.commit-edit">, BindingDefinition<"crtr.setup.keybindings.cancel-edit">, BindingDefinition<"crtr.setup.keybindings.unbind">, BindingDefinition<"crtr.setup.keybindings.reset-row">, BindingDefinition<"crtr.setup.keybindings.reset-all">, BindingDefinition<"crtr.setup.keybindings.save">, BindingDefinition<"crtr.setup.keybindings.discard">, BindingDefinition<"crtr.setup.keybindings.page-up">, BindingDefinition<"crtr.setup.keybindings.page-down">, BindingDefinition<"crtr.setup.keybindings.filter">, BindingDefinition<"crtr.setup.keybindings.filter-delete">, BindingDefinition<"crtr.setup.keybindings.filter-clear">];
11
+ export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, BindingDefinition<"crtr.tmux.menu.promote">, BindingDefinition<"crtr.tmux.menu.resume">, BindingDefinition<"crtr.tmux.menu.demote">, BindingDefinition<"crtr.tmux.menu.detach">, BindingDefinition<"crtr.tmux.menu.close-subtree">, BindingDefinition<"crtr.tmux.menu.context">, BindingDefinition<"crtr.tmux.menu.providers">, BindingDefinition<"crtr.tmux.menu.graph">, BindingDefinition<"crtr.tmux.menu.focus-manager">, BindingDefinition<"crtr.tmux.menu.issues">, BindingDefinition<"crtr.tmux.menu.issue.idea">, BindingDefinition<"crtr.tmux.menu.issue.bug">, BindingDefinition<"crtr.tmux.node.next">, BindingDefinition<"crtr.tmux.node.previous">, BindingDefinition<"crtr.tmux.view.next">, BindingDefinition<"crtr.tmux.view.previous">, BindingDefinition<"crtr.attach.detach">, BindingDefinition<"crtr.attach.clear-or-detach">, BindingDefinition<"crtr.attach.graph.toggle">, BindingDefinition<"crtr.attach.model-ladder.next">, BindingDefinition<"crtr.mode.cycle">, BindingDefinition<"crtr.graph.down">, BindingDefinition<"crtr.graph.up">, BindingDefinition<"crtr.graph.first">, BindingDefinition<"crtr.graph.last">, BindingDefinition<"crtr.graph.collapse-or-manager">, BindingDefinition<"crtr.graph.expand-or-child">, BindingDefinition<"crtr.graph.focus">, BindingDefinition<"crtr.graph.focus-manager">, BindingDefinition<"crtr.graph.close-subtree">, BindingDefinition<"crtr.graph.dismiss">, BindingDefinition<"crtr.graph.confirm.accept">, BindingDefinition<"crtr.graph.confirm.cancel">, BindingDefinition<"crtr.view.host.force-quit">, BindingDefinition<"crtr.view.host.editor.backspace">, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<"crtr.view.chat.submit">, BindingDefinition<"crtr.view.chat.contextual-cancel">, BindingDefinition<"crtr.view.chat.reconnect">, BindingDefinition<"crtr.view.chat.request-control">, BindingDefinition<"crtr.view.chat.release-control">, BindingDefinition<"crtr.view.chat.quit">, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.view.prompt-review.down"> | BindingDefinition<"crtr.view.prompt-review.up"> | BindingDefinition<"crtr.view.prompt-review.compare-previous"> | BindingDefinition<"crtr.view.prompt-review.compare-next"> | BindingDefinition<"crtr.view.prompt-review.toggle-mode"> | BindingDefinition<"crtr.view.prompt-review.expand"> | BindingDefinition<"crtr.view.prompt-review.export"> | BindingDefinition<"crtr.view.prompt-review.refresh"> | BindingDefinition<"crtr.view.prompt-review.quit"> | BindingDefinition<"crtr.view.settings.down"> | BindingDefinition<"crtr.view.settings.up"> | BindingDefinition<"crtr.view.settings.cell-previous"> | BindingDefinition<"crtr.view.settings.cell-next"> | BindingDefinition<"crtr.view.settings.refresh"> | BindingDefinition<"crtr.view.settings.quit"> | BindingDefinition<"crtr.view.settings.edit-or-submit"> | BindingDefinition<"crtr.view.settings.cancel-edit"> | BindingDefinition<"crtr.view.workspace-sidebar.down"> | BindingDefinition<"crtr.view.workspace-sidebar.up"> | BindingDefinition<"crtr.view.workspace-sidebar.open"> | BindingDefinition<"crtr.view.workspace-sidebar.refresh"> | BindingDefinition<"crtr.view.workspace-sidebar.quit"> | BindingDefinition<"crtr.view-picker.down"> | BindingDefinition<"crtr.view-picker.up"> | BindingDefinition<"crtr.view-picker.run"> | BindingDefinition<"crtr.view-picker.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.6" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.9">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.search.up">, BindingDefinition<"crtr.profile.search.down">, BindingDefinition<"crtr.profile.search.pick">, BindingDefinition<"crtr.profile.search.back">, BindingDefinition<"crtr.profile.search.delete">, BindingDefinition<"crtr.profile.search.abort">, BindingDefinition<"crtr.setup.cancel">, BindingDefinition<"crtr.setup.tab-next">, BindingDefinition<"crtr.setup.tab-previous">, BindingDefinition<"crtr.setup.open">, BindingDefinition<"crtr.setup.list-down">, BindingDefinition<"crtr.setup.list-up">, BindingDefinition<"crtr.setup.toggle">, BindingDefinition<"crtr.setup.details">, BindingDefinition<"crtr.setup.advance">, BindingDefinition<"crtr.setup.confirm.accept">, BindingDefinition<"crtr.setup.confirm.back">, BindingDefinition<"crtr.setup.keybindings.edit">, BindingDefinition<"crtr.setup.keybindings.commit-edit">, BindingDefinition<"crtr.setup.keybindings.cancel-edit">, BindingDefinition<"crtr.setup.keybindings.unbind">, BindingDefinition<"crtr.setup.keybindings.reset-row">, BindingDefinition<"crtr.setup.keybindings.reset-all">, BindingDefinition<"crtr.setup.keybindings.save">, BindingDefinition<"crtr.setup.keybindings.discard">, BindingDefinition<"crtr.setup.keybindings.page-up">, BindingDefinition<"crtr.setup.keybindings.page-down">, BindingDefinition<"crtr.setup.keybindings.filter">, BindingDefinition<"crtr.setup.keybindings.filter-delete">, BindingDefinition<"crtr.setup.keybindings.filter-clear">];
12
12
  export declare const BINDING_IDS: ReadonlySet<string>;
13
13
  export declare const SAFETY_REQUIREMENTS: readonly SafetyRequirement[];
14
14
  export {};
@@ -32,6 +32,7 @@ export const BINDING_CATALOG = Object.freeze([
32
32
  b('crtr.tmux.menu.detach', 'Global tmux', 'Detach node', 'Detach the focused node to the background.', ['shift+d'], 'tmux-menu-selector', ['tmux.menu']),
33
33
  b('crtr.tmux.menu.close-subtree', 'Global tmux', 'Close subtree', 'Close the focused node and its subtree.', ['x'], 'tmux-menu-selector', ['tmux.menu']),
34
34
  b('crtr.tmux.menu.context', 'Global tmux', 'Browse context', 'Browse the focused node context.', ['c'], 'tmux-menu-selector', ['tmux.menu']),
35
+ b('crtr.tmux.menu.providers', 'Global tmux', 'Manage providers', 'Open the provider management panel.', ['p'], 'tmux-menu-selector', ['tmux.menu']),
35
36
  b('crtr.tmux.menu.graph', 'Global tmux', 'Open graph', 'Open the canvas graph.', ['g'], 'tmux-menu-selector', ['tmux.menu']),
36
37
  b('crtr.tmux.menu.focus-manager', 'Global tmux', 'Focus manager', 'Focus the node manager.', ['m'], 'tmux-menu-selector', ['tmux.menu']),
37
38
  b('crtr.tmux.menu.issues', 'Global tmux', 'Open issue menu', 'Open the issue type submenu.', ['i'], 'tmux-menu-selector', ['tmux.menu']),
@@ -155,7 +156,7 @@ function browseBindings() {
155
156
  b('crtr.browse.toggle-fold', 'Browse and pickers', 'Toggle fold', 'Toggle the selected fold.', ['space'], 'terminal', [nav]),
156
157
  b('crtr.browse.tab-next', 'Browse and pickers', 'Next tab', 'Select the next browse tab.', ['tab', ']'], 'terminal', [nav]),
157
158
  b('crtr.browse.tab-previous', 'Browse and pickers', 'Previous tab', 'Select the previous browse tab.', ['shift+tab', '['], 'terminal', [nav]),
158
- ...[1, 2, 3, 4].map((slot) => b(`crtr.browse.tab.${slot}`, 'Browse and pickers', `Tab ${slot}`, `Select browse tab ${slot}.`, [`${slot}`], 'terminal', [nav])),
159
+ ...[1, 2, 3, 4, 5].map((slot) => b(`crtr.browse.tab.${slot}`, 'Browse and pickers', `Tab ${slot}`, `Select browse tab ${slot}.`, [`${slot}`], 'terminal', [nav])),
159
160
  b('crtr.browse.close-subtree', 'Browse and pickers', 'Close subtree', 'Ask to close the selected subtree.', ['x'], 'terminal', [nav]),
160
161
  b('crtr.browse.confirm-close', 'Browse and pickers', 'Confirm close', 'Confirm closing the selected subtree.', ['y'], 'terminal', ['browse.confirm']),
161
162
  b('crtr.browse.sort', 'Browse and pickers', 'Change sort', 'Change the browse sort.', ['s'], 'terminal', [nav]),
@@ -1,4 +1,9 @@
1
1
  import { type ProfileEntry } from './manifest.js';
2
+ import { type BindingId, type BindingResolution, type RawTerminalInput } from '../keybindings/index.js';
3
+ declare const PROFILE_MENU_ACTIONS: readonly ["crtr.profile.menu.abort", "crtr.profile.menu.up", "crtr.profile.menu.down", "crtr.profile.menu.select", "crtr.profile.menu.accept-default", "crtr.profile.menu.slot.1", "crtr.profile.menu.slot.2", "crtr.profile.menu.slot.3", "crtr.profile.menu.slot.4", "crtr.profile.menu.slot.5", "crtr.profile.menu.slot.6", "crtr.profile.menu.slot.7", "crtr.profile.menu.slot.8", "crtr.profile.menu.slot.9", "crtr.profile.menu.create", "crtr.profile.menu.search-existing", "crtr.profile.menu.use-root", "crtr.profile.menu.set-default", "crtr.profile.menu.add-directory", "crtr.profile.menu.unpin"];
4
+ declare const PROFILE_SEARCH_ACTIONS: readonly ["crtr.profile.search.abort", "crtr.profile.search.back", "crtr.profile.search.pick", "crtr.profile.search.up", "crtr.profile.search.down", "crtr.profile.search.delete"];
5
+ export declare function resolveProfileMenuAction(bindings: BindingResolution<BindingId>, raw: RawTerminalInput): (typeof PROFILE_MENU_ACTIONS)[number] | null;
6
+ export declare function resolveProfileSearchAction(bindings: BindingResolution<BindingId>, raw: RawTerminalInput): (typeof PROFILE_SEARCH_ACTIONS)[number] | null;
2
7
  /** Whether a profile's purview covers `cwd` (cwd at/under one of its project
3
8
  * dirs, or a project dir under cwd — the workspace-root case). Exported for
4
9
  * `crtr profile default set`, which rejects pinning a profile that doesn't
@@ -34,3 +39,4 @@ export declare function profileCoversCwd(entry: ProfileEntry, cwd: string): bool
34
39
  * default to the root profile and print the recovery instruction to STDERR —
35
40
  * never stdout, which the caller may be piping. */
36
41
  export declare function selectProfileForCwd(cwd: string, explicitProfile?: string | null, forcePicker?: boolean): Promise<string>;
42
+ export {};
@@ -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
@@ -597,15 +631,12 @@ export async function selectProfileForCwd(cwd, explicitProfile, forcePicker = fa
597
631
  if (pinnedId !== null && pinned === null)
598
632
  clearDefaultProfile(resolvedCwd);
599
633
  // A valid pin decides it outright and STOPS ASKING — the whole point of
600
- // setting a default. Auto-pick it (interactive too), printing a one-line
601
- // breadcrumb so the choice isn't invisible and the user knows how to change
602
- // it. `crtr --pick-profile` (forcePicker) re-opens the menu to re-pin/unpin.
634
+ // setting a default. Auto-pick it silently: the crouton banner the viewer
635
+ // paints a beat later shows the active profile and the `crtr --pick-profile`
636
+ // hint, so a boot-time breadcrumb here would just be redundant.
637
+ // `crtr --pick-profile` (forcePicker) re-opens the menu to re-pin/unpin.
603
638
  if (pinned !== null && !forcePicker) {
604
639
  updateProfileLastUsed(pinned.profileId);
605
- if (isInteractive()) {
606
- process.stdout.write(` ${accent('\u2713')} ${bold(pinned.manifest.name)} ${dim('\u00b7 default for this directory')}` +
607
- ` ${dim('(crtr --pick-profile to change)')}\n`);
608
- }
609
640
  return pinned.profileId;
610
641
  }
611
642
  // Exactly one profile's project dir IS cwd — unambiguous project root, use it
@@ -633,10 +664,10 @@ export async function selectProfileForCwd(cwd, explicitProfile, forcePicker = fa
633
664
  // the create-or-root prompt (root is only ever offered there).
634
665
  const validPinnedId = pinned !== null ? pinned.profileId : null;
635
666
  const chosen = exact.length > 1
636
- ? await promptPickProfileOrCreate(exact, resolvedCwd, true, validPinnedId)
667
+ ? await promptPickProfileOrCreate(exact, resolvedCwd, true, validPinnedId, bindings)
637
668
  : covering.length > 0
638
- ? await promptPickProfileOrCreate(covering, resolvedCwd, false, validPinnedId)
639
- : await promptCreateOrRoot(resolvedCwd);
669
+ ? await promptPickProfileOrCreate(covering, resolvedCwd, false, validPinnedId, bindings)
670
+ : await promptCreateOrRoot(resolvedCwd, bindings);
640
671
  updateProfileLastUsed(chosen);
641
672
  return chosen;
642
673
  }
@@ -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
+ }
@@ -0,0 +1,13 @@
1
+ export interface BannerInfo {
2
+ /** crtr version, e.g. "0.3.78" → shown as "v0.3.78". */
3
+ version?: string;
4
+ /** Active profile name, e.g. "northlight". */
5
+ profile?: string;
6
+ /** Launch directory (already home-abbreviated if desired). */
7
+ cwd?: string;
8
+ }
9
+ /** The crouton banner as pre-colored terminal lines: the cube on the left with
10
+ * name/version, tagline, profile, and launch path stacked beside rows 1–4.
11
+ * Missing facts are simply omitted (their row shows just the cube). No
12
+ * leading/trailing blank lines — the caller frames it. */
13
+ export declare function croutonBannerLines(info?: BannerInfo): string[];
@@ -0,0 +1,51 @@
1
+ // The crouton welcome banner — a little crouton rendered at the very top of a
2
+ // node's transcript in the attach viewer (the way Claude Code opens with its
3
+ // logo + session facts), with the launch facts stacked to its right: name +
4
+ // version, tagline, profile, and the launch path. It scrolls up into history as
5
+ // the conversation grows. Purely cosmetic.
6
+ //
7
+ // This lives in the VIEWER, not the front-door stdout: the attach TUI takes over
8
+ // the whole terminal, so anything printed to stdout before it starts is wiped.
9
+ // ChatView pins these lines as the first children of its chat container.
10
+ // Clean line-art isometric cube in a single warm crust tone, with a few subtly
11
+ // darker toast flecks. No background fills (they read as detached color slabs in
12
+ // a dark terminal) and no inverted-brightness speckle.
13
+ const RESET = '\u001b[0m';
14
+ const CRUST = '\u001b[38;5;180m'; // warm tan — cube outline + accents
15
+ const FLECK = '\u001b[38;5;137m'; // slightly darker toast dots
16
+ const BOLD = '\u001b[1m';
17
+ const DIM = '\u001b[2m';
18
+ // Darker toast flecks on the cube faces; `.` glyphs recolor a touch darker.
19
+ function crumb(text) {
20
+ return CRUST + text.replace(/\./g, `${FLECK}.${CRUST}`) + RESET;
21
+ }
22
+ // Rows 1–4 all share the same visible width (15 cells), so a fixed 3-space
23
+ // gutter lands every info line in the same column — the stacked-facts layout.
24
+ const CROUTON = [
25
+ crumb(' ________'),
26
+ crumb(' / /|'),
27
+ crumb(' / . . / |'),
28
+ crumb(' /_______/ |'),
29
+ crumb(' | . . | |'),
30
+ crumb(' | . . | /'),
31
+ crumb(' |_______|/'),
32
+ ];
33
+ /** The crouton banner as pre-colored terminal lines: the cube on the left with
34
+ * name/version, tagline, profile, and launch path stacked beside rows 1–4.
35
+ * Missing facts are simply omitted (their row shows just the cube). No
36
+ * leading/trailing blank lines — the caller frames it. */
37
+ export function croutonBannerLines(info = {}) {
38
+ const name = `${BOLD}${CRUST}crouter${RESET}${info.version ? `${DIM} v${info.version}${RESET}` : ''}`;
39
+ const facts = [name, `${DIM}the agentic runtime${RESET}`];
40
+ if (info.profile)
41
+ facts.push(`${DIM}profile: ${RESET}${CRUST}${info.profile}${RESET}`);
42
+ if (info.cwd)
43
+ facts.push(`${DIM}${info.cwd}${RESET}`);
44
+ const out = [];
45
+ for (let i = 0; i < CROUTON.length; i++) {
46
+ // Facts sit beside rows 1..N (the top of the cube), one per row.
47
+ const fact = i >= 1 && i - 1 < facts.length ? ` ${facts[i - 1]}` : '';
48
+ out.push(CROUTON[i] + fact);
49
+ }
50
+ return out;
51
+ }
@@ -19,7 +19,7 @@ import { createServer } from 'node:net';
19
19
  import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
20
20
  import { execFile } from 'node:child_process';
21
21
  import { tmpdir } from 'node:os';
22
- import { dirname, join } from 'node:path';
22
+ import { join } from 'node:path';
23
23
  import { randomUUID } from 'node:crypto';
24
24
  import { getAgentDir, initTheme, } from '@earendil-works/pi-coding-agent';
25
25
  import { jobDir, nodeDir, sessionListCachePath, viewSocketPath } from '../canvas/paths.js';
@@ -35,7 +35,7 @@ import { CONNECTION_FAULT_RECOVERY_BODY, PROVIDER_FAULT_RECOVERY_BODY } from './
35
35
  import { markFatalOnExhaust, nextFaultRetry, policyFor } from './fault-recovery.js';
36
36
  import { probeOnline as probeOnlineReal } from './connectivity.js';
37
37
  import { CRTR_CYCLE_CUSTOM_TYPE, cycleAwareMessages } from './session-cycles.js';
38
- import { BUILTIN_SLASH_COMMANDS } from './pi-vendored.js';
38
+ import { BUILTIN_SLASH_COMMANDS, piSessionsRoot } from './pi-vendored.js';
39
39
  import { StreamWatchdog, resolveStreamWatchdogMs } from './stream-watchdog.js';
40
40
  import { encodeFrame, FrameDecoder, FrameOverflowError, BROKER_READ_CAPS, } from './broker-protocol.js';
41
41
  const THINKING_LEVELS = new Set(['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
@@ -1197,15 +1197,16 @@ export async function runBroker(nodeId) {
1197
1197
  return { type: 'data', id, kind: 'get_settings', settings };
1198
1198
  };
1199
1199
  // Cache per-session wire info keyed by (path, mtime, size) so reopening the
1200
- // picker doesn't re-read the whole corpus (see session-list-cache.ts). The
1201
- // sessions root is `dirname` of any cwd's session dir — no need to resolve
1202
- // pi's private path config.
1200
+ // picker doesn't re-read the whole corpus (see session-list-cache.ts).
1203
1201
  const sessionListCache = new SessionListCache(sessionListCachePath());
1202
+ // Pre-warm the cwd scope (the picker's default tab) in the background so the
1203
+ // first /resume open on this fresh broker is already warm.
1204
+ sessionListCache.prewarm(session.sessionManager.getSessionDir());
1204
1205
  const buildSessionsData = async (id, scope) => {
1205
1206
  const sm = session.sessionManager;
1206
1207
  const sessionDir = sm.getSessionDir();
1207
1208
  const sessions = scope === 'all'
1208
- ? await sessionListCache.listAll(dirname(sessionDir))
1209
+ ? await sessionListCache.listAll(piSessionsRoot())
1209
1210
  : await sessionListCache.listDir(sessionDir);
1210
1211
  return {
1211
1212
  type: 'data',
@@ -1,3 +1,11 @@
1
+ /** pi's sessions root, VENDORED from pi `config.getSessionsDir()` (= `<agentDir>/
2
+ * sessions`). pi's package `exports` map is `.`-only, so config.js can't be
3
+ * deep-imported, and a ROOT import of `getAgentDir` would eager-load the entire
4
+ * heavy pi SDK index on crtr's front-door hot path (the reason broker-sdk.ts
5
+ * dynamic-imports the engine). Mirrors pi: `PI_CODING_AGENT_DIR` env, else
6
+ * `~/.pi/agent` (APP_NAME defaults to 'pi'). Re-sync on a pi SDK bump that moves
7
+ * the sessions dir — same vendoring rationale as the slash-command list below. */
8
+ export declare function piSessionsRoot(): string;
1
9
  /**
2
10
  * pi's builtin slash commands — vendored verbatim from pi `core/slash-commands.js`
3
11
  * `BUILTIN_SLASH_COMMANDS` (review C1). **21 entries at 0.78.1** (review n1 — NOT 23).