@pellux/goodvibes-tui 1.1.0 → 1.9.0

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 (267) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +28 -18
  3. package/docs/foundation-artifacts/operator-contract.json +4045 -1763
  4. package/package.json +2 -2
  5. package/src/audio/spoken-turn-controller.ts +12 -2
  6. package/src/audio/spoken-turn-wiring.ts +2 -1
  7. package/src/cli/help.ts +8 -1
  8. package/src/cli/service-posture.ts +2 -1
  9. package/src/cli/status.ts +2 -1
  10. package/src/cli/surface-command.ts +2 -2
  11. package/src/core/alert-gating.ts +67 -0
  12. package/src/core/approval-alert.ts +72 -0
  13. package/src/core/budget-breach-notifier.ts +106 -0
  14. package/src/core/composer-state.ts +11 -3
  15. package/src/core/conversation-line-cache.ts +27 -3
  16. package/src/core/conversation-rendering.ts +90 -14
  17. package/src/core/conversation.ts +18 -0
  18. package/src/core/focus-tracker.ts +41 -0
  19. package/src/core/long-task-notifier.ts +33 -6
  20. package/src/core/stream-event-wiring.ts +20 -1
  21. package/src/core/system-message-noise.ts +87 -0
  22. package/src/core/system-message-router.ts +104 -51
  23. package/src/core/turn-cancellation.ts +25 -0
  24. package/src/core/turn-event-wiring.ts +72 -3
  25. package/src/daemon/cli.ts +29 -2
  26. package/src/daemon/handlers/register.ts +8 -1
  27. package/src/daemon/service-commands.ts +329 -0
  28. package/src/export/cost-utils.ts +36 -0
  29. package/src/input/autocomplete.ts +27 -1
  30. package/src/input/command-registry.ts +84 -5
  31. package/src/input/commands/checkpoint-runtime.ts +280 -0
  32. package/src/input/commands/codebase-runtime.ts +232 -0
  33. package/src/input/commands/config.ts +43 -3
  34. package/src/input/commands/eval.ts +1 -1
  35. package/src/input/commands/health-runtime.ts +10 -2
  36. package/src/input/commands/image-runtime.ts +112 -0
  37. package/src/input/commands/intelligence-runtime.ts +11 -1
  38. package/src/input/commands/local-auth-runtime.ts +4 -1
  39. package/src/input/commands/local-runtime.ts +9 -3
  40. package/src/input/commands/marketplace-runtime.ts +2 -2
  41. package/src/input/commands/memory.ts +73 -35
  42. package/src/input/commands/operator-panel-runtime.ts +71 -31
  43. package/src/input/commands/planning-runtime.ts +116 -4
  44. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  45. package/src/input/commands/plugin-runtime.ts +2 -2
  46. package/src/input/commands/policy-dispatch.ts +21 -0
  47. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  48. package/src/input/commands/qrcode-runtime.ts +26 -6
  49. package/src/input/commands/recall-review.ts +35 -0
  50. package/src/input/commands/remote-runtime-setup.ts +5 -3
  51. package/src/input/commands/remote-runtime.ts +3 -3
  52. package/src/input/commands/runtime-services.ts +54 -13
  53. package/src/input/commands/services-runtime.ts +1 -1
  54. package/src/input/commands/session-content.ts +20 -9
  55. package/src/input/commands/session-workflow.ts +16 -1
  56. package/src/input/commands/settings-sync-runtime.ts +16 -4
  57. package/src/input/commands/shell-core.ts +25 -8
  58. package/src/input/commands/skills-runtime.ts +2 -8
  59. package/src/input/commands/subscription-runtime.ts +2 -2
  60. package/src/input/commands/test-runtime.ts +277 -0
  61. package/src/input/commands/websearch-runtime.ts +101 -0
  62. package/src/input/commands/work-plan-runtime.ts +36 -10
  63. package/src/input/commands/workstream-runtime.ts +488 -0
  64. package/src/input/commands.ts +12 -0
  65. package/src/input/config-modal-types.ts +175 -0
  66. package/src/input/config-modal.ts +592 -0
  67. package/src/input/feed-context-factory.ts +10 -8
  68. package/src/input/handler-command-route.ts +37 -20
  69. package/src/input/handler-content-actions.ts +17 -2
  70. package/src/input/handler-feed-routes.ts +74 -114
  71. package/src/input/handler-feed.ts +79 -27
  72. package/src/input/handler-interactions.ts +1 -3
  73. package/src/input/handler-modal-routes.ts +136 -6
  74. package/src/input/handler-modal-stack.ts +14 -7
  75. package/src/input/handler-modal-token-routes.ts +16 -49
  76. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  77. package/src/input/handler-onboarding.ts +71 -59
  78. package/src/input/handler-picker-routes.ts +26 -102
  79. package/src/input/handler-shortcuts.ts +74 -14
  80. package/src/input/handler-types.ts +2 -6
  81. package/src/input/handler-ui-state.ts +10 -22
  82. package/src/input/handler.ts +12 -29
  83. package/src/input/keybindings.ts +23 -8
  84. package/src/input/model-picker-types.ts +24 -6
  85. package/src/input/model-picker.ts +77 -2
  86. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  87. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  88. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  89. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  90. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  91. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  92. package/src/input/panel-integration-actions.ts +9 -170
  93. package/src/input/panel-mouse-geometry.ts +97 -0
  94. package/src/input/panel-paste-flood-guard.ts +86 -0
  95. package/src/input/selection-modal.ts +11 -0
  96. package/src/input/session-picker-modal.ts +44 -4
  97. package/src/input/settings-modal-data.ts +146 -2
  98. package/src/input/settings-modal-mutations.ts +6 -4
  99. package/src/input/settings-modal-types.ts +4 -2
  100. package/src/main.ts +49 -51
  101. package/src/panels/agent-inspector-shared.ts +2 -1
  102. package/src/panels/base-panel.ts +22 -1
  103. package/src/panels/builtin/agent.ts +21 -107
  104. package/src/panels/builtin/development.ts +15 -61
  105. package/src/panels/builtin/knowledge.ts +8 -32
  106. package/src/panels/builtin/operations.ts +84 -428
  107. package/src/panels/builtin/session.ts +21 -112
  108. package/src/panels/builtin/shared.ts +18 -46
  109. package/src/panels/builtin-modals.ts +218 -0
  110. package/src/panels/builtin-panels.ts +5 -0
  111. package/src/panels/cost-tracker-panel.ts +36 -10
  112. package/src/panels/diff-panel.ts +12 -43
  113. package/src/panels/eval-registry.ts +60 -0
  114. package/src/panels/fleet-deep-link.ts +31 -0
  115. package/src/panels/fleet-panel-format.ts +62 -0
  116. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  117. package/src/panels/fleet-panel.ts +758 -0
  118. package/src/panels/fleet-read-model.ts +520 -0
  119. package/src/panels/fleet-steer.ts +125 -0
  120. package/src/panels/fleet-stop.ts +153 -0
  121. package/src/panels/fleet-tabs.ts +230 -0
  122. package/src/panels/fleet-transcript.ts +356 -0
  123. package/src/panels/index.ts +7 -31
  124. package/src/panels/modals/hooks-modal.ts +187 -0
  125. package/src/panels/modals/keybindings-modal.ts +166 -0
  126. package/src/panels/modals/knowledge-modal.ts +158 -0
  127. package/src/panels/modals/local-auth-modal.ts +132 -0
  128. package/src/panels/modals/marketplace-modal.ts +218 -0
  129. package/src/panels/modals/memory-modal.ts +180 -0
  130. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  131. package/src/panels/modals/modal-theme.ts +42 -0
  132. package/src/panels/modals/pairing-modal.ts +163 -0
  133. package/src/panels/modals/planning-modal.ts +304 -0
  134. package/src/panels/modals/plugins-modal.ts +174 -0
  135. package/src/panels/modals/policy-modal.ts +256 -0
  136. package/src/panels/modals/provider-health-modal.ts +136 -0
  137. package/src/panels/modals/remote-modal.ts +115 -0
  138. package/src/panels/modals/sandbox-modal.ts +150 -0
  139. package/src/panels/modals/security-modal.ts +217 -0
  140. package/src/panels/modals/services-modal.ts +179 -0
  141. package/src/panels/modals/settings-sync-modal.ts +142 -0
  142. package/src/panels/modals/skills-modal.ts +189 -0
  143. package/src/panels/modals/subscription-modal.ts +172 -0
  144. package/src/panels/modals/work-plan-modal.ts +177 -0
  145. package/src/panels/panel-confirm-overlay.ts +72 -0
  146. package/src/panels/panel-manager.ts +123 -9
  147. package/src/panels/polish-core.ts +38 -25
  148. package/src/panels/project-planning-answer-actions.ts +8 -11
  149. package/src/panels/skills-panel.ts +7 -51
  150. package/src/panels/types.ts +37 -0
  151. package/src/permissions/hunk-selection.ts +179 -0
  152. package/src/permissions/prompt.ts +216 -13
  153. package/src/renderer/autocomplete-overlay.ts +28 -2
  154. package/src/renderer/compaction-history-modal.ts +19 -3
  155. package/src/renderer/compaction-preview.ts +13 -2
  156. package/src/renderer/compaction-quality.ts +100 -0
  157. package/src/renderer/compositor.ts +2 -3
  158. package/src/renderer/config-modal.ts +95 -0
  159. package/src/renderer/conversation-overlays.ts +10 -17
  160. package/src/renderer/fleet-tab-strip.ts +56 -0
  161. package/src/renderer/footer-tips.ts +15 -3
  162. package/src/renderer/fullscreen-primitives.ts +32 -22
  163. package/src/renderer/git-status.ts +43 -1
  164. package/src/renderer/help-overlay.ts +2 -2
  165. package/src/renderer/layout.ts +0 -4
  166. package/src/renderer/markdown.ts +7 -3
  167. package/src/renderer/modal-factory.ts +25 -20
  168. package/src/renderer/model-workspace.ts +62 -4
  169. package/src/renderer/overlay-box.ts +21 -17
  170. package/src/renderer/panel-workspace-bar.ts +8 -2
  171. package/src/renderer/process-indicator.ts +14 -3
  172. package/src/renderer/selection-modal-overlay.ts +6 -1
  173. package/src/renderer/session-picker-modal.ts +196 -3
  174. package/src/renderer/settings-modal-helpers.ts +2 -0
  175. package/src/renderer/settings-modal.ts +7 -0
  176. package/src/renderer/shell-surface.ts +8 -1
  177. package/src/renderer/status-glyphs.ts +14 -15
  178. package/src/renderer/system-message.ts +15 -3
  179. package/src/renderer/terminal-bg-probe.ts +339 -0
  180. package/src/renderer/terminal-escapes.ts +20 -0
  181. package/src/renderer/theme-mode-config.ts +67 -0
  182. package/src/renderer/theme.ts +91 -1
  183. package/src/renderer/thinking.ts +11 -3
  184. package/src/renderer/tool-call.ts +15 -9
  185. package/src/renderer/tool-result-summary.ts +148 -0
  186. package/src/renderer/turn-injection.ts +133 -0
  187. package/src/renderer/ui-factory.ts +154 -85
  188. package/src/renderer/ui-primitives.ts +30 -129
  189. package/src/runtime/bootstrap-command-context.ts +27 -1
  190. package/src/runtime/bootstrap-command-parts.ts +41 -10
  191. package/src/runtime/bootstrap-core.ts +47 -15
  192. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  193. package/src/runtime/bootstrap-shell.ts +48 -17
  194. package/src/runtime/bootstrap.ts +129 -22
  195. package/src/runtime/code-index-services.ts +135 -0
  196. package/src/runtime/legacy-daemon-migration.ts +516 -0
  197. package/src/runtime/memory-fold.ts +26 -0
  198. package/src/runtime/onboarding/derivation.ts +7 -2
  199. package/src/runtime/onboarding/snapshot.ts +27 -1
  200. package/src/runtime/onboarding/types.ts +31 -1
  201. package/src/runtime/operator-token-cleanup.ts +82 -1
  202. package/src/runtime/orchestrator-core-services.ts +59 -0
  203. package/src/runtime/process-lifecycle.ts +3 -1
  204. package/src/runtime/resume-notice.ts +209 -0
  205. package/src/runtime/services.ts +98 -46
  206. package/src/runtime/session-inbound-inputs.ts +252 -0
  207. package/src/runtime/session-spine-transport.ts +64 -0
  208. package/src/runtime/terminal-output-guard.ts +15 -8
  209. package/src/runtime/ui-services.ts +27 -3
  210. package/src/runtime/workstream-services.ts +327 -0
  211. package/src/runtime/wrfc-persistence.ts +124 -17
  212. package/src/shell/blocking-input.ts +68 -4
  213. package/src/shell/recovery-input-helpers.ts +170 -1
  214. package/src/shell/ui-openers.ts +171 -26
  215. package/src/utils/format-duration.ts +8 -18
  216. package/src/utils/splash-lines.ts +1 -1
  217. package/src/utils/terminal-width.ts +52 -0
  218. package/src/version.ts +1 -1
  219. package/src/panels/agent-inspector-panel.ts +0 -786
  220. package/src/panels/approval-panel.ts +0 -252
  221. package/src/panels/automation-control-panel.ts +0 -479
  222. package/src/panels/cockpit-panel.ts +0 -481
  223. package/src/panels/cockpit-read-model.ts +0 -238
  224. package/src/panels/communication-panel.ts +0 -256
  225. package/src/panels/control-plane-panel.ts +0 -470
  226. package/src/panels/debug-panel.ts +0 -615
  227. package/src/panels/docs-panel.ts +0 -384
  228. package/src/panels/eval-panel.ts +0 -627
  229. package/src/panels/file-explorer-panel.ts +0 -673
  230. package/src/panels/file-preview-panel.ts +0 -517
  231. package/src/panels/hooks-panel.ts +0 -401
  232. package/src/panels/incident-review-panel.ts +0 -406
  233. package/src/panels/intelligence-panel.ts +0 -383
  234. package/src/panels/knowledge-graph-panel.ts +0 -515
  235. package/src/panels/marketplace-panel.ts +0 -399
  236. package/src/panels/memory-panel.ts +0 -558
  237. package/src/panels/ops-control-panel.ts +0 -329
  238. package/src/panels/ops-strategy-panel.ts +0 -321
  239. package/src/panels/orchestration-panel.ts +0 -465
  240. package/src/panels/panel-list-panel.ts +0 -566
  241. package/src/panels/plan-dashboard-panel.ts +0 -707
  242. package/src/panels/policy-panel.ts +0 -517
  243. package/src/panels/project-planning-panel.ts +0 -731
  244. package/src/panels/provider-health-panel.ts +0 -678
  245. package/src/panels/provider-health-tracker.ts +0 -310
  246. package/src/panels/provider-health-views.ts +0 -567
  247. package/src/panels/qr-panel.ts +0 -280
  248. package/src/panels/remote-panel.ts +0 -534
  249. package/src/panels/routes-panel.ts +0 -241
  250. package/src/panels/sandbox-panel.ts +0 -456
  251. package/src/panels/security-panel.ts +0 -447
  252. package/src/panels/services-panel.ts +0 -329
  253. package/src/panels/session-browser-panel.ts +0 -496
  254. package/src/panels/settings-sync-panel.ts +0 -398
  255. package/src/panels/subscription-panel.ts +0 -342
  256. package/src/panels/symbol-outline-panel.ts +0 -619
  257. package/src/panels/system-messages-panel.ts +0 -364
  258. package/src/panels/tasks-panel.ts +0 -608
  259. package/src/panels/thinking-panel.ts +0 -333
  260. package/src/panels/tool-inspector-panel.ts +0 -537
  261. package/src/panels/work-plan-panel.ts +0 -540
  262. package/src/panels/worktree-panel.ts +0 -360
  263. package/src/panels/wrfc-panel.ts +0 -790
  264. package/src/renderer/agent-detail-modal.ts +0 -466
  265. package/src/renderer/live-tail-modal.ts +0 -156
  266. package/src/renderer/process-modal.ts +0 -671
  267. package/src/renderer/qr-renderer.ts +0 -120
