@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
package/src/panels/polish.ts
CHANGED
|
@@ -2,152 +2,31 @@ import type { Line } from '../types/grid.ts';
|
|
|
2
2
|
import { createEmptyLine, createStyledCell } from '../types/grid.ts';
|
|
3
3
|
import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
|
|
4
4
|
import { getSurfaceContentRows, getTrackedVisibleWindow, getVisibleWindow, type VisibleWindow } from '../renderer/surface-layout.ts';
|
|
5
|
-
import { GLYPHS
|
|
5
|
+
import { GLYPHS } from '../renderer/ui-primitives.ts';
|
|
6
6
|
import { type StatusState, STATE_GLYPHS } from '../renderer/status-glyphs.ts';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
label: UI_TONES.fg.muted,
|
|
31
|
-
value: UI_TONES.fg.primary,
|
|
32
|
-
dim: UI_TONES.fg.dim,
|
|
33
|
-
info: UI_TONES.state.info,
|
|
34
|
-
good: UI_TONES.state.good,
|
|
35
|
-
warn: UI_TONES.state.warn,
|
|
36
|
-
bad: UI_TONES.state.bad,
|
|
37
|
-
empty: '#334155',
|
|
38
|
-
surfaceBg: UI_TONES.bg.surface,
|
|
39
|
-
sectionBg: UI_TONES.bg.section,
|
|
40
|
-
summaryBg: UI_TONES.bg.summary,
|
|
41
|
-
inputBg: UI_TONES.bg.input,
|
|
42
|
-
accent: UI_TONES.fg.secondary,
|
|
43
|
-
selectBg: UI_TONES.bg.selected,
|
|
44
|
-
} as const;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Extend the base panel palette with domain-specific colors.
|
|
48
|
-
*
|
|
49
|
-
* Convention: raw hex colors may only live inside a palette constant declared
|
|
50
|
-
* at the top of a panel file, not inline in render calls.
|
|
51
|
-
*
|
|
52
|
-
* @example
|
|
53
|
-
* ```ts
|
|
54
|
-
* const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
55
|
-
* decision: '#38bdf8',
|
|
56
|
-
* incident: '#ef4444',
|
|
57
|
-
* });
|
|
58
|
-
* ```
|
|
59
|
-
*/
|
|
60
|
-
export function extendPalette<T extends Record<string, string>>(
|
|
61
|
-
base: typeof DEFAULT_PANEL_PALETTE,
|
|
62
|
-
extras: T,
|
|
63
|
-
): typeof DEFAULT_PANEL_PALETTE & T {
|
|
64
|
-
return { ...base, ...extras };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function buildPanelLine(
|
|
68
|
-
width: number,
|
|
69
|
-
segments: Array<StyledPanelSegment | [string, string, string?]>,
|
|
70
|
-
): Line {
|
|
71
|
-
return buildStyledPanelLine(
|
|
72
|
-
width,
|
|
73
|
-
segments.map((seg) =>
|
|
74
|
-
Array.isArray(seg) ? { text: seg[0], fg: seg[1], bg: seg[2] } : seg,
|
|
75
|
-
),
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface StyledPanelSegment {
|
|
80
|
-
readonly text: string;
|
|
81
|
-
readonly fg: string;
|
|
82
|
-
readonly bg?: string;
|
|
83
|
-
readonly bold?: boolean;
|
|
84
|
-
readonly dim?: boolean;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function buildSelectablePanelLine(
|
|
88
|
-
width: number,
|
|
89
|
-
segments: ReadonlyArray<StyledPanelSegment>,
|
|
90
|
-
options: { selected?: boolean; selectedBg?: string; fillFg?: string; fillBg?: string; leadingMarker?: string } = {},
|
|
91
|
-
): Line {
|
|
92
|
-
const selected = options.selected ?? false;
|
|
93
|
-
const selectedBg = selected ? (options.selectedBg ?? DEFAULT_PANEL_PALETTE.selectBg) : '';
|
|
94
|
-
const fillBg = selectedBg || options.fillBg || '';
|
|
95
|
-
const fillFg = options.fillFg ?? '';
|
|
96
|
-
const cells = createEmptyLine(width);
|
|
97
|
-
if (fillBg) {
|
|
98
|
-
for (let col = 0; col < width; col++) {
|
|
99
|
-
cells[col] = createStyledCell(' ', { bg: fillBg, fg: fillFg });
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
let col = 0;
|
|
104
|
-
if (selected && options.leadingMarker) {
|
|
105
|
-
for (const ch of options.leadingMarker) {
|
|
106
|
-
const charWidth = getDisplayWidth(ch);
|
|
107
|
-
if (charWidth <= 0 || col + charWidth > width) break;
|
|
108
|
-
cells[col] = createStyledCell(ch, { fg: DEFAULT_PANEL_PALETTE.info, bg: selectedBg, bold: true });
|
|
109
|
-
if (charWidth > 1 && col + 1 < width) cells[col + 1] = createStyledCell(' ', { fg: DEFAULT_PANEL_PALETTE.info, bg: selectedBg, bold: true });
|
|
110
|
-
col += charWidth;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
for (const segment of segments) {
|
|
114
|
-
const fg = segment.fg;
|
|
115
|
-
const bg = segment.bg ?? fillBg;
|
|
116
|
-
for (const ch of segment.text) {
|
|
117
|
-
const charWidth = getDisplayWidth(ch);
|
|
118
|
-
if (charWidth <= 0) continue;
|
|
119
|
-
if (col + charWidth > width) return cells;
|
|
120
|
-
cells[col] = createStyledCell(ch, {
|
|
121
|
-
fg,
|
|
122
|
-
bg,
|
|
123
|
-
bold: segment.bold ?? false,
|
|
124
|
-
dim: segment.dim ?? false,
|
|
125
|
-
});
|
|
126
|
-
if (charWidth > 1 && col + 1 < width) {
|
|
127
|
-
cells[col + 1] = createStyledCell(' ', {
|
|
128
|
-
fg,
|
|
129
|
-
bg,
|
|
130
|
-
bold: segment.bold ?? false,
|
|
131
|
-
dim: segment.dim ?? false,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
col += charWidth;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
while (col < width) {
|
|
139
|
-
cells[col++] = createStyledCell(' ', { bg: fillBg, fg: fillFg });
|
|
140
|
-
}
|
|
141
|
-
return cells;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export function buildStyledPanelLine(
|
|
145
|
-
width: number,
|
|
146
|
-
segments: ReadonlyArray<StyledPanelSegment>,
|
|
147
|
-
options: { fillBg?: string; fillFg?: string } = {},
|
|
148
|
-
): Line {
|
|
149
|
-
return buildSelectablePanelLine(width, segments, options);
|
|
150
|
-
}
|
|
7
|
+
import { buildPanelLine, buildStyledPanelLine, DEFAULT_PANEL_PALETTE } from './polish-core.ts';
|
|
8
|
+
import type { PanelPalette, StyledPanelSegment } from './polish-core.ts';
|
|
9
|
+
|
|
10
|
+
// The panel formatting toolkit is split across three modules to stay under the
|
|
11
|
+
// architecture line-count cap; every symbol remains importable from './polish.ts':
|
|
12
|
+
// - ./polish-core.ts palette + base line primitives (leaf)
|
|
13
|
+
// - ./polish-tables.ts table / tree / badge / aligned-row primitives
|
|
14
|
+
// This module keeps the higher-level builders + workspace layout.
|
|
15
|
+
export * from './polish-core.ts';
|
|
16
|
+
export {
|
|
17
|
+
buildAlignedRow,
|
|
18
|
+
buildStatusBadge,
|
|
19
|
+
buildTreeRow,
|
|
20
|
+
buildTable,
|
|
21
|
+
type ColumnAlign,
|
|
22
|
+
type ColumnSpec,
|
|
23
|
+
type AlignedCell,
|
|
24
|
+
type StatusBadgeKind,
|
|
25
|
+
type TreeItemSpec,
|
|
26
|
+
type TableColumn,
|
|
27
|
+
type TableCell,
|
|
28
|
+
type TableRow,
|
|
29
|
+
} from './polish-tables.ts';
|
|
151
30
|
|
|
152
31
|
export function buildGuidanceLine(
|
|
153
32
|
width: number,
|
|
@@ -680,6 +559,26 @@ export function resolveStackedScrollableSections(
|
|
|
680
559
|
});
|
|
681
560
|
}
|
|
682
561
|
|
|
562
|
+
/**
|
|
563
|
+
* Standardized footer keyboard-hint row so every panel's hints look identical.
|
|
564
|
+
* Each hint renders as `keys` (info color) + `label` (dim).
|
|
565
|
+
*/
|
|
566
|
+
export function buildKeyboardHints(
|
|
567
|
+
width: number,
|
|
568
|
+
hints: ReadonlyArray<{ keys: string; label: string }>,
|
|
569
|
+
palette: PanelPalette,
|
|
570
|
+
): Line {
|
|
571
|
+
const segments: Array<[string, string, string?]> = [];
|
|
572
|
+
hints.forEach((hint, i) => {
|
|
573
|
+
if (i > 0) segments.push([' ', palette.dim]);
|
|
574
|
+
else segments.push([' ', palette.dim]);
|
|
575
|
+
segments.push([hint.keys, palette.info]);
|
|
576
|
+
segments.push([' ', palette.dim]);
|
|
577
|
+
segments.push([hint.label, palette.dim]);
|
|
578
|
+
});
|
|
579
|
+
return buildPanelLine(width, segments);
|
|
580
|
+
}
|
|
581
|
+
|
|
683
582
|
export function buildPanelWorkspace(
|
|
684
583
|
width: number,
|
|
685
584
|
height: number,
|
|
@@ -367,12 +367,34 @@ export class ProjectPlanningPanel extends BasePanel {
|
|
|
367
367
|
: readiness === 'needs-user-input'
|
|
368
368
|
? C.blocked
|
|
369
369
|
: C.dim;
|
|
370
|
+
const blockingGaps = (evaluation?.gaps ?? []).filter((gap) => gap.severity === 'blocking').length;
|
|
371
|
+
const openQuestions = state.openQuestions.filter((q) => (q.status ?? 'open') === 'open').length;
|
|
372
|
+
// Surface the most important thing first: is this plan executable, and what
|
|
373
|
+
// is the single blocker to getting there.
|
|
374
|
+
const nextStep = state.executionApproved
|
|
375
|
+
? 'approved — execution may proceed'
|
|
376
|
+
: openQuestions > 0
|
|
377
|
+
? `answer ${openQuestions} open question${openQuestions === 1 ? '' : 's'}`
|
|
378
|
+
: blockingGaps > 0
|
|
379
|
+
? `resolve ${blockingGaps} blocking gap${blockingGaps === 1 ? '' : 's'}`
|
|
380
|
+
: readiness === 'executable'
|
|
381
|
+
? 'press a to approve execution'
|
|
382
|
+
: 'continue the planning interview';
|
|
370
383
|
const lines: Line[] = [
|
|
371
384
|
buildKeyValueLine(width, [
|
|
372
385
|
{ label: 'readiness', value: readiness, valueColor: readinessColor },
|
|
373
386
|
{ label: 'approved', value: state.executionApproved ? 'yes' : 'no', valueColor: state.executionApproved ? C.approved : C.blocked },
|
|
374
387
|
{ label: 'questions', value: `${state.openQuestions.length} open / ${state.answeredQuestions.length} answered`, valueColor: C.value },
|
|
375
388
|
], C),
|
|
389
|
+
buildKeyValueLine(width, [
|
|
390
|
+
{ label: 'blocking gaps', value: String(blockingGaps), valueColor: blockingGaps > 0 ? C.blocked : C.good },
|
|
391
|
+
{ label: 'tasks', value: String(state.tasks.length), valueColor: state.tasks.length > 0 ? C.value : C.dim },
|
|
392
|
+
{ label: 'gates', value: String(state.verificationGates.length), valueColor: state.verificationGates.length > 0 ? C.good : C.dim },
|
|
393
|
+
], C),
|
|
394
|
+
buildPanelLine(width, [
|
|
395
|
+
[' Next step ', C.label],
|
|
396
|
+
[nextStep, state.executionApproved ? C.approved : C.planning],
|
|
397
|
+
]),
|
|
376
398
|
...buildBodyText(width, `Goal: ${state.goal || '(not set)'}`, C, state.goal ? C.value : C.blocked),
|
|
377
399
|
];
|
|
378
400
|
if (state.scope) lines.push(...buildBodyText(width, `Scope: ${state.scope}`, C, C.value));
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
4
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
5
|
import {
|
|
6
|
+
buildAlignedRow,
|
|
5
7
|
buildDetailBlock,
|
|
6
8
|
buildEmptyState,
|
|
7
9
|
buildGuidanceLine,
|
|
10
|
+
buildKeyboardHints,
|
|
8
11
|
buildKeyValueLine,
|
|
9
|
-
buildPanelListRow,
|
|
10
12
|
buildPanelLine,
|
|
11
13
|
buildSummaryBlock,
|
|
12
14
|
buildPanelWorkspace,
|
|
@@ -50,14 +52,32 @@ export class ProviderAccountsPanel extends ScrollableListPanel<ProviderAccountRe
|
|
|
50
52
|
return this.records;
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
private readonly listColumns = [
|
|
56
|
+
{ width: 16 },
|
|
57
|
+
{ width: 14 },
|
|
58
|
+
{ width: 8, align: 'right' as const },
|
|
59
|
+
{ width: 11 },
|
|
60
|
+
{ width: 8, align: 'right' as const },
|
|
61
|
+
];
|
|
62
|
+
|
|
53
63
|
protected renderItem(item: ProviderAccountRecord, _index: number, selected: boolean, width: number): Line {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
const routeFg = item.activeRoute === 'subscription' ? C.info
|
|
65
|
+
: item.activeRoute === 'api-key' ? C.warn
|
|
66
|
+
: item.activeRoute === 'service-oauth' ? C.value : C.dim;
|
|
67
|
+
const freshFg = item.authFreshness === 'expired' ? C.bad
|
|
68
|
+
: item.authFreshness === 'expiring' || item.authFreshness === 'pending' ? C.warn : C.dim;
|
|
69
|
+
return buildAlignedRow(
|
|
70
|
+
width,
|
|
71
|
+
[
|
|
72
|
+
{ text: item.providerId, fg: item.active ? C.good : C.value, bold: selected },
|
|
73
|
+
{ text: item.activeRoute, fg: routeFg },
|
|
74
|
+
{ text: `${item.modelCount} mdl`, fg: C.dim },
|
|
75
|
+
{ text: item.authFreshness, fg: freshFg },
|
|
76
|
+
{ text: item.issues.length > 0 ? `${item.issues.length} !` : 'ok', fg: item.issues.length > 0 ? C.bad : C.good },
|
|
77
|
+
],
|
|
78
|
+
this.listColumns,
|
|
79
|
+
{ selected, selectedBg: C.selectBg, marker: '▸' },
|
|
80
|
+
);
|
|
61
81
|
}
|
|
62
82
|
|
|
63
83
|
public handleInput(key: string): boolean {
|
|
@@ -85,7 +105,11 @@ export class ProviderAccountsPanel extends ScrollableListPanel<ProviderAccountRe
|
|
|
85
105
|
public render(width: number, height: number): Line[] {
|
|
86
106
|
this.needsRender = false;
|
|
87
107
|
const intro = 'Provider auth routes, subscription posture, quota-window hints, and routing-safety notes.';
|
|
88
|
-
const footerLines = [
|
|
108
|
+
const footerLines = [buildKeyboardHints(width, [
|
|
109
|
+
{ keys: 'Up/Down', label: 'select' },
|
|
110
|
+
{ keys: 'r', label: 'refresh' },
|
|
111
|
+
{ keys: '/accounts repair <provider>', label: 'recover routing' },
|
|
112
|
+
], C)];
|
|
89
113
|
if (this.loading && this.records.length === 0) {
|
|
90
114
|
const lines = buildPanelWorkspace(width, height, {
|
|
91
115
|
title: 'Provider Account Control Room',
|
|
@@ -140,8 +164,8 @@ export class ProviderAccountsPanel extends ScrollableListPanel<ProviderAccountRe
|
|
|
140
164
|
{ label: 'oauth ready', value: selected.oauthReady ? 'yes' : 'no', valueColor: selected.oauthReady ? C.info : C.dim },
|
|
141
165
|
{ label: 'pending login', value: selected.pendingLogin ? 'yes' : 'no', valueColor: selected.pendingLogin ? C.warn : C.dim },
|
|
142
166
|
], C),
|
|
143
|
-
buildPanelLine(width, [[` Active route reason: ${selected.activeRouteReason}
|
|
144
|
-
buildPanelLine(width, [[` Available routes: ${selected.availableRoutes.join(', ') || 'unconfigured'}
|
|
167
|
+
buildPanelLine(width, [[truncateDisplay(` Active route reason: ${selected.activeRouteReason}`, width), C.dim]]),
|
|
168
|
+
buildPanelLine(width, [[truncateDisplay(` Available routes: ${selected.availableRoutes.join(', ') || 'unconfigured'}`, width), C.dim]]),
|
|
145
169
|
];
|
|
146
170
|
if (selected.expiresAt) {
|
|
147
171
|
detailRows.push(buildPanelLine(width, [
|
|
@@ -152,28 +176,28 @@ export class ProviderAccountsPanel extends ScrollableListPanel<ProviderAccountRe
|
|
|
152
176
|
]));
|
|
153
177
|
}
|
|
154
178
|
if (selected.fallbackRisk) {
|
|
155
|
-
detailRows.push(buildPanelLine(width, [[` fallback: ${selected.fallbackRisk}
|
|
179
|
+
detailRows.push(buildPanelLine(width, [[truncateDisplay(` fallback: ${selected.fallbackRisk}`, width), C.warn]]));
|
|
156
180
|
}
|
|
157
181
|
for (const route of selected.routeRecords) {
|
|
158
182
|
detailRows.push(buildPanelLine(width, [[
|
|
159
|
-
` route ${route.route}: ${route.usable ? 'usable' : 'blocked'} • ${route.freshness} • ${route.detail}
|
|
183
|
+
truncateDisplay(` route ${route.route}: ${route.usable ? 'usable' : 'blocked'} • ${route.freshness} • ${route.detail}`, width),
|
|
160
184
|
route.usable ? C.dim : C.bad,
|
|
161
185
|
]]));
|
|
162
186
|
for (const issue of route.issues) {
|
|
163
|
-
detailRows.push(buildPanelLine(width, [[` issue: ${issue}
|
|
187
|
+
detailRows.push(buildPanelLine(width, [[truncateDisplay(` issue: ${issue}`, width), C.bad]]));
|
|
164
188
|
}
|
|
165
189
|
}
|
|
166
190
|
for (const windowHint of selected.usageWindows) {
|
|
167
|
-
detailRows.push(buildPanelLine(width, [[` ${windowHint.label}: ${windowHint.detail}
|
|
191
|
+
detailRows.push(buildPanelLine(width, [[truncateDisplay(` ${windowHint.label}: ${windowHint.detail}`, width), C.dim]]));
|
|
168
192
|
}
|
|
169
193
|
for (const issue of selected.issues) {
|
|
170
|
-
detailRows.push(buildPanelLine(width, [[` issue: ${issue}
|
|
194
|
+
detailRows.push(buildPanelLine(width, [[truncateDisplay(` issue: ${issue}`, width), C.bad]]));
|
|
171
195
|
}
|
|
172
196
|
for (const note of selected.notes) {
|
|
173
|
-
detailRows.push(buildPanelLine(width, [[` note: ${note}
|
|
197
|
+
detailRows.push(buildPanelLine(width, [[truncateDisplay(` note: ${note}`, width), C.info]]));
|
|
174
198
|
}
|
|
175
199
|
for (const action of selected.recommendedActions) {
|
|
176
|
-
detailRows.push(buildPanelLine(width, [[` next: ${action}
|
|
200
|
+
detailRows.push(buildPanelLine(width, [[truncateDisplay(` next: ${action}`, width), C.value]]));
|
|
177
201
|
}
|
|
178
202
|
if (selected.issues.length === 0 && selected.notes.length === 0 && selected.usageWindows.length === 0 && selected.recommendedActions.length === 0) {
|
|
179
203
|
detailRows.push(buildPanelLine(width, [[' No active account warnings for this provider.', C.dim]]));
|
|
@@ -183,6 +207,18 @@ export class ProviderAccountsPanel extends ScrollableListPanel<ProviderAccountRe
|
|
|
183
207
|
const rawProviderLines: Line[] = this.records.map((record, absolute) =>
|
|
184
208
|
this.renderItem(record, absolute, absolute === this.selectedIndex, width),
|
|
185
209
|
);
|
|
210
|
+
const columnHeader = buildAlignedRow(
|
|
211
|
+
width,
|
|
212
|
+
[
|
|
213
|
+
{ text: 'provider', fg: C.label, bold: true },
|
|
214
|
+
{ text: 'route', fg: C.label, bold: true },
|
|
215
|
+
{ text: 'models', fg: C.label, bold: true },
|
|
216
|
+
{ text: 'auth', fg: C.label, bold: true },
|
|
217
|
+
{ text: 'issues', fg: C.label, bold: true },
|
|
218
|
+
],
|
|
219
|
+
this.listColumns,
|
|
220
|
+
{ marker: '▸' },
|
|
221
|
+
);
|
|
186
222
|
const resolvedProvidersSection = resolvePrimaryScrollableSection(width, height, {
|
|
187
223
|
intro,
|
|
188
224
|
footerLines,
|
|
@@ -190,6 +226,7 @@ export class ProviderAccountsPanel extends ScrollableListPanel<ProviderAccountRe
|
|
|
190
226
|
beforeSections: [postureSection],
|
|
191
227
|
section: {
|
|
192
228
|
title: 'Providers',
|
|
229
|
+
fixedLines: [columnHeader],
|
|
193
230
|
scrollableLines: rawProviderLines,
|
|
194
231
|
selectedIndex: this.selectedIndex,
|
|
195
232
|
scrollOffset: this.scrollStart,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
2
|
+
import { formatLatencyMs } from '../utils/format-duration.ts';
|
|
2
3
|
import { BasePanel } from './base-panel.ts';
|
|
3
4
|
import { createEmptyLine, createStyledCell, type Line } from '../types/grid.ts';
|
|
4
5
|
import type { ProviderAuthRouteDescriptor } from '@pellux/goodvibes-sdk/platform/providers';
|
|
@@ -26,13 +27,13 @@ import type {
|
|
|
26
27
|
} from '../runtime/ui-read-models.ts';
|
|
27
28
|
import { evaluateSessionMaintenance } from '@/runtime/index.ts';
|
|
28
29
|
import {
|
|
30
|
+
buildAlignedRow,
|
|
29
31
|
buildBodyText,
|
|
30
32
|
buildDetailBlock,
|
|
31
33
|
buildEmptyState,
|
|
32
34
|
buildGuidanceLine,
|
|
35
|
+
buildKeyboardHints,
|
|
33
36
|
buildKeyValueLine,
|
|
34
|
-
buildPanelListRow,
|
|
35
|
-
buildPanelLine,
|
|
36
37
|
buildPanelWorkspace,
|
|
37
38
|
buildSummaryBlock,
|
|
38
39
|
DEFAULT_PANEL_PALETTE,
|
|
@@ -66,6 +67,7 @@ export interface ProviderHealthPanelDeps {
|
|
|
66
67
|
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
67
68
|
title: '#00ffff',
|
|
68
69
|
unknown: '244',
|
|
70
|
+
rowSelectBg: '#111827',
|
|
69
71
|
});
|
|
70
72
|
|
|
71
73
|
const LATENCY_WARN_MS = 2_000;
|
|
@@ -104,10 +106,7 @@ function latencyColor(ms: number): string {
|
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
function fmtMs(ms: number): string {
|
|
107
|
-
|
|
108
|
-
if (ms >= 10_000) return `${(ms / 1000).toFixed(1)}s`;
|
|
109
|
-
if (ms >= 1_000) return `${(ms / 1000).toFixed(2)}s`;
|
|
110
|
-
return `${Math.round(ms)}ms`;
|
|
109
|
+
return formatLatencyMs(ms);
|
|
111
110
|
}
|
|
112
111
|
|
|
113
112
|
function fmtAgo(ts: number | undefined): string {
|
|
@@ -592,13 +591,19 @@ export class ProviderHealthPanel extends BasePanel {
|
|
|
592
591
|
worktrees: this.deps.worktrees.getSnapshot(),
|
|
593
592
|
session: this.deps.session.getSnapshot(),
|
|
594
593
|
})) {
|
|
595
|
-
domainLines.push(
|
|
596
|
-
|
|
597
|
-
[
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
594
|
+
domainLines.push(buildAlignedRow(
|
|
595
|
+
width,
|
|
596
|
+
[
|
|
597
|
+
{ text: domain.name, fg: C.value, bold: true },
|
|
598
|
+
{ text: domain.summary, fg: domainColor(domain.level) },
|
|
599
|
+
{ text: domain.next, fg: C.dim },
|
|
600
|
+
],
|
|
601
|
+
[
|
|
602
|
+
{ width: 14 },
|
|
603
|
+
{ width: Math.max(10, width - 38) },
|
|
604
|
+
{ width: 20 },
|
|
605
|
+
],
|
|
606
|
+
));
|
|
602
607
|
for (const detail of domain.details.slice(0, 2)) {
|
|
603
608
|
domainLines.push(...buildBodyText(width, ` ${detail}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.dim));
|
|
604
609
|
}
|
|
@@ -672,26 +677,63 @@ export class ProviderHealthPanel extends BasePanel {
|
|
|
672
677
|
const domainsSection: PanelWorkspaceSection = { title: 'Repair Domains', lines: domainLines };
|
|
673
678
|
const maintenanceSections = maintenanceLines.length > 0 ? [{ title: 'Session Maintenance', lines: maintenanceLines } satisfies PanelWorkspaceSection] : [];
|
|
674
679
|
const selectedSections = selectedLines.length > 0 ? [{ lines: buildDetailBlock(width, 'Selected provider', selectedLines, { ...DEFAULT_PANEL_PALETTE, header: C.title }) } satisfies PanelWorkspaceSection] : [];
|
|
680
|
+
const footerHint = buildKeyboardHints(width, [
|
|
681
|
+
{ keys: 'j/k or Up/Down', label: 'select provider' },
|
|
682
|
+
{ keys: '/provider', label: 'switch model' },
|
|
683
|
+
{ keys: '/accounts', label: 'auth routes' },
|
|
684
|
+
], { ...DEFAULT_PANEL_PALETTE, header: C.title });
|
|
685
|
+
const providerColumnHeader = buildAlignedRow(
|
|
686
|
+
width,
|
|
687
|
+
[
|
|
688
|
+
{ text: 'provider', fg: C.label, bold: true },
|
|
689
|
+
{ text: 'status', fg: C.label, bold: true },
|
|
690
|
+
{ text: 'lat', fg: C.label, bold: true },
|
|
691
|
+
{ text: 'last ok', fg: C.label, bold: true },
|
|
692
|
+
{ text: 'auth', fg: C.label, bold: true },
|
|
693
|
+
],
|
|
694
|
+
[
|
|
695
|
+
{ width: 18 },
|
|
696
|
+
{ width: 13 },
|
|
697
|
+
{ width: 8, align: 'right' },
|
|
698
|
+
{ width: 10, align: 'right' },
|
|
699
|
+
{ width: 12 },
|
|
700
|
+
],
|
|
701
|
+
{ marker: '▸' },
|
|
702
|
+
);
|
|
675
703
|
const resolvedProvidersSection = resolvePrimaryScrollableSection(width, height, {
|
|
676
704
|
intro,
|
|
677
|
-
footerLines: [
|
|
705
|
+
footerLines: [footerHint],
|
|
678
706
|
palette: { ...DEFAULT_PANEL_PALETTE, header: C.title },
|
|
679
707
|
beforeSections: [postureSection, domainsSection, ...maintenanceSections],
|
|
680
708
|
section: {
|
|
681
709
|
title: 'Providers',
|
|
710
|
+
fixedLines: [providerColumnHeader],
|
|
682
711
|
scrollableLines: providers.map((name, absolute) => {
|
|
683
712
|
const health = this.providerHealthTracker.get(name);
|
|
684
713
|
const status = health?.status ?? 'unknown';
|
|
714
|
+
const dot = statusDot(status);
|
|
685
715
|
const latency = health?.lastLatencyMs !== undefined ? fmtMs(health.lastLatencyMs) : 'n/a';
|
|
686
716
|
const latencyFg = health?.lastLatencyMs !== undefined ? latencyColor(health.lastLatencyMs) : C.dim;
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
717
|
+
const account = this._accountRecords.get(name);
|
|
718
|
+
const authFg = account ? freshnessColor(account.authFreshness) : C.dim;
|
|
719
|
+
return buildAlignedRow(
|
|
720
|
+
width,
|
|
721
|
+
[
|
|
722
|
+
{ text: `${dot.char} ${name}`, fg: C.value, bold: absolute === this._selectedIndex },
|
|
723
|
+
{ text: statusLabel(status), fg: dot.color },
|
|
724
|
+
{ text: latency, fg: latencyFg },
|
|
725
|
+
{ text: fmtAgo(health?.lastSuccessAt), fg: C.value },
|
|
726
|
+
{ text: account ? account.authFreshness : '-', fg: authFg },
|
|
727
|
+
],
|
|
728
|
+
[
|
|
729
|
+
{ width: 18 },
|
|
730
|
+
{ width: 13 },
|
|
731
|
+
{ width: 8, align: 'right' },
|
|
732
|
+
{ width: 10, align: 'right' },
|
|
733
|
+
{ width: 12 },
|
|
734
|
+
],
|
|
735
|
+
{ selected: absolute === this._selectedIndex, selectedBg: C.rowSelectBg, marker: '▸' },
|
|
736
|
+
);
|
|
695
737
|
}),
|
|
696
738
|
selectedIndex: this._selectedIndex,
|
|
697
739
|
scrollOffset: this._scrollOffset,
|
|
@@ -713,7 +755,7 @@ export class ProviderHealthPanel extends BasePanel {
|
|
|
713
755
|
title: 'Health',
|
|
714
756
|
intro,
|
|
715
757
|
sections,
|
|
716
|
-
footerLines: [
|
|
758
|
+
footerLines: [footerHint],
|
|
717
759
|
palette: { ...DEFAULT_PANEL_PALETTE, header: C.title },
|
|
718
760
|
});
|
|
719
761
|
}
|