@pellux/goodvibes-tui 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +124 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +60 -26
- package/src/input/handler-feed.ts +17 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +53 -8
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +117 -1
- package/src/main.ts +5 -2
- package/src/panels/agent-inspector-panel.ts +139 -45
- package/src/panels/agent-inspector-shared.ts +147 -3
- package/src/panels/approval-panel.ts +204 -102
- package/src/panels/automation-control-panel.ts +370 -103
- package/src/panels/base-panel.ts +26 -2
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +253 -75
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +159 -56
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +354 -95
- package/src/panels/cost-tracker-panel.ts +243 -57
- package/src/panels/debug-panel.ts +347 -159
- package/src/panels/diff-panel.ts +257 -82
- package/src/panels/docs-panel.ts +166 -64
- package/src/panels/eval-panel.ts +336 -109
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +238 -159
- package/src/panels/file-preview-panel.ts +141 -59
- package/src/panels/git-panel.ts +360 -191
- package/src/panels/hooks-panel.ts +181 -19
- package/src/panels/incident-review-panel.ts +252 -43
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +310 -103
- package/src/panels/knowledge-graph-panel.ts +480 -58
- package/src/panels/local-auth-panel.ts +251 -33
- package/src/panels/marketplace-panel.ts +219 -33
- package/src/panels/memory-panel.ts +83 -81
- package/src/panels/ops-control-panel.ts +211 -32
- package/src/panels/ops-strategy-panel.ts +131 -45
- package/src/panels/orchestration-panel.ts +259 -67
- package/src/panels/panel-list-panel.ts +184 -136
- package/src/panels/panel-manager.ts +120 -13
- package/src/panels/plan-dashboard-panel.ts +507 -74
- package/src/panels/plugins-panel.ts +227 -34
- package/src/panels/policy-panel.ts +264 -55
- package/src/panels/polish-core.ts +162 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +67 -149
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +84 -113
- package/src/panels/provider-health-panel.ts +438 -480
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +136 -38
- package/src/panels/remote-panel.ts +123 -38
- package/src/panels/routes-panel.ts +87 -24
- package/src/panels/sandbox-panel.ts +232 -65
- package/src/panels/scrollable-list-panel.ts +143 -145
- package/src/panels/security-panel.ts +204 -52
- package/src/panels/services-panel.ts +174 -76
- package/src/panels/session-browser-panel.ts +101 -6
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +346 -68
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +105 -34
- package/src/panels/symbol-outline-panel.ts +287 -154
- package/src/panels/system-messages-panel.ts +172 -38
- package/src/panels/tasks-panel.ts +348 -134
- package/src/panels/thinking-panel.ts +66 -32
- package/src/panels/token-budget-panel.ts +233 -46
- package/src/panels/tool-inspector-panel.ts +170 -54
- package/src/panels/types.ts +65 -4
- package/src/panels/work-plan-panel.ts +393 -39
- package/src/panels/worktree-panel.ts +239 -61
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +146 -150
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +22 -16
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +30 -11
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +8 -6
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +41 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +45 -36
- package/src/renderer/ui-primitives.ts +23 -2
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/utils/splash-lines.ts +44 -3
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -666
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -214
- package/src/panels/forensics-panel.ts +0 -364
- package/src/panels/panel-picker.ts +0 -106
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -218
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/schedule-panel.ts +0 -342
- package/src/panels/watchers-panel.ts +0 -193
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -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,
|
|
@@ -568,16 +447,26 @@ export function resolveScrollablePanelSection(
|
|
|
568
447
|
afterSections: options.afterSections,
|
|
569
448
|
minRows: options.section.minRows ?? 1,
|
|
570
449
|
});
|
|
450
|
+
// When a window-summary row will render (list longer than the budget can
|
|
451
|
+
// show), it occupies one of the section's rows. Reserve that row up front so
|
|
452
|
+
// the summary is accounted INSIDE the budget; otherwise the appended summary
|
|
453
|
+
// pushes total output one line past the panel's exact-height contract and the
|
|
454
|
+
// tail line (typically the footer's keyboard-hints row) is silently dropped.
|
|
455
|
+
const summaryReserved = options.section.appendWindowSummary
|
|
456
|
+
&& options.section.scrollableLines.length > budget
|
|
457
|
+
? 1
|
|
458
|
+
: 0;
|
|
459
|
+
const windowBudget = Math.max(0, budget - summaryReserved);
|
|
571
460
|
const window = options.section.selectedIndex === undefined
|
|
572
461
|
? getVisibleWindow(
|
|
573
462
|
options.section.scrollableLines.length,
|
|
574
463
|
options.section.scrollOffset,
|
|
575
|
-
|
|
464
|
+
windowBudget,
|
|
576
465
|
)
|
|
577
466
|
: getTrackedVisibleWindow(
|
|
578
467
|
options.section.scrollableLines.length,
|
|
579
468
|
options.section.selectedIndex,
|
|
580
|
-
|
|
469
|
+
windowBudget,
|
|
581
470
|
options.section.scrollOffset,
|
|
582
471
|
options.section.guardRows ?? 1,
|
|
583
472
|
);
|
|
@@ -650,12 +539,21 @@ export function resolveStackedScrollableSections(
|
|
|
650
539
|
|
|
651
540
|
return options.sections.map((section, index) => {
|
|
652
541
|
const fixedLines = [...(section.fixedLines ?? [])];
|
|
542
|
+
// Same reservation as resolveScrollablePanelSection: a window-summary row
|
|
543
|
+
// occupies one of this section's allocated rows, so shrink the visible
|
|
544
|
+
// window rather than overflowing the stack's exact-height budget.
|
|
545
|
+
const budget = budgets[index]!;
|
|
546
|
+
const summaryReserved = section.appendWindowSummary
|
|
547
|
+
&& section.scrollableLines.length > budget
|
|
548
|
+
? 1
|
|
549
|
+
: 0;
|
|
550
|
+
const windowBudget = Math.max(0, budget - summaryReserved);
|
|
653
551
|
const window = section.selectedIndex === undefined
|
|
654
|
-
? getVisibleWindow(section.scrollableLines.length, section.scrollOffset,
|
|
552
|
+
? getVisibleWindow(section.scrollableLines.length, section.scrollOffset, windowBudget)
|
|
655
553
|
: getTrackedVisibleWindow(
|
|
656
554
|
section.scrollableLines.length,
|
|
657
555
|
section.selectedIndex,
|
|
658
|
-
|
|
556
|
+
windowBudget,
|
|
659
557
|
section.scrollOffset,
|
|
660
558
|
section.guardRows ?? 1,
|
|
661
559
|
);
|
|
@@ -680,6 +578,26 @@ export function resolveStackedScrollableSections(
|
|
|
680
578
|
});
|
|
681
579
|
}
|
|
682
580
|
|
|
581
|
+
/**
|
|
582
|
+
* Standardized footer keyboard-hint row so every panel's hints look identical.
|
|
583
|
+
* Each hint renders as `keys` (info color) + `label` (dim).
|
|
584
|
+
*/
|
|
585
|
+
export function buildKeyboardHints(
|
|
586
|
+
width: number,
|
|
587
|
+
hints: ReadonlyArray<{ keys: string; label: string }>,
|
|
588
|
+
palette: PanelPalette,
|
|
589
|
+
): Line {
|
|
590
|
+
const segments: Array<[string, string, string?]> = [];
|
|
591
|
+
hints.forEach((hint, i) => {
|
|
592
|
+
if (i > 0) segments.push([' ', palette.dim]);
|
|
593
|
+
else segments.push([' ', palette.dim]);
|
|
594
|
+
segments.push([hint.keys, palette.info]);
|
|
595
|
+
segments.push([' ', palette.dim]);
|
|
596
|
+
segments.push([hint.label, palette.dim]);
|
|
597
|
+
});
|
|
598
|
+
return buildPanelLine(width, segments);
|
|
599
|
+
}
|
|
600
|
+
|
|
683
601
|
export function buildPanelWorkspace(
|
|
684
602
|
width: number,
|
|
685
603
|
height: number,
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { ProjectPlanningQuestion } from '@pellux/goodvibes-sdk/platform/knowledge';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Answer-action helpers for the Project Planning panel.
|
|
5
|
+
//
|
|
6
|
+
// Extracted from project-planning-panel.ts to keep that module under the
|
|
7
|
+
// architecture line-count cap. Leaf module (only the SDK question type);
|
|
8
|
+
// the panel is their public import site.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
export interface PlanningAnswerAction {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly label: string;
|
|
14
|
+
readonly detail: string;
|
|
15
|
+
readonly answer: string;
|
|
16
|
+
readonly kind?: 'answer' | 'approve' | 'dismiss';
|
|
17
|
+
readonly disabled?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** SDK placeholder recommendations that carry no real signal — never surfaced as an answer action. */
|
|
21
|
+
export function isGenericRecommendation(value: string): boolean {
|
|
22
|
+
return /\bdefine the first-pass scope\b/i.test(value)
|
|
23
|
+
|| /\bcreate task records\b/i.test(value)
|
|
24
|
+
|| /\brecord concrete tests\b/i.test(value)
|
|
25
|
+
|| /\bseparate out-of-scope work\b/i.test(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function compactAnswerDetail(text: string): string {
|
|
29
|
+
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
30
|
+
return normalized.length > 86 ? `${normalized.slice(0, 83)}...` : normalized;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Builds the answer-action list for the current question: keyword-matched
|
|
35
|
+
* canned suggestions first (de-duplicated by normalized answer text — a
|
|
36
|
+
* question that matches more than one keyword category, e.g. scope +
|
|
37
|
+
* recommendedAnswer echoing one of the scope answers verbatim, must not show
|
|
38
|
+
* the same suggested answer twice), then the fixed ask-narrower/custom/
|
|
39
|
+
* dismiss actions.
|
|
40
|
+
*/
|
|
41
|
+
export function buildAnswerActions(question: ProjectPlanningQuestion, draftAnswer: string): PlanningAnswerAction[] {
|
|
42
|
+
const canned: PlanningAnswerAction[] = [];
|
|
43
|
+
const prompt = question.prompt.toLowerCase();
|
|
44
|
+
const isScopeQuestion = prompt.includes('scope') || prompt.includes('in or out');
|
|
45
|
+
const isTaskQuestion = prompt.includes('task') || prompt.includes('dependency') || prompt.includes('work breakdown');
|
|
46
|
+
const isVerificationQuestion = prompt.includes('verification') || prompt.includes('test') || prompt.includes('prove');
|
|
47
|
+
const isApprovalQuestion = prompt.includes('approved') || prompt.includes('approve') || prompt.includes('execution');
|
|
48
|
+
if (isApprovalQuestion) {
|
|
49
|
+
canned.push({
|
|
50
|
+
id: 'approve-execution',
|
|
51
|
+
label: 'Approve execution',
|
|
52
|
+
detail: 'Mark this plan approved so execution may proceed.',
|
|
53
|
+
answer: 'Approve this planning state for execution.',
|
|
54
|
+
kind: 'approve',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (isScopeQuestion) {
|
|
58
|
+
canned.push({
|
|
59
|
+
id: 'scope-focused-first-pass',
|
|
60
|
+
label: 'Use focused first-pass scope',
|
|
61
|
+
detail: 'Fill a concrete end-to-end scope for this goal and keep unrelated work out.',
|
|
62
|
+
answer: 'Use a focused first-pass scope for this goal.',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (isTaskQuestion) {
|
|
66
|
+
canned.push({
|
|
67
|
+
id: 'tasks-default-breakdown',
|
|
68
|
+
label: 'Create default task breakdown',
|
|
69
|
+
detail: 'Create inspect, implement, wire, and verify tasks with dependencies.',
|
|
70
|
+
answer: 'Create the default task breakdown for this goal.',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (isVerificationQuestion) {
|
|
74
|
+
canned.push({
|
|
75
|
+
id: 'verification-default-gates',
|
|
76
|
+
label: 'Use standard verification gates',
|
|
77
|
+
detail: 'Require focused regression coverage, typecheck/build validation, and a runtime smoke where feasible.',
|
|
78
|
+
answer: 'Use standard verification gates for this goal.',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (question.recommendedAnswer?.trim() && !isGenericRecommendation(question.recommendedAnswer)) {
|
|
82
|
+
canned.push({
|
|
83
|
+
id: 'recommended',
|
|
84
|
+
label: 'Use recommended answer',
|
|
85
|
+
detail: compactAnswerDetail(question.recommendedAnswer),
|
|
86
|
+
answer: question.recommendedAnswer,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (isScopeQuestion) {
|
|
90
|
+
canned.push({
|
|
91
|
+
id: 'scope-end-to-end',
|
|
92
|
+
label: 'End-to-end required scope',
|
|
93
|
+
detail: 'Let the plan include every component needed to make this work, but avoid unrelated cleanup.',
|
|
94
|
+
answer: 'Scope is everything required to make the requested outcome work end-to-end. Include TUI, daemon composition, configuration, docs, and tests if they are required. Do not include unrelated cleanup or broad refactors unless they are necessary for this task.',
|
|
95
|
+
});
|
|
96
|
+
canned.push({
|
|
97
|
+
id: 'scope-tui-first',
|
|
98
|
+
label: 'TUI-first scope',
|
|
99
|
+
detail: 'Fix TUI behavior here; report SDK blockers instead of patching around SDK-owned bugs.',
|
|
100
|
+
answer: 'Scope is TUI-owned behavior first. If a blocker is SDK-owned, report the exact SDK contract/runtime issue instead of patching around it in the TUI. Include daemon composition only where the TUI owns the wiring.',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const seenAnswers = new Set<string>();
|
|
105
|
+
const actions: PlanningAnswerAction[] = [];
|
|
106
|
+
for (const action of canned) {
|
|
107
|
+
const key = action.answer.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
108
|
+
if (key.length > 0 && seenAnswers.has(key)) continue;
|
|
109
|
+
if (key.length > 0) seenAnswers.add(key);
|
|
110
|
+
actions.push(action);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
actions.push({
|
|
114
|
+
id: 'ask-narrower',
|
|
115
|
+
label: 'I am not sure yet',
|
|
116
|
+
detail: 'Break this into smaller concrete choices with examples and a recommended default.',
|
|
117
|
+
answer: `I do not know enough to answer "${question.prompt}" as asked. Break it into smaller concrete questions with 2-4 specific choices, explain the tradeoffs, recommend a default, and ask me the first one.`,
|
|
118
|
+
});
|
|
119
|
+
actions.push({
|
|
120
|
+
id: 'custom',
|
|
121
|
+
label: 'Submit typed answer',
|
|
122
|
+
detail: draftAnswer ? compactAnswerDetail(draftAnswer) : 'Type an answer first; this row becomes the custom answer.',
|
|
123
|
+
answer: draftAnswer.trim(),
|
|
124
|
+
disabled: !draftAnswer.trim(),
|
|
125
|
+
});
|
|
126
|
+
actions.push({
|
|
127
|
+
id: 'dismiss-planning',
|
|
128
|
+
label: 'Close planning and continue without it',
|
|
129
|
+
detail: 'Pause project planning for this workspace. Normal chat continues; /plan can reopen it later.',
|
|
130
|
+
answer: 'Pause project planning for this workspace and continue without the planning panel.',
|
|
131
|
+
kind: 'dismiss',
|
|
132
|
+
});
|
|
133
|
+
return actions;
|
|
134
|
+
}
|