@pellux/goodvibes-tui 0.26.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 +40 -0
- package/README.md +12 -7
- package/docs/foundation-artifacts/operator-contract.json +4 -1
- package/package.json +2 -2
- package/src/cli/bundle-command.ts +2 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation-rendering.ts +2 -2
- package/src/core/conversation.ts +53 -8
- package/src/core/session-recovery.ts +26 -18
- package/src/core/system-message-router.ts +42 -26
- package/src/core/turn-event-wiring.ts +13 -16
- package/src/daemon/handlers/calendar/caldav-client.ts +2 -1
- package/src/daemon/handlers/inbox/index.ts +2 -1
- package/src/daemon/handlers/inbox/poller.ts +2 -1
- package/src/daemon/handlers/inbox/providers/discord.ts +2 -1
- package/src/daemon/handlers/inbox/providers/email.ts +2 -1
- package/src/daemon/handlers/inbox/providers/route-util.ts +2 -1
- package/src/daemon/handlers/inbox/providers/slack.ts +2 -1
- package/src/daemon/handlers/triage/integration.ts +2 -1
- package/src/daemon/handlers/triage/pipeline.ts +2 -1
- package/src/daemon/handlers/triage/tagger/discord.ts +2 -1
- package/src/daemon/handlers/triage/tagger/imap.ts +4 -3
- package/src/export/gist-uploader.ts +3 -1
- package/src/input/command-registry.ts +1 -0
- package/src/input/commands/cloudflare-runtime.ts +2 -1
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/guidance-runtime.ts +2 -4
- package/src/input/commands/health-runtime.ts +13 -7
- package/src/input/commands/knowledge.ts +2 -1
- package/src/input/commands/runtime-services.ts +40 -10
- package/src/input/handler-command-route.ts +1 -1
- package/src/input/handler-feed-routes.ts +61 -4
- package/src/input/handler-feed.ts +13 -0
- package/src/input/handler-interactions.ts +2 -1
- package/src/input/handler-modal-routes.ts +2 -1
- package/src/input/handler-onboarding-cloudflare.ts +2 -1
- package/src/input/handler-onboarding.ts +8 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-ui-state.ts +5 -1
- 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/input/tts-settings-actions.ts +2 -1
- package/src/main.ts +52 -60
- package/src/panels/agent-inspector-panel.ts +90 -15
- package/src/panels/agent-inspector-shared.ts +3 -3
- package/src/panels/agent-logs-panel.ts +149 -72
- package/src/panels/approval-panel.ts +146 -95
- package/src/panels/automation-control-panel.ts +24 -1
- package/src/panels/base-panel.ts +28 -3
- package/src/panels/builtin/agent.ts +3 -1
- package/src/panels/builtin/development.ts +2 -1
- package/src/panels/builtin/session.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 +49 -15
- package/src/panels/control-plane-panel.ts +27 -1
- package/src/panels/cost-tracker-panel.ts +87 -65
- package/src/panels/debug-panel.ts +61 -37
- package/src/panels/diff-panel.ts +59 -30
- package/src/panels/docs-panel.ts +30 -24
- package/src/panels/eval-panel.ts +130 -81
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +42 -42
- package/src/panels/file-preview-panel.ts +11 -3
- package/src/panels/forensics-panel.ts +27 -13
- package/src/panels/git-panel.ts +65 -84
- 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 +33 -13
- package/src/panels/memory-panel.ts +7 -5
- 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 +27 -4
- package/src/panels/provider-accounts-panel.ts +55 -18
- package/src/panels/provider-health-panel.ts +118 -87
- 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 +44 -24
- 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 +42 -26
- package/src/panels/session-maintenance.ts +2 -2
- package/src/panels/settings-sync-panel.ts +30 -15
- package/src/panels/skills-panel.ts +2 -1
- 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 +36 -14
- package/src/panels/thinking-panel.ts +32 -36
- package/src/panels/token-budget-panel.ts +80 -53
- package/src/panels/tool-inspector-panel.ts +37 -39
- package/src/panels/types.ts +25 -0
- package/src/panels/watchers-panel.ts +25 -5
- package/src/panels/work-plan-panel.ts +127 -35
- package/src/panels/worktree-panel.ts +65 -20
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +53 -147
- package/src/renderer/agent-detail-modal.ts +2 -2
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/modal-utils.ts +0 -10
- 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/process-modal.ts +2 -2
- 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/bootstrap-command-context.ts +3 -0
- package/src/runtime/bootstrap-command-parts.ts +3 -1
- package/src/runtime/bootstrap-core.ts +31 -31
- package/src/runtime/bootstrap-shell.ts +1 -0
- package/src/runtime/onboarding/apply.ts +4 -3
- package/src/runtime/onboarding/snapshot.ts +4 -3
- package/src/runtime/process-lifecycle.ts +195 -0
- package/src/runtime/services.ts +4 -1
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/runtime/wrfc-persistence.ts +20 -5
- package/src/shell/ui-openers.ts +3 -2
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/verification/live-verifier.ts +4 -3
- package/src/version.ts +1 -1
- package/src/core/context-auto-compact.ts +0 -110
- 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
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type { Line } from '../types/grid.ts';
|
|
2
|
+
import { getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
|
+
import { GLYPHS, UI_TONES } from '../renderer/ui-primitives.ts';
|
|
4
|
+
// Build on the leaf primitives in ./polish-core.ts (palette + base line
|
|
5
|
+
// builders). polish-core has no back-edge to this file, so there is no cycle.
|
|
6
|
+
import { buildSelectablePanelLine, DEFAULT_PANEL_PALETTE } from './polish-core.ts';
|
|
7
|
+
import type { PanelPalette, StyledPanelSegment } from './polish-core.ts';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Shared formatting primitives (the toolkit panels standardize on).
|
|
11
|
+
//
|
|
12
|
+
// All are display-width aware (via getDisplayWidth / truncateDisplay) so they
|
|
13
|
+
// stay aligned across emoji, CJK, and other wide characters — never hand-roll
|
|
14
|
+
// .slice()/.padEnd() truncation or alignment in panels.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export type ColumnAlign = 'left' | 'right' | 'center';
|
|
18
|
+
|
|
19
|
+
export interface ColumnSpec {
|
|
20
|
+
/** Fixed display width of the column. */
|
|
21
|
+
readonly width: number;
|
|
22
|
+
/** Horizontal alignment within the column. Default 'left'. */
|
|
23
|
+
readonly align?: ColumnAlign;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AlignedCell {
|
|
27
|
+
readonly text: string;
|
|
28
|
+
readonly fg: string;
|
|
29
|
+
readonly bg?: string;
|
|
30
|
+
readonly bold?: boolean;
|
|
31
|
+
readonly dim?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Truncate + pad `text` to exactly `width` display columns with alignment. */
|
|
35
|
+
function alignText(text: string, width: number, align: ColumnAlign): string {
|
|
36
|
+
if (width <= 0) return '';
|
|
37
|
+
const truncated = truncateDisplay(text, width);
|
|
38
|
+
const pad = Math.max(0, width - getDisplayWidth(truncated));
|
|
39
|
+
if (pad === 0) return truncated;
|
|
40
|
+
if (align === 'right') return ' '.repeat(pad) + truncated;
|
|
41
|
+
if (align === 'center') {
|
|
42
|
+
const left = Math.floor(pad / 2);
|
|
43
|
+
return ' '.repeat(left) + truncated + ' '.repeat(pad - left);
|
|
44
|
+
}
|
|
45
|
+
return truncated + ' '.repeat(pad);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build a multi-column row with display-width-aware alignment. Replaces manual
|
|
50
|
+
* `.padEnd()`/`.padStart()` column math (which misaligns on wide chars).
|
|
51
|
+
*
|
|
52
|
+
* `cells` and `columns` are zip-aligned by index; extra cells are ignored.
|
|
53
|
+
*/
|
|
54
|
+
export function buildAlignedRow(
|
|
55
|
+
width: number,
|
|
56
|
+
cells: ReadonlyArray<AlignedCell>,
|
|
57
|
+
columns: ReadonlyArray<ColumnSpec>,
|
|
58
|
+
options: {
|
|
59
|
+
readonly gap?: number;
|
|
60
|
+
readonly selected?: boolean;
|
|
61
|
+
readonly selectedBg?: string;
|
|
62
|
+
readonly marker?: string;
|
|
63
|
+
readonly fillBg?: string;
|
|
64
|
+
} = {},
|
|
65
|
+
): Line {
|
|
66
|
+
const gap = options.gap ?? 1;
|
|
67
|
+
const segments: StyledPanelSegment[] = [];
|
|
68
|
+
const count = Math.min(cells.length, columns.length);
|
|
69
|
+
for (let i = 0; i < count; i++) {
|
|
70
|
+
const col = columns[i]!;
|
|
71
|
+
const cell = cells[i]!;
|
|
72
|
+
if (i > 0 && gap > 0) segments.push({ text: ' '.repeat(gap), fg: cell.fg, bg: cell.bg });
|
|
73
|
+
segments.push({
|
|
74
|
+
text: alignText(cell.text, col.width, col.align ?? 'left'),
|
|
75
|
+
fg: cell.fg,
|
|
76
|
+
bg: cell.bg,
|
|
77
|
+
bold: cell.bold,
|
|
78
|
+
dim: cell.dim,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return buildSelectablePanelLine(width, segments, {
|
|
82
|
+
selected: options.selected,
|
|
83
|
+
selectedBg: options.selectedBg,
|
|
84
|
+
fillBg: options.fillBg,
|
|
85
|
+
leadingMarker: options.marker,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type StatusBadgeKind =
|
|
90
|
+
| 'pending'
|
|
91
|
+
| 'running'
|
|
92
|
+
| 'completed'
|
|
93
|
+
| 'failed'
|
|
94
|
+
| 'cancelled'
|
|
95
|
+
| 'blocked'
|
|
96
|
+
| 'review';
|
|
97
|
+
|
|
98
|
+
interface BadgeSpec {
|
|
99
|
+
readonly glyph: string;
|
|
100
|
+
readonly fg: string;
|
|
101
|
+
readonly label: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const STATUS_BADGES: Record<StatusBadgeKind, BadgeSpec> = {
|
|
105
|
+
pending: { glyph: GLYPHS.status.pending, fg: DEFAULT_PANEL_PALETTE.info, label: 'Pending' },
|
|
106
|
+
running: { glyph: GLYPHS.status.active, fg: UI_TONES.state.active, label: 'Running' },
|
|
107
|
+
completed: { glyph: GLYPHS.status.success, fg: DEFAULT_PANEL_PALETTE.good, label: 'Completed' },
|
|
108
|
+
failed: { glyph: GLYPHS.status.failure, fg: DEFAULT_PANEL_PALETTE.bad, label: 'Failed' },
|
|
109
|
+
cancelled: { glyph: GLYPHS.status.skipped, fg: DEFAULT_PANEL_PALETTE.dim, label: 'Cancelled' },
|
|
110
|
+
blocked: { glyph: GLYPHS.status.blocked, fg: UI_TONES.state.blocked, label: 'Blocked' },
|
|
111
|
+
review: { glyph: GLYPHS.status.review, fg: DEFAULT_PANEL_PALETTE.info, label: 'Review' },
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Canonical status badge (glyph + label) with a consistent color per lifecycle
|
|
116
|
+
* state. Returns StyledPanelSegment[] — spread into a buildPanelLine call:
|
|
117
|
+
* buildPanelLine(width, [[' ', C.label], ...buildStatusBadge('running')])
|
|
118
|
+
* Supersedes ad-hoc per-panel status→color maps.
|
|
119
|
+
*/
|
|
120
|
+
export function buildStatusBadge(
|
|
121
|
+
kind: StatusBadgeKind,
|
|
122
|
+
label?: string,
|
|
123
|
+
opts?: { glyph?: string; bg?: string; count?: number },
|
|
124
|
+
): StyledPanelSegment[] {
|
|
125
|
+
const spec = STATUS_BADGES[kind];
|
|
126
|
+
const glyph = opts?.glyph ?? spec.glyph;
|
|
127
|
+
const text = label ?? spec.label;
|
|
128
|
+
const rendered = opts?.count !== undefined ? `${glyph} ${text} (${opts.count})` : `${glyph} ${text}`;
|
|
129
|
+
return [{ text: rendered, fg: spec.fg, bg: opts?.bg }];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface TreeItemSpec {
|
|
133
|
+
readonly depth: number;
|
|
134
|
+
readonly label: string;
|
|
135
|
+
readonly icon?: string;
|
|
136
|
+
readonly expandable?: boolean;
|
|
137
|
+
readonly expanded?: boolean;
|
|
138
|
+
readonly labelColor?: string;
|
|
139
|
+
/** Right-aligned metadata columns (e.g. size, mtime). */
|
|
140
|
+
readonly metadata?: ReadonlyArray<{ text: string; fg: string }>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Build a hierarchical tree row: indentation + expand/collapse glyph + optional
|
|
145
|
+
* icon + label, with right-aligned metadata. Replaces hand-rolled indentation
|
|
146
|
+
* in file-explorer / symbol-outline / orchestration panels.
|
|
147
|
+
*/
|
|
148
|
+
export function buildTreeRow(
|
|
149
|
+
width: number,
|
|
150
|
+
item: TreeItemSpec,
|
|
151
|
+
palette: PanelPalette,
|
|
152
|
+
options: { selected?: boolean; selectedBg?: string; indentWidth?: number } = {},
|
|
153
|
+
): Line {
|
|
154
|
+
const indent = ' '.repeat(Math.max(0, item.depth) * (options.indentWidth ?? 2));
|
|
155
|
+
const toggle = item.expandable
|
|
156
|
+
? (item.expanded ? GLYPHS.navigation.expanded : GLYPHS.navigation.collapsed)
|
|
157
|
+
: ' ';
|
|
158
|
+
const iconPart = item.icon ? `${item.icon} ` : '';
|
|
159
|
+
const prefix = `${indent}${toggle} ${iconPart}`;
|
|
160
|
+
const prefixW = getDisplayWidth(prefix);
|
|
161
|
+
|
|
162
|
+
const metadata = item.metadata ?? [];
|
|
163
|
+
const metaText = metadata.map((m) => m.text).join(' ');
|
|
164
|
+
const metaW = getDisplayWidth(metaText);
|
|
165
|
+
const reserve = metaW > 0 ? metaW + 1 : 0;
|
|
166
|
+
|
|
167
|
+
const labelBudget = Math.max(0, width - prefixW - reserve);
|
|
168
|
+
const label = truncateDisplay(item.label, labelBudget);
|
|
169
|
+
const usedW = prefixW + getDisplayWidth(label);
|
|
170
|
+
|
|
171
|
+
const segments: StyledPanelSegment[] = [
|
|
172
|
+
{ text: prefix, fg: palette.dim },
|
|
173
|
+
{ text: label, fg: item.labelColor ?? palette.value },
|
|
174
|
+
];
|
|
175
|
+
if (metaW > 0) {
|
|
176
|
+
const spacer = Math.max(1, width - usedW - metaW);
|
|
177
|
+
segments.push({ text: ' '.repeat(spacer), fg: palette.dim });
|
|
178
|
+
metadata.forEach((m, i) => {
|
|
179
|
+
if (i > 0) segments.push({ text: ' ', fg: palette.dim });
|
|
180
|
+
segments.push({ text: m.text, fg: m.fg });
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return buildSelectablePanelLine(width, segments, {
|
|
184
|
+
selected: options.selected,
|
|
185
|
+
selectedBg: options.selectedBg ?? palette.selectBg,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface TableColumn {
|
|
190
|
+
readonly label: string;
|
|
191
|
+
/** Fixed width; omit to auto-distribute remaining space. */
|
|
192
|
+
readonly width?: number;
|
|
193
|
+
readonly align?: ColumnAlign;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface TableCell {
|
|
197
|
+
readonly text: string;
|
|
198
|
+
readonly fg?: string;
|
|
199
|
+
readonly bg?: string;
|
|
200
|
+
readonly bold?: boolean;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface TableRow {
|
|
204
|
+
readonly cells: ReadonlyArray<TableCell>;
|
|
205
|
+
readonly selected?: boolean;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Resolve concrete per-column widths, auto-distributing any unsized columns. */
|
|
209
|
+
function resolveColumnWidths(width: number, columns: ReadonlyArray<TableColumn>, gap: number): number[] {
|
|
210
|
+
const totalGap = gap * Math.max(0, columns.length - 1);
|
|
211
|
+
const explicit = columns.reduce((s, c) => s + (c.width ?? 0), 0);
|
|
212
|
+
const autoIdx = columns.map((c, i) => (c.width === undefined ? i : -1)).filter((i) => i >= 0);
|
|
213
|
+
const autoBudget = Math.max(0, width - totalGap - explicit);
|
|
214
|
+
const each = autoIdx.length > 0 ? Math.floor(autoBudget / autoIdx.length) : 0;
|
|
215
|
+
const widths = columns.map((c) => c.width ?? each);
|
|
216
|
+
if (autoIdx.length > 0) {
|
|
217
|
+
const used = each * autoIdx.length;
|
|
218
|
+
widths[autoIdx[autoIdx.length - 1]!] += autoBudget - used; // remainder to last auto col
|
|
219
|
+
}
|
|
220
|
+
return widths;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Build a table: a header row plus aligned data rows. Replaces ad-hoc
|
|
225
|
+
* row/column offset math in history/ledger/roster panels.
|
|
226
|
+
*/
|
|
227
|
+
export function buildTable(
|
|
228
|
+
width: number,
|
|
229
|
+
columns: ReadonlyArray<TableColumn>,
|
|
230
|
+
rows: ReadonlyArray<TableRow>,
|
|
231
|
+
palette: PanelPalette,
|
|
232
|
+
options: { gap?: number; selectedBg?: string } = {},
|
|
233
|
+
): Line[] {
|
|
234
|
+
const gap = options.gap ?? 1;
|
|
235
|
+
const widths = resolveColumnWidths(width, columns, gap);
|
|
236
|
+
const specs: ColumnSpec[] = columns.map((c, i) => ({ width: widths[i]!, align: c.align }));
|
|
237
|
+
|
|
238
|
+
const header = buildAlignedRow(
|
|
239
|
+
width,
|
|
240
|
+
columns.map((c) => ({ text: c.label, fg: palette.label, bold: true })),
|
|
241
|
+
specs,
|
|
242
|
+
{ gap },
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const dataRows = rows.map((row) =>
|
|
246
|
+
buildAlignedRow(
|
|
247
|
+
width,
|
|
248
|
+
columns.map((_, i) => {
|
|
249
|
+
const cell = row.cells[i] ?? { text: '', fg: palette.value };
|
|
250
|
+
return { text: cell.text, fg: cell.fg ?? palette.value, bg: cell.bg, bold: cell.bold };
|
|
251
|
+
}),
|
|
252
|
+
specs,
|
|
253
|
+
{ gap, selected: row.selected, selectedBg: options.selectedBg ?? palette.selectBg },
|
|
254
|
+
),
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
return [header, ...dataRows];
|
|
258
|
+
}
|
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,
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
resolveScrollablePanelSection,
|
|
24
24
|
type PanelWorkspaceSection,
|
|
25
25
|
} from './polish.ts';
|
|
26
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
26
27
|
|
|
27
28
|
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
28
29
|
planning: '#38bdf8',
|
|
@@ -366,12 +367,34 @@ export class ProjectPlanningPanel extends BasePanel {
|
|
|
366
367
|
: readiness === 'needs-user-input'
|
|
367
368
|
? C.blocked
|
|
368
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';
|
|
369
383
|
const lines: Line[] = [
|
|
370
384
|
buildKeyValueLine(width, [
|
|
371
385
|
{ label: 'readiness', value: readiness, valueColor: readinessColor },
|
|
372
386
|
{ label: 'approved', value: state.executionApproved ? 'yes' : 'no', valueColor: state.executionApproved ? C.approved : C.blocked },
|
|
373
387
|
{ label: 'questions', value: `${state.openQuestions.length} open / ${state.answeredQuestions.length} answered`, valueColor: C.value },
|
|
374
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
|
+
]),
|
|
375
398
|
...buildBodyText(width, `Goal: ${state.goal || '(not set)'}`, C, state.goal ? C.value : C.blocked),
|
|
376
399
|
];
|
|
377
400
|
if (state.scope) lines.push(...buildBodyText(width, `Scope: ${state.scope}`, C, C.value));
|
|
@@ -512,7 +535,7 @@ export class ProjectPlanningPanel extends BasePanel {
|
|
|
512
535
|
};
|
|
513
536
|
this.clearError();
|
|
514
537
|
} catch (err) {
|
|
515
|
-
this.setError(
|
|
538
|
+
this.setError(summarizeError(err));
|
|
516
539
|
} finally {
|
|
517
540
|
this.loading = false;
|
|
518
541
|
this.markDirty();
|
|
@@ -647,7 +670,7 @@ export class ProjectPlanningPanel extends BasePanel {
|
|
|
647
670
|
this.refresh(true);
|
|
648
671
|
this.registerTimer(setTimeout(() => this.refresh(true), 250));
|
|
649
672
|
} catch (err) {
|
|
650
|
-
this.setError(
|
|
673
|
+
this.setError(summarizeError(err));
|
|
651
674
|
this.requestRender();
|
|
652
675
|
}
|
|
653
676
|
})();
|
|
@@ -698,7 +721,7 @@ export class ProjectPlanningPanel extends BasePanel {
|
|
|
698
721
|
this.dismissPlanning?.();
|
|
699
722
|
this.refresh(true);
|
|
700
723
|
} catch (err) {
|
|
701
|
-
this.setError(
|
|
724
|
+
this.setError(summarizeError(err));
|
|
702
725
|
this.requestRender();
|
|
703
726
|
}
|
|
704
727
|
})();
|
|
@@ -741,7 +764,7 @@ export class ProjectPlanningPanel extends BasePanel {
|
|
|
741
764
|
});
|
|
742
765
|
this.refresh(true);
|
|
743
766
|
} catch (err) {
|
|
744
|
-
this.setError(
|
|
767
|
+
this.setError(summarizeError(err));
|
|
745
768
|
this.requestRender();
|
|
746
769
|
}
|
|
747
770
|
})();
|
|
@@ -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,
|