@pellux/goodvibes-tui 0.28.0 → 1.0.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 (144) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/conversation-line-cache.ts +432 -0
  5. package/src/core/conversation-rendering.ts +11 -3
  6. package/src/core/conversation.ts +50 -4
  7. package/src/input/commands/cost-runtime.ts +49 -0
  8. package/src/input/commands/operator-runtime.ts +5 -1
  9. package/src/input/commands.ts +2 -0
  10. package/src/input/handler-content-actions.ts +8 -3
  11. package/src/input/handler-feed-routes.ts +12 -35
  12. package/src/input/handler-feed.ts +4 -8
  13. package/src/input/handler-shortcuts.ts +51 -0
  14. package/src/input/handler.ts +43 -6
  15. package/src/input/input-history.ts +17 -4
  16. package/src/input/keybindings.ts +48 -8
  17. package/src/input/panel-integration-actions.ts +107 -1
  18. package/src/main.ts +19 -11
  19. package/src/panels/agent-inspector-panel.ts +125 -36
  20. package/src/panels/agent-inspector-shared.ts +144 -0
  21. package/src/panels/approval-panel.ts +67 -16
  22. package/src/panels/automation-control-panel.ts +368 -124
  23. package/src/panels/base-panel.ts +1 -1
  24. package/src/panels/builtin/agent.ts +28 -37
  25. package/src/panels/builtin/development.ts +40 -32
  26. package/src/panels/builtin/knowledge.ts +15 -6
  27. package/src/panels/builtin/operations.ts +178 -117
  28. package/src/panels/builtin/session.ts +35 -9
  29. package/src/panels/builtin/shared.ts +98 -6
  30. package/src/panels/cockpit-panel.ts +232 -73
  31. package/src/panels/communication-panel.ts +58 -20
  32. package/src/panels/confirm-state.ts +8 -1
  33. package/src/panels/control-plane-panel.ts +351 -118
  34. package/src/panels/cost-tracker-panel.ts +165 -7
  35. package/src/panels/debug-panel.ts +312 -159
  36. package/src/panels/diff-panel.ts +209 -57
  37. package/src/panels/docs-panel.ts +152 -66
  38. package/src/panels/eval-panel.ts +220 -42
  39. package/src/panels/file-explorer-panel.ts +202 -122
  40. package/src/panels/file-preview-panel.ts +132 -57
  41. package/src/panels/git-panel.ts +310 -129
  42. package/src/panels/hooks-panel.ts +150 -22
  43. package/src/panels/incident-review-panel.ts +223 -35
  44. package/src/panels/index.ts +0 -4
  45. package/src/panels/intelligence-panel.ts +212 -86
  46. package/src/panels/knowledge-graph-panel.ts +461 -101
  47. package/src/panels/local-auth-panel.ts +240 -28
  48. package/src/panels/marketplace-panel.ts +193 -26
  49. package/src/panels/memory-panel.ts +78 -77
  50. package/src/panels/ops-control-panel.ts +146 -29
  51. package/src/panels/ops-strategy-panel.ts +72 -4
  52. package/src/panels/orchestration-panel.ts +231 -69
  53. package/src/panels/panel-list-panel.ts +154 -131
  54. package/src/panels/panel-manager.ts +97 -9
  55. package/src/panels/plan-dashboard-panel.ts +333 -17
  56. package/src/panels/plugins-panel.ts +184 -29
  57. package/src/panels/policy-panel.ts +210 -38
  58. package/src/panels/polish-core.ts +7 -2
  59. package/src/panels/polish.ts +23 -4
  60. package/src/panels/project-planning-answer-actions.ts +134 -0
  61. package/src/panels/project-planning-panel.ts +62 -113
  62. package/src/panels/provider-health-panel.ts +434 -518
  63. package/src/panels/provider-health-routes.ts +203 -0
  64. package/src/panels/provider-health-tracker.ts +194 -6
  65. package/src/panels/provider-health-views.ts +560 -0
  66. package/src/panels/qr-panel.ts +116 -30
  67. package/src/panels/remote-panel.ts +114 -36
  68. package/src/panels/routes-panel.ts +63 -22
  69. package/src/panels/sandbox-panel.ts +174 -42
  70. package/src/panels/scrollable-list-panel.ts +19 -135
  71. package/src/panels/security-panel.ts +133 -33
  72. package/src/panels/services-panel.ts +116 -64
  73. package/src/panels/session-browser-panel.ts +73 -2
  74. package/src/panels/session-maintenance.ts +4 -122
  75. package/src/panels/settings-sync-panel.ts +335 -72
  76. package/src/panels/skills-panel.ts +157 -89
  77. package/src/panels/subscription-panel.ts +86 -33
  78. package/src/panels/symbol-outline-panel.ts +248 -108
  79. package/src/panels/system-messages-panel.ts +114 -13
  80. package/src/panels/tasks-panel.ts +326 -139
  81. package/src/panels/thinking-panel.ts +43 -10
  82. package/src/panels/token-budget-panel.ts +194 -18
  83. package/src/panels/tool-inspector-panel.ts +144 -34
  84. package/src/panels/types.ts +40 -4
  85. package/src/panels/work-plan-panel.ts +280 -17
  86. package/src/panels/worktree-panel.ts +175 -42
  87. package/src/panels/wrfc-panel.ts +116 -24
  88. package/src/renderer/agent-detail-modal.ts +37 -8
  89. package/src/renderer/code-block.ts +58 -28
  90. package/src/renderer/context-inspector.ts +3 -6
  91. package/src/renderer/conversation-overlays.ts +25 -11
  92. package/src/renderer/conversation-surface.ts +1 -21
  93. package/src/renderer/diff-view.ts +6 -4
  94. package/src/renderer/footer-tips.ts +41 -0
  95. package/src/renderer/fullscreen-primitives.ts +23 -17
  96. package/src/renderer/fullscreen-workspace.ts +2 -7
  97. package/src/renderer/help-overlay.ts +91 -14
  98. package/src/renderer/hint-grammar.ts +52 -0
  99. package/src/renderer/history-search-overlay.ts +10 -16
  100. package/src/renderer/layout.ts +7 -7
  101. package/src/renderer/markdown.ts +9 -1
  102. package/src/renderer/modal-factory.ts +23 -15
  103. package/src/renderer/model-picker-overlay.ts +8 -485
  104. package/src/renderer/model-workspace.ts +9 -24
  105. package/src/renderer/overlay-box.ts +23 -19
  106. package/src/renderer/process-indicator.ts +15 -25
  107. package/src/renderer/profile-picker-modal.ts +13 -14
  108. package/src/renderer/prompt-content-width.ts +16 -0
  109. package/src/renderer/search-overlay.ts +27 -6
  110. package/src/renderer/selection-modal-overlay.ts +3 -1
  111. package/src/renderer/semantic-diff.ts +1 -1
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal-helpers.ts +10 -34
  114. package/src/renderer/settings-modal.ts +70 -10
  115. package/src/renderer/shell-surface.ts +54 -15
  116. package/src/renderer/status-glyphs.ts +11 -9
  117. package/src/renderer/surface-layout.ts +0 -12
  118. package/src/renderer/term-caps.ts +1 -1
  119. package/src/renderer/theme.ts +60 -3
  120. package/src/renderer/tool-call.ts +6 -20
  121. package/src/renderer/ui-factory.ts +48 -37
  122. package/src/renderer/ui-primitives.ts +41 -3
  123. package/src/runtime/bootstrap-shell.ts +35 -18
  124. package/src/runtime/diagnostics/panels/index.ts +0 -3
  125. package/src/runtime/render-scheduler.ts +80 -0
  126. package/src/shell/ui-openers.ts +14 -22
  127. package/src/utils/format-duration.ts +2 -2
  128. package/src/utils/splash-lines.ts +54 -5
  129. package/src/version.ts +1 -1
  130. package/src/work-plans/work-plan-store.ts +13 -0
  131. package/src/panels/agent-logs-panel.ts +0 -635
  132. package/src/panels/agent-logs-shared.ts +0 -129
  133. package/src/panels/context-visualizer-panel.ts +0 -238
  134. package/src/panels/forensics-panel.ts +0 -378
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -255
  137. package/src/panels/provider-stats-panel.ts +0 -391
  138. package/src/panels/schedule-panel.ts +0 -365
  139. package/src/panels/watchers-panel.ts +0 -213
  140. package/src/renderer/file-tree.ts +0 -153
  141. package/src/renderer/progress.ts +0 -99
  142. package/src/renderer/status-token.ts +0 -67
  143. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  144. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -5,14 +5,12 @@
