parallel-codex-tui 0.1.3 → 0.1.5

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.
Files changed (78) hide show
  1. package/.parallel-codex/config.example.toml +136 -3
  2. package/README.md +299 -21
  3. package/dist/bootstrap.js +37 -17
  4. package/dist/cli-args.js +34 -3
  5. package/dist/cli-help.js +20 -0
  6. package/dist/cli-startup-preflight.js +18 -0
  7. package/dist/cli-startup-recovery.js +82 -0
  8. package/dist/cli-workspace-picker.js +330 -0
  9. package/dist/cli-workspace-transition.js +33 -0
  10. package/dist/cli-workspace.js +7 -71
  11. package/dist/cli.js +234 -24
  12. package/dist/core/collaboration-timeline.js +268 -0
  13. package/dist/core/config-errors.js +14 -0
  14. package/dist/core/config.js +297 -109
  15. package/dist/core/file-store.js +119 -6
  16. package/dist/core/lease-finalization.js +22 -0
  17. package/dist/core/paths.js +7 -0
  18. package/dist/core/process-identity.js +48 -0
  19. package/dist/core/process-mutation-turn.js +128 -0
  20. package/dist/core/process-ownership.js +276 -0
  21. package/dist/core/process-tree.js +90 -0
  22. package/dist/core/router-audit.js +155 -0
  23. package/dist/core/router-redaction.js +31 -0
  24. package/dist/core/router.js +462 -35
  25. package/dist/core/session-index.js +412 -88
  26. package/dist/core/session-manager.js +1110 -40
  27. package/dist/core/task-session-details.js +175 -0
  28. package/dist/core/task-state-machine.js +18 -0
  29. package/dist/core/workspace-commit-recovery.js +118 -0
  30. package/dist/core/workspace.js +19 -11
  31. package/dist/doctor.js +373 -34
  32. package/dist/domain/schemas.js +142 -7
  33. package/dist/orchestrator/collaboration-channel.js +289 -6
  34. package/dist/orchestrator/feature-plan.js +70 -0
  35. package/dist/orchestrator/final-acceptance.js +86 -0
  36. package/dist/orchestrator/judge-artifacts.js +236 -0
  37. package/dist/orchestrator/orchestrator.js +2086 -203
  38. package/dist/orchestrator/prompts.js +168 -5
  39. package/dist/orchestrator/supervisor-summary.js +56 -2
  40. package/dist/orchestrator/workspace-sandbox.js +927 -0
  41. package/dist/tui/App.js +3187 -161
  42. package/dist/tui/AppShell.js +196 -25
  43. package/dist/tui/CollaborationTimelineView.js +327 -0
  44. package/dist/tui/FeatureBoardView.js +232 -0
  45. package/dist/tui/InputBar.js +581 -57
  46. package/dist/tui/RouterDiagnosticsView.js +469 -0
  47. package/dist/tui/StatusBar.js +610 -57
  48. package/dist/tui/StatusDetailView.js +164 -0
  49. package/dist/tui/TaskSessionDetailView.js +222 -0
  50. package/dist/tui/TaskSessionsView.js +210 -0
  51. package/dist/tui/TerminalOutput.js +53 -9
  52. package/dist/tui/WorkerOutputView.js +1404 -161
  53. package/dist/tui/WorkerOverviewView.js +269 -0
  54. package/dist/tui/chat-history.js +25 -0
  55. package/dist/tui/chat-input.js +67 -19
  56. package/dist/tui/chat-paste.js +76 -0
  57. package/dist/tui/display-width.js +41 -3
  58. package/dist/tui/incremental-text-file.js +101 -0
  59. package/dist/tui/keyboard.js +49 -0
  60. package/dist/tui/markdown-text.js +14 -0
  61. package/dist/tui/raw-input-decoder.js +3 -0
  62. package/dist/tui/scrolling.js +2 -1
  63. package/dist/tui/status-line.js +360 -11
  64. package/dist/tui/task-memory.js +13 -1
  65. package/dist/tui/task-result.js +105 -0
  66. package/dist/tui/terminal-screen.js +13 -1
  67. package/dist/tui/theme-contrast.js +144 -0
  68. package/dist/tui/theme-preview.js +109 -0
  69. package/dist/tui/theme.js +158 -0
  70. package/dist/version.js +1 -1
  71. package/dist/workers/capabilities.js +213 -0
  72. package/dist/workers/live-probe.js +177 -0
  73. package/dist/workers/mock-adapter.js +73 -8
  74. package/dist/workers/native-attach.js +106 -16
  75. package/dist/workers/process-adapter.js +572 -77
  76. package/dist/workers/provider.js +26 -0
  77. package/dist/workers/registry.js +12 -20
  78. package/package.json +11 -2
