@agwab/pi-workflow 0.2.0 → 0.3.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/README.md +2 -0
- package/dist/compiler.d.ts +4 -6
- package/dist/compiler.js +70 -39
- package/dist/dynamic-decision.d.ts +0 -1
- package/dist/dynamic-decision.js +0 -7
- package/dist/dynamic-generated-task-runtime.d.ts +2 -0
- package/dist/dynamic-generated-task-runtime.js +21 -8
- package/dist/dynamic-profiles.d.ts +0 -1
- package/dist/dynamic-profiles.js +0 -3
- package/dist/engine-run-graph.d.ts +1 -0
- package/dist/engine-run-graph.js +142 -2
- package/dist/engine.d.ts +10 -6
- package/dist/engine.js +146 -77
- package/dist/extension.d.ts +2 -1
- package/dist/extension.js +38 -15
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -1
- package/dist/store.d.ts +3 -1
- package/dist/store.js +189 -49
- package/dist/subagent-backend.d.ts +4 -0
- package/dist/subagent-backend.js +281 -31
- package/dist/types.d.ts +9 -1
- package/dist/workflow-runtime.d.ts +2 -0
- package/dist/workflow-runtime.js +40 -1
- package/dist/workflow-view.js +3 -1
- package/dist/workflow-web-source-extension.js +167 -48
- package/dist/workflow-web-source.d.ts +2 -1
- package/dist/workflow-web-source.js +84 -19
- package/docs/usage.md +11 -0
- package/node_modules/@agwab/pi-subagent/README.md +3 -3
- package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
- package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
- package/node_modules/@agwab/pi-subagent/package.json +2 -2
- package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
- package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
- package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
- package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
- package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
- package/node_modules/@agwab/pi-subagent/src/index.ts +995 -573
- package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
- package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
- package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
- package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
- package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
- package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
- package/node_modules/@agwab/pi-subagent/src/panel.ts +1352 -560
- package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
- package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
- package/package.json +2 -2
- package/src/compiler.ts +127 -66
- package/src/dynamic-decision.ts +0 -11
- package/src/dynamic-generated-task-runtime.ts +47 -12
- package/src/dynamic-profiles.ts +0 -4
- package/src/engine-run-graph.ts +185 -2
- package/src/engine.ts +192 -107
- package/src/extension.ts +50 -17
- package/src/index.ts +3 -1
- package/src/store.ts +253 -55
- package/src/subagent-backend.ts +369 -32
- package/src/types.ts +13 -1
- package/src/workflow-runtime.ts +53 -2
- package/src/workflow-view.ts +2 -1
- package/src/workflow-web-source-extension.ts +621 -228
- package/src/workflow-web-source.ts +118 -28
- package/workflows/deep-research/helpers/claim-evidence-gate.mjs +56 -16
- package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
- package/workflows/deep-research/helpers/normalize-input-packet.mjs +1 -1
- package/workflows/deep-research/helpers/render-executive.mjs +8 -21
- package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
- package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +0 -1
- package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +4 -1
- package/workflows/impact-review/spec.json +3 -3
- package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
- package/dist/dynamic-loader.d.ts +0 -25
- package/dist/dynamic-loader.js +0 -13
- package/src/dynamic-loader.ts +0 -49
- package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
- package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
- package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
|
@@ -1,79 +1,111 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
-
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
2
|
+
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import type { Component } from "@earendil-works/pi-tui";
|
|
4
4
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import type { ResultEnvelope } from "./artifacts/index.ts";
|
|
5
|
+
import type { ResultEnvelope, RunEvent } from "./artifacts/index.ts";
|
|
6
6
|
import type { Status } from "./core/constants.ts";
|
|
7
|
+
import { listRunLocators, type RunRefLocator } from "./orchestrate/run-ref.ts";
|
|
8
|
+
import {
|
|
9
|
+
summarizeChildEvents,
|
|
10
|
+
type RunChildSummary,
|
|
11
|
+
} from "./orchestrate/status.ts";
|
|
7
12
|
|
|
8
13
|
const DEFAULT_RUNS_DIR = ".pi/agent/runs";
|
|
9
14
|
const LIVE_REFRESH_MS = 1_500;
|
|
10
15
|
const LOG_TAIL_LINES = 5;
|
|
16
|
+
const STALE_RUN_AFTER_MS = 30_000;
|
|
17
|
+
const PANEL_MIN_LINES = 12;
|
|
18
|
+
const PANEL_MAX_LINES = 30;
|
|
19
|
+
const PANEL_RESERVED_TUI_LINES = 8;
|
|
20
|
+
const DEFAULT_RECENT_TERMINAL_LIMIT = 20;
|
|
21
|
+
const ALL_SCOPE_RECENT_TERMINAL_LIMIT = 50;
|
|
11
22
|
|
|
12
|
-
type
|
|
23
|
+
type ScopeFilter = "session" | "cwd" | "all";
|
|
24
|
+
type StatusFilter = "all" | "running" | "completed" | "failed";
|
|
25
|
+
type FocusGroup = "scope" | "status" | "detail";
|
|
13
26
|
|
|
14
27
|
interface TaskRow {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
attemptId: string;
|
|
29
|
+
status: Status;
|
|
30
|
+
backend: string;
|
|
31
|
+
failureKind: string | null;
|
|
32
|
+
startedAt: string;
|
|
33
|
+
completedAt: string | null;
|
|
34
|
+
durationMs: number | null;
|
|
35
|
+
resultPath: string;
|
|
36
|
+
logPath: string | null;
|
|
37
|
+
logTail: string[];
|
|
38
|
+
workspace: string;
|
|
39
|
+
worktreePath: string | null;
|
|
40
|
+
modelLabel: string;
|
|
28
41
|
}
|
|
29
42
|
|
|
30
43
|
interface RunRow {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
key: string;
|
|
45
|
+
runId: string;
|
|
46
|
+
sourceCwd: string;
|
|
47
|
+
runsDir: string;
|
|
48
|
+
status: Status;
|
|
49
|
+
backend: string;
|
|
50
|
+
updatedMs: number;
|
|
51
|
+
startedAt: string;
|
|
52
|
+
completedAt: string | null;
|
|
53
|
+
dependency: string | null;
|
|
54
|
+
eventTail: string[];
|
|
55
|
+
childSummary?: RunChildSummary;
|
|
56
|
+
tasks: TaskRow[];
|
|
40
57
|
}
|
|
41
58
|
|
|
42
59
|
interface PanelSnapshot {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
60
|
+
runs: RunRow[];
|
|
61
|
+
totalRuns: number;
|
|
62
|
+
hiddenRuns: number;
|
|
63
|
+
loadedAt: Date;
|
|
64
|
+
staleLocators: number;
|
|
65
|
+
invalidLocators: number;
|
|
66
|
+
skippedLocators: number;
|
|
46
67
|
}
|
|
47
68
|
|
|
48
69
|
interface PanelTheme {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
70
|
+
// Method signatures are intentionally used so the host Theme (with a narrower
|
|
71
|
+
// ThemeColor union) remains assignable under bivariant method checks.
|
|
72
|
+
fg?(color: string, text: string): string;
|
|
73
|
+
bg?(color: string, text: string): string;
|
|
74
|
+
bold?(text: string): string;
|
|
54
75
|
}
|
|
55
76
|
|
|
56
77
|
interface PanelTui {
|
|
57
|
-
|
|
78
|
+
requestRender?: () => void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface LoadOptions {
|
|
82
|
+
cwd: string;
|
|
83
|
+
scope: ScopeFilter;
|
|
84
|
+
statusFilter: StatusFilter;
|
|
85
|
+
currentSessionId?: string;
|
|
86
|
+
showMorePages: number;
|
|
58
87
|
}
|
|
59
88
|
|
|
60
89
|
function isInsideOrEqual(parent: string, child: string): boolean {
|
|
61
|
-
|
|
62
|
-
|
|
90
|
+
const childRelative = relative(parent, child);
|
|
91
|
+
return (
|
|
92
|
+
childRelative === "" ||
|
|
93
|
+
(!childRelative.startsWith("..") && !isAbsolute(childRelative))
|
|
94
|
+
);
|
|
63
95
|
}
|
|
64
96
|
|
|
65
97
|
function safeRelative(cwd: string, path: string): string {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
98
|
+
const rel = relative(cwd, path);
|
|
99
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return path;
|
|
100
|
+
return rel.split(sep).join("/");
|
|
69
101
|
}
|
|
70
102
|
|
|
71
103
|
function style(theme: PanelTheme, color: string, text: string): string {
|
|
72
|
-
|
|
104
|
+
return theme.fg?.(color, text) ?? text;
|
|
73
105
|
}
|
|
74
106
|
|
|
75
107
|
function bold(theme: PanelTheme, text: string): string {
|
|
76
|
-
|
|
108
|
+
return theme.bold?.(text) ?? text;
|
|
77
109
|
}
|
|
78
110
|
|
|
79
111
|
const ANSI_PATTERN = /\u001b\[[0-?]*[ -/]*[@-~]/g;
|
|
@@ -84,595 +116,1355 @@ const ANSI_PATTERN = /\u001b\[[0-?]*[ -/]*[@-~]/g;
|
|
|
84
116
|
// host TUI measures lines, otherwise CJK-heavy text under-counts and overflows
|
|
85
117
|
// the terminal width (which crashes the renderer).
|
|
86
118
|
function charWidth(cp: number): number {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
// C0/C1 control characters render with no horizontal advance here.
|
|
120
|
+
if (cp < 0x20 || (cp >= 0x7f && cp < 0xa0)) return 0;
|
|
121
|
+
// Zero-width and combining ranges.
|
|
122
|
+
if (
|
|
123
|
+
cp === 0x200b || // zero width space
|
|
124
|
+
(cp >= 0x0300 && cp <= 0x036f) || // combining diacritical marks
|
|
125
|
+
(cp >= 0x1ab0 && cp <= 0x1aff) ||
|
|
126
|
+
(cp >= 0x1dc0 && cp <= 0x1dff) ||
|
|
127
|
+
(cp >= 0x20d0 && cp <= 0x20ff) ||
|
|
128
|
+
(cp >= 0xfe00 && cp <= 0xfe0f) || // variation selectors
|
|
129
|
+
(cp >= 0xfe20 && cp <= 0xfe2f)
|
|
130
|
+
) {
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
// Wide (2-column) ranges.
|
|
134
|
+
if (
|
|
135
|
+
(cp >= 0x1100 && cp <= 0x115f) || // Hangul Jamo
|
|
136
|
+
cp === 0x2329 ||
|
|
137
|
+
cp === 0x232a ||
|
|
138
|
+
(cp >= 0x2e80 && cp <= 0x303e) || // CJK radicals, Kangxi, punctuation
|
|
139
|
+
(cp >= 0x3041 && cp <= 0x33ff) || // Hiragana..CJK compatibility
|
|
140
|
+
(cp >= 0x3400 && cp <= 0x4dbf) || // CJK ext A
|
|
141
|
+
(cp >= 0x4e00 && cp <= 0x9fff) || // CJK unified
|
|
142
|
+
(cp >= 0xa000 && cp <= 0xa4cf) || // Yi
|
|
143
|
+
(cp >= 0xac00 && cp <= 0xd7a3) || // Hangul syllables
|
|
144
|
+
(cp >= 0xf900 && cp <= 0xfaff) || // CJK compatibility ideographs
|
|
145
|
+
(cp >= 0xfe10 && cp <= 0xfe19) || // vertical forms
|
|
146
|
+
(cp >= 0xfe30 && cp <= 0xfe6f) || // CJK compatibility forms
|
|
147
|
+
(cp >= 0xff00 && cp <= 0xff60) || // fullwidth forms
|
|
148
|
+
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
149
|
+
(cp >= 0x1f300 && cp <= 0x1faff) || // emoji & pictographs
|
|
150
|
+
(cp >= 0x20000 && cp <= 0x3fffd) // CJK ext B+
|
|
151
|
+
) {
|
|
152
|
+
return 2;
|
|
153
|
+
}
|
|
154
|
+
return 1;
|
|
123
155
|
}
|
|
124
156
|
|
|
125
157
|
// Measure the visible terminal width of a string, ignoring ANSI escapes and
|
|
126
158
|
// accounting for wide characters.
|
|
127
159
|
function visibleLength(text: string): number {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
160
|
+
let width = 0;
|
|
161
|
+
for (let index = 0; index < text.length; ) {
|
|
162
|
+
if (text.charCodeAt(index) === 0x1b) {
|
|
163
|
+
ANSI_PATTERN.lastIndex = index;
|
|
164
|
+
const match = ANSI_PATTERN.exec(text);
|
|
165
|
+
if (match && match.index === index) {
|
|
166
|
+
index = ANSI_PATTERN.lastIndex;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const cp = text.codePointAt(index) ?? 0;
|
|
171
|
+
width += charWidth(cp);
|
|
172
|
+
index += cp > 0xffff ? 2 : 1;
|
|
173
|
+
}
|
|
174
|
+
return width;
|
|
143
175
|
}
|
|
144
176
|
|
|
145
177
|
function clip(text: string, width: number): string {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
178
|
+
if (width <= 0) return "";
|
|
179
|
+
if (visibleLength(text) <= width) return text;
|
|
180
|
+
if (width <= 1) return "…";
|
|
181
|
+
|
|
182
|
+
let output = "";
|
|
183
|
+
let visible = 0;
|
|
184
|
+
for (let index = 0; index < text.length; ) {
|
|
185
|
+
if (text.charCodeAt(index) === 0x1b) {
|
|
186
|
+
ANSI_PATTERN.lastIndex = index;
|
|
187
|
+
const match = ANSI_PATTERN.exec(text);
|
|
188
|
+
if (match && match.index === index) {
|
|
189
|
+
output += match[0];
|
|
190
|
+
index = ANSI_PATTERN.lastIndex;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const cp = text.codePointAt(index) ?? 0;
|
|
195
|
+
const w = charWidth(cp);
|
|
196
|
+
// Reserve one column for the ellipsis.
|
|
197
|
+
if (visible + w > width - 1) break;
|
|
198
|
+
output += String.fromCodePoint(cp);
|
|
199
|
+
visible += w;
|
|
200
|
+
index += cp > 0xffff ? 2 : 1;
|
|
201
|
+
}
|
|
202
|
+
return `${output}…`;
|
|
171
203
|
}
|
|
172
204
|
|
|
173
205
|
function pad(text: string, width: number): string {
|
|
174
|
-
|
|
175
|
-
|
|
206
|
+
const visible = visibleLength(text);
|
|
207
|
+
return visible >= width
|
|
208
|
+
? clip(text, width)
|
|
209
|
+
: text + " ".repeat(width - visible);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function sanitizeRunText(text: string, currentSessionId?: string): string {
|
|
213
|
+
let sanitized = text
|
|
214
|
+
.replace(ANSI_PATTERN, "")
|
|
215
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, "")
|
|
216
|
+
.replace(/\r/g, "");
|
|
217
|
+
if (currentSessionId && currentSessionId.length > 0)
|
|
218
|
+
sanitized = sanitized.split(currentSessionId).join("[session]");
|
|
219
|
+
return sanitized;
|
|
176
220
|
}
|
|
177
221
|
|
|
178
222
|
function fmtAge(ms: number, now = Date.now()): string {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
223
|
+
const delta = Math.max(0, now - ms);
|
|
224
|
+
if (delta < 1_000) return "now";
|
|
225
|
+
if (delta < 60_000) return `${Math.floor(delta / 1_000)}s ago`;
|
|
226
|
+
if (delta < 3_600_000) return `${Math.floor(delta / 60_000)}m ago`;
|
|
227
|
+
return `${Math.floor(delta / 3_600_000)}h ago`;
|
|
184
228
|
}
|
|
185
229
|
|
|
186
230
|
function fmtElapsed(startedAt: string, completedAt: string | null): string {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
231
|
+
const start = Date.parse(startedAt);
|
|
232
|
+
const end = completedAt === null ? Date.now() : Date.parse(completedAt);
|
|
233
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return "—";
|
|
234
|
+
const seconds = Math.max(0, Math.floor((end - start) / 1_000));
|
|
235
|
+
const mins = Math.floor(seconds / 60);
|
|
236
|
+
const secs = seconds % 60;
|
|
237
|
+
return `${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
|
|
194
238
|
}
|
|
195
239
|
|
|
196
240
|
function statusPriority(status: Status): number {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
241
|
+
if (status === "running") return 0;
|
|
242
|
+
if (status === "pending") return 1;
|
|
243
|
+
if (status === "failed") return 2;
|
|
244
|
+
if (status === "cancelled") return 3;
|
|
245
|
+
return 4;
|
|
202
246
|
}
|
|
203
247
|
|
|
204
248
|
function aggregateRunStatus(attempts: TaskRow[]): Status {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
249
|
+
if (attempts.some((attempt) => attempt.status === "running"))
|
|
250
|
+
return "running";
|
|
251
|
+
if (attempts.some((attempt) => attempt.status === "pending"))
|
|
252
|
+
return "pending";
|
|
253
|
+
if (attempts.some((attempt) => attempt.status === "failed")) return "failed";
|
|
254
|
+
if (attempts.some((attempt) => attempt.status === "cancelled"))
|
|
255
|
+
return "cancelled";
|
|
256
|
+
return "completed";
|
|
210
257
|
}
|
|
211
258
|
|
|
212
259
|
function isEscapeKey(data: string): boolean {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
260
|
+
return (
|
|
261
|
+
data === "\u001b" ||
|
|
262
|
+
data === "escape" ||
|
|
263
|
+
data === "esc" ||
|
|
264
|
+
data === "Esc" ||
|
|
265
|
+
data === "ctrl+[" ||
|
|
266
|
+
data.startsWith("escape") ||
|
|
267
|
+
data.startsWith("esc") ||
|
|
268
|
+
/^\u001b\[27(?:;\d+)?(?::\d+)?u$/.test(data)
|
|
269
|
+
);
|
|
223
270
|
}
|
|
224
271
|
|
|
225
272
|
function isEnterKey(data: string): boolean {
|
|
226
|
-
|
|
273
|
+
return (
|
|
274
|
+
data === "\r" ||
|
|
275
|
+
data === "\n" ||
|
|
276
|
+
data === "enter" ||
|
|
277
|
+
data === "return" ||
|
|
278
|
+
data === "\u001b[13u"
|
|
279
|
+
);
|
|
227
280
|
}
|
|
228
281
|
|
|
229
282
|
function isTabKey(data: string): boolean {
|
|
230
|
-
|
|
283
|
+
return data === "\t" || data === "tab" || data === "\u001b[9u";
|
|
231
284
|
}
|
|
232
285
|
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
286
|
+
function isPageKey(data: string, direction: "up" | "down"): boolean {
|
|
287
|
+
if (direction === "up")
|
|
288
|
+
return data === "pageup" || data === "pgup" || data === "\u001b[5~";
|
|
289
|
+
return data === "pagedown" || data === "pgdown" || data === "\u001b[6~";
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function isArrowKey(
|
|
293
|
+
data: string,
|
|
294
|
+
direction: "up" | "down" | "left" | "right",
|
|
295
|
+
): boolean {
|
|
296
|
+
if (data === direction) return true;
|
|
297
|
+
const legacy: Record<typeof direction, string[]> = {
|
|
298
|
+
up: ["\u001b[A", "\u001bOA", "\u001b[a"],
|
|
299
|
+
down: ["\u001b[B", "\u001bOB", "\u001b[b"],
|
|
300
|
+
left: ["\u001b[D", "\u001bOD", "\u001b[d"],
|
|
301
|
+
right: ["\u001b[C", "\u001bOC", "\u001b[c"],
|
|
302
|
+
};
|
|
303
|
+
if (legacy[direction].includes(data)) return true;
|
|
304
|
+
const suffix: Record<typeof direction, string> = {
|
|
305
|
+
up: "A",
|
|
306
|
+
down: "B",
|
|
307
|
+
right: "C",
|
|
308
|
+
left: "D",
|
|
309
|
+
};
|
|
310
|
+
return new RegExp(`^\\u001b\\[1;\\d+(?::\\d+)?${suffix[direction]}$`).test(
|
|
311
|
+
data,
|
|
312
|
+
);
|
|
244
313
|
}
|
|
245
314
|
|
|
246
315
|
function statusColor(status: Status): string {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
316
|
+
if (status === "completed") return "success";
|
|
317
|
+
if (status === "running" || status === "pending") return "warning";
|
|
318
|
+
if (status === "failed" || status === "cancelled") return "error";
|
|
319
|
+
return "accent";
|
|
251
320
|
}
|
|
252
321
|
|
|
253
322
|
function statusLabel(status: Status): string {
|
|
254
|
-
|
|
255
|
-
|
|
323
|
+
if (status === "completed") return "done";
|
|
324
|
+
return status;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function childFailureCount(summary: RunChildSummary | undefined): number {
|
|
328
|
+
return (summary?.failed ?? 0) + (summary?.cancelled ?? 0);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function runHasFailure(run: Pick<RunRow, "status" | "childSummary">): boolean {
|
|
332
|
+
return (
|
|
333
|
+
run.status === "failed" ||
|
|
334
|
+
run.status === "cancelled" ||
|
|
335
|
+
childFailureCount(run.childSummary) > 0
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function runStatusLabel(run: Pick<RunRow, "status" | "childSummary">): string {
|
|
340
|
+
const base = statusLabel(run.status);
|
|
341
|
+
return childFailureCount(run.childSummary) > 0 ? `${base}+child` : base;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function runStatusDetail(run: Pick<RunRow, "status" | "childSummary">): string {
|
|
345
|
+
const failures = childFailureCount(run.childSummary);
|
|
346
|
+
return failures > 0
|
|
347
|
+
? `${statusLabel(run.status)} (child failures: ${failures})`
|
|
348
|
+
: statusLabel(run.status);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function runStatusColor(run: Pick<RunRow, "status" | "childSummary">): string {
|
|
352
|
+
return childFailureCount(run.childSummary) > 0
|
|
353
|
+
? "error"
|
|
354
|
+
: statusColor(run.status);
|
|
256
355
|
}
|
|
257
356
|
|
|
258
357
|
function isActive(status: Status): boolean {
|
|
259
|
-
|
|
358
|
+
return status === "pending" || status === "running";
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function pidAlive(pid: number | undefined): boolean {
|
|
362
|
+
if (pid === undefined || !Number.isInteger(pid) || pid <= 0) return false;
|
|
363
|
+
try {
|
|
364
|
+
process.kill(pid, 0);
|
|
365
|
+
return true;
|
|
366
|
+
} catch (error) {
|
|
367
|
+
return (
|
|
368
|
+
error !== null &&
|
|
369
|
+
typeof error === "object" &&
|
|
370
|
+
"code" in error &&
|
|
371
|
+
error.code === "EPERM"
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function timestampFresh(
|
|
377
|
+
value: string | undefined,
|
|
378
|
+
staleAfterMs = STALE_RUN_AFTER_MS,
|
|
379
|
+
): boolean {
|
|
380
|
+
if (value === undefined) return false;
|
|
381
|
+
const time = Date.parse(value);
|
|
382
|
+
return Number.isFinite(time) && Date.now() - time <= staleAfterMs;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function runKey(cwd: string, runsDir: string, runId: string): string {
|
|
386
|
+
return `${cwd}\u0000${runsDir}\u0000${runId}`;
|
|
260
387
|
}
|
|
261
388
|
|
|
262
389
|
async function readJson(path: string): Promise<unknown | null> {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
390
|
+
try {
|
|
391
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
392
|
+
} catch {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
268
395
|
}
|
|
269
396
|
|
|
270
397
|
function isResultEnvelope(value: unknown): value is ResultEnvelope {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
398
|
+
return (
|
|
399
|
+
typeof value === "object" &&
|
|
400
|
+
value !== null &&
|
|
401
|
+
typeof (value as { runId?: unknown }).runId === "string" &&
|
|
402
|
+
(typeof (value as { attemptId?: unknown }).attemptId === "string" ||
|
|
403
|
+
typeof (value as { taskId?: unknown }).taskId === "string")
|
|
404
|
+
);
|
|
277
405
|
}
|
|
278
406
|
|
|
279
407
|
interface RegistryTaskRecord {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
408
|
+
attemptId?: string;
|
|
409
|
+
taskId?: string;
|
|
410
|
+
status: Status;
|
|
411
|
+
backend?: string;
|
|
412
|
+
failureKind?: string | null;
|
|
413
|
+
startedAt?: string;
|
|
414
|
+
completedAt?: string | null;
|
|
415
|
+
updatedAt?: string;
|
|
416
|
+
heartbeatAt?: string;
|
|
417
|
+
artifactCwd?: string;
|
|
418
|
+
resultPath?: string;
|
|
419
|
+
outputPath?: string;
|
|
420
|
+
stdoutPath?: string;
|
|
421
|
+
stderrPath?: string;
|
|
422
|
+
process?: { pid?: number; workerPid?: number };
|
|
423
|
+
workspace?: { cwd?: string; worktreePath?: string | null };
|
|
294
424
|
}
|
|
295
425
|
|
|
296
426
|
interface RegistryRunRecord {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
427
|
+
runId: string;
|
|
428
|
+
mode?: string;
|
|
429
|
+
status: Status;
|
|
430
|
+
backend?: string;
|
|
431
|
+
dependency?: string | null;
|
|
432
|
+
parentSessionId?: string;
|
|
433
|
+
startedAt: string;
|
|
434
|
+
updatedAt: string;
|
|
435
|
+
completedAt: string | null;
|
|
436
|
+
attempts?: RegistryTaskRecord[];
|
|
437
|
+
tasks?: RegistryTaskRecord[];
|
|
307
438
|
}
|
|
308
439
|
|
|
309
440
|
function isRegistryRunRecord(value: unknown): value is RegistryRunRecord {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
441
|
+
return (
|
|
442
|
+
typeof value === "object" &&
|
|
443
|
+
value !== null &&
|
|
444
|
+
typeof (value as { runId?: unknown }).runId === "string" &&
|
|
445
|
+
typeof (value as { startedAt?: unknown }).startedAt === "string" &&
|
|
446
|
+
typeof (value as { updatedAt?: unknown }).updatedAt === "string" &&
|
|
447
|
+
(Array.isArray((value as { attempts?: unknown }).attempts) ||
|
|
448
|
+
Array.isArray((value as { tasks?: unknown }).tasks))
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function parseRunEvents(text: string): RunEvent[] {
|
|
453
|
+
return text
|
|
454
|
+
.split(/\r?\n/)
|
|
455
|
+
.filter(Boolean)
|
|
456
|
+
.map((line) => {
|
|
457
|
+
try {
|
|
458
|
+
return JSON.parse(line) as RunEvent;
|
|
459
|
+
} catch {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
})
|
|
463
|
+
.filter((event): event is RunEvent => event !== null);
|
|
316
464
|
}
|
|
317
465
|
|
|
318
|
-
async function
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
466
|
+
async function readTextTail(
|
|
467
|
+
path: string,
|
|
468
|
+
currentSessionId?: string,
|
|
469
|
+
): Promise<string[]> {
|
|
470
|
+
const text = await readFile(path, "utf8").catch(() => "");
|
|
471
|
+
return text
|
|
472
|
+
.split(/\r?\n/)
|
|
473
|
+
.map((line) => sanitizeRunText(line, currentSessionId))
|
|
474
|
+
.filter(Boolean)
|
|
475
|
+
.slice(-LOG_TAIL_LINES);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function readLogTail(
|
|
479
|
+
cwd: string,
|
|
480
|
+
result: ResultEnvelope,
|
|
481
|
+
loadTails: boolean,
|
|
482
|
+
currentSessionId?: string,
|
|
483
|
+
): Promise<{ path: string | null; tail: string[] }> {
|
|
484
|
+
const artifact =
|
|
485
|
+
result.artifacts.find((candidate) => candidate.type === "output") ??
|
|
486
|
+
result.artifacts.find((candidate) => candidate.type === "stdout") ??
|
|
487
|
+
result.artifacts.find((candidate) => candidate.type === "stderr");
|
|
488
|
+
if (artifact === undefined) return { path: null, tail: [] };
|
|
489
|
+
if (isAbsolute(artifact.path) || artifact.path.split("/").includes(".."))
|
|
490
|
+
return { path: artifact.path, tail: [] };
|
|
491
|
+
const path = resolve(cwd, artifact.path.split("/").join(sep));
|
|
492
|
+
if (!isInsideOrEqual(cwd, path)) return { path: artifact.path, tail: [] };
|
|
493
|
+
const tail = loadTails ? await readTextTail(path, currentSessionId) : [];
|
|
494
|
+
return { path: artifact.path, tail };
|
|
326
495
|
}
|
|
327
496
|
|
|
328
497
|
function modelLabel(result: ResultEnvelope): string {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
498
|
+
const pieces: string[] = [];
|
|
499
|
+
const maybeResult = result as ResultEnvelope & {
|
|
500
|
+
model?: string;
|
|
501
|
+
thinking?: string;
|
|
502
|
+
};
|
|
503
|
+
if (typeof maybeResult.model === "string") pieces.push(maybeResult.model);
|
|
504
|
+
if (typeof maybeResult.thinking === "string")
|
|
505
|
+
pieces.push(maybeResult.thinking);
|
|
506
|
+
return pieces.join(" · ");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function readTask(
|
|
510
|
+
cwd: string,
|
|
511
|
+
resultPath: string,
|
|
512
|
+
mtimeMs: number,
|
|
513
|
+
loadTails: boolean,
|
|
514
|
+
currentSessionId?: string,
|
|
515
|
+
): Promise<TaskRow | null> {
|
|
516
|
+
const parsed = await readJson(resultPath);
|
|
517
|
+
if (!isResultEnvelope(parsed)) return null;
|
|
518
|
+
const log = await readLogTail(cwd, parsed, loadTails, currentSessionId);
|
|
519
|
+
const stale =
|
|
520
|
+
isActive(parsed.status) && Date.now() - mtimeMs > STALE_RUN_AFTER_MS;
|
|
521
|
+
return {
|
|
522
|
+
attemptId: parsed.attemptId ?? parsed.taskId ?? "unknown",
|
|
523
|
+
status: stale ? "failed" : parsed.status,
|
|
524
|
+
backend: parsed.backend,
|
|
525
|
+
failureKind: stale ? "stale" : parsed.failureKind,
|
|
526
|
+
startedAt: parsed.startedAt,
|
|
527
|
+
completedAt: stale ? new Date(mtimeMs).toISOString() : parsed.completedAt,
|
|
528
|
+
durationMs: parsed.durationMs,
|
|
529
|
+
resultPath: safeRelative(cwd, resultPath),
|
|
530
|
+
logPath: log.path,
|
|
531
|
+
logTail: log.tail,
|
|
532
|
+
workspace: parsed.workspace.cwd,
|
|
533
|
+
worktreePath: parsed.workspace.worktreePath,
|
|
534
|
+
modelLabel: modelLabel(parsed),
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function readTailFromRegistryPath(
|
|
539
|
+
task: RegistryTaskRecord,
|
|
540
|
+
loadTails: boolean,
|
|
541
|
+
currentSessionId?: string,
|
|
542
|
+
): Promise<{ path: string | null; tail: string[] }> {
|
|
543
|
+
const artifactCwd = task.artifactCwd;
|
|
544
|
+
const path = task.outputPath ?? task.stdoutPath ?? task.stderrPath;
|
|
545
|
+
if (
|
|
546
|
+
artifactCwd === undefined ||
|
|
547
|
+
path === undefined ||
|
|
548
|
+
isAbsolute(path) ||
|
|
549
|
+
path.split("/").includes("..")
|
|
550
|
+
)
|
|
551
|
+
return { path: path ?? null, tail: [] };
|
|
552
|
+
const absolute = resolve(artifactCwd, path.split("/").join(sep));
|
|
553
|
+
if (!isInsideOrEqual(resolve(artifactCwd), absolute))
|
|
554
|
+
return { path, tail: [] };
|
|
555
|
+
const tail = loadTails ? await readTextTail(absolute, currentSessionId) : [];
|
|
556
|
+
return { path, tail };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function registryTaskStale(task: RegistryTaskRecord): boolean {
|
|
560
|
+
if (!isActive(task.status)) return false;
|
|
561
|
+
if (pidAlive(task.process?.pid) || pidAlive(task.process?.workerPid))
|
|
562
|
+
return false;
|
|
563
|
+
if (timestampFresh(task.heartbeatAt) || timestampFresh(task.updatedAt))
|
|
564
|
+
return false;
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async function readTaskFromRegistry(
|
|
569
|
+
cwd: string,
|
|
570
|
+
task: RegistryTaskRecord,
|
|
571
|
+
loadTails: boolean,
|
|
572
|
+
currentSessionId?: string,
|
|
573
|
+
): Promise<TaskRow> {
|
|
574
|
+
if (
|
|
575
|
+
task.artifactCwd !== undefined &&
|
|
576
|
+
task.resultPath !== undefined &&
|
|
577
|
+
!isAbsolute(task.resultPath) &&
|
|
578
|
+
!task.resultPath.split("/").includes("..")
|
|
579
|
+
) {
|
|
580
|
+
const absolute = resolve(
|
|
581
|
+
task.artifactCwd,
|
|
582
|
+
task.resultPath.split("/").join(sep),
|
|
583
|
+
);
|
|
584
|
+
if (isInsideOrEqual(resolve(task.artifactCwd), absolute)) {
|
|
585
|
+
const statInfo = await stat(absolute).catch(() => null);
|
|
586
|
+
if (statInfo !== null) {
|
|
587
|
+
const parsed = await readTask(
|
|
588
|
+
task.artifactCwd,
|
|
589
|
+
absolute,
|
|
590
|
+
statInfo.mtimeMs,
|
|
591
|
+
loadTails,
|
|
592
|
+
currentSessionId,
|
|
593
|
+
);
|
|
594
|
+
if (parsed !== null) return parsed;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const log = await readTailFromRegistryPath(task, loadTails, currentSessionId);
|
|
599
|
+
const stale = registryTaskStale(task);
|
|
600
|
+
return {
|
|
601
|
+
attemptId: task.attemptId ?? task.taskId ?? "unknown",
|
|
602
|
+
status: stale ? "failed" : task.status,
|
|
603
|
+
backend: task.backend ?? "unknown",
|
|
604
|
+
failureKind: stale ? "stale" : (task.failureKind ?? null),
|
|
605
|
+
startedAt: task.startedAt ?? task.updatedAt ?? new Date().toISOString(),
|
|
606
|
+
completedAt:
|
|
607
|
+
task.completedAt ??
|
|
608
|
+
(stale
|
|
609
|
+
? (task.updatedAt ?? task.heartbeatAt ?? new Date().toISOString())
|
|
610
|
+
: null),
|
|
611
|
+
durationMs: null,
|
|
612
|
+
resultPath: task.resultPath ?? "—",
|
|
613
|
+
logPath: log.path,
|
|
614
|
+
logTail: log.tail,
|
|
615
|
+
workspace: task.workspace?.cwd ?? cwd,
|
|
616
|
+
worktreePath: task.workspace?.worktreePath ?? null,
|
|
617
|
+
modelLabel: "",
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async function readRunFromRegistry(
|
|
622
|
+
cwd: string,
|
|
623
|
+
runsDir: string,
|
|
624
|
+
runDir: string,
|
|
625
|
+
registry: RegistryRunRecord,
|
|
626
|
+
loadTails: boolean,
|
|
627
|
+
currentSessionId?: string,
|
|
628
|
+
): Promise<RunRow | null> {
|
|
629
|
+
const eventsText = await readFile(join(runDir, "events.jsonl"), "utf8").catch(
|
|
630
|
+
() => "",
|
|
631
|
+
);
|
|
632
|
+
const eventTail = loadTails
|
|
633
|
+
? eventsText
|
|
634
|
+
.split(/\r?\n/)
|
|
635
|
+
.map((line) => sanitizeRunText(line, currentSessionId))
|
|
636
|
+
.filter(Boolean)
|
|
637
|
+
.slice(-LOG_TAIL_LINES)
|
|
638
|
+
: [];
|
|
639
|
+
const childSummary = summarizeChildEvents(parseRunEvents(eventsText));
|
|
640
|
+
const records = registry.attempts ?? registry.tasks ?? [];
|
|
641
|
+
const tasks = await Promise.all(
|
|
642
|
+
records.map((task) =>
|
|
643
|
+
readTaskFromRegistry(cwd, task, loadTails, currentSessionId),
|
|
644
|
+
),
|
|
645
|
+
);
|
|
646
|
+
if (tasks.length === 0) return null;
|
|
647
|
+
tasks.sort((a, b) =>
|
|
648
|
+
a.attemptId.localeCompare(b.attemptId, undefined, { numeric: true }),
|
|
649
|
+
);
|
|
650
|
+
return {
|
|
651
|
+
key: runKey(cwd, runsDir, registry.runId),
|
|
652
|
+
runId: registry.runId,
|
|
653
|
+
sourceCwd: cwd,
|
|
654
|
+
runsDir,
|
|
655
|
+
status: aggregateRunStatus(tasks),
|
|
656
|
+
backend: registry.backend ?? tasks[0]?.backend ?? "unknown",
|
|
657
|
+
updatedMs: Number.isFinite(Date.parse(registry.updatedAt))
|
|
658
|
+
? Date.parse(registry.updatedAt)
|
|
659
|
+
: Date.now(),
|
|
660
|
+
startedAt: registry.startedAt,
|
|
661
|
+
completedAt: registry.completedAt,
|
|
662
|
+
dependency: registry.dependency ?? null,
|
|
663
|
+
eventTail,
|
|
664
|
+
...(childSummary === undefined ? {} : { childSummary }),
|
|
665
|
+
tasks,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
async function loadRunsFromCwd(
|
|
670
|
+
cwd: string,
|
|
671
|
+
options: Pick<LoadOptions, "currentSessionId"> & { sessionOnly?: string },
|
|
672
|
+
): Promise<{
|
|
673
|
+
runs: RunRow[];
|
|
674
|
+
stale: number;
|
|
675
|
+
invalid: number;
|
|
676
|
+
skipped: number;
|
|
677
|
+
}> {
|
|
678
|
+
const runsDir = resolve(cwd, DEFAULT_RUNS_DIR);
|
|
679
|
+
if (!isInsideOrEqual(cwd, runsDir))
|
|
680
|
+
return { runs: [], stale: 0, invalid: 0, skipped: 0 };
|
|
681
|
+
const runEntries = await readdir(runsDir, { withFileTypes: true }).catch(
|
|
682
|
+
() => [],
|
|
683
|
+
);
|
|
684
|
+
const runs: RunRow[] = [];
|
|
685
|
+
let invalid = 0;
|
|
686
|
+
|
|
687
|
+
for (const runEntry of runEntries) {
|
|
688
|
+
if (!runEntry.isDirectory()) continue;
|
|
689
|
+
const runDir = join(runsDir, runEntry.name);
|
|
690
|
+
const registry = await readJson(join(runDir, "run.json"));
|
|
691
|
+
if (isRegistryRunRecord(registry)) {
|
|
692
|
+
if (
|
|
693
|
+
options.sessionOnly !== undefined &&
|
|
694
|
+
registry.parentSessionId !== options.sessionOnly
|
|
695
|
+
)
|
|
696
|
+
continue;
|
|
697
|
+
const row = await readRunFromRegistry(
|
|
698
|
+
cwd,
|
|
699
|
+
DEFAULT_RUNS_DIR,
|
|
700
|
+
runDir,
|
|
701
|
+
registry,
|
|
702
|
+
true,
|
|
703
|
+
options.currentSessionId,
|
|
704
|
+
).catch(() => null);
|
|
705
|
+
if (row !== null) runs.push(row);
|
|
706
|
+
else invalid += 1;
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (options.sessionOnly !== undefined) continue;
|
|
711
|
+
|
|
712
|
+
const taskEntries = await readdir(runDir, { withFileTypes: true }).catch(
|
|
713
|
+
() => [],
|
|
714
|
+
);
|
|
715
|
+
const attemptEntries = await readdir(join(runDir, "attempts"), {
|
|
716
|
+
withFileTypes: true,
|
|
717
|
+
}).catch(() => []);
|
|
718
|
+
const candidates = [
|
|
719
|
+
...attemptEntries
|
|
720
|
+
.filter((entry) => entry.isDirectory())
|
|
721
|
+
.map((entry) => join(runDir, "attempts", entry.name, "result.json")),
|
|
722
|
+
...taskEntries
|
|
723
|
+
.filter((entry) => entry.isDirectory() && entry.name !== "attempts")
|
|
724
|
+
.map((entry) => join(runDir, entry.name, "result.json")),
|
|
725
|
+
];
|
|
726
|
+
const eventsText = await readFile(
|
|
727
|
+
join(runDir, "events.jsonl"),
|
|
728
|
+
"utf8",
|
|
729
|
+
).catch(() => "");
|
|
730
|
+
const eventTail = eventsText
|
|
731
|
+
.split(/\r?\n/)
|
|
732
|
+
.map((line) => sanitizeRunText(line, options.currentSessionId))
|
|
733
|
+
.filter(Boolean)
|
|
734
|
+
.slice(-LOG_TAIL_LINES);
|
|
735
|
+
const childSummary = summarizeChildEvents(parseRunEvents(eventsText));
|
|
736
|
+
const tasks: TaskRow[] = [];
|
|
737
|
+
let updatedMs = 0;
|
|
738
|
+
for (const resultPath of candidates) {
|
|
739
|
+
const resultStat = await stat(resultPath).catch(() => null);
|
|
740
|
+
if (resultStat === null) continue;
|
|
741
|
+
updatedMs = Math.max(updatedMs, resultStat.mtimeMs);
|
|
742
|
+
const task = await readTask(
|
|
743
|
+
cwd,
|
|
744
|
+
resultPath,
|
|
745
|
+
resultStat.mtimeMs,
|
|
746
|
+
true,
|
|
747
|
+
options.currentSessionId,
|
|
748
|
+
);
|
|
749
|
+
if (task !== null) tasks.push(task);
|
|
750
|
+
}
|
|
751
|
+
if (tasks.length === 0) continue;
|
|
752
|
+
tasks.sort((a, b) =>
|
|
753
|
+
a.attemptId.localeCompare(b.attemptId, undefined, { numeric: true }),
|
|
754
|
+
);
|
|
755
|
+
const status = aggregateRunStatus(tasks);
|
|
756
|
+
runs.push({
|
|
757
|
+
key: runKey(cwd, DEFAULT_RUNS_DIR, runEntry.name),
|
|
758
|
+
runId: runEntry.name,
|
|
759
|
+
sourceCwd: cwd,
|
|
760
|
+
runsDir: DEFAULT_RUNS_DIR,
|
|
761
|
+
status,
|
|
762
|
+
backend: tasks[0]?.backend ?? "unknown",
|
|
763
|
+
updatedMs,
|
|
764
|
+
startedAt:
|
|
765
|
+
tasks.map((task) => task.startedAt).sort()[0] ??
|
|
766
|
+
new Date(updatedMs).toISOString(),
|
|
767
|
+
completedAt: tasks.every((task) => task.completedAt !== null)
|
|
768
|
+
? (tasks
|
|
769
|
+
.map((task) => task.completedAt)
|
|
770
|
+
.sort()
|
|
771
|
+
.at(-1) ?? null)
|
|
772
|
+
: null,
|
|
773
|
+
dependency: null,
|
|
774
|
+
eventTail,
|
|
775
|
+
...(childSummary === undefined ? {} : { childSummary }),
|
|
776
|
+
tasks,
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
return { runs, stale: 0, invalid, skipped: 0 };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async function loadRunFromLocator(
|
|
783
|
+
locator: RunRefLocator,
|
|
784
|
+
options: Pick<LoadOptions, "scope" | "currentSessionId">,
|
|
785
|
+
): Promise<{ row: RunRow | null; stale: boolean; invalid: boolean }> {
|
|
786
|
+
try {
|
|
787
|
+
const cwd = resolve(locator.cwd);
|
|
788
|
+
const runsDir = locator.runsDir ?? DEFAULT_RUNS_DIR;
|
|
789
|
+
const absoluteRunsDir = resolve(cwd, runsDir);
|
|
790
|
+
if (!isInsideOrEqual(cwd, absoluteRunsDir))
|
|
791
|
+
return { row: null, stale: false, invalid: true };
|
|
792
|
+
const runDir = join(absoluteRunsDir, locator.runId);
|
|
793
|
+
const runDirStat = await stat(runDir).catch(() => null);
|
|
794
|
+
if (runDirStat === null || !runDirStat.isDirectory())
|
|
795
|
+
return { row: null, stale: true, invalid: false };
|
|
796
|
+
const registry = await readJson(join(runDir, "run.json"));
|
|
797
|
+
if (!isRegistryRunRecord(registry))
|
|
798
|
+
return { row: null, stale: false, invalid: true };
|
|
799
|
+
if (options.scope === "session") {
|
|
800
|
+
if (
|
|
801
|
+
options.currentSessionId === undefined ||
|
|
802
|
+
registry.parentSessionId !== options.currentSessionId
|
|
803
|
+
)
|
|
804
|
+
return { row: null, stale: false, invalid: false };
|
|
805
|
+
}
|
|
806
|
+
const row = await readRunFromRegistry(
|
|
807
|
+
cwd,
|
|
808
|
+
runsDir,
|
|
809
|
+
runDir,
|
|
810
|
+
registry,
|
|
811
|
+
false,
|
|
812
|
+
options.currentSessionId,
|
|
813
|
+
);
|
|
814
|
+
return { row, stale: row === null, invalid: false };
|
|
815
|
+
} catch {
|
|
816
|
+
return { row: null, stale: false, invalid: true };
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function statusMatches(run: RunRow, filter: StatusFilter): boolean {
|
|
821
|
+
if (filter === "all") return true;
|
|
822
|
+
if (filter === "running")
|
|
823
|
+
return run.status === "running" || run.status === "pending";
|
|
824
|
+
if (filter === "completed")
|
|
825
|
+
return run.status === "completed" && !runHasFailure(run);
|
|
826
|
+
return runHasFailure(run);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function recentTerminalLimit(
|
|
830
|
+
scope: ScopeFilter,
|
|
831
|
+
showMorePages: number,
|
|
832
|
+
): number {
|
|
833
|
+
const base =
|
|
834
|
+
scope === "all"
|
|
835
|
+
? ALL_SCOPE_RECENT_TERMINAL_LIMIT
|
|
836
|
+
: DEFAULT_RECENT_TERMINAL_LIMIT;
|
|
837
|
+
return base * Math.max(1, showMorePages + 1);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function compareRecentRuns(a: RunRow, b: RunRow): number {
|
|
841
|
+
return b.updatedMs - a.updatedMs || a.key.localeCompare(b.key);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function takeRecentRuns(runs: RunRow[], limit: number): RunRow[] {
|
|
845
|
+
if (runs.length <= limit) return runs;
|
|
846
|
+
const visibleKeys = new Set(
|
|
847
|
+
runs
|
|
848
|
+
.toSorted(compareRecentRuns)
|
|
849
|
+
.slice(0, limit)
|
|
850
|
+
.map((run) => run.key),
|
|
851
|
+
);
|
|
852
|
+
return runs.filter((run) => visibleKeys.has(run.key));
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function limitRunsForPanel(
|
|
856
|
+
runs: RunRow[],
|
|
857
|
+
options: Pick<LoadOptions, "scope" | "statusFilter" | "showMorePages">,
|
|
858
|
+
): { runs: RunRow[]; hiddenRuns: number } {
|
|
859
|
+
if (options.statusFilter === "running") return { runs, hiddenRuns: 0 };
|
|
860
|
+
const limit = recentTerminalLimit(options.scope, options.showMorePages);
|
|
861
|
+
if (options.statusFilter !== "all") {
|
|
862
|
+
const limited = takeRecentRuns(runs, limit);
|
|
863
|
+
return {
|
|
864
|
+
runs: limited,
|
|
865
|
+
hiddenRuns: Math.max(0, runs.length - limited.length),
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
const active = runs.filter((run) => isActive(run.status));
|
|
869
|
+
const terminal = runs.filter((run) => !isActive(run.status));
|
|
870
|
+
const limitedTerminal = takeRecentRuns(terminal, limit);
|
|
871
|
+
return {
|
|
872
|
+
runs: [...active, ...limitedTerminal],
|
|
873
|
+
hiddenRuns: Math.max(0, terminal.length - limitedTerminal.length),
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
async function loadRuns(options: LoadOptions): Promise<PanelSnapshot> {
|
|
878
|
+
const effectiveScope =
|
|
879
|
+
options.scope === "session" && options.currentSessionId === undefined
|
|
880
|
+
? "cwd"
|
|
881
|
+
: options.scope;
|
|
882
|
+
const loaded = await loadRunsForScope({ ...options, scope: effectiveScope });
|
|
883
|
+
const unique = new Map<string, RunRow>();
|
|
884
|
+
for (const run of loaded.runs) unique.set(run.key, run);
|
|
885
|
+
const allRuns = [...unique.values()].sort(
|
|
886
|
+
(a, b) =>
|
|
887
|
+
statusPriority(a.status) - statusPriority(b.status) ||
|
|
888
|
+
b.updatedMs - a.updatedMs ||
|
|
889
|
+
a.key.localeCompare(b.key),
|
|
890
|
+
);
|
|
891
|
+
const filtered = allRuns.filter((run) =>
|
|
892
|
+
statusMatches(run, options.statusFilter),
|
|
893
|
+
);
|
|
894
|
+
const limited = limitRunsForPanel(filtered, options);
|
|
895
|
+
return {
|
|
896
|
+
runs: limited.runs,
|
|
897
|
+
totalRuns: filtered.length,
|
|
898
|
+
hiddenRuns: limited.hiddenRuns,
|
|
899
|
+
loadedAt: new Date(),
|
|
900
|
+
staleLocators: loaded.stale,
|
|
901
|
+
invalidLocators: loaded.invalid,
|
|
902
|
+
skippedLocators: loaded.skipped,
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
async function mergeLoadedRuns(
|
|
907
|
+
...groups: Array<{
|
|
908
|
+
runs: RunRow[];
|
|
909
|
+
stale: number;
|
|
910
|
+
invalid: number;
|
|
911
|
+
skipped: number;
|
|
912
|
+
}>
|
|
913
|
+
): Promise<{
|
|
914
|
+
runs: RunRow[];
|
|
915
|
+
stale: number;
|
|
916
|
+
invalid: number;
|
|
917
|
+
skipped: number;
|
|
918
|
+
}> {
|
|
919
|
+
return {
|
|
920
|
+
runs: groups.flatMap((group) => group.runs),
|
|
921
|
+
stale: groups.reduce((sum, group) => sum + group.stale, 0),
|
|
922
|
+
invalid: groups.reduce((sum, group) => sum + group.invalid, 0),
|
|
923
|
+
skipped: groups.reduce((sum, group) => sum + group.skipped, 0),
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
async function loadRunsForScope(options: LoadOptions): Promise<{
|
|
928
|
+
runs: RunRow[];
|
|
929
|
+
stale: number;
|
|
930
|
+
invalid: number;
|
|
931
|
+
skipped: number;
|
|
932
|
+
}> {
|
|
933
|
+
if (options.scope === "cwd") return loadRunsFromCwd(options.cwd, options);
|
|
934
|
+
if (options.scope === "session") {
|
|
935
|
+
const indexed = await loadRunsFromIndex(options);
|
|
936
|
+
if (options.currentSessionId === undefined) return indexed;
|
|
937
|
+
const local = await loadRunsFromCwd(options.cwd, {
|
|
938
|
+
currentSessionId: options.currentSessionId,
|
|
939
|
+
sessionOnly: options.currentSessionId,
|
|
940
|
+
});
|
|
941
|
+
return mergeLoadedRuns(indexed, local);
|
|
942
|
+
}
|
|
943
|
+
const indexed = await loadRunsFromIndex(options);
|
|
944
|
+
const local = await loadRunsFromCwd(options.cwd, options);
|
|
945
|
+
return mergeLoadedRuns(indexed, local);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
async function loadRunsFromIndex(options: LoadOptions): Promise<{
|
|
949
|
+
runs: RunRow[];
|
|
950
|
+
stale: number;
|
|
951
|
+
invalid: number;
|
|
952
|
+
skipped: number;
|
|
953
|
+
}> {
|
|
954
|
+
const listed = await listRunLocators();
|
|
955
|
+
const runs: RunRow[] = [];
|
|
956
|
+
let stale = 0;
|
|
957
|
+
let invalid = listed.invalidCount;
|
|
958
|
+
for (const locator of listed.locators) {
|
|
959
|
+
const loaded = await loadRunFromLocator(locator, options);
|
|
960
|
+
if (loaded.row !== null) runs.push(loaded.row);
|
|
961
|
+
if (loaded.stale) stale += 1;
|
|
962
|
+
if (loaded.invalid) invalid += 1;
|
|
963
|
+
}
|
|
964
|
+
return { runs, stale, invalid, skipped: listed.skippedCount };
|
|
466
965
|
}
|
|
467
966
|
|
|
468
967
|
function splitLine(left: string, right: string, width: number): string {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
968
|
+
const gap = width - visibleLength(left) - visibleLength(right);
|
|
969
|
+
if (gap <= 1) return clip(`${left} ${right}`, width);
|
|
970
|
+
return `${left}${" ".repeat(gap)}${right}`;
|
|
472
971
|
}
|
|
473
972
|
|
|
474
973
|
function border(width: number): string {
|
|
475
|
-
|
|
974
|
+
return "─".repeat(Math.max(1, width));
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function panelLineBudget(): number {
|
|
978
|
+
const rows = process.stdout.rows;
|
|
979
|
+
if (typeof rows !== "number" || !Number.isFinite(rows) || rows <= 0)
|
|
980
|
+
return PANEL_MAX_LINES;
|
|
981
|
+
return Math.max(
|
|
982
|
+
PANEL_MIN_LINES,
|
|
983
|
+
Math.min(PANEL_MAX_LINES, rows - PANEL_RESERVED_TUI_LINES),
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function currentSessionIdFromCtx(
|
|
988
|
+
ctx: ExtensionCommandContext,
|
|
989
|
+
): string | undefined {
|
|
990
|
+
const raw = ctx as unknown as {
|
|
991
|
+
sessionManager?: { getSessionId?: () => unknown };
|
|
992
|
+
};
|
|
993
|
+
try {
|
|
994
|
+
const id = raw.sessionManager?.getSessionId?.();
|
|
995
|
+
return typeof id === "string" && id.length > 0 ? id : undefined;
|
|
996
|
+
} catch {
|
|
997
|
+
return undefined;
|
|
998
|
+
}
|
|
476
999
|
}
|
|
477
1000
|
|
|
478
1001
|
export class SubagentPanel implements Component {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
1002
|
+
private snapshot: PanelSnapshot = {
|
|
1003
|
+
runs: [],
|
|
1004
|
+
totalRuns: 0,
|
|
1005
|
+
loadedAt: new Date(),
|
|
1006
|
+
hiddenRuns: 0,
|
|
1007
|
+
staleLocators: 0,
|
|
1008
|
+
invalidLocators: 0,
|
|
1009
|
+
skippedLocators: 0,
|
|
1010
|
+
};
|
|
1011
|
+
private selectedRun = 0;
|
|
1012
|
+
private showMorePages = 0;
|
|
1013
|
+
private scope: ScopeFilter;
|
|
1014
|
+
private statusFilter: StatusFilter = "all";
|
|
1015
|
+
private focus: FocusGroup = "scope";
|
|
1016
|
+
private detailOffset = 0;
|
|
1017
|
+
private timer: NodeJS.Timeout | undefined;
|
|
1018
|
+
private disposed = false;
|
|
1019
|
+
private loading = false;
|
|
1020
|
+
|
|
1021
|
+
constructor(
|
|
1022
|
+
private readonly cwd: string,
|
|
1023
|
+
private readonly theme: PanelTheme,
|
|
1024
|
+
private readonly tui: PanelTui,
|
|
1025
|
+
private readonly done: () => void,
|
|
1026
|
+
private readonly currentSessionId?: string,
|
|
1027
|
+
) {
|
|
1028
|
+
this.scope = currentSessionId === undefined ? "cwd" : "session";
|
|
1029
|
+
void this.refresh({ preserveSelection: false });
|
|
1030
|
+
this.timer = setInterval(() => void this.refresh(), LIVE_REFRESH_MS);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
dispose(): void {
|
|
1034
|
+
this.disposed = true;
|
|
1035
|
+
if (this.timer !== undefined) clearInterval(this.timer);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
invalidate(): void {
|
|
1039
|
+
// Stateless render; refresh loop owns data invalidation.
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
handleInput(data: string): void {
|
|
1043
|
+
if (data === "q" || isEscapeKey(data)) {
|
|
1044
|
+
this.dispose();
|
|
1045
|
+
this.done();
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (data === "r") {
|
|
1049
|
+
void this.refresh();
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
if (data === "m") {
|
|
1053
|
+
if (this.snapshot.hiddenRuns > 0) {
|
|
1054
|
+
this.showMorePages += 1;
|
|
1055
|
+
void this.refresh();
|
|
1056
|
+
}
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
if (isTabKey(data) || data === "shift+tab" || data === "\u001b[Z") {
|
|
1060
|
+
const groups: FocusGroup[] = ["scope", "status", "detail"];
|
|
1061
|
+
const direction = data === "shift+tab" || data === "\u001b[Z" ? -1 : 1;
|
|
1062
|
+
const current = groups.indexOf(this.focus);
|
|
1063
|
+
this.focus =
|
|
1064
|
+
groups[(current + direction + groups.length) % groups.length] ??
|
|
1065
|
+
"scope";
|
|
1066
|
+
this.tui.requestRender?.();
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
if (isArrowKey(data, "right") || data === "l") {
|
|
1070
|
+
void this.cycleFocused(1);
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
if (isArrowKey(data, "left") || data === "h") {
|
|
1074
|
+
void this.cycleFocused(-1);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
if (isEnterKey(data)) return;
|
|
1078
|
+
if (this.focus === "detail") {
|
|
1079
|
+
if (isArrowKey(data, "up") || data === "k") this.scrollDetail(-1);
|
|
1080
|
+
if (isArrowKey(data, "down") || data === "j") this.scrollDetail(1);
|
|
1081
|
+
if (isPageKey(data, "up")) this.scrollDetail(-8);
|
|
1082
|
+
if (isPageKey(data, "down")) this.scrollDetail(8);
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
if (isArrowKey(data, "up") || data === "k") this.moveRun(-1);
|
|
1086
|
+
if (isArrowKey(data, "down") || data === "j") this.moveRun(1);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
render(width: number): string[] {
|
|
1090
|
+
const safeWidth = Math.max(48, width);
|
|
1091
|
+
const maxLines = panelLineBudget();
|
|
1092
|
+
const lines: string[] = [];
|
|
1093
|
+
const active = this.snapshot.runs.filter((run) =>
|
|
1094
|
+
isActive(run.status),
|
|
1095
|
+
).length;
|
|
1096
|
+
const failed = this.snapshot.runs.filter((run) =>
|
|
1097
|
+
runHasFailure(run),
|
|
1098
|
+
).length;
|
|
1099
|
+
const title = `${style(this.theme, "accent", "●")} ${bold(this.theme, "Subagents")}`;
|
|
1100
|
+
const stale = this.snapshot.staleLocators + this.snapshot.invalidLocators;
|
|
1101
|
+
const staleText =
|
|
1102
|
+
stale > 0 || this.snapshot.skippedLocators > 0
|
|
1103
|
+
? ` · stale ${this.snapshot.staleLocators} · skipped ${this.snapshot.invalidLocators + this.snapshot.skippedLocators}`
|
|
1104
|
+
: "";
|
|
1105
|
+
const status = `live · ${active} active · ${failed} failed · ${this.snapshot.runs.length}/${this.snapshot.totalRuns} shown${staleText} · updated ${fmtAge(this.snapshot.loadedAt.getTime())}`;
|
|
1106
|
+
lines.push(splitLine(title, style(this.theme, "muted", status), safeWidth));
|
|
1107
|
+
lines.push(style(this.theme, "border", border(safeWidth)));
|
|
1108
|
+
lines.push(this.renderControls(safeWidth));
|
|
1109
|
+
lines.push(this.renderScopeHelp(safeWidth));
|
|
1110
|
+
lines.push(style(this.theme, "border", border(safeWidth)));
|
|
1111
|
+
|
|
1112
|
+
if (this.snapshot.runs.length === 0) {
|
|
1113
|
+
const bodyHeight = Math.max(1, maxLines - lines.length - 2);
|
|
1114
|
+
lines.push(
|
|
1115
|
+
style(this.theme, "muted", clip(this.emptyMessage(), safeWidth)),
|
|
1116
|
+
);
|
|
1117
|
+
for (let index = 1; index < bodyHeight; index += 1) lines.push("");
|
|
1118
|
+
lines.push(style(this.theme, "border", border(safeWidth)));
|
|
1119
|
+
lines.push(style(this.theme, "dim", this.footerHelp(false)));
|
|
1120
|
+
return lines.slice(0, maxLines).map((line) => clip(line, safeWidth));
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
let leftWidth = Math.max(30, Math.min(64, Math.floor(safeWidth * 0.42)));
|
|
1124
|
+
if (safeWidth - leftWidth - 3 < 30)
|
|
1125
|
+
leftWidth = Math.max(18, safeWidth - 33);
|
|
1126
|
+
const rightWidth = safeWidth - leftWidth - 3;
|
|
1127
|
+
const selectedRun =
|
|
1128
|
+
this.snapshot.runs[
|
|
1129
|
+
Math.min(this.selectedRun, this.snapshot.runs.length - 1)
|
|
1130
|
+
];
|
|
1131
|
+
const selectedTask = selectedRun.tasks[0];
|
|
1132
|
+
const bodyHeight = Math.max(1, maxLines - lines.length - 2);
|
|
1133
|
+
const runLines = this.renderRuns(leftWidth, bodyHeight);
|
|
1134
|
+
const detailLines = this.renderDetailWindow(
|
|
1135
|
+
this.renderDetail(selectedRun, selectedTask, rightWidth),
|
|
1136
|
+
rightWidth,
|
|
1137
|
+
bodyHeight,
|
|
1138
|
+
);
|
|
1139
|
+
const bodyLines = bodyHeight;
|
|
1140
|
+
for (let index = 0; index < bodyLines; index += 1) {
|
|
1141
|
+
lines.push(
|
|
1142
|
+
`${pad(runLines[index] ?? "", leftWidth)} ${style(this.theme, "border", "│")} ${pad(detailLines[index] ?? "", rightWidth)}`,
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
lines.push(style(this.theme, "border", border(safeWidth)));
|
|
1146
|
+
lines.push(style(this.theme, "dim", this.footerHelp(true)));
|
|
1147
|
+
return lines.slice(0, maxLines).map((line) => clip(line, safeWidth));
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
private emptyMessage(): string {
|
|
1151
|
+
if (this.scope === "session" && this.currentSessionId === undefined)
|
|
1152
|
+
return `No current session id; showing current cwd ${DEFAULT_RUNS_DIR}`;
|
|
1153
|
+
if (this.scope === "session")
|
|
1154
|
+
return "No subagent runs found for this session";
|
|
1155
|
+
if (this.scope === "all")
|
|
1156
|
+
return "No indexed or current-workspace subagent runs found";
|
|
1157
|
+
return `No subagent runs found under ${DEFAULT_RUNS_DIR}`;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
private async refresh(
|
|
1161
|
+
options: { preserveSelection?: boolean } = {},
|
|
1162
|
+
): Promise<void> {
|
|
1163
|
+
if (this.loading || this.disposed) return;
|
|
1164
|
+
this.loading = true;
|
|
1165
|
+
try {
|
|
1166
|
+
const previousKey =
|
|
1167
|
+
options.preserveSelection === false
|
|
1168
|
+
? undefined
|
|
1169
|
+
: this.snapshot.runs[this.selectedRun]?.key;
|
|
1170
|
+
const snapshot = await loadRuns({
|
|
1171
|
+
cwd: this.cwd,
|
|
1172
|
+
scope: this.scope,
|
|
1173
|
+
statusFilter: this.statusFilter,
|
|
1174
|
+
currentSessionId: this.currentSessionId,
|
|
1175
|
+
showMorePages: this.showMorePages,
|
|
1176
|
+
});
|
|
1177
|
+
this.snapshot = snapshot;
|
|
1178
|
+
const oldSelectedRun = this.selectedRun;
|
|
1179
|
+
const nextIndex =
|
|
1180
|
+
previousKey === undefined
|
|
1181
|
+
? -1
|
|
1182
|
+
: snapshot.runs.findIndex((run) => run.key === previousKey);
|
|
1183
|
+
this.selectedRun =
|
|
1184
|
+
nextIndex >= 0
|
|
1185
|
+
? nextIndex
|
|
1186
|
+
: Math.min(this.selectedRun, Math.max(0, snapshot.runs.length - 1));
|
|
1187
|
+
if (this.selectedRun !== oldSelectedRun) this.detailOffset = 0;
|
|
1188
|
+
this.tui.requestRender?.();
|
|
1189
|
+
} finally {
|
|
1190
|
+
this.loading = false;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
private async cycleFocused(delta: number): Promise<void> {
|
|
1195
|
+
if (this.focus === "scope") {
|
|
1196
|
+
const scopes: ScopeFilter[] = ["session", "cwd", "all"];
|
|
1197
|
+
const current = scopes.indexOf(this.scope);
|
|
1198
|
+
this.scope =
|
|
1199
|
+
scopes[(current + delta + scopes.length) % scopes.length] ?? "cwd";
|
|
1200
|
+
this.detailOffset = 0;
|
|
1201
|
+
} else if (this.focus === "status") {
|
|
1202
|
+
const filters: StatusFilter[] = ["all", "running", "completed", "failed"];
|
|
1203
|
+
const current = filters.indexOf(this.statusFilter);
|
|
1204
|
+
this.statusFilter =
|
|
1205
|
+
filters[(current + delta + filters.length) % filters.length] ?? "all";
|
|
1206
|
+
this.detailOffset = 0;
|
|
1207
|
+
} else {
|
|
1208
|
+
this.scrollDetail(delta > 0 ? 1 : -1);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
this.selectedRun = 0;
|
|
1212
|
+
this.showMorePages = 0;
|
|
1213
|
+
await this.refresh({ preserveSelection: false });
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
private footerHelp(withDetailKeys: boolean): string {
|
|
1217
|
+
const detail = withDetailKeys ? " · PgUp/PgDn detail" : "";
|
|
1218
|
+
const more = this.snapshot.hiddenRuns > 0 ? " · m show more" : "";
|
|
1219
|
+
return `tab focus scope/status/detail · ←→ change · ↑↓/j/k select/scroll${detail} · r refresh${more} · q/esc close`;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
private moveRun(delta: number): void {
|
|
1223
|
+
const runCount = this.snapshot.runs.length;
|
|
1224
|
+
if (runCount === 0) return;
|
|
1225
|
+
const current = Math.max(0, Math.min(runCount - 1, this.selectedRun));
|
|
1226
|
+
const next = (current + delta + runCount) % runCount;
|
|
1227
|
+
if (next !== this.selectedRun) this.detailOffset = 0;
|
|
1228
|
+
this.selectedRun = next;
|
|
1229
|
+
this.tui.requestRender?.();
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
private scrollDetail(delta: number): void {
|
|
1233
|
+
this.detailOffset = Math.max(0, this.detailOffset + delta);
|
|
1234
|
+
this.tui.requestRender?.();
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
private renderControls(width: number): string {
|
|
1238
|
+
const focused = (group: FocusGroup, text: string): string =>
|
|
1239
|
+
this.focus === group
|
|
1240
|
+
? style(this.theme, "accent", text)
|
|
1241
|
+
: style(this.theme, "dim", text);
|
|
1242
|
+
const scopeTabs = this.renderTabSet(
|
|
1243
|
+
[
|
|
1244
|
+
["session", "session"],
|
|
1245
|
+
["cwd", "cwd"],
|
|
1246
|
+
["all", "all"],
|
|
1247
|
+
],
|
|
1248
|
+
this.scope,
|
|
1249
|
+
);
|
|
1250
|
+
const statusTabs = this.renderTabSet(
|
|
1251
|
+
[
|
|
1252
|
+
["all", "all"],
|
|
1253
|
+
["running", "running"],
|
|
1254
|
+
["completed", "completed"],
|
|
1255
|
+
["failed", "failed"],
|
|
1256
|
+
],
|
|
1257
|
+
this.statusFilter,
|
|
1258
|
+
);
|
|
1259
|
+
return clip(
|
|
1260
|
+
`${focused("scope", "scope:")} ${scopeTabs} ${focused("status", "status:")} ${statusTabs} ${focused("detail", "detail:")} scroll`,
|
|
1261
|
+
width,
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
private renderTabSet<T extends string>(
|
|
1266
|
+
tabs: Array<[T, string]>,
|
|
1267
|
+
current: T,
|
|
1268
|
+
): string {
|
|
1269
|
+
return tabs
|
|
1270
|
+
.map(([value, label]) =>
|
|
1271
|
+
value === current
|
|
1272
|
+
? style(this.theme, "accent", `[${label}]`)
|
|
1273
|
+
: style(this.theme, "dim", label),
|
|
1274
|
+
)
|
|
1275
|
+
.join(" ");
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
private renderScopeHelp(width: number): string {
|
|
1279
|
+
return clip(
|
|
1280
|
+
style(
|
|
1281
|
+
this.theme,
|
|
1282
|
+
"dim",
|
|
1283
|
+
"session: this conversation · cwd: this workspace · all: global index + cwd legacy",
|
|
1284
|
+
),
|
|
1285
|
+
width,
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
private renderRuns(width: number, maxVisible: number): string[] {
|
|
1290
|
+
const maxStart = Math.max(0, this.snapshot.runs.length - maxVisible);
|
|
1291
|
+
const windowStart = Math.min(
|
|
1292
|
+
maxStart,
|
|
1293
|
+
Math.max(0, this.selectedRun - maxVisible + 1),
|
|
1294
|
+
);
|
|
1295
|
+
const showCwd = this.scope !== "cwd" && width >= 46;
|
|
1296
|
+
return this.snapshot.runs
|
|
1297
|
+
.slice(windowStart, windowStart + maxVisible)
|
|
1298
|
+
.map((run, index) => {
|
|
1299
|
+
const runIndex = windowStart + index;
|
|
1300
|
+
const marker =
|
|
1301
|
+
runIndex === this.selectedRun
|
|
1302
|
+
? style(this.theme, "accent", "▸")
|
|
1303
|
+
: " ";
|
|
1304
|
+
const status = runStatusLabel(run);
|
|
1305
|
+
const age = fmtAge(run.updatedMs);
|
|
1306
|
+
const cwdLabel = showCwd
|
|
1307
|
+
? ` · ${basename(run.sourceCwd) || run.sourceCwd}`
|
|
1308
|
+
: "";
|
|
1309
|
+
const fullMeta = `${age}${cwdLabel}`;
|
|
1310
|
+
const statusWidth = Math.max(4, Math.min(13, status.length));
|
|
1311
|
+
const fullIdWidth = visibleLength(run.runId);
|
|
1312
|
+
let metaWidth = visibleLength(fullMeta);
|
|
1313
|
+
let idWidth = width - statusWidth - metaWidth - 4;
|
|
1314
|
+
if (showCwd && idWidth < fullIdWidth) {
|
|
1315
|
+
metaWidth = Math.max(
|
|
1316
|
+
visibleLength(age),
|
|
1317
|
+
width - statusWidth - fullIdWidth - 4,
|
|
1318
|
+
);
|
|
1319
|
+
idWidth = width - statusWidth - metaWidth - 4;
|
|
1320
|
+
}
|
|
1321
|
+
idWidth = Math.max(6, idWidth);
|
|
1322
|
+
const meta = clip(fullMeta, metaWidth);
|
|
1323
|
+
const line = `${marker} ${pad(clip(run.runId, idWidth), idWidth)} ${style(this.theme, runStatusColor(run), pad(status, statusWidth))} ${style(this.theme, "muted", meta)}`;
|
|
1324
|
+
return clip(line, width);
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
private renderDetailWindow(
|
|
1329
|
+
detailLines: string[],
|
|
1330
|
+
width: number,
|
|
1331
|
+
height: number,
|
|
1332
|
+
): string[] {
|
|
1333
|
+
if (detailLines.length <= height) {
|
|
1334
|
+
this.detailOffset = 0;
|
|
1335
|
+
return detailLines;
|
|
1336
|
+
}
|
|
1337
|
+
const hintHeight = 1;
|
|
1338
|
+
const contentHeight = Math.max(1, height - hintHeight);
|
|
1339
|
+
const maxOffset = Math.max(0, detailLines.length - contentHeight);
|
|
1340
|
+
this.detailOffset = Math.min(this.detailOffset, maxOffset);
|
|
1341
|
+
const end = Math.min(detailLines.length, this.detailOffset + contentHeight);
|
|
1342
|
+
const hint = style(
|
|
1343
|
+
this.theme,
|
|
1344
|
+
this.focus === "detail" ? "accent" : "dim",
|
|
1345
|
+
`detail ${this.detailOffset + 1}-${end}/${detailLines.length} · ${this.focus === "detail" ? "↑↓/Pg scroll" : "tab to detail"}`,
|
|
1346
|
+
);
|
|
1347
|
+
return [...detailLines.slice(this.detailOffset, end), clip(hint, width)];
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
private renderDetail(run: RunRow, task: TaskRow, width: number): string[] {
|
|
1351
|
+
const lines: string[] = [];
|
|
1352
|
+
const labelWidth = Math.max(8, Math.min(12, Math.floor(width * 0.18)));
|
|
1353
|
+
const divider = (): void => {
|
|
1354
|
+
lines.push(style(this.theme, "border", "─".repeat(Math.max(1, width))));
|
|
1355
|
+
};
|
|
1356
|
+
const section = (title: string): void => {
|
|
1357
|
+
if (lines.length > 0) divider();
|
|
1358
|
+
lines.push(style(this.theme, "accent", title));
|
|
1359
|
+
};
|
|
1360
|
+
const field = (
|
|
1361
|
+
name: string,
|
|
1362
|
+
value: string | null | undefined,
|
|
1363
|
+
color = "muted",
|
|
1364
|
+
): void => {
|
|
1365
|
+
const rendered =
|
|
1366
|
+
value && value.length > 0
|
|
1367
|
+
? sanitizeRunText(value, this.currentSessionId)
|
|
1368
|
+
: "—";
|
|
1369
|
+
const label = style(
|
|
1370
|
+
this.theme,
|
|
1371
|
+
"dim",
|
|
1372
|
+
pad(clip(name, labelWidth), labelWidth),
|
|
1373
|
+
);
|
|
1374
|
+
lines.push(
|
|
1375
|
+
`${label} ${style(this.theme, color, clip(rendered, Math.max(1, width - labelWidth - 1)))}`,
|
|
1376
|
+
);
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1379
|
+
section("RUN");
|
|
1380
|
+
field("Run ID", run.runId, "text");
|
|
1381
|
+
field("Status", runStatusDetail(run), runStatusColor(run));
|
|
1382
|
+
field("Elapsed", fmtElapsed(run.startedAt, run.completedAt));
|
|
1383
|
+
field("Updated", fmtAge(run.updatedMs));
|
|
1384
|
+
|
|
1385
|
+
if (run.childSummary !== undefined) {
|
|
1386
|
+
const latest = run.childSummary.latestFailure;
|
|
1387
|
+
field(
|
|
1388
|
+
"Children",
|
|
1389
|
+
latest === null
|
|
1390
|
+
? `total ${run.childSummary.total} · running ${run.childSummary.running} · failed ${run.childSummary.failed}`
|
|
1391
|
+
: `total ${run.childSummary.total} · failed ${run.childSummary.failed} · latest ${latest.childRunId}${latest.taskId ? `/${latest.taskId}` : ""}${latest.failureKind ? ` · ${latest.failureKind}` : ""}`,
|
|
1392
|
+
childFailureCount(run.childSummary) > 0 ? "error" : "muted",
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
section("ATTEMPT");
|
|
1397
|
+
field(
|
|
1398
|
+
"All",
|
|
1399
|
+
run.tasks
|
|
1400
|
+
.map(
|
|
1401
|
+
(candidate) =>
|
|
1402
|
+
`${candidate.attemptId}:${statusLabel(candidate.status)}`,
|
|
1403
|
+
)
|
|
1404
|
+
.join(" · "),
|
|
1405
|
+
);
|
|
1406
|
+
field(
|
|
1407
|
+
"Selected",
|
|
1408
|
+
`${run.tasks.indexOf(task) + 1}/${run.tasks.length} · ${task.attemptId} · ${statusLabel(task.status)} · ${fmtElapsed(task.startedAt, task.completedAt)}${task.modelLabel ? ` · ${task.modelLabel}` : ""}`,
|
|
1409
|
+
statusColor(task.status),
|
|
1410
|
+
);
|
|
1411
|
+
field("Started", task.startedAt);
|
|
1412
|
+
field("Completed", task.completedAt ?? "running");
|
|
1413
|
+
if (task.failureKind !== null) field("Failure", task.failureKind, "error");
|
|
1414
|
+
|
|
1415
|
+
section(`LOG TAIL (${task.attemptId})`);
|
|
1416
|
+
field("Source", task.logPath ?? task.resultPath);
|
|
1417
|
+
const tail =
|
|
1418
|
+
task.logTail.length > 0
|
|
1419
|
+
? task.logTail
|
|
1420
|
+
: ["No log output loaded for this scope yet."];
|
|
1421
|
+
for (const logLine of tail)
|
|
1422
|
+
lines.push(
|
|
1423
|
+
`${style(this.theme, "dim", "›")} ${clip(sanitizeRunText(logLine, this.currentSessionId), Math.max(1, width - 2))}`,
|
|
1424
|
+
);
|
|
1425
|
+
|
|
1426
|
+
field("Result", task.resultPath);
|
|
1427
|
+
field("Log", task.logPath ?? "—");
|
|
1428
|
+
|
|
1429
|
+
section("WORKSPACE");
|
|
1430
|
+
field("Registry", safeRelative(this.cwd, run.sourceCwd));
|
|
1431
|
+
field("RunsDir", run.runsDir);
|
|
1432
|
+
field("Attempt", safeRelative(this.cwd, task.workspace));
|
|
1433
|
+
field(
|
|
1434
|
+
"Worktree",
|
|
1435
|
+
task.worktreePath === null
|
|
1436
|
+
? "—"
|
|
1437
|
+
: safeRelative(this.cwd, task.worktreePath),
|
|
1438
|
+
);
|
|
1439
|
+
|
|
1440
|
+
if (run.eventTail.length > 0) {
|
|
1441
|
+
section("EVENTS");
|
|
1442
|
+
for (const eventLine of run.eventTail)
|
|
1443
|
+
lines.push(
|
|
1444
|
+
`${style(this.theme, "dim", "›")} ${clip(sanitizeRunText(eventLine, this.currentSessionId), Math.max(1, width - 2))}`,
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
return lines;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
export async function showSubagentPanel(
|
|
1452
|
+
ctx: ExtensionCommandContext,
|
|
1453
|
+
): Promise<void> {
|
|
1454
|
+
if (ctx.mode !== "tui" || !ctx.hasUI) {
|
|
1455
|
+
ctx.ui.notify?.(
|
|
1456
|
+
"/subagent panel is available only in the interactive TUI.",
|
|
1457
|
+
"warning",
|
|
1458
|
+
);
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
const currentSessionId = currentSessionIdFromCtx(ctx);
|
|
1462
|
+
await ctx.ui.custom<void>(
|
|
1463
|
+
(
|
|
1464
|
+
tui: PanelTui,
|
|
1465
|
+
theme: PanelTheme,
|
|
1466
|
+
_keybindings: unknown,
|
|
1467
|
+
done: () => void,
|
|
1468
|
+
) => new SubagentPanel(ctx.cwd, theme, tui, done, currentSessionId),
|
|
1469
|
+
);
|
|
678
1470
|
}
|