5
5
  import type { Line } from '../types/grid.ts';
6
6
  import { BasePanel } from './base-panel.ts';
7
7
  import { buildKeyboardHints, 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';
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,23 +190,25 @@ 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;
155
- // Context-aware footer: while searching, surface only the keys that work in
156
- // the search field; otherwise surface section + navigation keys.
198
+ // Context-aware footer: while filtering, surface only the keys that work in
199
+ // the filter field; otherwise surface section + navigation keys.
157
200
  const footerLines = [this.searching
158
201
  ? buildKeyboardHints(width, [
159
202
  { keys: 'type', label: 'filter' },
160
- { keys: 'Enter/Esc', label: 'apply / exit search' },
161
- { keys: '', label: 'back to list' },
203
+ { keys: 'Enter', label: 'apply' },
204
+ { keys: 'Esc', label: 'clear' },
162
205
  ], DEFAULT_PANEL_PALETTE)
163
206
  : buildKeyboardHints(width, [
164
207
  { keys: 't/m/k', label: 'tools / models / shortcuts' },
165
208
  { keys: '↑/↓', label: 'navigate' },
166
- { keys: '/', label: 'search' },
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' },
167
212
  ], DEFAULT_PANEL_PALETTE)];
168
213
 
169
214
  const sectionWindow = resolveScrollablePanelSection(width, height, {
@@ -209,6 +254,24 @@ export class DocsPanel extends BasePanel {
209
254
  this.markDirty();
210
255
  }
211
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
+
212
275
  private _move(delta: number): void {
213
276
  if (this.rows.length === 0) return;
214
277
  this.cursorIndex = Math.max(0, Math.min(this.rows.length - 1, this.cursorIndex + delta));
@@ -227,7 +290,7 @@ export class DocsPanel extends BasePanel {
227
290
  } else {
228
291
  rows.push({ kind: 'header', text: ` Tools (${filtered.length})`, fg: C.sectionFg, bg: C.sectionBg, bold: true });
229
292
  for (const tool of filtered) {
230
- 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 });
231
294
  if (tool.definition.description) {
232
295
  rows.push({ kind: 'detail', text: ` ${tool.definition.description}`, fg: C.label, bg: '' });
233
296
  }
@@ -262,23 +325,46 @@ export class DocsPanel extends BasePanel {
262
325
  if (!arr) { arr = []; byProvider.set(m.provider, arr); }
263
326
  arr.push(m);
264
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;
265
332
  for (const [provider, pModels] of byProvider) {
266
333
  rows.push({ kind: 'header', text: ` ${provider} (${pModels.length})`, fg: C.sectionFg, bg: C.sectionBg, bold: true });
267
334
  for (const m of pModels) {
268
335
  const ctxK = m.contextWindow > 0 ? `${(m.contextWindow / 1000).toFixed(0)}k` : '?';
269
336
  const caps = [m.contextWindow > 0 ? `ctx:${ctxK}` : ''].filter(Boolean).join(' ');
270
- rows.push({ kind: 'item', text: ` ${m.displayName} ${caps}`, fg: C.toolFg, bg: '' });
271
- 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: '' });
272
348
  }
273
349
  }
274
350
  }
275
351
  } else {
276
- // Shortcuts
277
- const filtered = q ? SHORTCUTS.filter(s => s.key.toLowerCase().includes(q) || s.desc.toLowerCase().includes(q)) : SHORTCUTS;
278
- rows.push({ kind: 'header', text: ' Keyboard Shortcuts', fg: C.sectionFg, bg: C.sectionBg, bold: true });
279
- for (const s of filtered) {
280
- const key = s.key.padEnd(16);
281
- 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
+ }
282
368
  }
283
369
  }
284
370