@pellux/goodvibes-tui 0.27.0 → 0.29.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 (143) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/context-usage.ts +53 -0
  5. package/src/core/conversation.ts +4 -6
  6. package/src/core/session-recovery.ts +2 -0
  7. package/src/core/turn-event-wiring.ts +1 -0
  8. package/src/input/commands/cost-runtime.ts +49 -0
  9. package/src/input/commands/eval.ts +4 -3
  10. package/src/input/commands/operator-runtime.ts +5 -1
  11. package/src/input/commands.ts +2 -0
  12. package/src/input/handler-feed-routes.ts +60 -26
  13. package/src/input/handler-feed.ts +17 -8
  14. package/src/input/handler-picker-routes.ts +18 -13
  15. package/src/input/handler-shortcuts.ts +51 -0
  16. package/src/input/handler-ui-state.ts +4 -0
  17. package/src/input/handler.ts +43 -6
  18. package/src/input/keybindings.ts +53 -8
  19. package/src/input/model-picker.ts +5 -0
  20. package/src/input/panel-integration-actions.ts +117 -1
  21. package/src/main.ts +5 -2
  22. package/src/panels/agent-inspector-panel.ts +139 -45
  23. package/src/panels/agent-inspector-shared.ts +147 -3
  24. package/src/panels/approval-panel.ts +204 -102
  25. package/src/panels/automation-control-panel.ts +370 -103
  26. package/src/panels/base-panel.ts +26 -2
  27. package/src/panels/builtin/agent.ts +28 -37
  28. package/src/panels/builtin/development.ts +40 -32
  29. package/src/panels/builtin/knowledge.ts +15 -6
  30. package/src/panels/builtin/operations.ts +178 -117
  31. package/src/panels/builtin/session.ts +35 -9
  32. package/src/panels/builtin/shared.ts +98 -6
  33. package/src/panels/cockpit-panel.ts +253 -75
  34. package/src/panels/cockpit-read-model.ts +2 -1
  35. package/src/panels/communication-panel.ts +159 -56
  36. package/src/panels/confirm-state.ts +8 -1
  37. package/src/panels/control-plane-panel.ts +354 -95
  38. package/src/panels/cost-tracker-panel.ts +243 -57
  39. package/src/panels/debug-panel.ts +347 -159
  40. package/src/panels/diff-panel.ts +257 -82
  41. package/src/panels/docs-panel.ts +166 -64
  42. package/src/panels/eval-panel.ts +336 -109
  43. package/src/panels/expandable-list-panel.ts +189 -0
  44. package/src/panels/file-explorer-panel.ts +238 -159
  45. package/src/panels/file-preview-panel.ts +141 -59
  46. package/src/panels/git-panel.ts +360 -191
  47. package/src/panels/hooks-panel.ts +181 -19
  48. package/src/panels/incident-review-panel.ts +252 -43
  49. package/src/panels/index.ts +0 -4
  50. package/src/panels/intelligence-panel.ts +310 -103
  51. package/src/panels/knowledge-graph-panel.ts +480 -58
  52. package/src/panels/local-auth-panel.ts +251 -33
  53. package/src/panels/marketplace-panel.ts +219 -33
  54. package/src/panels/memory-panel.ts +83 -81
  55. package/src/panels/ops-control-panel.ts +211 -32
  56. package/src/panels/ops-strategy-panel.ts +131 -45
  57. package/src/panels/orchestration-panel.ts +259 -67
  58. package/src/panels/panel-list-panel.ts +184 -136
  59. package/src/panels/panel-manager.ts +120 -13
  60. package/src/panels/plan-dashboard-panel.ts +507 -74
  61. package/src/panels/plugins-panel.ts +227 -34
  62. package/src/panels/policy-panel.ts +264 -55
  63. package/src/panels/polish-core.ts +162 -0
  64. package/src/panels/polish-tables.ts +258 -0
  65. package/src/panels/polish.ts +67 -149
  66. package/src/panels/project-planning-answer-actions.ts +134 -0
  67. package/src/panels/project-planning-panel.ts +84 -113
  68. package/src/panels/provider-health-panel.ts +438 -480
  69. package/src/panels/provider-health-routes.ts +203 -0
  70. package/src/panels/provider-health-tracker.ts +194 -6
  71. package/src/panels/provider-health-views.ts +560 -0
  72. package/src/panels/qr-panel.ts +136 -38
  73. package/src/panels/remote-panel.ts +123 -38
  74. package/src/panels/routes-panel.ts +87 -24
  75. package/src/panels/sandbox-panel.ts +232 -65
  76. package/src/panels/scrollable-list-panel.ts +143 -145
  77. package/src/panels/security-panel.ts +204 -52
  78. package/src/panels/services-panel.ts +174 -76
  79. package/src/panels/session-browser-panel.ts +101 -6
  80. package/src/panels/session-maintenance.ts +4 -122
  81. package/src/panels/settings-sync-panel.ts +346 -68
  82. package/src/panels/skills-panel.ts +157 -89
  83. package/src/panels/subscription-panel.ts +105 -34
  84. package/src/panels/symbol-outline-panel.ts +287 -154
  85. package/src/panels/system-messages-panel.ts +172 -38
  86. package/src/panels/tasks-panel.ts +348 -134
  87. package/src/panels/thinking-panel.ts +66 -32
  88. package/src/panels/token-budget-panel.ts +233 -46
  89. package/src/panels/tool-inspector-panel.ts +170 -54
  90. package/src/panels/types.ts +65 -4
  91. package/src/panels/work-plan-panel.ts +393 -39
  92. package/src/panels/worktree-panel.ts +239 -61
  93. package/src/panels/wrfc-panel-format.ts +133 -0
  94. package/src/panels/wrfc-panel.ts +146 -150
  95. package/src/renderer/compaction-preview.ts +2 -1
  96. package/src/renderer/compositor.ts +46 -43
  97. package/src/renderer/conversation-overlays.ts +25 -11
  98. package/src/renderer/footer-tips.ts +41 -0
  99. package/src/renderer/fullscreen-primitives.ts +22 -16
  100. package/src/renderer/help-overlay.ts +91 -14
  101. package/src/renderer/hint-grammar.ts +52 -0
  102. package/src/renderer/layout.ts +7 -7
  103. package/src/renderer/modal-factory.ts +23 -15
  104. package/src/renderer/model-picker-overlay.ts +30 -11
  105. package/src/renderer/model-workspace.ts +2 -3
  106. package/src/renderer/overlay-box.ts +16 -10
  107. package/src/renderer/panel-composite.ts +7 -20
  108. package/src/renderer/panel-workspace-bar.ts +14 -8
  109. package/src/renderer/process-indicator.ts +3 -1
  110. package/src/renderer/progress.ts +8 -6
  111. package/src/renderer/search-overlay.ts +27 -6
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal.ts +70 -10
  114. package/src/renderer/shell-surface.ts +41 -15
  115. package/src/renderer/status-glyphs.ts +11 -9
  116. package/src/renderer/tab-strip.ts +148 -34
  117. package/src/renderer/theme.ts +60 -3
  118. package/src/renderer/ui-factory.ts +45 -36
  119. package/src/renderer/ui-primitives.ts +23 -2
  120. package/src/runtime/bootstrap-shell.ts +35 -18
  121. package/src/runtime/diagnostics/panels/index.ts +0 -3
  122. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  123. package/src/runtime/ui/model-picker/types.ts +4 -0
  124. package/src/shell/ui-openers.ts +14 -22
  125. package/src/utils/format-duration.ts +55 -0
  126. package/src/utils/format-number.ts +71 -0
  127. package/src/utils/splash-lines.ts +44 -3
  128. package/src/version.ts +1 -1
  129. package/src/work-plans/work-plan-store.ts +13 -0
  130. package/src/panels/agent-logs-panel.ts +0 -666
  131. package/src/panels/agent-logs-shared.ts +0 -129
  132. package/src/panels/context-visualizer-panel.ts +0 -214
  133. package/src/panels/forensics-panel.ts +0 -364
  134. package/src/panels/panel-picker.ts +0 -106
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -218
  137. package/src/panels/provider-stats-panel.ts +0 -366
  138. package/src/panels/schedule-panel.ts +0 -342
  139. package/src/panels/watchers-panel.ts +0 -193
  140. package/src/renderer/panel-picker-overlay.ts +0 -202
  141. package/src/renderer/panel-tab-bar.ts +0 -69
  142. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  143. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -4,15 +4,13 @@
