@pellux/goodvibes-tui 0.27.0 → 0.28.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 +23 -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/eval.ts +4 -3
- package/src/input/handler-feed-routes.ts +61 -4
- package/src/input/handler-feed.ts +13 -0
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/keybindings.ts +5 -0
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +10 -0
- package/src/main.ts +0 -1
- package/src/panels/agent-inspector-panel.ts +18 -13
- package/src/panels/agent-inspector-shared.ts +3 -3
- package/src/panels/agent-logs-panel.ts +22 -53
- package/src/panels/approval-panel.ts +146 -95
- package/src/panels/automation-control-panel.ts +24 -1
- package/src/panels/base-panel.ts +26 -2
- package/src/panels/builtin/development.ts +1 -1
- package/src/panels/cockpit-panel.ts +24 -5
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +108 -43
- package/src/panels/context-visualizer-panel.ts +25 -1
- package/src/panels/control-plane-panel.ts +27 -1
- package/src/panels/cost-tracker-panel.ts +82 -54
- package/src/panels/debug-panel.ts +51 -16
- package/src/panels/diff-panel.ts +59 -30
- package/src/panels/docs-panel.ts +19 -3
- package/src/panels/eval-panel.ts +130 -81
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +40 -41
- package/src/panels/file-preview-panel.ts +9 -2
- package/src/panels/forensics-panel.ts +27 -13
- package/src/panels/git-panel.ts +58 -70
- package/src/panels/hooks-panel.ts +36 -2
- package/src/panels/incident-review-panel.ts +31 -10
- package/src/panels/intelligence-panel.ts +152 -71
- package/src/panels/knowledge-graph-panel.ts +89 -27
- package/src/panels/local-auth-panel.ts +17 -11
- package/src/panels/marketplace-panel.ts +31 -12
- package/src/panels/memory-panel.ts +5 -4
- package/src/panels/ops-control-panel.ts +69 -7
- package/src/panels/ops-strategy-panel.ts +61 -43
- package/src/panels/orchestration-panel.ts +34 -4
- package/src/panels/panel-list-panel.ts +33 -8
- package/src/panels/panel-manager.ts +23 -4
- package/src/panels/plan-dashboard-panel.ts +183 -66
- package/src/panels/plugins-panel.ts +48 -10
- package/src/panels/policy-panel.ts +60 -23
- package/src/panels/polish-core.ts +157 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +44 -145
- package/src/panels/project-planning-panel.ts +22 -0
- package/src/panels/provider-accounts-panel.ts +55 -18
- package/src/panels/provider-health-panel.ts +65 -23
- package/src/panels/provider-stats-panel.ts +47 -22
- package/src/panels/qr-panel.ts +23 -11
- package/src/panels/remote-panel.ts +12 -5
- package/src/panels/routes-panel.ts +27 -5
- package/src/panels/sandbox-panel.ts +62 -27
- package/src/panels/schedule-panel.ts +40 -17
- package/src/panels/scrollable-list-panel.ts +124 -10
- package/src/panels/security-panel.ts +72 -20
- package/src/panels/services-panel.ts +68 -22
- package/src/panels/session-browser-panel.ts +29 -5
- package/src/panels/session-maintenance.ts +2 -2
- package/src/panels/settings-sync-panel.ts +30 -15
- package/src/panels/subscription-panel.ts +21 -3
- package/src/panels/symbol-outline-panel.ts +40 -47
- package/src/panels/system-messages-panel.ts +60 -27
- package/src/panels/tasks-panel.ts +34 -7
- package/src/panels/thinking-panel.ts +26 -25
- package/src/panels/token-budget-panel.ts +49 -38
- package/src/panels/tool-inspector-panel.ts +27 -21
- package/src/panels/types.ts +25 -0
- package/src/panels/watchers-panel.ts +25 -5
- package/src/panels/work-plan-panel.ts +125 -34
- package/src/panels/worktree-panel.ts +65 -20
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +31 -127
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/model-picker-overlay.ts +9 -4
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/progress.ts +3 -2
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/ui-factory.ts +4 -6
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/version.ts +1 -1
- package/src/panels/panel-picker.ts +0 -106
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
DEFAULT_PANEL_PALETTE,
|
|
17
17
|
type PanelWorkspaceSection,
|
|
18
18
|
} from './polish.ts';
|
|
19
|
-
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
19
|
+
import { truncateDisplay, wrapText } from '../utils/terminal-width.ts';
|
|
20
20
|
|
|
21
21
|
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
22
22
|
// Domain-specific reasoning tones (purples) and the active-stream cyan have no
|
|
@@ -34,23 +34,6 @@ interface ReasoningBlock {
|
|
|
34
34
|
collapsed: boolean;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function wrapLines(text: string, width: number): string[] {
|
|
38
|
-
const result: string[] = [];
|
|
39
|
-
const raw = text.split('\n');
|
|
40
|
-
for (const line of raw) {
|
|
41
|
-
if (line.length <= width) {
|
|
42
|
-
result.push(line);
|
|
43
|
-
} else {
|
|
44
|
-
let start = 0;
|
|
45
|
-
while (start < line.length) {
|
|
46
|
-
result.push(line.slice(start, start + width));
|
|
47
|
-
start += width;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return result;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
37
|
type FlatRow = { kind: 'header'; blockIndex: number; text: string } | { kind: 'content'; text: string };
|
|
55
38
|
|
|
56
39
|
export class ThinkingPanel extends BasePanel {
|
|
@@ -121,8 +104,8 @@ export class ThinkingPanel extends BasePanel {
|
|
|
121
104
|
lines: buildEmptyState(
|
|
122
105
|
width,
|
|
123
106
|
' No reasoning content yet',
|
|
124
|
-
'When the model emits thinking or reasoning deltas, they accumulate here in expandable blocks.',
|
|
125
|
-
[],
|
|
107
|
+
'When the model emits thinking or reasoning deltas during a turn, they accumulate here in expandable per-turn blocks.',
|
|
108
|
+
[{ command: '/chat <prompt>', summary: 'start a turn with a reasoning-capable model to stream thinking here' }],
|
|
126
109
|
DEFAULT_PANEL_PALETTE,
|
|
127
110
|
),
|
|
128
111
|
},
|
|
@@ -152,12 +135,30 @@ export class ThinkingPanel extends BasePanel {
|
|
|
152
135
|
],
|
|
153
136
|
};
|
|
154
137
|
|
|
155
|
-
|
|
138
|
+
// Resolve the reasoning block that owns the cursor row (walk back to its
|
|
139
|
+
// header) so the detail block describes the actual turn, not just row kind.
|
|
140
|
+
let selectedBlockIndex = -1;
|
|
141
|
+
for (let i = Math.min(this.cursorIndex, flat.length - 1); i >= 0; i--) {
|
|
142
|
+
const row = flat[i];
|
|
143
|
+
if (row?.kind === 'header') { selectedBlockIndex = row.blockIndex; break; }
|
|
144
|
+
}
|
|
145
|
+
const selectedBlock = selectedBlockIndex >= 0 ? this.blocks[selectedBlockIndex] : undefined;
|
|
156
146
|
const selectedSection: PanelWorkspaceSection = {
|
|
157
147
|
title: 'Selected',
|
|
158
|
-
lines:
|
|
159
|
-
|
|
160
|
-
|
|
148
|
+
lines: selectedBlock
|
|
149
|
+
? [
|
|
150
|
+
buildPanelLine(width, [
|
|
151
|
+
[' Turn ', DEFAULT_PANEL_PALETTE.label],
|
|
152
|
+
[String(selectedBlock.turnId), DEFAULT_PANEL_PALETTE.value],
|
|
153
|
+
[' State ', DEFAULT_PANEL_PALETTE.label],
|
|
154
|
+
[selectedBlock.active ? 'streaming' : 'complete', selectedBlock.active ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.good],
|
|
155
|
+
[' View ', DEFAULT_PANEL_PALETTE.label],
|
|
156
|
+
[selectedBlock.collapsed ? 'collapsed' : 'expanded', DEFAULT_PANEL_PALETTE.info],
|
|
157
|
+
[' Chars ', DEFAULT_PANEL_PALETTE.label],
|
|
158
|
+
[String(selectedBlock.content.length), DEFAULT_PANEL_PALETTE.dim],
|
|
159
|
+
]),
|
|
160
|
+
]
|
|
161
|
+
: [buildPanelLine(width, [[' No block selected', DEFAULT_PANEL_PALETTE.dim]])],
|
|
161
162
|
};
|
|
162
163
|
|
|
163
164
|
const reasoningSection = resolveScrollablePanelSection(width, height, {
|
|
@@ -212,7 +213,7 @@ export class ThinkingPanel extends BasePanel {
|
|
|
212
213
|
const turnLabel = `Turn ${block.turnId}${block.active ? ' (streaming)' : ''}`;
|
|
213
214
|
rows.push({ kind: 'header', blockIndex: i, text: turnLabel });
|
|
214
215
|
if (!block.collapsed) {
|
|
215
|
-
const wrapped =
|
|
216
|
+
const wrapped = wrapText(block.content || '(empty)', Math.max(1, width - 2));
|
|
216
217
|
for (const line of wrapped) {
|
|
217
218
|
rows.push({ kind: 'content', text: line });
|
|
218
219
|
}
|
|
@@ -8,7 +8,10 @@ import type { SessionMemoryQuery } from '../runtime/ui-service-queries.ts';
|
|
|
8
8
|
import {
|
|
9
9
|
buildEmptyState,
|
|
10
10
|
buildGuidanceLine,
|
|
11
|
+
buildKeyboardHints,
|
|
12
|
+
buildMeterLine,
|
|
11
13
|
buildStyledPanelLine,
|
|
14
|
+
buildTable,
|
|
12
15
|
buildPanelWorkspace,
|
|
13
16
|
resolveScrollablePanelSection,
|
|
14
17
|
DEFAULT_PANEL_PALETTE,
|
|
@@ -237,6 +240,12 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
237
240
|
const priorSections = [...sections];
|
|
238
241
|
const turnsSection = resolveScrollablePanelSection(width, height, {
|
|
239
242
|
intro: 'Live context pressure, session token composition, cache usage, and recent turn deltas.',
|
|
243
|
+
footerLines: [
|
|
244
|
+
buildKeyboardHints(width, [
|
|
245
|
+
{ keys: '/compact', label: 'compress context' },
|
|
246
|
+
{ keys: '/clear', label: 'reset session' },
|
|
247
|
+
], DEFAULT_PANEL_PALETTE),
|
|
248
|
+
],
|
|
240
249
|
palette: DEFAULT_PANEL_PALETTE,
|
|
241
250
|
beforeSections: priorSections,
|
|
242
251
|
section: {
|
|
@@ -273,6 +282,12 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
273
282
|
title: ' Token Budget',
|
|
274
283
|
intro: 'Live context pressure, session token composition, cache usage, and recent turn deltas.',
|
|
275
284
|
sections,
|
|
285
|
+
footerLines: [
|
|
286
|
+
buildKeyboardHints(width, [
|
|
287
|
+
{ keys: '/compact', label: 'compress context' },
|
|
288
|
+
{ keys: '/clear', label: 'reset session' },
|
|
289
|
+
], DEFAULT_PANEL_PALETTE),
|
|
290
|
+
],
|
|
276
291
|
palette: DEFAULT_PANEL_PALETTE,
|
|
277
292
|
});
|
|
278
293
|
});
|
|
@@ -398,12 +413,12 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
398
413
|
const suffix = ` ${fmtTok(this.lastInputTokens)}/${fmtTok(this.contextWindow)} (${pctInt}%)${warnSuffix}`;
|
|
399
414
|
const BAR_W = Math.max(8, width - label.length - suffix.length - 2);
|
|
400
415
|
const filled = Math.round(pct * BAR_W);
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
{
|
|
405
|
-
{
|
|
406
|
-
|
|
416
|
+
// Context fill is the headline metric — render it with the shared meter
|
|
417
|
+
// primitive so the bar glyphs and width handling match every other panel.
|
|
418
|
+
lines.push(buildMeterLine(width, filled, BAR_W,
|
|
419
|
+
{ filled: barColor, empty: C.barBg, label: C.label },
|
|
420
|
+
{ prefix: label, suffix },
|
|
421
|
+
));
|
|
407
422
|
return lines;
|
|
408
423
|
}
|
|
409
424
|
|
|
@@ -436,41 +451,37 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
436
451
|
|
|
437
452
|
/** Last N turns table: turn#, in, out, CR, CW, total. */
|
|
438
453
|
private renderTurnHistory(width: number, maxRows: number): Line[] {
|
|
439
|
-
const lines: Line[] = [];
|
|
440
|
-
|
|
441
|
-
// Column headers
|
|
442
|
-
const colLine = createEmptyLine(width);
|
|
443
|
-
const headers = [' #', ' Input', ' Output', ' CR', ' CW', ' Total'];
|
|
444
|
-
const cols = [3, 9, 9, 6, 6, 9];
|
|
445
|
-
let hx = 0;
|
|
446
|
-
for (let i = 0; i < headers.length; i++) {
|
|
447
|
-
const h = headers[i]!;
|
|
448
|
-
for (const ch of h.slice(0, cols[i]!)) {
|
|
449
|
-
if (hx >= width) break;
|
|
450
|
-
colLine[hx++] = createStyledCell(ch, { fg: C.turnHeader, dim: true });
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
lines.push(colLine);
|
|
454
|
-
|
|
455
454
|
const available = Math.max(0, maxRows - 1); // minus col header
|
|
456
455
|
const toShow = this.turnHistory.slice(-Math.max(0, available));
|
|
457
456
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
]
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
457
|
+
// Width-aware aligned table via the shared primitive (replaces manual
|
|
458
|
+
// per-cell column math, which miscounts on wide glyphs).
|
|
459
|
+
return buildTable(
|
|
460
|
+
width,
|
|
461
|
+
[
|
|
462
|
+
{ label: '#', width: 4, align: 'right' },
|
|
463
|
+
{ label: 'Input', width: 9, align: 'right' },
|
|
464
|
+
{ label: 'Output', width: 9, align: 'right' },
|
|
465
|
+
{ label: 'CR', width: 7, align: 'right' },
|
|
466
|
+
{ label: 'CW', width: 7, align: 'right' },
|
|
467
|
+
{ label: 'Total', align: 'right' },
|
|
468
|
+
],
|
|
469
|
+
toShow.map((t, i) => {
|
|
470
|
+
const turnNum = this.turnHistory.length - toShow.length + i + 1;
|
|
471
|
+
const total = t.input + t.output + t.cacheRead + t.cacheWrite;
|
|
472
|
+
return {
|
|
473
|
+
cells: [
|
|
474
|
+
{ text: String(turnNum), fg: C.dim },
|
|
475
|
+
{ text: fmtTok(t.input), fg: C.input },
|
|
476
|
+
{ text: fmtTok(t.output), fg: C.output },
|
|
477
|
+
{ text: fmtTok(t.cacheRead), fg: C.cacheRead },
|
|
478
|
+
{ text: fmtTok(t.cacheWrite), fg: C.cacheWrite },
|
|
479
|
+
{ text: fmtTok(total), fg: C.value },
|
|
480
|
+
],
|
|
481
|
+
};
|
|
482
|
+
}),
|
|
483
|
+
{ ...DEFAULT_PANEL_PALETTE, label: C.turnHeader },
|
|
484
|
+
);
|
|
474
485
|
}
|
|
475
486
|
|
|
476
487
|
/** Paint a plain text string into a new Line. */
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type PanelWorkspaceSection,
|
|
18
18
|
} from './polish.ts';
|
|
19
19
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
20
|
+
import { formatDuration } from '../utils/format-duration.ts';
|
|
20
21
|
|
|
21
22
|
// Panel-specific accents only; shared tones come from DEFAULT_PANEL_PALETTE so
|
|
22
23
|
// theme changes propagate. selectedBg->selectBg, errorFg->bad are shared keys.
|
|
@@ -56,14 +57,12 @@ function shortTime(ms: number): string {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
function formatMs(ms: number): string {
|
|
59
|
-
|
|
60
|
-
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
61
|
-
return `${Math.floor(ms / 60000)}m${Math.floor((ms % 60000) / 1000)}s`;
|
|
60
|
+
return formatDuration(ms);
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
function truncateJson(val: unknown, maxLen = 120): string {
|
|
65
64
|
const s = JSON.stringify(val) ?? 'null';
|
|
66
|
-
return s
|
|
65
|
+
return truncateDisplay(s, maxLen);
|
|
67
66
|
}
|
|
68
67
|
|
|
69
68
|
function summarizeResult(result: unknown): string | undefined {
|
|
@@ -73,7 +72,7 @@ function summarizeResult(result: unknown): string | undefined {
|
|
|
73
72
|
const record = result as Record<string, unknown>;
|
|
74
73
|
if (typeof record.preview === 'string' && record.preview.trim()) {
|
|
75
74
|
const compact = record.preview.replace(/\s+/g, ' ').trim();
|
|
76
|
-
return compact
|
|
75
|
+
return truncateDisplay(compact, 72);
|
|
77
76
|
}
|
|
78
77
|
if (typeof record.kind === 'string' && typeof record.byteSize === 'number') {
|
|
79
78
|
return `${record.kind} (${record.byteSize}B)`;
|
|
@@ -142,22 +141,23 @@ export class ToolInspectorPanel extends BasePanel {
|
|
|
142
141
|
if (height <= 0 || width <= 0) return [];
|
|
143
142
|
|
|
144
143
|
const running = this.records.filter(r => r.endMs === undefined).length;
|
|
144
|
+
const distinctTools = new Set(this.records.map((r) => r.tool)).size;
|
|
145
145
|
const filterLabel = this.filterMode === 'all' ? '' : ` [${this.filterMode}]`;
|
|
146
146
|
const title = ` Tools [${this.records.length} calls${running > 0 ? `, ${running} running` : ''}]${filterLabel}`;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
[' expand', DEFAULT_PANEL_PALETTE.dim],
|
|
153
|
-
[' f', DEFAULT_PANEL_PALETTE.info],
|
|
154
|
-
[' filter', DEFAULT_PANEL_PALETTE.dim],
|
|
155
|
-
[' c', DEFAULT_PANEL_PALETTE.info],
|
|
156
|
-
[' clear', DEFAULT_PANEL_PALETTE.dim],
|
|
157
|
-
[' g', DEFAULT_PANEL_PALETTE.info],
|
|
158
|
-
[' end', DEFAULT_PANEL_PALETTE.dim],
|
|
159
|
-
]),
|
|
147
|
+
// Context-aware footer: only advertise keys that do something in the current
|
|
148
|
+
// state — filter needs >1 tool, clear/end need at least one call.
|
|
149
|
+
const footerSegments: Array<[string, string, string?]> = [
|
|
150
|
+
[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' scroll', DEFAULT_PANEL_PALETTE.dim],
|
|
151
|
+
[' Enter', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim],
|
|
160
152
|
];
|
|
153
|
+
if (distinctTools > 1) {
|
|
154
|
+
footerSegments.push([' f', DEFAULT_PANEL_PALETTE.info], [' filter', DEFAULT_PANEL_PALETTE.dim]);
|
|
155
|
+
}
|
|
156
|
+
if (this.records.length > 0) {
|
|
157
|
+
footerSegments.push([' c', DEFAULT_PANEL_PALETTE.info], [' clear', DEFAULT_PANEL_PALETTE.dim]);
|
|
158
|
+
footerSegments.push([' g', DEFAULT_PANEL_PALETTE.info], [this.autoScroll ? ' end (live)' : ' jump to end', DEFAULT_PANEL_PALETTE.dim]);
|
|
159
|
+
}
|
|
160
|
+
const footerLines = [buildPanelLine(width, footerSegments)];
|
|
161
161
|
|
|
162
162
|
const flat = this._getFlat();
|
|
163
163
|
|
|
@@ -174,9 +174,15 @@ export class ToolInspectorPanel extends BasePanel {
|
|
|
174
174
|
title: 'Calls',
|
|
175
175
|
lines: buildEmptyState(
|
|
176
176
|
width,
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
this.records.length > 0 && this.filterMode !== 'all'
|
|
178
|
+
? ` No "${this.filterMode}" calls`
|
|
179
|
+
: ' No tool calls yet',
|
|
180
|
+
this.records.length > 0 && this.filterMode !== 'all'
|
|
181
|
+
? 'The active tool filter hides every recorded call.'
|
|
182
|
+
: 'Tool executions appear here as the agent works. Expand a call to inspect its arguments and result payload.',
|
|
183
|
+
this.records.length > 0 && this.filterMode !== 'all'
|
|
184
|
+
? [{ command: 'f', summary: 'cycle the tool filter back to all calls' }]
|
|
185
|
+
: [{ command: '/spawn <task>', summary: 'run an agent to populate the tool-call timeline' }],
|
|
180
186
|
DEFAULT_PANEL_PALETTE,
|
|
181
187
|
),
|
|
182
188
|
},
|
package/src/panels/types.ts
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
|
+
import type { StatusState } from '../renderer/status-glyphs.ts';
|
|
2
3
|
import type { ComponentResourceContract, ComponentHealthState } from '../runtime/perf/panel-contracts.ts';
|
|
4
|
+
// Routed through the `@/` alias (not `./panel-manager.ts`) so this foundational
|
|
5
|
+
// types module stays a leaf in the relative-import graph the architecture
|
|
6
|
+
// cycle-checker walks. Type-only, erased at runtime — no real dependency edge.
|
|
7
|
+
import type { PanelManager } from '@/panels/panel-manager.ts';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Context passed to a panel's `handlePanelIntegrationAction` hook so it can
|
|
11
|
+
* drive cross-panel behavior (e.g. file-explorer opening the preview panel)
|
|
12
|
+
* without the input layer needing `instanceof` knowledge of each panel type.
|
|
13
|
+
*/
|
|
14
|
+
export interface PanelIntegrationContext {
|
|
15
|
+
readonly panelManager: PanelManager;
|
|
16
|
+
readonly executeCommand?: (name: string, args: string[]) => Promise<unknown>;
|
|
17
|
+
}
|
|
3
18
|
|
|
4
19
|
/**
|
|
5
20
|
* Named logical key identifiers emitted by the input tokenizer.
|
|
@@ -67,6 +82,16 @@ export interface Panel {
|
|
|
67
82
|
// Scroll input (optional)
|
|
68
83
|
// Positive delta scrolls down; negative delta scrolls up.
|
|
69
84
|
handleScroll?(deltaRows: number): boolean;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Cross-panel integration hook (optional). Called before the panel's own
|
|
88
|
+
* `handleInput` when a navigation/confirm key is pressed, so a panel can
|
|
89
|
+
* drive another panel (e.g. open a file in the preview panel). Return `true`
|
|
90
|
+
* to consume the key. The legacy `handlePanelIntegrationAction` router in
|
|
91
|
+
* `src/input/panel-integration-actions.ts` consults this first, then falls
|
|
92
|
+
* back to its built-in `instanceof` routing.
|
|
93
|
+
*/
|
|
94
|
+
handlePanelIntegrationAction?(key: string, ctx: PanelIntegrationContext): boolean;
|
|
70
95
|
}
|
|
71
96
|
|
|
72
97
|
export interface PanelRegistration extends Pick<Panel, 'id' | 'name' | 'icon' | 'category'> {
|
|
@@ -4,9 +4,11 @@ import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
|
4
4
|
import type { UiReadModel, UiWatchersSnapshot } from '../runtime/ui-read-models.ts';
|
|
5
5
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
6
6
|
import {
|
|
7
|
+
buildDetailBlock,
|
|
7
8
|
buildEmptyState,
|
|
8
9
|
buildGuidanceLine,
|
|
9
10
|
buildKeyValueLine,
|
|
11
|
+
buildKeyboardHints,
|
|
10
12
|
buildPanelLine,
|
|
11
13
|
buildPanelWorkspace,
|
|
12
14
|
DEFAULT_PANEL_PALETTE,
|
|
@@ -59,10 +61,18 @@ export class WatchersPanel extends ScrollableListPanel<WatcherEntry> {
|
|
|
59
61
|
public constructor(readModel?: UiReadModel<UiWatchersSnapshot>) {
|
|
60
62
|
super('watchers', 'Watchers', 'W', 'monitoring');
|
|
61
63
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
64
|
+
this.filterEnabled = true;
|
|
65
|
+
this.filterLabel = 'Filter watchers';
|
|
62
66
|
this.readModel = readModel;
|
|
63
67
|
this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
protected override filterMatches(watcher: WatcherEntry, q: string): boolean {
|
|
71
|
+
return watcher.label.toLowerCase().includes(q)
|
|
72
|
+
|| watcher.state.toLowerCase().includes(q)
|
|
73
|
+
|| String(watcher.sourceStatus ?? '').toLowerCase().includes(q);
|
|
74
|
+
}
|
|
75
|
+
|
|
66
76
|
public override onDestroy(): void {
|
|
67
77
|
this.unsub?.();
|
|
68
78
|
}
|
|
@@ -144,7 +154,7 @@ export class WatchersPanel extends ScrollableListPanel<WatcherEntry> {
|
|
|
144
154
|
this.clampSelection();
|
|
145
155
|
const selected = watchers[this.selectedIndex]!;
|
|
146
156
|
|
|
147
|
-
const
|
|
157
|
+
const detailRows: Line[] = [
|
|
148
158
|
buildPanelLine(width, [
|
|
149
159
|
[' Watcher: ', C.label],
|
|
150
160
|
[selected.label, C.value],
|
|
@@ -171,23 +181,33 @@ export class WatchersPanel extends ScrollableListPanel<WatcherEntry> {
|
|
|
171
181
|
]),
|
|
172
182
|
];
|
|
173
183
|
if (selected.degradedReason) {
|
|
174
|
-
|
|
184
|
+
detailRows.push(buildPanelLine(width, [
|
|
175
185
|
[' Reason: ', C.label],
|
|
176
186
|
[truncateDisplay(selected.degradedReason, Math.max(0, width - 11)), C.warn],
|
|
177
187
|
]));
|
|
178
188
|
}
|
|
179
189
|
if (selected.lastError) {
|
|
180
|
-
|
|
190
|
+
detailRows.push(buildPanelLine(width, [
|
|
181
191
|
[' Error: ', C.label],
|
|
182
192
|
[truncateDisplay(selected.lastError, Math.max(0, width - 10)), C.error],
|
|
183
193
|
]));
|
|
184
194
|
}
|
|
185
|
-
|
|
195
|
+
|
|
196
|
+
const hints = this.filterActive
|
|
197
|
+
? [{ keys: 'type', label: 'filter' }, { keys: 'Enter', label: 'apply' }, { keys: 'Esc', label: 'clear' }]
|
|
198
|
+
: [
|
|
199
|
+
{ keys: 'Up/Down', label: 'move' },
|
|
200
|
+
{ keys: '/schedule list', label: 'consumers' },
|
|
201
|
+
{ keys: '/', label: 'filter' },
|
|
202
|
+
];
|
|
186
203
|
|
|
187
204
|
return this.renderList(width, height, {
|
|
188
205
|
title: 'Watchers',
|
|
189
206
|
header: headerLines,
|
|
190
|
-
footer:
|
|
207
|
+
footer: [
|
|
208
|
+
...buildDetailBlock(width, `Watcher · ${selected.label}`, detailRows, C),
|
|
209
|
+
buildKeyboardHints(width, hints, C),
|
|
210
|
+
],
|
|
191
211
|
});
|
|
192
212
|
}
|
|
193
213
|
}
|
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import {
|
|
2
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
3
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
|
+
import {
|
|
5
|
+
buildDetailBlock,
|
|
6
|
+
buildGuidanceLine,
|
|
7
|
+
buildKeyboardHints,
|
|
8
|
+
buildKeyValueLine,
|
|
9
|
+
buildMeterLine,
|
|
10
|
+
buildPanelLine,
|
|
11
|
+
buildPanelListRow,
|
|
12
|
+
buildSummaryBlock,
|
|
13
|
+
DEFAULT_PANEL_PALETTE,
|
|
14
|
+
extendPalette,
|
|
15
|
+
type PanelPalette,
|
|
16
|
+
} from './polish.ts';
|
|
4
17
|
import type { WorkPlanItem, WorkPlanItemStatus, WorkPlanStore } from '../work-plans/work-plan-store.ts';
|
|
5
18
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
6
19
|
|
|
20
|
+
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
21
|
+
pending: '#94a3b8',
|
|
22
|
+
inProgress: '#38bdf8',
|
|
23
|
+
blocked: '#f59e0b',
|
|
24
|
+
done: '#22c55e',
|
|
25
|
+
failed: '#ef4444',
|
|
26
|
+
cancelled: '#64748b',
|
|
27
|
+
accent: '#a5b4fc',
|
|
28
|
+
selectBg: '#1e293b',
|
|
29
|
+
});
|
|
30
|
+
|
|
7
31
|
const STATUS_LABEL: Record<WorkPlanItemStatus, string> = {
|
|
8
32
|
pending: '[ ]',
|
|
9
33
|
in_progress: '[>]',
|
|
@@ -14,17 +38,15 @@ const STATUS_LABEL: Record<WorkPlanItemStatus, string> = {
|
|
|
14
38
|
};
|
|
15
39
|
|
|
16
40
|
const STATUS_COLOR: Record<WorkPlanItemStatus, string> = {
|
|
17
|
-
pending:
|
|
18
|
-
in_progress:
|
|
19
|
-
blocked:
|
|
20
|
-
done:
|
|
21
|
-
failed:
|
|
22
|
-
cancelled:
|
|
41
|
+
pending: C.pending,
|
|
42
|
+
in_progress: C.inProgress,
|
|
43
|
+
blocked: C.blocked,
|
|
44
|
+
done: C.done,
|
|
45
|
+
failed: C.failed,
|
|
46
|
+
cancelled: C.cancelled,
|
|
23
47
|
};
|
|
24
48
|
|
|
25
|
-
|
|
26
|
-
return UIFactory.stringToLine(text.padEnd(width).slice(0, width), width, style);
|
|
27
|
-
}
|
|
49
|
+
const STATUS_ORDER: WorkPlanItemStatus[] = ['pending', 'in_progress', 'blocked', 'done', 'failed', 'cancelled'];
|
|
28
50
|
|
|
29
51
|
function compactDate(value: number): string {
|
|
30
52
|
return new Date(value).toISOString().replace('T', ' ').slice(0, 16);
|
|
@@ -48,13 +70,16 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
|
|
|
48
70
|
this.refresh();
|
|
49
71
|
}
|
|
50
72
|
|
|
73
|
+
protected override getPalette(): PanelPalette {
|
|
74
|
+
return C;
|
|
75
|
+
}
|
|
76
|
+
|
|
51
77
|
render(width: number, height: number): Line[] {
|
|
52
78
|
this.refresh();
|
|
53
79
|
return this.renderList(width, height, {
|
|
54
80
|
title: 'Work Plan',
|
|
55
81
|
header: this.renderHeader(width),
|
|
56
82
|
footer: this.renderFooter(width),
|
|
57
|
-
emptyMessage: 'No work plan items yet',
|
|
58
83
|
});
|
|
59
84
|
}
|
|
60
85
|
|
|
@@ -108,10 +133,14 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
|
|
|
108
133
|
return this.items;
|
|
109
134
|
}
|
|
110
135
|
|
|
136
|
+
protected override getEmptyStateMessage(): string {
|
|
137
|
+
return ' No work plan items yet';
|
|
138
|
+
}
|
|
139
|
+
|
|
111
140
|
protected getEmptyStateActions(): Array<{ command: string; summary: string }> {
|
|
112
141
|
return [
|
|
113
|
-
{ command: '/work-plan add <title>', summary: 'add a persistent item' },
|
|
114
|
-
{ command: '/work-plan list', summary: 'print the current plan' },
|
|
142
|
+
{ command: '/work-plan add <title>', summary: 'add a persistent item that survives across sessions' },
|
|
143
|
+
{ command: '/work-plan list', summary: 'print the current plan to the shell' },
|
|
115
144
|
];
|
|
116
145
|
}
|
|
117
146
|
|
|
@@ -119,12 +148,14 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
|
|
|
119
148
|
const status = STATUS_LABEL[item.status];
|
|
120
149
|
const owner = item.owner ? ` @${item.owner}` : '';
|
|
121
150
|
const source = item.source ? ` (${item.source})` : '';
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
bold: selected || item.status === 'in_progress',
|
|
127
|
-
|
|
151
|
+
const reserve = status.length + owner.length + source.length + 4;
|
|
152
|
+
const title = truncateDisplay(item.title, Math.max(6, width - reserve));
|
|
153
|
+
return buildPanelListRow(width, [
|
|
154
|
+
{ text: `${status} `, fg: STATUS_COLOR[item.status], bold: item.status === 'in_progress' },
|
|
155
|
+
{ text: title, fg: selected ? C.value : C.label, bold: selected || item.status === 'in_progress' },
|
|
156
|
+
{ text: owner, fg: C.accent },
|
|
157
|
+
{ text: source, fg: C.dim },
|
|
158
|
+
], C, { selected, selectedBg: C.selectBg });
|
|
128
159
|
}
|
|
129
160
|
|
|
130
161
|
private setSelectedStatus(status: WorkPlanItemStatus): boolean {
|
|
@@ -147,30 +178,90 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
|
|
|
147
178
|
private renderHeader(width: number): Line[] {
|
|
148
179
|
const plan = this.store.getActivePlan();
|
|
149
180
|
const counts = new Map<WorkPlanItemStatus, number>();
|
|
150
|
-
for (const status of
|
|
181
|
+
for (const status of STATUS_ORDER) counts.set(status, 0);
|
|
151
182
|
for (const item of plan.items) counts.set(item.status, (counts.get(item.status) ?? 0) + 1);
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
183
|
+
|
|
184
|
+
const total = plan.items.length;
|
|
185
|
+
const done = (counts.get('done') ?? 0) + (counts.get('cancelled') ?? 0);
|
|
186
|
+
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
187
|
+
const meterWidth = Math.max(10, Math.min(26, width - 34));
|
|
188
|
+
|
|
189
|
+
const postureLines: Line[] = [
|
|
190
|
+
buildPanelLine(width, [
|
|
191
|
+
[' Project ', C.label],
|
|
192
|
+
[truncateDisplay(plan.projectRoot, Math.max(8, width - 11)), C.value],
|
|
193
|
+
]),
|
|
194
|
+
buildMeterLine(width, Math.round((pct / 100) * meterWidth), meterWidth, {
|
|
195
|
+
filled: pct === 100 ? C.done : C.inProgress,
|
|
196
|
+
empty: C.empty,
|
|
197
|
+
label: C.label,
|
|
198
|
+
}, { prefix: ' Progress ', suffix: ` ${done}/${total} (${pct}%) ` }),
|
|
199
|
+
buildKeyValueLine(width, [
|
|
200
|
+
{ label: 'pending', value: String(counts.get('pending') ?? 0), valueColor: (counts.get('pending') ?? 0) > 0 ? C.pending : C.dim },
|
|
201
|
+
{ label: 'active', value: String(counts.get('in_progress') ?? 0), valueColor: (counts.get('in_progress') ?? 0) > 0 ? C.inProgress : C.dim },
|
|
202
|
+
{ label: 'blocked', value: String(counts.get('blocked') ?? 0), valueColor: (counts.get('blocked') ?? 0) > 0 ? C.blocked : C.dim },
|
|
203
|
+
{ label: 'done', value: String(counts.get('done') ?? 0), valueColor: (counts.get('done') ?? 0) > 0 ? C.done : C.dim },
|
|
204
|
+
], C),
|
|
205
|
+
buildGuidanceLine(width, '/work-plan add <title>', 'append a persistent item to the active plan', C),
|
|
162
206
|
];
|
|
207
|
+
|
|
208
|
+
const header: Line[] = buildSummaryBlock(width, 'Persistent Work Plan', postureLines, C);
|
|
209
|
+
|
|
210
|
+
const active = this.items[this.selectedIndex];
|
|
163
211
|
if (active) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
212
|
+
const detailRows: Line[] = [
|
|
213
|
+
buildPanelLine(width, [
|
|
214
|
+
[' ', C.label],
|
|
215
|
+
[STATUS_LABEL[active.status], STATUS_COLOR[active.status]],
|
|
216
|
+
[' ', C.label],
|
|
217
|
+
[truncateDisplay(active.title, Math.max(8, width - 6)), C.value],
|
|
218
|
+
]),
|
|
219
|
+
buildKeyValueLine(width, [
|
|
220
|
+
{ label: 'status', value: statusName(active.status), valueColor: STATUS_COLOR[active.status] },
|
|
221
|
+
...(active.owner ? [{ label: 'owner', value: active.owner, valueColor: C.accent }] : []),
|
|
222
|
+
{ label: 'updated', value: compactDate(active.updatedAt), valueColor: C.dim },
|
|
223
|
+
], C),
|
|
224
|
+
];
|
|
225
|
+
if (active.source) {
|
|
226
|
+
detailRows.push(buildPanelLine(width, [[' source ', C.label], [active.source, C.info]]));
|
|
227
|
+
}
|
|
228
|
+
if (active.notes) {
|
|
229
|
+
detailRows.push(buildPanelLine(width, [
|
|
230
|
+
[' notes ', C.label],
|
|
231
|
+
[truncateDisplay(active.notes, Math.max(8, width - 8)), C.value],
|
|
232
|
+
]));
|
|
233
|
+
}
|
|
234
|
+
header.push(...buildDetailBlock(width, 'Selected item', detailRows, C));
|
|
167
235
|
}
|
|
168
236
|
return header;
|
|
169
237
|
}
|
|
170
238
|
|
|
171
239
|
private renderFooter(width: number): Line[] {
|
|
240
|
+
const hasItem = this.items.length > 0;
|
|
241
|
+
if (!hasItem) {
|
|
242
|
+
return [
|
|
243
|
+
buildKeyboardHints(width, [
|
|
244
|
+
{ keys: '↑/↓', label: 'navigate' },
|
|
245
|
+
], C),
|
|
246
|
+
];
|
|
247
|
+
}
|
|
172
248
|
return [
|
|
173
|
-
|
|
249
|
+
buildKeyboardHints(width, [
|
|
250
|
+
{ keys: this.items.length > 0 ? `${this.selectedIndex + 1}/${this.items.length}` : '0/0', label: 'item' },
|
|
251
|
+
{ keys: 'Enter', label: 'cycle status' },
|
|
252
|
+
{ keys: '1-6', label: 'set status' },
|
|
253
|
+
{ keys: 'd', label: 'delete' },
|
|
254
|
+
{ keys: 'c', label: 'clear done' },
|
|
255
|
+
{ keys: 'r', label: 'refresh' },
|
|
256
|
+
], C),
|
|
257
|
+
buildPanelLine(width, [
|
|
258
|
+
[' 1', C.info], [' pending ', C.dim],
|
|
259
|
+
['2', C.info], [' active ', C.dim],
|
|
260
|
+
['3', C.info], [' blocked ', C.dim],
|
|
261
|
+
['4', C.info], [' done ', C.dim],
|
|
262
|
+
['5', C.info], [' failed ', C.dim],
|
|
263
|
+
['6', C.info], [' cancelled', C.dim],
|
|
264
|
+
]),
|
|
174
265
|
];
|
|
175
266
|
}
|
|
176
267
|
}
|