parallel-codex-tui 0.1.0 → 0.1.4
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/.parallel-codex/config.example.toml +90 -3
- package/README.md +269 -12
- package/dist/bootstrap.js +50 -18
- package/dist/cli-args.js +96 -14
- package/dist/cli-help.js +20 -0
- package/dist/cli-startup-recovery.js +70 -0
- package/dist/cli-workspace-picker.js +330 -0
- package/dist/cli-workspace-transition.js +33 -0
- package/dist/cli-workspace.js +40 -0
- package/dist/cli.js +291 -35
- package/dist/core/app-root.js +8 -0
- package/dist/core/collaboration-timeline.js +261 -0
- package/dist/core/config-errors.js +14 -0
- package/dist/core/config.js +191 -23
- package/dist/core/file-store.js +130 -6
- package/dist/core/lease-finalization.js +22 -0
- package/dist/core/paths.js +10 -0
- package/dist/core/process-identity.js +48 -0
- package/dist/core/process-mutation-turn.js +128 -0
- package/dist/core/process-ownership.js +276 -0
- package/dist/core/process-tree.js +90 -0
- package/dist/core/router-audit.js +155 -0
- package/dist/core/router-redaction.js +31 -0
- package/dist/core/router.js +473 -42
- package/dist/core/session-index.js +225 -30
- package/dist/core/session-manager.js +1182 -44
- package/dist/core/task-state-machine.js +17 -0
- package/dist/core/workspace-commit-recovery.js +118 -0
- package/dist/core/workspace.js +126 -0
- package/dist/doctor.js +384 -30
- package/dist/domain/schemas.js +127 -6
- package/dist/orchestrator/collaboration-channel.js +255 -4
- package/dist/orchestrator/feature-plan.js +70 -0
- package/dist/orchestrator/judge-artifacts.js +236 -0
- package/dist/orchestrator/orchestrator.js +1777 -212
- package/dist/orchestrator/prompts.js +126 -2
- package/dist/orchestrator/supervisor-summary.js +56 -2
- package/dist/orchestrator/workspace-sandbox.js +911 -0
- package/dist/tui/App.js +2838 -159
- package/dist/tui/AppShell.js +188 -23
- package/dist/tui/CollaborationTimelineView.js +327 -0
- package/dist/tui/FeatureBoardView.js +227 -0
- package/dist/tui/InputBar.js +514 -57
- package/dist/tui/RouterDiagnosticsView.js +469 -0
- package/dist/tui/StatusBar.js +610 -57
- package/dist/tui/TaskSessionsView.js +207 -0
- package/dist/tui/TerminalOutput.js +53 -9
- package/dist/tui/WorkerOutputView.js +1403 -161
- package/dist/tui/WorkerOverviewView.js +250 -0
- package/dist/tui/chat-history.js +25 -0
- package/dist/tui/chat-input.js +67 -19
- package/dist/tui/chat-paste.js +76 -0
- package/dist/tui/display-width.js +41 -3
- package/dist/tui/incremental-text-file.js +101 -0
- package/dist/tui/keyboard.js +46 -0
- package/dist/tui/markdown-text.js +14 -0
- package/dist/tui/raw-input-decoder.js +3 -0
- package/dist/tui/scrolling.js +2 -1
- package/dist/tui/status-line.js +318 -11
- package/dist/tui/task-memory.js +15 -0
- package/dist/tui/task-result.js +105 -0
- package/dist/tui/terminal-screen.js +13 -1
- package/dist/tui/theme-contrast.js +144 -0
- package/dist/tui/theme-preview.js +109 -0
- package/dist/tui/theme.js +158 -0
- package/dist/version.js +1 -1
- package/dist/workers/capabilities.js +212 -0
- package/dist/workers/live-probe.js +176 -0
- package/dist/workers/mock-adapter.js +39 -6
- package/dist/workers/native-attach.js +147 -8
- package/dist/workers/native-session-detection.js +17 -0
- package/dist/workers/process-adapter.js +580 -81
- package/dist/workers/registry.js +4 -2
- package/package.json +17 -2
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function decodeHtmlEntities(text) {
|
|
2
|
+
return text
|
|
3
|
+
.replace(/←/g, "←")
|
|
4
|
+
.replace(/→/g, "→")
|
|
5
|
+
.replace(/↑/g, "↑")
|
|
6
|
+
.replace(/↓/g, "↓")
|
|
7
|
+
.replace(/·/g, "·")
|
|
8
|
+
.replace(/ /g, " ")
|
|
9
|
+
.replace(/&/g, "&")
|
|
10
|
+
.replace(/</g, "<")
|
|
11
|
+
.replace(/>/g, ">")
|
|
12
|
+
.replace(/"/g, "\"")
|
|
13
|
+
.replace(/'/g, "'");
|
|
14
|
+
}
|
package/dist/tui/scrolling.js
CHANGED
|
@@ -12,7 +12,8 @@ export function selectViewportLines(text, height, offsetFromBottom) {
|
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
export function nextScrollOffset(current, delta, maxOffset) {
|
|
15
|
-
|
|
15
|
+
const next = Math.max(0, current + delta);
|
|
16
|
+
return maxOffset === null ? next : Math.min(next, Math.max(0, maxOffset));
|
|
16
17
|
}
|
|
17
18
|
function splitLines(text) {
|
|
18
19
|
if (!text) {
|
package/dist/tui/status-line.js
CHANGED
|
@@ -1,15 +1,31 @@
|
|
|
1
|
+
import { classifyRouterFailure } from "../core/router-audit.js";
|
|
2
|
+
import { compactEndByDisplayWidth } from "./display-width.js";
|
|
3
|
+
export function effectiveWorkerWatchdog(watchdogMs, totalTimeoutMs) {
|
|
4
|
+
if (!watchdogMs || watchdogMs <= 0) {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
if (totalTimeoutMs && totalTimeoutMs > 0 && watchdogMs >= totalTimeoutMs) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
return watchdogMs;
|
|
11
|
+
}
|
|
1
12
|
export function formatStatusLine(state) {
|
|
2
13
|
if (!state) {
|
|
3
14
|
return "idle";
|
|
4
15
|
}
|
|
5
16
|
const parts = [compactTaskId(state.taskId)];
|
|
17
|
+
if (state.main) {
|
|
18
|
+
const mainIdentity = state.mainEngine ? `main/${state.mainEngine}` : "main";
|
|
19
|
+
parts.push(`${mainIdentity} ${formatMainStatus(state.main, state.mainProgress)}`);
|
|
20
|
+
return parts.join(" | ");
|
|
21
|
+
}
|
|
22
|
+
if (state.featureProgress) {
|
|
23
|
+
parts.push(formatFeatureProgress(state.featureProgress));
|
|
24
|
+
}
|
|
6
25
|
if (state.workers?.length) {
|
|
7
26
|
parts.push(formatWorkerSummary(state.workers));
|
|
8
27
|
return parts.join(" | ");
|
|
9
28
|
}
|
|
10
|
-
if (state.main) {
|
|
11
|
-
parts.push(`main ${compactStatus(state.main)}`);
|
|
12
|
-
}
|
|
13
29
|
if (state.judge) {
|
|
14
30
|
parts.push(`judge ${compactStatus(state.judge)}`);
|
|
15
31
|
}
|
|
@@ -21,6 +37,254 @@ export function formatStatusLine(state) {
|
|
|
21
37
|
}
|
|
22
38
|
return parts.join(" | ");
|
|
23
39
|
}
|
|
40
|
+
function formatMainStatus(status, progress) {
|
|
41
|
+
const state = compactStatus(status);
|
|
42
|
+
if (!progress || state === "done" || state === "fail" || state === "stop") {
|
|
43
|
+
return state;
|
|
44
|
+
}
|
|
45
|
+
if (progress.phase === "initialized") {
|
|
46
|
+
return "starting";
|
|
47
|
+
}
|
|
48
|
+
if (progress.phase === "process-starting" || progress.phase === "native-resume-fallback") {
|
|
49
|
+
return formatMainWaitProgress("waiting output", progress.elapsedMs, progress.firstOutputTimeoutMs, "first");
|
|
50
|
+
}
|
|
51
|
+
if (progress.phase === "process-output") {
|
|
52
|
+
return formatMainWaitProgress("responding", progress.elapsedMs, progress.idleTimeoutMs, "idle");
|
|
53
|
+
}
|
|
54
|
+
if (progress.phase === "process-stopping") {
|
|
55
|
+
return "stopping";
|
|
56
|
+
}
|
|
57
|
+
return state;
|
|
58
|
+
}
|
|
59
|
+
function formatMainWaitProgress(label, elapsedMs, budgetMs, budgetLabel) {
|
|
60
|
+
if (!budgetMs || budgetMs <= 0) {
|
|
61
|
+
return label;
|
|
62
|
+
}
|
|
63
|
+
const elapsed = Math.min(Math.max(0, elapsedMs), budgetMs);
|
|
64
|
+
return `${label} · ${formatMainElapsed(elapsed)} / ${formatMainBudget(budgetMs)} ${budgetLabel}`;
|
|
65
|
+
}
|
|
66
|
+
function formatMainElapsed(durationMs) {
|
|
67
|
+
return `${Math.floor(durationMs / 1000)}s`;
|
|
68
|
+
}
|
|
69
|
+
function formatMainBudget(durationMs) {
|
|
70
|
+
if (durationMs >= 60_000 && durationMs % 60_000 === 0) {
|
|
71
|
+
return `${durationMs / 60_000}m`;
|
|
72
|
+
}
|
|
73
|
+
return formatRouteDuration(durationMs);
|
|
74
|
+
}
|
|
75
|
+
function formatFeatureProgress(progress) {
|
|
76
|
+
return `wave ${progress.wave}/${progress.waves} · ${progress.phase} ${progress.completed}/${progress.total}`;
|
|
77
|
+
}
|
|
78
|
+
export function formatRouteStatus(route) {
|
|
79
|
+
if (!route) {
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
const details = [route.mode];
|
|
83
|
+
if (route.source === "forced" || route.source === "fallback") {
|
|
84
|
+
details.push(route.source);
|
|
85
|
+
}
|
|
86
|
+
if (route.router_fallback_resolution === "main") {
|
|
87
|
+
details.push("user Main");
|
|
88
|
+
}
|
|
89
|
+
else if (route.router_fallback_resolution === "parallel") {
|
|
90
|
+
details.push("user Parallel");
|
|
91
|
+
}
|
|
92
|
+
else if (route.router_fallback_resolution === "cancelled") {
|
|
93
|
+
details.push("user cancelled");
|
|
94
|
+
}
|
|
95
|
+
else if (route.router_fallback_resolution === "retry") {
|
|
96
|
+
details.push("Router retry");
|
|
97
|
+
}
|
|
98
|
+
else if (route.router_fallback_resolution === "auto-retry") {
|
|
99
|
+
details.push("auto retry");
|
|
100
|
+
}
|
|
101
|
+
const recovery = routeRecoveryLabel(route);
|
|
102
|
+
if (recovery) {
|
|
103
|
+
details.push(recovery);
|
|
104
|
+
}
|
|
105
|
+
if (typeof route.router_attempt === "number" && route.router_attempt > 1) {
|
|
106
|
+
details.push(`try ${route.router_attempt}`);
|
|
107
|
+
}
|
|
108
|
+
if (route.source === "fallback") {
|
|
109
|
+
const cause = route.router_failure_kind && route.router_failure_kind !== "unknown"
|
|
110
|
+
? route.router_failure_kind
|
|
111
|
+
: classifyRouterFailure(route.reason) ?? "unknown";
|
|
112
|
+
details.push(routeFailureKindLabel(cause, route));
|
|
113
|
+
const proxy = routeProxyStatus(route, cause);
|
|
114
|
+
if (proxy) {
|
|
115
|
+
details.push(proxy);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else if (route.proxy_configured && route.proxy_endpoint) {
|
|
119
|
+
details.push(`via ${route.proxy_endpoint}`);
|
|
120
|
+
}
|
|
121
|
+
if (typeof route.router_total_duration_ms === "number"
|
|
122
|
+
&& typeof route.router_attempt === "number"
|
|
123
|
+
&& route.router_attempt > 1) {
|
|
124
|
+
details.push(`${formatRouteDuration(route.router_total_duration_ms)} total`);
|
|
125
|
+
}
|
|
126
|
+
else if (typeof route.duration_ms === "number"
|
|
127
|
+
&& (route.source !== "forced" || route.duration_ms > 0)) {
|
|
128
|
+
details.push(formatRouteDuration(route.duration_ms));
|
|
129
|
+
}
|
|
130
|
+
return `route ${details.join(" · ")}`;
|
|
131
|
+
}
|
|
132
|
+
function routeRecoveryLabel(route) {
|
|
133
|
+
if (!route.router_recovered_from) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const prefix = route.router_recovered_via === "auto-retry" ? "auto recovered" : "recovered";
|
|
137
|
+
if (route.router_recovered_from !== "timeout") {
|
|
138
|
+
return `${prefix} ${route.router_recovered_from.replaceAll("-", " ")}`;
|
|
139
|
+
}
|
|
140
|
+
const timeout = route.router_recovered_timeout_kind === "first-output"
|
|
141
|
+
? "first output timeout"
|
|
142
|
+
: route.router_recovered_timeout_kind === "idle"
|
|
143
|
+
? "idle timeout"
|
|
144
|
+
: route.router_recovered_timeout_kind === "total"
|
|
145
|
+
? "total timeout"
|
|
146
|
+
: "timeout";
|
|
147
|
+
return `${prefix} ${timeout}`;
|
|
148
|
+
}
|
|
149
|
+
export function formatRoutePendingStatus(state, elapsedMs) {
|
|
150
|
+
if (!state) {
|
|
151
|
+
return "";
|
|
152
|
+
}
|
|
153
|
+
if (state.mode !== "auto") {
|
|
154
|
+
return `route ${state.mode} · forced`;
|
|
155
|
+
}
|
|
156
|
+
const path = routePendingPathLabel(state);
|
|
157
|
+
if (state.phase === "retrying") {
|
|
158
|
+
const details = [
|
|
159
|
+
`retry ${state.attempt}/${state.maxAttempts}`,
|
|
160
|
+
...(state.command && state.command !== "codex" ? [`runner ${state.command}`] : []),
|
|
161
|
+
...(path ? [path] : []),
|
|
162
|
+
`${formatRouteDuration(state.retryDelayMs ?? 0)} backoff`
|
|
163
|
+
];
|
|
164
|
+
return `route ${details.join(" · ")}`;
|
|
165
|
+
}
|
|
166
|
+
const label = routePendingPhaseLabel(state);
|
|
167
|
+
const details = [
|
|
168
|
+
label,
|
|
169
|
+
...(state.attempt > 1 ? [`try ${state.attempt}`] : []),
|
|
170
|
+
...(state.command && state.command !== "codex" ? [`runner ${state.command}`] : []),
|
|
171
|
+
...(path ? [path] : [])
|
|
172
|
+
];
|
|
173
|
+
details.push(...routePendingBudgetDetails(state, elapsedMs));
|
|
174
|
+
return `route ${details.join(" · ")}`;
|
|
175
|
+
}
|
|
176
|
+
function routePendingBudgetDetails(state, elapsedMs) {
|
|
177
|
+
const firstOutputActive = state.phase === "dispatching"
|
|
178
|
+
|| state.phase === "starting"
|
|
179
|
+
|| state.phase === "waiting-output";
|
|
180
|
+
const idleActive = state.phase === "receiving-stderr" || state.phase === "receiving-response";
|
|
181
|
+
const firstOutputIsSeparate = firstOutputActive
|
|
182
|
+
&& typeof state.firstOutputTimeoutMs === "number"
|
|
183
|
+
&& state.firstOutputTimeoutMs < state.timeoutMs;
|
|
184
|
+
const idleIsSeparate = idleActive
|
|
185
|
+
&& typeof state.idleTimeoutMs === "number"
|
|
186
|
+
&& state.idleTimeoutMs < state.timeoutMs;
|
|
187
|
+
if (typeof elapsedMs === "number") {
|
|
188
|
+
if (firstOutputIsSeparate) {
|
|
189
|
+
const boundedElapsedMs = Math.min(state.firstOutputTimeoutMs, Math.max(0, elapsedMs));
|
|
190
|
+
return [
|
|
191
|
+
`${formatRouteElapsed(boundedElapsedMs)} / ${formatRouteDuration(state.firstOutputTimeoutMs)} first`,
|
|
192
|
+
`${formatRouteDuration(state.timeoutMs)} total`
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
const boundedElapsedMs = Math.min(state.timeoutMs, Math.max(0, elapsedMs));
|
|
196
|
+
if (idleIsSeparate) {
|
|
197
|
+
return [
|
|
198
|
+
`${formatRouteElapsed(boundedElapsedMs)} / ${formatRouteDuration(state.timeoutMs)} total`,
|
|
199
|
+
`${formatRouteDuration(state.idleTimeoutMs)} idle`
|
|
200
|
+
];
|
|
201
|
+
}
|
|
202
|
+
return [`${formatRouteElapsed(boundedElapsedMs)} / ${formatRouteDuration(state.timeoutMs)}`];
|
|
203
|
+
}
|
|
204
|
+
if (firstOutputIsSeparate) {
|
|
205
|
+
return [
|
|
206
|
+
`${formatRouteDuration(state.firstOutputTimeoutMs)} first`,
|
|
207
|
+
`${formatRouteDuration(state.timeoutMs)} total`
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
if (idleIsSeparate) {
|
|
211
|
+
return [
|
|
212
|
+
`${formatRouteDuration(state.timeoutMs)} total`,
|
|
213
|
+
`${formatRouteDuration(state.idleTimeoutMs)} idle`
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
return [`${formatRouteDuration(state.timeoutMs)} max`];
|
|
217
|
+
}
|
|
218
|
+
function routeFailureKindLabel(kind, route) {
|
|
219
|
+
if (kind === "timeout") {
|
|
220
|
+
const stage = routeTimeoutStageSuffix(route);
|
|
221
|
+
if (route.router_timeout_kind === "first-output") {
|
|
222
|
+
return "first output timeout";
|
|
223
|
+
}
|
|
224
|
+
if (route.router_timeout_kind === "idle") {
|
|
225
|
+
return `idle timeout${stage}`;
|
|
226
|
+
}
|
|
227
|
+
if (route.router_timeout_kind === "total") {
|
|
228
|
+
return `total timeout${stage}`;
|
|
229
|
+
}
|
|
230
|
+
return `timeout${stage}`;
|
|
231
|
+
}
|
|
232
|
+
if (kind === "unknown") {
|
|
233
|
+
return "unknown failure";
|
|
234
|
+
}
|
|
235
|
+
return kind.replaceAll("-", " ");
|
|
236
|
+
}
|
|
237
|
+
function routeTimeoutStageSuffix(route) {
|
|
238
|
+
if (route.router_failure_stage === "waiting-output") {
|
|
239
|
+
return " waiting output";
|
|
240
|
+
}
|
|
241
|
+
if (route.router_failure_stage !== "streaming") {
|
|
242
|
+
return "";
|
|
243
|
+
}
|
|
244
|
+
if (route.router_stdout_bytes && route.router_stdout_bytes > 0) {
|
|
245
|
+
return " after stdout";
|
|
246
|
+
}
|
|
247
|
+
if (route.router_stderr_bytes && route.router_stderr_bytes > 0) {
|
|
248
|
+
return " after stderr";
|
|
249
|
+
}
|
|
250
|
+
return " after output";
|
|
251
|
+
}
|
|
252
|
+
function routeProxyStatus(route, cause) {
|
|
253
|
+
if (route.proxy_configured === true) {
|
|
254
|
+
return route.proxy_endpoint ? `via ${route.proxy_endpoint}` : "via proxy";
|
|
255
|
+
}
|
|
256
|
+
if (route.proxy_configured === false) {
|
|
257
|
+
return "direct";
|
|
258
|
+
}
|
|
259
|
+
return cause === "timeout" && /\bproxy\b|代理/i.test(route.reason) ? "proxy set" : null;
|
|
260
|
+
}
|
|
261
|
+
function routePendingPhaseLabel(state) {
|
|
262
|
+
if (state.phase === "dispatching" || state.phase === "starting") {
|
|
263
|
+
return "starting";
|
|
264
|
+
}
|
|
265
|
+
if (state.phase === "waiting-output") {
|
|
266
|
+
return "waiting output";
|
|
267
|
+
}
|
|
268
|
+
if (state.phase === "receiving-stderr") {
|
|
269
|
+
return "diagnostics";
|
|
270
|
+
}
|
|
271
|
+
if (state.phase === "receiving-response") {
|
|
272
|
+
return "receiving";
|
|
273
|
+
}
|
|
274
|
+
if (state.phase === "parsing") {
|
|
275
|
+
return "parsing";
|
|
276
|
+
}
|
|
277
|
+
if (state.phase === "stopping") {
|
|
278
|
+
return "stopping";
|
|
279
|
+
}
|
|
280
|
+
return state.scope === "follow-up" ? "follow-up" : "checking";
|
|
281
|
+
}
|
|
282
|
+
function routePendingPathLabel(state) {
|
|
283
|
+
if (state.proxyConfigured === true) {
|
|
284
|
+
return state.proxyEndpoint ? `via ${state.proxyEndpoint}` : "via proxy";
|
|
285
|
+
}
|
|
286
|
+
return state.proxyConfigured === false ? "direct" : null;
|
|
287
|
+
}
|
|
24
288
|
export function formatSelectedWorkerStatus(state, selectedIndex) {
|
|
25
289
|
const worker = state?.workers?.[selectedIndex];
|
|
26
290
|
if (!worker) {
|
|
@@ -28,30 +292,70 @@ export function formatSelectedWorkerStatus(state, selectedIndex) {
|
|
|
28
292
|
}
|
|
29
293
|
return `${compactWorkerLabel(worker.label)} ${compactStatus(worker.status)}`;
|
|
30
294
|
}
|
|
295
|
+
export function selectedWorkerStatusIsRedundant(state) {
|
|
296
|
+
const workers = state?.workers;
|
|
297
|
+
if (!workers?.length) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
const first = compactStatus(workers[0]?.status ?? "");
|
|
301
|
+
return workers.every((worker) => compactStatus(worker.status) === first);
|
|
302
|
+
}
|
|
31
303
|
export function formatWorkerRuntimeStatus(status) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
304
|
+
const detail = [
|
|
305
|
+
status.state.trim() || "idle",
|
|
306
|
+
humanizeWorkerPhase(status.phase),
|
|
307
|
+
status.native_session_id ? `session ${compactNativeSessionId(status.native_session_id)}` : "",
|
|
308
|
+
status.summary.trim() || "no summary"
|
|
309
|
+
].filter(Boolean).join(" · ");
|
|
310
|
+
return compactEndByDisplayWidth(detail, 96);
|
|
35
311
|
}
|
|
36
312
|
export function formatFooterHelp(mode = "chat") {
|
|
37
313
|
if (mode === "native") {
|
|
38
|
-
return "
|
|
314
|
+
return "scroll · ^] logs";
|
|
39
315
|
}
|
|
40
316
|
if (mode === "worker") {
|
|
41
|
-
return "
|
|
317
|
+
return "scroll · Tab · ^O attach · Esc chat";
|
|
42
318
|
}
|
|
43
|
-
return "^W logs · Tab
|
|
319
|
+
return "^W logs · Tab · ^O attach";
|
|
44
320
|
}
|
|
45
321
|
function compactNativeSessionId(sessionId) {
|
|
46
322
|
return sessionId.length > 12 ? `${sessionId.slice(0, 8)}...` : sessionId;
|
|
47
323
|
}
|
|
324
|
+
function formatRouteDuration(durationMs) {
|
|
325
|
+
if (durationMs < 1000) {
|
|
326
|
+
return `${Math.round(durationMs)}ms`;
|
|
327
|
+
}
|
|
328
|
+
if (durationMs < 10000) {
|
|
329
|
+
return `${(durationMs / 1000).toFixed(1).replace(/\.0$/, "")}s`;
|
|
330
|
+
}
|
|
331
|
+
return `${Math.round(durationMs / 1000)}s`;
|
|
332
|
+
}
|
|
333
|
+
function formatRouteElapsed(durationMs) {
|
|
334
|
+
return `${Math.floor(durationMs / 1000)}s`;
|
|
335
|
+
}
|
|
336
|
+
function humanizeWorkerPhase(phase) {
|
|
337
|
+
const normalized = phase.trim().toLowerCase();
|
|
338
|
+
if (!normalized) {
|
|
339
|
+
return "";
|
|
340
|
+
}
|
|
341
|
+
if (normalized === "process-idle-timeout") {
|
|
342
|
+
return "idle timeout";
|
|
343
|
+
}
|
|
344
|
+
if (normalized === "process-exited") {
|
|
345
|
+
return "exited";
|
|
346
|
+
}
|
|
347
|
+
if (normalized === "native-resume-failed") {
|
|
348
|
+
return "resume failed";
|
|
349
|
+
}
|
|
350
|
+
return normalized.replace(/[-_]+/g, " ");
|
|
351
|
+
}
|
|
48
352
|
function formatWorkerSummary(workers) {
|
|
49
353
|
const counts = new Map();
|
|
50
354
|
for (const worker of workers) {
|
|
51
355
|
const state = compactStatus(worker.status);
|
|
52
356
|
counts.set(state, (counts.get(state) ?? 0) + 1);
|
|
53
357
|
}
|
|
54
|
-
const priority = ["fail", "run", "wait", "done"];
|
|
358
|
+
const priority = ["fail", "stop", "run", "wait", "done"];
|
|
55
359
|
const orderedStates = [
|
|
56
360
|
...priority.filter((state) => counts.has(state)),
|
|
57
361
|
...Array.from(counts.keys()).filter((state) => !priority.includes(state)).sort()
|
|
@@ -71,6 +375,9 @@ function compactStatus(status) {
|
|
|
71
375
|
if (state === "failed" || state === "error") {
|
|
72
376
|
return "fail";
|
|
73
377
|
}
|
|
378
|
+
if (state === "cancelled" || state === "canceled") {
|
|
379
|
+
return "stop";
|
|
380
|
+
}
|
|
74
381
|
if (state === "waiting" || state === "queued") {
|
|
75
382
|
return "wait";
|
|
76
383
|
}
|
|
@@ -82,7 +389,7 @@ function compactTaskId(taskId) {
|
|
|
82
389
|
return dated?.[1] ?? withoutPrefix;
|
|
83
390
|
}
|
|
84
391
|
function compactWorkerLabel(label) {
|
|
85
|
-
const match = label.match(/^\s*([^(]+?)\s*\(([^)]+)\)
|
|
392
|
+
const match = label.match(/^\s*([^(]+?)\s*\(([^)]+)\)/);
|
|
86
393
|
if (match) {
|
|
87
394
|
return `${match[1].trim().toLowerCase()}/${match[2].trim().toLowerCase()}`;
|
|
88
395
|
}
|
package/dist/tui/task-memory.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
export function currentSubmitMemoryState(activeTaskId, activeMode) {
|
|
2
|
+
return {
|
|
3
|
+
activeTaskId,
|
|
4
|
+
activeMode: activeTaskId ? "complex" : activeMode
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export function newTaskMemoryState() {
|
|
8
|
+
return {
|
|
9
|
+
activeTaskId: null,
|
|
10
|
+
activeMode: null
|
|
11
|
+
};
|
|
12
|
+
}
|
|
1
13
|
export function chooseSubmitTarget(state, route) {
|
|
2
14
|
if (state.activeTaskId && state.activeMode === "complex") {
|
|
3
15
|
if (route?.mode === "simple") {
|
|
@@ -24,3 +36,6 @@ export function nextSubmitMemoryState(current, target, result) {
|
|
|
24
36
|
activeTaskId: result.mode === "complex" ? result.taskId : null
|
|
25
37
|
};
|
|
26
38
|
}
|
|
39
|
+
export function shouldClearWorkersForSubmit(target) {
|
|
40
|
+
return target.kind === "new-request";
|
|
41
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const RESULT_SECTIONS = [
|
|
2
|
+
{ key: "requirements", heading: "Requirements:", redundantHeading: /^(?:requirements|scope)$/i },
|
|
3
|
+
{ key: "implementation", heading: "Actor work:", redundantHeading: /^(?:actor work|worklog|implementation)$/i },
|
|
4
|
+
{ key: "changes", heading: "Changed files:", redundantHeading: /^(?:changed files|changes)$/i },
|
|
5
|
+
{ key: "review", heading: "Critic review:", redundantHeading: /^(?:critic review|review)$/i },
|
|
6
|
+
{ key: "verification", heading: "Verification:", redundantHeading: /^(?:verification|checks)$/i },
|
|
7
|
+
{ key: "findings", heading: "Critic findings:", redundantHeading: /^(?:critic findings|findings)$/i }
|
|
8
|
+
];
|
|
9
|
+
export function parseTaskResultSummary(text) {
|
|
10
|
+
const lines = text.split(/\r?\n/);
|
|
11
|
+
if (!/^Complex task completed\.$/i.test((lines[0] ?? "").trim())) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const indexes = RESULT_SECTIONS.map((section) => (lines.findIndex((line) => line.trim().toLowerCase() === section.heading.toLowerCase())));
|
|
15
|
+
if (indexes.every((index) => index < 0)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const values = RESULT_SECTIONS.map((section, sectionIndex) => {
|
|
19
|
+
const start = indexes[sectionIndex] ?? -1;
|
|
20
|
+
if (start < 0) {
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
const end = indexes
|
|
24
|
+
.slice(sectionIndex + 1)
|
|
25
|
+
.filter((index) => index > start)
|
|
26
|
+
.reduce((minimum, index) => Math.min(minimum, index), lines.length);
|
|
27
|
+
return normalizeTaskResultSection(lines.slice(start + 1, end), section.redundantHeading);
|
|
28
|
+
});
|
|
29
|
+
const sections = {
|
|
30
|
+
requirements: values[0] ?? "",
|
|
31
|
+
implementation: values[1] ?? "",
|
|
32
|
+
changes: values[2] ?? "",
|
|
33
|
+
review: values[3] ?? "",
|
|
34
|
+
verification: values[4] ?? "",
|
|
35
|
+
findings: values[5] ?? ""
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
outcome: taskResultOutcome(sections.review),
|
|
39
|
+
sections
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function latestTaskResultMessageIndex(messages, taskId) {
|
|
43
|
+
if (taskId) {
|
|
44
|
+
const scoped = findTaskResultMessageIndex(messages, (message) => message.taskId === taskId);
|
|
45
|
+
if (scoped >= 0) {
|
|
46
|
+
return scoped;
|
|
47
|
+
}
|
|
48
|
+
return findTaskResultMessageIndex(messages, (message) => message.taskId === undefined);
|
|
49
|
+
}
|
|
50
|
+
return findTaskResultMessageIndex(messages, () => true);
|
|
51
|
+
}
|
|
52
|
+
function findTaskResultMessageIndex(messages, matchesScope) {
|
|
53
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
54
|
+
const message = messages[index];
|
|
55
|
+
if (message?.from === "system"
|
|
56
|
+
&& matchesScope(message)
|
|
57
|
+
&& parseTaskResultSummary(message.text)) {
|
|
58
|
+
return index;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return -1;
|
|
62
|
+
}
|
|
63
|
+
function normalizeTaskResultSection(lines, redundantHeading) {
|
|
64
|
+
const normalized = lines
|
|
65
|
+
.map(sanitizeTaskResultLine)
|
|
66
|
+
.filter((line, index, all) => !isRedundantTaskResultHeading(line, redundantHeading, index, all));
|
|
67
|
+
while (normalized[0]?.trim() === "") {
|
|
68
|
+
normalized.shift();
|
|
69
|
+
}
|
|
70
|
+
while (normalized.at(-1)?.trim() === "") {
|
|
71
|
+
normalized.pop();
|
|
72
|
+
}
|
|
73
|
+
const value = normalized.join("\n").trim();
|
|
74
|
+
return /^\(empty\)$/i.test(value) ? "" : value;
|
|
75
|
+
}
|
|
76
|
+
function sanitizeTaskResultLine(line) {
|
|
77
|
+
return line
|
|
78
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
79
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "")
|
|
80
|
+
.trimEnd();
|
|
81
|
+
}
|
|
82
|
+
function isRedundantTaskResultHeading(line, redundantHeading, index, all) {
|
|
83
|
+
const heading = line.trim().replace(/^#{1,6}\s+/, "").replace(/:$/, "");
|
|
84
|
+
if (!redundantHeading.test(heading)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return all.slice(0, index).every((previous) => previous.trim() === "");
|
|
88
|
+
}
|
|
89
|
+
function taskResultOutcome(review) {
|
|
90
|
+
for (const line of review.split(/\r?\n/)) {
|
|
91
|
+
const decision = line
|
|
92
|
+
.trim()
|
|
93
|
+
.replace(/^#{1,6}\s+/, "")
|
|
94
|
+
.replace(/^[-*]\s+/, "")
|
|
95
|
+
.replace(/^\*\*([^*]+)\*\*$/, "$1")
|
|
96
|
+
.trim();
|
|
97
|
+
if (/^(?:REVISION_REQUIRED|REJECTED|FAILED)\b/i.test(decision)) {
|
|
98
|
+
return "revision-required";
|
|
99
|
+
}
|
|
100
|
+
if (/^APPROVED\b/i.test(decision)) {
|
|
101
|
+
return "approved";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return "completed";
|
|
105
|
+
}
|
|
@@ -16,6 +16,15 @@ export class NativeTerminalScreen {
|
|
|
16
16
|
this.terminal.write(chunk, resolve);
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
|
+
resize(cols, rows) {
|
|
20
|
+
this.terminal.resize(Math.max(1, Math.trunc(cols)), Math.max(1, Math.trunc(rows)));
|
|
21
|
+
}
|
|
22
|
+
dimensions() {
|
|
23
|
+
return {
|
|
24
|
+
cols: this.terminal.cols,
|
|
25
|
+
rows: this.terminal.rows
|
|
26
|
+
};
|
|
27
|
+
}
|
|
19
28
|
snapshot() {
|
|
20
29
|
return this.snapshotLines().join("\n");
|
|
21
30
|
}
|
|
@@ -151,11 +160,14 @@ function sameStyle(left, right) {
|
|
|
151
160
|
}
|
|
152
161
|
function trimTrailingBlankTerminalLines(lines) {
|
|
153
162
|
let end = lines.length;
|
|
154
|
-
while (end > 0 &&
|
|
163
|
+
while (end > 0 && isUnstyledBlankTerminalLine(lines[end - 1])) {
|
|
155
164
|
end -= 1;
|
|
156
165
|
}
|
|
157
166
|
return lines.slice(0, end);
|
|
158
167
|
}
|
|
168
|
+
function isUnstyledBlankTerminalLine(line) {
|
|
169
|
+
return terminalLineText(line).trim() === "" && line.chunks.every((chunk) => Object.keys(chunk.style).length === 0);
|
|
170
|
+
}
|
|
159
171
|
function terminalLineText(line) {
|
|
160
172
|
return line.chunks.map((chunk) => chunk.text).join("");
|
|
161
173
|
}
|