@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
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
-
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
|
-
import type { UiReadModel, UiWatchersSnapshot } from '../runtime/ui-read-models.ts';
|
|
5
|
-
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
6
|
-
import {
|
|
7
|
-
buildEmptyState,
|
|
8
|
-
buildGuidanceLine,
|
|
9
|
-
buildKeyValueLine,
|
|
10
|
-
buildPanelLine,
|
|
11
|
-
buildPanelWorkspace,
|
|
12
|
-
DEFAULT_PANEL_PALETTE,
|
|
13
|
-
type PanelPalette,
|
|
14
|
-
} from './polish.ts';
|
|
15
|
-
|
|
16
|
-
const C = {
|
|
17
|
-
...DEFAULT_PANEL_PALETTE,
|
|
18
|
-
header: '#94a3b8',
|
|
19
|
-
headerBg: '#1e293b',
|
|
20
|
-
ok: '#22c55e',
|
|
21
|
-
warn: '#eab308',
|
|
22
|
-
error: '#ef4444',
|
|
23
|
-
info: '#38bdf8',
|
|
24
|
-
selectBg: '#0f172a',
|
|
25
|
-
} as const;
|
|
26
|
-
|
|
27
|
-
function stateColor(state: string): string {
|
|
28
|
-
if (state === 'running') return C.ok;
|
|
29
|
-
if (state === 'degraded') return C.warn;
|
|
30
|
-
if (state === 'failed') return C.error;
|
|
31
|
-
return C.dim;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function sourceStatusColor(state?: string): string {
|
|
35
|
-
if (state === 'healthy') return C.ok;
|
|
36
|
-
if (state === 'lagging' || state === 'stale' || state === 'degraded') return C.warn;
|
|
37
|
-
if (state === 'failed') return C.error;
|
|
38
|
-
return C.dim;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function formatLag(value?: number): string {
|
|
42
|
-
if (!value || value <= 0) return 'n/a';
|
|
43
|
-
if (value < 1000) return `${value}ms`;
|
|
44
|
-
if (value < 60_000) return `${Math.round(value / 1000)}s`;
|
|
45
|
-
return `${Math.round(value / 60_000)}m`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function formatTime(value?: number): string {
|
|
49
|
-
if (!value) return 'n/a';
|
|
50
|
-
return new Date(value).toLocaleString();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
type WatcherEntry = UiWatchersSnapshot['watchers'][number];
|
|
54
|
-
|
|
55
|
-
export class WatchersPanel extends ScrollableListPanel<WatcherEntry> {
|
|
56
|
-
private readonly readModel?: UiReadModel<UiWatchersSnapshot>;
|
|
57
|
-
private readonly unsub: (() => void) | null;
|
|
58
|
-
|
|
59
|
-
public constructor(readModel?: UiReadModel<UiWatchersSnapshot>) {
|
|
60
|
-
super('watchers', 'Watchers', 'W', 'monitoring');
|
|
61
|
-
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
62
|
-
this.readModel = readModel;
|
|
63
|
-
this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
public override onDestroy(): void {
|
|
67
|
-
this.unsub?.();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
protected override getPalette(): PanelPalette {
|
|
71
|
-
return C;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
protected getItems(): readonly WatcherEntry[] {
|
|
75
|
-
if (!this.readModel) return [];
|
|
76
|
-
return this.readModel.getSnapshot().watchers;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
protected renderItem(watcher: WatcherEntry, _index: number, selected: boolean, width: number): Line {
|
|
80
|
-
const bg = selected ? C.selectBg : undefined;
|
|
81
|
-
return buildPanelLine(width, [
|
|
82
|
-
[' ', C.label, bg],
|
|
83
|
-
[watcher.state.padEnd(10), stateColor(watcher.state), bg],
|
|
84
|
-
[` ${truncateDisplay(watcher.label, 18).padEnd(18)}`, C.value, bg],
|
|
85
|
-
[` ${String(watcher.sourceStatus ?? 'unknown').padEnd(10)}`, sourceStatusColor(watcher.sourceStatus), bg],
|
|
86
|
-
[` ${truncateDisplay(formatLag(watcher.sourceLagMs), Math.max(0, width - 43))}`, C.dim, bg],
|
|
87
|
-
]);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
protected override getEmptyStateMessage(): string {
|
|
91
|
-
return ' No watchers registered.';
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
|
|
95
|
-
return [
|
|
96
|
-
{ command: '/schedule list', summary: 'review automation that will consume watcher events' },
|
|
97
|
-
{ command: '/services auth-review', summary: 'validate integration credentials before enabling remote watchers' },
|
|
98
|
-
];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
public render(width: number, height: number): Line[] {
|
|
102
|
-
const intro = 'Managed watchers and source health used to trigger automation, refresh routes, and surface degraded upstream conditions.';
|
|
103
|
-
|
|
104
|
-
if (!this.readModel) {
|
|
105
|
-
const workspace = buildPanelWorkspace(width, height, {
|
|
106
|
-
title: 'Watchers',
|
|
107
|
-
intro,
|
|
108
|
-
sections: [{
|
|
109
|
-
lines: buildEmptyState(
|
|
110
|
-
width,
|
|
111
|
-
' Runtime store not wired.',
|
|
112
|
-
'This panel needs the shared runtime store to inspect watcher health and source lag.',
|
|
113
|
-
[{ command: '/services auth-review', summary: 'inspect supporting services until watcher wiring is available' }],
|
|
114
|
-
C,
|
|
115
|
-
),
|
|
116
|
-
}],
|
|
117
|
-
palette: C,
|
|
118
|
-
});
|
|
119
|
-
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
120
|
-
return workspace;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const snapshot = this.readModel.getSnapshot();
|
|
124
|
-
const watchers = this.getItems();
|
|
125
|
-
|
|
126
|
-
const headerLines: Line[] = [
|
|
127
|
-
buildKeyValueLine(width, [
|
|
128
|
-
{ label: 'watchers', value: String(snapshot.totalWatchers), valueColor: snapshot.totalWatchers > 0 ? C.info : C.dim },
|
|
129
|
-
{ label: 'active', value: String(snapshot.activeWatcherIds.length), valueColor: snapshot.activeWatcherIds.length > 0 ? C.ok : C.dim },
|
|
130
|
-
{ label: 'degraded', value: String(snapshot.totalDegraded), valueColor: snapshot.totalDegraded > 0 ? C.warn : C.dim },
|
|
131
|
-
{ label: 'lagged', value: String(snapshot.totalLagged), valueColor: snapshot.totalLagged > 0 ? C.warn : C.dim },
|
|
132
|
-
], C),
|
|
133
|
-
buildGuidanceLine(width, '/schedule list', 'verify jobs consuming these sources and use daemon APIs for watcher lifecycle control', C),
|
|
134
|
-
];
|
|
135
|
-
|
|
136
|
-
if (watchers.length === 0) {
|
|
137
|
-
return this.renderList(width, height, {
|
|
138
|
-
title: 'Watchers',
|
|
139
|
-
header: headerLines,
|
|
140
|
-
emptyMessage: ' No watchers registered.',
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
this.clampSelection();
|
|
145
|
-
const selected = watchers[this.selectedIndex]!;
|
|
146
|
-
|
|
147
|
-
const footerLines: Line[] = [
|
|
148
|
-
buildPanelLine(width, [
|
|
149
|
-
[' Watcher: ', C.label],
|
|
150
|
-
[selected.label, C.value],
|
|
151
|
-
[' Kind: ', C.label],
|
|
152
|
-
[selected.kind, C.info],
|
|
153
|
-
]),
|
|
154
|
-
buildPanelLine(width, [
|
|
155
|
-
[' State: ', C.label],
|
|
156
|
-
[selected.state, stateColor(selected.state)],
|
|
157
|
-
[' Source: ', C.label],
|
|
158
|
-
[selected.source.kind, C.value],
|
|
159
|
-
]),
|
|
160
|
-
buildPanelLine(width, [
|
|
161
|
-
[' Source status: ', C.label],
|
|
162
|
-
[selected.sourceStatus ?? 'unknown', sourceStatusColor(selected.sourceStatus)],
|
|
163
|
-
[' Lag: ', C.label],
|
|
164
|
-
[formatLag(selected.sourceLagMs), selected.sourceLagMs ? C.warn : C.dim],
|
|
165
|
-
]),
|
|
166
|
-
buildPanelLine(width, [
|
|
167
|
-
[' Heartbeat: ', C.label],
|
|
168
|
-
[formatTime(selected.lastHeartbeatAt), C.dim],
|
|
169
|
-
[' Checkpoint: ', C.label],
|
|
170
|
-
[truncateDisplay(selected.lastCheckpoint ?? 'n/a', Math.max(0, width - 38)), C.dim],
|
|
171
|
-
]),
|
|
172
|
-
];
|
|
173
|
-
if (selected.degradedReason) {
|
|
174
|
-
footerLines.push(buildPanelLine(width, [
|
|
175
|
-
[' Reason: ', C.label],
|
|
176
|
-
[truncateDisplay(selected.degradedReason, Math.max(0, width - 11)), C.warn],
|
|
177
|
-
]));
|
|
178
|
-
}
|
|
179
|
-
if (selected.lastError) {
|
|
180
|
-
footerLines.push(buildPanelLine(width, [
|
|
181
|
-
[' Error: ', C.label],
|
|
182
|
-
[truncateDisplay(selected.lastError, Math.max(0, width - 10)), C.error],
|
|
183
|
-
]));
|
|
184
|
-
}
|
|
185
|
-
footerLines.push(buildPanelLine(width, [[' Up/Down move through watchers', C.dim]]));
|
|
186
|
-
|
|
187
|
-
return this.renderList(width, height, {
|
|
188
|
-
title: 'Watchers',
|
|
189
|
-
header: headerLines,
|
|
190
|
-
footer: footerLines,
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
}
|
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
import { type Line } from '../types/grid.ts';
|
|
2
|
-
import { fitDisplay, getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
|
-
import type { PanelPicker } from '../panels/panel-picker.ts';
|
|
4
|
-
import { CATEGORY_LABELS, CATEGORY_ORDER } from '../panels/panel-picker.ts';
|
|
5
|
-
import type { PanelCategory, PanelRegistration } from '../panels/types.ts';
|
|
6
|
-
import {
|
|
7
|
-
createOverlayBorderLine,
|
|
8
|
-
createOverlayBoxLayout,
|
|
9
|
-
createOverlayContentLine,
|
|
10
|
-
DEFAULT_OVERLAY_PALETTE,
|
|
11
|
-
putOverlayText,
|
|
12
|
-
} from './overlay-box.ts';
|
|
13
|
-
import { getOverlaySurfaceMetrics } from './overlay-viewport.ts';
|
|
14
|
-
|
|
15
|
-
const TITLE_FG = '#cbd5e1';
|
|
16
|
-
const CATEGORY_FG = '#94a3b8';
|
|
17
|
-
const SELECTED_FG = '#e2e8f0';
|
|
18
|
-
const SELECTED_BG = '#1e293b';
|
|
19
|
-
const BODY_FG = '252';
|
|
20
|
-
const BORDER_FG = DEFAULT_OVERLAY_PALETTE.borderFg;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Render the panel picker modal as Line[] for overlay in the viewport.
|
|
24
|
-
* Panels are grouped by category. Category headers are inserted between groups.
|
|
25
|
-
* When a search query is active a search-bar row is shown beneath the title.
|
|
26
|
-
*/
|
|
27
|
-
export function renderPanelPickerOverlay(
|
|
28
|
-
picker: PanelPicker,
|
|
29
|
-
width: number,
|
|
30
|
-
viewportHeight = 24,
|
|
31
|
-
): Line[] {
|
|
32
|
-
if (!picker.active) return [];
|
|
33
|
-
|
|
34
|
-
const lines: Line[] = [];
|
|
35
|
-
const metrics = getOverlaySurfaceMetrics(width, viewportHeight, {
|
|
36
|
-
margin: 4,
|
|
37
|
-
maxWidth: 72,
|
|
38
|
-
chromeRows: picker.searchQuery.length > 0 ? 7 : 6,
|
|
39
|
-
minContentRows: 6,
|
|
40
|
-
maxContentRows: 10,
|
|
41
|
-
});
|
|
42
|
-
const layout = createOverlayBoxLayout(width, metrics.margin, metrics.boxWidth);
|
|
43
|
-
const contentW = layout.innerWidth;
|
|
44
|
-
const titleFg = TITLE_FG;
|
|
45
|
-
const borderFg = BORDER_FG;
|
|
46
|
-
|
|
47
|
-
// ── Title bar ──────────────────────────────────────────────────────────────
|
|
48
|
-
const titleLine = createOverlayBorderLine(width, layout, '┌', '─', '┐', borderFg);
|
|
49
|
-
putOverlayText(titleLine, layout.margin + 2, layout.width - 4, 'Open Panel Workspace', { fg: titleFg, bold: true });
|
|
50
|
-
lines.push(titleLine);
|
|
51
|
-
|
|
52
|
-
// ── Search bar (shown when query is non-empty) ──────────────────────────────
|
|
53
|
-
if (picker.searchQuery.length > 0) {
|
|
54
|
-
const searchLine = createOverlayContentLine(width, layout, borderFg);
|
|
55
|
-
const searchLabel = '\u2315 ';
|
|
56
|
-
const queryAvail = contentW - getDisplayWidth(searchLabel);
|
|
57
|
-
const queryText = fitDisplay(picker.searchQuery, queryAvail);
|
|
58
|
-
putOverlayText(searchLine, layout.margin + 2, getDisplayWidth(searchLabel), searchLabel, { fg: titleFg });
|
|
59
|
-
putOverlayText(searchLine, layout.margin + 2 + getDisplayWidth(searchLabel), queryAvail, queryText, { fg: titleFg });
|
|
60
|
-
lines.push(searchLine);
|
|
61
|
-
|
|
62
|
-
lines.push(createOverlayBorderLine(width, layout, '├', '─', '┤', borderFg));
|
|
63
|
-
} else {
|
|
64
|
-
const placeholder = ' Browse by category or start typing to filter by panel name, purpose, or category.';
|
|
65
|
-
const row = createOverlayContentLine(width, layout, borderFg);
|
|
66
|
-
putOverlayText(row, layout.margin + 2, contentW, fitDisplay(placeholder, contentW), { fg: '244', dim: true });
|
|
67
|
-
lines.push(row);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const visible = picker.getVisible();
|
|
71
|
-
|
|
72
|
-
if (visible.length === 0) {
|
|
73
|
-
const noResults = 'No panels match your search';
|
|
74
|
-
const noRow = createOverlayContentLine(width, layout, borderFg);
|
|
75
|
-
putOverlayText(noRow, layout.margin + 2, contentW, fitDisplay(noResults, contentW), { fg: '244', dim: true });
|
|
76
|
-
lines.push(noRow);
|
|
77
|
-
} else {
|
|
78
|
-
// Build a flat render list of { type: 'header' | 'item', ... } entries
|
|
79
|
-
// so we can compute the correct scroll window over all rows.
|
|
80
|
-
type HeaderEntry = { type: 'header'; category: PanelCategory };
|
|
81
|
-
type ItemEntry = { type: 'item'; reg: PanelRegistration; flatIndex: number };
|
|
82
|
-
type RenderEntry = HeaderEntry | ItemEntry;
|
|
83
|
-
|
|
84
|
-
const renderEntries: RenderEntry[] = [];
|
|
85
|
-
|
|
86
|
-
// When no search query, group by category; when searching, show flat list
|
|
87
|
-
// with a single "Results" header.
|
|
88
|
-
if (picker.searchQuery.length === 0) {
|
|
89
|
-
const byCategory = new Map<PanelCategory, PanelRegistration[]>();
|
|
90
|
-
for (const reg of visible) {
|
|
91
|
-
const group = byCategory.get(reg.category) ?? [];
|
|
92
|
-
group.push(reg);
|
|
93
|
-
byCategory.set(reg.category, group);
|
|
94
|
-
}
|
|
95
|
-
let flatIndex = 0;
|
|
96
|
-
for (const cat of CATEGORY_ORDER) {
|
|
97
|
-
const group = byCategory.get(cat);
|
|
98
|
-
if (!group || group.length === 0) continue;
|
|
99
|
-
renderEntries.push({ type: 'header', category: cat });
|
|
100
|
-
for (const reg of group) {
|
|
101
|
-
renderEntries.push({ type: 'item', reg, flatIndex });
|
|
102
|
-
flatIndex++;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
} else {
|
|
106
|
-
for (let i = 0; i < visible.length; i++) {
|
|
107
|
-
renderEntries.push({ type: 'item', reg: visible[i], flatIndex: i });
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Compute scroll window: ensure the selected item row stays visible.
|
|
112
|
-
// Find the render-entry index of the selected item.
|
|
113
|
-
const selectedEntryIdx = renderEntries.findIndex(
|
|
114
|
-
e => e.type === 'item' && e.flatIndex === picker.selectedIndex,
|
|
115
|
-
);
|
|
116
|
-
const total = renderEntries.length;
|
|
117
|
-
const maxVisible = metrics.contentRows;
|
|
118
|
-
let startEntry = 0;
|
|
119
|
-
if (total > maxVisible && selectedEntryIdx >= 0) {
|
|
120
|
-
startEntry = Math.max(
|
|
121
|
-
0,
|
|
122
|
-
Math.min(
|
|
123
|
-
selectedEntryIdx - Math.floor(maxVisible / 2),
|
|
124
|
-
total - maxVisible,
|
|
125
|
-
),
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
const endEntry = Math.min(startEntry + maxVisible, total);
|
|
129
|
-
|
|
130
|
-
for (let i = startEntry; i < endEntry; i++) {
|
|
131
|
-
const entry = renderEntries[i];
|
|
132
|
-
|
|
133
|
-
if (entry.type === 'header') {
|
|
134
|
-
// Category header row
|
|
135
|
-
const label = CATEGORY_LABELS[entry.category].toUpperCase();
|
|
136
|
-
const headerRow = createOverlayContentLine(width, layout, borderFg);
|
|
137
|
-
putOverlayText(headerRow, layout.margin + 2, contentW, fitDisplay(` ${label}`, contentW), { fg: CATEGORY_FG, dim: true });
|
|
138
|
-
lines.push(headerRow);
|
|
139
|
-
} else {
|
|
140
|
-
const { reg, flatIndex } = entry;
|
|
141
|
-
const isSelected = flatIndex === picker.selectedIndex;
|
|
142
|
-
const indicator = isSelected ? '\u25b6 ' : ' ';
|
|
143
|
-
|
|
144
|
-
// icon (1 char) + 2 spaces + name
|
|
145
|
-
const iconStr = reg.icon + ' ';
|
|
146
|
-
const iconW = getDisplayWidth(iconStr);
|
|
147
|
-
|
|
148
|
-
// separator between name and description: ' \u2014 '
|
|
149
|
-
const sep = ' \u2014 ';
|
|
150
|
-
const sepW = getDisplayWidth(sep);
|
|
151
|
-
|
|
152
|
-
// Allocate space: indicator(2) + icon(iconW) + name + sep + desc fills contentW
|
|
153
|
-
const nameMaxW = Math.max(8, Math.floor((contentW - 2 - iconW - sepW) * 0.35));
|
|
154
|
-
const descMaxW = contentW - 2 - iconW - nameMaxW - sepW;
|
|
155
|
-
|
|
156
|
-
const nameRaw = reg.name;
|
|
157
|
-
const nameStr = fitDisplay(nameRaw, nameMaxW);
|
|
158
|
-
|
|
159
|
-
const descRaw = reg.description;
|
|
160
|
-
const descStr = fitDisplay(descRaw, descMaxW);
|
|
161
|
-
|
|
162
|
-
const row = createOverlayContentLine(width, layout, borderFg, isSelected ? SELECTED_BG : '');
|
|
163
|
-
const rowText = indicator + iconStr + nameStr + sep + descStr;
|
|
164
|
-
putOverlayText(row, layout.margin + 2, contentW, fitDisplay(truncateDisplay(rowText, contentW), contentW), {
|
|
165
|
-
fg: isSelected ? SELECTED_FG : BODY_FG,
|
|
166
|
-
bg: isSelected ? SELECTED_BG : '',
|
|
167
|
-
bold: isSelected,
|
|
168
|
-
});
|
|
169
|
-
lines.push(row);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const selected = picker.getSelected();
|
|
174
|
-
if (selected) {
|
|
175
|
-
lines.push(createOverlayBorderLine(width, layout, '├', '─', '┤', borderFg));
|
|
176
|
-
const categoryLabel = CATEGORY_LABELS[selected.category].toUpperCase();
|
|
177
|
-
const selectedLine = createOverlayContentLine(width, layout, borderFg);
|
|
178
|
-
putOverlayText(selectedLine, layout.margin + 2, contentW, fitDisplay(`${selected.icon} ${selected.name} [${categoryLabel}]`, contentW), { fg: SELECTED_FG });
|
|
179
|
-
lines.push(selectedLine);
|
|
180
|
-
const desc = fitDisplay(truncateDisplay(selected.description, contentW), contentW);
|
|
181
|
-
const descRow = createOverlayContentLine(width, layout, borderFg);
|
|
182
|
-
putOverlayText(descRow, layout.margin + 2, contentW, desc, { fg: '244', dim: true });
|
|
183
|
-
lines.push(descRow);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// ── Scroll indicator ───────────────────────────────────────────────────
|
|
187
|
-
if (total > maxVisible) {
|
|
188
|
-
const scrollInfo = `${picker.selectedIndex + 1}/${visible.length}`;
|
|
189
|
-
const scrollRow = createOverlayContentLine(width, layout, borderFg);
|
|
190
|
-
putOverlayText(scrollRow, layout.margin + 2 + Math.max(0, contentW - getDisplayWidth(scrollInfo)), getDisplayWidth(scrollInfo), scrollInfo, { fg: '240', dim: true });
|
|
191
|
-
lines.push(scrollRow);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ── Bottom border with hints ───────────────────────────────────────────────
|
|
196
|
-
const hints = '[Up/Down] Navigate [Enter] Open [/] Filter [Esc] Cancel';
|
|
197
|
-
const bottomLine = createOverlayBorderLine(width, layout, '└', '─', '┘', borderFg);
|
|
198
|
-
putOverlayText(bottomLine, layout.margin + 2, layout.width - 4, truncateDisplay(hints, layout.width - 4), { fg: '240', dim: true });
|
|
199
|
-
lines.push(bottomLine);
|
|
200
|
-
|
|
201
|
-
return lines;
|
|
202
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { type Line } from '../types/grid.ts';
|
|
2
|
-
import type { Panel } from '../panels/types.ts';
|
|
3
|
-
import { renderTabStrip } from './tab-strip.ts';
|
|
4
|
-
|
|
5
|
-
const ACTIVE_FG = '#e2e8f0';
|
|
6
|
-
const ACTIVE_FG_UNFOCUSED = '#94a3b8';
|
|
7
|
-
const ACTIVE_BG = '#1e293b';
|
|
8
|
-
const INACTIVE_FG = '244';
|
|
9
|
-
const SEPARATOR_FG = '238';
|
|
10
|
-
const CLOSE_FG = '238';
|
|
11
|
-
const LABEL_FG = '#cbd5e1';
|
|
12
|
-
const LABEL_BG = '#0f172a';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Render the panel tab bar.
|
|
16
|
-
*
|
|
17
|
-
* Shows open panel tabs with the active one highlighted.
|
|
18
|
-
* Format: │ icon Name │ icon Name │ ...
|
|
19
|
-
*
|
|
20
|
-
* Active tab: slate-white, bold, bg #1e293b
|
|
21
|
-
* Inactive tab: grey (244), no background
|
|
22
|
-
* Separators: │ in dim grey (238)
|
|
23
|
-
* Overflow: > right scroll indicator (stateless - shows when tabs extend beyond width)
|
|
24
|
-
* Close button: x at far right for active tab
|
|
25
|
-
*/
|
|
26
|
-
export function renderPanelTabBar(
|
|
27
|
-
panels: Panel[],
|
|
28
|
-
activeIndex: number,
|
|
29
|
-
width: number,
|
|
30
|
-
focused: boolean = true,
|
|
31
|
-
paneLabel?: string,
|
|
32
|
-
): Line {
|
|
33
|
-
if (panels.length === 0) return renderTabStrip({ width, tabs: [], style: {
|
|
34
|
-
activeFg: ACTIVE_FG,
|
|
35
|
-
activeBg: ACTIVE_BG,
|
|
36
|
-
activeBold: focused,
|
|
37
|
-
inactiveFg: INACTIVE_FG,
|
|
38
|
-
separatorFg: SEPARATOR_FG,
|
|
39
|
-
labelFg: LABEL_FG,
|
|
40
|
-
labelBg: focused ? LABEL_BG : '',
|
|
41
|
-
labelBold: focused,
|
|
42
|
-
overflowFg: SEPARATOR_FG,
|
|
43
|
-
trailingFg: focused ? ACTIVE_FG_UNFOCUSED : INACTIVE_FG,
|
|
44
|
-
} });
|
|
45
|
-
|
|
46
|
-
const tabs = panels.map((panel, index) => ({
|
|
47
|
-
label: `${panel.icon} ${panel.name}`,
|
|
48
|
-
active: index === activeIndex,
|
|
49
|
-
}));
|
|
50
|
-
|
|
51
|
-
return renderTabStrip({
|
|
52
|
-
width,
|
|
53
|
-
tabs,
|
|
54
|
-
prefixLabel: paneLabel ? ` ${paneLabel.toUpperCase()} ` : undefined,
|
|
55
|
-
suffixLabel: ` ${panels.length} x `,
|
|
56
|
-
style: {
|
|
57
|
-
activeFg: focused ? ACTIVE_FG : ACTIVE_FG_UNFOCUSED,
|
|
58
|
-
activeBg: ACTIVE_BG,
|
|
59
|
-
activeBold: focused,
|
|
60
|
-
inactiveFg: INACTIVE_FG,
|
|
61
|
-
separatorFg: SEPARATOR_FG,
|
|
62
|
-
labelFg: LABEL_FG,
|
|
63
|
-
labelBg: focused ? LABEL_BG : '',
|
|
64
|
-
labelBold: focused,
|
|
65
|
-
overflowFg: SEPARATOR_FG,
|
|
66
|
-
trailingFg: CLOSE_FG,
|
|
67
|
-
},
|
|
68
|
-
});
|
|
69
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Panel resource diagnostics panel data provider.
|
|
3
|
-
*
|
|
4
|
-
* Polls the shared TUI-owned ComponentHealthMonitor and produces PanelResourceSnapshot
|
|
5
|
-
* values for the diagnostics panel to render.
|
|
6
|
-
*/
|
|
7
|
-
import type { ComponentHealthMonitor } from '../../perf/panel-health-monitor.ts';
|
|
8
|
-
import type {
|
|
9
|
-
ComponentResourceEntry,
|
|
10
|
-
ComponentResourceSnapshot,
|
|
11
|
-
} from '@/runtime/index.ts';
|
|
12
|
-
|
|
13
|
-
const DEFAULT_POLL_INTERVAL_MS = 500;
|
|
14
|
-
|
|
15
|
-
const HEALTH_ORDER: Record<string, number> = {
|
|
16
|
-
overloaded: 0,
|
|
17
|
-
warning: 1,
|
|
18
|
-
healthy: 2,
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export class PanelResourcesPanel {
|
|
22
|
-
private readonly _pollIntervalMs: number;
|
|
23
|
-
private readonly _monitor: ComponentHealthMonitor;
|
|
24
|
-
private _current: ComponentResourceSnapshot;
|
|
25
|
-
private _timerId: ReturnType<typeof setInterval> | null = null;
|
|
26
|
-
private readonly _subscribers = new Set<() => void>();
|
|
27
|
-
|
|
28
|
-
constructor(monitor: ComponentHealthMonitor, pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS) {
|
|
29
|
-
this._monitor = monitor;
|
|
30
|
-
this._pollIntervalMs = pollIntervalMs;
|
|
31
|
-
this._current = this._buildSnapshot(Date.now());
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
start(): void {
|
|
35
|
-
if (this._timerId !== null) return;
|
|
36
|
-
this._timerId = setInterval(() => {
|
|
37
|
-
this._current = this._buildSnapshot(Date.now());
|
|
38
|
-
this._notify();
|
|
39
|
-
}, this._pollIntervalMs);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
stop(): void {
|
|
43
|
-
if (this._timerId !== null) {
|
|
44
|
-
clearInterval(this._timerId);
|
|
45
|
-
this._timerId = null;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
getSnapshot(): ComponentResourceSnapshot {
|
|
50
|
-
return this._current;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
refresh(now: number = Date.now()): ComponentResourceSnapshot {
|
|
54
|
-
this._current = this._buildSnapshot(now);
|
|
55
|
-
return this._current;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
subscribe(callback: () => void): () => void {
|
|
59
|
-
this._subscribers.add(callback);
|
|
60
|
-
return () => this._subscribers.delete(callback);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
dispose(): void {
|
|
64
|
-
this.stop();
|
|
65
|
-
this._subscribers.clear();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
private _buildSnapshot(capturedAt: number): ComponentResourceSnapshot {
|
|
69
|
-
const healthStates = this._monitor.getAllHealth();
|
|
70
|
-
|
|
71
|
-
const panels: ComponentResourceEntry[] = healthStates.map((health) => {
|
|
72
|
-
const contract = this._monitor.getContract(health.componentId);
|
|
73
|
-
return {
|
|
74
|
-
componentId: health.componentId,
|
|
75
|
-
throttleStatus: health.throttleStatus,
|
|
76
|
-
healthStatus: health.healthStatus,
|
|
77
|
-
renderP95Ms: health.renderP95Ms,
|
|
78
|
-
maxRenderMs: contract?.maxRenderMs ?? 0,
|
|
79
|
-
rendersInWindow: health.rendersInWindow,
|
|
80
|
-
maxUpdatesPerSecond: contract?.maxUpdatesPerSecond ?? 0,
|
|
81
|
-
consecutiveViolations: health.consecutiveViolations,
|
|
82
|
-
totalSuppressed: health.totalSuppressed,
|
|
83
|
-
totalPermitted: health.totalPermitted,
|
|
84
|
-
lastRenderAt: health.lastRenderAt,
|
|
85
|
-
nextAllowedAt: health.nextAllowedAt,
|
|
86
|
-
};
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
panels.sort((left, right) => {
|
|
90
|
-
const diff = (HEALTH_ORDER[left.healthStatus] ?? 2) - (HEALTH_ORDER[right.healthStatus] ?? 2);
|
|
91
|
-
return diff !== 0 ? diff : left.componentId.localeCompare(right.componentId);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
const overloadedCount = panels.filter((panel) => panel.healthStatus === 'overloaded').length;
|
|
95
|
-
const warningCount = panels.filter((panel) => panel.healthStatus === 'warning').length;
|
|
96
|
-
const healthyCount = panels.filter((panel) => panel.healthStatus === 'healthy').length;
|
|
97
|
-
const totalSuppressed = panels.reduce((sum, panel) => sum + panel.totalSuppressed, 0);
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
panels,
|
|
101
|
-
overloadedCount,
|
|
102
|
-
warningCount,
|
|
103
|
-
healthyCount,
|
|
104
|
-
totalSuppressed,
|
|
105
|
-
capturedAt,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private _notify(): void {
|
|
110
|
-
for (const callback of this._subscribers) {
|
|
111
|
-
try {
|
|
112
|
-
callback();
|
|
113
|
-
} catch {
|
|
114
|
-
// Subscriber failures must not take down diagnostics polling.
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|