4
4
 
5
5
  import type { Line } from '../types/grid.ts';
6
6
  import { BasePanel } from './base-panel.ts';
7
- import { buildPanelLine, buildPanelWorkspace, buildSearchInputLine, resolveScrollablePanelSection, extendPalette, DEFAULT_PANEL_PALETTE } from './polish.ts';
8
- import type { ProviderModelCatalogQuery, ToolCatalogQuery } from '../runtime/ui-service-queries.ts';
9
- import {
10
- getPanelSearchFocusTransition,
11
- isPanelSearchBackspace,
12
- isPanelSearchCancel,
13
- isPanelSearchCommit,
14
- isPanelSearchPrintable,
15
- } from './search-focus.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';
16
14
 
17
15
  const C = extendPalette(DEFAULT_PANEL_PALETTE, {
18
16
  // Panel-specific domain colors with no clean shared equivalent.
@@ -21,31 +19,17 @@ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
21
19
  selected: '#00ffff',
22
20
  });
23
21
 
24
- // ---------------------------------------------------------------------------
25
- // Hardcoded keyboard shortcut reference
26
- // ---------------------------------------------------------------------------
27
- const SHORTCUTS: Array<{ key: string; desc: string }> = [
28
- { key: 'Ctrl+C', desc: 'Cancel generation / exit (double)' },
29
- { key: 'Ctrl+P', desc: 'Open panel picker' },
30
- { key: 'Ctrl+F', desc: 'Search conversation' },
31
- { key: 'Ctrl+K', desc: 'Copy last response to clipboard' },
32
- { key: 'Ctrl+L', desc: 'Clear screen' },
33
- { key: 'Ctrl+Z', desc: 'Undo input' },
34
- { key: 'Alt+Enter', desc: 'Insert newline in prompt' },
35
- { key: 'PageUp/Down', desc: 'Scroll conversation' },
36
- { key: 'Alt+PgUp/Dn', desc: 'Scroll panel' },
37
- { key: 'Tab', desc: 'Path completion / tab panels' },
38
- { key: '/', desc: 'Start slash command' },
39
- { key: 'Enter', desc: 'Submit prompt' },
40
- { key: 'Esc', desc: 'Close overlay / cancel search' },
41
- { key: 'Up/Down', desc: 'Scroll history / navigate list' },
42
- { key: 'Alt+Up/Down', desc: 'Move cursor in multi-line input' },
43
- { key: 'Ctrl+A/E', desc: 'Jump to start/end of line' },
44
- { key: 'Ctrl+W', desc: 'Delete word backward' },
45
- { key: 'Ctrl+U', desc: 'Clear line' },
46
- { key: 'F1', desc: 'Toggle help overlay' },
47
- { key: 'F2', desc: 'Toggle shortcuts overlay' },
48
- ];
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
+ }
49
33
 
