@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.
- package/CHANGELOG.md +124 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +60 -26
- package/src/input/handler-feed.ts +17 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +53 -8
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +117 -1
- package/src/main.ts +5 -2
- package/src/panels/agent-inspector-panel.ts +139 -45
- package/src/panels/agent-inspector-shared.ts +147 -3
- package/src/panels/approval-panel.ts +204 -102
- package/src/panels/automation-control-panel.ts +370 -103
- package/src/panels/base-panel.ts +26 -2
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +253 -75
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +159 -56
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +354 -95
- package/src/panels/cost-tracker-panel.ts +243 -57
- package/src/panels/debug-panel.ts +347 -159
- package/src/panels/diff-panel.ts +257 -82
- package/src/panels/docs-panel.ts +166 -64
- package/src/panels/eval-panel.ts +336 -109
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +238 -159
- package/src/panels/file-preview-panel.ts +141 -59
- package/src/panels/git-panel.ts +360 -191
- package/src/panels/hooks-panel.ts +181 -19
- package/src/panels/incident-review-panel.ts +252 -43
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +310 -103
- package/src/panels/knowledge-graph-panel.ts +480 -58
- package/src/panels/local-auth-panel.ts +251 -33
- package/src/panels/marketplace-panel.ts +219 -33
- package/src/panels/memory-panel.ts +83 -81
- package/src/panels/ops-control-panel.ts +211 -32
- package/src/panels/ops-strategy-panel.ts +131 -45
- package/src/panels/orchestration-panel.ts +259 -67
- package/src/panels/panel-list-panel.ts +184 -136
- package/src/panels/panel-manager.ts +120 -13
- package/src/panels/plan-dashboard-panel.ts +507 -74
- package/src/panels/plugins-panel.ts +227 -34
- package/src/panels/policy-panel.ts +264 -55
- package/src/panels/polish-core.ts +162 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +67 -149
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +84 -113
- package/src/panels/provider-health-panel.ts +438 -480
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +136 -38
- package/src/panels/remote-panel.ts +123 -38
- package/src/panels/routes-panel.ts +87 -24
- package/src/panels/sandbox-panel.ts +232 -65
- package/src/panels/scrollable-list-panel.ts +143 -145
- package/src/panels/security-panel.ts +204 -52
- package/src/panels/services-panel.ts +174 -76
- package/src/panels/session-browser-panel.ts +101 -6
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +346 -68
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +105 -34
- package/src/panels/symbol-outline-panel.ts +287 -154
- package/src/panels/system-messages-panel.ts +172 -38
- package/src/panels/tasks-panel.ts +348 -134
- package/src/panels/thinking-panel.ts +66 -32
- package/src/panels/token-budget-panel.ts +233 -46
- package/src/panels/tool-inspector-panel.ts +170 -54
- package/src/panels/types.ts +65 -4
- package/src/panels/work-plan-panel.ts +393 -39
- package/src/panels/worktree-panel.ts +239 -61
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +146 -150
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +22 -16
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +30 -11
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +8 -6
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +41 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +45 -36
- package/src/renderer/ui-primitives.ts +23 -2
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/utils/splash-lines.ts +44 -3
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -666
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -214
- package/src/panels/forensics-panel.ts +0 -364
- package/src/panels/panel-picker.ts +0 -106
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -218
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/schedule-panel.ts +0 -342
- package/src/panels/watchers-panel.ts +0 -193
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- 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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
...(
|
|
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 =
|
|
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
|
-
}
|