@pellux/goodvibes-tui 1.1.0 → 1.7.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 (194) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +20 -11
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/core/alert-gating.ts +67 -0
  6. package/src/core/approval-alert.ts +72 -0
  7. package/src/core/budget-breach-notifier.ts +106 -0
  8. package/src/core/conversation-rendering.ts +19 -0
  9. package/src/core/conversation.ts +18 -0
  10. package/src/core/focus-tracker.ts +41 -0
  11. package/src/core/long-task-notifier.ts +33 -6
  12. package/src/core/system-message-router.ts +37 -51
  13. package/src/core/turn-cancellation.ts +20 -0
  14. package/src/core/turn-event-wiring.ts +64 -3
  15. package/src/export/cost-utils.ts +36 -0
  16. package/src/input/command-registry.ts +38 -1
  17. package/src/input/commands/checkpoint-runtime.ts +280 -0
  18. package/src/input/commands/codebase-runtime.ts +192 -0
  19. package/src/input/commands/eval.ts +1 -1
  20. package/src/input/commands/health-runtime.ts +1 -1
  21. package/src/input/commands/image-runtime.ts +112 -0
  22. package/src/input/commands/intelligence-runtime.ts +11 -1
  23. package/src/input/commands/local-auth-runtime.ts +4 -1
  24. package/src/input/commands/local-runtime.ts +9 -3
  25. package/src/input/commands/marketplace-runtime.ts +2 -2
  26. package/src/input/commands/memory.ts +6 -1
  27. package/src/input/commands/operator-panel-runtime.ts +40 -24
  28. package/src/input/commands/planning-runtime.ts +26 -3
  29. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  30. package/src/input/commands/plugin-runtime.ts +2 -2
  31. package/src/input/commands/policy-dispatch.ts +21 -0
  32. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  33. package/src/input/commands/qrcode-runtime.ts +3 -3
  34. package/src/input/commands/recall-review.ts +35 -0
  35. package/src/input/commands/remote-runtime.ts +3 -3
  36. package/src/input/commands/runtime-services.ts +54 -13
  37. package/src/input/commands/services-runtime.ts +1 -1
  38. package/src/input/commands/session-workflow.ts +16 -1
  39. package/src/input/commands/settings-sync-runtime.ts +1 -1
  40. package/src/input/commands/shell-core.ts +15 -7
  41. package/src/input/commands/skills-runtime.ts +2 -8
  42. package/src/input/commands/subscription-runtime.ts +2 -2
  43. package/src/input/commands/test-runtime.ts +277 -0
  44. package/src/input/commands/websearch-runtime.ts +101 -0
  45. package/src/input/commands/work-plan-runtime.ts +36 -10
  46. package/src/input/commands/workstream-runtime.ts +338 -0
  47. package/src/input/commands.ts +12 -0
  48. package/src/input/config-modal-types.ts +161 -0
  49. package/src/input/config-modal.ts +377 -0
  50. package/src/input/feed-context-factory.ts +7 -8
  51. package/src/input/handler-command-route.ts +27 -17
  52. package/src/input/handler-feed-routes.ts +61 -23
  53. package/src/input/handler-feed.ts +47 -23
  54. package/src/input/handler-interactions.ts +1 -3
  55. package/src/input/handler-modal-routes.ts +65 -0
  56. package/src/input/handler-modal-stack.ts +3 -5
  57. package/src/input/handler-modal-token-routes.ts +16 -49
  58. package/src/input/handler-picker-routes.ts +11 -94
  59. package/src/input/handler-shortcuts.ts +24 -14
  60. package/src/input/handler-types.ts +2 -6
  61. package/src/input/handler-ui-state.ts +10 -22
  62. package/src/input/handler.ts +6 -28
  63. package/src/input/keybindings.ts +22 -8
  64. package/src/input/model-picker-types.ts +24 -6
  65. package/src/input/model-picker.ts +58 -0
  66. package/src/input/panel-integration-actions.ts +9 -170
  67. package/src/input/settings-modal-data.ts +95 -0
  68. package/src/input/settings-modal-mutations.ts +6 -4
  69. package/src/main.ts +24 -27
  70. package/src/panels/agent-inspector-shared.ts +2 -1
  71. package/src/panels/base-panel.ts +22 -1
  72. package/src/panels/builtin/agent.ts +21 -107
  73. package/src/panels/builtin/development.ts +15 -61
  74. package/src/panels/builtin/knowledge.ts +8 -32
  75. package/src/panels/builtin/operations.ts +84 -428
  76. package/src/panels/builtin/session.ts +21 -112
  77. package/src/panels/builtin/shared.ts +9 -43
  78. package/src/panels/builtin-modals.ts +218 -0
  79. package/src/panels/builtin-panels.ts +5 -0
  80. package/src/panels/cost-tracker-panel.ts +36 -10
  81. package/src/panels/diff-panel.ts +12 -43
  82. package/src/panels/eval-registry.ts +60 -0
  83. package/src/panels/fleet-panel.ts +800 -0
  84. package/src/panels/fleet-read-model.ts +477 -0
  85. package/src/panels/fleet-steer.ts +92 -0
  86. package/src/panels/fleet-stop.ts +125 -0
  87. package/src/panels/fleet-tabs.ts +230 -0
  88. package/src/panels/fleet-transcript.ts +356 -0
  89. package/src/panels/index.ts +7 -31
  90. package/src/panels/modals/hooks-modal.ts +187 -0
  91. package/src/panels/modals/keybindings-modal.ts +151 -0
  92. package/src/panels/modals/knowledge-modal.ts +158 -0
  93. package/src/panels/modals/local-auth-modal.ts +132 -0
  94. package/src/panels/modals/marketplace-modal.ts +218 -0
  95. package/src/panels/modals/memory-modal.ts +180 -0
  96. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  97. package/src/panels/modals/modal-theme.ts +36 -0
  98. package/src/panels/modals/pairing-modal.ts +144 -0
  99. package/src/panels/modals/planning-modal.ts +282 -0
  100. package/src/panels/modals/plugins-modal.ts +174 -0
  101. package/src/panels/modals/policy-modal.ts +256 -0
  102. package/src/panels/modals/provider-health-modal.ts +136 -0
  103. package/src/panels/modals/remote-modal.ts +115 -0
  104. package/src/panels/modals/sandbox-modal.ts +150 -0
  105. package/src/panels/modals/security-modal.ts +217 -0
  106. package/src/panels/modals/services-modal.ts +179 -0
  107. package/src/panels/modals/settings-sync-modal.ts +142 -0
  108. package/src/panels/modals/skills-modal.ts +189 -0
  109. package/src/panels/modals/subscription-modal.ts +172 -0
  110. package/src/panels/modals/work-plan-modal.ts +149 -0
  111. package/src/panels/panel-confirm-overlay.ts +72 -0
  112. package/src/panels/panel-manager.ts +108 -5
  113. package/src/panels/project-planning-answer-actions.ts +4 -2
  114. package/src/panels/skills-panel.ts +7 -51
  115. package/src/panels/types.ts +13 -0
  116. package/src/permissions/hunk-selection.ts +179 -0
  117. package/src/permissions/prompt.ts +60 -4
  118. package/src/renderer/compaction-history-modal.ts +19 -3
  119. package/src/renderer/compaction-preview.ts +13 -2
  120. package/src/renderer/compaction-quality.ts +100 -0
  121. package/src/renderer/config-modal.ts +81 -0
  122. package/src/renderer/conversation-overlays.ts +10 -17
  123. package/src/renderer/fleet-tab-strip.ts +56 -0
  124. package/src/renderer/footer-tips.ts +10 -2
  125. package/src/renderer/git-status.ts +40 -0
  126. package/src/renderer/help-overlay.ts +2 -2
  127. package/src/renderer/model-workspace.ts +44 -1
  128. package/src/renderer/panel-workspace-bar.ts +8 -2
  129. package/src/renderer/turn-injection.ts +114 -0
  130. package/src/runtime/bootstrap-command-context.ts +21 -1
  131. package/src/runtime/bootstrap-command-parts.ts +34 -7
  132. package/src/runtime/bootstrap-core.ts +14 -4
  133. package/src/runtime/bootstrap-shell.ts +29 -16
  134. package/src/runtime/bootstrap.ts +11 -17
  135. package/src/runtime/code-index-services.ts +112 -0
  136. package/src/runtime/orchestrator-core-services.ts +49 -0
  137. package/src/runtime/process-lifecycle.ts +3 -1
  138. package/src/runtime/services.ts +91 -43
  139. package/src/runtime/ui-services.ts +8 -0
  140. package/src/runtime/workstream-services.ts +195 -0
  141. package/src/shell/blocking-input.ts +22 -1
  142. package/src/shell/ui-openers.ts +129 -17
  143. package/src/utils/format-duration.ts +8 -18
  144. package/src/utils/splash-lines.ts +1 -1
  145. package/src/version.ts +1 -1
  146. package/src/panels/agent-inspector-panel.ts +0 -786
  147. package/src/panels/approval-panel.ts +0 -252
  148. package/src/panels/automation-control-panel.ts +0 -479
  149. package/src/panels/cockpit-panel.ts +0 -481
  150. package/src/panels/cockpit-read-model.ts +0 -238
  151. package/src/panels/communication-panel.ts +0 -256
  152. package/src/panels/control-plane-panel.ts +0 -470
  153. package/src/panels/debug-panel.ts +0 -615
  154. package/src/panels/docs-panel.ts +0 -384
  155. package/src/panels/eval-panel.ts +0 -627
  156. package/src/panels/file-explorer-panel.ts +0 -673
  157. package/src/panels/file-preview-panel.ts +0 -517
  158. package/src/panels/hooks-panel.ts +0 -401
  159. package/src/panels/incident-review-panel.ts +0 -406
  160. package/src/panels/intelligence-panel.ts +0 -383
  161. package/src/panels/knowledge-graph-panel.ts +0 -515
  162. package/src/panels/marketplace-panel.ts +0 -399
  163. package/src/panels/memory-panel.ts +0 -558
  164. package/src/panels/ops-control-panel.ts +0 -329
  165. package/src/panels/ops-strategy-panel.ts +0 -321
  166. package/src/panels/orchestration-panel.ts +0 -465
  167. package/src/panels/panel-list-panel.ts +0 -566
  168. package/src/panels/plan-dashboard-panel.ts +0 -707
  169. package/src/panels/policy-panel.ts +0 -517
  170. package/src/panels/project-planning-panel.ts +0 -731
  171. package/src/panels/provider-health-panel.ts +0 -678
  172. package/src/panels/provider-health-tracker.ts +0 -310
  173. package/src/panels/provider-health-views.ts +0 -567
  174. package/src/panels/qr-panel.ts +0 -280
  175. package/src/panels/remote-panel.ts +0 -534
  176. package/src/panels/routes-panel.ts +0 -241
  177. package/src/panels/sandbox-panel.ts +0 -456
  178. package/src/panels/security-panel.ts +0 -447
  179. package/src/panels/services-panel.ts +0 -329
  180. package/src/panels/session-browser-panel.ts +0 -496
  181. package/src/panels/settings-sync-panel.ts +0 -398
  182. package/src/panels/subscription-panel.ts +0 -342
  183. package/src/panels/symbol-outline-panel.ts +0 -619
  184. package/src/panels/system-messages-panel.ts +0 -364
  185. package/src/panels/tasks-panel.ts +0 -608
  186. package/src/panels/thinking-panel.ts +0 -333
  187. package/src/panels/tool-inspector-panel.ts +0 -537
  188. package/src/panels/work-plan-panel.ts +0 -540
  189. package/src/panels/worktree-panel.ts +0 -360
  190. package/src/panels/wrfc-panel.ts +0 -790
  191. package/src/renderer/agent-detail-modal.ts +0 -466
  192. package/src/renderer/live-tail-modal.ts +0 -156
  193. package/src/renderer/process-modal.ts +0 -671
  194. package/src/renderer/qr-renderer.ts +0 -120
