@pellux/goodvibes-tui 0.28.0 → 1.0.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 +133 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-line-cache.ts +432 -0
- package/src/core/conversation-rendering.ts +11 -3
- package/src/core/conversation.ts +50 -4
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-content-actions.ts +8 -3
- package/src/input/handler-feed-routes.ts +12 -35
- package/src/input/handler-feed.ts +4 -8
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler.ts +43 -6
- package/src/input/input-history.ts +17 -4
- package/src/input/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +19 -11
- package/src/panels/agent-inspector-panel.ts +125 -36
- package/src/panels/agent-inspector-shared.ts +144 -0
- package/src/panels/approval-panel.ts +67 -16
- package/src/panels/automation-control-panel.ts +368 -124
- package/src/panels/base-panel.ts +1 -1
- 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 +232 -73
- package/src/panels/communication-panel.ts +58 -20
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +351 -118
- package/src/panels/cost-tracker-panel.ts +165 -7
- package/src/panels/debug-panel.ts +312 -159
- package/src/panels/diff-panel.ts +209 -57
- package/src/panels/docs-panel.ts +152 -66
- package/src/panels/eval-panel.ts +220 -42
- package/src/panels/file-explorer-panel.ts +202 -122
- package/src/panels/file-preview-panel.ts +132 -57
- package/src/panels/git-panel.ts +310 -129
- package/src/panels/hooks-panel.ts +150 -22
- package/src/panels/incident-review-panel.ts +223 -35
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +212 -86
- package/src/panels/knowledge-graph-panel.ts +461 -101
- package/src/panels/local-auth-panel.ts +240 -28
- package/src/panels/marketplace-panel.ts +193 -26
- package/src/panels/memory-panel.ts +78 -77
- package/src/panels/ops-control-panel.ts +146 -29
- package/src/panels/ops-strategy-panel.ts +72 -4
- package/src/panels/orchestration-panel.ts +231 -69
- package/src/panels/panel-list-panel.ts +154 -131
- package/src/panels/panel-manager.ts +97 -9
- package/src/panels/plan-dashboard-panel.ts +333 -17
- package/src/panels/plugins-panel.ts +184 -29
- package/src/panels/policy-panel.ts +210 -38
- package/src/panels/polish-core.ts +7 -2
- package/src/panels/polish.ts +23 -4
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +62 -113
- package/src/panels/provider-health-panel.ts +434 -518
- 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 +116 -30
- package/src/panels/remote-panel.ts +114 -36
- package/src/panels/routes-panel.ts +63 -22
- package/src/panels/sandbox-panel.ts +174 -42
- package/src/panels/scrollable-list-panel.ts +19 -135
- package/src/panels/security-panel.ts +133 -33
- package/src/panels/services-panel.ts +116 -64
- package/src/panels/session-browser-panel.ts +73 -2
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +335 -72
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +86 -33
- package/src/panels/symbol-outline-panel.ts +248 -108
- package/src/panels/system-messages-panel.ts +114 -13
- package/src/panels/tasks-panel.ts +326 -139
- package/src/panels/thinking-panel.ts +43 -10
- package/src/panels/token-budget-panel.ts +194 -18
- package/src/panels/tool-inspector-panel.ts +144 -34
- package/src/panels/types.ts +40 -4
- package/src/panels/work-plan-panel.ts +280 -17
- package/src/panels/worktree-panel.ts +175 -42
- package/src/panels/wrfc-panel.ts +116 -24
- package/src/renderer/agent-detail-modal.ts +37 -8
- package/src/renderer/code-block.ts +58 -28
- package/src/renderer/context-inspector.ts +3 -6
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/conversation-surface.ts +1 -21
- package/src/renderer/diff-view.ts +6 -4
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +23 -17
- package/src/renderer/fullscreen-workspace.ts +2 -7
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/history-search-overlay.ts +10 -16
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/markdown.ts +9 -1
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +8 -485
- package/src/renderer/model-workspace.ts +9 -24
- package/src/renderer/overlay-box.ts +23 -19
- package/src/renderer/process-indicator.ts +15 -25
- package/src/renderer/profile-picker-modal.ts +13 -14
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/selection-modal-overlay.ts +3 -1
- package/src/renderer/semantic-diff.ts +1 -1
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal-helpers.ts +10 -34
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +54 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/surface-layout.ts +0 -12
- package/src/renderer/term-caps.ts +1 -1
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/tool-call.ts +6 -20
- package/src/renderer/ui-factory.ts +48 -37
- package/src/renderer/ui-primitives.ts +41 -3
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/render-scheduler.ts +80 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +54 -5
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -635
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -238
- package/src/panels/forensics-panel.ts +0 -378
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -255
- package/src/panels/provider-stats-panel.ts +0 -391
- package/src/panels/schedule-panel.ts +0 -365
- package/src/panels/watchers-panel.ts +0 -213
- package/src/renderer/file-tree.ts +0 -153
- package/src/renderer/progress.ts +0 -99
- package/src/renderer/status-token.ts +0 -67
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -3,20 +3,43 @@ import { createEmptyLine } from '../types/grid.ts';
|
|
|
3
3
|
import { basename } from 'node:path';
|
|
4
4
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
5
5
|
import { buildAlignedRow, buildKeyValueLine, buildKeyboardHints, buildPanelLine, buildPanelWorkspace, DEFAULT_PANEL_PALETTE, resolvePrimaryScrollableSection, type ColumnSpec, type PanelWorkspaceSection } from './polish.ts';
|
|
6
|
-
import {
|
|
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';
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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;
|
|
16
39
|
|
|
17
40
|
function stateColor(state: WorktreeStatusRecord['state']): string {
|
|
18
41
|
switch (state) {
|
|
19
|
-
case 'active': return C.
|
|
42
|
+
case 'active': return C.good;
|
|
20
43
|
case 'paused':
|
|
21
44
|
case 'kept': return C.warn;
|
|
22
45
|
default: return C.dim;
|
|
@@ -61,16 +84,37 @@ function buildWorktreeRow(width: number, row: WorktreeStatusRecord, selected: bo
|
|
|
61
84
|
);
|
|
62
85
|
}
|
|
63
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
|
+
|
|
64
95
|
export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
|
|
65
96
|
private rows: WorktreeStatusRecord[] = [];
|
|
66
97
|
private loading = false;
|
|
67
|
-
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;
|
|
68
103
|
|
|
69
|
-
public constructor(worktreeRegistry:
|
|
70
|
-
super('worktrees', 'Worktrees', '
|
|
104
|
+
public constructor(worktreeRegistry: WorktreeRegistryLike, requestRender: () => void = () => {}) {
|
|
105
|
+
super('worktrees', 'Worktrees', '▯', 'automation-control');
|
|
71
106
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
72
107
|
this.worktreeRegistry = worktreeRegistry;
|
|
108
|
+
this.requestRender = requestRender;
|
|
73
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
|
+
}
|
|
74
118
|
}
|
|
75
119
|
|
|
76
120
|
public override onActivate(): void {
|
|
@@ -78,14 +122,102 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
|
|
|
78
122
|
if (!this.loading) void this.refresh();
|
|
79
123
|
}
|
|
80
124
|
|
|
81
|
-
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
|
+
|
|
82
156
|
if (key === 'r') {
|
|
83
157
|
void this.refresh();
|
|
84
158
|
return true;
|
|
85
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
|
+
|
|
86
201
|
return super.handleInput(key);
|
|
87
202
|
}
|
|
88
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
|
+
|
|
89
221
|
protected getItems(): readonly WorktreeStatusRecord[] {
|
|
90
222
|
return this.rows;
|
|
91
223
|
}
|
|
@@ -103,11 +235,33 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
|
|
|
103
235
|
} finally {
|
|
104
236
|
this.loading = false;
|
|
105
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();
|
|
106
249
|
}
|
|
107
250
|
}
|
|
108
251
|
|
|
109
252
|
public render(width: number, height: number): Line[] {
|
|
110
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
|
+
|
|
111
265
|
const sections: PanelWorkspaceSection[] = [];
|
|
112
266
|
|
|
113
267
|
if (this.loading && this.rows.length === 0) {
|
|
@@ -127,7 +281,7 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
|
|
|
127
281
|
lines: [
|
|
128
282
|
buildKeyValueLine(width, [
|
|
129
283
|
{ label: 'total', value: String(summary.total), valueColor: C.value },
|
|
130
|
-
{ label: 'active', value: String(summary.active), valueColor: C.
|
|
284
|
+
{ label: 'active', value: String(summary.active), valueColor: C.good },
|
|
131
285
|
{ label: 'paused', value: String(summary.paused), valueColor: summary.paused > 0 ? C.warn : C.dim },
|
|
132
286
|
{ label: 'cleanup', value: String(summary.pendingCleanup), valueColor: summary.pendingCleanup > 0 ? C.warn : C.dim },
|
|
133
287
|
], C),
|
|
@@ -139,19 +293,7 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
|
|
|
139
293
|
], C),
|
|
140
294
|
],
|
|
141
295
|
});
|
|
142
|
-
|
|
143
|
-
title: 'Next Actions',
|
|
144
|
-
lines: [
|
|
145
|
-
buildPanelLine(width, [[
|
|
146
|
-
summary.pendingCleanup > 0 || summary.discard > 0
|
|
147
|
-
? ' Review pending-cleanup and discard-marked worktrees before they drift from orchestrator ownership.'
|
|
148
|
-
: ' Worktree ownership is healthy. Use the task and session links below for restore, merge, or cleanup review.',
|
|
149
|
-
summary.pendingCleanup > 0 || summary.discard > 0 ? C.warn : C.dim,
|
|
150
|
-
]]),
|
|
151
|
-
buildPanelLine(width, [[' /worktree task <task-id> /worktree session <session-id> /worktree inspect <path>', C.info]]),
|
|
152
|
-
],
|
|
153
|
-
});
|
|
154
|
-
const selected = this.rows[this.selectedIndex]!;
|
|
296
|
+
const selected = this.getSelectedItem()!;
|
|
155
297
|
const detailSection: PanelWorkspaceSection = {
|
|
156
298
|
title: 'Details',
|
|
157
299
|
lines: [
|
|
@@ -165,23 +307,11 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
|
|
|
165
307
|
: ' Unattached worktree detected. Review whether it should be attached, kept, discarded, or cleaned up.',
|
|
166
308
|
selected.sessionId || selected.taskId ? C.info : C.warn,
|
|
167
309
|
]]),
|
|
168
|
-
buildPanelLine(width, [[
|
|
169
|
-
selected.state === 'paused'
|
|
170
|
-
? ` Next: /worktree resume ${selected.path}`
|
|
171
|
-
: selected.state === 'discard' || selected.state === 'pending-cleanup'
|
|
172
|
-
? ` Next: /worktree cleanup ${selected.path}`
|
|
173
|
-
: selected.taskId
|
|
174
|
-
? ` Next: /worktree task ${selected.taskId}`
|
|
175
|
-
: selected.sessionId
|
|
176
|
-
? ` Next: /worktree session ${selected.sessionId}`
|
|
177
|
-
: ` Next: /worktree inspect ${selected.path}`,
|
|
178
|
-
C.dim,
|
|
179
|
-
]]),
|
|
180
310
|
],
|
|
181
311
|
};
|
|
182
312
|
const resolvedWorktreesSection = resolvePrimaryScrollableSection(width, height, {
|
|
183
313
|
intro: 'Orchestrator-owned worktree lifecycle, attachments, pause/resume posture, and cleanup state.',
|
|
184
|
-
footerLines: [buildKeyboardHints(width,
|
|
314
|
+
footerLines: [buildKeyboardHints(width, WORKTREE_HINTS, C)],
|
|
185
315
|
palette: C,
|
|
186
316
|
beforeSections: sections,
|
|
187
317
|
section: {
|
|
@@ -214,11 +344,14 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
|
|
|
214
344
|
sections.push(detailSection);
|
|
215
345
|
}
|
|
216
346
|
|
|
347
|
+
const errorLine = this.renderErrorLine(width);
|
|
348
|
+
if (errorLine) sections.push({ title: 'Error', lines: [errorLine] });
|
|
349
|
+
|
|
217
350
|
const lines = buildPanelWorkspace(width, height, {
|
|
218
351
|
title: 'Worktree Control Room',
|
|
219
352
|
intro: 'Orchestrator-owned worktree lifecycle, attachments, pause/resume posture, and cleanup state.',
|
|
220
353
|
sections,
|
|
221
|
-
footerLines: [buildKeyboardHints(width,
|
|
354
|
+
footerLines: [buildKeyboardHints(width, WORKTREE_HINTS, C)],
|
|
222
355
|
palette: C,
|
|
223
356
|
});
|
|
224
357
|
while (lines.length < height) lines.push(createEmptyLine(width));
|
package/src/panels/wrfc-panel.ts
CHANGED
|
@@ -21,6 +21,9 @@ import {
|
|
|
21
21
|
handleConfirmInput,
|
|
22
22
|
} from './confirm-state.ts';
|
|
23
23
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
24
|
+
import { formatDuration } from '../utils/format-duration.ts';
|
|
25
|
+
import { AgentInspectorPanel } from './agent-inspector-panel.ts';
|
|
26
|
+
import type { PanelIntegrationContext } from './types.ts';
|
|
24
27
|
|
|
25
28
|
// ---------------------------------------------------------------------------
|
|
26
29
|
// Constants
|
|
@@ -73,6 +76,17 @@ export class WrfcPanel extends BasePanel {
|
|
|
73
76
|
private chains: WrfcChain[] = [];
|
|
74
77
|
private selectedIndex = 0;
|
|
75
78
|
private scrollOffset = 0;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The chain under the cursor. This panel owns its own selection state
|
|
82
|
+
* (`selectedIndex` navigates `this.chains` directly), so every selected-row
|
|
83
|
+
* read routes through this one accessor — indexing `this.chains` by the
|
|
84
|
+
* cursor directly is banned by the no-raw-selectedindex-read architecture rule.
|
|
85
|
+
*/
|
|
86
|
+
private selectedChain(): WrfcChain | undefined {
|
|
87
|
+
return this.chains.at(this.selectedIndex);
|
|
88
|
+
}
|
|
89
|
+
|
|
76
90
|
private expandedChainIds = new Set<string>();
|
|
77
91
|
private unsubscribers: Array<() => void> = [];
|
|
78
92
|
/** Last event timestamp per chain id, for stall detection. */
|
|
@@ -137,10 +151,64 @@ export class WrfcPanel extends BasePanel {
|
|
|
137
151
|
case 'enter': this.toggleExpanded(); return true;
|
|
138
152
|
case 'c': this.beginCancelConfirm(); return true;
|
|
139
153
|
case 'r': this.doResume(); return true;
|
|
154
|
+
// 'a' jumps to the selected chain's owner agent in the Inspector. The
|
|
155
|
+
// actual navigation happens in handlePanelIntegrationAction (it needs
|
|
156
|
+
// the PanelManager); consuming the key here just requires a selected
|
|
157
|
+
// chain to exist so the router below fires next.
|
|
158
|
+
case 'a': return this.selectedChain() !== undefined;
|
|
140
159
|
default: return false;
|
|
141
160
|
}
|
|
142
161
|
}
|
|
143
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Cross-panel jump: 'a' on a selected chain opens the Inspector focused on
|
|
165
|
+
* that chain's owner agent (inspectAgent, reverse of the forward jump
|
|
166
|
+
* agent-detail-modal.ts makes from an agent record's wrfcId to its chain).
|
|
167
|
+
*/
|
|
168
|
+
handlePanelIntegrationAction(key: string, ctx: PanelIntegrationContext): boolean {
|
|
169
|
+
if (key !== 'a') return false;
|
|
170
|
+
const chain = this.selectedChain();
|
|
171
|
+
if (!chain) return false;
|
|
172
|
+
const inspector = ctx.panelManager.open('inspector');
|
|
173
|
+
if (inspector instanceof AgentInspectorPanel) {
|
|
174
|
+
inspector.inspectAgent(chain.ownerAgentId);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Mouse-wheel scroll: pan the chain list viewport without moving the
|
|
182
|
+
* selection cursor (BasePanel's default forwards wheel deltas into
|
|
183
|
+
* handleInput('up'/'down'), which would also move the selected row).
|
|
184
|
+
* scrollOffset is a hint consumed by resolveScrollablePanelSection's
|
|
185
|
+
* tracked window on the next render, which clamps it into range and keeps
|
|
186
|
+
* the selected row inside the guard band — so an over-large delta here is
|
|
187
|
+
* always safe.
|
|
188
|
+
*/
|
|
189
|
+
override handleScroll(deltaRows: number): boolean {
|
|
190
|
+
if (this.confirmCancel) return false;
|
|
191
|
+
const rows = Math.trunc(deltaRows);
|
|
192
|
+
if (rows === 0) return false;
|
|
193
|
+
const next = Math.max(0, this.scrollOffset + rows);
|
|
194
|
+
if (next === this.scrollOffset) return false;
|
|
195
|
+
this.scrollOffset = next;
|
|
196
|
+
this.markDirty();
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Cross-panel jump target: select the chain by id and expand it. Used by
|
|
202
|
+
* WorkPlanPanel's 'w' jump key when an item links a wrfcId.
|
|
203
|
+
*/
|
|
204
|
+
selectChain(chainId: string): void {
|
|
205
|
+
const index = this.chains.findIndex((chain) => chain.id === chainId);
|
|
206
|
+
if (index < 0) return;
|
|
207
|
+
this.selectedIndex = index;
|
|
208
|
+
this.expandedChainIds.add(chainId);
|
|
209
|
+
this.markDirty();
|
|
210
|
+
}
|
|
211
|
+
|
|
144
212
|
// -------------------------------------------------------------------------
|
|
145
213
|
// Render
|
|
146
214
|
// -------------------------------------------------------------------------
|
|
@@ -159,7 +227,8 @@ export class WrfcPanel extends BasePanel {
|
|
|
159
227
|
' No WRFC chains yet',
|
|
160
228
|
'WRFC chains appear here as review/fix cycles execute. Expanded rows show scores, gates, issues, and failure detail.',
|
|
161
229
|
[
|
|
162
|
-
{ command: '/
|
|
230
|
+
{ command: '/teamwork create-mode local-engineer <task>', summary: 'run an engineer task through the write-review-fix-commit loop (reviewMode: wrfc)' },
|
|
231
|
+
{ command: '/teamwork modes', summary: 'list every teamwork mode and see which ones use reviewMode wrfc' },
|
|
163
232
|
],
|
|
164
233
|
DEFAULT_PANEL_PALETTE,
|
|
165
234
|
),
|
|
@@ -194,21 +263,25 @@ export class WrfcPanel extends BasePanel {
|
|
|
194
263
|
const stalled = this.isStalled(chain, now);
|
|
195
264
|
|
|
196
265
|
if (isSelected) selectedLineIndex = chainLines.length;
|
|
197
|
-
chainLines.push(...this.renderChainRow(chain, width, isSelected, isExpanded, rowBg, rowFg, stalled));
|
|
266
|
+
chainLines.push(...this.renderChainRow(chain, width, isSelected, isExpanded, rowBg, rowFg, stalled, now));
|
|
198
267
|
|
|
199
268
|
if (isExpanded) {
|
|
200
269
|
chainLines.push(...this.renderChainDetail(chain, width, 12));
|
|
201
270
|
}
|
|
202
271
|
}
|
|
203
272
|
|
|
204
|
-
const selectedChain = this.
|
|
273
|
+
const selectedChain = this.selectedChain();
|
|
205
274
|
const selectedLines: Line[] = selectedChain
|
|
206
275
|
? [
|
|
207
276
|
buildPanelLine(width, [
|
|
208
277
|
[' State ', DEFAULT_PANEL_PALETTE.label],
|
|
209
278
|
[stateLabel(selectedChain.state), stateColor(selectedChain.state)],
|
|
210
|
-
['
|
|
211
|
-
[
|
|
279
|
+
[TERMINAL_STATES.includes(selectedChain.state) ? ' Duration ' : ' Elapsed ', DEFAULT_PANEL_PALETTE.label],
|
|
280
|
+
[this.durationLabel(selectedChain, now), DEFAULT_PANEL_PALETTE.info],
|
|
281
|
+
]),
|
|
282
|
+
buildPanelLine(width, [
|
|
283
|
+
[' Task ', DEFAULT_PANEL_PALETTE.label],
|
|
284
|
+
[truncate(selectedChain.task, Math.max(8, width - 8)), DEFAULT_PANEL_PALETTE.value],
|
|
212
285
|
]),
|
|
213
286
|
buildPanelLine(width, [
|
|
214
287
|
[' Reviews ', DEFAULT_PANEL_PALETTE.label],
|
|
@@ -283,7 +356,7 @@ export class WrfcPanel extends BasePanel {
|
|
|
283
356
|
} : null;
|
|
284
357
|
|
|
285
358
|
// Footer: show resume-disabled reason for the selected chain.
|
|
286
|
-
const selectedForFooter = this.
|
|
359
|
+
const selectedForFooter = this.selectedChain();
|
|
287
360
|
const resumeReason = selectedForFooter ? this.resumeDisabledReason(selectedForFooter) : null;
|
|
288
361
|
const footerLines: Line[] = [
|
|
289
362
|
buildPanelLine(width, [
|
|
@@ -292,6 +365,7 @@ export class WrfcPanel extends BasePanel {
|
|
|
292
365
|
[' c', DEFAULT_PANEL_PALETTE.info], [' cancel', DEFAULT_PANEL_PALETTE.dim],
|
|
293
366
|
[' r', resumeReason ? DEFAULT_PANEL_PALETTE.dim : DEFAULT_PANEL_PALETTE.info],
|
|
294
367
|
[resumeReason ? ` resume (${resumeReason})` : ' resume', DEFAULT_PANEL_PALETTE.dim],
|
|
368
|
+
[' a', DEFAULT_PANEL_PALETTE.info], [' jump to agent', DEFAULT_PANEL_PALETTE.dim],
|
|
295
369
|
]),
|
|
296
370
|
];
|
|
297
371
|
|
|
@@ -353,6 +427,7 @@ export class WrfcPanel extends BasePanel {
|
|
|
353
427
|
bg: string,
|
|
354
428
|
fg: string,
|
|
355
429
|
stalled = false,
|
|
430
|
+
now: number = Date.now(),
|
|
356
431
|
): Line[] {
|
|
357
432
|
const stateCol = stalled ? C.warn : stateColor(chain.state);
|
|
358
433
|
const stateTag = stalled
|
|
@@ -367,6 +442,7 @@ export class WrfcPanel extends BasePanel {
|
|
|
367
442
|
? ` ${chain.reviewScores[chain.reviewScores.length - 1].toFixed(1)}/10`
|
|
368
443
|
: '';
|
|
369
444
|
const stalledBadge = stalled ? ' [STALLED]' : '';
|
|
445
|
+
const durationTag = ` ${this.durationLabel(chain, now)}`;
|
|
370
446
|
// Constraint badge: c:sat/total — only when constraints exist
|
|
371
447
|
let constraintBadge = '';
|
|
372
448
|
if (chain.constraints.length > 0) {
|
|
@@ -375,7 +451,7 @@ export class WrfcPanel extends BasePanel {
|
|
|
375
451
|
const satisfied = findings ? findings.filter(f => f.satisfied).length : 0;
|
|
376
452
|
constraintBadge = ` c:${satisfied}/${total}`;
|
|
377
453
|
}
|
|
378
|
-
const rightInfo = `${stalledBadge}${latestScore}${fixes}${cycles}${constraintBadge} `;
|
|
454
|
+
const rightInfo = `${stalledBadge}${durationTag}${latestScore}${fixes}${cycles}${constraintBadge} `;
|
|
379
455
|
|
|
380
456
|
// Compute how much space the task text can use, then check if rightInfo fits.
|
|
381
457
|
// If the terminal is narrow and rightInfo would overflow, omit it entirely
|
|
@@ -433,6 +509,11 @@ export class WrfcPanel extends BasePanel {
|
|
|
433
509
|
}
|
|
434
510
|
|
|
435
511
|
private renderChainDetail(chain: WrfcChain, width: number, maxLines: number): Line[] {
|
|
512
|
+
// Build the FULL detail content first (no per-section maxLines guards
|
|
513
|
+
// below), then apply one cap at the end so a truncation always ends with
|
|
514
|
+
// a visible "+N more" indicator instead of silently dropping content —
|
|
515
|
+
// the constraint sub-list keeps its own independent (+N more) cap since
|
|
516
|
+
// that is documented, unrelated badge/marker behavior (PRESERVE).
|
|
436
517
|
const lines: Line[] = [];
|
|
437
518
|
const indent = ' ';
|
|
438
519
|
|
|
@@ -459,13 +540,12 @@ export class WrfcPanel extends BasePanel {
|
|
|
459
540
|
}
|
|
460
541
|
|
|
461
542
|
// Constraints section (between Cycles and Gates)
|
|
462
|
-
if (chain.constraints.length > 0
|
|
543
|
+
if (chain.constraints.length > 0) {
|
|
463
544
|
lines.push(buildStyledPanelLine(width, [{ text: `${indent}Constraints`, fg: C.label }]));
|
|
464
545
|
const MAX_CONSTRAINTS = 10;
|
|
465
546
|
const findings = chain.reviewerReport?.constraintFindings;
|
|
466
547
|
const displayed = chain.constraints.slice(0, MAX_CONSTRAINTS);
|
|
467
548
|
for (const constraint of displayed) {
|
|
468
|
-
if (lines.length >= maxLines) break;
|
|
469
549
|
const marker = constraintStatusMarker(constraint, findings);
|
|
470
550
|
const statusTag = marker.tag;
|
|
471
551
|
const rowPrefix = `${indent} ${statusTag} `;
|
|
@@ -479,7 +559,7 @@ export class WrfcPanel extends BasePanel {
|
|
|
479
559
|
]));
|
|
480
560
|
}
|
|
481
561
|
const remaining = chain.constraints.length - MAX_CONSTRAINTS;
|
|
482
|
-
if (remaining > 0
|
|
562
|
+
if (remaining > 0) {
|
|
483
563
|
lines.push(buildStyledPanelLine(width, [
|
|
484
564
|
{ text: `${indent} (+${remaining} more)`, fg: C.dim, dim: true },
|
|
485
565
|
]));
|
|
@@ -490,7 +570,6 @@ export class WrfcPanel extends BasePanel {
|
|
|
490
570
|
if (chain.gateResults && chain.gateResults.length > 0) {
|
|
491
571
|
lines.push(buildStyledPanelLine(width, [{ text: `${indent}Gates`, fg: C.label }]));
|
|
492
572
|
for (const gate of chain.gateResults) {
|
|
493
|
-
if (lines.length >= maxLines) break;
|
|
494
573
|
lines.push(this.renderGateResult(gate, width, indent + ' '));
|
|
495
574
|
}
|
|
496
575
|
}
|
|
@@ -503,10 +582,9 @@ export class WrfcPanel extends BasePanel {
|
|
|
503
582
|
}
|
|
504
583
|
|
|
505
584
|
// Synthetic issues injected by controller (continuity violations)
|
|
506
|
-
if (chain.syntheticIssues && chain.syntheticIssues.length > 0
|
|
585
|
+
if (chain.syntheticIssues && chain.syntheticIssues.length > 0) {
|
|
507
586
|
lines.push(buildStyledPanelLine(width, [{ text: `${indent}Controller flags`, fg: C.issueCrit, bold: true }]));
|
|
508
587
|
for (const synthetic of chain.syntheticIssues) {
|
|
509
|
-
if (lines.length >= maxLines) break;
|
|
510
588
|
const prefix = `${indent} [CRIT] `;
|
|
511
589
|
const descMax = Math.max(8, width - prefix.length);
|
|
512
590
|
const desc = truncate(synthetic.description, descMax);
|
|
@@ -519,10 +597,9 @@ export class WrfcPanel extends BasePanel {
|
|
|
519
597
|
|
|
520
598
|
// Issues from reviewer
|
|
521
599
|
const issues = chain.reviewerReport?.issues ?? [];
|
|
522
|
-
if (issues.length > 0
|
|
600
|
+
if (issues.length > 0) {
|
|
523
601
|
lines.push(buildStyledPanelLine(width, [{ text: `${indent}Issues`, fg: C.label }]));
|
|
524
602
|
for (const issue of issues) {
|
|
525
|
-
if (lines.length >= maxLines) break;
|
|
526
603
|
const prefix = `${indent} ${issuePrefix(issue.severity)}`;
|
|
527
604
|
const descMax = width - prefix.length;
|
|
528
605
|
const desc = truncate(issue.description, Math.max(8, descMax));
|
|
@@ -534,7 +611,7 @@ export class WrfcPanel extends BasePanel {
|
|
|
534
611
|
}
|
|
535
612
|
|
|
536
613
|
// Error
|
|
537
|
-
if (chain.error
|
|
614
|
+
if (chain.error) {
|
|
538
615
|
const errPrefix = `${indent}Error `;
|
|
539
616
|
lines.push(buildStyledPanelLine(width, [
|
|
540
617
|
{ text: errPrefix, fg: C.failed, bold: true },
|
|
@@ -543,11 +620,15 @@ export class WrfcPanel extends BasePanel {
|
|
|
543
620
|
}
|
|
544
621
|
|
|
545
622
|
// Divider after expanded section
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
623
|
+
lines.push(buildStyledPanelLine(width, [{ text: ' ' + '-'.repeat(Math.max(0, width - 4)) + ' ', fg: C.border, dim: true }]));
|
|
624
|
+
|
|
625
|
+
if (lines.length <= maxLines) return lines;
|
|
626
|
+
const visible = lines.slice(0, Math.max(0, maxLines - 1));
|
|
627
|
+
const hidden = lines.length - visible.length;
|
|
628
|
+
visible.push(buildStyledPanelLine(width, [
|
|
629
|
+
{ text: `${indent}+${hidden} more (press a to inspect the owner agent for full detail)`, fg: C.dim, dim: true },
|
|
630
|
+
]));
|
|
631
|
+
return visible;
|
|
551
632
|
}
|
|
552
633
|
|
|
553
634
|
private renderGateResult(gate: QualityGateResult, width: number, indent: string): Line {
|
|
@@ -575,7 +656,7 @@ export class WrfcPanel extends BasePanel {
|
|
|
575
656
|
}
|
|
576
657
|
|
|
577
658
|
private toggleExpanded(): void {
|
|
578
|
-
const chain = this.
|
|
659
|
+
const chain = this.selectedChain();
|
|
579
660
|
if (!chain) return;
|
|
580
661
|
if (this.expandedChainIds.has(chain.id)) {
|
|
581
662
|
this.expandedChainIds.delete(chain.id);
|
|
@@ -658,7 +739,7 @@ export class WrfcPanel extends BasePanel {
|
|
|
658
739
|
|
|
659
740
|
/** Initiate cancel-confirm flow for the selected chain (noop if terminal). */
|
|
660
741
|
private beginCancelConfirm(): void {
|
|
661
|
-
const chain = this.
|
|
742
|
+
const chain = this.selectedChain();
|
|
662
743
|
if (!chain || TERMINAL_STATES.includes(chain.state)) return;
|
|
663
744
|
this.confirmCancel = {
|
|
664
745
|
subject: chain.id,
|
|
@@ -673,7 +754,7 @@ export class WrfcPanel extends BasePanel {
|
|
|
673
754
|
* Emits a visible noop reason when the chain is not resumable.
|
|
674
755
|
*/
|
|
675
756
|
private doResume(): void {
|
|
676
|
-
const chain = this.
|
|
757
|
+
const chain = this.selectedChain();
|
|
677
758
|
if (!chain) return;
|
|
678
759
|
if (!RESUMABLE_STATES.includes(chain.state)) return;
|
|
679
760
|
this.deps.controller.resumeChain(chain.id);
|
|
@@ -695,4 +776,15 @@ export class WrfcPanel extends BasePanel {
|
|
|
695
776
|
const last = this.lastEventAt.get(chain.id) ?? chain.createdAt;
|
|
696
777
|
return (now - last) >= STALL_THRESHOLD_MS;
|
|
697
778
|
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Elapsed time since createdAt for an active chain, or total duration from
|
|
782
|
+
* createdAt to completedAt for a terminal one. createdAt/completedAt are
|
|
783
|
+
* already consumed for chain sort order (syncFromController) — this just
|
|
784
|
+
* surfaces the same fields as a human-readable label.
|
|
785
|
+
*/
|
|
786
|
+
private durationLabel(chain: WrfcChain, now: number): string {
|
|
787
|
+
const end = chain.completedAt ?? now;
|
|
788
|
+
return formatDuration(Math.max(0, end - chain.createdAt));
|
|
789
|
+
}
|
|
698
790
|
}
|