@@ -0,0 +1,166 @@
1
+ import { infoRow } from './modal-surface-helpers.ts';
2
+ import type {
3
+ ConfigModalActionContext,
4
+ ConfigModalRow,
5
+ ConfigModalSurface,
6
+ ConfigModalTab,
7
+ ConfigModalView,
8
+ } from '../../input/config-modal-types.ts';
9
+ import { KeybindingsManager } from '../../input/keybindings.ts';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Docs + Shortcuts → 'keybindings' config-modal surface (W6.1 group-B port).
13
+ // A MERGE of the retired DocsPanel (tools/models/shortcuts) and the
14
+ // shortcuts-overlay reference. Three tabs: 'Tools', 'Models', and 'Shortcuts'
15
+ // (the categorized keyboard reference — driven live off keybindingsManager so
16
+ // user overrides show up — followed by an exhaustive 'All Bindings (live)'
17
+ // table). Enter opens the tool-inspector successor ('fleet') or switches the
18
+ // active model, both via the command path (fleet is a panel; /model is a
19
+ // settings mutation). Selection-blind port: the panel's selected tool/model
20
+ // detail is folded into each row label. The panel's live '/' filter is dropped
21
+ // (the host has no query state).
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export interface KeybindingsModalTool {
25
+ readonly definition: {
26
+ readonly name: string;
27
+ readonly description?: string;
28
+ readonly sideEffects?: readonly string[];
29
+ readonly concurrency?: string;
30
+ readonly supportsProgress?: boolean;
31
+ readonly supportsStreamingOutput?: boolean;
32
+ };
33
+ }
34
+ export interface KeybindingsModalToolRegistry { list(): readonly KeybindingsModalTool[]; }
35
+ export interface KeybindingsModalModel { readonly id: string; readonly provider: string; readonly registryKey: string; readonly displayName: string; readonly contextWindow: number; readonly selectable: boolean; }
36
+ export interface KeybindingsModalProviderRegistry { listModels(): readonly KeybindingsModalModel[]; getCurrentModel?(): { readonly registryKey: string } | undefined; }
37
+
38
+ export interface KeybindingsModalDeps {
39
+ readonly toolRegistry?: KeybindingsModalToolRegistry;
40
+ readonly providerRegistry?: KeybindingsModalProviderRegistry;
41
+ readonly keybindingsManager?: KeybindingsManager;
42
+ }
43
+
44
+ class KeybindingsModalSurface implements ConfigModalSurface {
45
+ readonly name = 'keybindings-modal';
46
+ readonly title = 'Keybindings';
47
+
48
+ constructor(private readonly deps: KeybindingsModalDeps) {}
49
+
50
+ readonly actions = [
51
+ { key: 'enter', id: 'activate', label: 'activate', enabledFor: (row: ConfigModalRow | null, tabId: string) => (tabId === 'tools') || (tabId === 'models' && row !== null) },
52
+ { key: 'r', id: 'refresh', label: 'refresh' },
53
+ ];
54
+
55
+ private sortedModels(): readonly KeybindingsModalModel[] {
56
+ const models = this.deps.providerRegistry?.listModels() ?? [];
57
+ return [...models].sort((a, b) => a.provider.localeCompare(b.provider) || a.displayName.localeCompare(b.displayName));
58
+ }
59
+
60
+ private toolsTab(): ConfigModalTab {
61
+ if (!this.deps.toolRegistry) return { id: 'tools', label: 'Tools', rows: [infoRow('tools:none', 'Tool registry not wired into this session.', { dim: true })], emptyText: 'Tool registry not wired into this session.' };
62
+ const tools = this.deps.toolRegistry.list();
63
+ const rows: ConfigModalRow[] = tools.map((tool) => {
64
+ const d = tool.definition;
65
+ const meta: string[] = [];
66
+ if (d.sideEffects?.length) meta.push(`effects: ${d.sideEffects.join(', ')}`);
67
+ if (d.concurrency) meta.push(`concurrency: ${d.concurrency}`);
68
+ if (d.supportsProgress) meta.push('progress');
69
+ if (d.supportsStreamingOutput) meta.push('streaming');
70
+ const detail = [d.description, meta.join(' | ') || null].filter((s): s is string => Boolean(s)).join(' · ');
71
+ return { id: `tool:${d.name}`, label: detail ? `${d.name.padEnd(22)} ${detail}` : d.name };
72
+ });
73
+ return { id: 'tools', label: 'Tools', rows, emptyText: 'No tools registered.', hints: ['enter open in fleet'] };
74
+ }
75
+
76
+ private modelsTab(): ConfigModalTab {
77
+ if (!this.deps.providerRegistry) return { id: 'models', label: 'Models', rows: [infoRow('models:none', 'Provider registry not wired into this session.', { dim: true })], emptyText: 'Provider registry not wired into this session.' };
78
+ const activeKey = this.deps.providerRegistry.getCurrentModel?.()?.registryKey;
79
+ const rows: ConfigModalRow[] = this.sortedModels().map((model) => {
80
+ const ctxK = model.contextWindow > 0 ? `${(model.contextWindow / 1000).toFixed(0)}k` : '?';
81
+ const isActive = model.registryKey === activeKey;
82
+ return { id: `model:${model.registryKey}`, label: `${model.provider.padEnd(14)} ${model.displayName.padEnd(24)} ctx:${ctxK}${isActive ? ' ACTIVE' : ''} ${model.id}${model.selectable ? '' : ' (not selectable)'}` };
83
+ });
84
+ return { id: 'models', label: 'Models', rows, emptyText: 'No models registered.', hints: ['enter set active model'] };
85
+ }
86
+
87
+ private shortcutsTab(): ConfigModalTab {
88
+ const km = this.deps.keybindingsManager;
89
+ if (!km) return { id: 'shortcuts', label: 'Shortcuts', rows: [infoRow('sc:none', 'Keybindings manager not wired into this session.', { dim: true })], emptyText: 'Keybindings manager not wired into this session.' };
90
+ const kb = (action: Parameters<KeybindingsManager['getComboLabel']>[0]) => km.getComboLabel(action);
91
+ const rows: ConfigModalRow[] = [];
92
+ let n = 0;
93
+ const category = (title: string, entries: ReadonlyArray<readonly [string, string]>): void => {
94
+ rows.push(infoRow(`sc:t:${n++}`, title));
95
+ for (const [key, desc] of entries) rows.push(infoRow(`sc:${n++}`, `${key.padEnd(20)} ${desc}`));
96
+ };
97
+ category('Navigation', [['Up / Down', 'Scroll / history recall'], ['PageUp / PageDn', 'Scroll by full page'], ['Home / End', 'Jump to start / end of line'], [kb('search'), 'Search conversation'], ['n / N (search)', 'Next / previous match'], ['Mouse wheel', 'Scroll conversation or hovered panel']]);
98
+ category('Editing', [['Enter', 'Submit message'], ['Shift+Enter', 'Insert newline'], ['@', 'Open file picker'], ['/', 'Slash command mode'], [kb('paste'), 'Paste (image priority)'], [`${kb('undo')} / ${kb('redo')}`, 'Undo / redo'], [kb('clear-prompt'), 'Clear prompt'], [kb('delete-word'), 'Delete word backward'], [kb('kill-line'), 'Kill to end of line']]);
99
+ category('Actions', [['Tab', 'Collapse/expand block'], [kb('bookmark'), 'Bookmark block'], [kb('block-copy'), 'Copy block to clipboard'], [kb('block-save'), 'Save block to file'], [kb('copy-selection'), 'Copy selection'], ['F2', 'Process monitor'], ['?', 'Help overlay'], [`${kb('clear-cancel')} x2`, 'Exit']]);
100
+ category('Panels', [['Tab', 'Swap focus between input and panel workspace'], [kb('panel-picker'), 'Open / focus / hide panel workspace'], [kb('panel-tab-next'), 'Next workspace panel tab'], [kb('panel-tab-prev'), 'Previous workspace panel tab'], [`${kb('panel-tab-1')}...${kb('panel-tab-9')}`, 'Jump to workspace tab 1-9'], [kb('panel-focus-toggle'), 'Swap focus between top / bottom pane'], [kb('panel-close'), 'Close active panel'], [kb('panel-close-all'), 'Close all panels'], [kb('panel-ops'), 'Open the Ops Control panel']]);
101
+ category('In-Panel Controls', [['j / k', 'Move selection down / up'], ['g / G', 'Jump to top / bottom'], ['/', 'Filter the list']]);
102
+ const allBindings = km.getAll().map((entry) => [entry.combos.length > 0 ? entry.combos.map((combo) => km.formatCombo(combo)).join(', ') : '(unbound)', entry.description] as const);
103
+ category('All Bindings (live)', allBindings);
104
+ return { id: 'shortcuts', label: 'Shortcuts', rows, emptyText: 'No shortcuts recorded.' };
105
+ }
106
+
107
+ buildView(): ConfigModalView {
108
+ return { title: 'Keybindings', tabs: [this.toolsTab(), this.modelsTab(), this.shortcutsTab()] };
109
+ }
110
+
111
+ onAction(id: string, ctx: ConfigModalActionContext): void {
112
+ if (id === 'refresh') { ctx.setStatus('Docs & shortcuts are read live.'); ctx.requestRender(); return; }
113
+ if (id !== 'activate') return;
114
+ if (ctx.tabId === 'tools') {
115
+ // DEBT-5 item 4: pass the tool name through as a deep-link target.
116
+ // Honest caveat (documented, not a bug to silently paper over): fleet's
117
+ // ProcessKind set has no 'tool' node — a docs Tools row names a static
118
+ // tool DEFINITION, not a live process, so FleetPanel.receiveDeepLink
119
+ // will not currently find a match and shows its honest "node no longer
120
+ // present" line. The plumbing is still worth wiring now (matches the
121
+ // work-plan agent/wrfc jumps' shape) — it starts resolving for free the
122
+ // day fleet grows a per-tool-call node (the retired DocsPanel's own
123
+ // comment already anticipated this: "no filter-by-tool equivalent
124
+ // there yet").
125
+ const toolName = ctx.row?.id.startsWith('tool:') ? ctx.row.id.slice('tool:'.length) : null;
126
+ void ctx.executeCommand?.('panel', toolName ? ['open', 'fleet', '--target', `${toolName}:tool`] : ['open', 'fleet']);
127
+ ctx.setStatus('Opened the tool inspector (fleet).');
128
+ return;
129
+ }
130
+ if (ctx.tabId === 'models') {
131
+ const key = ctx.row?.id.startsWith('model:') ? ctx.row.id.slice('model:'.length) : null;
132
+ if (!key) return;
133
+ const model = this.sortedModels().find((m) => m.registryKey === key);
134
+ if (!model || !model.selectable) { ctx.setStatus('This model is not selectable.'); return; }
135
+ void ctx.executeCommand?.('model', [model.registryKey]);
136
+ ctx.setStatus(`Dispatched /model ${model.registryKey}.`);
137
+ }
138
+ }
139
+ }
140
+
141
+ export function createKeybindingsModalSurface(deps: KeybindingsModalDeps): ConfigModalSurface {
142
+ return new KeybindingsModalSurface(deps);
143
+ }
144
+
145
+ /**
146
+ * Deterministic golden fixture. KeybindingsManager is pointed at a nonexistent
147
+ * config path so it always resolves to DEFAULT_KEYBINDINGS. Tool and model
148
+ * fixtures are frozen literals — no disk, no live registry.
149
+ */
150
+ export function keybindingsModalGoldenSurface(): ConfigModalSurface {
151
+ const keybindingsManager = new KeybindingsManager({ configPath: '/nonexistent/golden-keybindings.json' });
152
+ const toolRegistry: KeybindingsModalToolRegistry = {
153
+ list: () => [
154
+ { definition: { name: 'read_file', description: 'Read a file from disk.', sideEffects: ['read_fs'], concurrency: 'parallel', supportsProgress: false, supportsStreamingOutput: false } },
155
+ { definition: { name: 'run_shell', description: 'Execute a shell command.', sideEffects: ['exec'], concurrency: 'serial', supportsProgress: true, supportsStreamingOutput: true } },
156
+ ],
157
+ };
158
+ const providerRegistry: KeybindingsModalProviderRegistry = {
159
+ listModels: () => [
160
+ { id: 'golden-a', provider: 'golden-provider', registryKey: 'golden-provider:golden-a', displayName: 'Golden Model A', contextWindow: 128000, selectable: true },
161
+ { id: 'golden-b', provider: 'golden-provider', registryKey: 'golden-provider:golden-b', displayName: 'Golden Model B', contextWindow: 32000, selectable: false },
162
+ ],
163
+ getCurrentModel: () => ({ registryKey: 'golden-provider:golden-a' }),
164
+ };
165
+ return createKeybindingsModalSurface({ toolRegistry, providerRegistry, keybindingsManager });
166
+ }
@@ -0,0 +1,158 @@
1
+ import { infoRow } from './modal-surface-helpers.ts';
2
+ import type {
3
+ ConfigModalActionContext,
4
+ ConfigModalRow,
5
+ ConfigModalSurface,
6
+ ConfigModalTab,
7
+ ConfigModalView,
8
+ } from '../../input/config-modal-types.ts';
9
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Knowledge (graph) → config-modal surface (W6.1 group-B port). Two tabs:
13
+ // 'Browse' (sources/nodes/issues + schedules) and 'Review' (the open-issue
14
+ // queue) — the panel's browse/review toggle, now host tabs. Issue review
15
+ // mutations (accept/reject/resolve/reopen) route to the `/knowledge
16
+ // review-issue` command. 'm' cross-opens the memory surface. Selection-blind
17
+ // port: the panel's selected-row id/detail is folded into each row label.
18
+ // refresh() is the only place this module touches the SDK.
19
+ // ---------------------------------------------------------------------------
20
+
21
+ interface KnowledgeNodeLike { readonly id: string; readonly kind: string; readonly title: string; readonly summary?: string | undefined; }
22
+ interface KnowledgeSourceLike { readonly id: string; readonly sourceType: string; readonly status: string; readonly title?: string | undefined; readonly canonicalUri?: string | undefined; readonly sourceUri?: string | undefined; readonly summary?: string | undefined; }
23
+ interface KnowledgeIssueLike { readonly id: string; readonly severity: 'info' | 'warning' | 'error'; readonly code: string; readonly message: string; readonly status: 'open' | 'resolved'; readonly sourceId?: string | undefined; readonly nodeId?: string | undefined; }
24
+ interface KnowledgeScheduleLike { readonly id: string; readonly label: string; readonly enabled: boolean; }
25
+
26
+ export interface KnowledgeModalDeps {
27
+ readonly knowledgeApi: {
28
+ readonly graph: {
29
+ readonly nodes: { list(limit?: number): readonly KnowledgeNodeLike[] };
30
+ readonly issues: { list(limit?: number): readonly KnowledgeIssueLike[] };
31
+ };
32
+ readonly sources: { list(limit?: number): readonly KnowledgeSourceLike[] };
33
+ readonly jobs: { schedules: { list(limit?: number): readonly KnowledgeScheduleLike[] } };
34
+ };
35
+ }
36
+
37
+ type BrowseKind = 'node' | 'source' | 'issue';
38
+ interface BrowseRow { readonly kind: BrowseKind; readonly id: string; readonly title: string; readonly tag: string; readonly detail: string; readonly extra: string; }
39
+
40
+ const LIST_LIMIT = 60;
41
+
42
+ function cleanInline(value: string | undefined): string { return (value ?? '').replace(/\s+/g, ' ').trim(); }
43
+
44
+ function nodeRow(node: KnowledgeNodeLike): BrowseRow {
45
+ return { kind: 'node', id: node.id, title: cleanInline(node.title) || 'untitled', tag: `node/${node.kind}`, detail: cleanInline(node.summary), extra: '' };
46
+ }
47
+ function sourceRow(source: KnowledgeSourceLike): BrowseRow {
48
+ return { kind: 'source', id: source.id, title: cleanInline(source.title) || cleanInline(source.canonicalUri) || cleanInline(source.sourceUri) || source.id, tag: `source/${source.status}`, detail: cleanInline(source.summary), extra: `type ${source.sourceType}` };
49
+ }
50
+ function issueRow(issue: KnowledgeIssueLike): BrowseRow {
51
+ const refs = [issue.sourceId ? `source ${issue.sourceId}` : null, issue.nodeId ? `node ${issue.nodeId}` : null].filter((ref): ref is string => ref !== null);
52
+ return { kind: 'issue', id: issue.id, title: issue.code, tag: `issue/${issue.severity}`, detail: cleanInline(issue.message), extra: refs.join(' ') };
53
+ }
54
+
55
+ class KnowledgeModalSurface implements ConfigModalSurface {
56
+ readonly name = 'knowledge-modal';
57
+ readonly title = 'Knowledge';
58
+ private browseRows: BrowseRow[] = [];
59
+ private reviewRows: BrowseRow[] = [];
60
+ private sourceCount = 0;
61
+ private nodeCount = 0;
62
+ private issueCount = 0;
63
+ private schedules: readonly KnowledgeScheduleLike[] = [];
64
+ private loadError: string | null = null;
65
+
66
+ constructor(private readonly deps: KnowledgeModalDeps) {}
67
+
68
+ private readonly issueGate = (row: ConfigModalRow | null, tabId: string): boolean => tabId === 'review' && (row?.id.startsWith('issue:') ?? false);
69
+
70
+ readonly actions = [
71
+ { key: 'a', id: 'accept', label: 'accept', enabledFor: this.issueGate },
72
+ { key: 'x', id: 'reject', label: 'reject', enabledFor: this.issueGate },
73
+ { key: 's', id: 'resolve', label: 'resolve', enabledFor: this.issueGate },
74
+ { key: 'o', id: 'reopen', label: 'reopen', enabledFor: this.issueGate },
75
+ { key: 'm', id: 'openMemory', label: 'memory' },
76
+ { key: 'r', id: 'refresh', label: 'refresh' },
77
+ ];
78
+
79
+ onOpen(): void { this.refresh(); }
80
+
81
+ private refresh(): void {
82
+ try {
83
+ const nodes = this.deps.knowledgeApi.graph.nodes.list(LIST_LIMIT);
84
+ const sources = this.deps.knowledgeApi.sources.list(LIST_LIMIT);
85
+ const issues = this.deps.knowledgeApi.graph.issues.list(LIST_LIMIT);
86
+ this.sourceCount = sources.length;
87
+ this.nodeCount = nodes.length;
88
+ this.issueCount = issues.length;
89
+ this.browseRows = [...sources.map(sourceRow), ...nodes.map(nodeRow), ...issues.map(issueRow)];
90
+ this.reviewRows = issues.filter((issue) => issue.status === 'open').map(issueRow);
91
+ this.schedules = this.deps.knowledgeApi.jobs.schedules.list(20);
92
+ this.loadError = null;
93
+ } catch (e) {
94
+ this.browseRows = []; this.reviewRows = []; this.schedules = [];
95
+ this.loadError = `Knowledge graph load failed: ${summarizeError(e)}`;
96
+ }
97
+ }
98
+
99
+ private rowFor(row: BrowseRow): ConfigModalRow {
100
+ return { id: `${row.kind}:${row.id}`, label: `${row.tag.padEnd(16)} ${row.title.padEnd(28)} ${row.detail}${row.extra ? ` · ${row.extra}` : ''}` };
101
+ }
102
+
103
+ private browseTab(): ConfigModalTab {
104
+ const readySources = this.sourceCount > 0 && this.nodeCount > 0;
105
+ const header = [
106
+ `sources ${this.sourceCount} nodes ${this.nodeCount} issues ${this.issueCount} schedules ${this.schedules.length}`,
107
+ readySources ? 'retrieval ready (sources + nodes indexed)' : 'retrieval not ready (needs sources and nodes)',
108
+ ];
109
+ const rows: ConfigModalRow[] = this.browseRows.map((row) => this.rowFor(row));
110
+ if (this.schedules.length > 0) {
111
+ rows.push(infoRow('sched:title', 'Schedules'));
112
+ this.schedules.slice(0, 4).forEach((s, i) => rows.push(infoRow(`sched:${i}`, `${s.enabled ? 'on ' : 'off'} ${s.label}`, s.enabled ? undefined : { dim: true })));
113
+ }
114
+ return { id: 'browse', label: 'Browse', header, rows, emptyText: 'No ingested knowledge yet.', hints: ['m memory'] };
115
+ }
116
+
117
+ private reviewTab(): ConfigModalTab {
118
+ return { id: 'review', label: 'Review', rows: this.reviewRows.map((row) => this.rowFor(row)), emptyText: 'No issues waiting for review.', hints: ['a accept', 'x reject', 's resolve', 'o reopen'] };
119
+ }
120
+
121
+ buildView(): ConfigModalView {
122
+ if (this.loadError) return { title: 'Knowledge', degraded: this.loadError, tabs: [{ id: 'browse', label: 'Browse', rows: [] }] };
123
+ return { title: 'Knowledge', tabs: [this.browseTab(), this.reviewTab()] };
124
+ }
125
+
126
+ onAction(id: string, ctx: ConfigModalActionContext): void {
127
+ if (id === 'refresh') { this.refresh(); ctx.setStatus('Reloaded knowledge graph.'); return; }
128
+ if (id === 'openMemory') { ctx.openModal?.('memory-modal'); return; }
129
+ const action = id === 'accept' ? 'accept' : id === 'reject' ? 'reject' : id === 'resolve' ? 'resolve' : id === 'reopen' ? 'reopen' : null;
130
+ if (!action) return;
131
+ const issueId = ctx.row?.id.startsWith('issue:') ? ctx.row.id.slice('issue:'.length) : null;
132
+ if (!issueId) return;
133
+ void ctx.executeCommand?.('knowledge', ['review-issue', issueId, action, '--reviewer', 'tui']);
134
+ ctx.setStatus(`Dispatched /knowledge review-issue ${issueId} ${action}.`);
135
+ }
136
+ }
137
+
138
+ export function createKnowledgeModalSurface(deps: KnowledgeModalDeps): ConfigModalSurface {
139
+ return new KnowledgeModalSurface(deps);
140
+ }
141
+
142
+ /**
143
+ * Deterministic golden fixture: fixed node/source/issue/schedule records —
144
+ * no live SDK calls, no random ids, no wall-clock reads.
145
+ */
146
+ export function knowledgeModalGoldenSurface(): ConfigModalSurface {
147
+ const nodes: readonly KnowledgeNodeLike[] = [{ id: 'node-1', kind: 'topic', title: 'Release cadence', summary: 'Batched internal releases, no per-change tags.' }];
148
+ const sources: readonly KnowledgeSourceLike[] = [{ id: 'source-1', sourceType: 'repo', status: 'indexed', title: 'goodvibes-tui', canonicalUri: 'repo://goodvibes-tui', summary: 'Primary TUI repository.' }];
149
+ const issues: readonly KnowledgeIssueLike[] = [{ id: 'issue-1', severity: 'warning', code: 'stale-source', message: 'Source has not been recrawled in 30 days.', status: 'open', sourceId: 'source-1' }];
150
+ const schedules: readonly KnowledgeScheduleLike[] = [{ id: 'sched-1', label: 'Nightly reindex', enabled: true }];
151
+ return createKnowledgeModalSurface({
152
+ knowledgeApi: {
153
+ graph: { nodes: { list: () => nodes }, issues: { list: () => issues } },
154
+ sources: { list: () => sources },
155
+ jobs: { schedules: { list: () => schedules } },
156
+ },
157
+ });
158
+ }
@@ -0,0 +1,132 @@
1
+ import type { ConfigModalAction, ConfigModalActionContext, ConfigModalRow, ConfigModalSurface, ConfigModalView } from '../../input/config-modal-types.ts';
2
+ import type { LocalAuthInspectionQuery } from '../../runtime/ui-service-queries.ts';
3
+ import type { UserAuthManager } from '@pellux/goodvibes-sdk/platform/security';
4
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
5
+ import { postureLine, kv } from './modal-surface-helpers.ts';
6
+
7
+ /** Mutation surface the surface needs to gate p/d/b on — a subset of UserAuthManager,
8
+ * mirroring LocalAuthPanel's own `hasLocalAuthMutations` guard. */
9
+ type LocalAuthMutations = Pick<UserAuthManager, 'addUser' | 'deleteUser' | 'rotatePassword' | 'clearBootstrapCredentialFile'>;
10
+
11
+ function hasLocalAuthMutations(value: LocalAuthInspectionQuery): value is LocalAuthInspectionQuery & LocalAuthMutations {
12
+ const candidate = value as Partial<LocalAuthMutations>;
13
+ return typeof candidate.addUser === 'function'
14
+ && typeof candidate.deleteUser === 'function'
15
+ && typeof candidate.rotatePassword === 'function'
16
+ && typeof candidate.clearBootstrapCredentialFile === 'function';
17
+ }
18
+
19
+ function formatRoles(roles: readonly string[]): string {
20
+ return roles.length > 0 ? roles.join(', ') : '(none)';
21
+ }
22
+
23
+ function usernameOf(row: ConfigModalRow | null): string | null {
24
+ return row?.id.startsWith('user:') ? row.id.slice('user:'.length) : null;
25
+ }
26
+
27
+ /**
28
+ * Local-auth config-modal surface (migrated from the `local-auth` panel).
29
+ * `inspect()` is synchronous, so buildView reads it live every tick. All
30
+ * mutations (add-user, rotate-password, delete-user, clear-bootstrap-file)
31
+ * dispatch through the existing `/local-auth` command rather than touching
32
+ * the auth manager directly — the command opens masked password entry for
33
+ * add-user/rotate-password when no password argument is supplied, which is
34
+ * exactly how the retired panel kept plaintext out of history and the
35
+ * transcript. This surface never renders, accepts, or holds a password.
36
+ */
37
+ class LocalAuthModalSurface implements ConfigModalSurface {
38
+ readonly name = 'local-auth-modal';
39
+ readonly title = 'Local Auth';
40
+ /** Cached from the last buildView() — read by the 'b' action's enabledFor,
41
+ * set fresh every tick (buildView always runs before enabledFor is
42
+ * consulted; see settings-sync-modal's `hasStaged` for the same idiom). */
43
+ private bootstrapPresent = false;
44
+
45
+ constructor(private readonly authManager: LocalAuthInspectionQuery) {}
46
+
47
+ readonly actions: ConfigModalAction[] = [
48
+ { key: 'a', id: 'add-user', label: 'add user' },
49
+ { key: 'p', id: 'rotate-pw', label: 'rotate password', enabledFor: (row) => usernameOf(row) !== null },
50
+ { key: 'd', id: 'delete', label: 'delete user', confirm: true, enabledFor: (row) => usernameOf(row) !== null },
51
+ { key: 'b', id: 'clear-bootstrap', label: 'clear bootstrap', enabledFor: () => this.bootstrapPresent },
52
+ ];
53
+
54
+ buildView(): ConfigModalView {
55
+ let mutationsUnavailable = false;
56
+ try {
57
+ mutationsUnavailable = !hasLocalAuthMutations(this.authManager);
58
+ const snapshot = this.authManager.inspect();
59
+ this.bootstrapPresent = snapshot.bootstrapCredentialPresent;
60
+
61
+ const header = [postureLine([
62
+ kv('users', snapshot.userCount),
63
+ kv('sessions', snapshot.sessionCount),
64
+ kv('bootstrap', snapshot.bootstrapCredentialPresent ? 'present' : 'cleared'),
65
+ ])];
66
+
67
+ const rows: ConfigModalRow[] = snapshot.users.map((user) => ({
68
+ id: `user:${user.username}`,
69
+ label: `${user.username} ${formatRoles(user.roles)}`,
70
+ }));
71
+
72
+ return {
73
+ title: 'Local Auth',
74
+ degraded: mutationsUnavailable ? 'Local auth mutations are not available in this session.' : undefined,
75
+ tabs: [{
76
+ id: 'users',
77
+ label: 'Users',
78
+ header,
79
+ rows,
80
+ emptyText: 'No local auth users configured.',
81
+ hints: ['a add user'],
82
+ }],
83
+ };
84
+ } catch (error) {
85
+ this.bootstrapPresent = false;
86
+ return {
87
+ title: 'Local Auth',
88
+ degraded: `Local auth inspection failed: ${summarizeError(error)}`,
89
+ tabs: [{ id: 'users', label: 'Users', rows: [], emptyText: 'Local auth data unavailable.' }],
90
+ };
91
+ }
92
+ }
93
+
94
+ onAction(id: string, ctx: ConfigModalActionContext): void {
95
+ switch (id) {
96
+ // add-user / rotate-password require the masked password-entry sub-mode,
97
+ // which renders on the LocalAuthPanel and cannot draw or capture input
98
+ // underneath this fullscreen modal. Point the operator at the command
99
+ // (which opens masked entry) rather than dispatching it into a hidden
100
+ // surface — the secure flow stays a keystroke away, just not from here.
101
+ case 'add-user':
102
+ ctx.print('To add a user securely, run /local-auth add-user <username> (opens masked password entry).');
103
+ ctx.setStatus('Run /local-auth add-user <username> for masked entry.');
104
+ break;
105
+ case 'rotate-pw': {
106
+ const username = usernameOf(ctx.row);
107
+ if (!username) return;
108
+ ctx.print(`To rotate securely, run /local-auth rotate-password ${username} (opens masked password entry).`);
109
+ ctx.setStatus(`Run /local-auth rotate-password ${username} for masked entry.`);
110
+ break;
111
+ }
112
+ case 'delete': {
113
+ const username = usernameOf(ctx.row);
114
+ if (!username) return;
115
+ void ctx.executeCommand?.('local-auth', ['delete-user', username]);
116
+ ctx.setStatus(`Deleting ${username}…`);
117
+ break;
118
+ }
119
+ case 'clear-bootstrap':
120
+ void ctx.executeCommand?.('local-auth', ['clear-bootstrap-file']);
121
+ ctx.setStatus('Clearing bootstrap credential file…');
122
+ break;
123
+ default:
124
+ return;
125
+ }
126
+ ctx.requestRender();
127
+ }
128
+ }
129
+
130
+ export function createLocalAuthModalSurface(authManager: LocalAuthInspectionQuery): ConfigModalSurface {
131
+ return new LocalAuthModalSurface(authManager);
132
+ }