@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
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* Features:
|
|
5
5
|
* - Arrow keys to navigate, Enter to open the selected panel
|
|
6
6
|
* - Open/closed indicator (● open, ○ closed)
|
|
7
|
-
* -
|
|
7
|
+
* - Press `/` to filter; typing narrows, Esc clears, action keys work outside filter mode
|
|
8
8
|
* - Grouped by category
|
|
9
9
|
* - `T` / `B` to place a panel in the top/bottom pane
|
|
10
10
|
* - `M` to move an open panel to the other pane
|
|
11
11
|
* - `S` to toggle the split and Tab to switch pane focus
|
|
12
|
+
* - `w` to close the selected panel if it is currently open (either pane)
|
|
12
13
|
*
|
|
13
14
|
* Open via /panel list.
|
|
14
15
|
*/
|
|
@@ -27,74 +28,89 @@ import {
|
|
|
27
28
|
buildSummaryBlock,
|
|
28
29
|
buildPanelWorkspace,
|
|
29
30
|
DEFAULT_PANEL_PALETTE,
|
|
31
|
+
extendPalette,
|
|
30
32
|
resolvePrimaryScrollableSection,
|
|
31
33
|
type PanelWorkspaceSection,
|
|
32
34
|
} from './polish.ts';
|
|
33
|
-
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
35
|
+
import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
|
|
34
36
|
import { wrapWithHangingIndent } from '../renderer/text-layout.ts';
|
|
35
|
-
import {
|
|
36
|
-
getPanelSearchFocusTransition,
|
|
37
|
-
isPanelSearchBackspace,
|
|
38
|
-
isPanelSearchCancel,
|
|
39
|
-
isPanelSearchCommit,
|
|
40
|
-
isPanelSearchPrintable,
|
|
41
|
-
} from './search-focus.ts';
|
|
37
|
+
import { isPanelSearchPrintable } from './search-focus.ts';
|
|
42
38
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
43
39
|
|
|
44
40
|
// ── Colour palette ────────────────────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const CATEGORY_ORDER: PanelCategory[] = ['development', 'agent', 'monitoring', 'session', 'ai'];
|
|
41
|
+
// Domain accents only; base chrome (header/headerBg/label/value/dim/good/bad/
|
|
42
|
+
// info/selectBg) comes from DEFAULT_PANEL_PALETTE.
|
|
43
|
+
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
44
|
+
search: '#f97316', // active search-filter highlight
|
|
45
|
+
paneBottom: '#a78bfa', // bottom-pane placement marker (distinct from top/info cyan)
|
|
46
|
+
} as const);
|
|
47
|
+
|
|
48
|
+
// WO-152: the former single 'monitoring' bucket is split into five operator
|
|
49
|
+
// domains (see PanelCategory in types.ts). Order groups related domains
|
|
50
|
+
// together in the picker: build/dev surfaces, agent-facing surfaces, then
|
|
51
|
+
// the five former-monitoring domains (providers -> security -> automation ->
|
|
52
|
+
// incidents -> live runtime consoles), then session/AI utility surfaces.
|
|
53
|
+
const CATEGORY_ORDER: PanelCategory[] = [
|
|
54
|
+
'development',
|
|
55
|
+
'agent',
|
|
56
|
+
'providers',
|
|
57
|
+
'security-policy',
|
|
58
|
+
'automation-control',
|
|
59
|
+
'incidents-diagnostics',
|
|
60
|
+
'runtime-ops',
|
|
61
|
+
'session',
|
|
62
|
+
'ai',
|
|
63
|
+
];
|
|
69
64
|
const CATEGORY_LABELS: Record<PanelCategory, string> = {
|
|
70
|
-
development:
|
|
71
|
-
agent:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
65
|
+
development: 'Development',
|
|
66
|
+
agent: 'Agent',
|
|
67
|
+
providers: 'Providers',
|
|
68
|
+
'security-policy': 'Security & Policy',
|
|
69
|
+
'automation-control': 'Automation Control',
|
|
70
|
+
'incidents-diagnostics': 'Incidents & Diagnostics',
|
|
71
|
+
'runtime-ops': 'Runtime Ops',
|
|
72
|
+
session: 'Session',
|
|
73
|
+
ai: 'AI',
|
|
75
74
|
};
|
|
76
75
|
|
|
77
76
|
// ── Layout constants ──────────────────────────────────────────────────────────
|
|
78
77
|
const NAME_COL_WIDTH = 22;
|
|
79
|
-
const PREFIX_WIDTH =
|
|
78
|
+
const PREFIX_WIDTH = 5; // arrow + dot + health-flag + space + space
|
|
80
79
|
|
|
81
|
-
/** A flat entry in the navigable list — either a
|
|
80
|
+
/** A flat entry in the navigable list — either a section header or a panel row. */
|
|
82
81
|
type ListEntry =
|
|
83
|
-
| { kind: 'header';
|
|
82
|
+
| { kind: 'header'; label: string }
|
|
84
83
|
| { kind: 'panel'; reg: PanelRegistration };
|
|
85
84
|
|
|
85
|
+
/** Number of most-recently-opened panels to surface in the Recent group. */
|
|
86
|
+
const RECENT_LIMIT = 5;
|
|
87
|
+
|
|
86
88
|
function panelPlacementMarker(options: {
|
|
87
89
|
isTopOpen: boolean;
|
|
88
90
|
isBottomOpen: boolean;
|
|
89
91
|
focusedPane: 'top' | 'bottom';
|
|
90
92
|
}): { text: string; color: string } {
|
|
91
93
|
const { isTopOpen, isBottomOpen, focusedPane } = options;
|
|
92
|
-
if (isTopOpen && isBottomOpen) return { text: '◆', color: C.
|
|
93
|
-
if (isTopOpen) return { text: focusedPane === 'top' ? '▲' : '△', color: C.
|
|
94
|
+
if (isTopOpen && isBottomOpen) return { text: '◆', color: C.value };
|
|
95
|
+
if (isTopOpen) return { text: focusedPane === 'top' ? '▲' : '△', color: C.info };
|
|
94
96
|
if (isBottomOpen) return { text: focusedPane === 'bottom' ? '▼' : '▽', color: C.paneBottom };
|
|
95
97
|
return { text: ' ', color: C.dim };
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Optional per-row resource-health flag, sourced straight from the injected
|
|
102
|
+
* ComponentHealthMonitor (fills the gap left by WO-004's panel-resources
|
|
103
|
+
* panel removal — health is still readable, just from here instead of a
|
|
104
|
+
* dedicated panel). Blank for panels with no health record yet (never
|
|
105
|
+
* rendered) or currently healthy.
|
|
106
|
+
*/
|
|
107
|
+
function panelHealthMarker(health: { healthStatus: 'healthy' | 'warning' | 'overloaded' } | undefined): { text: string; color: string } {
|
|
108
|
+
if (!health) return { text: ' ', color: C.dim };
|
|
109
|
+
if (health.healthStatus === 'overloaded') return { text: '!', color: C.bad };
|
|
110
|
+
if (health.healthStatus === 'warning') return { text: '~', color: C.warn };
|
|
111
|
+
return { text: ' ', color: C.dim };
|
|
112
|
+
}
|
|
113
|
+
|
|
98
114
|
function wrapPanelDescription(text: string, width: number, maxLines = 2): string[] {
|
|
99
115
|
if (width <= 0) return [''];
|
|
100
116
|
const lines = wrapWithHangingIndent(text, width, '', maxLines);
|
|
@@ -107,7 +123,8 @@ export class PanelListPanel extends BasePanel {
|
|
|
107
123
|
private _selectedIndex = 0;
|
|
108
124
|
private _scrollOffset = 0;
|
|
109
125
|
private _query = '';
|
|
110
|
-
|
|
126
|
+
/** WO-153: converged modal '/' filter — mirrors ScrollableListPanel's _handleFilterKey contract. */
|
|
127
|
+
private _filterActive = false;
|
|
111
128
|
private _cachedEntries: ListEntry[] | null = null;
|
|
112
129
|
private _entriesDirty = true;
|
|
113
130
|
|
|
@@ -123,7 +140,7 @@ export class PanelListPanel extends BasePanel {
|
|
|
123
140
|
this._selectedIndex = 0;
|
|
124
141
|
this._scrollOffset = 0;
|
|
125
142
|
this._query = '';
|
|
126
|
-
this.
|
|
143
|
+
this._filterActive = false;
|
|
127
144
|
this._entriesDirty = true;
|
|
128
145
|
}
|
|
129
146
|
|
|
@@ -132,33 +149,24 @@ export class PanelListPanel extends BasePanel {
|
|
|
132
149
|
this._entriesDirty = true;
|
|
133
150
|
}
|
|
134
151
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Filter-mode key handling (WO-153: converged modal '/' filter, mirrors
|
|
154
|
+
* ScrollableListPanel's private _handleFilterKey). Returns `true`/`false`
|
|
155
|
+
* when consumed/ignored in filter context, or `null` to fall through to
|
|
156
|
+
* normal navigation and single-letter action keys (T/B/M/S/w/tab).
|
|
157
|
+
*/
|
|
158
|
+
private _handleFilterKey(key: string): boolean | null {
|
|
159
|
+
if (this._filterActive) {
|
|
160
|
+
if (key === 'escape') {
|
|
161
|
+
this._filterActive = false;
|
|
162
|
+
this._query = '';
|
|
143
163
|
this._selectedIndex = 0;
|
|
144
164
|
this._scrollOffset = 0;
|
|
145
165
|
this.markDirty();
|
|
146
166
|
return true;
|
|
147
167
|
}
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
this._query = this._query.slice(0, -1);
|
|
151
|
-
this._clampSelection(entries);
|
|
152
|
-
this.markDirty();
|
|
153
|
-
return true;
|
|
154
|
-
}
|
|
155
|
-
if (isPanelSearchCancel(key)) {
|
|
156
|
-
this._filterFocused = false;
|
|
157
|
-
this.markDirty();
|
|
158
|
-
return true;
|
|
159
|
-
}
|
|
160
|
-
if (isPanelSearchCommit(key)) {
|
|
161
|
-
this._filterFocused = false;
|
|
168
|
+
if (key === 'return' || key === 'enter') {
|
|
169
|
+
this._filterActive = false; // commit; keep the query applied
|
|
162
170
|
const selectedPanel = this._getSelectedPanelEntry(this._buildEntries());
|
|
163
171
|
if (selectedPanel) {
|
|
164
172
|
try {
|
|
@@ -170,6 +178,17 @@ export class PanelListPanel extends BasePanel {
|
|
|
170
178
|
this.markDirty();
|
|
171
179
|
return true;
|
|
172
180
|
}
|
|
181
|
+
if (key === 'backspace' || key === 'delete') {
|
|
182
|
+
this._query = this._query.slice(0, -1);
|
|
183
|
+
this._clampSelection(this._buildEntries());
|
|
184
|
+
this.markDirty();
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
// Arrow/paging keys navigate the filtered list — fall through.
|
|
188
|
+
if (key === 'up' || key === 'down' || key === 'pageup' || key === 'pagedown' || key === 'home' || key === 'end') {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
// Any printable character (including single-letter action keys like T/B) extends the query.
|
|
173
192
|
if (isPanelSearchPrintable(key)) {
|
|
174
193
|
this._query += key;
|
|
175
194
|
this._selectedIndex = 0;
|
|
@@ -179,13 +198,37 @@ export class PanelListPanel extends BasePanel {
|
|
|
179
198
|
}
|
|
180
199
|
return false;
|
|
181
200
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (transition === 'focus-search') {
|
|
185
|
-
this._filterFocused = true;
|
|
201
|
+
if (key === '/') {
|
|
202
|
+
this._filterActive = true;
|
|
186
203
|
this.markDirty();
|
|
187
204
|
return true;
|
|
188
205
|
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The filter input line — pinned rendering contract shared with
|
|
211
|
+
* ScrollableListPanel.buildFilterLine: 'Filter: ' unfocused / '[Filter] '
|
|
212
|
+
* focused, literal trailing '_' cursor while active (active:false is
|
|
213
|
+
* passed to buildSearchInputLine to suppress its block-glyph cursor
|
|
214
|
+
* substitution).
|
|
215
|
+
*/
|
|
216
|
+
private _buildFilterLine(width: number): Line {
|
|
217
|
+
const label = this._filterActive ? '[Filter] ' : 'Filter: ';
|
|
218
|
+
const value = this._filterActive ? `${this._query}_` : this._query;
|
|
219
|
+
return buildSearchInputLine(width, label, value, C, {
|
|
220
|
+
active: false,
|
|
221
|
+
bg: this._filterActive ? C.headerBg : C.sectionBg,
|
|
222
|
+
emptyLabel: '(/ to filter)',
|
|
223
|
+
valueColor: this._filterActive ? C.search : (this._query ? C.value : C.dim),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
public handleInput(key: string): boolean {
|
|
228
|
+
const filterResult = this._handleFilterKey(key);
|
|
229
|
+
if (filterResult !== null) return filterResult;
|
|
230
|
+
|
|
231
|
+
const entries = this._buildEntries();
|
|
189
232
|
|
|
190
233
|
// Navigation
|
|
191
234
|
if (key === 'up' || key === 'k') {
|
|
@@ -248,35 +291,27 @@ export class PanelListPanel extends BasePanel {
|
|
|
248
291
|
return true;
|
|
249
292
|
}
|
|
250
293
|
|
|
251
|
-
if (key === '
|
|
252
|
-
this.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
this._clampSelection(entries);
|
|
294
|
+
if (key === 'w') {
|
|
295
|
+
const selectedPanel = this._getSelectedPanelEntry(entries);
|
|
296
|
+
if (selectedPanel) {
|
|
297
|
+
try {
|
|
298
|
+
// Existing public PanelManager.close() API only — panel-manager.ts
|
|
299
|
+
// itself is WO-150's surface this wave.
|
|
300
|
+
this.panelManager.close(selectedPanel.reg.id);
|
|
301
|
+
} catch (err) {
|
|
302
|
+
logger.warn(`[panel-list] failed to close panel: ${err}`);
|
|
303
|
+
}
|
|
262
304
|
this.markDirty();
|
|
263
|
-
return true;
|
|
264
305
|
}
|
|
265
|
-
return
|
|
306
|
+
return true;
|
|
266
307
|
}
|
|
267
308
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
this._clampSelection(entries);
|
|
273
|
-
this.markDirty();
|
|
274
|
-
return true;
|
|
275
|
-
}
|
|
276
|
-
return false;
|
|
309
|
+
if (key === 'tab') {
|
|
310
|
+
this.panelManager.togglePaneFocus();
|
|
311
|
+
this.markDirty();
|
|
312
|
+
return true;
|
|
277
313
|
}
|
|
278
314
|
|
|
279
|
-
// Printable character: append to search query
|
|
280
315
|
return false;
|
|
281
316
|
}
|
|
282
317
|
|
|
@@ -295,18 +330,17 @@ export class PanelListPanel extends BasePanel {
|
|
|
295
330
|
intro,
|
|
296
331
|
sections: [{
|
|
297
332
|
title: 'Filter',
|
|
298
|
-
lines: [
|
|
299
|
-
active: this._filterFocused,
|
|
300
|
-
bg: C.searchBg,
|
|
301
|
-
emptyLabel: this._filterFocused ? '(type to filter)' : '(/ or up at top)',
|
|
302
|
-
valueColor: C.search,
|
|
303
|
-
})],
|
|
333
|
+
lines: [this._buildFilterLine(width)],
|
|
304
334
|
}, {
|
|
305
335
|
lines: buildEmptyState(
|
|
306
336
|
width,
|
|
307
337
|
' No panels match filter.',
|
|
308
338
|
'Clear the filter or search for another panel by id, name, description, or category.',
|
|
309
|
-
|
|
339
|
+
// The real remedy for a stuck filter is clearing it in-panel;
|
|
340
|
+
// '/panel list' would just re-activate this same instance with
|
|
341
|
+
// the filter still applied. A committed filter needs '/' to
|
|
342
|
+
// re-enter editing before Esc clears it.
|
|
343
|
+
[{ command: '/ then Esc', summary: 'clear the filter and show every panel' }],
|
|
310
344
|
C,
|
|
311
345
|
),
|
|
312
346
|
}],
|
|
@@ -323,32 +357,33 @@ export class PanelListPanel extends BasePanel {
|
|
|
323
357
|
const topIds = new Set(pm.getTopPane().panels.map(p => p.id));
|
|
324
358
|
const bottomIds = new Set(pm.getBottomPane().panels.map(p => p.id));
|
|
325
359
|
const focusedPane = pm.getFocusedPane();
|
|
326
|
-
const footerLines = [buildPanelLine(width, [[` [${this._selectedIndex + 1}/${panelEntries.length}] ↑/↓ nav Enter open T/B place M move S split Tab focus
|
|
360
|
+
const footerLines = [buildPanelLine(width, [[truncateDisplay(` [${this._selectedIndex + 1}/${panelEntries.length}] ↑/↓ nav Enter open w close T/B place M move S split Tab focus`, width), C.dim]])];
|
|
361
|
+
// Optional health rollup — only present when a ComponentHealthMonitor was
|
|
362
|
+
// injected (fills the gap left by WO-004's panel-resources panel removal).
|
|
363
|
+
const allHealth = this.componentHealthMonitor?.getAllHealth() ?? [];
|
|
364
|
+
const unhealthyCount = allHealth.filter((h) => h.healthStatus !== 'healthy').length;
|
|
365
|
+
|
|
327
366
|
const postureLines: Line[] = [
|
|
328
367
|
buildKeyValueLine(width, [
|
|
329
|
-
{ label: 'visible panels', value: String(pm.getAllOpen().length), valueColor: pm.getAllOpen().length > 0 ? C.
|
|
330
|
-
{ label: 'focused pane', value: focusedPane, valueColor: focusedPane === 'top' ? C.
|
|
368
|
+
{ label: 'visible panels', value: String(pm.getAllOpen().length), valueColor: pm.getAllOpen().length > 0 ? C.value : C.dim },
|
|
369
|
+
{ label: 'focused pane', value: focusedPane, valueColor: focusedPane === 'top' ? C.info : C.paneBottom },
|
|
331
370
|
{ label: 'split', value: pm.isBottomPaneVisible() ? 'dual' : 'single', valueColor: pm.isBottomPaneVisible() ? C.info : C.dim },
|
|
332
371
|
{ label: 'results', value: String(panelEntries.length), valueColor: C.value },
|
|
372
|
+
...(this.componentHealthMonitor
|
|
373
|
+
? [{ label: 'flagged', value: String(unhealthyCount), valueColor: unhealthyCount > 0 ? C.warn : C.good }]
|
|
374
|
+
: []),
|
|
333
375
|
], C),
|
|
334
|
-
buildPanelLine(width, [[` Filter owns input only when selected. Open and switch operations should land directly in focused panel state.`, C.
|
|
335
|
-
];
|
|
336
|
-
const entryLines: Line[] = [
|
|
337
|
-
buildSearchInputLine(width, 'Filter: ', `${this._query}${this._filterFocused ? '_' : ''}`, C, {
|
|
338
|
-
active: this._filterFocused,
|
|
339
|
-
bg: C.searchBg,
|
|
340
|
-
emptyLabel: this._filterFocused ? '(type to filter)' : '(/ or up at top)',
|
|
341
|
-
valueColor: C.search,
|
|
342
|
-
}),
|
|
376
|
+
buildPanelLine(width, [[` Filter owns input only when selected. Open and switch operations should land directly in focused panel state.`, C.header]]),
|
|
343
377
|
];
|
|
378
|
+
const entryLines: Line[] = [this._buildFilterLine(width)];
|
|
344
379
|
const renderedBlocks: Array<{ entry: ListEntry; lines: Line[]; panelFlatIndex?: number }> = [];
|
|
345
380
|
let flatPanelIndex = 0;
|
|
346
381
|
for (const entry of entries) {
|
|
347
382
|
if (entry.kind === 'header') {
|
|
348
|
-
const label = ` ── ${
|
|
383
|
+
const label = ` ── ${entry.label} ${'─'.repeat(Math.max(0, width - 6 - entry.label.length))}`;
|
|
349
384
|
renderedBlocks.push({
|
|
350
385
|
entry,
|
|
351
|
-
lines: [buildPanelLine(width, [[label
|
|
386
|
+
lines: [buildPanelLine(width, [[truncateDisplay(label, width), C.label, C.headerBg]])],
|
|
352
387
|
});
|
|
353
388
|
} else {
|
|
354
389
|
const flatIdx = flatPanelIndex++;
|
|
@@ -356,28 +391,36 @@ export class PanelListPanel extends BasePanel {
|
|
|
356
391
|
const isTopOpen = topIds.has(entry.reg.id);
|
|
357
392
|
const isBottomOpen = bottomIds.has(entry.reg.id);
|
|
358
393
|
const dot = isTopOpen || isBottomOpen ? '●' : '○';
|
|
359
|
-
const dotColor = isTopOpen || isBottomOpen ? C.
|
|
360
|
-
const nameColor =
|
|
361
|
-
const nameStr = entry.reg.name
|
|
394
|
+
const dotColor = isTopOpen || isBottomOpen ? C.good : C.dim;
|
|
395
|
+
const nameColor = C.value;
|
|
396
|
+
const nameStr = fitDisplay(entry.reg.name, NAME_COL_WIDTH);
|
|
362
397
|
const descStartCol = PREFIX_WIDTH + NAME_COL_WIDTH + 1;
|
|
363
398
|
const descWidth = Math.max(1, width - descStartCol);
|
|
364
399
|
const descLines = wrapPanelDescription(entry.reg.description, descWidth, 2);
|
|
365
400
|
const placement = panelPlacementMarker({ isTopOpen, isBottomOpen, focusedPane });
|
|
401
|
+
const health = panelHealthMarker(this.componentHealthMonitor?.getHealth(entry.reg.id));
|
|
402
|
+
// When searching, the flat result list loses its category grouping — so
|
|
403
|
+
// tag each result with a dim [Category] so the origin stays discoverable.
|
|
404
|
+
const categoryTag = this._query
|
|
405
|
+
? `[${CATEGORY_LABELS[entry.reg.category]}] `
|
|
406
|
+
: '';
|
|
366
407
|
const blockLines: Line[] = [
|
|
367
408
|
buildPanelListRow(width, [
|
|
368
409
|
{ text: dot, fg: dotColor },
|
|
369
410
|
{ text: placement.text, fg: placement.color },
|
|
411
|
+
{ text: health.text, fg: health.color },
|
|
370
412
|
{ text: ' ', fg: C.dim },
|
|
371
413
|
{ text: `${nameStr} `, fg: nameColor },
|
|
372
|
-
{ text:
|
|
373
|
-
|
|
414
|
+
{ text: categoryTag, fg: C.label },
|
|
415
|
+
{ text: descLines[0] ?? '', fg: C.label },
|
|
416
|
+
], C, { selected: isSelected, selectedBg: C.selectBg, markerColor: C.info }),
|
|
374
417
|
];
|
|
375
418
|
if ((descLines[1] ?? '').length > 0) {
|
|
376
419
|
blockLines.push(buildPanelLine(width, [
|
|
377
|
-
[' '.repeat(PREFIX_WIDTH), C.dim, isSelected ? C.
|
|
378
|
-
[' '.repeat(NAME_COL_WIDTH), C.dim, isSelected ? C.
|
|
379
|
-
[' ', C.dim, isSelected ? C.
|
|
380
|
-
[descLines[1] ?? '', C.
|
|
420
|
+
[' '.repeat(PREFIX_WIDTH), C.dim, isSelected ? C.selectBg : C.surfaceBg],
|
|
421
|
+
[' '.repeat(NAME_COL_WIDTH), C.dim, isSelected ? C.selectBg : C.surfaceBg],
|
|
422
|
+
[' ', C.dim, isSelected ? C.selectBg : C.surfaceBg],
|
|
423
|
+
[descLines[1] ?? '', C.label, isSelected ? C.selectBg : C.surfaceBg],
|
|
381
424
|
]));
|
|
382
425
|
}
|
|
383
426
|
renderedBlocks.push({ entry, lines: blockLines, panelFlatIndex: flatIdx });
|
|
@@ -443,6 +486,22 @@ export class PanelListPanel extends BasePanel {
|
|
|
443
486
|
const q = this._query.toLowerCase();
|
|
444
487
|
const entries: ListEntry[] = [];
|
|
445
488
|
|
|
489
|
+
// Recent group (browse mode only): the most-recently-opened panels, newest
|
|
490
|
+
// first, so frequently used panels are one keystroke away.
|
|
491
|
+
if (!q) {
|
|
492
|
+
const byId = new Map(manager.getRegisteredTypes().map((r) => [r.id, r]));
|
|
493
|
+
const recent: PanelRegistration[] = [];
|
|
494
|
+
for (const id of manager.getRecentlyOpened()) {
|
|
495
|
+
const reg = byId.get(id);
|
|
496
|
+
if (reg) recent.push(reg);
|
|
497
|
+
if (recent.length >= RECENT_LIMIT) break;
|
|
498
|
+
}
|
|
499
|
+
if (recent.length > 0) {
|
|
500
|
+
entries.push({ kind: 'header', label: 'Recent' });
|
|
501
|
+
for (const reg of recent) entries.push({ kind: 'panel', reg });
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
446
505
|
for (const cat of CATEGORY_ORDER) {
|
|
447
506
|
const regs = byCategory.get(cat) ?? [];
|
|
448
507
|
const filtered = q
|
|
@@ -456,7 +515,7 @@ export class PanelListPanel extends BasePanel {
|
|
|
456
515
|
|
|
457
516
|
if (filtered.length === 0) continue;
|
|
458
517
|
|
|
459
|
-
entries.push({ kind: 'header',
|
|
518
|
+
entries.push({ kind: 'header', label: CATEGORY_LABELS[cat] });
|
|
460
519
|
for (const reg of filtered) {
|
|
461
520
|
entries.push({ kind: 'panel', reg });
|
|
462
521
|
}
|
|
@@ -467,17 +526,6 @@ export class PanelListPanel extends BasePanel {
|
|
|
467
526
|
return entries;
|
|
468
527
|
}
|
|
469
528
|
|
|
470
|
-
/** Get the flat panel index (counting only panel entries) for a given panel id. */
|
|
471
|
-
private _flatPanelIndex(entries: ListEntry[], id: string): number {
|
|
472
|
-
let idx = 0;
|
|
473
|
-
for (const e of entries) {
|
|
474
|
-
if (e.kind !== 'panel') continue;
|
|
475
|
-
if (e.reg.id === id) return idx;
|
|
476
|
-
idx++;
|
|
477
|
-
}
|
|
478
|
-
return -1;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
529
|
/** Get the ListEntry for the currently selected panel. */
|
|
482
530
|
private _getSelectedPanelEntry(entries: ListEntry[]): Extract<ListEntry, { kind: 'panel' }> | null {
|
|
483
531
|
let idx = 0;
|