@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
|
@@ -1,37 +1,120 @@
|
|
|
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 {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
import { buildAlignedRow, buildKeyValueLine, buildKeyboardHints, buildPanelLine, buildPanelWorkspace, DEFAULT_PANEL_PALETTE, resolvePrimaryScrollableSection, type ColumnSpec, type PanelWorkspaceSection } from './polish.ts';
|
|
6
|
+
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
7
|
+
import { summarizeWorktreeOwnership, type WorktreeStatusRecord } from '@/runtime/index.ts';
|
|
8
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
9
|
+
import type { PanelIntegrationContext } from './types.ts';
|
|
10
|
+
|
|
11
|
+
// Base chrome only — state colors and text tokens come straight from
|
|
12
|
+
// DEFAULT_PANEL_PALETTE (WO-002).
|
|
13
|
+
const C = DEFAULT_PANEL_PALETTE;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Structural shape the panel actually needs from a worktree registry. The
|
|
17
|
+
* real `WorktreeRegistry` (from the SDK) satisfies this directly; it has no
|
|
18
|
+
* `subscribe()` method today, so the panel falls back to polling when one
|
|
19
|
+
* isn't provided (see constructor). Kept narrow and `Like`-suffixed per the
|
|
20
|
+
* project's duck-typing convention (see `agent-inspector-shared.ts`).
|
|
21
|
+
*/
|
|
22
|
+
export interface WorktreeRegistryLike {
|
|
23
|
+
list(): Promise<WorktreeStatusRecord[]>;
|
|
24
|
+
attach(path: string, target: { sessionId?: string; taskId?: string }): void;
|
|
25
|
+
setState(path: string, state: WorktreeStatusRecord['state']): void;
|
|
26
|
+
cleanup(path: string): Promise<void>;
|
|
27
|
+
/** Optional live-update hook; when present the panel prefers it over polling. */
|
|
28
|
+
subscribe?(listener: () => void): () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What a pending destructive confirm will do once the user confirms. */
|
|
32
|
+
interface WorktreeConfirmSubject {
|
|
33
|
+
readonly path: string;
|
|
34
|
+
readonly action: 'discard' | 'cleanup';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Poll cadence used only when the registry has no `subscribe()` method. */
|
|
38
|
+
const POLL_INTERVAL_MS = 5_000;
|
|
15
39
|
|
|
16
40
|
function stateColor(state: WorktreeStatusRecord['state']): string {
|
|
17
41
|
switch (state) {
|
|
18
|
-
case 'active': return C.
|
|
42
|
+
case 'active': return C.good;
|
|
19
43
|
case 'paused':
|
|
20
44
|
case 'kept': return C.warn;
|
|
21
45
|
default: return C.dim;
|
|
22
46
|
}
|
|
23
47
|
}
|
|
24
48
|
|
|
49
|
+
/** Glyph that reads at a glance: ● active, ◌ paused/kept, ⊘ discard/cleanup. */
|
|
50
|
+
function stateGlyph(state: WorktreeStatusRecord['state']): string {
|
|
51
|
+
switch (state) {
|
|
52
|
+
case 'active': return '●';
|
|
53
|
+
case 'paused':
|
|
54
|
+
case 'kept': return '◌';
|
|
55
|
+
default: return '⊘';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* One worktree row, aligned with display-width-aware columns so a wide-char or
|
|
61
|
+
* long branch name never shoves the path column out of alignment. The active
|
|
62
|
+
* worktree is flagged with a ● glyph and selection uses the shared marker.
|
|
63
|
+
*/
|
|
64
|
+
function buildWorktreeRow(width: number, row: WorktreeStatusRecord, selected: boolean): Line {
|
|
65
|
+
const pathW = Math.max(8, width - (2 + 10 + 13 + 20) - 4 - 2);
|
|
66
|
+
const columns: ColumnSpec[] = [
|
|
67
|
+
{ width: 2 },
|
|
68
|
+
{ width: 10 },
|
|
69
|
+
{ width: 13 },
|
|
70
|
+
{ width: 20 },
|
|
71
|
+
{ width: pathW },
|
|
72
|
+
];
|
|
73
|
+
return buildAlignedRow(
|
|
74
|
+
width,
|
|
75
|
+
[
|
|
76
|
+
{ text: stateGlyph(row.state), fg: stateColor(row.state) },
|
|
77
|
+
{ text: row.kind, fg: C.info },
|
|
78
|
+
{ text: row.state, fg: stateColor(row.state) },
|
|
79
|
+
{ text: row.branch, fg: C.value },
|
|
80
|
+
{ text: basename(row.path) || row.path, fg: C.dim },
|
|
81
|
+
],
|
|
82
|
+
columns,
|
|
83
|
+
{ selected, selectedBg: C.headerBg, marker: '▸' },
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const WORKTREE_HINTS = [
|
|
88
|
+
{ keys: '↑/↓', label: 'select' },
|
|
89
|
+
{ keys: 'p/u/k', label: 'pause/resume/keep' },
|
|
90
|
+
{ keys: 'd/c', label: 'discard/cleanup' },
|
|
91
|
+
{ keys: 'enter', label: 'jump to session/task' },
|
|
92
|
+
{ keys: 'r', label: 'refresh' },
|
|
93
|
+
] as const;
|
|
94
|
+
|
|
25
95
|
export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
|
|
26
96
|
private rows: WorktreeStatusRecord[] = [];
|
|
27
97
|
private loading = false;
|
|
28
|
-
private
|
|
98
|
+
private confirm: ConfirmState<WorktreeConfirmSubject> | null = null;
|
|
99
|
+
private pendingJump: { targetPanel: 'sessions' | 'tasks' } | null = null;
|
|
100
|
+
private readonly worktreeRegistry: WorktreeRegistryLike;
|
|
101
|
+
private readonly requestRender: () => void;
|
|
102
|
+
private unsubscribe: (() => void) | null = null;
|
|
29
103
|
|
|
30
|
-
public constructor(worktreeRegistry:
|
|
31
|
-
super('worktrees', 'Worktrees', '
|
|
104
|
+
public constructor(worktreeRegistry: WorktreeRegistryLike, requestRender: () => void = () => {}) {
|
|
105
|
+
super('worktrees', 'Worktrees', '▯', 'automation-control');
|
|
32
106
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
33
107
|
this.worktreeRegistry = worktreeRegistry;
|
|
108
|
+
this.requestRender = requestRender;
|
|
34
109
|
void this.refresh();
|
|
110
|
+
// Live state: prefer a registry subscription when the caller provides one
|
|
111
|
+
// (matches the mock shape used by contract tests); otherwise poll, so the
|
|
112
|
+
// real SDK WorktreeRegistry (which has no subscribe() today) still stays live.
|
|
113
|
+
if (typeof this.worktreeRegistry.subscribe === 'function') {
|
|
114
|
+
this.unsubscribe = this.worktreeRegistry.subscribe(() => { void this.refresh(); });
|
|
115
|
+
} else {
|
|
116
|
+
this.registerTimer(setInterval(() => { void this.refresh(); }, POLL_INTERVAL_MS));
|
|
117
|
+
}
|
|
35
118
|
}
|
|
36
119
|
|
|
37
120
|
public override onActivate(): void {
|
|
@@ -39,26 +122,108 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
|
|
|
39
122
|
if (!this.loading) void this.refresh();
|
|
40
123
|
}
|
|
41
124
|
|
|
42
|
-
public
|
|
125
|
+
public override onDestroy(): void {
|
|
126
|
+
super.onDestroy();
|
|
127
|
+
if (this.unsubscribe) {
|
|
128
|
+
this.unsubscribe();
|
|
129
|
+
this.unsubscribe = null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public override handleInput(key: string): boolean {
|
|
134
|
+
if (this.lastError !== null) this.clearError();
|
|
135
|
+
|
|
136
|
+
const confirmResult = handleConfirmInput(this.confirm, key);
|
|
137
|
+
if (confirmResult === 'confirmed') {
|
|
138
|
+
const subject = this.confirm!.subject;
|
|
139
|
+
this.confirm = null;
|
|
140
|
+
if (subject.action === 'discard') {
|
|
141
|
+
this.worktreeRegistry.setState(subject.path, 'discard');
|
|
142
|
+
void this.refresh();
|
|
143
|
+
} else {
|
|
144
|
+
void this._cleanup(subject.path);
|
|
145
|
+
}
|
|
146
|
+
this.markDirty();
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
if (confirmResult === 'cancelled') {
|
|
150
|
+
this.confirm = null;
|
|
151
|
+
this.markDirty();
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
if (confirmResult === 'absorbed') return true;
|
|
155
|
+
|
|
43
156
|
if (key === 'r') {
|
|
44
157
|
void this.refresh();
|
|
45
158
|
return true;
|
|
46
159
|
}
|
|
160
|
+
|
|
161
|
+
const selected = this.getSelectedItem();
|
|
162
|
+
|
|
163
|
+
if (key === 'p' && selected && selected.state !== 'paused') {
|
|
164
|
+
this.worktreeRegistry.setState(selected.path, 'paused');
|
|
165
|
+
void this.refresh();
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
if (key === 'u' && selected && selected.state !== 'active') {
|
|
169
|
+
this.worktreeRegistry.setState(selected.path, 'active');
|
|
170
|
+
void this.refresh();
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
if (key === 'k' && selected && selected.state !== 'kept') {
|
|
174
|
+
this.worktreeRegistry.setState(selected.path, 'kept');
|
|
175
|
+
void this.refresh();
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
if (key === 'd' && selected && selected.state !== 'discard') {
|
|
179
|
+
this.confirm = {
|
|
180
|
+
subject: { path: selected.path, action: 'discard' },
|
|
181
|
+
label: `worktree ${basename(selected.path)} (${selected.branch})`,
|
|
182
|
+
verb: 'Discard',
|
|
183
|
+
};
|
|
184
|
+
this.markDirty();
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
if (key === 'c' && selected) {
|
|
188
|
+
this.confirm = {
|
|
189
|
+
subject: { path: selected.path, action: 'cleanup' },
|
|
190
|
+
label: `worktree ${basename(selected.path)} (${selected.branch})`,
|
|
191
|
+
verb: 'Clean up',
|
|
192
|
+
};
|
|
193
|
+
this.markDirty();
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
if ((key === 'enter' || key === 'return') && selected && (selected.sessionId || selected.taskId)) {
|
|
197
|
+
this.pendingJump = { targetPanel: selected.sessionId ? 'sessions' : 'tasks' };
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
47
201
|
return super.handleInput(key);
|
|
48
202
|
}
|
|
49
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Enter on an attached worktree row jumps to the owning session/task panel.
|
|
206
|
+
* The actual `PanelManager.open()` call requires the integration context,
|
|
207
|
+
* which is only available here (invoked right after `handleInput` returns
|
|
208
|
+
* `true` for the same key) — same staged-pending-action pattern as
|
|
209
|
+
* `incident-review-panel.ts`.
|
|
210
|
+
*/
|
|
211
|
+
public handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
|
|
212
|
+
if (this.pendingJump) {
|
|
213
|
+
const jump = this.pendingJump;
|
|
214
|
+
this.pendingJump = null;
|
|
215
|
+
ctx.panelManager.open(jump.targetPanel);
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
50
221
|
protected getItems(): readonly WorktreeStatusRecord[] {
|
|
51
222
|
return this.rows;
|
|
52
223
|
}
|
|
53
224
|
|
|
54
225
|
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
|
-
]);
|
|
226
|
+
return buildWorktreeRow(width, row, index === this.selectedIndex);
|
|
62
227
|
}
|
|
63
228
|
|
|
64
229
|
private async refresh(): Promise<void> {
|
|
@@ -70,17 +235,45 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
|
|
|
70
235
|
} finally {
|
|
71
236
|
this.loading = false;
|
|
72
237
|
this.markDirty();
|
|
238
|
+
this.requestRender();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private async _cleanup(path: string): Promise<void> {
|
|
243
|
+
try {
|
|
244
|
+
await this.worktreeRegistry.cleanup(path);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
this.setError(summarizeError(err));
|
|
247
|
+
} finally {
|
|
248
|
+
await this.refresh();
|
|
73
249
|
}
|
|
74
250
|
}
|
|
75
251
|
|
|
76
252
|
public render(width: number, height: number): Line[] {
|
|
77
253
|
this.needsRender = false;
|
|
254
|
+
|
|
255
|
+
if (this.confirm) {
|
|
256
|
+
const lines = buildPanelWorkspace(width, height, {
|
|
257
|
+
title: 'Worktree Control Room',
|
|
258
|
+
sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this.confirm) }],
|
|
259
|
+
palette: C,
|
|
260
|
+
});
|
|
261
|
+
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
262
|
+
return lines.slice(0, height);
|
|
263
|
+
}
|
|
264
|
+
|
|
78
265
|
const sections: PanelWorkspaceSection[] = [];
|
|
79
266
|
|
|
80
267
|
if (this.loading && this.rows.length === 0) {
|
|
81
268
|
sections.push({ title: 'Worktrees', lines: [buildPanelLine(width, [[' Loading worktree state...', C.info]])] });
|
|
82
269
|
} else if (this.rows.length === 0) {
|
|
83
|
-
sections.push({
|
|
270
|
+
sections.push({
|
|
271
|
+
title: 'Worktrees',
|
|
272
|
+
lines: [
|
|
273
|
+
buildPanelLine(width, [[' No git worktrees discovered for this project yet.', C.dim]]),
|
|
274
|
+
buildPanelLine(width, [[' /worktree attach <path>', C.info], [' register a worktree for orchestrator-managed lifecycle', C.dim]]),
|
|
275
|
+
],
|
|
276
|
+
});
|
|
84
277
|
} else {
|
|
85
278
|
const summary = summarizeWorktreeOwnership(this.rows);
|
|
86
279
|
sections.push({
|
|
@@ -88,7 +281,7 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
|
|
|
88
281
|
lines: [
|
|
89
282
|
buildKeyValueLine(width, [
|
|
90
283
|
{ label: 'total', value: String(summary.total), valueColor: C.value },
|
|
91
|
-
{ label: 'active', value: String(summary.active), valueColor: C.
|
|
284
|
+
{ label: 'active', value: String(summary.active), valueColor: C.good },
|
|
92
285
|
{ label: 'paused', value: String(summary.paused), valueColor: summary.paused > 0 ? C.warn : C.dim },
|
|
93
286
|
{ label: 'cleanup', value: String(summary.pendingCleanup), valueColor: summary.pendingCleanup > 0 ? C.warn : C.dim },
|
|
94
287
|
], C),
|
|
@@ -100,19 +293,7 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
|
|
|
100
293
|
], C),
|
|
101
294
|
],
|
|
102
295
|
});
|
|
103
|
-
|
|
104
|
-
title: 'Next Actions',
|
|
105
|
-
lines: [
|
|
106
|
-
buildPanelLine(width, [[
|
|
107
|
-
summary.pendingCleanup > 0 || summary.discard > 0
|
|
108
|
-
? ' Review pending-cleanup and discard-marked worktrees before they drift from orchestrator ownership.'
|
|
109
|
-
: ' Worktree ownership is healthy. Use the task and session links below for restore, merge, or cleanup review.',
|
|
110
|
-
summary.pendingCleanup > 0 || summary.discard > 0 ? C.warn : C.dim,
|
|
111
|
-
]]),
|
|
112
|
-
buildPanelLine(width, [[' /worktree task <task-id> /worktree session <session-id> /worktree inspect <path>', C.info]]),
|
|
113
|
-
],
|
|
114
|
-
});
|
|
115
|
-
const selected = this.rows[this.selectedIndex]!;
|
|
296
|
+
const selected = this.getSelectedItem()!;
|
|
116
297
|
const detailSection: PanelWorkspaceSection = {
|
|
117
298
|
title: 'Details',
|
|
118
299
|
lines: [
|
|
@@ -126,36 +307,30 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
|
|
|
126
307
|
: ' Unattached worktree detected. Review whether it should be attached, kept, discarded, or cleaned up.',
|
|
127
308
|
selected.sessionId || selected.taskId ? C.info : C.warn,
|
|
128
309
|
]]),
|
|
129
|
-
buildPanelLine(width, [[
|
|
130
|
-
selected.state === 'paused'
|
|
131
|
-
? ` Next: /worktree resume ${selected.path}`
|
|
132
|
-
: selected.state === 'discard' || selected.state === 'pending-cleanup'
|
|
133
|
-
? ` Next: /worktree cleanup ${selected.path}`
|
|
134
|
-
: selected.taskId
|
|
135
|
-
? ` Next: /worktree task ${selected.taskId}`
|
|
136
|
-
: selected.sessionId
|
|
137
|
-
? ` Next: /worktree session ${selected.sessionId}`
|
|
138
|
-
: ` Next: /worktree inspect ${selected.path}`,
|
|
139
|
-
C.dim,
|
|
140
|
-
]]),
|
|
141
310
|
],
|
|
142
311
|
};
|
|
143
312
|
const resolvedWorktreesSection = resolvePrimaryScrollableSection(width, height, {
|
|
144
313
|
intro: 'Orchestrator-owned worktree lifecycle, attachments, pause/resume posture, and cleanup state.',
|
|
145
|
-
footerLines: [
|
|
314
|
+
footerLines: [buildKeyboardHints(width, WORKTREE_HINTS, C)],
|
|
146
315
|
palette: C,
|
|
147
316
|
beforeSections: sections,
|
|
148
317
|
section: {
|
|
149
318
|
title: 'Worktrees',
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
]
|
|
158
|
-
|
|
319
|
+
fixedLines: [
|
|
320
|
+
buildAlignedRow(width, [
|
|
321
|
+
{ text: '', fg: C.dim },
|
|
322
|
+
{ text: 'KIND', fg: C.label, bold: true },
|
|
323
|
+
{ text: 'STATE', fg: C.label, bold: true },
|
|
324
|
+
{ text: 'BRANCH', fg: C.label, bold: true },
|
|
325
|
+
{ text: 'WORKTREE', fg: C.label, bold: true },
|
|
326
|
+
], [
|
|
327
|
+
{ width: 2 }, { width: 10 }, { width: 13 }, { width: 20 },
|
|
328
|
+
{ width: Math.max(8, width - (2 + 10 + 13 + 20) - 4 - 2) },
|
|
329
|
+
], { marker: ' ' }),
|
|
330
|
+
],
|
|
331
|
+
scrollableLines: this.rows.map((row, absolute) =>
|
|
332
|
+
buildWorktreeRow(width, row, absolute === this.selectedIndex),
|
|
333
|
+
),
|
|
159
334
|
selectedIndex: this.selectedIndex,
|
|
160
335
|
scrollOffset: this.scrollStart,
|
|
161
336
|
guardRows: 1,
|
|
@@ -169,11 +344,14 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
|
|
|
169
344
|
sections.push(detailSection);
|
|
170
345
|
}
|
|
171
346
|
|
|
347
|
+
const errorLine = this.renderErrorLine(width);
|
|
348
|
+
if (errorLine) sections.push({ title: 'Error', lines: [errorLine] });
|
|
349
|
+
|
|
172
350
|
const lines = buildPanelWorkspace(width, height, {
|
|
173
351
|
title: 'Worktree Control Room',
|
|
174
352
|
intro: 'Orchestrator-owned worktree lifecycle, attachments, pause/resume posture, and cleanup state.',
|
|
175
353
|
sections,
|
|
176
|
-
footerLines: [
|
|
354
|
+
footerLines: [buildKeyboardHints(width, WORKTREE_HINTS, C)],
|
|
177
355
|
palette: C,
|
|
178
356
|
});
|
|
179
357
|
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
|
+
}
|