@pellux/goodvibes-tui 1.1.0 → 1.7.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 +85 -21
- package/README.md +20 -11
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/core/alert-gating.ts +67 -0
- package/src/core/approval-alert.ts +72 -0
- package/src/core/budget-breach-notifier.ts +106 -0
- package/src/core/conversation-rendering.ts +19 -0
- package/src/core/conversation.ts +18 -0
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/long-task-notifier.ts +33 -6
- package/src/core/system-message-router.ts +37 -51
- package/src/core/turn-cancellation.ts +20 -0
- package/src/core/turn-event-wiring.ts +64 -3
- package/src/export/cost-utils.ts +36 -0
- package/src/input/command-registry.ts +38 -1
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +192 -0
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +1 -1
- package/src/input/commands/image-runtime.ts +112 -0
- package/src/input/commands/intelligence-runtime.ts +11 -1
- package/src/input/commands/local-auth-runtime.ts +4 -1
- package/src/input/commands/local-runtime.ts +9 -3
- package/src/input/commands/marketplace-runtime.ts +2 -2
- package/src/input/commands/memory.ts +6 -1
- package/src/input/commands/operator-panel-runtime.ts +40 -24
- package/src/input/commands/planning-runtime.ts +26 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -6
- package/src/input/commands/plugin-runtime.ts +2 -2
- package/src/input/commands/policy-dispatch.ts +21 -0
- package/src/input/commands/provider-accounts-runtime.ts +1 -1
- package/src/input/commands/qrcode-runtime.ts +3 -3
- package/src/input/commands/recall-review.ts +35 -0
- package/src/input/commands/remote-runtime.ts +3 -3
- package/src/input/commands/runtime-services.ts +54 -13
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +16 -1
- package/src/input/commands/settings-sync-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +15 -7
- package/src/input/commands/skills-runtime.ts +2 -8
- package/src/input/commands/subscription-runtime.ts +2 -2
- package/src/input/commands/test-runtime.ts +277 -0
- package/src/input/commands/websearch-runtime.ts +101 -0
- package/src/input/commands/work-plan-runtime.ts +36 -10
- package/src/input/commands/workstream-runtime.ts +338 -0
- package/src/input/commands.ts +12 -0
- package/src/input/config-modal-types.ts +161 -0
- package/src/input/config-modal.ts +377 -0
- package/src/input/feed-context-factory.ts +7 -8
- package/src/input/handler-command-route.ts +27 -17
- package/src/input/handler-feed-routes.ts +61 -23
- package/src/input/handler-feed.ts +47 -23
- package/src/input/handler-interactions.ts +1 -3
- package/src/input/handler-modal-routes.ts +65 -0
- package/src/input/handler-modal-stack.ts +3 -5
- package/src/input/handler-modal-token-routes.ts +16 -49
- package/src/input/handler-picker-routes.ts +11 -94
- package/src/input/handler-shortcuts.ts +24 -14
- package/src/input/handler-types.ts +2 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +6 -28
- package/src/input/keybindings.ts +22 -8
- package/src/input/model-picker-types.ts +24 -6
- package/src/input/model-picker.ts +58 -0
- package/src/input/panel-integration-actions.ts +9 -170
- package/src/input/settings-modal-data.ts +95 -0
- package/src/input/settings-modal-mutations.ts +6 -4
- package/src/main.ts +24 -27
- package/src/panels/agent-inspector-shared.ts +2 -1
- package/src/panels/base-panel.ts +22 -1
- package/src/panels/builtin/agent.ts +21 -107
- package/src/panels/builtin/development.ts +15 -61
- package/src/panels/builtin/knowledge.ts +8 -32
- package/src/panels/builtin/operations.ts +84 -428
- package/src/panels/builtin/session.ts +21 -112
- package/src/panels/builtin/shared.ts +9 -43
- package/src/panels/builtin-modals.ts +218 -0
- package/src/panels/builtin-panels.ts +5 -0
- package/src/panels/cost-tracker-panel.ts +36 -10
- package/src/panels/diff-panel.ts +12 -43
- package/src/panels/eval-registry.ts +60 -0
- package/src/panels/fleet-panel.ts +800 -0
- package/src/panels/fleet-read-model.ts +477 -0
- package/src/panels/fleet-steer.ts +92 -0
- package/src/panels/fleet-stop.ts +125 -0
- package/src/panels/fleet-tabs.ts +230 -0
- package/src/panels/fleet-transcript.ts +356 -0
- package/src/panels/index.ts +7 -31
- package/src/panels/modals/hooks-modal.ts +187 -0
- package/src/panels/modals/keybindings-modal.ts +151 -0
- package/src/panels/modals/knowledge-modal.ts +158 -0
- package/src/panels/modals/local-auth-modal.ts +132 -0
- package/src/panels/modals/marketplace-modal.ts +218 -0
- package/src/panels/modals/memory-modal.ts +180 -0
- package/src/panels/modals/modal-surface-helpers.ts +54 -0
- package/src/panels/modals/modal-theme.ts +36 -0
- package/src/panels/modals/pairing-modal.ts +144 -0
- package/src/panels/modals/planning-modal.ts +282 -0
- package/src/panels/modals/plugins-modal.ts +174 -0
- package/src/panels/modals/policy-modal.ts +256 -0
- package/src/panels/modals/provider-health-modal.ts +136 -0
- package/src/panels/modals/remote-modal.ts +115 -0
- package/src/panels/modals/sandbox-modal.ts +150 -0
- package/src/panels/modals/security-modal.ts +217 -0
- package/src/panels/modals/services-modal.ts +179 -0
- package/src/panels/modals/settings-sync-modal.ts +142 -0
- package/src/panels/modals/skills-modal.ts +189 -0
- package/src/panels/modals/subscription-modal.ts +172 -0
- package/src/panels/modals/work-plan-modal.ts +149 -0
- package/src/panels/panel-confirm-overlay.ts +72 -0
- package/src/panels/panel-manager.ts +108 -5
- package/src/panels/project-planning-answer-actions.ts +4 -2
- package/src/panels/skills-panel.ts +7 -51
- package/src/panels/types.ts +13 -0
- package/src/permissions/hunk-selection.ts +179 -0
- package/src/permissions/prompt.ts +60 -4
- package/src/renderer/compaction-history-modal.ts +19 -3
- package/src/renderer/compaction-preview.ts +13 -2
- package/src/renderer/compaction-quality.ts +100 -0
- package/src/renderer/config-modal.ts +81 -0
- package/src/renderer/conversation-overlays.ts +10 -17
- package/src/renderer/fleet-tab-strip.ts +56 -0
- package/src/renderer/footer-tips.ts +10 -2
- package/src/renderer/git-status.ts +40 -0
- package/src/renderer/help-overlay.ts +2 -2
- package/src/renderer/model-workspace.ts +44 -1
- package/src/renderer/panel-workspace-bar.ts +8 -2
- package/src/renderer/turn-injection.ts +114 -0
- package/src/runtime/bootstrap-command-context.ts +21 -1
- package/src/runtime/bootstrap-command-parts.ts +34 -7
- package/src/runtime/bootstrap-core.ts +14 -4
- package/src/runtime/bootstrap-shell.ts +29 -16
- package/src/runtime/bootstrap.ts +11 -17
- package/src/runtime/code-index-services.ts +112 -0
- package/src/runtime/orchestrator-core-services.ts +49 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/services.ts +91 -43
- package/src/runtime/ui-services.ts +8 -0
- package/src/runtime/workstream-services.ts +195 -0
- package/src/shell/blocking-input.ts +22 -1
- package/src/shell/ui-openers.ts +129 -17
- package/src/utils/format-duration.ts +8 -18
- package/src/utils/splash-lines.ts +1 -1
- package/src/version.ts +1 -1
- package/src/panels/agent-inspector-panel.ts +0 -786
- package/src/panels/approval-panel.ts +0 -252
- package/src/panels/automation-control-panel.ts +0 -479
- package/src/panels/cockpit-panel.ts +0 -481
- package/src/panels/cockpit-read-model.ts +0 -238
- package/src/panels/communication-panel.ts +0 -256
- package/src/panels/control-plane-panel.ts +0 -470
- package/src/panels/debug-panel.ts +0 -615
- package/src/panels/docs-panel.ts +0 -384
- package/src/panels/eval-panel.ts +0 -627
- package/src/panels/file-explorer-panel.ts +0 -673
- package/src/panels/file-preview-panel.ts +0 -517
- package/src/panels/hooks-panel.ts +0 -401
- package/src/panels/incident-review-panel.ts +0 -406
- package/src/panels/intelligence-panel.ts +0 -383
- package/src/panels/knowledge-graph-panel.ts +0 -515
- package/src/panels/marketplace-panel.ts +0 -399
- package/src/panels/memory-panel.ts +0 -558
- package/src/panels/ops-control-panel.ts +0 -329
- package/src/panels/ops-strategy-panel.ts +0 -321
- package/src/panels/orchestration-panel.ts +0 -465
- package/src/panels/panel-list-panel.ts +0 -566
- package/src/panels/plan-dashboard-panel.ts +0 -707
- package/src/panels/policy-panel.ts +0 -517
- package/src/panels/project-planning-panel.ts +0 -731
- package/src/panels/provider-health-panel.ts +0 -678
- package/src/panels/provider-health-tracker.ts +0 -310
- package/src/panels/provider-health-views.ts +0 -567
- package/src/panels/qr-panel.ts +0 -280
- package/src/panels/remote-panel.ts +0 -534
- package/src/panels/routes-panel.ts +0 -241
- package/src/panels/sandbox-panel.ts +0 -456
- package/src/panels/security-panel.ts +0 -447
- package/src/panels/services-panel.ts +0 -329
- package/src/panels/session-browser-panel.ts +0 -496
- package/src/panels/settings-sync-panel.ts +0 -398
- package/src/panels/subscription-panel.ts +0 -342
- package/src/panels/symbol-outline-panel.ts +0 -619
- package/src/panels/system-messages-panel.ts +0 -364
- package/src/panels/tasks-panel.ts +0 -608
- package/src/panels/thinking-panel.ts +0 -333
- package/src/panels/tool-inspector-panel.ts +0 -537
- package/src/panels/work-plan-panel.ts +0 -540
- package/src/panels/worktree-panel.ts +0 -360
- package/src/panels/wrfc-panel.ts +0 -790
- package/src/renderer/agent-detail-modal.ts +0 -466
- package/src/renderer/live-tail-modal.ts +0 -156
- package/src/renderer/process-modal.ts +0 -671
- package/src/renderer/qr-renderer.ts +0 -120
|
@@ -1,566 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PanelListPanel — Shows all registered panels grouped by category.
|
|
3
|
-
*
|
|
4
|
-
* Features:
|
|
5
|
-
* - Arrow keys to navigate, Enter to open the selected panel
|
|
6
|
-
* - Open/closed indicator (● open, ○ closed)
|
|
7
|
-
* - Press `/` to filter; typing narrows, Esc clears, action keys work outside filter mode
|
|
8
|
-
* - Grouped by category
|
|
9
|
-
* - `T` / `B` to place a panel in the top/bottom pane
|
|
10
|
-
* - `M` to move an open panel to the other pane
|
|
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)
|
|
13
|
-
*
|
|
14
|
-
* Open via /panel list.
|
|
15
|
-
*/
|
|
16
|
-
import type { Line, Cell } from '../types/grid.ts';
|
|
17
|
-
import type { PanelCategory, PanelRegistration } from './types.ts';
|
|
18
|
-
import { BasePanel } from './base-panel.ts';
|
|
19
|
-
import type { PanelManager } from './panel-manager.ts';
|
|
20
|
-
import type { ComponentHealthMonitor } from '../runtime/perf/panel-health-monitor.ts';
|
|
21
|
-
import { createEmptyLine } from '../types/grid.ts';
|
|
22
|
-
import {
|
|
23
|
-
buildEmptyState,
|
|
24
|
-
buildKeyValueLine,
|
|
25
|
-
buildPanelListRow,
|
|
26
|
-
buildPanelLine,
|
|
27
|
-
buildSearchInputLine,
|
|
28
|
-
buildSummaryBlock,
|
|
29
|
-
buildPanelWorkspace,
|
|
30
|
-
DEFAULT_PANEL_PALETTE,
|
|
31
|
-
extendPalette,
|
|
32
|
-
resolvePrimaryScrollableSection,
|
|
33
|
-
type PanelWorkspaceSection,
|
|
34
|
-
} from './polish.ts';
|
|
35
|
-
import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
|
|
36
|
-
import { wrapWithHangingIndent } from '../renderer/text-layout.ts';
|
|
37
|
-
import { isPanelSearchPrintable } from './search-focus.ts';
|
|
38
|
-
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
39
|
-
|
|
40
|
-
// ── Colour palette ────────────────────────────────────────────────────────────
|
|
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
|
-
];
|
|
64
|
-
const CATEGORY_LABELS: Record<PanelCategory, string> = {
|
|
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',
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
// ── Layout constants ──────────────────────────────────────────────────────────
|
|
77
|
-
const NAME_COL_WIDTH = 22;
|
|
78
|
-
const PREFIX_WIDTH = 5; // arrow + dot + health-flag + space + space
|
|
79
|
-
|
|
80
|
-
/** A flat entry in the navigable list — either a section header or a panel row. */
|
|
81
|
-
type ListEntry =
|
|
82
|
-
| { kind: 'header'; label: string }
|
|
83
|
-
| { kind: 'panel'; reg: PanelRegistration };
|
|
84
|
-
|
|
85
|
-
/** Number of most-recently-opened panels to surface in the Recent group. */
|
|
86
|
-
const RECENT_LIMIT = 5;
|
|
87
|
-
|
|
88
|
-
function panelPlacementMarker(options: {
|
|
89
|
-
isTopOpen: boolean;
|
|
90
|
-
isBottomOpen: boolean;
|
|
91
|
-
focusedPane: 'top' | 'bottom';
|
|
92
|
-
}): { text: string; color: string } {
|
|
93
|
-
const { isTopOpen, isBottomOpen, focusedPane } = options;
|
|
94
|
-
if (isTopOpen && isBottomOpen) return { text: '◆', color: C.value };
|
|
95
|
-
if (isTopOpen) return { text: focusedPane === 'top' ? '▲' : '△', color: C.info };
|
|
96
|
-
if (isBottomOpen) return { text: focusedPane === 'bottom' ? '▼' : '▽', color: C.paneBottom };
|
|
97
|
-
return { text: ' ', color: C.dim };
|
|
98
|
-
}
|
|
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
|
-
|
|
114
|
-
function wrapPanelDescription(text: string, width: number, maxLines = 2): string[] {
|
|
115
|
-
if (width <= 0) return [''];
|
|
116
|
-
const lines = wrapWithHangingIndent(text, width, '', maxLines);
|
|
117
|
-
return lines.length > 0 ? lines.map((line) => truncateDisplay(line, width)) : [''];
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// ── PanelListPanel ────────────────────────────────────────────────────────────
|
|
121
|
-
|
|
122
|
-
export class PanelListPanel extends BasePanel {
|
|
123
|
-
private _selectedIndex = 0;
|
|
124
|
-
private _scrollOffset = 0;
|
|
125
|
-
private _query = '';
|
|
126
|
-
/** WO-153: converged modal '/' filter — mirrors ScrollableListPanel's _handleFilterKey contract. */
|
|
127
|
-
private _filterActive = false;
|
|
128
|
-
private _cachedEntries: ListEntry[] | null = null;
|
|
129
|
-
private _entriesDirty = true;
|
|
130
|
-
|
|
131
|
-
public constructor(
|
|
132
|
-
private readonly panelManager: PanelManager,
|
|
133
|
-
componentHealthMonitor?: ComponentHealthMonitor,
|
|
134
|
-
) {
|
|
135
|
-
super('panel-list', 'Panel List', 'L', 'session', componentHealthMonitor);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
public override onActivate(): void {
|
|
139
|
-
super.onActivate();
|
|
140
|
-
this._selectedIndex = 0;
|
|
141
|
-
this._scrollOffset = 0;
|
|
142
|
-
this._query = '';
|
|
143
|
-
this._filterActive = false;
|
|
144
|
-
this._entriesDirty = true;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
protected override markDirty(): void {
|
|
148
|
-
super.markDirty();
|
|
149
|
-
this._entriesDirty = true;
|
|
150
|
-
}
|
|
151
|
-
|
|
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 = '';
|
|
163
|
-
this._selectedIndex = 0;
|
|
164
|
-
this._scrollOffset = 0;
|
|
165
|
-
this.markDirty();
|
|
166
|
-
return true;
|
|
167
|
-
}
|
|
168
|
-
if (key === 'return' || key === 'enter') {
|
|
169
|
-
this._filterActive = false; // commit; keep the query applied
|
|
170
|
-
const selectedPanel = this._getSelectedPanelEntry(this._buildEntries());
|
|
171
|
-
if (selectedPanel) {
|
|
172
|
-
try {
|
|
173
|
-
this.panelManager.open(selectedPanel.reg.id);
|
|
174
|
-
} catch (err) {
|
|
175
|
-
logger.warn(`[panel-list] failed to open panel: ${err}`);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
this.markDirty();
|
|
179
|
-
return true;
|
|
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.
|
|
192
|
-
if (isPanelSearchPrintable(key)) {
|
|
193
|
-
this._query += key;
|
|
194
|
-
this._selectedIndex = 0;
|
|
195
|
-
this._scrollOffset = 0;
|
|
196
|
-
this.markDirty();
|
|
197
|
-
return true;
|
|
198
|
-
}
|
|
199
|
-
return false;
|
|
200
|
-
}
|
|
201
|
-
if (key === '/') {
|
|
202
|
-
this._filterActive = true;
|
|
203
|
-
this.markDirty();
|
|
204
|
-
return true;
|
|
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
|
-
/**
|
|
228
|
-
* The `/`-to-filter buffer wants every character of a burst (paste, or a
|
|
229
|
-
* fast-typed query landing in one input.feed() call), same as it always
|
|
230
|
-
* has — see the interface doc on `Panel.isCapturingTextBurst`.
|
|
231
|
-
*/
|
|
232
|
-
public isCapturingTextBurst(): boolean {
|
|
233
|
-
return this._filterActive;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
public handleInput(key: string): boolean {
|
|
237
|
-
const filterResult = this._handleFilterKey(key);
|
|
238
|
-
if (filterResult !== null) return filterResult;
|
|
239
|
-
|
|
240
|
-
const entries = this._buildEntries();
|
|
241
|
-
|
|
242
|
-
// Navigation
|
|
243
|
-
if (key === 'up' || key === 'k') {
|
|
244
|
-
this._movePrevPanel();
|
|
245
|
-
this.markDirty();
|
|
246
|
-
return true;
|
|
247
|
-
}
|
|
248
|
-
if (key === 'down' || key === 'j') {
|
|
249
|
-
this._moveNextPanel(entries);
|
|
250
|
-
this.markDirty();
|
|
251
|
-
return true;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Open selected panel
|
|
255
|
-
if (key === 'return' || key === 'enter') {
|
|
256
|
-
const selectedPanel = this._getSelectedPanelEntry(entries);
|
|
257
|
-
if (selectedPanel) {
|
|
258
|
-
try {
|
|
259
|
-
this.panelManager.open(selectedPanel.reg.id);
|
|
260
|
-
} catch (err) {
|
|
261
|
-
logger.warn(`[panel-list] failed to open panel: ${err}`);
|
|
262
|
-
}
|
|
263
|
-
this.markDirty();
|
|
264
|
-
}
|
|
265
|
-
return true;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (key === 'T' || key === 'B') {
|
|
269
|
-
const selectedPanel = this._getSelectedPanelEntry(entries);
|
|
270
|
-
if (selectedPanel) {
|
|
271
|
-
const pane = key === 'T' ? 'top' : 'bottom';
|
|
272
|
-
try {
|
|
273
|
-
const pm = this.panelManager;
|
|
274
|
-
pm.open(selectedPanel.reg.id, pane);
|
|
275
|
-
pm.show();
|
|
276
|
-
} catch (err) {
|
|
277
|
-
logger.warn(`[panel-list] failed to place panel: ${err}`);
|
|
278
|
-
}
|
|
279
|
-
this.markDirty();
|
|
280
|
-
}
|
|
281
|
-
return true;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (key === 'M') {
|
|
285
|
-
const selectedPanel = this._getSelectedPanelEntry(entries);
|
|
286
|
-
if (selectedPanel) {
|
|
287
|
-
try {
|
|
288
|
-
this.panelManager.moveToOtherPane(selectedPanel.reg.id);
|
|
289
|
-
} catch (err) {
|
|
290
|
-
logger.warn(`[panel-list] failed to move panel: ${err}`);
|
|
291
|
-
}
|
|
292
|
-
this.markDirty();
|
|
293
|
-
}
|
|
294
|
-
return true;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (key === 'S') {
|
|
298
|
-
this.panelManager.toggleBottomPane();
|
|
299
|
-
this.markDirty();
|
|
300
|
-
return true;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (key === 'w') {
|
|
304
|
-
const selectedPanel = this._getSelectedPanelEntry(entries);
|
|
305
|
-
if (selectedPanel) {
|
|
306
|
-
try {
|
|
307
|
-
// Existing public PanelManager.close() API only — panel-manager.ts
|
|
308
|
-
// itself is WO-150's surface this wave.
|
|
309
|
-
this.panelManager.close(selectedPanel.reg.id);
|
|
310
|
-
} catch (err) {
|
|
311
|
-
logger.warn(`[panel-list] failed to close panel: ${err}`);
|
|
312
|
-
}
|
|
313
|
-
this.markDirty();
|
|
314
|
-
}
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (key === 'tab') {
|
|
319
|
-
this.panelManager.togglePaneFocus();
|
|
320
|
-
this.markDirty();
|
|
321
|
-
return true;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return false;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
public render(width: number, height: number): Line[] {
|
|
328
|
-
if (!this.canRenderNow()) {
|
|
329
|
-
return Array.from({ length: height }, () => createEmptyLine(width));
|
|
330
|
-
}
|
|
331
|
-
const start = Date.now();
|
|
332
|
-
this.needsRender = false;
|
|
333
|
-
const intro = 'Browse, place, split, and move panels without dropping into raw panel-management commands.';
|
|
334
|
-
const entries = this._buildEntries();
|
|
335
|
-
|
|
336
|
-
if (entries.length === 0) {
|
|
337
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
338
|
-
title: 'Panel Workspace',
|
|
339
|
-
intro,
|
|
340
|
-
sections: [{
|
|
341
|
-
title: 'Filter',
|
|
342
|
-
lines: [this._buildFilterLine(width)],
|
|
343
|
-
}, {
|
|
344
|
-
lines: buildEmptyState(
|
|
345
|
-
width,
|
|
346
|
-
' No panels match filter.',
|
|
347
|
-
'Clear the filter or search for another panel by id, name, description, or category.',
|
|
348
|
-
// The real remedy for a stuck filter is clearing it in-panel;
|
|
349
|
-
// '/panel list' would just re-activate this same instance with
|
|
350
|
-
// the filter still applied. A committed filter needs '/' to
|
|
351
|
-
// re-enter editing before Esc clears it.
|
|
352
|
-
[{ command: '/ then Esc', summary: 'clear the filter and show every panel' }],
|
|
353
|
-
C,
|
|
354
|
-
),
|
|
355
|
-
}],
|
|
356
|
-
palette: C,
|
|
357
|
-
});
|
|
358
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
359
|
-
if (lines.length > height) lines.length = height;
|
|
360
|
-
this.reportRenderDuration(Date.now() - start);
|
|
361
|
-
return lines;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const panelEntries = entries.filter(e => e.kind === 'panel');
|
|
365
|
-
const pm = this.panelManager;
|
|
366
|
-
const topIds = new Set(pm.getTopPane().panels.map(p => p.id));
|
|
367
|
-
const bottomIds = new Set(pm.getBottomPane().panels.map(p => p.id));
|
|
368
|
-
const focusedPane = pm.getFocusedPane();
|
|
369
|
-
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]])];
|
|
370
|
-
// Optional health rollup — only present when a ComponentHealthMonitor was
|
|
371
|
-
// injected (fills the gap left by WO-004's panel-resources panel removal).
|
|
372
|
-
const allHealth = this.componentHealthMonitor?.getAllHealth() ?? [];
|
|
373
|
-
const unhealthyCount = allHealth.filter((h) => h.healthStatus !== 'healthy').length;
|
|
374
|
-
|
|
375
|
-
const postureLines: Line[] = [
|
|
376
|
-
buildKeyValueLine(width, [
|
|
377
|
-
{ label: 'visible panels', value: String(pm.getAllOpen().length), valueColor: pm.getAllOpen().length > 0 ? C.value : C.dim },
|
|
378
|
-
{ label: 'focused pane', value: focusedPane, valueColor: focusedPane === 'top' ? C.info : C.paneBottom },
|
|
379
|
-
{ label: 'split', value: pm.isBottomPaneVisible() ? 'dual' : 'single', valueColor: pm.isBottomPaneVisible() ? C.info : C.dim },
|
|
380
|
-
{ label: 'results', value: String(panelEntries.length), valueColor: C.value },
|
|
381
|
-
...(this.componentHealthMonitor
|
|
382
|
-
? [{ label: 'flagged', value: String(unhealthyCount), valueColor: unhealthyCount > 0 ? C.warn : C.good }]
|
|
383
|
-
: []),
|
|
384
|
-
], C),
|
|
385
|
-
buildPanelLine(width, [[` Filter owns input only when selected. Open and switch operations should land directly in focused panel state.`, C.header]]),
|
|
386
|
-
];
|
|
387
|
-
const entryLines: Line[] = [this._buildFilterLine(width)];
|
|
388
|
-
const renderedBlocks: Array<{ entry: ListEntry; lines: Line[]; panelFlatIndex?: number }> = [];
|
|
389
|
-
let flatPanelIndex = 0;
|
|
390
|
-
for (const entry of entries) {
|
|
391
|
-
if (entry.kind === 'header') {
|
|
392
|
-
const label = ` ── ${entry.label} ${'─'.repeat(Math.max(0, width - 6 - entry.label.length))}`;
|
|
393
|
-
renderedBlocks.push({
|
|
394
|
-
entry,
|
|
395
|
-
lines: [buildPanelLine(width, [[truncateDisplay(label, width), C.label, C.headerBg]])],
|
|
396
|
-
});
|
|
397
|
-
} else {
|
|
398
|
-
const flatIdx = flatPanelIndex++;
|
|
399
|
-
const isSelected = flatIdx === this._selectedIndex;
|
|
400
|
-
const isTopOpen = topIds.has(entry.reg.id);
|
|
401
|
-
const isBottomOpen = bottomIds.has(entry.reg.id);
|
|
402
|
-
const dot = isTopOpen || isBottomOpen ? '●' : '○';
|
|
403
|
-
const dotColor = isTopOpen || isBottomOpen ? C.good : C.dim;
|
|
404
|
-
const nameColor = C.value;
|
|
405
|
-
const nameStr = fitDisplay(entry.reg.name, NAME_COL_WIDTH);
|
|
406
|
-
const descStartCol = PREFIX_WIDTH + NAME_COL_WIDTH + 1;
|
|
407
|
-
const descWidth = Math.max(1, width - descStartCol);
|
|
408
|
-
const descLines = wrapPanelDescription(entry.reg.description, descWidth, 2);
|
|
409
|
-
const placement = panelPlacementMarker({ isTopOpen, isBottomOpen, focusedPane });
|
|
410
|
-
const health = panelHealthMarker(this.componentHealthMonitor?.getHealth(entry.reg.id));
|
|
411
|
-
// When searching, the flat result list loses its category grouping — so
|
|
412
|
-
// tag each result with a dim [Category] so the origin stays discoverable.
|
|
413
|
-
const categoryTag = this._query
|
|
414
|
-
? `[${CATEGORY_LABELS[entry.reg.category]}] `
|
|
415
|
-
: '';
|
|
416
|
-
const blockLines: Line[] = [
|
|
417
|
-
buildPanelListRow(width, [
|
|
418
|
-
{ text: dot, fg: dotColor },
|
|
419
|
-
{ text: placement.text, fg: placement.color },
|
|
420
|
-
{ text: health.text, fg: health.color },
|
|
421
|
-
{ text: ' ', fg: C.dim },
|
|
422
|
-
{ text: `${nameStr} `, fg: nameColor },
|
|
423
|
-
{ text: categoryTag, fg: C.label },
|
|
424
|
-
{ text: descLines[0] ?? '', fg: C.label },
|
|
425
|
-
], C, { selected: isSelected, selectedBg: C.selectBg, markerColor: C.info }),
|
|
426
|
-
];
|
|
427
|
-
if ((descLines[1] ?? '').length > 0) {
|
|
428
|
-
blockLines.push(buildPanelLine(width, [
|
|
429
|
-
[' '.repeat(PREFIX_WIDTH), C.dim, isSelected ? C.selectBg : C.surfaceBg],
|
|
430
|
-
[' '.repeat(NAME_COL_WIDTH), C.dim, isSelected ? C.selectBg : C.surfaceBg],
|
|
431
|
-
[' ', C.dim, isSelected ? C.selectBg : C.surfaceBg],
|
|
432
|
-
[descLines[1] ?? '', C.label, isSelected ? C.selectBg : C.surfaceBg],
|
|
433
|
-
]));
|
|
434
|
-
}
|
|
435
|
-
renderedBlocks.push({ entry, lines: blockLines, panelFlatIndex: flatIdx });
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
const blockStarts: number[] = [];
|
|
439
|
-
let totalRows = 0;
|
|
440
|
-
for (const block of renderedBlocks) {
|
|
441
|
-
blockStarts.push(totalRows);
|
|
442
|
-
totalRows += block.lines.length;
|
|
443
|
-
}
|
|
444
|
-
const selectedEntryIndex = renderedBlocks.findIndex((block) => block.panelFlatIndex === this._selectedIndex);
|
|
445
|
-
const selectedRow = selectedEntryIndex >= 0 ? blockStarts[selectedEntryIndex] ?? 0 : 0;
|
|
446
|
-
const postureSection: PanelWorkspaceSection = { lines: buildSummaryBlock(width, 'Panel posture', postureLines, C) };
|
|
447
|
-
const resolvedSection = resolvePrimaryScrollableSection(width, height, {
|
|
448
|
-
intro,
|
|
449
|
-
footerLines,
|
|
450
|
-
palette: C,
|
|
451
|
-
beforeSections: [postureSection],
|
|
452
|
-
section: {
|
|
453
|
-
title: 'Panels',
|
|
454
|
-
fixedLines: entryLines,
|
|
455
|
-
scrollableLines: renderedBlocks.flatMap((block) => block.lines),
|
|
456
|
-
selectedIndex: selectedRow,
|
|
457
|
-
scrollOffset: this._scrollOffset,
|
|
458
|
-
guardRows: 1,
|
|
459
|
-
minRows: 1,
|
|
460
|
-
appendWindowSummary: {
|
|
461
|
-
dimColor: C.dim,
|
|
462
|
-
},
|
|
463
|
-
},
|
|
464
|
-
});
|
|
465
|
-
this._scrollOffset = resolvedSection.scrollOffset;
|
|
466
|
-
|
|
467
|
-
const sections: PanelWorkspaceSection[] = [
|
|
468
|
-
postureSection,
|
|
469
|
-
resolvedSection.section,
|
|
470
|
-
];
|
|
471
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
472
|
-
title: 'Panel Workspace',
|
|
473
|
-
intro,
|
|
474
|
-
sections,
|
|
475
|
-
footerLines,
|
|
476
|
-
palette: C,
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
480
|
-
if (lines.length > height) lines.length = height;
|
|
481
|
-
this.reportRenderDuration(Date.now() - start);
|
|
482
|
-
return lines;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// ── Private helpers ───────────────────────────────────────────────────────
|
|
486
|
-
|
|
487
|
-
/** Build flat list of entries (headers + panel rows) filtered by query. */
|
|
488
|
-
private _buildEntries(): ListEntry[] {
|
|
489
|
-
if (this._cachedEntries !== null && !this._entriesDirty) {
|
|
490
|
-
return this._cachedEntries;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
const manager = this.panelManager;
|
|
494
|
-
const byCategory = manager.getTypesByCategory();
|
|
495
|
-
const q = this._query.toLowerCase();
|
|
496
|
-
const entries: ListEntry[] = [];
|
|
497
|
-
|
|
498
|
-
// Recent group (browse mode only): the most-recently-opened panels, newest
|
|
499
|
-
// first, so frequently used panels are one keystroke away.
|
|
500
|
-
if (!q) {
|
|
501
|
-
const byId = new Map(manager.getRegisteredTypes().map((r) => [r.id, r]));
|
|
502
|
-
const recent: PanelRegistration[] = [];
|
|
503
|
-
for (const id of manager.getRecentlyOpened()) {
|
|
504
|
-
const reg = byId.get(id);
|
|
505
|
-
if (reg) recent.push(reg);
|
|
506
|
-
if (recent.length >= RECENT_LIMIT) break;
|
|
507
|
-
}
|
|
508
|
-
if (recent.length > 0) {
|
|
509
|
-
entries.push({ kind: 'header', label: 'Recent' });
|
|
510
|
-
for (const reg of recent) entries.push({ kind: 'panel', reg });
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
for (const cat of CATEGORY_ORDER) {
|
|
515
|
-
const regs = byCategory.get(cat) ?? [];
|
|
516
|
-
const filtered = q
|
|
517
|
-
? regs.filter(r =>
|
|
518
|
-
r.id.toLowerCase().includes(q) ||
|
|
519
|
-
r.name.toLowerCase().includes(q) ||
|
|
520
|
-
r.description.toLowerCase().includes(q) ||
|
|
521
|
-
r.category.toLowerCase().includes(q)
|
|
522
|
-
)
|
|
523
|
-
: regs;
|
|
524
|
-
|
|
525
|
-
if (filtered.length === 0) continue;
|
|
526
|
-
|
|
527
|
-
entries.push({ kind: 'header', label: CATEGORY_LABELS[cat] });
|
|
528
|
-
for (const reg of filtered) {
|
|
529
|
-
entries.push({ kind: 'panel', reg });
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
this._cachedEntries = entries;
|
|
534
|
-
this._entriesDirty = false;
|
|
535
|
-
return entries;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/** Get the ListEntry for the currently selected panel. */
|
|
539
|
-
private _getSelectedPanelEntry(entries: ListEntry[]): Extract<ListEntry, { kind: 'panel' }> | null {
|
|
540
|
-
let idx = 0;
|
|
541
|
-
for (const e of entries) {
|
|
542
|
-
if (e.kind !== 'panel') continue;
|
|
543
|
-
if (idx === this._selectedIndex) return e;
|
|
544
|
-
idx++;
|
|
545
|
-
}
|
|
546
|
-
return null;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
/** Move selection to the previous panel entry. */
|
|
550
|
-
private _movePrevPanel(): void {
|
|
551
|
-
this._selectedIndex = Math.max(0, this._selectedIndex - 1);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
/** Move selection to the next panel entry. */
|
|
555
|
-
private _moveNextPanel(entries: ListEntry[]): void {
|
|
556
|
-
const panelCount = entries.filter(e => e.kind === 'panel').length;
|
|
557
|
-
this._selectedIndex = Math.min(panelCount - 1, this._selectedIndex + 1);
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
/** Clamp selectedIndex within available panels. */
|
|
561
|
-
private _clampSelection(entries: ListEntry[]): void {
|
|
562
|
-
const panelCount = entries.filter(e => e.kind === 'panel').length;
|
|
563
|
-
this._selectedIndex = Math.max(0, Math.min(this._selectedIndex, panelCount - 1));
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
}
|