@@ -7,6 +7,39 @@ export function isAttachShortcut(input, key) {
7
7
  export function isExitShortcut(input, key) {
8
8
  return (key.ctrl === true && input.toLowerCase() === "c") || input === "\u0003";
9
9
  }
10
+ export function isNewTaskShortcut(input, key) {
11
+ return (key.ctrl === true && input.toLowerCase() === "n") || input === "\u000e";
12
+ }
13
+ export function isWorkspaceShortcut(input, key) {
14
+ return (key.ctrl === true && input.toLowerCase() === "p") || input === "\u0010";
15
+ }
16
+ export function isRouterDiagnosticsShortcut(input, key) {
17
+ return (key.ctrl === true && input.toLowerCase() === "g") || input === "\u0007";
18
+ }
19
+ export function isWorkerOverviewShortcut(input, key) {
20
+ return (key.ctrl === true && input.toLowerCase() === "b") || input === "\u0002";
21
+ }
22
+ export function isTaskSessionsShortcut(input, key) {
23
+ return (key.ctrl === true && input.toLowerCase() === "t") || input === "\u0014";
24
+ }
25
+ export function isTaskResultShortcut(input, key) {
26
+ return (key.ctrl === true && input.toLowerCase() === "d") || input === "\u0004";
27
+ }
28
+ export function isStatusDetailsShortcut(input, key) {
29
+ return (key.ctrl === true && input.toLowerCase() === "s") || input === "\u0013";
30
+ }
31
+ export function isWorkerSearchShortcut(input, key) {
32
+ return (key.ctrl === true && input.toLowerCase() === "f") || input === "\u0006";
33
+ }
34
+ export function workerLogJumpKind(input) {
35
+ if (input === "e" || input === "E") {
36
+ return "error";
37
+ }
38
+ if (input === "d" || input === "D") {
39
+ return "diff";
40
+ }
41
+ return null;
42
+ }
10
43
  export function scrollDelta(input, key, pageSize) {
11
44
  if (key.pageUp || (key.ctrl === true && input.toLowerCase() === "u")) {
12
45
  return pageSize;
@@ -16,6 +49,22 @@ export function scrollDelta(input, key, pageSize) {
16
49
  }
17
50
  return 0;
18
51
  }
52
+ export function rawPageScrollDelta(input, pageSize) {
53
+ const size = Math.max(1, pageSize);
54
+ let delta = 0;
55
+ for (const match of input.matchAll(/\x1b\[(5|6)~/g)) {
56
+ delta += match[1] === "5" ? size : -size;
57
+ }
58
+ return delta;
59
+ }
60
+ export function rawHistoryDelta(input) {
61
+ let delta = 0;
62
+ for (const match of input.matchAll(/\x1b(?:O([AB])|\[[0-9;?]*([AB]))/g)) {
63
+ const direction = match[1] ?? match[2];
64
+ delta += direction === "A" ? 1 : -1;
65
+ }
66
+ return delta;
67
+ }
19
68
  export function mouseScrollDelta(input, linesPerWheel = 3) {
20
69
  let delta = 0;
21
70
  for (const match of input.matchAll(/\x1b\[<(\d+);\d+;\d+[mM]/g)) {
@@ -0,0 +1,14 @@
1
+ export function decodeHtmlEntities(text) {
2
+ return text
3
+ .replace(/&larr;/g, "←")
4
+ .replace(/&rarr;/g, "→")
5
+ .replace(/&uarr;/g, "↑")
6
+ .replace(/&darr;/g, "↓")
7
+ .replace(/&middot;/g, "·")
8
+ .replace(/&nbsp;/g, " ")
9
+ .replace(/&amp;/g, "&")
10
+ .replace(/&lt;/g, "<")
11
+ .replace(/&gt;/g, ">")
12
+ .replace(/&quot;/g, "\"")
13
+ .replace(/&#39;/g, "'");
14
+ }
@@ -16,3 +16,6 @@ export function createRawInputDecoder() {
16
16
  }
17
17
  };
18
18
  }
19
+ export function tokenizeRawInput(input) {
20
+ return Array.from(input.matchAll(/\x1b\[M[\s\S]{3}|\x1b\[[0-?]*[ -/]*[@-~]|\x1bO[\s\S]|\x1b[^\x00-\x1f\x7f]|[\s\S]/gu), (match) => match[0]);
21
+ }
@@ -12,7 +12,8 @@ export function selectViewportLines(text, height, offsetFromBottom) {
12
12
  };
13
13
  }
14
14
  export function nextScrollOffset(current, delta, maxOffset) {
15
- return clamp(current + delta, 0, Math.max(0, maxOffset));
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) {
@@ -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,293 @@ 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
+ export function formatRouteSummaryStatus(route) {
133
+ if (!route) {
134
+ return "";
135
+ }
136
+ const details = [route.mode];
137
+ if (route.source === "forced") {
138
+ details.push("forced");
139
+ }
140
+ else if (route.source === "fallback") {
141
+ details.push("fallback");
142
+ const cause = route.router_failure_kind && route.router_failure_kind !== "unknown"
143
+ ? route.router_failure_kind
144
+ : classifyRouterFailure(route.reason) ?? "unknown";
145
+ details.push(cause === "timeout" ? "timeout" : cause.replaceAll("-", " "));
146
+ }
147
+ else if (route.router_recovered_from) {
148
+ details.push("recovered");
149
+ }
150
+ return `route ${details.join(" · ")}`;
151
+ }
152
+ export function formatRoutePendingSummaryStatus(state, elapsedMs) {
153
+ if (!state) {
154
+ return "";
155
+ }
156
+ if (state.mode !== "auto") {
157
+ return `route ${state.mode} · forced`;
158
+ }
159
+ if (state.phase === "retrying") {
160
+ return `route retry ${state.attempt}/${state.maxAttempts}`;
161
+ }
162
+ const details = [routePendingPhaseLabel(state)];
163
+ if (state.attempt > 1) {
164
+ details.push(`try ${state.attempt}`);
165
+ }
166
+ if (typeof elapsedMs === "number") {
167
+ details.push(formatRouteElapsed(Math.max(0, elapsedMs)));
168
+ }
169
+ return `route ${details.join(" · ")}`;
170
+ }
171
+ function routeRecoveryLabel(route) {
172
+ if (!route.router_recovered_from) {
173
+ return null;
174
+ }
175
+ const prefix = route.router_recovered_via === "auto-retry" ? "auto recovered" : "recovered";
176
+ if (route.router_recovered_from !== "timeout") {
177
+ return `${prefix} ${route.router_recovered_from.replaceAll("-", " ")}`;
178
+ }
179
+ const timeout = route.router_recovered_timeout_kind === "first-output"
180
+ ? "first output timeout"
181
+ : route.router_recovered_timeout_kind === "idle"
182
+ ? "idle timeout"
183
+ : route.router_recovered_timeout_kind === "total"
184
+ ? "total timeout"
185
+ : "timeout";
186
+ return `${prefix} ${timeout}`;
187
+ }
188
+ export function formatRoutePendingStatus(state, elapsedMs) {
189
+ if (!state) {
190
+ return "";
191
+ }
192
+ if (state.mode !== "auto") {
193
+ return `route ${state.mode} · forced`;
194
+ }
195
+ const path = routePendingPathLabel(state);
196
+ if (state.phase === "retrying") {
197
+ const details = [
198
+ `retry ${state.attempt}/${state.maxAttempts}`,
199
+ ...(state.command && state.command !== "codex" ? [`runner ${state.command}`] : []),
200
+ ...(path ? [path] : []),
201
+ `${formatRouteDuration(state.retryDelayMs ?? 0)} backoff`
202
+ ];
203
+ return `route ${details.join(" · ")}`;
204
+ }
205
+ const label = routePendingPhaseLabel(state);
206
+ const details = [
207
+ label,
208
+ ...(state.attempt > 1 ? [`try ${state.attempt}`] : []),
209
+ ...(state.command && state.command !== "codex" ? [`runner ${state.command}`] : []),
210
+ ...(path ? [path] : [])
211
+ ];
212
+ details.push(...routePendingBudgetDetails(state, elapsedMs));
213
+ return `route ${details.join(" · ")}`;
214
+ }
215
+ function routePendingBudgetDetails(state, elapsedMs) {
216
+ const firstOutputActive = state.phase === "dispatching"
217
+ || state.phase === "starting"
218
+ || state.phase === "waiting-output";
219
+ const idleActive = state.phase === "receiving-stderr" || state.phase === "receiving-response";
220
+ const firstOutputIsSeparate = firstOutputActive
221
+ && typeof state.firstOutputTimeoutMs === "number"
222
+ && state.firstOutputTimeoutMs < state.timeoutMs;
223
+ const idleIsSeparate = idleActive
224
+ && typeof state.idleTimeoutMs === "number"
225
+ && state.idleTimeoutMs < state.timeoutMs;
226
+ if (typeof elapsedMs === "number") {
227
+ if (firstOutputIsSeparate) {
228
+ const boundedElapsedMs = Math.min(state.firstOutputTimeoutMs, Math.max(0, elapsedMs));
229
+ return [
230
+ `${formatRouteElapsed(boundedElapsedMs)} / ${formatRouteDuration(state.firstOutputTimeoutMs)} first`,
231
+ `${formatRouteDuration(state.timeoutMs)} total`
232
+ ];
233
+ }
234
+ const boundedElapsedMs = Math.min(state.timeoutMs, Math.max(0, elapsedMs));
235
+ if (idleIsSeparate) {
236
+ return [
237
+ `${formatRouteElapsed(boundedElapsedMs)} / ${formatRouteDuration(state.timeoutMs)} total`,
238
+ `${formatRouteDuration(state.idleTimeoutMs)} idle`
239
+ ];
240
+ }
241
+ return [`${formatRouteElapsed(boundedElapsedMs)} / ${formatRouteDuration(state.timeoutMs)}`];
242
+ }
243
+ if (firstOutputIsSeparate) {
244
+ return [
245
+ `${formatRouteDuration(state.firstOutputTimeoutMs)} first`,
246
+ `${formatRouteDuration(state.timeoutMs)} total`
247
+ ];
248
+ }
249
+ if (idleIsSeparate) {
250
+ return [
251
+ `${formatRouteDuration(state.timeoutMs)} total`,
252
+ `${formatRouteDuration(state.idleTimeoutMs)} idle`
253
+ ];
254
+ }
255
+ return [`${formatRouteDuration(state.timeoutMs)} max`];
256
+ }
257
+ function routeFailureKindLabel(kind, route) {
258
+ if (kind === "timeout") {
259
+ const stage = routeTimeoutStageSuffix(route);
260
+ if (route.router_timeout_kind === "first-output") {
261
+ return "first output timeout";
262
+ }
263
+ if (route.router_timeout_kind === "idle") {
264
+ return `idle timeout${stage}`;
265
+ }
266
+ if (route.router_timeout_kind === "total") {
267
+ return `total timeout${stage}`;
268
+ }
269
+ return `timeout${stage}`;
270
+ }
271
+ if (kind === "unknown") {
272
+ return "unknown failure";
273
+ }
274
+ return kind.replaceAll("-", " ");
275
+ }
276
+ function routeTimeoutStageSuffix(route) {
277
+ if (route.router_failure_stage === "waiting-output") {
278
+ return " waiting output";
279
+ }
280
+ if (route.router_failure_stage !== "streaming") {
281
+ return "";
282
+ }
283
+ if (route.router_stdout_bytes && route.router_stdout_bytes > 0) {
284
+ return " after stdout";
285
+ }
286
+ if (route.router_stderr_bytes && route.router_stderr_bytes > 0) {
287
+ return " after stderr";
288
+ }
289
+ return " after output";
290
+ }
291
+ function routeProxyStatus(route, cause) {
292
+ if (route.proxy_configured === true) {
293
+ return route.proxy_endpoint ? `via ${route.proxy_endpoint}` : "via proxy";
294
+ }
295
+ if (route.proxy_configured === false) {
296
+ return "direct";
297
+ }
298
+ return cause === "timeout" && /\bproxy\b|代理/i.test(route.reason) ? "proxy set" : null;
299
+ }
300
+ function routePendingPhaseLabel(state) {
301
+ if (state.phase === "dispatching" || state.phase === "starting") {
302
+ return "starting";
303
+ }
304
+ if (state.phase === "waiting-output") {
305
+ return "waiting output";
306
+ }
307
+ if (state.phase === "receiving-stderr") {
308
+ return "diagnostics";
309
+ }
310
+ if (state.phase === "receiving-response") {
311
+ return "receiving";
312
+ }
313
+ if (state.phase === "parsing") {
314
+ return "parsing";
315
+ }
316
+ if (state.phase === "stopping") {
317
+ return "stopping";
318
+ }
319
+ return state.scope === "follow-up" ? "follow-up" : "checking";
320
+ }
321
+ function routePendingPathLabel(state) {
322
+ if (state.proxyConfigured === true) {
323
+ return state.proxyEndpoint ? `via ${state.proxyEndpoint}` : "via proxy";
324
+ }
325
+ return state.proxyConfigured === false ? "direct" : null;
326
+ }
24
327
  export function formatSelectedWorkerStatus(state, selectedIndex) {
25
328
  const worker = state?.workers?.[selectedIndex];
26
329
  if (!worker) {
@@ -28,30 +331,70 @@ export function formatSelectedWorkerStatus(state, selectedIndex) {
28
331
  }
29
332
  return `${compactWorkerLabel(worker.label)} ${compactStatus(worker.status)}`;
30
333
  }
334
+ export function selectedWorkerStatusIsRedundant(state) {
335
+ const workers = state?.workers;
336
+ if (!workers?.length) {
337
+ return false;
338
+ }
339
+ const first = compactStatus(workers[0]?.status ?? "");
340
+ return workers.every((worker) => compactStatus(worker.status) === first);
341
+ }
31
342
  export function formatWorkerRuntimeStatus(status) {
32
- const native = status.native_session_id ? ` native:${compactNativeSessionId(status.native_session_id)}` : "";
33
- const detail = `${status.state}/${status.phase}${native}: ${status.summary.trim() || "no summary"}`;
34
- return detail.length > 96 ? `${detail.slice(0, 93)}...` : detail;
343
+ const detail = [
344
+ status.state.trim() || "idle",
345
+ humanizeWorkerPhase(status.phase),
346
+ status.native_session_id ? `session ${compactNativeSessionId(status.native_session_id)}` : "",
347
+ status.summary.trim() || "no summary"
348
+ ].filter(Boolean).join(" · ");
349
+ return compactEndByDisplayWidth(detail, 96);
35
350
  }
36
351
  export function formatFooterHelp(mode = "chat") {
37
352
  if (mode === "native") {
38
- return "wheel/Pg · ^] detach";
353
+ return "scroll · ^] logs";
39
354
  }
40
355
  if (mode === "worker") {
41
- return "wheel/Pg · Tab worker · ^O attach · Esc chat";
356
+ return "scroll · Tab · ^O attach · Esc chat";
42
357
  }
43
- return "^W logs · Tab worker · ^O attach";
358
+ return "^W logs · Tab · ^O attach";
44
359
  }
45
360
  function compactNativeSessionId(sessionId) {
46
361
  return sessionId.length > 12 ? `${sessionId.slice(0, 8)}...` : sessionId;
47
362
  }
363
+ function formatRouteDuration(durationMs) {
364
+ if (durationMs < 1000) {
365
+ return `${Math.round(durationMs)}ms`;
366
+ }
367
+ if (durationMs < 10000) {
368
+ return `${(durationMs / 1000).toFixed(1).replace(/\.0$/, "")}s`;
369
+ }
370
+ return `${Math.round(durationMs / 1000)}s`;
371
+ }
372
+ function formatRouteElapsed(durationMs) {
373
+ return `${Math.floor(durationMs / 1000)}s`;
374
+ }
375
+ function humanizeWorkerPhase(phase) {
376
+ const normalized = phase.trim().toLowerCase();
377
+ if (!normalized) {
378
+ return "";
379
+ }
380
+ if (normalized === "process-idle-timeout") {
381
+ return "idle timeout";
382
+ }
383
+ if (normalized === "process-exited") {
384
+ return "exited";
385
+ }
386
+ if (normalized === "native-resume-failed") {
387
+ return "resume failed";
388
+ }
389
+ return normalized.replace(/[-_]+/g, " ");
390
+ }
48
391
  function formatWorkerSummary(workers) {
49
392
  const counts = new Map();
50
393
  for (const worker of workers) {
51
394
  const state = compactStatus(worker.status);
52
395
  counts.set(state, (counts.get(state) ?? 0) + 1);
53
396
  }
54
- const priority = ["fail", "run", "wait", "done"];
397
+ const priority = ["fail", "stop", "run", "wait", "done"];
55
398
  const orderedStates = [
56
399
  ...priority.filter((state) => counts.has(state)),
57
400
  ...Array.from(counts.keys()).filter((state) => !priority.includes(state)).sort()
@@ -71,6 +414,12 @@ function compactStatus(status) {
71
414
  if (state === "failed" || state === "error") {
72
415
  return "fail";
73
416
  }
417
+ if (state === "cancelled" || state === "canceled") {
418
+ return "stop";
419
+ }
420
+ if (state === "paused") {
421
+ return "wait";
422
+ }
74
423
  if (state === "waiting" || state === "queued") {
75
424
  return "wait";
76
425
  }
@@ -82,7 +431,7 @@ function compactTaskId(taskId) {
82
431
  return dated?.[1] ?? withoutPrefix;
83
432
  }
84
433
  function compactWorkerLabel(label) {
85
- const match = label.match(/^\s*([^(]+?)\s*\(([^)]+)\)\s*$/);
434
+ const match = label.match(/^\s*([^(]+?)\s*\(([^)]+)\)/);
86
435
  if (match) {
87
436
  return `${match[1].trim().toLowerCase()}/${match[2].trim().toLowerCase()}`;
88
437
  }
@@ -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") {
@@ -25,5 +37,5 @@ export function nextSubmitMemoryState(current, target, result) {
25
37
  };
26
38
  }
27
39
  export function shouldClearWorkersForSubmit(target) {
28
- return target.kind !== "task-question";
40
+ return target.kind === "new-request";
29
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 && terminalLineText(lines[end - 1]).trim() === "") {
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
  }