parallel-codex-tui 0.1.3 → 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.
Files changed (72) hide show
  1. package/.parallel-codex/config.example.toml +90 -3
  2. package/README.md +240 -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-recovery.js +70 -0
  7. package/dist/cli-workspace-picker.js +330 -0
  8. package/dist/cli-workspace-transition.js +33 -0
  9. package/dist/cli-workspace.js +7 -71
  10. package/dist/cli.js +221 -23
  11. package/dist/core/collaboration-timeline.js +261 -0
  12. package/dist/core/config-errors.js +14 -0
  13. package/dist/core/config.js +154 -24
  14. package/dist/core/file-store.js +119 -6
  15. package/dist/core/lease-finalization.js +22 -0
  16. package/dist/core/paths.js +7 -0
  17. package/dist/core/process-identity.js +48 -0
  18. package/dist/core/process-mutation-turn.js +128 -0
  19. package/dist/core/process-ownership.js +276 -0
  20. package/dist/core/process-tree.js +90 -0
  21. package/dist/core/router-audit.js +155 -0
  22. package/dist/core/router-redaction.js +31 -0
  23. package/dist/core/router.js +462 -35
  24. package/dist/core/session-index.js +188 -37
  25. package/dist/core/session-manager.js +1086 -40
  26. package/dist/core/task-state-machine.js +17 -0
  27. package/dist/core/workspace-commit-recovery.js +118 -0
  28. package/dist/core/workspace.js +19 -11
  29. package/dist/doctor.js +343 -23
  30. package/dist/domain/schemas.js +127 -6
  31. package/dist/orchestrator/collaboration-channel.js +255 -4
  32. package/dist/orchestrator/feature-plan.js +70 -0
  33. package/dist/orchestrator/judge-artifacts.js +236 -0
  34. package/dist/orchestrator/orchestrator.js +1749 -202
  35. package/dist/orchestrator/prompts.js +126 -2
  36. package/dist/orchestrator/supervisor-summary.js +56 -2
  37. package/dist/orchestrator/workspace-sandbox.js +911 -0
  38. package/dist/tui/App.js +2830 -153
  39. package/dist/tui/AppShell.js +188 -23
  40. package/dist/tui/CollaborationTimelineView.js +327 -0
  41. package/dist/tui/FeatureBoardView.js +227 -0
  42. package/dist/tui/InputBar.js +514 -57
  43. package/dist/tui/RouterDiagnosticsView.js +469 -0
  44. package/dist/tui/StatusBar.js +610 -57
  45. package/dist/tui/TaskSessionsView.js +207 -0
  46. package/dist/tui/TerminalOutput.js +53 -9
  47. package/dist/tui/WorkerOutputView.js +1403 -161
  48. package/dist/tui/WorkerOverviewView.js +250 -0
  49. package/dist/tui/chat-history.js +25 -0
  50. package/dist/tui/chat-input.js +67 -19
  51. package/dist/tui/chat-paste.js +76 -0
  52. package/dist/tui/display-width.js +41 -3
  53. package/dist/tui/incremental-text-file.js +101 -0
  54. package/dist/tui/keyboard.js +46 -0
  55. package/dist/tui/markdown-text.js +14 -0
  56. package/dist/tui/raw-input-decoder.js +3 -0
  57. package/dist/tui/scrolling.js +2 -1
  58. package/dist/tui/status-line.js +318 -11
  59. package/dist/tui/task-memory.js +13 -1
  60. package/dist/tui/task-result.js +105 -0
  61. package/dist/tui/terminal-screen.js +13 -1
  62. package/dist/tui/theme-contrast.js +144 -0
  63. package/dist/tui/theme-preview.js +109 -0
  64. package/dist/tui/theme.js +158 -0
  65. package/dist/version.js +1 -1
  66. package/dist/workers/capabilities.js +212 -0
  67. package/dist/workers/live-probe.js +176 -0
  68. package/dist/workers/mock-adapter.js +39 -6
  69. package/dist/workers/native-attach.js +78 -3
  70. package/dist/workers/process-adapter.js +570 -77
  71. package/dist/workers/registry.js +4 -2
  72. package/package.json +5 -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(/&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,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 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;
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 "wheel/Pg · ^] detach";
314
+ return "scroll · ^] logs";
39
315
  }
40
316
  if (mode === "worker") {
41
- return "wheel/Pg · Tab worker · ^O attach · Esc chat";
317
+ return "scroll · Tab · ^O attach · Esc chat";
42
318
  }
43
- return "^W logs · Tab worker · ^O attach";
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*\(([^)]+)\)\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
  }
@@ -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
  }