@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,35 +1,40 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
4
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
|
-
import type { RuntimeTask, TaskLifecycleState } from '@/runtime/index.ts';
|
|
5
|
+
import type { OpsApi, RuntimeTask, TaskLifecycleState } from '@/runtime/index.ts';
|
|
5
6
|
import type { ManagedWorktreeMeta } from '@/runtime/index.ts';
|
|
6
7
|
import type { UiReadModel, UiTasksSnapshot, UiWorktreeSnapshot } from '../runtime/ui-read-models.ts';
|
|
8
|
+
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
9
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
7
10
|
import {
|
|
8
|
-
buildDetailBlock,
|
|
9
11
|
buildEmptyState,
|
|
10
12
|
buildGuidanceLine,
|
|
13
|
+
buildKeyboardHints,
|
|
11
14
|
buildPanelListRow,
|
|
12
15
|
buildPanelLine,
|
|
13
16
|
buildSummaryBlock,
|
|
14
17
|
buildPanelWorkspace,
|
|
18
|
+
resolveScrollablePanelSection,
|
|
15
19
|
DEFAULT_PANEL_PALETTE,
|
|
20
|
+
extendPalette,
|
|
16
21
|
} from './polish.ts';
|
|
17
22
|
import { formatElapsed } from '../utils/format-elapsed.ts';
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
running: '#22c55e',
|
|
25
|
-
blocked: '#f59e0b',
|
|
26
|
-
failed: '#ef4444',
|
|
27
|
-
completed: '#a78bfa',
|
|
28
|
-
selectBg: '#0f172a',
|
|
29
|
-
} as const;
|
|
24
|
+
// Domain accents only; base chrome (header/headerBg/info/good/warn/bad/
|
|
25
|
+
// selectBg) comes from DEFAULT_PANEL_PALETTE.
|
|
26
|
+
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
27
|
+
completed: '#a78bfa', // completed-task badge — distinct from running/good
|
|
28
|
+
} as const);
|
|
30
29
|
|
|
31
30
|
const STATUS_ORDER: TaskLifecycleState[] = ['queued', 'running', 'blocked', 'failed', 'completed'];
|
|
32
31
|
|
|
32
|
+
/** The follow-up action Enter dispatches once the panel-integration router runs, per WO-131. */
|
|
33
|
+
type TaskFollowUp =
|
|
34
|
+
| { readonly kind: 'agent-jump'; readonly agentId: string }
|
|
35
|
+
| { readonly kind: 'worktree-review'; readonly taskId: string }
|
|
36
|
+
| { readonly kind: 'teamwork-review' };
|
|
37
|
+
|
|
33
38
|
function formatWhen(value?: number): string {
|
|
34
39
|
return value ? new Date(value).toLocaleString() : 'n/a';
|
|
35
40
|
}
|
|
@@ -55,13 +60,13 @@ function kindLabel(kind: RuntimeTask['kind']): string {
|
|
|
55
60
|
function statusColor(status: TaskLifecycleState): string {
|
|
56
61
|
switch (status) {
|
|
57
62
|
case 'queued':
|
|
58
|
-
return C.
|
|
63
|
+
return C.info;
|
|
59
64
|
case 'running':
|
|
60
|
-
return C.
|
|
65
|
+
return C.good;
|
|
61
66
|
case 'blocked':
|
|
62
|
-
return C.
|
|
67
|
+
return C.warn;
|
|
63
68
|
case 'failed':
|
|
64
|
-
return C.
|
|
69
|
+
return C.bad;
|
|
65
70
|
case 'completed':
|
|
66
71
|
return C.completed;
|
|
67
72
|
case 'cancelled':
|
|
@@ -144,26 +149,39 @@ function reviewTaskWorktreeAttachments(
|
|
|
144
149
|
export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
|
|
145
150
|
private readonly readModel?: UiReadModel<UiTasksSnapshot>;
|
|
146
151
|
private readonly worktrees?: UiReadModel<UiWorktreeSnapshot>;
|
|
152
|
+
private readonly opsApi?: OpsApi;
|
|
147
153
|
private readonly unsubscribers: readonly (() => void)[];
|
|
148
154
|
|
|
155
|
+
/** Task id currently expanded into the detail view, or null while browsing the list. */
|
|
156
|
+
private _detailTaskId: string | null = null;
|
|
157
|
+
private _detailScroll = 0;
|
|
158
|
+
|
|
159
|
+
/** Pending cancel confirmation — subject is the task id being cancelled. */
|
|
160
|
+
private _confirm: ConfirmState<string> | null = null;
|
|
161
|
+
|
|
162
|
+
/** Set by handleInput on Enter; consumed by the panel-integration router immediately after. */
|
|
163
|
+
private _pendingFollowUp: TaskFollowUp | null = null;
|
|
164
|
+
|
|
149
165
|
public constructor(
|
|
150
166
|
readModel: UiReadModel<UiTasksSnapshot> | undefined,
|
|
151
167
|
worktrees?: UiReadModel<UiWorktreeSnapshot>,
|
|
168
|
+
opsApi?: OpsApi,
|
|
152
169
|
) {
|
|
153
|
-
super('tasks', 'Tasks', '
|
|
170
|
+
super('tasks', 'Tasks', '▦', 'runtime-ops');
|
|
154
171
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
172
|
+
this.filterEnabled = true;
|
|
173
|
+
this.filterLabel = 'Filter tasks';
|
|
155
174
|
this.readModel = readModel;
|
|
156
175
|
this.worktrees = worktrees;
|
|
176
|
+
this.opsApi = opsApi;
|
|
157
177
|
this.unsubscribers = [
|
|
158
178
|
readModel?.subscribe(() => this.markDirty()),
|
|
159
179
|
worktrees?.subscribe(() => this.markDirty()),
|
|
160
180
|
].filter((unsubscribe): unsubscribe is () => void => Boolean(unsubscribe));
|
|
161
181
|
}
|
|
162
182
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
this.selectedIndex = 0;
|
|
166
|
-
}
|
|
183
|
+
// WO-131: selection is preserved across onActivate — BasePanel's default
|
|
184
|
+
// (mark dirty, no index reset) is exactly what we want, so no override.
|
|
167
185
|
|
|
168
186
|
public override onDestroy(): void {
|
|
169
187
|
for (const unsubscribe of this.unsubscribers) unsubscribe();
|
|
@@ -178,6 +196,13 @@ export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
|
|
|
178
196
|
];
|
|
179
197
|
}
|
|
180
198
|
|
|
199
|
+
protected override filterMatches(task: RuntimeTask, q: string): boolean {
|
|
200
|
+
return task.title.toLowerCase().includes(q)
|
|
201
|
+
|| task.status.toLowerCase().includes(q)
|
|
202
|
+
|| task.id.toLowerCase().includes(q)
|
|
203
|
+
|| String(task.kind).toLowerCase().includes(q);
|
|
204
|
+
}
|
|
205
|
+
|
|
181
206
|
protected getItems(): readonly RuntimeTask[] {
|
|
182
207
|
if (!this.readModel) return [];
|
|
183
208
|
return sortTasks([...this.readModel.getSnapshot().tasks]);
|
|
@@ -188,29 +213,192 @@ export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
|
|
|
188
213
|
{ text: task.status.padEnd(10), fg: statusColor(task.status) },
|
|
189
214
|
{ text: ` ${kindLabel(task.kind).padEnd(12)}`, fg: C.value },
|
|
190
215
|
{ text: ` ${task.id.slice(0, 8)} `, fg: C.dim },
|
|
191
|
-
{ text: task.title
|
|
216
|
+
{ text: truncateDisplay(task.title, Math.max(0, width - 37)), fg: C.value },
|
|
192
217
|
], C, { selected });
|
|
193
218
|
}
|
|
194
219
|
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Input
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
195
224
|
public handleInput(key: string): boolean {
|
|
225
|
+
if (this.lastError !== null) this.clearError();
|
|
226
|
+
|
|
227
|
+
if (this._confirm) {
|
|
228
|
+
const outcome = handleConfirmInput(this._confirm, key);
|
|
229
|
+
if (outcome === 'confirmed') {
|
|
230
|
+
this._executeCancel(this._confirm.subject);
|
|
231
|
+
this._confirm = null;
|
|
232
|
+
this.markDirty();
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
if (outcome === 'cancelled') {
|
|
236
|
+
this._confirm = null;
|
|
237
|
+
this.markDirty();
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
return true; // absorbed — keep the confirm dialog pending
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const tasks = this.getVisibleItems();
|
|
244
|
+
|
|
245
|
+
// --- Detail mode: WO-131 moves the deep per-task field dump behind an
|
|
246
|
+
// Enter-toggled view so the list itself keeps full viewport in normal
|
|
247
|
+
// browsing. Esc/Left collapses back to the list; Enter (again, while
|
|
248
|
+
// already expanded) dispatches the task's advertised worktree follow-up.
|
|
249
|
+
if (this._detailTaskId !== null) {
|
|
250
|
+
if (key === 'escape' || key === 'left') {
|
|
251
|
+
this._detailTaskId = null;
|
|
252
|
+
this._detailScroll = 0;
|
|
253
|
+
this.markDirty();
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
if (key === 'enter' || key === 'return') {
|
|
257
|
+
const task = tasks.find((t) => t.id === this._detailTaskId);
|
|
258
|
+
if (task) this._pendingFollowUp = { kind: 'worktree-review', taskId: task.id };
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
switch (key) {
|
|
262
|
+
case 'up':
|
|
263
|
+
case 'k':
|
|
264
|
+
this._detailScroll = Math.max(0, this._detailScroll - 1);
|
|
265
|
+
this.markDirty();
|
|
266
|
+
return true;
|
|
267
|
+
case 'down':
|
|
268
|
+
case 'j':
|
|
269
|
+
this._detailScroll += 1;
|
|
270
|
+
this.markDirty();
|
|
271
|
+
return true;
|
|
272
|
+
case 'pageup':
|
|
273
|
+
this._detailScroll = Math.max(0, this._detailScroll - this.getPageSize());
|
|
274
|
+
this.markDirty();
|
|
275
|
+
return true;
|
|
276
|
+
case 'pagedown':
|
|
277
|
+
this._detailScroll += this.getPageSize();
|
|
278
|
+
this.markDirty();
|
|
279
|
+
return true;
|
|
280
|
+
case 'home':
|
|
281
|
+
case 'g':
|
|
282
|
+
this._detailScroll = 0;
|
|
283
|
+
this.markDirty();
|
|
284
|
+
return true;
|
|
285
|
+
default:
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// --- List mode ---
|
|
196
291
|
if (key === 'home') {
|
|
197
292
|
this.selectedIndex = 0;
|
|
198
293
|
this.markDirty();
|
|
199
294
|
return true;
|
|
200
295
|
}
|
|
201
296
|
if (key === 'end') {
|
|
202
|
-
const tasks = this.getItems();
|
|
203
297
|
this.selectedIndex = Math.max(0, tasks.length - 1);
|
|
204
298
|
this.markDirty();
|
|
205
299
|
return true;
|
|
206
300
|
}
|
|
301
|
+
|
|
302
|
+
if (key === 'c' && !this.filterActive) {
|
|
303
|
+
const task = this.getSelectedItem();
|
|
304
|
+
if (!task || !task.cancellable) return false;
|
|
305
|
+
this._confirm = { subject: task.id, label: `task "${task.title}"`, verb: 'Cancel' };
|
|
306
|
+
this.markDirty();
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// The task-family posture surface is bound for real: w stages a follow-up
|
|
311
|
+
// the integration router dispatches as '/teamwork review' via executeCommand.
|
|
312
|
+
if (key === 'w' && !this.filterActive) {
|
|
313
|
+
this._pendingFollowUp = { kind: 'teamwork-review' };
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Enter commits the filter query while actively typing (ScrollableListPanel
|
|
318
|
+
// contract) — only treated as detail-mode/agent-jump once filtering isn't active.
|
|
319
|
+
if ((key === 'enter' || key === 'return') && !this.filterActive) {
|
|
320
|
+
const task = this.getSelectedItem();
|
|
321
|
+
if (!task) return false;
|
|
322
|
+
// Agent-kind tasks jump straight to the Agent Inspector — that panel
|
|
323
|
+
// already owns the deep per-agent timeline, so Tasks does not duplicate it.
|
|
324
|
+
if (task.kind === 'agent') {
|
|
325
|
+
this._pendingFollowUp = { kind: 'agent-jump', agentId: task.owner };
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
this._detailTaskId = task.id;
|
|
329
|
+
this._detailScroll = 0;
|
|
330
|
+
this.markDirty();
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
|
|
207
334
|
return super.handleInput(key);
|
|
208
335
|
}
|
|
209
336
|
|
|
337
|
+
/**
|
|
338
|
+
* Consumed by the panel-integration router immediately after handleInput
|
|
339
|
+
* returns true for the same Enter keystroke. Returns and clears whatever
|
|
340
|
+
* follow-up (if any) that keystroke queued.
|
|
341
|
+
*/
|
|
342
|
+
public consumePendingFollowUp(): TaskFollowUp | null {
|
|
343
|
+
const pending = this._pendingFollowUp;
|
|
344
|
+
this._pendingFollowUp = null;
|
|
345
|
+
return pending;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private _executeCancel(taskId: string): void {
|
|
349
|
+
if (!this.opsApi) {
|
|
350
|
+
this.setError('Ops API is not wired for this runtime.');
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
this.opsApi.tasks.cancel(taskId);
|
|
355
|
+
} catch (err) {
|
|
356
|
+
this.setError(summarizeError(err));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
// Render
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
|
|
210
364
|
public render(width: number, height: number): Line[] {
|
|
211
365
|
this.clampSelection();
|
|
366
|
+
|
|
367
|
+
if (this._detailTaskId !== null) {
|
|
368
|
+
const task = this.getVisibleItems().find((t) => t.id === this._detailTaskId);
|
|
369
|
+
if (task) return this._renderDetail(width, height, task);
|
|
370
|
+
this._detailTaskId = null; // task vanished (pruned/completed away) — fall back to list
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return this._renderListView(width, height);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private _renderListView(width: number, height: number): Line[] {
|
|
212
377
|
const intro = 'Live task lifecycle, ownership, retries, and result/error details across runtime execution domains.';
|
|
213
|
-
const
|
|
378
|
+
const visibleTasks = this.getVisibleItems();
|
|
379
|
+
const selected = this.getSelectedItem();
|
|
380
|
+
// Context-aware footer: position + only the keys that apply in the current
|
|
381
|
+
// (filtering vs browsing) state, and to the selected task's kind/cancellable flag.
|
|
382
|
+
const browseHints: Array<{ keys: string; label: string }> = [
|
|
383
|
+
{ keys: visibleTasks.length > 0 ? `${this.selectedIndex + 1}/${visibleTasks.length}` : '0/0', label: 'task' },
|
|
384
|
+
{ keys: '↑/↓', label: 'move' },
|
|
385
|
+
{ keys: 'Home/End', label: 'jump' },
|
|
386
|
+
{ keys: '/', label: 'filter' },
|
|
387
|
+
{ keys: 'w', label: 'teamwork review' },
|
|
388
|
+
];
|
|
389
|
+
if (selected) {
|
|
390
|
+
browseHints.push({ keys: 'Enter', label: selected.kind === 'agent' ? 'agent detail' : 'task detail' });
|
|
391
|
+
if (selected.cancellable) browseHints.push({ keys: 'c', label: 'cancel' });
|
|
392
|
+
}
|
|
393
|
+
const footerLines = [
|
|
394
|
+
this.filterActive
|
|
395
|
+
? buildKeyboardHints(width, [
|
|
396
|
+
{ keys: 'type', label: 'filter tasks' },
|
|
397
|
+
{ keys: 'Enter', label: 'apply' },
|
|
398
|
+
{ keys: 'Esc', label: 'clear' },
|
|
399
|
+
], C)
|
|
400
|
+
: buildKeyboardHints(width, browseHints, C),
|
|
401
|
+
];
|
|
214
402
|
|
|
215
403
|
if (!this.readModel) {
|
|
216
404
|
const workspace = buildPanelWorkspace(width, height, {
|
|
@@ -245,19 +433,18 @@ export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
|
|
|
245
433
|
const postureLines: Line[] = [
|
|
246
434
|
buildPanelLine(width, [
|
|
247
435
|
[' queued ', C.label],
|
|
248
|
-
[String(queuedCount), queuedCount > 0 ? C.
|
|
436
|
+
[String(queuedCount), queuedCount > 0 ? C.info : C.dim],
|
|
249
437
|
[' running ', C.label],
|
|
250
|
-
[String(runningCount), runningCount > 0 ? C.
|
|
438
|
+
[String(runningCount), runningCount > 0 ? C.good : C.dim],
|
|
251
439
|
[' blocked ', C.label],
|
|
252
|
-
[String(blockedCount), blockedCount > 0 ? C.
|
|
440
|
+
[String(blockedCount), blockedCount > 0 ? C.warn : C.dim],
|
|
253
441
|
[' failed ', C.label],
|
|
254
|
-
[String(failedCount), failedCount > 0 ? C.
|
|
442
|
+
[String(failedCount), failedCount > 0 ? C.bad : C.dim],
|
|
255
443
|
[' completed ', C.label],
|
|
256
444
|
[String(completedCount), completedCount > 0 ? C.completed : C.dim],
|
|
257
445
|
]),
|
|
258
446
|
];
|
|
259
447
|
|
|
260
|
-
const selected = tasks[this.selectedIndex];
|
|
261
448
|
if (selected) {
|
|
262
449
|
postureLines.push(buildPanelLine(width, [
|
|
263
450
|
[' selected ', C.label],
|
|
@@ -267,128 +454,155 @@ export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
|
|
|
267
454
|
[' kind ', C.label],
|
|
268
455
|
[selected.kind, C.value],
|
|
269
456
|
[' owner ', C.label],
|
|
270
|
-
[selected.owner
|
|
457
|
+
[truncateDisplay(selected.owner, Math.max(0, width - 46)), C.dim],
|
|
271
458
|
]));
|
|
272
459
|
}
|
|
273
460
|
postureLines.push(
|
|
274
|
-
buildGuidanceLine(width, '/teamwork review', 'inspect task-family posture, archetype metadata, and recovery options for active work', C),
|
|
275
|
-
buildGuidanceLine(width, '/worktree task <task-id>', 'review worktree ownership, restore, and merge posture for the selected task', C),
|
|
461
|
+
buildGuidanceLine(width, '/teamwork review', 'inspect task-family posture, archetype metadata, and recovery options for active work — press w to run it', C),
|
|
276
462
|
);
|
|
277
463
|
|
|
464
|
+
const headerLines: Line[] = buildSummaryBlock(width, 'Task posture', postureLines, C);
|
|
465
|
+
|
|
466
|
+
const effectiveFooter = this._confirm
|
|
467
|
+
? [...renderConfirmLines(width, this._confirm), ...footerLines]
|
|
468
|
+
: footerLines;
|
|
469
|
+
|
|
470
|
+
return this.renderList(width, height, {
|
|
471
|
+
title: 'Task Control Room',
|
|
472
|
+
header: headerLines,
|
|
473
|
+
footer: effectiveFooter,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Detail-mode body for one task — the deep field dump previously always shown inline. */
|
|
478
|
+
private _renderDetail(width: number, height: number, task: RuntimeTask): Line[] {
|
|
479
|
+
this.needsRender = false;
|
|
480
|
+
const descriptor = task.description ? parseTaskDescriptor(task.description) : null;
|
|
278
481
|
const detailRows: Line[] = [];
|
|
279
|
-
|
|
280
|
-
|
|
482
|
+
detailRows.push(buildPanelLine(width, [
|
|
483
|
+
[' Title: ', C.label],
|
|
484
|
+
[task.title, C.value],
|
|
485
|
+
[' Status: ', C.label],
|
|
486
|
+
[task.status, statusColor(task.status)],
|
|
487
|
+
[' Kind: ', C.label],
|
|
488
|
+
[task.kind, C.value],
|
|
489
|
+
]));
|
|
490
|
+
detailRows.push(buildPanelLine(width, [
|
|
491
|
+
[' Owner: ', C.label],
|
|
492
|
+
[task.owner, C.value],
|
|
493
|
+
[' Cancellable: ', C.label],
|
|
494
|
+
[task.cancellable ? 'yes' : 'no', task.cancellable ? C.good : C.bad],
|
|
495
|
+
[' Queue: ', C.label],
|
|
496
|
+
[formatWhen(task.queuedAt), C.dim],
|
|
497
|
+
]));
|
|
498
|
+
detailRows.push(buildPanelLine(width, [
|
|
499
|
+
[' Started: ', C.label],
|
|
500
|
+
[formatWhen(task.startedAt), C.dim],
|
|
501
|
+
[' Ended: ', C.label],
|
|
502
|
+
[formatWhen(task.endedAt), C.dim],
|
|
503
|
+
[' Duration: ', C.label],
|
|
504
|
+
[formatDuration(task.startedAt, task.endedAt), C.dim],
|
|
505
|
+
]));
|
|
506
|
+
if (descriptor?.mode || descriptor?.family || descriptor?.source) {
|
|
281
507
|
detailRows.push(buildPanelLine(width, [
|
|
282
|
-
['
|
|
283
|
-
[
|
|
284
|
-
['
|
|
285
|
-
[
|
|
286
|
-
['
|
|
287
|
-
[
|
|
508
|
+
[' Mode: ', C.label],
|
|
509
|
+
[descriptor?.mode ?? 'n/a', C.value],
|
|
510
|
+
[' Family: ', C.label],
|
|
511
|
+
[descriptor?.family ?? 'n/a', C.info],
|
|
512
|
+
[' Source: ', C.label],
|
|
513
|
+
[descriptor?.source ?? 'builtin/runtime', C.dim],
|
|
288
514
|
]));
|
|
515
|
+
}
|
|
516
|
+
if (descriptor?.reviewMode || descriptor?.executionProtocol || descriptor?.template) {
|
|
289
517
|
detailRows.push(buildPanelLine(width, [
|
|
290
|
-
['
|
|
291
|
-
[
|
|
292
|
-
['
|
|
293
|
-
[
|
|
294
|
-
['
|
|
295
|
-
[
|
|
518
|
+
[' Review: ', C.label],
|
|
519
|
+
[descriptor?.reviewMode ?? 'n/a', C.value],
|
|
520
|
+
[' Protocol: ', C.label],
|
|
521
|
+
[descriptor?.executionProtocol ?? 'n/a', C.value],
|
|
522
|
+
[' Template: ', C.label],
|
|
523
|
+
[descriptor?.template ?? 'n/a', C.dim],
|
|
296
524
|
]));
|
|
525
|
+
}
|
|
526
|
+
if (task.correlationId || task.turnId) {
|
|
297
527
|
detailRows.push(buildPanelLine(width, [
|
|
298
|
-
['
|
|
299
|
-
[
|
|
300
|
-
['
|
|
301
|
-
[
|
|
302
|
-
[' Duration: ', C.label],
|
|
303
|
-
[formatDuration(selected.startedAt, selected.endedAt), C.dim],
|
|
528
|
+
[' Correlation: ', C.label],
|
|
529
|
+
[task.correlationId ?? 'n/a', C.dim],
|
|
530
|
+
[' Turn: ', C.label],
|
|
531
|
+
[task.turnId ?? 'n/a', C.dim],
|
|
304
532
|
]));
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (selected.correlationId || selected.turnId) {
|
|
326
|
-
detailRows.push(buildPanelLine(width, [
|
|
327
|
-
[' Correlation: ', C.label],
|
|
328
|
-
[selected.correlationId ?? 'n/a', C.dim],
|
|
329
|
-
[' Turn: ', C.label],
|
|
330
|
-
[selected.turnId ?? 'n/a', C.dim],
|
|
331
|
-
]));
|
|
332
|
-
}
|
|
333
|
-
if (selected.parentTaskId || selected.childTaskIds.length > 0) {
|
|
334
|
-
detailRows.push(buildPanelLine(width, [
|
|
335
|
-
[' Parent: ', C.label],
|
|
336
|
-
[selected.parentTaskId ?? 'none', C.dim],
|
|
337
|
-
[' Children: ', C.label],
|
|
338
|
-
[selected.childTaskIds.length > 0 ? selected.childTaskIds.join(', ') : 'none', C.dim],
|
|
339
|
-
]));
|
|
340
|
-
}
|
|
341
|
-
const attachedWorktrees = reviewTaskWorktreeAttachments(selected.id, this.worktrees);
|
|
342
|
-
if (attachedWorktrees.total > 0) {
|
|
343
|
-
detailRows.push(buildPanelLine(width, [
|
|
344
|
-
[' Worktrees: ', C.label],
|
|
345
|
-
[`${attachedWorktrees.total} tracked`, C.info],
|
|
346
|
-
[' Active: ', C.label],
|
|
347
|
-
[String(attachedWorktrees.active), attachedWorktrees.active > 0 ? C.running : C.dim],
|
|
348
|
-
[' Paused: ', C.label],
|
|
349
|
-
[String(attachedWorktrees.paused), attachedWorktrees.paused > 0 ? C.blocked : C.dim],
|
|
350
|
-
]));
|
|
533
|
+
}
|
|
534
|
+
if (task.parentTaskId || task.childTaskIds.length > 0) {
|
|
535
|
+
detailRows.push(buildPanelLine(width, [
|
|
536
|
+
[' Parent: ', C.label],
|
|
537
|
+
[task.parentTaskId ?? 'none', C.dim],
|
|
538
|
+
[' Children: ', C.label],
|
|
539
|
+
[task.childTaskIds.length > 0 ? task.childTaskIds.join(', ') : 'none', C.dim],
|
|
540
|
+
]));
|
|
541
|
+
}
|
|
542
|
+
const attachedWorktrees = reviewTaskWorktreeAttachments(task.id, this.worktrees);
|
|
543
|
+
if (attachedWorktrees.total > 0) {
|
|
544
|
+
detailRows.push(buildPanelLine(width, [
|
|
545
|
+
[' Worktrees: ', C.label],
|
|
546
|
+
[`${attachedWorktrees.total} tracked`, C.info],
|
|
547
|
+
[' Active: ', C.label],
|
|
548
|
+
[String(attachedWorktrees.active), attachedWorktrees.active > 0 ? C.good : C.dim],
|
|
549
|
+
[' Paused: ', C.label],
|
|
550
|
+
[String(attachedWorktrees.paused), attachedWorktrees.paused > 0 ? C.warn : C.dim],
|
|
551
|
+
]));
|
|
552
|
+
for (const record of attachedWorktrees.records.slice(0, 2)) {
|
|
351
553
|
detailRows.push(buildPanelLine(width, [[
|
|
352
|
-
`
|
|
353
|
-
C.dim,
|
|
554
|
+
truncateDisplay(` ${record.state.padEnd(15)} ${record.path}`, Math.max(0, width - 2)),
|
|
555
|
+
record.state === 'active' ? C.good : record.state === 'paused' ? C.warn : C.dim,
|
|
354
556
|
]]));
|
|
355
|
-
for (const record of attachedWorktrees.records.slice(0, 2)) {
|
|
356
|
-
detailRows.push(buildPanelLine(width, [[
|
|
357
|
-
` ${record.state.padEnd(15)} ${record.path}`.slice(0, Math.max(0, width - 2)),
|
|
358
|
-
record.state === 'active' ? C.running : record.state === 'paused' ? C.blocked : C.dim,
|
|
359
|
-
]]));
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
if (selected.retryPolicy) {
|
|
363
|
-
detailRows.push(buildPanelLine(width, [
|
|
364
|
-
[' Retry: ', C.label],
|
|
365
|
-
[`${selected.retryPolicy.currentAttempt}/${selected.retryPolicy.maxAttempts} ${selected.retryPolicy.backoff}`, C.value],
|
|
366
|
-
]));
|
|
367
|
-
}
|
|
368
|
-
if (selected.error) {
|
|
369
|
-
detailRows.push(buildPanelLine(width, [
|
|
370
|
-
[' Error: ', C.label],
|
|
371
|
-
[selected.error.slice(0, Math.max(0, width - 10)), C.failed],
|
|
372
|
-
]));
|
|
373
|
-
}
|
|
374
|
-
if (selected.result !== undefined) {
|
|
375
|
-
const resultText = safeJson(selected.result);
|
|
376
|
-
detailRows.push(buildPanelLine(width, [
|
|
377
|
-
[' Result: ', C.label],
|
|
378
|
-
[resultText.slice(0, Math.max(0, width - 11)), C.dim],
|
|
379
|
-
]));
|
|
380
557
|
}
|
|
381
558
|
}
|
|
559
|
+
if (task.retryPolicy) {
|
|
560
|
+
detailRows.push(buildPanelLine(width, [
|
|
561
|
+
[' Retry: ', C.label],
|
|
562
|
+
[`${task.retryPolicy.currentAttempt}/${task.retryPolicy.maxAttempts} ${task.retryPolicy.backoff}`, C.value],
|
|
563
|
+
]));
|
|
564
|
+
}
|
|
565
|
+
if (task.error) {
|
|
566
|
+
detailRows.push(buildPanelLine(width, [
|
|
567
|
+
[' Error: ', C.label],
|
|
568
|
+
[truncateDisplay(task.error, Math.max(0, width - 10)), C.bad],
|
|
569
|
+
]));
|
|
570
|
+
}
|
|
571
|
+
if (task.result !== undefined) {
|
|
572
|
+
const resultText = safeJson(task.result);
|
|
573
|
+
detailRows.push(buildPanelLine(width, [
|
|
574
|
+
[' Result: ', C.label],
|
|
575
|
+
[truncateDisplay(resultText, Math.max(0, width - 11)), C.dim],
|
|
576
|
+
]));
|
|
577
|
+
}
|
|
382
578
|
|
|
383
|
-
|
|
579
|
+
// The actionable follow-up replaces the old printed "/worktree task <task-id>"
|
|
580
|
+
// signpost — pressing Enter here dispatches it for real via ctx.executeCommand.
|
|
581
|
+
detailRows.push(buildGuidanceLine(width, `/worktree task ${task.id}`, 'review worktree ownership, restore, and merge posture for this task — press Enter to run it', C));
|
|
384
582
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
583
|
+
const footer = buildKeyboardHints(width, [
|
|
584
|
+
{ keys: '↑/↓', label: 'scroll' },
|
|
585
|
+
{ keys: 'Enter', label: 'review worktree ownership' },
|
|
586
|
+
{ keys: 'Esc', label: 'back' },
|
|
587
|
+
], C);
|
|
588
|
+
|
|
589
|
+
const resolved = resolveScrollablePanelSection(width, height, {
|
|
590
|
+
palette: C,
|
|
591
|
+
afterSections: [{ lines: [footer] }],
|
|
592
|
+
section: {
|
|
593
|
+
scrollableLines: detailRows,
|
|
594
|
+
scrollOffset: this._detailScroll,
|
|
595
|
+
appendWindowSummary: detailRows.length > 5 ? { dimColor: C.dim } : undefined,
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
this._detailScroll = resolved.scrollOffset;
|
|
599
|
+
|
|
600
|
+
const lines = buildPanelWorkspace(width, height, {
|
|
601
|
+
title: `Task · ${task.title}`,
|
|
602
|
+
sections: [resolved.section, { lines: [footer] }],
|
|
603
|
+
palette: C,
|
|
392
604
|
});
|
|
605
|
+
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
606
|
+
return lines.slice(0, height);
|
|
393
607
|
}
|
|
394
608
|
}
|