@@ -1,384 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // DocsPanel — tool list, model capabilities, and keyboard shortcut reference.
3
- // ---------------------------------------------------------------------------
4
-
5
- import type { Line } from '../types/grid.ts';
6
- import { BasePanel } from './base-panel.ts';
7
- import { buildKeyboardHints, buildPanelLine, buildPanelWorkspace, buildSearchInputLine, resolveScrollablePanelSection, extendPalette, DEFAULT_PANEL_PALETTE } from './polish.ts';
8
- import type { ToolCatalogQuery } from '../runtime/ui-service-queries.ts';
9
- import type { ModelDefinition } from '@pellux/goodvibes-sdk/platform/providers';
10
- import type { KeybindingsManager } from '../input/keybindings.ts';
11
- import type { PanelIntegrationContext } from './types.ts';
12
- import { ToolInspectorPanel } from './tool-inspector-panel.ts';
13
- import { isPanelSearchBackspace, isPanelSearchPrintable } from './search-focus.ts';
14
-
15
- const C = extendPalette(DEFAULT_PANEL_PALETTE, {
16
- // Panel-specific domain colors with no clean shared equivalent.
17
- sectionFg: '#00ffff',
18
- toolFg: '#88ccff',
19
- selected: '#00ffff',
20
- });
21
-
22
- /**
23
- * Minimal provider-registry surface DocsPanel needs: list models, read the
24
- * live active model (to mark it in the list), and switch models (Enter on a
25
- * model row is a real in-panel action, not a printed signpost). Structurally
26
- * satisfied by the full ProviderRegistry passed in from bootstrap.
27
- */
28
- interface DocsProviderRegistry {
29
- listModels(): ModelDefinition[];
30
- getCurrentModel?(): ModelDefinition | undefined;
31
- setCurrentModel?(registryKey: string): void;
32
- }
33
-
34
- type DocSection = 'tools' | 'models' | 'shortcuts';
35
-
36
- interface FlatRow {
37
- kind: 'header' | 'item' | 'detail' | 'empty';
38
- text: string;
39
- fg: string;
40
- bg: string;
41
- bold?: boolean;
42
- /** Present on tool 'item' rows — the tool name Enter should filter the inspector to. */
43
- toolName?: string;
44
- /** Present on model 'item' rows — the model's registryKey (provider:id) for setCurrentModel. */
45
- modelKey?: string;
46
- /** Present on model 'item' rows — whether Enter is allowed to switch to this model. */
47
- modelSelectable?: boolean;
48
- }
49
-
50
- function renderRow(width: number, row: FlatRow, isCursor: boolean): Line {
51
- const bg = isCursor ? C.selectBg : row.bg;
52
- return buildPanelLine(width, [
53
- [isCursor ? '▸' : ' ', C.selected, bg],
54
- [row.text, isCursor ? C.selected : row.fg, bg],
55
- ]);
56
- }
57
-
58
- export class DocsPanel extends BasePanel {
59
- private toolRegistry: ToolCatalogQuery | null = null;
60
- private providerRegistry: DocsProviderRegistry | null = null;
61
- private keybindingsManager: KeybindingsManager | null = null;
62
- private section: DocSection = 'tools';
63
- private searchQuery = '';
64
- private searching = false;
65
- private rows: FlatRow[] = [];
66
- private cursorIndex = 0;
67
- private scrollOffset = 0;
68
- /** Set by handleInput('enter') on a tool row; consumed by handlePanelIntegrationAction, which has the PanelManager reference needed to open the sibling inspector. */
69
- private _pendingToolJump: string | null = null;
70
-
71
- constructor(toolRegistry?: ToolCatalogQuery, providerRegistry?: DocsProviderRegistry, keybindingsManager?: KeybindingsManager) {
72
- super('docs', 'Docs', '?', 'session');
73
- this.toolRegistry = toolRegistry ?? null;
74
- this.providerRegistry = providerRegistry ?? null;
75
- this.keybindingsManager = keybindingsManager ?? null;
76
- }
77
-
78
- override onActivate(): void {
79
- this.needsRender = true;
80
- this._buildRows();
81
- }
82
-
83
- /**
84
- * WO-153: converged modal '/' filter (mirrors ScrollableListPanel's
85
- * _handleFilterKey). Returns `true`/`false` when consumed/ignored in
86
- * filter context, or `null` to fall through to section hotkeys/navigation.
87
- */
88
- private _handleSearchKey(key: string): boolean | null {
89
- if (this.searching) {
90
- if (key === 'escape') {
91
- this.searching = false;
92
- this.searchQuery = '';
93
- this._buildRows();
94
- return true;
95
- }
96
- if (key === 'return' || key === 'enter') {
97
- this.searching = false; // commit; keep the query applied
98
- this._buildRows();
99
- return true;
100
- }
101
- if (isPanelSearchBackspace(key)) {
102
- this.searchQuery = this.searchQuery.slice(0, -1);
103
- this._buildRows();
104
- return true;
105
- }
106
- // Arrow/paging keys navigate the filtered rows — fall through.
107
- if (key === 'up' || key === 'down' || key === 'pageup' || key === 'pagedown') {
108
- return null;
109
- }
110
- // Any printable character (including section hotkeys like t/m/k) extends the query.
111
- if (isPanelSearchPrintable(key)) {
112
- this.searchQuery += key;
113
- this._buildRows();
114
- return true;
115
- }
116
- return false;
117
- }
118
- if (key === '/') {
119
- this._startSearch();
120
- return true;
121
- }
122
- return null;
123
- }
124
-
125
- /**
126
- * The `/`-to-search buffer wants every character of a burst (paste, or a
127
- * fast-typed query landing in one input.feed() call), same as it always
128
- * has — see the interface doc on `Panel.isCapturingTextBurst`.
129
- */
130
- isCapturingTextBurst(): boolean {
131
- return this.searching;
132
- }
133
-
134
- handleInput(key: string): boolean {
135
- const searchResult = this._handleSearchKey(key);
136
- if (searchResult !== null) return searchResult;
137
-
138
- switch (key) {
139
- case 'up': this._move(-1); return true;
140
- case 'down': this._move(1); return true;
141
- case 'pageup': this._move(-10); return true;
142
- case 'pagedown': this._move(10); return true;
143
- case 't': this._setSection('tools'); return true;
144
- case 'm': this._setSection('models'); return true;
145
- case 'k': this._setSection('shortcuts'); return true;
146
- case 'enter':
147
- case 'return': return this._activateSelected();
148
- default: return false;
149
- }
150
- }
151
-
152
- /**
153
- * Enter on the cursor row. Tool rows can't act directly — opening the tool
154
- * inspector needs the PanelManager, which only handlePanelIntegrationAction
155
- * has — so this just records intent and lets that hook finish the jump.
156
- * Model rows need no cross-panel access: switching the active model is a
157
- * direct providerRegistry call, so it happens right here.
158
- */
159
- private _activateSelected(): boolean {
160
- const row = this.rows[this.cursorIndex];
161
- if (!row || row.kind !== 'item') return false;
162
- if (this.section === 'tools' && row.toolName) {
163
- this._pendingToolJump = row.toolName;
164
- return true;
165
- }
166
- if (this.section === 'models' && row.modelKey) {
167
- if (row.modelSelectable && this.providerRegistry?.setCurrentModel) {
168
- this.providerRegistry.setCurrentModel(row.modelKey);
169
- this._buildRows();
170
- }
171
- return true;
172
- }
173
- return false;
174
- }
175
-
176
- /**
177
- * Cross-panel integration hook — opens ToolInspectorPanel filtered to the
178
- * tool selected on Enter (session.ts:102 sibling-open pattern), consuming
179
- * the intent recorded by _activateSelected.
180
- */
181
- handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
182
- if (!this._pendingToolJump) return false;
183
- const tool = this._pendingToolJump;
184
- this._pendingToolJump = null;
185
- const inspector = ctx.panelManager.open('tools');
186
- if (!(inspector instanceof ToolInspectorPanel)) return false;
187
- inspector.filterByTool(tool);
188
- return true;
189
- }
190
-
191
- render(width: number, height: number): Line[] {
192
- if (height <= 0 || width <= 0) return [];
193
- const sectionLabel = this.section === 'tools' ? 'Tools' : this.section === 'models' ? 'Models' : 'Shortcuts';
194
- this.cursorIndex = Math.max(0, Math.min(this.cursorIndex, Math.max(0, this.rows.length - 1)));
195
- const controlsSection = {
196
- title: 'Controls',
197
- lines: [
198
- buildPanelLine(width, [
199
- [' t', DEFAULT_PANEL_PALETTE.info], [' tools', DEFAULT_PANEL_PALETTE.dim],
200
- [' m', DEFAULT_PANEL_PALETTE.info], [' models', DEFAULT_PANEL_PALETTE.dim],
201
- [' k', DEFAULT_PANEL_PALETTE.info], [' shortcuts', DEFAULT_PANEL_PALETTE.dim],
202
- [' /', DEFAULT_PANEL_PALETTE.info], [' filter', DEFAULT_PANEL_PALETTE.dim],
203
- ]),
204
- this._buildFilterLine(width),
205
- ],
206
- } as const;
207
- // Context-aware footer: while filtering, surface only the keys that work in
208
- // the filter field; otherwise surface section + navigation keys.
209
- const footerLines = [this.searching
210
- ? buildKeyboardHints(width, [
211
- { keys: 'type', label: 'filter' },
212
- { keys: 'Enter', label: 'apply' },
213
- { keys: 'Esc', label: 'clear' },
214
- ], DEFAULT_PANEL_PALETTE)
215
- : buildKeyboardHints(width, [
216
- { keys: 't/m/k', label: 'tools / models / shortcuts' },
217
- { keys: '↑/↓', label: 'navigate' },
218
- ...(this.section === 'tools' ? [{ keys: 'Enter', label: 'open in tool inspector' }] : []),
219
- ...(this.section === 'models' ? [{ keys: 'Enter', label: 'set active model' }] : []),
220
- { keys: '/', label: 'filter' },
221
- ], DEFAULT_PANEL_PALETTE)];
222
-
223
- const sectionWindow = resolveScrollablePanelSection(width, height, {
224
- intro: 'Browse built-in tool docs, available models, and keyboard shortcuts from one shared reference surface.',
225
- palette: DEFAULT_PANEL_PALETTE,
226
- footerLines,
227
- beforeSections: [controlsSection],
228
- section: {
229
- title: sectionLabel,
230
- scrollableLines: this.rows.map((row, absIdx) => {
231
- const isCursor = absIdx === this.cursorIndex && row.kind !== 'header' && row.kind !== 'empty';
232
- return renderRow(width, row, isCursor);
233
- }),
234
- selectedIndex: this.cursorIndex,
235
- scrollOffset: this.scrollOffset,
236
- minRows: 8,
237
- },
238
- });
239
- this.scrollOffset = sectionWindow.scrollOffset;
240
-
241
- return buildPanelWorkspace(width, height, {
242
- title: ` Docs / ${sectionLabel}`,
243
- intro: 'Browse built-in tool docs, available models, and keyboard shortcuts from one shared reference surface.',
244
- sections: [
245
- controlsSection,
246
- sectionWindow.section.lines.length > 0 ? sectionWindow.section : { title: sectionLabel, lines: [buildPanelLine(width, [[' No matching docs', DEFAULT_PANEL_PALETTE.dim]])] },
247
- ],
248
- footerLines,
249
- palette: DEFAULT_PANEL_PALETTE,
250
- });
251
- }
252
-
253
- private _setSection(section: DocSection): void {
254
- this.section = section;
255
- this.searchQuery = '';
256
- this.cursorIndex = 0;
257
- this.scrollOffset = 0;
258
- this._buildRows();
259
- }
260
-
261
- private _startSearch(): void {
262
- this.searching = true;
263
- this.markDirty();
264
- }
265
-
266
- /**
267
- * The filter input line — pinned rendering contract shared with
268
- * ScrollableListPanel.buildFilterLine: 'Filter: ' unfocused / '[Filter] '
269
- * focused, literal trailing '_' cursor while active (active:false is
270
- * passed to buildSearchInputLine to suppress its block-glyph cursor
271
- * substitution).
272
- */
273
- private _buildFilterLine(width: number): Line {
274
- const label = this.searching ? '[Filter] ' : 'Filter: ';
275
- const value = this.searching ? `${this.searchQuery}_` : this.searchQuery;
276
- return buildSearchInputLine(width, label, value, DEFAULT_PANEL_PALETTE, {
277
- active: false,
278
- bg: this.searching ? DEFAULT_PANEL_PALETTE.inputBg : DEFAULT_PANEL_PALETTE.sectionBg,
279
- emptyLabel: '(/ to filter)',
280
- valueColor: this.searching ? DEFAULT_PANEL_PALETTE.info : (this.searchQuery ? DEFAULT_PANEL_PALETTE.value : DEFAULT_PANEL_PALETTE.dim),
281
- });
282
- }
283
-
284
- private _move(delta: number): void {
285
- if (this.rows.length === 0) return;
286
- this.cursorIndex = Math.max(0, Math.min(this.rows.length - 1, this.cursorIndex + delta));
287
- this.markDirty();
288
- }
289
-
290
- private _buildRows(): void {
291
- const q = this.searchQuery.trim().toLowerCase();
292
- const rows: FlatRow[] = [];
293
-
294
- if (this.section === 'tools') {
295
- const tools = this.toolRegistry?.list() ?? [];
296
- const filtered = q ? tools.filter(t => t.definition.name.toLowerCase().includes(q) || (t.definition.description ?? '').toLowerCase().includes(q)) : tools;
297
- if (filtered.length === 0) {
298
- rows.push({ kind: 'empty', text: q ? ` No tools match "${this.searchQuery}". Press Esc to clear the filter.` : ' Tool registry not wired into this session.', fg: C.dim, bg: '' });
299
- } else {
300
- rows.push({ kind: 'header', text: ` Tools (${filtered.length})`, fg: C.sectionFg, bg: C.sectionBg, bold: true });
301
- for (const tool of filtered) {
302
- rows.push({ kind: 'item', text: ` ${tool.definition.name}`, fg: C.toolFg, bg: '', bold: true, toolName: tool.definition.name });
303
- if (tool.definition.description) {
304
- rows.push({ kind: 'detail', text: ` ${tool.definition.description}`, fg: C.label, bg: '' });
305
- }
306
- const metadata: string[] = [];
307
- if (tool.definition.sideEffects && tool.definition.sideEffects.length > 0) {
308
- metadata.push(`effects: ${tool.definition.sideEffects.join(', ')}`);
309
- }
310
- if (tool.definition.concurrency) {
311
- metadata.push(`concurrency: ${tool.definition.concurrency}`);
312
- }
313
- if (tool.definition.supportsProgress) {
314
- metadata.push('progress');
315
- }
316
- if (tool.definition.supportsStreamingOutput) {
317
- metadata.push('streaming');
318
- }
319
- if (metadata.length > 0) {
320
- rows.push({ kind: 'detail', text: ` ${metadata.join(' | ')}`, fg: C.dim, bg: '' });
321
- }
322
- }
323
- }
324
- } else if (this.section === 'models') {
325
- const models = this.providerRegistry?.listModels() ?? [];
326
- const filtered = q ? models.filter(m => m.id.toLowerCase().includes(q) || m.displayName.toLowerCase().includes(q) || m.provider.toLowerCase().includes(q)) : models;
327
- if (filtered.length === 0) {
328
- rows.push({ kind: 'empty', text: q ? ` No models match "${this.searchQuery}". Press Esc to clear the filter.` : ' Provider registry not wired into this session.', fg: C.dim, bg: '' });
329
- } else {
330
- // Group by provider
331
- const byProvider = new Map<string, typeof filtered>();
332
- for (const m of filtered) {
333
- let arr = byProvider.get(m.provider);
334
- if (!arr) { arr = []; byProvider.set(m.provider, arr); }
335
- arr.push(m);
336
- }
337
- // Live active-model marker (WO-136): Enter on a selectable row calls
338
- // providerRegistry.setCurrentModel, so re-derive this on every build
339
- // instead of caching it — it can change without this panel's input.
340
- const activeKey = this.providerRegistry?.getCurrentModel?.()?.registryKey;
341
- for (const [provider, pModels] of byProvider) {
342
- rows.push({ kind: 'header', text: ` ${provider} (${pModels.length})`, fg: C.sectionFg, bg: C.sectionBg, bold: true });
343
- for (const m of pModels) {
344
- const ctxK = m.contextWindow > 0 ? `${(m.contextWindow / 1000).toFixed(0)}k` : '?';
345
- const caps = [m.contextWindow > 0 ? `ctx:${ctxK}` : ''].filter(Boolean).join(' ');
346
- const isActive = m.registryKey === activeKey;
347
- const activeTag = isActive ? ' ACTIVE' : '';
348
- rows.push({
349
- kind: 'item',
350
- text: ` ${m.displayName} ${caps}${activeTag}`,
351
- fg: isActive ? C.good : C.toolFg,
352
- bg: '',
353
- modelKey: m.registryKey,
354
- modelSelectable: m.selectable,
355
- });
356
- rows.push({ kind: 'detail', text: ` ${m.id}${m.selectable ? '' : ' (not selectable)'}`, fg: C.label, bg: '' });
357
- }
358
- }
359
- }
360
- } else {
361
- // Shortcuts — live bindings from KeybindingsManager.getAll() (WO-136),
362
- // including any user overrides from ~/.goodvibes/tui/keybindings.json.
363
- const km = this.keybindingsManager;
364
- const shortcuts = (km?.getAll() ?? []).map((entry) => ({
365
- key: entry.combos.length > 0 ? entry.combos.map((combo) => km!.formatCombo(combo)).join(', ') : '(unbound)',
366
- desc: entry.description,
367
- }));
368
- const filtered = q ? shortcuts.filter(s => s.key.toLowerCase().includes(q) || s.desc.toLowerCase().includes(q)) : shortcuts;
369
- if (shortcuts.length === 0) {
370
- rows.push({ kind: 'empty', text: ' Keybindings manager not wired into this session.', fg: C.dim, bg: '' });
371
- } else {
372
- rows.push({ kind: 'header', text: ' Keyboard Shortcuts', fg: C.sectionFg, bg: C.sectionBg, bold: true });
373
- for (const s of filtered) {
374
- const key = s.key.padEnd(20);
375
- rows.push({ kind: 'item', text: ` ${key} ${s.desc}`, fg: C.value, bg: '' });
376
- }
377
- }
378
- }
379
-
380
- this.rows = rows;
381
- this.cursorIndex = Math.min(this.cursorIndex, Math.max(0, rows.length - 1));
382
- this.markDirty();
383
- }
384
- }