50
34
  type DocSection = 'tools' | 'models' | 'shortcuts';
51
35
 
@@ -55,6 +39,12 @@ interface FlatRow {
55
39
  fg: string;
56
40
  bg: string;
57
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;
58
48
  }
59
49
 
60
50
  function renderRow(width: number, row: FlatRow, isCursor: boolean): Line {
@@ -67,18 +57,22 @@ function renderRow(width: number, row: FlatRow, isCursor: boolean): Line {
67
57
 
68
58
  export class DocsPanel extends BasePanel {
69
59
  private toolRegistry: ToolCatalogQuery | null = null;
70
- private providerRegistry: ProviderModelCatalogQuery | null = null;
60
+ private providerRegistry: DocsProviderRegistry | null = null;
61
+ private keybindingsManager: KeybindingsManager | null = null;
71
62
  private section: DocSection = 'tools';
72
63
  private searchQuery = '';
73
64
  private searching = false;
74
65
  private rows: FlatRow[] = [];
75
66
  private cursorIndex = 0;
76
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;
77
70
 
78
- constructor(toolRegistry?: ToolCatalogQuery, providerRegistry?: ProviderModelCatalogQuery) {
71
+ constructor(toolRegistry?: ToolCatalogQuery, providerRegistry?: DocsProviderRegistry, keybindingsManager?: KeybindingsManager) {
79
72
  super('docs', 'Docs', '?', 'session');
80
73
  this.toolRegistry = toolRegistry ?? null;
81
74
  this.providerRegistry = providerRegistry ?? null;
75
+ this.keybindingsManager = keybindingsManager ?? null;
82
76
  }
83
77
 
84
78
  override onActivate(): void {
@@ -86,17 +80,21 @@ export class DocsPanel extends BasePanel {
86
80
  this._buildRows();
87
81
  }
88
82
 
89
- handleInput(key: string): boolean {
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 {
90
89
  if (this.searching) {
91
- const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.cursorIndex, itemCount: this.rows.length });
92
- if (transition === 'focus-list') {
90
+ if (key === 'escape') {
93
91
  this.searching = false;
94
- this.cursorIndex = 0;
95
- this.markDirty();
92
+ this.searchQuery = '';
93
+ this._buildRows();
96
94
  return true;
97
95
  }
98
- if (isPanelSearchCancel(key) || isPanelSearchCommit(key)) {
99
- this.searching = false;
96
+ if (key === 'return' || key === 'enter') {
97
+ this.searching = false; // commit; keep the query applied
100
98
  this._buildRows();
101
99
  return true;
102
100
  }
@@ -105,6 +103,11 @@ export class DocsPanel extends BasePanel {
105
103
  this._buildRows();
106
104
  return true;
107
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.
108
111
  if (isPanelSearchPrintable(key)) {
109
112
  this.searchQuery += key;
110
113
  this._buildRows();
@@ -112,12 +115,16 @@ export class DocsPanel extends BasePanel {
112
115
  }
113
116
  return false;
114
117
  }
115
-
116
- const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.cursorIndex, itemCount: this.rows.length });
117
- if (transition === 'focus-search') {
118
+ if (key === '/') {
118
119
  this._startSearch();
119
120
  return true;
120
121
  }
122
+ return null;
123
+ }
124
+
125
+ handleInput(key: string): boolean {
126
+ const searchResult = this._handleSearchKey(key);
127
+ if (searchResult !== null) return searchResult;
121
128
 
122
129
  switch (key) {
123
130
  case 'up': this._move(-1); return true;
@@ -127,18 +134,54 @@ export class DocsPanel extends BasePanel {
127
134
  case 't': this._setSection('tools'); return true;
128
135
  case 'm': this._setSection('models'); return true;
129
136
  case 'k': this._setSection('shortcuts'); return true;
137
+ case 'enter':
138
+ case 'return': return this._activateSelected();
130
139
  default: return false;
131
140
  }
132
141
  }
133
142
 
143
+ /**
144
+ * Enter on the cursor row. Tool rows can't act directly — opening the tool
145
+ * inspector needs the PanelManager, which only handlePanelIntegrationAction
146
+ * has — so this just records intent and lets that hook finish the jump.
147
+ * Model rows need no cross-panel access: switching the active model is a
148
+ * direct providerRegistry call, so it happens right here.
149
+ */
150
+ private _activateSelected(): boolean {
151
+ const row = this.rows[this.cursorIndex];
152
+ if (!row || row.kind !== 'item') return false;
153
+ if (this.section === 'tools' && row.toolName) {
154
+ this._pendingToolJump = row.toolName;
155
+ return true;
156
+ }
157
+ if (this.section === 'models' && row.modelKey) {
158
+ if (row.modelSelectable && this.providerRegistry?.setCurrentModel) {
159
+ this.providerRegistry.setCurrentModel(row.modelKey);
160
+ this._buildRows();
161
+ }
162
+ return true;
163
+ }
164
+ return false;
165
+ }
166
+
167
+ /**
168
+ * Cross-panel integration hook — opens ToolInspectorPanel filtered to the
169
+ * tool selected on Enter (session.ts:102 sibling-open pattern), consuming
170
+ * the intent recorded by _activateSelected.
171
+ */
172
+ handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
173
+ if (!this._pendingToolJump) return false;
174
+ const tool = this._pendingToolJump;
175
+ this._pendingToolJump = null;
176
+ const inspector = ctx.panelManager.open('tools');
177
+ if (!(inspector instanceof ToolInspectorPanel)) return false;
178
+ inspector.filterByTool(tool);
179
+ return true;
180
+ }
181
+
134
182
  render(width: number, height: number): Line[] {
135
183
  if (height <= 0 || width <= 0) return [];
136
184
  const sectionLabel = this.section === 'tools' ? 'Tools' : this.section === 'models' ? 'Models' : 'Shortcuts';
137
- const searchLine = this.searching
138
- ? ` Search: ${this.searchQuery}\u258a`
139
- : this.searchQuery
140
- ? ` Filter: ${this.searchQuery} (/ or up at top to edit)`
141
- : ` / or up at top to search`;
142
185
  this.cursorIndex = Math.max(0, Math.min(this.cursorIndex, Math.max(0, this.rows.length - 1)));
143
186
  const controlsSection = {
144
187
  title: 'Controls',
@@ -147,14 +190,31 @@ export class DocsPanel extends BasePanel {
147
190
  [' t', DEFAULT_PANEL_PALETTE.info], [' tools', DEFAULT_PANEL_PALETTE.dim],
148
191
  [' m', DEFAULT_PANEL_PALETTE.info], [' models', DEFAULT_PANEL_PALETTE.dim],
149
192
  [' k', DEFAULT_PANEL_PALETTE.info], [' shortcuts', DEFAULT_PANEL_PALETTE.dim],
150
- [' /', DEFAULT_PANEL_PALETTE.info], [' search', DEFAULT_PANEL_PALETTE.dim],
193
+ [' /', DEFAULT_PANEL_PALETTE.info], [' filter', DEFAULT_PANEL_PALETTE.dim],
151
194
  ]),
152
- buildSearchInputLine(width, '', searchLine.trimStart(), DEFAULT_PANEL_PALETTE, { active: this.searching }),
195
+ this._buildFilterLine(width),
153
196
  ],
154
197
  } as const;
198
+ // Context-aware footer: while filtering, surface only the keys that work in
199
+ // the filter field; otherwise surface section + navigation keys.
200
+ const footerLines = [this.searching
201
+ ? buildKeyboardHints(width, [
202
+ { keys: 'type', label: 'filter' },
203
+ { keys: 'Enter', label: 'apply' },
204
+ { keys: 'Esc', label: 'clear' },
205
+ ], DEFAULT_PANEL_PALETTE)
206
+ : buildKeyboardHints(width, [
207
+ { keys: 't/m/k', label: 'tools / models / shortcuts' },
208
+ { keys: '↑/↓', label: 'navigate' },
209
+ ...(this.section === 'tools' ? [{ keys: 'Enter', label: 'open in tool inspector' }] : []),
210
+ ...(this.section === 'models' ? [{ keys: 'Enter', label: 'set active model' }] : []),
211
+ { keys: '/', label: 'filter' },
212
+ ], DEFAULT_PANEL_PALETTE)];
213
+
155
214
  const sectionWindow = resolveScrollablePanelSection(width, height, {
156
215
  intro: 'Browse built-in tool docs, available models, and keyboard shortcuts from one shared reference surface.',
157
216
  palette: DEFAULT_PANEL_PALETTE,
217
+ footerLines,
158
218
  beforeSections: [controlsSection],
159
219
  section: {
160
220
  title: sectionLabel,
@@ -176,6 +236,7 @@ export class DocsPanel extends BasePanel {
176
236
  controlsSection,
177
237
  sectionWindow.section.lines.length > 0 ? sectionWindow.section : { title: sectionLabel, lines: [buildPanelLine(width, [[' No matching docs', DEFAULT_PANEL_PALETTE.dim]])] },
178
238
  ],
239
+ footerLines,
179
240
  palette: DEFAULT_PANEL_PALETTE,
180
241
  });
181
242
  }
@@ -193,6 +254,24 @@ export class DocsPanel extends BasePanel {
193
254
  this.markDirty();
194
255
  }
195
256
 
257
+ /**
258
+ * The filter input line — pinned rendering contract shared with
259
+ * ScrollableListPanel.buildFilterLine: 'Filter: ' unfocused / '[Filter] '
260
+ * focused, literal trailing '_' cursor while active (active:false is
261
+ * passed to buildSearchInputLine to suppress its block-glyph cursor
262
+ * substitution).
263
+ */
264
+ private _buildFilterLine(width: number): Line {
265
+ const label = this.searching ? '[Filter] ' : 'Filter: ';
266
+ const value = this.searching ? `${this.searchQuery}_` : this.searchQuery;
267
+ return buildSearchInputLine(width, label, value, DEFAULT_PANEL_PALETTE, {
268
+ active: false,
269
+ bg: this.searching ? DEFAULT_PANEL_PALETTE.inputBg : DEFAULT_PANEL_PALETTE.sectionBg,
270
+ emptyLabel: '(/ to filter)',
271
+ valueColor: this.searching ? DEFAULT_PANEL_PALETTE.info : (this.searchQuery ? DEFAULT_PANEL_PALETTE.value : DEFAULT_PANEL_PALETTE.dim),
272
+ });
273
+ }
274
+
196
275
  private _move(delta: number): void {
197
276
  if (this.rows.length === 0) return;
198
277
  this.cursorIndex = Math.max(0, Math.min(this.rows.length - 1, this.cursorIndex + delta));
@@ -207,11 +286,11 @@ export class DocsPanel extends BasePanel {
207
286
  const tools = this.toolRegistry?.list() ?? [];
208
287
  const filtered = q ? tools.filter(t => t.definition.name.toLowerCase().includes(q) || (t.definition.description ?? '').toLowerCase().includes(q)) : tools;
209
288
  if (filtered.length === 0) {
210
- rows.push({ kind: 'empty', text: ' No tools match.', fg: C.dim, bg: '' });
289
+ 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: '' });
211
290
  } else {
212
291
  rows.push({ kind: 'header', text: ` Tools (${filtered.length})`, fg: C.sectionFg, bg: C.sectionBg, bold: true });
213
292
  for (const tool of filtered) {
214
- rows.push({ kind: 'item', text: ` ${tool.definition.name}`, fg: C.toolFg, bg: '', bold: true });
293
+ rows.push({ kind: 'item', text: ` ${tool.definition.name}`, fg: C.toolFg, bg: '', bold: true, toolName: tool.definition.name });
215
294
  if (tool.definition.description) {
216
295
  rows.push({ kind: 'detail', text: ` ${tool.definition.description}`, fg: C.label, bg: '' });
217
296
  }
@@ -237,7 +316,7 @@ export class DocsPanel extends BasePanel {
237
316
  const models = this.providerRegistry?.listModels() ?? [];
238
317
  const filtered = q ? models.filter(m => m.id.toLowerCase().includes(q) || m.displayName.toLowerCase().includes(q) || m.provider.toLowerCase().includes(q)) : models;
239
318
  if (filtered.length === 0) {
240
- rows.push({ kind: 'empty', text: ' No models match.', fg: C.dim, bg: '' });
319
+ 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: '' });
241
320
  } else {
242
321
  // Group by provider
243
322
  const byProvider = new Map<string, typeof filtered>();
@@ -246,23 +325,46 @@ export class DocsPanel extends BasePanel {
246
325
  if (!arr) { arr = []; byProvider.set(m.provider, arr); }
247
326
  arr.push(m);
248
327
  }
328
+ // Live active-model marker (WO-136): Enter on a selectable row calls
329
+ // providerRegistry.setCurrentModel, so re-derive this on every build
330
+ // instead of caching it — it can change without this panel's input.
331
+ const activeKey = this.providerRegistry?.getCurrentModel?.()?.registryKey;
249
332
  for (const [provider, pModels] of byProvider) {
250
333
  rows.push({ kind: 'header', text: ` ${provider} (${pModels.length})`, fg: C.sectionFg, bg: C.sectionBg, bold: true });
251
334
  for (const m of pModels) {
252
335
  const ctxK = m.contextWindow > 0 ? `${(m.contextWindow / 1000).toFixed(0)}k` : '?';
253
336
  const caps = [m.contextWindow > 0 ? `ctx:${ctxK}` : ''].filter(Boolean).join(' ');
254
- rows.push({ kind: 'item', text: ` ${m.displayName} ${caps}`, fg: C.toolFg, bg: '' });
255
- rows.push({ kind: 'detail', text: ` ${m.id}`, fg: C.label, bg: '' });
337
+ const isActive = m.registryKey === activeKey;
338
+ const activeTag = isActive ? ' ACTIVE' : '';
339
+ rows.push({
340
+ kind: 'item',
341
+ text: ` ${m.displayName} ${caps}${activeTag}`,
342
+ fg: isActive ? C.good : C.toolFg,
343
+ bg: '',
344
+ modelKey: m.registryKey,
345
+ modelSelectable: m.selectable,
346
+ });
347
+ rows.push({ kind: 'detail', text: ` ${m.id}${m.selectable ? '' : ' (not selectable)'}`, fg: C.label, bg: '' });
256
348
  }
257
349
  }
258
350
  }
259
351
  } else {
260
- // Shortcuts
261
- const filtered = q ? SHORTCUTS.filter(s => s.key.toLowerCase().includes(q) || s.desc.toLowerCase().includes(q)) : SHORTCUTS;
262
- rows.push({ kind: 'header', text: ' Keyboard Shortcuts', fg: C.sectionFg, bg: C.sectionBg, bold: true });
263
- for (const s of filtered) {
264
- const key = s.key.padEnd(16);
265
- rows.push({ kind: 'item', text: ` ${key} ${s.desc}`, fg: C.value, bg: '' });
352
+ // Shortcuts — live bindings from KeybindingsManager.getAll() (WO-136),
353
+ // including any user overrides from ~/.goodvibes/tui/keybindings.json.
354
+ const km = this.keybindingsManager;
355
+ const shortcuts = (km?.getAll() ?? []).map((entry) => ({
356
+ key: entry.combos.length > 0 ? entry.combos.map((combo) => km!.formatCombo(combo)).join(', ') : '(unbound)',
357
+ desc: entry.description,
358
+ }));
359
+ const filtered = q ? shortcuts.filter(s => s.key.toLowerCase().includes(q) || s.desc.toLowerCase().includes(q)) : shortcuts;
360
+ if (shortcuts.length === 0) {
361
+ rows.push({ kind: 'empty', text: ' Keybindings manager not wired into this session.', fg: C.dim, bg: '' });
362
+ } else {
363
+ rows.push({ kind: 'header', text: ' Keyboard Shortcuts', fg: C.sectionFg, bg: C.sectionBg, bold: true });
364
+ for (const s of filtered) {
365
+ const key = s.key.padEnd(20);
366
+ rows.push({ kind: 'item', text: ` ${key} ${s.desc}`, fg: C.value, bg: '' });
367
+ }
266
368
  }
267
369
  }
268
370