@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
|
@@ -1,7 +1,32 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import {
|
|
2
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
3
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
|
+
import {
|
|
5
|
+
buildDetailBlock,
|
|
6
|
+
buildGuidanceLine,
|
|
7
|
+
buildKeyboardHints,
|
|
8
|
+
buildKeyValueLine,
|
|
9
|
+
buildMeterLine,
|
|
10
|
+
buildPanelLine,
|
|
11
|
+
buildPanelListRow,
|
|
12
|
+
buildSummaryBlock,
|
|
13
|
+
DEFAULT_PANEL_PALETTE,
|
|
14
|
+
extendPalette,
|
|
15
|
+
type PanelPalette,
|
|
16
|
+
} from './polish.ts';
|
|
4
17
|
import type { WorkPlanItem, WorkPlanItemStatus, WorkPlanStore } from '../work-plans/work-plan-store.ts';
|
|
18
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
19
|
+
|
|
20
|
+
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
21
|
+
pending: '#94a3b8',
|
|
22
|
+
inProgress: '#38bdf8',
|
|
23
|
+
blocked: '#f59e0b',
|
|
24
|
+
done: '#22c55e',
|
|
25
|
+
failed: '#ef4444',
|
|
26
|
+
cancelled: '#64748b',
|
|
27
|
+
accent: '#a5b4fc',
|
|
28
|
+
selectBg: '#1e293b',
|
|
29
|
+
});
|
|
5
30
|
|
|
6
31
|
const STATUS_LABEL: Record<WorkPlanItemStatus, string> = {
|
|
7
32
|
pending: '[ ]',
|
|
@@ -13,17 +38,15 @@ const STATUS_LABEL: Record<WorkPlanItemStatus, string> = {
|
|
|
13
38
|
};
|
|
14
39
|
|
|
15
40
|
const STATUS_COLOR: Record<WorkPlanItemStatus, string> = {
|
|
16
|
-
pending:
|
|
17
|
-
in_progress:
|
|
18
|
-
blocked:
|
|
19
|
-
done:
|
|
20
|
-
failed:
|
|
21
|
-
cancelled:
|
|
41
|
+
pending: C.pending,
|
|
42
|
+
in_progress: C.inProgress,
|
|
43
|
+
blocked: C.blocked,
|
|
44
|
+
done: C.done,
|
|
45
|
+
failed: C.failed,
|
|
46
|
+
cancelled: C.cancelled,
|
|
22
47
|
};
|
|
23
48
|
|
|
24
|
-
|
|
25
|
-
return UIFactory.stringToLine(text.padEnd(width).slice(0, width), width, style);
|
|
26
|
-
}
|
|
49
|
+
const STATUS_ORDER: WorkPlanItemStatus[] = ['pending', 'in_progress', 'blocked', 'done', 'failed', 'cancelled'];
|
|
27
50
|
|
|
28
51
|
function compactDate(value: number): string {
|
|
29
52
|
return new Date(value).toISOString().replace('T', ' ').slice(0, 16);
|
|
@@ -47,13 +70,16 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
|
|
|
47
70
|
this.refresh();
|
|
48
71
|
}
|
|
49
72
|
|
|
73
|
+
protected override getPalette(): PanelPalette {
|
|
74
|
+
return C;
|
|
75
|
+
}
|
|
76
|
+
|
|
50
77
|
render(width: number, height: number): Line[] {
|
|
51
78
|
this.refresh();
|
|
52
79
|
return this.renderList(width, height, {
|
|
53
80
|
title: 'Work Plan',
|
|
54
81
|
header: this.renderHeader(width),
|
|
55
82
|
footer: this.renderFooter(width),
|
|
56
|
-
emptyMessage: 'No work plan items yet',
|
|
57
83
|
});
|
|
58
84
|
}
|
|
59
85
|
|
|
@@ -98,7 +124,7 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
|
|
|
98
124
|
return super.handleInput(key);
|
|
99
125
|
}
|
|
100
126
|
} catch (error) {
|
|
101
|
-
this.setError(
|
|
127
|
+
this.setError(summarizeError(error));
|
|
102
128
|
return true;
|
|
103
129
|
}
|
|
104
130
|
}
|
|
@@ -107,10 +133,14 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
|
|
|
107
133
|
return this.items;
|
|
108
134
|
}
|
|
109
135
|
|
|
136
|
+
protected override getEmptyStateMessage(): string {
|
|
137
|
+
return ' No work plan items yet';
|
|
138
|
+
}
|
|
139
|
+
|
|
110
140
|
protected getEmptyStateActions(): Array<{ command: string; summary: string }> {
|
|
111
141
|
return [
|
|
112
|
-
{ command: '/work-plan add <title>', summary: 'add a persistent item' },
|
|
113
|
-
{ command: '/work-plan list', summary: 'print the current plan' },
|
|
142
|
+
{ command: '/work-plan add <title>', summary: 'add a persistent item that survives across sessions' },
|
|
143
|
+
{ command: '/work-plan list', summary: 'print the current plan to the shell' },
|
|
114
144
|
];
|
|
115
145
|
}
|
|
116
146
|
|
|
@@ -118,12 +148,14 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
|
|
|
118
148
|
const status = STATUS_LABEL[item.status];
|
|
119
149
|
const owner = item.owner ? ` @${item.owner}` : '';
|
|
120
150
|
const source = item.source ? ` (${item.source})` : '';
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
bold: selected || item.status === 'in_progress',
|
|
126
|
-
|
|
151
|
+
const reserve = status.length + owner.length + source.length + 4;
|
|
152
|
+
const title = truncateDisplay(item.title, Math.max(6, width - reserve));
|
|
153
|
+
return buildPanelListRow(width, [
|
|
154
|
+
{ text: `${status} `, fg: STATUS_COLOR[item.status], bold: item.status === 'in_progress' },
|
|
155
|
+
{ text: title, fg: selected ? C.value : C.label, bold: selected || item.status === 'in_progress' },
|
|
156
|
+
{ text: owner, fg: C.accent },
|
|
157
|
+
{ text: source, fg: C.dim },
|
|
158
|
+
], C, { selected, selectedBg: C.selectBg });
|
|
127
159
|
}
|
|
128
160
|
|
|
129
161
|
private setSelectedStatus(status: WorkPlanItemStatus): boolean {
|
|
@@ -146,30 +178,90 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
|
|
|
146
178
|
private renderHeader(width: number): Line[] {
|
|
147
179
|
const plan = this.store.getActivePlan();
|
|
148
180
|
const counts = new Map<WorkPlanItemStatus, number>();
|
|
149
|
-
for (const status of
|
|
181
|
+
for (const status of STATUS_ORDER) counts.set(status, 0);
|
|
150
182
|
for (const item of plan.items) counts.set(item.status, (counts.get(item.status) ?? 0) + 1);
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
183
|
+
|
|
184
|
+
const total = plan.items.length;
|
|
185
|
+
const done = (counts.get('done') ?? 0) + (counts.get('cancelled') ?? 0);
|
|
186
|
+
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
187
|
+
const meterWidth = Math.max(10, Math.min(26, width - 34));
|
|
188
|
+
|
|
189
|
+
const postureLines: Line[] = [
|
|
190
|
+
buildPanelLine(width, [
|
|
191
|
+
[' Project ', C.label],
|
|
192
|
+
[truncateDisplay(plan.projectRoot, Math.max(8, width - 11)), C.value],
|
|
193
|
+
]),
|
|
194
|
+
buildMeterLine(width, Math.round((pct / 100) * meterWidth), meterWidth, {
|
|
195
|
+
filled: pct === 100 ? C.done : C.inProgress,
|
|
196
|
+
empty: C.empty,
|
|
197
|
+
label: C.label,
|
|
198
|
+
}, { prefix: ' Progress ', suffix: ` ${done}/${total} (${pct}%) ` }),
|
|
199
|
+
buildKeyValueLine(width, [
|
|
200
|
+
{ label: 'pending', value: String(counts.get('pending') ?? 0), valueColor: (counts.get('pending') ?? 0) > 0 ? C.pending : C.dim },
|
|
201
|
+
{ label: 'active', value: String(counts.get('in_progress') ?? 0), valueColor: (counts.get('in_progress') ?? 0) > 0 ? C.inProgress : C.dim },
|
|
202
|
+
{ label: 'blocked', value: String(counts.get('blocked') ?? 0), valueColor: (counts.get('blocked') ?? 0) > 0 ? C.blocked : C.dim },
|
|
203
|
+
{ label: 'done', value: String(counts.get('done') ?? 0), valueColor: (counts.get('done') ?? 0) > 0 ? C.done : C.dim },
|
|
204
|
+
], C),
|
|
205
|
+
buildGuidanceLine(width, '/work-plan add <title>', 'append a persistent item to the active plan', C),
|
|
161
206
|
];
|
|
207
|
+
|
|
208
|
+
const header: Line[] = buildSummaryBlock(width, 'Persistent Work Plan', postureLines, C);
|
|
209
|
+
|
|
210
|
+
const active = this.items[this.selectedIndex];
|
|
162
211
|
if (active) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
212
|
+
const detailRows: Line[] = [
|
|
213
|
+
buildPanelLine(width, [
|
|
214
|
+
[' ', C.label],
|
|
215
|
+
[STATUS_LABEL[active.status], STATUS_COLOR[active.status]],
|
|
216
|
+
[' ', C.label],
|
|
217
|
+
[truncateDisplay(active.title, Math.max(8, width - 6)), C.value],
|
|
218
|
+
]),
|
|
219
|
+
buildKeyValueLine(width, [
|
|
220
|
+
{ label: 'status', value: statusName(active.status), valueColor: STATUS_COLOR[active.status] },
|
|
221
|
+
...(active.owner ? [{ label: 'owner', value: active.owner, valueColor: C.accent }] : []),
|
|
222
|
+
{ label: 'updated', value: compactDate(active.updatedAt), valueColor: C.dim },
|
|
223
|
+
], C),
|
|
224
|
+
];
|
|
225
|
+
if (active.source) {
|
|
226
|
+
detailRows.push(buildPanelLine(width, [[' source ', C.label], [active.source, C.info]]));
|
|
227
|
+
}
|
|
228
|
+
if (active.notes) {
|
|
229
|
+
detailRows.push(buildPanelLine(width, [
|
|
230
|
+
[' notes ', C.label],
|
|
231
|
+
[truncateDisplay(active.notes, Math.max(8, width - 8)), C.value],
|
|
232
|
+
]));
|
|
233
|
+
}
|
|
234
|
+
header.push(...buildDetailBlock(width, 'Selected item', detailRows, C));
|
|
166
235
|
}
|
|
167
236
|
return header;
|
|
168
237
|
}
|
|
169
238
|
|
|
170
239
|
private renderFooter(width: number): Line[] {
|
|
240
|
+
const hasItem = this.items.length > 0;
|
|
241
|
+
if (!hasItem) {
|
|
242
|
+
return [
|
|
243
|
+
buildKeyboardHints(width, [
|
|
244
|
+
{ keys: '↑/↓', label: 'navigate' },
|
|
245
|
+
], C),
|
|
246
|
+
];
|
|
247
|
+
}
|
|
171
248
|
return [
|
|
172
|
-
|
|
249
|
+
buildKeyboardHints(width, [
|
|
250
|
+
{ keys: this.items.length > 0 ? `${this.selectedIndex + 1}/${this.items.length}` : '0/0', label: 'item' },
|
|
251
|
+
{ keys: 'Enter', label: 'cycle status' },
|
|
252
|
+
{ keys: '1-6', label: 'set status' },
|
|
253
|
+
{ keys: 'd', label: 'delete' },
|
|
254
|
+
{ keys: 'c', label: 'clear done' },
|
|
255
|
+
{ keys: 'r', label: 'refresh' },
|
|
256
|
+
], C),
|
|
257
|
+
buildPanelLine(width, [
|
|
258
|
+
[' 1', C.info], [' pending ', C.dim],
|
|
259
|
+
['2', C.info], [' active ', C.dim],
|
|
260
|
+
['3', C.info], [' blocked ', C.dim],
|
|
261
|
+
['4', C.info], [' done ', C.dim],
|
|
262
|
+
['5', C.info], [' failed ', C.dim],
|
|
263
|
+
['6', C.info], [' cancelled', C.dim],
|
|
264
|
+
]),
|
|
173
265
|
];
|
|
174
266
|
}
|
|
175
267
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
+
import { basename } from 'node:path';
|
|
3
4
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
|
-
import { buildKeyValueLine, buildPanelLine, buildPanelWorkspace, DEFAULT_PANEL_PALETTE, resolvePrimaryScrollableSection, type PanelWorkspaceSection } from './polish.ts';
|
|
5
|
+
import { buildAlignedRow, buildKeyValueLine, buildKeyboardHints, buildPanelLine, buildPanelWorkspace, DEFAULT_PANEL_PALETTE, resolvePrimaryScrollableSection, type ColumnSpec, type PanelWorkspaceSection } from './polish.ts';
|
|
5
6
|
import { summarizeWorktreeOwnership, type WorktreeRegistry, type WorktreeStatusRecord } from '@/runtime/index.ts';
|
|
6
7
|
|
|
7
8
|
const C = {
|
|
@@ -22,6 +23,44 @@ function stateColor(state: WorktreeStatusRecord['state']): string {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
/** Glyph that reads at a glance: ● active, ◌ paused/kept, ⊘ discard/cleanup. */
|
|
27
|
+
function stateGlyph(state: WorktreeStatusRecord['state']): string {
|
|
28
|
+
switch (state) {
|
|
29
|
+
case 'active': return '●';
|
|
30
|
+
case 'paused':
|
|
31
|
+
case 'kept': return '◌';
|
|
32
|
+
default: return '⊘';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* One worktree row, aligned with display-width-aware columns so a wide-char or
|
|
38
|
+
* long branch name never shoves the path column out of alignment. The active
|
|
39
|
+
* worktree is flagged with a ● glyph and selection uses the shared marker.
|
|
40
|
+
*/
|
|
41
|
+
function buildWorktreeRow(width: number, row: WorktreeStatusRecord, selected: boolean): Line {
|
|
42
|
+
const pathW = Math.max(8, width - (2 + 10 + 13 + 20) - 4 - 2);
|
|
43
|
+
const columns: ColumnSpec[] = [
|
|
44
|
+
{ width: 2 },
|
|
45
|
+
{ width: 10 },
|
|
46
|
+
{ width: 13 },
|
|
47
|
+
{ width: 20 },
|
|
48
|
+
{ width: pathW },
|
|
49
|
+
];
|
|
50
|
+
return buildAlignedRow(
|
|
51
|
+
width,
|
|
52
|
+
[
|
|
53
|
+
{ text: stateGlyph(row.state), fg: stateColor(row.state) },
|
|
54
|
+
{ text: row.kind, fg: C.info },
|
|
55
|
+
{ text: row.state, fg: stateColor(row.state) },
|
|
56
|
+
{ text: row.branch, fg: C.value },
|
|
57
|
+
{ text: basename(row.path) || row.path, fg: C.dim },
|
|
58
|
+
],
|
|
59
|
+
columns,
|
|
60
|
+
{ selected, selectedBg: C.headerBg, marker: '▸' },
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
25
64
|
export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
|
|
26
65
|
private rows: WorktreeStatusRecord[] = [];
|
|
27
66
|
private loading = false;
|
|
@@ -52,13 +91,7 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
|
|
|
52
91
|
}
|
|
53
92
|
|
|
54
93
|
protected renderItem(row: WorktreeStatusRecord, index: number, _selected: boolean, width: number): Line {
|
|
55
|
-
|
|
56
|
-
return buildPanelLine(width, [
|
|
57
|
-
[` ${row.kind}`.padEnd(14), C.info, bg],
|
|
58
|
-
[` ${row.state}`.padEnd(16), stateColor(row.state), bg],
|
|
59
|
-
[` ${row.branch}`.padEnd(24), C.value, bg],
|
|
60
|
-
[` ${row.path}`.slice(0, Math.max(0, width - 56)), C.dim, bg],
|
|
61
|
-
]);
|
|
94
|
+
return buildWorktreeRow(width, row, index === this.selectedIndex);
|
|
62
95
|
}
|
|
63
96
|
|
|
64
97
|
private async refresh(): Promise<void> {
|
|
@@ -80,7 +113,13 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
|
|
|
80
113
|
if (this.loading && this.rows.length === 0) {
|
|
81
114
|
sections.push({ title: 'Worktrees', lines: [buildPanelLine(width, [[' Loading worktree state...', C.info]])] });
|
|
82
115
|
} else if (this.rows.length === 0) {
|
|
83
|
-
sections.push({
|
|
116
|
+
sections.push({
|
|
117
|
+
title: 'Worktrees',
|
|
118
|
+
lines: [
|
|
119
|
+
buildPanelLine(width, [[' No git worktrees discovered for this project yet.', C.dim]]),
|
|
120
|
+
buildPanelLine(width, [[' /worktree attach <path>', C.info], [' register a worktree for orchestrator-managed lifecycle', C.dim]]),
|
|
121
|
+
],
|
|
122
|
+
});
|
|
84
123
|
} else {
|
|
85
124
|
const summary = summarizeWorktreeOwnership(this.rows);
|
|
86
125
|
sections.push({
|
|
@@ -142,20 +181,26 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
|
|
|
142
181
|
};
|
|
143
182
|
const resolvedWorktreesSection = resolvePrimaryScrollableSection(width, height, {
|
|
144
183
|
intro: 'Orchestrator-owned worktree lifecycle, attachments, pause/resume posture, and cleanup state.',
|
|
145
|
-
footerLines: [
|
|
184
|
+
footerLines: [buildKeyboardHints(width, [{ keys: '↑/↓', label: 'select' }, { keys: 'r', label: 'refresh' }, { keys: '/worktree inspect', label: '<path>' }, { keys: 'attach·pause·resume·keep·discard·cleanup', label: '' }], C)],
|
|
146
185
|
palette: C,
|
|
147
186
|
beforeSections: sections,
|
|
148
187
|
section: {
|
|
149
188
|
title: 'Worktrees',
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
]
|
|
158
|
-
|
|
189
|
+
fixedLines: [
|
|
190
|
+
buildAlignedRow(width, [
|
|
191
|
+
{ text: '', fg: C.dim },
|
|
192
|
+
{ text: 'KIND', fg: C.label, bold: true },
|
|
193
|
+
{ text: 'STATE', fg: C.label, bold: true },
|
|
194
|
+
{ text: 'BRANCH', fg: C.label, bold: true },
|
|
195
|
+
{ text: 'WORKTREE', fg: C.label, bold: true },
|
|
196
|
+
], [
|
|
197
|
+
{ width: 2 }, { width: 10 }, { width: 13 }, { width: 20 },
|
|
198
|
+
{ width: Math.max(8, width - (2 + 10 + 13 + 20) - 4 - 2) },
|
|
199
|
+
], { marker: ' ' }),
|
|
200
|
+
],
|
|
201
|
+
scrollableLines: this.rows.map((row, absolute) =>
|
|
202
|
+
buildWorktreeRow(width, row, absolute === this.selectedIndex),
|
|
203
|
+
),
|
|
159
204
|
selectedIndex: this.selectedIndex,
|
|
160
205
|
scrollOffset: this.scrollStart,
|
|
161
206
|
guardRows: 1,
|
|
@@ -173,7 +218,7 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
|
|
|
173
218
|
title: 'Worktree Control Room',
|
|
174
219
|
intro: 'Orchestrator-owned worktree lifecycle, attachments, pause/resume posture, and cleanup state.',
|
|
175
220
|
sections,
|
|
176
|
-
footerLines: [
|
|
221
|
+
footerLines: [buildKeyboardHints(width, [{ keys: '↑/↓', label: 'select' }, { keys: 'r', label: 'refresh' }, { keys: '/worktree inspect', label: '<path>' }, { keys: 'attach·pause·resume·keep·discard·cleanup', label: '' }], C)],
|
|
177
222
|
palette: C,
|
|
178
223
|
});
|
|
179
224
|
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
2
|
+
import type { WrfcState, Constraint, ConstraintFinding } from '@pellux/goodvibes-sdk/platform/agents';
|
|
3
|
+
import { DEFAULT_PANEL_PALETTE, extendPalette } from './polish.ts';
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Colour palette + formatting helpers for the WRFC panel.
|
|
7
|
+
//
|
|
8
|
+
// Extracted from wrfc-panel.ts to keep that module under the architecture
|
|
9
|
+
// line-count cap. Leaf module (only polish + terminal-width + sdk types); the
|
|
10
|
+
// panel re-exports the public helpers so ./wrfc-panel.ts stays their import site.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
13
|
+
// WRFC state-machine colours (domain status -- no shared equivalent)
|
|
14
|
+
passed: '#22c55e', // green
|
|
15
|
+
failed: '#ef4444', // red
|
|
16
|
+
reviewing: '#eab308', // yellow
|
|
17
|
+
engineering:'#22d3ee', // cyan
|
|
18
|
+
fixing: '#f97316', // orange
|
|
19
|
+
pending: '#6b7280', // grey
|
|
20
|
+
gating: '#a78bfa', // violet
|
|
21
|
+
committing: '#38bdf8', // sky
|
|
22
|
+
integrating:'#818cf8', // indigo
|
|
23
|
+
|
|
24
|
+
// Issue-severity ramp (domain -- no shared equivalent)
|
|
25
|
+
issueCrit: '#ef4444',
|
|
26
|
+
issueMaj: '#f97316',
|
|
27
|
+
issueMin: '#eab308',
|
|
28
|
+
issueSug: '#6b7280',
|
|
29
|
+
|
|
30
|
+
// Selection + divider chrome with no shared equivalent
|
|
31
|
+
selected: '#1e40af', // selection bg
|
|
32
|
+
selectedFg: '#f8fafc',
|
|
33
|
+
border: '#334155',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Helpers
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
const SPARKLINE_CHARS = '._-:=+*#';
|
|
40
|
+
|
|
41
|
+
export function sparkline(scores: number[], maxScore = 10): string {
|
|
42
|
+
if (scores.length === 0) return '';
|
|
43
|
+
return scores
|
|
44
|
+
.map(s => {
|
|
45
|
+
const ratio = Math.max(0, Math.min(1, s / maxScore));
|
|
46
|
+
const idx = Math.round(ratio * (SPARKLINE_CHARS.length - 1));
|
|
47
|
+
return SPARKLINE_CHARS[idx];
|
|
48
|
+
})
|
|
49
|
+
.join('');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function stateColor(state: WrfcState): string {
|
|
53
|
+
switch (state) {
|
|
54
|
+
case 'passed': return C.passed;
|
|
55
|
+
case 'failed': return C.failed;
|
|
56
|
+
case 'reviewing': return C.reviewing;
|
|
57
|
+
case 'engineering': return C.engineering;
|
|
58
|
+
case 'fixing': return C.fixing;
|
|
59
|
+
case 'gating':
|
|
60
|
+
case 'awaiting_gates': return C.gating;
|
|
61
|
+
case 'committing': return C.committing;
|
|
62
|
+
case 'integrating': return C.integrating;
|
|
63
|
+
default: return C.pending;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function stateLabel(state: WrfcState): string {
|
|
68
|
+
switch (state) {
|
|
69
|
+
case 'engineering': return 'ENG';
|
|
70
|
+
case 'reviewing': return 'REV';
|
|
71
|
+
case 'fixing': return 'FIX';
|
|
72
|
+
case 'gating': return 'GATE';
|
|
73
|
+
case 'awaiting_gates': return 'WAIT';
|
|
74
|
+
case 'committing': return 'COMMIT';
|
|
75
|
+
case 'integrating': return 'INTG';
|
|
76
|
+
case 'passed': return 'PASS';
|
|
77
|
+
case 'failed': return 'FAIL';
|
|
78
|
+
default: return 'PEND';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function issueColor(severity: string): string {
|
|
83
|
+
switch (severity) {
|
|
84
|
+
case 'critical': return C.issueCrit;
|
|
85
|
+
case 'major': return C.issueMaj;
|
|
86
|
+
case 'minor': return C.issueMin;
|
|
87
|
+
default: return C.issueSug;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function issuePrefix(severity: string): string {
|
|
92
|
+
switch (severity) {
|
|
93
|
+
case 'critical': return '[CRIT] ';
|
|
94
|
+
case 'major': return '[MAJR] ';
|
|
95
|
+
case 'minor': return '[MINR] ';
|
|
96
|
+
default: return '[SUGG] ';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function truncate(s: string, max: number): string {
|
|
101
|
+
return truncateDisplay(s, max);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Constraint helpers
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Returns display tag, foreground colour, and dim flag for a single constraint
|
|
110
|
+
* based on whether a reviewer finding exists for it.
|
|
111
|
+
*/
|
|
112
|
+
export function constraintStatusMarker(
|
|
113
|
+
constraint: Constraint,
|
|
114
|
+
findings: ConstraintFinding[] | undefined,
|
|
115
|
+
): { tag: string; fg: string; dim: boolean } {
|
|
116
|
+
const finding = findings?.find(f => f.constraintId === constraint.id);
|
|
117
|
+
if (!finding) {
|
|
118
|
+
return { tag: '[UNV]', fg: C.dim, dim: true };
|
|
119
|
+
}
|
|
120
|
+
if (finding.satisfied) {
|
|
121
|
+
return { tag: '[SAT]', fg: C.good, dim: false };
|
|
122
|
+
}
|
|
123
|
+
// Unsatisfied — use severity to pick colour and tag text
|
|
124
|
+
const sev = finding.severity ?? 'major';
|
|
125
|
+
let sevTag: string;
|
|
126
|
+
let fg: string;
|
|
127
|
+
switch (sev) {
|
|
128
|
+
case 'critical': sevTag = '[UNS CRIT]'; fg = C.issueCrit; break;
|
|
129
|
+
case 'minor': sevTag = '[UNS MINOR]'; fg = C.issueMin; break;
|
|
130
|
+
default: sevTag = '[UNS MAJOR]'; fg = C.issueMaj; break;
|
|
131
|
+
}
|
|
132
|
+
return { tag: sevTag, fg, dim: false };
|
|
133
|
+
}
|