@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
@@ -5,6 +5,7 @@ import type { PanelCategory } from './types.ts';
5
5
  import type { ComponentHealthMonitor } from '../runtime/perf/panel-health-monitor.ts';
6
6
  import {
7
7
  buildEmptyState,
8
+ buildKeyboardHints,
8
9
  buildPanelWorkspace,
9
10
  buildSearchInputLine,
10
11
  DEFAULT_PANEL_PALETTE,
@@ -12,11 +13,6 @@ import {
12
13
  type PanelPalette,
13
14
  } from './polish.ts';
14
15
  import { GLYPHS } from '../renderer/ui-primitives.ts';
15
- import {
16
- isPanelSearchBackspace,
17
- isPanelSearchCancel,
18
- isPanelSearchPrintable,
19
- } from './search-focus.ts';
20
16
 
21
17
  // ---------------------------------------------------------------------------
22
18
  // ScrollableListPanel<T>
@@ -56,6 +52,115 @@ export abstract class ScrollableListPanel<T> extends BasePanel {
56
52
  */
57
53
  protected showSelectionGutter = false;
58
54
 
55
+ // -------------------------------------------------------------------------
56
+ // Opt-in inline filter ( / to filter, Esc to clear )
57
+ // -------------------------------------------------------------------------
58
+ // Set `filterEnabled = true` and override `filterMatches()` to give a list
59
+ // panel a `/`-to-filter affordance that coexists with single-letter action
60
+ // keys (filtering is modal: action keys work until you press `/`). A filter
61
+ // input line is auto-rendered at the top of the list.
62
+ protected filterEnabled = false;
63
+ protected filterQuery = '';
64
+ protected filterActive = false;
65
+ /** Short noun shown in the filter hint, e.g. "Filter sessions". */
66
+ protected filterLabel = 'Filter';
67
+
68
+ /** Override to define what the lower-cased filter query matches for an item. */
69
+ protected filterMatches(_item: T, _query: string): boolean {
70
+ return false;
71
+ }
72
+
73
+ /**
74
+ * The list after applying the active filter — used for both display and
75
+ * navigation. Identical to `getItems()` unless a filter is enabled and a
76
+ * non-empty query is set, so existing panels are unaffected.
77
+ */
78
+ protected getVisibleItems(): readonly T[] {
79
+ const all = this.getItems();
80
+ const q = this.filterQuery.trim().toLowerCase();
81
+ if (!this.filterEnabled || !q) return all;
82
+ return all.filter((item) => this.filterMatches(item, q));
83
+ }
84
+
85
+ /**
86
+ * The item currently under the cursor, resolved against the FILTERED list
87
+ * that navigation actually moves over (`getVisibleItems()`) — never the raw
88
+ * `getItems()` source.
89
+ *
90
+ * Subclasses MUST read the selected row through this method (or, when a
91
+ * function also needs the list for counts/windows, through a function-scope
92
+ * `const visible = this.getVisibleItems()` local — indexing that local, never
93
+ * the raw source). Indexing a raw item array (`this.rows`, `this.entries`,
94
+ * `getItems()`, …) with `this.selectedIndex` silently returns the wrong row
95
+ * whenever a filter is active, because `selectedIndex` is an offset into the
96
+ * visible list, not the raw one. The `no-raw-selectedindex-read` architecture
97
+ * rule enforces this by banning the `[this.selectedIndex]` token outside the
98
+ * base classes.
99
+ */
100
+ protected getSelectedItem(): T | undefined {
101
+ return this.getVisibleItems()[this.selectedIndex];
102
+ }
103
+
104
+ /**
105
+ * Filter-mode key handling. Returns `true`/`false` when consumed/ignored in
106
+ * filter context, or `null` to fall through to normal navigation.
107
+ */
108
+ private _handleFilterKey(key: string): boolean | null {
109
+ if (!this.filterEnabled) return null;
110
+ if (this.filterActive) {
111
+ if (key === 'escape') {
112
+ this.filterActive = false;
113
+ this.filterQuery = '';
114
+ this.selectedIndex = 0;
115
+ this.needsRender = true;
116
+ return true;
117
+ }
118
+ if (key === 'return' || key === 'enter') {
119
+ this.filterActive = false; // commit; keep the query applied
120
+ this.needsRender = true;
121
+ return true;
122
+ }
123
+ if (key === 'backspace' || key === 'delete') {
124
+ this.filterQuery = this.filterQuery.slice(0, -1);
125
+ this.selectedIndex = 0;
126
+ this.needsRender = true;
127
+ return true;
128
+ }
129
+ // Arrow/paging keys navigate the filtered list — fall through.
130
+ if (key === 'up' || key === 'down' || key === 'pageup' || key === 'pagedown' || key === 'home' || key === 'end') {
131
+ return null;
132
+ }
133
+ // Any printable character (including j/k/g/G) extends the query.
134
+ if (key.length === 1 && key >= ' ') {
135
+ this.filterQuery += key;
136
+ this.selectedIndex = 0;
137
+ this.needsRender = true;
138
+ return true;
139
+ }
140
+ return false;
141
+ }
142
+ if (key === '/') {
143
+ this.filterActive = true;
144
+ this.needsRender = true;
145
+ return true;
146
+ }
147
+ return null;
148
+ }
149
+
150
+ /** The filter input line shown at the top of the list when filtering is enabled. */
151
+ protected buildFilterLine(width: number): Line {
152
+ const palette = this.getPalette();
153
+ const label = this.filterActive ? `[${this.filterLabel}] ` : `${this.filterLabel}: `;
154
+ const value = this.filterActive
155
+ ? `${this.filterQuery}_`
156
+ : this.filterQuery || '(/ to filter)';
157
+ return buildSearchInputLine(width, label, value, palette, {
158
+ active: false,
159
+ bg: this.filterActive ? palette.inputBg : palette.sectionBg,
160
+ valueColor: this.filterActive ? palette.info : (this.filterQuery ? palette.value : palette.dim),
161
+ });
162
+ }
163
+
59
164
  constructor(
60
165
  id: string,
61
166
  name: string,
@@ -143,7 +248,11 @@ export abstract class ScrollableListPanel<T> extends BasePanel {
143
248
  // call this.clearError() at the start of their handler.
144
249
  if (this.lastError !== null) this.clearError();
145
250
 
146
- const items = this.getItems();
251
+ // Opt-in inline filter consumes input before navigation when active.
252
+ const filterResult = this._handleFilterKey(key);
253
+ if (filterResult !== null) return filterResult;
254
+
255
+ const items = this.getVisibleItems();
147
256
  const total = items.length;
148
257
 
149
258
  switch (key) {
@@ -202,7 +311,7 @@ export abstract class ScrollableListPanel<T> extends BasePanel {
202
311
 
203
312
  handleScroll(deltaRows: number): boolean {
204
313
  if (this.lastError !== null) this.clearError();
205
- const total = this.getItems().length;
314
+ const total = this.getVisibleItems().length;
206
315
  const rows = Math.trunc(deltaRows);
207
316
  if (total === 0 || rows === 0) return false;
208
317
  const next = Math.max(0, Math.min(total - 1, this.selectedIndex + rows));
@@ -221,7 +330,7 @@ export abstract class ScrollableListPanel<T> extends BasePanel {
221
330
  * Must be called after any data refresh that may shrink the list.
222
331
  */
223
332
  protected clampSelection(): void {
224
- const total = this.getItems().length;
333
+ const total = this.getVisibleItems().length;
225
334
  if (total === 0) {
226
335
  this.selectedIndex = 0;
227
336
  } else {
@@ -253,6 +362,7 @@ export abstract class ScrollableListPanel<T> extends BasePanel {
253
362
  options: {
254
363
  readonly header?: readonly Line[];
255
364
  readonly footer?: readonly Line[];
365
+ readonly hints?: ReadonlyArray<{ keys: string; label: string }>;
256
366
  readonly emptyMessage?: string;
257
367
  readonly title?: string;
258
368
  readonly spinnerFrame?: number;
@@ -260,19 +370,32 @@ export abstract class ScrollableListPanel<T> extends BasePanel {
260
370
  ): Line[] {
261
371
  this.needsRender = false;
262
372
  const palette = this.getPalette();
263
- const items = this.getItems();
373
+ const items = this.getVisibleItems();
264
374
  const title = options.title ?? this.name;
265
375
 
376
+ // Auto-inject the filter input line at the top of the header when filtering
377
+ // is enabled, so every filterable panel gets the same affordance for free.
378
+ const baseHeader = options.header ? [...options.header as Line[]] : [];
379
+ const header: Line[] | undefined = this.filterEnabled
380
+ ? [this.buildFilterLine(width), ...baseHeader]
381
+ : (options.header ? baseHeader : undefined);
382
+
383
+ // Standardized keyboard-hints footer row (rendered below any explicit footer).
384
+ const hintsLine = options.hints && options.hints.length > 0
385
+ ? buildKeyboardHints(width, options.hints, palette)
386
+ : null;
387
+
266
388
  // I2: inject error line into footer when present
267
389
  const errorLine = this.renderErrorLine(width);
268
390
  const baseFooter = options.footer ? [...options.footer as Line[]] : [];
391
+ if (hintsLine) baseFooter.push(hintsLine);
269
392
  const effectiveFooter: Line[] = errorLine ? [errorLine, ...baseFooter] : baseFooter;
270
393
 
271
394
  // I3: if loading, show spinner in place of normal content
272
395
  const spinnerLine = this.renderLoadingLine(width, options.spinnerFrame ?? 0);
273
396
  if (spinnerLine) {
274
397
  const loadingSection = { lines: [spinnerLine] };
275
- const headerSection = options.header ? [{ lines: options.header as Line[] }] : [];
398
+ const headerSection = header ? [{ lines: header }] : [];
276
399
  const lines = buildPanelWorkspace(width, height, {
277
400
  title,
278
401
  sections: [...headerSection, loadingSection],
@@ -306,17 +429,22 @@ export abstract class ScrollableListPanel<T> extends BasePanel {
306
429
 
307
430
  // Empty state
308
431
  if (scrollableLines.length === 0) {
432
+ // Distinguish "filter hides everything" from "genuinely empty".
433
+ const filteredToEmpty = this.filterEnabled && this.filterQuery.trim() !== '' && this.getItems().length > 0;
434
+ const emptyMessage = filteredToEmpty
435
+ ? ` No matches for "${this.filterQuery.trim()}"`
436
+ : (options.emptyMessage ?? this.getEmptyStateMessage());
309
437
  const emptyLines = buildEmptyState(
310
438
  width,
311
- options.emptyMessage ?? this.getEmptyStateMessage(),
312
- '',
313
- this.getEmptyStateActions(),
439
+ emptyMessage,
440
+ filteredToEmpty ? 'Press Esc to clear the filter.' : '',
441
+ filteredToEmpty ? [] : this.getEmptyStateActions(),
314
442
  palette,
315
443
  );
316
444
  const lines = buildPanelWorkspace(width, height, {
317
445
  title,
318
446
  sections: [
319
- ...(options.header ? [{ lines: options.header as Line[] }] : []),
447
+ ...(header ? [{ lines: header }] : []),
320
448
  { lines: emptyLines },
321
449
  ...(effectiveFooter.length > 0 ? [{ lines: effectiveFooter }] : []),
322
450
  ],
@@ -327,7 +455,7 @@ export abstract class ScrollableListPanel<T> extends BasePanel {
327
455
  }
328
456
 
329
457
  // Resolve scrollable section (updates scrollStart)
330
- const beforeSections = options.header ? [{ lines: options.header as Line[] }] : [];
458
+ const beforeSections = header ? [{ lines: header }] : [];
331
459
  const afterSections = effectiveFooter.length > 0 ? [{ lines: effectiveFooter }] : [];
332
460
 
333
461
  const resolved = resolveScrollablePanelSection(width, height, {
@@ -359,133 +487,3 @@ export abstract class ScrollableListPanel<T> extends BasePanel {
359
487
  return lines.slice(0, height);
360
488
  }
361
489
  }
362
-
363
- // ---------------------------------------------------------------------------
364
- // SearchableListPanel<T>
365
- // ---------------------------------------------------------------------------
366
-
367
- /**
368
- * Extends `ScrollableListPanel<T>` with inline search/filter support.
369
- *
370
- * Subclasses implement:
371
- * - `getAllItems()` — the full (unfiltered) item list
372
- * - `matchesSearch(item, query)` — case-insensitive filter predicate
373
- *
374
- * `getItems()` is implemented here and returns filtered results. Do NOT
375
- * override `getItems()` in subclasses — override `getAllItems()` instead.
376
- *
377
- * Search state:
378
- * - Printable characters append to `searchQuery`.
379
- * - Backspace/Delete removes the last character.
380
- * - Escape clears the query.
381
- * - Navigation keys (up/down/etc.) are forwarded to the parent.
382
- *
383
- * Render the search input line by calling `buildSearchInput(width)` from
384
- * your panel's header builder.
385
- */
386
- export abstract class SearchableListPanel<T> extends ScrollableListPanel<T> {
387
- protected searchQuery = '';
388
-
389
- private _filteredItems: readonly T[] = [];
390
- private _filterDirty = true;
391
-
392
- // -------------------------------------------------------------------------
393
- // Abstract — subclasses must implement
394
- // -------------------------------------------------------------------------
395
-
396
- /** Return the full unfiltered item list. */
397
- protected abstract getAllItems(): readonly T[];
398
-
399
- /** Return true if `item` matches the search `query`. */
400
- protected abstract matchesSearch(item: T, query: string): boolean;
401
-
402
- // -------------------------------------------------------------------------
403
- // getItems — returns filtered list (do NOT override in subclasses)
404
- // -------------------------------------------------------------------------
405
-
406
- protected getItems(): readonly T[] {
407
- if (this._filterDirty) {
408
- const all = this.getAllItems();
409
- this._filteredItems = this.searchQuery
410
- ? all.filter((item) => this.matchesSearch(item, this.searchQuery))
411
- : all;
412
- this._filterDirty = false;
413
- // Clamp after filter to keep selection in bounds
414
- this.clampSelection();
415
- }
416
- return this._filteredItems;
417
- }
418
-
419
- /**
420
- * Mark the filter cache as stale.
421
- * Call this whenever `getAllItems()` returns new data.
422
- */
423
- protected invalidateFilter(): void {
424
- this._filterDirty = true;
425
- this.needsRender = true;
426
- }
427
-
428
- // -------------------------------------------------------------------------
429
- // Input — search first, navigation second
430
- // -------------------------------------------------------------------------
431
-
432
- handleInput(key: string): boolean {
433
- // Backspace: trim query
434
- if (isPanelSearchBackspace(key)) {
435
- if (this.searchQuery.length > 0) {
436
- this.searchQuery = this.searchQuery.slice(0, -1);
437
- this._filterDirty = true;
438
- this.needsRender = true;
439
- return true;
440
- }
441
- return false;
442
- }
443
-
444
- // Escape: clear query
445
- if (isPanelSearchCancel(key)) {
446
- if (this.searchQuery.length > 0) {
447
- this.searchQuery = '';
448
- this._filterDirty = true;
449
- this.needsRender = true;
450
- return true;
451
- }
452
- return false;
453
- }
454
-
455
- // Printable characters: append to query
456
- if (isPanelSearchPrintable(key)) {
457
- this.searchQuery += key;
458
- this._filterDirty = true;
459
- this.needsRender = true;
460
- return true;
461
- }
462
-
463
- // Navigation and Enter: delegate to parent
464
- return super.handleInput(key);
465
- }
466
-
467
- /**
468
- * Build the filter input `Line` for use in a panel header section.
469
- *
470
- * Renders the filter label and current query with context-sensitive formatting:
471
- *
472
- * - **Focused** (`focused = true`): `[Filter] query_` — active, bold, cursor visible
473
- * - **Unfocused** (`focused = false`): `Filter: query` — dim, no cursor
474
- *
475
- * @param width Panel width in columns.
476
- * @param label Label text (default: `'Filter'`).
477
- * @param focused Whether the filter input is currently active.
478
- */
479
- protected buildFilterInputLine(width: number, label = 'Filter', focused: boolean): Line {
480
- const palette = this.getPalette();
481
- const formattedLabel = focused ? `[${label}] ` : `${label}: `;
482
- const value = focused ? `${this.searchQuery}_` : this.searchQuery;
483
- // Pass active:false when focused to prevent buildSearchInputLine from converting the
484
- // trailing '_' cursor to the block-glyph (GLYPHS.surface.cursor). The focused visual
485
- // affordance is provided by the '[Label] ' bracket format and explicit inputBg/info colors.
486
- const opts = focused
487
- ? { active: false, bg: palette.inputBg, valueColor: palette.info }
488
- : { active: false };
489
- return buildSearchInputLine(width, formattedLabel, value, palette, opts);
490
- }
491
- }