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
@@ -1,60 +1,152 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from "react";
3
3
  import { basename, dirname, join } from "node:path";
4
- import { readdir } from "node:fs/promises";
4
+ import { readdir, stat } from "node:fs/promises";
5
5
  import { Box, Text } from "ink";
6
6
  import { pathExists, readTextIfExists } from "../core/file-store.js";
7
7
  import { compactEndByDisplayWidth, displayWidth, wrapByDisplayWidth } from "./display-width.js";
8
+ import { decodeHtmlEntities } from "./markdown-text.js";
9
+ import { createIncrementalTextFileReader } from "./incremental-text-file.js";
8
10
  import { selectViewportLines } from "./scrolling.js";
9
- const WORKER_PANEL_BACKGROUND = "ansi256(235)";
10
- const WORKER_PANEL_TITLE_BACKGROUND = "ansi256(24)";
11
- export function WorkerOutputView({ title, role, logPath, scrollOffset = 0, height = 24, terminalWidth = Number(process.stdout.columns) || 120, onViewportChange }) {
11
+ import { TUI_THEME } from "./theme.js";
12
+ const EMPTY_WORKER_OUTPUT_TEXT = "waiting for output";
13
+ const LOADING_WORKER_OUTPUT_TEXT = "loading output";
14
+ const NO_WORKER_OUTPUT_TEXT = "No workers yet · start a complex task";
15
+ export function workerOutputNavigationTargets(lines, height, query = "") {
16
+ const viewportHeight = Math.max(1, Math.trunc(height));
17
+ const maxOffset = Math.max(0, lines.length - viewportHeight);
18
+ const normalizedQuery = query.trim().toLocaleLowerCase();
19
+ const searchOffsets = [];
20
+ const searchLineIndexes = [];
21
+ const errorOffsets = [];
22
+ const diffOffsets = [];
23
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
24
+ const line = lines[index];
25
+ if (!line) {
26
+ continue;
27
+ }
28
+ const offset = Math.min(maxOffset, Math.max(0, lines.length - 1 - index));
29
+ if (normalizedQuery && line.text.toLocaleLowerCase().includes(normalizedQuery)) {
30
+ searchOffsets.push(offset);
31
+ searchLineIndexes.push(index);
32
+ }
33
+ if (line.kind === "error") {
34
+ errorOffsets.push(offset);
35
+ }
36
+ if (line.kind === "diff-file" || line.kind === "diff-hunk") {
37
+ diffOffsets.push(offset);
38
+ }
39
+ }
40
+ return { searchOffsets, searchLineIndexes, errorOffsets, diffOffsets };
41
+ }
42
+ export function WorkerOutputView({ title, role, featureId, logPath, scrollOffset = 0, height = 24, terminalWidth = Number(process.stdout.columns) || 120, searchQuery = "", searchMatchIndex = 0, onViewportChange, onNavigationChange }) {
12
43
  const nanoOutput = isNanoWorkerOutputWidth(terminalWidth);
13
- const contentKey = workerOutputContentKey(role, logPath, nanoOutput);
44
+ const contentKey = workerOutputContentKey(role, featureId, logPath, nanoOutput);
14
45
  const [contentState, setContentState] = useState({ key: "", lines: [] });
15
46
  useEffect(() => {
16
47
  let active = true;
48
+ let refreshing = false;
49
+ let refreshQueued = false;
50
+ let rendered = false;
51
+ let artifactSignature = null;
17
52
  const loadKey = contentKey;
53
+ const outputReader = logPath ? createIncrementalTextFileReader(logPath) : null;
18
54
  async function load() {
19
- if (!logPath) {
20
- setContentState({
21
- key: loadKey,
22
- lines: [{ kind: "content", text: "No worker log selected." }]
23
- });
24
- return;
25
- }
26
- if (nanoOutput) {
27
- const lines = await loadNanoWorkerOutputLines(logPath, height);
55
+ try {
56
+ if (!logPath || !outputReader) {
57
+ if (rendered) {
58
+ return;
59
+ }
60
+ rendered = true;
61
+ setContentState({
62
+ key: loadKey,
63
+ lines: [{ kind: "placeholder", text: NO_WORKER_OUTPUT_TEXT }]
64
+ });
65
+ return;
66
+ }
67
+ const snapshot = await outputReader.read();
68
+ const nextArtifactSignature = nanoOutput
69
+ ? ""
70
+ : await workerArtifactSignature(role, dirname(logPath), featureId);
71
+ if (rendered && !snapshot.changed && nextArtifactSignature === artifactSignature) {
72
+ return;
73
+ }
74
+ rendered = true;
75
+ artifactSignature = nextArtifactSignature;
76
+ const output = snapshot.text;
77
+ if (nanoOutput) {
78
+ const lines = await loadNanoWorkerOutputLines(logPath, height, output);
79
+ if (active) {
80
+ setContentState({
81
+ key: loadKey,
82
+ lines
83
+ });
84
+ }
85
+ return;
86
+ }
87
+ const sections = await loadWorkerOutputSections(role, logPath, output, featureId);
28
88
  if (active) {
29
89
  setContentState({
30
90
  key: loadKey,
31
- lines
91
+ lines: renderLinesFromSections(sections, { nanoProcess: nanoOutput, height })
32
92
  });
33
93
  }
34
- return;
35
94
  }
36
- const sections = await loadWorkerOutputSections(role, logPath);
37
- if (active) {
38
- setContentState({
39
- key: loadKey,
40
- lines: renderLinesFromSections(sections, { nanoProcess: nanoOutput, height })
41
- });
95
+ catch (error) {
96
+ if (active) {
97
+ const detail = error instanceof Error ? error.message : String(error);
98
+ setContentState({
99
+ key: loadKey,
100
+ lines: [{ kind: "error", text: `output read failed · ${detail}` }]
101
+ });
102
+ }
42
103
  }
43
104
  }
44
- void load();
105
+ const refresh = () => {
106
+ if (refreshing) {
107
+ refreshQueued = true;
108
+ return;
109
+ }
110
+ refreshing = true;
111
+ void (async () => {
112
+ do {
113
+ refreshQueued = false;
114
+ await load();
115
+ } while (active && refreshQueued);
116
+ })().finally(() => {
117
+ refreshing = false;
118
+ if (active && refreshQueued) {
119
+ refresh();
120
+ }
121
+ });
122
+ };
123
+ refresh();
45
124
  const interval = setInterval(() => {
46
- void load();
125
+ refresh();
47
126
  }, 1000);
48
127
  return () => {
49
128
  active = false;
50
129
  clearInterval(interval);
51
130
  };
52
- }, [contentKey, height, logPath, nanoOutput, role]);
53
- const content = contentState.key === contentKey
131
+ }, [contentKey, featureId, height, logPath, nanoOutput, role]);
132
+ const contentReady = contentState.key === contentKey;
133
+ const content = contentReady
54
134
  ? contentState.lines
55
- : [{ kind: "content", text: "Loading worker log..." }];
135
+ : [{ kind: "placeholder", text: LOADING_WORKER_OUTPUT_TEXT }];
56
136
  const sourceLines = isNanoWorkerOutputWidth(terminalWidth) ? tinyWorkerOutputSourceLines(content, height) : content;
57
- const displayLines = renderDisplayLines(sourceLines, terminalWidth);
137
+ const panelWidth = workerOutputPanelRailWidth(terminalWidth);
138
+ const nanoRender = isNanoWorkerOutputWidth(terminalWidth);
139
+ const displayLines = renderDisplayLines(sourceLines, terminalWidth, {
140
+ contentWidth: nanoRender ? undefined : Math.max(1, panelWidth - 2),
141
+ nano: nanoRender
142
+ });
143
+ const navigationTargets = workerOutputNavigationTargets(displayLines, height, searchQuery);
144
+ const selectedSearchIndex = navigationTargets.searchLineIndexes.length > 0
145
+ ? Math.min(navigationTargets.searchLineIndexes.length - 1, Math.max(0, Math.trunc(searchMatchIndex)))
146
+ : -1;
147
+ const selectedSearchLineIndex = selectedSearchIndex >= 0
148
+ ? navigationTargets.searchLineIndexes[selectedSearchIndex] ?? -1
149
+ : -1;
58
150
  const selection = selectViewportLines(displayLines.map((line) => line.text).join("\n"), height, scrollOffset);
59
151
  const end = displayLines.length - selection.clampedOffset;
60
152
  const rawStart = Math.max(0, end - Math.max(1, height));
@@ -67,30 +159,58 @@ export function WorkerOutputView({ title, role, logPath, scrollOffset = 0, heigh
67
159
  ? 0
68
160
  : workerOutputTailTopPaddingLines(selection.clampedOffset, selection.maxOffset, visibleLines.length, height);
69
161
  useEffect(() => {
162
+ if (!contentReady) {
163
+ return;
164
+ }
70
165
  onViewportChange?.({
71
166
  offset: selection.clampedOffset,
72
167
  maxOffset: selection.maxOffset
73
168
  });
74
- }, [onViewportChange, selection.clampedOffset, selection.maxOffset]);
75
- const panelWidth = workerOutputPanelRailWidth(terminalWidth);
169
+ }, [contentReady, onViewportChange, selection.clampedOffset, selection.maxOffset]);
170
+ useEffect(() => {
171
+ onNavigationChange?.(navigationTargets);
172
+ }, [
173
+ navigationTargets.diffOffsets.join(","),
174
+ navigationTargets.errorOffsets.join(","),
175
+ navigationTargets.searchLineIndexes.join(","),
176
+ navigationTargets.searchOffsets.join(","),
177
+ onNavigationChange
178
+ ]);
76
179
  const scrollLabel = workerOutputScrollDisplay(selection.clampedOffset, selection.maxOffset, terminalWidth);
77
- const displayTitle = workerOutputHeaderDisplay(title, selection.maxOffset > 0 ? scrollLabel : null, Math.max(1, panelWidth - 2));
78
- const nanoRender = isNanoWorkerOutputWidth(terminalWidth);
79
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(WorkerOutputTitleRail, { title: displayTitle, width: panelWidth }), Array.from({ length: topPaddingLines }, (_, index) => _jsx(Text, { children: " " }, `tail-pad-${index}`)), visibleLines.length > 0
180
+ const normalizedSearchQuery = searchQuery.trim();
181
+ const navigationLabel = normalizedSearchQuery
182
+ ? `find ${selectedSearchIndex >= 0 ? selectedSearchIndex + 1 : 0}/${navigationTargets.searchOffsets.length}`
183
+ : selection.maxOffset > 0 ? scrollLabel : null;
184
+ const displayTitle = workerOutputHeaderDisplay(title, navigationLabel, Math.max(1, panelWidth - 2));
185
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(WorkerOutputTitleRail, { title: displayTitle, width: panelWidth }), Array.from({ length: topPaddingLines }, (_, index) => _jsx(WorkerOutputBlankLine, { width: panelWidth }, `tail-pad-${index}`)), visibleLines.length > 0
80
186
  ? visibleLines.map((line, index) => nanoRender
81
- ? _jsx(WorkerOutputNanoLine, { line: line, width: terminalWidth }, index)
82
- : _jsx(WorkerOutputLine, { line: line }, index))
83
- : _jsx(Text, { children: "(empty log)" })] }));
187
+ ? _jsx(WorkerOutputNanoLine, { fillWidth: panelWidth, line: line, selected: start + index === selectedSearchLineIndex, width: terminalWidth }, index)
188
+ : _jsx(WorkerOutputLine, { line: line, selected: start + index === selectedSearchLineIndex, width: panelWidth }, index))
189
+ : _jsx(Text, { ...workerOutputEmptyFallbackTheme(), children: EMPTY_WORKER_OUTPUT_TEXT })] }));
84
190
  }
85
191
  function WorkerOutputTitleRail({ title, width }) {
86
192
  const titleText = ` ${title} `;
193
+ const segments = workerOutputTitleSegments(title);
87
194
  const renderWidth = typeof process.stdout.columns === "number"
88
195
  ? width
89
196
  : null;
90
197
  const trailingWidth = renderWidth === null
91
198
  ? 0
92
199
  : Math.max(0, renderWidth - displayWidth(titleText));
93
- return (_jsxs(Box, { children: [_jsx(Text, { backgroundColor: WORKER_PANEL_TITLE_BACKGROUND, color: "white", bold: true, children: titleText }), trailingWidth > 0 ? _jsx(Text, { backgroundColor: WORKER_PANEL_BACKGROUND, children: " ".repeat(trailingWidth) }) : null] }));
200
+ return (_jsxs(Box, { children: [_jsx(Text, { backgroundColor: TUI_THEME.chrome, children: " " }), _jsx(Text, { backgroundColor: TUI_THEME.chrome, color: TUI_THEME.accent, bold: true, children: segments.identity }), segments.metadata ? (_jsx(Text, { backgroundColor: TUI_THEME.chrome, color: TUI_THEME.muted, children: segments.metadata })) : null, _jsx(Text, { backgroundColor: TUI_THEME.chrome, children: " " }), trailingWidth > 0 ? _jsx(Text, { backgroundColor: TUI_THEME.chrome, children: " ".repeat(trailingWidth) }) : null] }));
201
+ }
202
+ function workerOutputTitleSegments(title) {
203
+ const separatorIndex = title.indexOf(" · ");
204
+ if (separatorIndex >= 0) {
205
+ return {
206
+ identity: title.slice(0, separatorIndex),
207
+ metadata: title.slice(separatorIndex)
208
+ };
209
+ }
210
+ const spaceIndex = title.indexOf(" ");
211
+ return spaceIndex > 0
212
+ ? { identity: title.slice(0, spaceIndex), metadata: title.slice(spaceIndex) }
213
+ : { identity: title, metadata: "" };
94
214
  }
95
215
  function workerOutputPanelRailWidth(terminalWidth) {
96
216
  const renderWidth = typeof process.stdout.columns === "number"
@@ -128,17 +248,33 @@ function lastIndexWhere(values, predicate) {
128
248
  }
129
249
  return -1;
130
250
  }
131
- function workerOutputContentKey(role, logPath, nanoOutput = false) {
132
- return `${role ?? ""}:${logPath ?? ""}:${nanoOutput ? "nano" : "full"}`;
251
+ function workerOutputContentKey(role, featureId, logPath, nanoOutput = false) {
252
+ return `${role ?? ""}:${featureId ?? ""}:${logPath ?? ""}:${nanoOutput ? "nano" : "full"}`;
133
253
  }
134
254
  export function workerOutputTitleDisplay(title, width) {
135
- const match = title.match(/^(.+?)\s+\(([^)]+)\)\s+output(?:\s+\((\d+\/\d+)\))?$/i);
255
+ const match = title.match(/^(.+?)\s+\(([^)]+)\)(?:\s+·\s+(.+?))?\s+output(?:\s+\((\d+\/\d+)\))?$/i);
136
256
  if (!match) {
137
- return title.replace(/\s+output\b/i, "");
257
+ return compactEndByDisplayWidth(title.replace(/\s+output\b/i, "").trim(), width);
138
258
  }
139
259
  const role = (match[1] ?? "").trim().toLowerCase();
140
260
  const provider = (match[2] ?? "").trim().toLowerCase();
141
- const page = match[3] ?? "";
261
+ const detail = (match[3] ?? "").trim();
262
+ const page = match[4] ?? "";
263
+ const detailed = joinWorkerOutputChromeParts([`${role}/${provider}`, detail, page]);
264
+ if (detail && displayWidth(detailed) <= width) {
265
+ return detailed;
266
+ }
267
+ if (detail) {
268
+ const fixed = joinWorkerOutputChromeParts([`${role}/${provider}`, page]);
269
+ const detailBudget = width - displayWidth(fixed) - displayWidth(" · ");
270
+ if (detailBudget >= 8) {
271
+ return joinWorkerOutputChromeParts([
272
+ `${role}/${provider}`,
273
+ compactEndByDisplayWidth(detail, detailBudget),
274
+ page
275
+ ]);
276
+ }
277
+ }
142
278
  const compact = joinWorkerOutputChromeParts([`${role}/${provider}`, page]);
143
279
  if (displayWidth(compact) <= width) {
144
280
  return compact;
@@ -361,7 +497,7 @@ function latestSectionBoundaryWithinTail(lines, start, end) {
361
497
  }
362
498
  return latestSectionStart !== null && end - latestSectionStart >= 3 ? latestSectionStart : null;
363
499
  }
364
- async function loadWorkerOutputSections(role, logPath) {
500
+ async function loadWorkerOutputSections(role, logPath, processOutput, featureId) {
365
501
  const workerDir = dirname(logPath);
366
502
  const sections = [];
367
503
  const artifactFiles = roleArtifactFiles(role);
@@ -372,28 +508,28 @@ async function loadWorkerOutputSections(role, logPath) {
372
508
  }
373
509
  sections.push({ group: "role", title: file, text });
374
510
  }
375
- for (const artifact of await featureArtifactSections(role, workerDir)) {
511
+ for (const artifact of await featureArtifactSections(role, workerDir, featureId)) {
376
512
  const text = artifact.text.trim();
377
513
  if (!text) {
378
514
  continue;
379
515
  }
380
516
  sections.push({ group: "feature", title: artifact.label, text });
381
517
  }
382
- const rawOutput = (await readTextIfExists(logPath)).trimEnd();
518
+ const rawOutput = (processOutput ?? await readTextIfExists(logPath)).trimEnd();
383
519
  if (rawOutput) {
384
520
  sections.push({ group: "process", title: "output.log", text: rawOutput });
385
521
  }
386
522
  return sections;
387
523
  }
388
- async function loadNanoWorkerOutputLines(logPath, height) {
389
- const rawOutput = (await readTextIfExists(logPath)).trimEnd();
524
+ async function loadNanoWorkerOutputLines(logPath, height, processOutput) {
525
+ const rawOutput = (processOutput ?? await readTextIfExists(logPath)).trimEnd();
390
526
  if (!rawOutput) {
391
- return [{ kind: "content", text: "No worker output." }];
527
+ return [{ kind: "placeholder", text: EMPTY_WORKER_OUTPUT_TEXT }];
392
528
  }
393
529
  const lines = renderNanoProcessContent(compactNanoProcessText(rawOutput, height));
394
530
  return lines.length > 0
395
531
  ? [{ kind: "group", text: "process" }, ...lines]
396
- : [{ kind: "content", text: "No worker output." }];
532
+ : [{ kind: "placeholder", text: EMPTY_WORKER_OUTPUT_TEXT }];
397
533
  }
398
534
  function renderNanoProcessContent(text) {
399
535
  const lines = [];
@@ -438,7 +574,7 @@ function roleArtifactFiles(role) {
438
574
  }
439
575
  return [];
440
576
  }
441
- async function featureArtifactSections(role, workerDir) {
577
+ async function featureArtifactSections(role, workerDir, featureId) {
442
578
  const files = featureArtifactFiles(role);
443
579
  if (files.length === 0) {
444
580
  return [];
@@ -448,11 +584,7 @@ async function featureArtifactSections(role, workerDir) {
448
584
  if (!(await pathExists(featuresDir))) {
449
585
  return [];
450
586
  }
451
- const entries = await readdir(featuresDir, { withFileTypes: true });
452
- const featureDirs = entries
453
- .filter((entry) => entry.isDirectory())
454
- .map((entry) => entry.name)
455
- .sort();
587
+ const featureDirs = await scopedFeatureDirectoryNames(featuresDir, workerDir, featureId);
456
588
  const sections = [];
457
589
  for (const featureDir of featureDirs) {
458
590
  for (const file of files) {
@@ -468,6 +600,57 @@ async function featureArtifactSections(role, workerDir) {
468
600
  }
469
601
  return sections;
470
602
  }
603
+ async function workerArtifactSignature(role, workerDir, featureId) {
604
+ const paths = roleArtifactFiles(role).map((file) => join(workerDir, file));
605
+ const featureFiles = featureArtifactFiles(role);
606
+ if (featureFiles.length > 0) {
607
+ const featuresDir = join(dirname(workerDir), "features");
608
+ if (await pathExists(featuresDir)) {
609
+ const featureDirs = await scopedFeatureDirectoryNames(featuresDir, workerDir, featureId);
610
+ for (const featureDir of featureDirs) {
611
+ for (const file of featureFiles) {
612
+ paths.push(join(featuresDir, featureDir, file));
613
+ }
614
+ }
615
+ }
616
+ }
617
+ const signatures = await Promise.all(paths.map(async (path) => {
618
+ try {
619
+ const metadata = await stat(path);
620
+ return `${path}\0${metadata.dev}:${metadata.ino}:${metadata.size}:${metadata.mtimeMs}:${metadata.ctimeMs}`;
621
+ }
622
+ catch (error) {
623
+ if (error.code === "ENOENT") {
624
+ return `${path}\0missing`;
625
+ }
626
+ throw error;
627
+ }
628
+ }));
629
+ return signatures.join("\u0001");
630
+ }
631
+ async function scopedFeatureDirectoryNames(featuresDir, workerDir, featureId) {
632
+ if (featureId) {
633
+ return [featureId];
634
+ }
635
+ const turnId = workerTurnIdFromDirectory(workerDir);
636
+ const entries = await readdir(featuresDir, { withFileTypes: true });
637
+ return entries
638
+ .filter((entry) => entry.isDirectory() && (!turnId || entry.name === turnId || entry.name.startsWith(`${turnId}-`)))
639
+ .map((entry) => entry.name)
640
+ .sort();
641
+ }
642
+ function workerTurnIdFromDirectory(workerDir) {
643
+ const workerId = basename(workerDir);
644
+ const waveTurn = workerId.match(/-wave-(\d{4,})-/)?.[1];
645
+ if (waveTurn) {
646
+ return waveTurn;
647
+ }
648
+ const scopedTurn = workerId.match(/-(\d{4,})(?:-|$)/)?.[1];
649
+ if (scopedTurn) {
650
+ return scopedTurn;
651
+ }
652
+ return /^(?:judge|actor|critic)-/.test(workerId) ? "0001" : null;
653
+ }
471
654
  function featureArtifactFiles(role) {
472
655
  if (role === "actor") {
473
656
  return ["actor-worklog.md", "actor-replies.jsonl"];
@@ -655,7 +838,7 @@ function renderLinesFromSections(sections, options = {}) {
655
838
  renderedArtifactFiles.add(basename(section.title));
656
839
  }
657
840
  }
658
- return lines;
841
+ return lines.length > 0 ? lines : [{ kind: "placeholder", text: EMPTY_WORKER_OUTPUT_TEXT }];
659
842
  }
660
843
  function compactNanoProcessText(text, height) {
661
844
  const rawLines = text.split(/\r?\n/);
@@ -773,8 +956,16 @@ function renderSectionContent(section, renderedArtifactFiles = new Set()) {
773
956
  index = collapseDiff ? skipProcessDiffTailContext(rawLines, diffBlock.nextIndex) : diffBlock.nextIndex;
774
957
  continue;
775
958
  }
776
- if (section.group === "process" && hiddenProcessDiffFiles.size > 0 && isDiffHunkLine(line.trimStart())) {
777
- index = collectBareDiffHunkBlock(rawLines, index).nextIndex;
959
+ if (section.group === "process" && parseHunkStart(line.trimStart())) {
960
+ const bareHunk = collectBareDiffHunkBlock(rawLines, index);
961
+ const bareDiffFile = inferBareDiffFile(rawLines, index);
962
+ if (!bareDiffTargetsRenderedArtifact(bareHunk.lines, bareDiffFile, hiddenProcessDiffFiles)) {
963
+ if (lines.length > 0 && lines[lines.length - 1]?.kind !== "blank") {
964
+ lines.push({ kind: "blank", text: "" });
965
+ }
966
+ lines.push(...renderBareDiffHunkContent(bareHunk.lines, bareDiffFile));
967
+ }
968
+ index = skipBareDiffTailContext(rawLines, bareHunk.nextIndex);
778
969
  continue;
779
970
  }
780
971
  if (section.group === "process" &&
@@ -797,13 +988,10 @@ function renderSectionContent(section, renderedArtifactFiles = new Set()) {
797
988
  continue;
798
989
  }
799
990
  }
800
- if (isMarkdownTableSeparator(trimmed)) {
801
- index += 1;
802
- continue;
803
- }
804
- if (isMarkdownTableRow(trimmed)) {
805
- lines.push({ kind: "table", text: renderMarkdownTableRow(trimmed) });
806
- index += 1;
991
+ const tableBlock = collectMarkdownTableBlock(rawLines, index);
992
+ if (tableBlock) {
993
+ lines.push(...tableBlock.lines);
994
+ index = tableBlock.nextIndex;
807
995
  continue;
808
996
  }
809
997
  lines.push(classifyRenderedLine(section, line));
@@ -820,6 +1008,25 @@ function processDiffBlockTargetsArtifacts(diffLines, artifactFiles) {
820
1008
  return artifactFiles.has(diffTitlePath(title));
821
1009
  });
822
1010
  }
1011
+ function bareDiffTargetsRenderedArtifact(diffLines, diffFile, artifactFiles) {
1012
+ if (artifactFiles.size === 0) {
1013
+ return false;
1014
+ }
1015
+ if (artifactFiles.has("patch.diff")) {
1016
+ return true;
1017
+ }
1018
+ if (diffFile) {
1019
+ return artifactFiles.has(basename(diffFile));
1020
+ }
1021
+ const additions = diffLines
1022
+ .filter((line) => line.startsWith("+") && !line.startsWith("+++"))
1023
+ .map((line) => line.slice(1).trim())
1024
+ .filter(Boolean);
1025
+ return (artifactFiles.has("review.md")
1026
+ && additions.some((line) => /^(?:#{1,6}\s+)?(?:Critic Review|APPROVED|REVISION_REQUIRED)\b/i.test(line))) || ((artifactFiles.has("worklog.md") || artifactFiles.has("actor-worklog.md"))
1027
+ && additions.some((line) => /^(?:#{1,6}\s+)?(?:Actor )?Worklog\b/i.test(line))) || (artifactFiles.has("decisions.md")
1028
+ && additions.some((line) => /^(?:#{1,6}\s+)?Decisions\b/i.test(line)));
1029
+ }
823
1030
  function compactRenderedBlankLines(lines) {
824
1031
  const compacted = [];
825
1032
  let blankPending = false;
@@ -1052,7 +1259,137 @@ function isSingleFilePathListItem(trimmed) {
1052
1259
  /^[A-Za-z0-9._@+-]+\/[A-Za-z0-9._@+*?/-]+$/.test(trimmed));
1053
1260
  }
1054
1261
  function cleanProcessRenderLines(lines) {
1055
- return removePatchDiffReadbackNoise(removeDiffAdjacentCodeSummaries(mergeSmokeStatusSuccessLines(lines)));
1262
+ return dedupeSupersededChecklistSnapshots(dedupeBareDiffHunks(dedupeCollapsedDiffSummaries(removePatchDiffReadbackNoise(removeDiffAdjacentCodeSummaries(mergeSmokeStatusSuccessLines(lines))))));
1263
+ }
1264
+ function dedupeBareDiffHunks(lines) {
1265
+ const blocks = [];
1266
+ let currentDiffFile = null;
1267
+ for (let index = 0; index < lines.length;) {
1268
+ const line = lines[index];
1269
+ if (!line) {
1270
+ index += 1;
1271
+ continue;
1272
+ }
1273
+ if (line.kind === "diff-file") {
1274
+ currentDiffFile = line.text.trim();
1275
+ index += 1;
1276
+ continue;
1277
+ }
1278
+ if (line.kind !== "diff-hunk") {
1279
+ if (line.kind !== "blank" && !isAnyDiffLine(line)) {
1280
+ currentDiffFile = null;
1281
+ }
1282
+ index += 1;
1283
+ continue;
1284
+ }
1285
+ const startIndex = index;
1286
+ index += 1;
1287
+ while (index < lines.length && isDiffHunkBodyLine(lines[index])) {
1288
+ index += 1;
1289
+ }
1290
+ const blockLines = lines.slice(startIndex, index);
1291
+ const diffFile = line.diffFile ?? currentDiffFile;
1292
+ blocks.push({
1293
+ startIndex,
1294
+ endIndex: index,
1295
+ key: [
1296
+ `file\0${diffFile ?? ""}`,
1297
+ ...blockLines.map((line) => `${line.kind}\0${line.text}`)
1298
+ ].join("\n")
1299
+ });
1300
+ }
1301
+ const lastBlockByKey = new Map();
1302
+ for (const block of blocks) {
1303
+ lastBlockByKey.set(block.key, block);
1304
+ }
1305
+ const hiddenIndexes = new Set();
1306
+ for (const block of blocks) {
1307
+ if (lastBlockByKey.get(block.key) === block) {
1308
+ continue;
1309
+ }
1310
+ for (let index = block.startIndex; index < block.endIndex; index += 1) {
1311
+ hiddenIndexes.add(index);
1312
+ }
1313
+ }
1314
+ return hiddenIndexes.size > 0
1315
+ ? lines.filter((_line, index) => !hiddenIndexes.has(index))
1316
+ : lines;
1317
+ }
1318
+ function isDiffHunkBodyLine(line) {
1319
+ return line?.kind === "diff-context" ||
1320
+ line?.kind === "diff-meta" ||
1321
+ line?.kind === "diff-add" ||
1322
+ line?.kind === "diff-remove";
1323
+ }
1324
+ function isAnyDiffLine(line) {
1325
+ return line.kind === "diff-file" ||
1326
+ line.kind === "diff-summary" ||
1327
+ line.kind === "diff-hunk" ||
1328
+ line.kind === "diff-context" ||
1329
+ line.kind === "diff-meta" ||
1330
+ line.kind === "diff-add" ||
1331
+ line.kind === "diff-remove";
1332
+ }
1333
+ function dedupeSupersededChecklistSnapshots(lines) {
1334
+ const snapshots = [];
1335
+ for (let index = 0; index < lines.length;) {
1336
+ const firstItem = checklistSnapshotItem(lines[index]);
1337
+ if (!firstItem) {
1338
+ index += 1;
1339
+ continue;
1340
+ }
1341
+ const items = [];
1342
+ const startIndex = index;
1343
+ while (index < lines.length) {
1344
+ const item = checklistSnapshotItem(lines[index]);
1345
+ if (!item) {
1346
+ break;
1347
+ }
1348
+ items.push(item);
1349
+ index += 1;
1350
+ }
1351
+ if (items.length >= 2) {
1352
+ snapshots.push({ startIndex, endIndex: index, key: items.join("\n") });
1353
+ }
1354
+ }
1355
+ const lastSnapshotByKey = new Map();
1356
+ for (const snapshot of snapshots) {
1357
+ lastSnapshotByKey.set(snapshot.key, snapshot);
1358
+ }
1359
+ const hidden = new Set();
1360
+ for (const snapshot of snapshots) {
1361
+ if (lastSnapshotByKey.get(snapshot.key) === snapshot) {
1362
+ continue;
1363
+ }
1364
+ for (let index = snapshot.startIndex; index < snapshot.endIndex; index += 1) {
1365
+ hidden.add(index);
1366
+ }
1367
+ }
1368
+ return hidden.size > 0 ? lines.filter((_line, index) => !hidden.has(index)) : lines;
1369
+ }
1370
+ function checklistSnapshotItem(line) {
1371
+ if (!line || (line.kind !== "content" && line.kind !== "success")) {
1372
+ return null;
1373
+ }
1374
+ return line.text.trim().match(/^(?:✓|✔|→|•)\s+(.+)$/)?.[1]?.trim() ?? null;
1375
+ }
1376
+ function dedupeCollapsedDiffSummaries(lines) {
1377
+ const deduped = [];
1378
+ const seen = new Set();
1379
+ for (const line of lines) {
1380
+ if (isCollapsedDiffSummaryLine(line)) {
1381
+ const key = line.text.trim();
1382
+ if (seen.has(key)) {
1383
+ while (deduped[deduped.length - 1]?.kind === "blank") {
1384
+ deduped.pop();
1385
+ }
1386
+ continue;
1387
+ }
1388
+ seen.add(key);
1389
+ }
1390
+ deduped.push(line);
1391
+ }
1392
+ return deduped;
1056
1393
  }
1057
1394
  function mergeSmokeStatusSuccessLines(lines) {
1058
1395
  const merged = [];
@@ -1197,13 +1534,13 @@ function diffTitlePath(title) {
1197
1534
  }
1198
1535
  const SOURCE_OUTPUT_COLLAPSE_MIN_LINES = 8;
1199
1536
  const CODE_OUTPUT_COLLAPSE_MIN_LINES = 4;
1200
- const FILE_LIST_OUTPUT_COLLAPSE_MIN_LINES = 12;
1537
+ const FILE_LIST_OUTPUT_COLLAPSE_MIN_LINES = 4;
1201
1538
  const READ_SUMMARY_RUN_COLLAPSE_MIN_ITEMS = 3;
1202
1539
  const NODE_TEST_OUTPUT_COLLAPSE_MIN_PASSES = 3;
1203
1540
  const PROCESS_DIFF_COLLAPSE_MIN_BODY_LINES = 1;
1204
1541
  const PROCESS_DIFF_COLLAPSE_MIN_FILES = 4;
1205
1542
  function sanitizeProcessOutput(text, options = {}) {
1206
- const rawLines = text.split("\n");
1543
+ const rawLines = dedupeRepeatedAssistantNarrativeBlocks(text.split("\n"));
1207
1544
  const kept = [];
1208
1545
  const hideAssistantNarration = options.hideAssistantNarration ?? false;
1209
1546
  const renderedArtifactFiles = options.renderedArtifactFiles ?? new Set();
@@ -1222,8 +1559,8 @@ function sanitizeProcessOutput(text, options = {}) {
1222
1559
  index = skipCodexStartupPreamble(rawLines, index);
1223
1560
  continue;
1224
1561
  }
1225
- if (hideAssistantNarration && isAssistantNarrativeMarker(trimmed)) {
1226
- index = skipAssistantNarrativeBlock(rawLines, index);
1562
+ if (isAssistantNarrativeMarker(trimmed)) {
1563
+ index = hideAssistantNarration ? skipAssistantNarrativeBlock(rawLines, index) : index + 1;
1227
1564
  continue;
1228
1565
  }
1229
1566
  if (hideAssistantNarration && isUnmarkedAssistantNarrativeStart(trimmed)) {
@@ -1240,16 +1577,30 @@ function sanitizeProcessOutput(text, options = {}) {
1240
1577
  index = skipRolePromptTranscript(rawLines, index);
1241
1578
  continue;
1242
1579
  }
1580
+ const hiddenCommandBatch = collectParallelHiddenCommandBatch(rawLines, index, renderedArtifactFiles);
1581
+ if (hiddenCommandBatch) {
1582
+ kept.push(...hiddenCommandBatch.lines);
1583
+ index = hiddenCommandBatch.nextIndex;
1584
+ continue;
1585
+ }
1586
+ const parallelRead = collectParallelReadCommandBatch(rawLines, index, renderedArtifactFiles);
1587
+ if (parallelRead) {
1588
+ kept.push(parallelRead.summary);
1589
+ index = parallelRead.nextIndex;
1590
+ continue;
1591
+ }
1243
1592
  const execCommand = collectProcessExecCommand(rawLines, index);
1244
1593
  if (execCommand) {
1245
- if (processCommandReadsRenderedArtifact(execCommand.command, renderedArtifactFiles) ||
1246
- processCommandReadsParallelCodexTaskFile(execCommand.command) ||
1247
- processCommandInspectsSessionMetadata(execCommand.command) ||
1248
- processCommandReadsSessionSystemFile(execCommand.command) ||
1249
- processCommandReadsSkillDocument(execCommand.command)) {
1594
+ if (shouldHideProcessCommand(execCommand, renderedArtifactFiles)) {
1250
1595
  index = skipProcessCommandOutputBlock(rawLines, execCommand.nextIndex);
1251
1596
  continue;
1252
1597
  }
1598
+ const chainedRead = collectChainedReadCommandBlock(rawLines, execCommand.command, execCommand.nextIndex);
1599
+ if (chainedRead) {
1600
+ kept.push(chainedRead.summary);
1601
+ index = chainedRead.nextIndex;
1602
+ continue;
1603
+ }
1253
1604
  kept.push(execCommand.commandLine);
1254
1605
  index = execCommand.nextIndex;
1255
1606
  continue;
@@ -1257,7 +1608,151 @@ function sanitizeProcessOutput(text, options = {}) {
1257
1608
  kept.push(rawLines[index] ?? "");
1258
1609
  index += 1;
1259
1610
  }
1260
- return collapseProcessOutputLines(compactReadSummaryRuns(compactNoMatchSearchCommandBlocks(compactAnnotatedStatusCommandPairs(compactNodeTestOutputBlocks(compactBuildOutputBlocks(compactDevServerFallbackBlocks(compactCollapsedCommandBlocks(dropSuccessfulInternalLaunchCommands(collapseVerboseProcessOutput(kept)))))))))).join("\n").trimEnd();
1611
+ const compactedNetworkOutput = compactRecoveredNetworkEvents(kept);
1612
+ const shieldedDiff = shieldProcessDiffBlocks(compactedNetworkOutput);
1613
+ const sanitized = collapseProcessOutputLines(compactReadSummaryRuns(compactNoMatchSearchCommandBlocks(compactAnnotatedStatusCommandPairs(compactNodeTestOutputBlocks(compactBuildOutputBlocks(compactDevServerFallbackBlocks(compactCollapsedCommandBlocks(dropSuccessfulInternalLaunchCommands(collapseVerboseProcessOutput(shieldedDiff.lines))))))))));
1614
+ return sanitized
1615
+ .map((line) => shieldedDiff.originals.get(line) ?? line)
1616
+ .join("\n")
1617
+ .trimEnd();
1618
+ }
1619
+ function shieldProcessDiffBlocks(lines) {
1620
+ const shielded = [];
1621
+ const originals = new Map();
1622
+ const occupied = new Set(lines.map((line) => normalizedProcessLine(line)));
1623
+ let namespace = 0;
1624
+ let tokenPrefix = "pct protected diff line";
1625
+ while (Array.from(occupied).some((line) => line.startsWith(`${tokenPrefix} `))) {
1626
+ namespace += 1;
1627
+ tokenPrefix = `pct protected diff line namespace ${namespace}`;
1628
+ }
1629
+ let tokenIndex = 0;
1630
+ const appendBlock = (blockLines) => {
1631
+ for (const line of blockLines) {
1632
+ const token = `${tokenPrefix} ${String(tokenIndex).padStart(8, "0")}`;
1633
+ tokenIndex += 1;
1634
+ originals.set(token, line);
1635
+ shielded.push(token);
1636
+ }
1637
+ };
1638
+ for (let index = 0; index < lines.length;) {
1639
+ const line = normalizedProcessLine(lines[index] ?? "");
1640
+ const trimmedStart = line.trimStart();
1641
+ if (isDiffStartLine(trimmedStart)) {
1642
+ const block = collectEmbeddedDiffBlock(lines, index);
1643
+ appendBlock(block.lines);
1644
+ index = block.nextIndex;
1645
+ continue;
1646
+ }
1647
+ if (parseHunkStart(trimmedStart)) {
1648
+ const block = collectBareDiffHunkBlock(lines, index);
1649
+ appendBlock(block.lines);
1650
+ index = block.nextIndex;
1651
+ continue;
1652
+ }
1653
+ shielded.push(lines[index] ?? "");
1654
+ index += 1;
1655
+ }
1656
+ return { lines: shielded, originals };
1657
+ }
1658
+ function compactRecoveredNetworkEvents(lines) {
1659
+ const compacted = [];
1660
+ const reconnectAttempts = [];
1661
+ let httpsFallbacks = 0;
1662
+ let summaryIndex = null;
1663
+ for (let index = 0; index < lines.length;) {
1664
+ const event = collectRecoveredNetworkEvent(lines, index);
1665
+ if (!event) {
1666
+ compacted.push(lines[index] ?? "");
1667
+ index += 1;
1668
+ continue;
1669
+ }
1670
+ summaryIndex ??= compacted.length;
1671
+ reconnectAttempts.push(...event.reconnectAttempts);
1672
+ httpsFallbacks += event.httpsFallbacks;
1673
+ index = event.nextIndex;
1674
+ }
1675
+ if (summaryIndex !== null) {
1676
+ compacted.splice(summaryIndex, 0, formatRecoveredNetworkSummary(reconnectAttempts, httpsFallbacks));
1677
+ }
1678
+ return compacted;
1679
+ }
1680
+ function collectRecoveredNetworkEvent(lines, startIndex) {
1681
+ const first = normalizedProcessLine(lines[startIndex] ?? "").trim();
1682
+ if (!isNetworkRetryDiagnostic(first) && !reconnectAttempt(first)) {
1683
+ return null;
1684
+ }
1685
+ const reconnectAttempts = [];
1686
+ let httpsFallbacks = 0;
1687
+ let index = startIndex;
1688
+ let sawNetworkLine = false;
1689
+ while (index < lines.length) {
1690
+ const trimmed = normalizedProcessLine(lines[index] ?? "").trim();
1691
+ const attempt = reconnectAttempt(trimmed);
1692
+ if (isNetworkRetryDiagnostic(trimmed)) {
1693
+ sawNetworkLine = true;
1694
+ index += 1;
1695
+ continue;
1696
+ }
1697
+ if (attempt) {
1698
+ sawNetworkLine = true;
1699
+ reconnectAttempts.push(attempt);
1700
+ index += 1;
1701
+ continue;
1702
+ }
1703
+ if (isHttpsFallbackLine(trimmed)) {
1704
+ sawNetworkLine = true;
1705
+ httpsFallbacks += 1;
1706
+ index += 1;
1707
+ continue;
1708
+ }
1709
+ if (sawNetworkLine && (!trimmed || /^context compacted$/i.test(trimmed))) {
1710
+ index += 1;
1711
+ continue;
1712
+ }
1713
+ break;
1714
+ }
1715
+ if (reconnectAttempts.length === 0) {
1716
+ return null;
1717
+ }
1718
+ const nextLine = nextSignificantProcessText(lines, index);
1719
+ if (!nextLine || isTerminalNetworkFailureLine(nextLine)) {
1720
+ return null;
1721
+ }
1722
+ return { reconnectAttempts, httpsFallbacks, nextIndex: index };
1723
+ }
1724
+ function nextSignificantProcessText(lines, startIndex) {
1725
+ for (let index = startIndex; index < lines.length; index += 1) {
1726
+ const trimmed = normalizedProcessLine(lines[index] ?? "").trim();
1727
+ if (trimmed) {
1728
+ return trimmed;
1729
+ }
1730
+ }
1731
+ return null;
1732
+ }
1733
+ function isNetworkRetryDiagnostic(line) {
1734
+ return /codex_core::responses_retry:.*retrying sampling request/i.test(line);
1735
+ }
1736
+ function reconnectAttempt(line) {
1737
+ return line.match(/^(?:ERROR:\s*)?Reconnecting\.\.\.\s+(\d+\/\d+)/i)?.[1] ?? null;
1738
+ }
1739
+ function isHttpsFallbackLine(line) {
1740
+ return /Falling back from WebSockets to HTTPS transport/i.test(line);
1741
+ }
1742
+ function isTerminalNetworkFailureLine(line) {
1743
+ return (/^(?:ERROR|FATAL)\b/i.test(line) ||
1744
+ /\b(?:request|process|router|worker)\b.*\b(?:timed out|timeout|failed)\b/i.test(line));
1745
+ }
1746
+ function formatRecoveredNetworkSummary(reconnectAttempts, httpsFallbacks) {
1747
+ const reconnect = reconnectAttempts.length === 1
1748
+ ? `reconnect ${reconnectAttempts[0] ?? ""}`.trim()
1749
+ : `${reconnectAttempts.length} reconnects`;
1750
+ const fallback = httpsFallbacks === 1
1751
+ ? " via HTTPS fallback"
1752
+ : httpsFallbacks > 1
1753
+ ? ` with ${httpsFallbacks} HTTPS fallbacks`
1754
+ : "";
1755
+ return `Network recovered after ${reconnect}${fallback}`;
1261
1756
  }
1262
1757
  function collectProcessExecCommand(rawLines, startIndex) {
1263
1758
  const line = normalizedProcessLine(rawLines[startIndex] ?? "");
@@ -1269,20 +1764,227 @@ function collectProcessExecCommand(rawLines, startIndex) {
1269
1764
  return {
1270
1765
  command,
1271
1766
  commandLine: `$ ${command}`,
1767
+ cwd: parseShellExecCwd(nextLine),
1272
1768
  nextIndex: startIndex + 2
1273
1769
  };
1274
1770
  }
1275
- return null;
1771
+ return null;
1772
+ }
1773
+ const command = parseShellExecCommand(trimmed);
1774
+ if (!command) {
1775
+ return null;
1776
+ }
1777
+ return {
1778
+ command,
1779
+ commandLine: `$ ${command}`,
1780
+ cwd: parseShellExecCwd(trimmed),
1781
+ nextIndex: startIndex + 1
1782
+ };
1783
+ }
1784
+ function collectParallelReadCommandBatch(rawLines, startIndex, renderedArtifactFiles) {
1785
+ const commands = [];
1786
+ let index = startIndex;
1787
+ while (index < rawLines.length) {
1788
+ const command = collectProcessExecCommand(rawLines, index);
1789
+ if (!command) {
1790
+ break;
1791
+ }
1792
+ commands.push(command);
1793
+ index = command.nextIndex;
1794
+ }
1795
+ if (commands.length < 2 || commands.some((command) => shouldHideProcessCommand(command, renderedArtifactFiles))) {
1796
+ return null;
1797
+ }
1798
+ const targets = [];
1799
+ for (const command of commands) {
1800
+ const readTargets = sedReadTargets(command.command);
1801
+ if (!readTargets) {
1802
+ return null;
1803
+ }
1804
+ targets.push(...readTargets);
1805
+ }
1806
+ let totalLines = 0;
1807
+ let contentLines = 0;
1808
+ for (let commandIndex = 0; commandIndex < commands.length; commandIndex += 1) {
1809
+ const status = normalizedProcessLine(rawLines[index] ?? "").trim();
1810
+ if (!/^succeeded\s+in\s+\d+(?:ms|s|m)?:?$/i.test(status)) {
1811
+ return null;
1812
+ }
1813
+ const outputStart = index + 1;
1814
+ const outputEnd = parallelReadOutputEnd(rawLines, outputStart, commandIndex < commands.length - 1);
1815
+ if (outputEnd === null) {
1816
+ return null;
1817
+ }
1818
+ const outputLines = compactableReadOutputLines(rawLines, outputStart, outputEnd);
1819
+ totalLines += outputLines.length;
1820
+ contentLines += outputLines.filter((line) => normalizedProcessLine(line).trim()).length;
1821
+ index = outputEnd;
1822
+ }
1823
+ if (contentLines < CODE_OUTPUT_COLLAPSE_MIN_LINES) {
1824
+ return null;
1825
+ }
1826
+ return {
1827
+ summary: `Collapsed read summaries: ${targets.length} chunks, ${totalLines} lines (${compactReadSummaryTargets(targets.map(formatReadTarget))})`,
1828
+ nextIndex: index
1829
+ };
1830
+ }
1831
+ function collectParallelHiddenCommandBatch(rawLines, startIndex, renderedArtifactFiles) {
1832
+ const commands = [];
1833
+ let index = startIndex;
1834
+ while (index < rawLines.length) {
1835
+ const command = collectProcessExecCommand(rawLines, index);
1836
+ if (!command) {
1837
+ break;
1838
+ }
1839
+ commands.push(command);
1840
+ index = command.nextIndex;
1841
+ }
1842
+ if (commands.length < 2 || commands.some((command) => !shouldHideProcessCommand(command, renderedArtifactFiles))) {
1843
+ return null;
1844
+ }
1845
+ const output = [];
1846
+ let allSucceeded = true;
1847
+ const includesWorkerLogReadback = commands.some((command) => processCommandReadsWorkerOutputLog(command.command));
1848
+ for (let commandIndex = 0; commandIndex < commands.length; commandIndex += 1) {
1849
+ const status = normalizedProcessLine(rawLines[index] ?? "").trim();
1850
+ if (!isProcessStatusLine(status)) {
1851
+ return null;
1852
+ }
1853
+ allSucceeded &&= /^succeeded\s+in\s+/i.test(status);
1854
+ const outputStart = index + 1;
1855
+ const isLastOutput = commandIndex === commands.length - 1;
1856
+ const outputEnd = includesWorkerLogReadback && isLastOutput
1857
+ ? nestedWorkerLogReadbackEnd(rawLines, outputStart)
1858
+ : parallelReadOutputEnd(rawLines, outputStart, !isLastOutput);
1859
+ if (outputEnd === null) {
1860
+ return null;
1861
+ }
1862
+ output.push(status, ...rawLines.slice(outputStart, outputEnd));
1863
+ index = outputEnd;
1864
+ }
1865
+ return {
1866
+ lines: allSucceeded ? [] : [...commands.map((command) => command.commandLine), ...output],
1867
+ nextIndex: index
1868
+ };
1869
+ }
1870
+ function processCommandReadsWorkerOutputLog(command) {
1871
+ const trimmed = command.trim();
1872
+ return (/^(?:cat|sed|nl|head|tail|bat|less|more)\b/.test(trimmed) &&
1873
+ trimmed.includes(".parallel-codex/sessions/") &&
1874
+ /\/output\.log\b/.test(trimmed));
1875
+ }
1876
+ function nestedWorkerLogReadbackEnd(rawLines, startIndex) {
1877
+ for (let index = startIndex; index < rawLines.length - 1; index += 1) {
1878
+ const marker = normalizedProcessLine(rawLines[index] ?? "").trim();
1879
+ const command = normalizedProcessLine(rawLines[index + 1] ?? "").trim();
1880
+ if (marker === "exec" && parseShellExecCommand(command) !== null) {
1881
+ return index;
1882
+ }
1883
+ }
1884
+ return null;
1885
+ }
1886
+ function parallelReadOutputEnd(rawLines, startIndex, expectsStatus) {
1887
+ for (let index = startIndex; index < rawLines.length; index += 1) {
1888
+ const trimmed = normalizedProcessLine(rawLines[index] ?? "").trim();
1889
+ if (expectsStatus && isProcessStatusLine(trimmed)) {
1890
+ return index;
1891
+ }
1892
+ if (isChainedReadOutputBoundary(trimmed)) {
1893
+ return expectsStatus ? null : index;
1894
+ }
1895
+ }
1896
+ return null;
1897
+ }
1898
+ function collectChainedReadCommandBlock(rawLines, command, statusIndex) {
1899
+ const targets = sedReadTargets(command);
1900
+ if (!targets || targets.length < 2) {
1901
+ return null;
1902
+ }
1903
+ const status = normalizedProcessLine(rawLines[statusIndex] ?? "").trim();
1904
+ if (!/^succeeded\s+in\s+\d+(?:ms|s|m)?:?$/i.test(status)) {
1905
+ return null;
1906
+ }
1907
+ const outputStart = statusIndex + 1;
1908
+ let nextIndex = outputStart;
1909
+ while (nextIndex < rawLines.length) {
1910
+ const trimmed = normalizedProcessLine(rawLines[nextIndex] ?? "").trim();
1911
+ if (isChainedReadOutputBoundary(trimmed)) {
1912
+ break;
1913
+ }
1914
+ nextIndex += 1;
1915
+ }
1916
+ const outputLines = compactableReadOutputLines(rawLines, outputStart, nextIndex);
1917
+ const contentLineCount = outputLines.filter((line) => normalizedProcessLine(line).trim()).length;
1918
+ if (contentLineCount < CODE_OUTPUT_COLLAPSE_MIN_LINES) {
1919
+ return null;
1920
+ }
1921
+ const displayTargets = compactReadSummaryTargets(targets.map(formatReadTarget));
1922
+ return {
1923
+ summary: `Collapsed read summaries: ${targets.length} chunks, ${outputLines.length} lines (${displayTargets})`,
1924
+ nextIndex
1925
+ };
1926
+ }
1927
+ function compactableReadOutputLines(rawLines, startIndex, endIndex) {
1928
+ const outputLines = rawLines
1929
+ .slice(startIndex, endIndex)
1930
+ .filter((line) => {
1931
+ const normalized = normalizedProcessLine(line);
1932
+ return !shouldDropNoisyProcessLine(normalized.trim(), normalized);
1933
+ });
1934
+ while (outputLines.length > 0 && !normalizedProcessLine(outputLines[0] ?? "").trim()) {
1935
+ outputLines.shift();
1936
+ }
1937
+ while (outputLines.length > 0 && !normalizedProcessLine(outputLines[outputLines.length - 1] ?? "").trim()) {
1938
+ outputLines.pop();
1939
+ }
1940
+ return outputLines;
1941
+ }
1942
+ function sedReadTargets(command) {
1943
+ const segments = command.split(/\s+&&\s+/);
1944
+ const targets = [];
1945
+ for (const segment of segments) {
1946
+ const match = segment.trim().match(/^sed\s+-n\s+(\S+)\s+(.+)$/);
1947
+ const range = match ? singleShellTokenValue(match[1] ?? "") : null;
1948
+ const target = match ? singleShellTokenValue(match[2] ?? "") : null;
1949
+ if (!range || !/^\d+,\d+p$/.test(range) || !target) {
1950
+ return null;
1951
+ }
1952
+ targets.push(target);
1276
1953
  }
1277
- const command = parseShellExecCommand(trimmed);
1278
- if (!command) {
1954
+ return targets;
1955
+ }
1956
+ function shouldHideProcessCommand(command, artifactFiles) {
1957
+ return (processCommandReadsRenderedArtifact(command.command, artifactFiles) ||
1958
+ processCommandReadsParallelCodexTaskFile(command.command) ||
1959
+ processCommandInspectsSessionMetadata(command.command) ||
1960
+ processCommandInspectsSessionCwd(command.command, command.cwd) ||
1961
+ processCommandReadsSessionSystemFile(command.command) ||
1962
+ processCommandReadsSkillDocument(command.command));
1963
+ }
1964
+ function singleShellTokenValue(text) {
1965
+ const trimmed = text.trim();
1966
+ if (!trimmed) {
1279
1967
  return null;
1280
1968
  }
1281
- return {
1282
- command,
1283
- commandLine: `$ ${command}`,
1284
- nextIndex: startIndex + 1
1285
- };
1969
+ if (trimmed[0] === "\"" || trimmed[0] === "'") {
1970
+ const segment = readQuotedShellSegment(trimmed, 0);
1971
+ return segment?.endIndex === trimmed.length ? segment.value : null;
1972
+ }
1973
+ return /^[^\s;&|]+$/.test(trimmed) ? trimmed : null;
1974
+ }
1975
+ function isChainedReadOutputBoundary(trimmed) {
1976
+ return (trimmed === "exec" ||
1977
+ trimmed.startsWith("$ ") ||
1978
+ parseShellExecCommand(trimmed) !== null ||
1979
+ isAssistantNarrativeMarker(trimmed) ||
1980
+ /^tokens used$/i.test(trimmed) ||
1981
+ isRolePromptTranscriptStart(trimmed) ||
1982
+ isDiffStartLine(trimmed) ||
1983
+ /^apply patch$/i.test(trimmed) ||
1984
+ /^patch:\s+(?:completed|failed)\b/i.test(trimmed));
1985
+ }
1986
+ function parseShellExecCwd(line) {
1987
+ return line.match(/\s+in\s+(\/.+)$/)?.[1]?.trim() ?? null;
1286
1988
  }
1287
1989
  function processCommandReadsRenderedArtifact(command, artifactFiles) {
1288
1990
  if (artifactFiles.size === 0) {
@@ -1301,19 +2003,47 @@ function processCommandReadsRenderedArtifact(command, artifactFiles) {
1301
2003
  }
1302
2004
  function processCommandReadsParallelCodexTaskFile(command) {
1303
2005
  const trimmed = command.trim();
2006
+ const conditionalTarget = readOnlySessionFileProbeTarget(trimmed);
2007
+ if (conditionalTarget) {
2008
+ return (conditionalTarget.includes(".parallel-codex/sessions/") &&
2009
+ /\.(?:md|json|jsonl|diff|log|toml|txt)$/.test(conditionalTarget));
2010
+ }
1304
2011
  if (!/^(?:cat|sed|nl|head|tail|bat|less|more)\b/.test(trimmed)) {
1305
2012
  return false;
1306
2013
  }
1307
2014
  if (!trimmed.includes(".parallel-codex/sessions/")) {
1308
2015
  return false;
1309
2016
  }
1310
- return /\.(?:md|json|jsonl|diff|toml|txt)\b/.test(trimmed);
2017
+ return /\.(?:md|json|jsonl|diff|log|toml|txt)\b/.test(trimmed);
2018
+ }
2019
+ function readOnlySessionFileProbeTarget(command) {
2020
+ const parts = command.split(/\s*;\s*/);
2021
+ if (parts.length !== 5 || parts[3] !== "else echo MISSING" || parts[4] !== "fi") {
2022
+ return null;
2023
+ }
2024
+ const existsMatch = parts[0]?.match(/^if\s+\[\s+-f\s+(.+)\s+\]$/);
2025
+ const countMatch = parts[1]?.match(/^then\s+wc\s+-c\s+(.+)$/);
2026
+ const existsTarget = existsMatch ? singleShellTokenValue(existsMatch[1] ?? "") : null;
2027
+ const countTarget = countMatch ? singleShellTokenValue(countMatch[1] ?? "") : null;
2028
+ const readTargets = sedReadTargets(parts[2] ?? "");
2029
+ if (!existsTarget || countTarget !== existsTarget || readTargets?.length !== 1 || readTargets[0] !== existsTarget) {
2030
+ return null;
2031
+ }
2032
+ return existsTarget;
1311
2033
  }
1312
2034
  function processCommandInspectsSessionMetadata(command) {
1313
2035
  const trimmed = command.trim();
1314
2036
  return (/^(?:find|ls|wc|stat|du)\b/.test(trimmed) &&
1315
2037
  trimmed.includes(".parallel-codex/sessions/"));
1316
2038
  }
2039
+ function processCommandInspectsSessionCwd(command, cwd) {
2040
+ if (!cwd || !/[\\/]\.parallel-codex[\\/]sessions[\\/]/.test(cwd)) {
2041
+ return false;
2042
+ }
2043
+ const trimmed = command.trim();
2044
+ return (/^(?:rg\s+--files|ls|find|fd|tree|stat|du|wc)\b/.test(trimmed) ||
2045
+ /^pwd\s*&&\s*(?:rg\s+--files|ls|find|fd|tree)\b/.test(trimmed));
2046
+ }
1317
2047
  function processCommandReadsSessionSystemFile(command) {
1318
2048
  const trimmed = command.trim();
1319
2049
  if (!/^(?:cat|sed|nl|head|tail|bat|less|more)\b/.test(trimmed)) {
@@ -1339,6 +2069,7 @@ function skipProcessCommandOutputBlock(rawLines, startIndex) {
1339
2069
  trimmed.startsWith("$ ") ||
1340
2070
  parseShellExecCommand(trimmed) !== null ||
1341
2071
  isAssistantNarrativeMarker(trimmed) ||
2072
+ /^tokens used$/i.test(trimmed) ||
1342
2073
  isDiffStartLine(trimmed)) {
1343
2074
  return index;
1344
2075
  }
@@ -1412,6 +2143,61 @@ function readQuotedShellSegment(text, startIndex) {
1412
2143
  function isAssistantNarrativeMarker(trimmed) {
1413
2144
  return trimmed === "codex" || trimmed === "assistant";
1414
2145
  }
2146
+ function dedupeRepeatedAssistantNarrativeBlocks(rawLines) {
2147
+ const blocks = collectAssistantNarrativeBlocks(rawLines);
2148
+ const lastBlockByKey = new Map();
2149
+ for (const block of blocks) {
2150
+ lastBlockByKey.set(block.key, block);
2151
+ }
2152
+ const removed = new Set();
2153
+ for (const block of blocks) {
2154
+ if (lastBlockByKey.get(block.key) === block) {
2155
+ continue;
2156
+ }
2157
+ for (let index = block.startIndex; index < block.endIndex; index += 1) {
2158
+ removed.add(index);
2159
+ }
2160
+ }
2161
+ return removed.size > 0
2162
+ ? rawLines.filter((_line, index) => !removed.has(index))
2163
+ : rawLines;
2164
+ }
2165
+ function collectAssistantNarrativeBlocks(rawLines) {
2166
+ const blocks = [];
2167
+ for (let index = 0; index < rawLines.length;) {
2168
+ const trimmed = normalizedProcessLine(rawLines[index] ?? "").trim();
2169
+ const marked = isAssistantNarrativeMarker(trimmed);
2170
+ const tokenTrailer = /^tokens used$/i.test(trimmed);
2171
+ if (!marked && !tokenTrailer) {
2172
+ index += 1;
2173
+ continue;
2174
+ }
2175
+ const contentStart = marked ? index + 1 : skipNoisyProcessLine(rawLines, index);
2176
+ const endIndex = skipAssistantNarrativeTextBlock(rawLines, contentStart);
2177
+ const key = assistantNarrativeBlockKey(rawLines, contentStart, endIndex);
2178
+ if (key) {
2179
+ blocks.push({
2180
+ startIndex: marked ? index : contentStart,
2181
+ endIndex,
2182
+ key
2183
+ });
2184
+ }
2185
+ index = Math.max(index + 1, endIndex);
2186
+ }
2187
+ return blocks;
2188
+ }
2189
+ function assistantNarrativeBlockKey(rawLines, startIndex, endIndex) {
2190
+ const lines = rawLines
2191
+ .slice(startIndex, endIndex)
2192
+ .map((line) => normalizedProcessLine(line).trimEnd());
2193
+ while (lines.length > 0 && !lines[0]?.trim()) {
2194
+ lines.shift();
2195
+ }
2196
+ while (lines.length > 0 && !lines[lines.length - 1]?.trim()) {
2197
+ lines.pop();
2198
+ }
2199
+ return lines.join("\n");
2200
+ }
1415
2201
  function skipAssistantNarrativeBlock(rawLines, startIndex) {
1416
2202
  return skipAssistantNarrativeTextBlock(rawLines, startIndex + 1);
1417
2203
  }
@@ -1893,6 +2679,19 @@ function isBuildOutputSummaryLine(line) {
1893
2679
  function isNoMatchesSummaryLine(line) {
1894
2680
  return /^No matches in \d+(?:ms|s|m)?(?:\s+\(\$ .+\))?$/i.test(line);
1895
2681
  }
2682
+ function isNetworkRecoverySummaryLine(line) {
2683
+ return networkRecoverySummaryParts(line) !== null;
2684
+ }
2685
+ function networkRecoverySummaryParts(line) {
2686
+ const match = line.match(/^Network recovered after (reconnect \d+\/\d+|\d+ reconnects?)(?: (via HTTPS fallback|with \d+ HTTPS fallbacks))?$/i);
2687
+ if (!match) {
2688
+ return null;
2689
+ }
2690
+ return {
2691
+ reconnect: match[1] ?? "",
2692
+ fallback: match[2]?.replace(/^via\s+/i, "").replace(/^with\s+/i, "") ?? ""
2693
+ };
2694
+ }
1896
2695
  function isCollapsedOutputSummaryLine(line) {
1897
2696
  return /^Collapsed (?:code|file list|source) output: \d+ (?:lines|paths)$/i.test(line);
1898
2697
  }
@@ -1945,7 +2744,7 @@ function isFileListOutputLine(line) {
1945
2744
  if (pathPattern.test(line)) {
1946
2745
  return true;
1947
2746
  }
1948
- return /^[A-Za-z0-9._@+-]+\.(?:cjs|css|diff|html|js|json|jsonl|lock|md|mjs|toml|ts|tsx|txt|yml|yaml)$/.test(line);
2747
+ return /^[A-Za-z0-9._@+-]+\.(?:cjs|css|diff|html|js|json|jsonl|lock|log|md|mjs|toml|ts|tsx|txt|yml|yaml)$/.test(line);
1949
2748
  }
1950
2749
  function collectSourceOutputRun(lines, startIndex) {
1951
2750
  let index = startIndex;
@@ -2026,7 +2825,7 @@ function isCssLikeProcessLine(line) {
2026
2825
  /^[A-Za-z][A-Za-z0-9-]*;$/.test(line));
2027
2826
  }
2028
2827
  function isJavaScriptLikeProcessLine(line) {
2029
- return (/^(?:const|let|var|import|export|await|return|if|else|for|while|switch|try|catch|finally|function|class|new)\b/.test(line) ||
2828
+ return (/^(?:const|let|var|import|export|await|return|delete|throw|yield|break|continue|if|else|for|while|switch|try|catch|finally|function|class|new)\b/.test(line) ||
2030
2829
  /^\}\s*else\b/.test(line) ||
2031
2830
  /^\.[A-Za-z_$][\w$]*(?:\s*\(|\.)/.test(line) ||
2032
2831
  /^[A-Za-z_$][\w$.[\]'"]*\s*(?:[+\-*/%]?=|\(|\.)/.test(line) ||
@@ -2054,6 +2853,18 @@ function skipNoisyProcessLine(rawLines, startIndex) {
2054
2853
  }
2055
2854
  return index;
2056
2855
  }
2856
+ if (isCodexPluginManifestNoise(trimmed)) {
2857
+ const next = normalizedProcessLine(rawLines[startIndex + 1] ?? "").trim();
2858
+ return /^supported path=.*(?:\.codex-plugin\/plugin\.json|\/plugin\.json)$/i.test(next)
2859
+ ? startIndex + 2
2860
+ : startIndex + 1;
2861
+ }
2862
+ if (isCodexTelemetryNoise(trimmed)) {
2863
+ const next = normalizedProcessLine(rawLines[startIndex + 1] ?? "").trim();
2864
+ return /^failed:\s+tag value contains invalid characters:/i.test(next)
2865
+ ? startIndex + 2
2866
+ : startIndex + 1;
2867
+ }
2057
2868
  return startIndex + 1;
2058
2869
  }
2059
2870
  function shouldDropNoisyProcessLine(trimmed, line) {
@@ -2061,6 +2872,10 @@ function shouldDropNoisyProcessLine(trimmed, line) {
2061
2872
  return false;
2062
2873
  }
2063
2874
  return (/^tokens used$/i.test(trimmed) ||
2875
+ /^apply patch$/i.test(trimmed) ||
2876
+ /^patch:\s+completed$/i.test(trimmed) ||
2877
+ isCodexPluginManifestNoise(trimmed) ||
2878
+ isCodexTelemetryNoise(trimmed) ||
2064
2879
  trimmed.includes("codex_models_manager::manager: failed to refresh available models") ||
2065
2880
  (trimmed.includes("failed to decode models response") && trimmed.includes("body: {\"data\"")) ||
2066
2881
  trimmed.startsWith("body: {\"data\"") ||
@@ -2070,6 +2885,12 @@ function shouldDropNoisyProcessLine(trimmed, line) {
2070
2885
  /^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]$/.test(trimmed) ||
2071
2886
  line === "--------");
2072
2887
  }
2888
+ function isCodexPluginManifestNoise(trimmed) {
2889
+ return /codex_?core_?plugins::manifest: ignoring interface\.defaultPrompt/.test(trimmed);
2890
+ }
2891
+ function isCodexTelemetryNoise(trimmed) {
2892
+ return trimmed.includes("codex_otel::events::session_telemetry: metrics counter");
2893
+ }
2073
2894
  function isNpmLifecycleEchoStart(trimmed) {
2074
2895
  return /^>\s+(?:@[^/\s]+\/)?[^@\s]+@\S+\s+\S+/.test(trimmed);
2075
2896
  }
@@ -2240,6 +3061,7 @@ function collectEmbeddedDiffBlock(rawLines, startIndex) {
2240
3061
  return { lines, nextIndex: index };
2241
3062
  }
2242
3063
  function collectBareDiffHunkBlock(rawLines, startIndex) {
3064
+ const lines = [];
2243
3065
  let index = startIndex;
2244
3066
  let insideHunk = false;
2245
3067
  let hunkCounts = null;
@@ -2249,6 +3071,7 @@ function collectBareDiffHunkBlock(rawLines, startIndex) {
2249
3071
  if (isDiffHunkLine(trimmedStart)) {
2250
3072
  insideHunk = true;
2251
3073
  hunkCounts = parseHunkCounts(trimmedStart);
3074
+ lines.push(trimmedStart);
2252
3075
  index += 1;
2253
3076
  continue;
2254
3077
  }
@@ -2256,17 +3079,63 @@ function collectBareDiffHunkBlock(rawLines, startIndex) {
2256
3079
  if (hunkCounts && hunkCounts.oldRemaining <= 0 && hunkCounts.newRemaining <= 0) {
2257
3080
  break;
2258
3081
  }
3082
+ lines.push(line);
2259
3083
  hunkCounts = consumeHunkLine(line, hunkCounts);
2260
3084
  index += 1;
2261
3085
  continue;
2262
3086
  }
2263
3087
  if (insideHunk && isDiffMetaLine(trimmedStart)) {
3088
+ lines.push(trimmedStart);
2264
3089
  index += 1;
2265
3090
  continue;
2266
3091
  }
2267
3092
  break;
2268
3093
  }
2269
- return { nextIndex: index };
3094
+ return { lines, nextIndex: index };
3095
+ }
3096
+ function inferBareDiffFile(rawLines, startIndex) {
3097
+ let newPath = null;
3098
+ let oldPath = null;
3099
+ let inspected = 0;
3100
+ for (let index = startIndex - 1; index >= 0 && inspected < 8; index -= 1) {
3101
+ const trimmed = normalizedProcessLine(rawLines[index] ?? "").trim();
3102
+ if (!trimmed) {
3103
+ continue;
3104
+ }
3105
+ inspected += 1;
3106
+ const nextFile = unifiedDiffHeaderPath(trimmed, "+++");
3107
+ if (nextFile !== null) {
3108
+ newPath = nextFile;
3109
+ continue;
3110
+ }
3111
+ const previousFile = unifiedDiffHeaderPath(trimmed, "---");
3112
+ if (previousFile !== null) {
3113
+ oldPath = previousFile;
3114
+ const selected = newPath && newPath !== "/dev/null" ? newPath : oldPath;
3115
+ return selected && selected !== "/dev/null" ? formatDiffDisplayPath(selected) : null;
3116
+ }
3117
+ if (!newPath && looksLikeStandaloneDiffPath(trimmed)) {
3118
+ return formatDiffDisplayPath(trimmed);
3119
+ }
3120
+ if (parseHunkStart(trimmed) || isDiffStartLine(trimmed) || isHardProcessBoundary(trimmed)) {
3121
+ break;
3122
+ }
3123
+ }
3124
+ const selected = newPath && newPath !== "/dev/null" ? newPath : oldPath;
3125
+ return selected && selected !== "/dev/null" ? formatDiffDisplayPath(selected) : null;
3126
+ }
3127
+ function unifiedDiffHeaderPath(line, marker) {
3128
+ if (!line.startsWith(`${marker} `) && !line.startsWith(`${marker}\t`)) {
3129
+ return null;
3130
+ }
3131
+ const value = line.slice(marker.length).trimStart().split("\t", 1)[0]?.trim() ?? "";
3132
+ return value.replace(/^[ab]\//, "");
3133
+ }
3134
+ function looksLikeStandaloneDiffPath(line) {
3135
+ return !/\s/.test(line)
3136
+ && /(?:^|\/)[A-Za-z0-9._@+-]+\.[A-Za-z0-9._@+-]+$/.test(line)
3137
+ && !line.startsWith("+")
3138
+ && !line.startsWith("-");
2270
3139
  }
2271
3140
  function collectBareProcessDiffBodyRun(rawLines, startIndex) {
2272
3141
  let index = startIndex;
@@ -2314,7 +3183,7 @@ function nextSignificantProcessLineStartsDiff(rawLines, startIndex) {
2314
3183
  isCollapsedOutputSummaryLine(trimmed)) {
2315
3184
  continue;
2316
3185
  }
2317
- return isDiffStartLine(trimmed);
3186
+ return isDiffStartLine(trimmed) || parseHunkStart(trimmed) !== null;
2318
3187
  }
2319
3188
  return false;
2320
3189
  }
@@ -2426,6 +3295,58 @@ function skipProcessDiffTailContext(rawLines, startIndex) {
2426
3295
  }
2427
3296
  return index;
2428
3297
  }
3298
+ function skipBareDiffTailContext(rawLines, startIndex) {
3299
+ let index = startIndex;
3300
+ let skippedTail = false;
3301
+ while (index < rawLines.length) {
3302
+ const line = normalizedProcessLine(rawLines[index] ?? "");
3303
+ const trimmed = line.trim();
3304
+ if (!trimmed) {
3305
+ if (skippedTail || nextLineLooksLikeBareDiffTail(rawLines, index + 1)) {
3306
+ index += 1;
3307
+ continue;
3308
+ }
3309
+ return index;
3310
+ }
3311
+ if (isHardProcessBoundary(trimmed)
3312
+ || isDiffStartLine(trimmed)
3313
+ || parseHunkStart(trimmed)
3314
+ || isAssistantNarrativeMarker(trimmed)
3315
+ || isRolePromptTranscriptStart(trimmed)) {
3316
+ return index;
3317
+ }
3318
+ if (isCollapsedOutputSummaryLine(trimmed)
3319
+ || isProcessCodeOutputLine(trimmed)
3320
+ || isBareProcessDiffBodyLine(line)
3321
+ || isIndentedBareDiffTailLine(line)) {
3322
+ skippedTail = true;
3323
+ index += 1;
3324
+ continue;
3325
+ }
3326
+ return index;
3327
+ }
3328
+ return index;
3329
+ }
3330
+ function nextLineLooksLikeBareDiffTail(rawLines, startIndex) {
3331
+ for (let index = startIndex; index < rawLines.length; index += 1) {
3332
+ const line = normalizedProcessLine(rawLines[index] ?? "");
3333
+ const trimmed = line.trim();
3334
+ if (!trimmed) {
3335
+ continue;
3336
+ }
3337
+ return (isCollapsedOutputSummaryLine(trimmed)
3338
+ || isProcessCodeOutputLine(trimmed)
3339
+ || isBareProcessDiffBodyLine(line)
3340
+ || isIndentedBareDiffTailLine(line));
3341
+ }
3342
+ return false;
3343
+ }
3344
+ function isIndentedBareDiffTailLine(line) {
3345
+ const trimmedStart = line.trimStart();
3346
+ return !trimmedStart.startsWith("+++")
3347
+ && !trimmedStart.startsWith("---")
3348
+ && /^[+-]\s{2,}\S/.test(trimmedStart);
3349
+ }
2429
3350
  function nextLineLooksLikeProcessDiffTail(rawLines, startIndex) {
2430
3351
  for (let index = startIndex; index < rawLines.length; index += 1) {
2431
3352
  const line = normalizedProcessLine(rawLines[index] ?? "");
@@ -2470,7 +3391,7 @@ function renderDiffContent(text) {
2470
3391
  const fileTitle = renderDiffFileTitle(trimmedStart);
2471
3392
  if (fileTitle) {
2472
3393
  flushCurrentFile();
2473
- currentFile = { title: fileTitle, added: 0, removed: 0, lines: [] };
3394
+ currentFile = { title: fileTitle, added: 0, removed: 0, hunks: 0, lines: [] };
2474
3395
  oldLineNumber = 0;
2475
3396
  newLineNumber = 0;
2476
3397
  insideHunk = false;
@@ -2478,6 +3399,12 @@ function renderDiffContent(text) {
2478
3399
  }
2479
3400
  const hunkStart = parseHunkStart(trimmedStart);
2480
3401
  if (hunkStart) {
3402
+ if (currentFile) {
3403
+ if (currentFile.hunks > 0) {
3404
+ currentFile.lines.push({ kind: "diff-hunk", text: trimmedStart });
3405
+ }
3406
+ currentFile.hunks += 1;
3407
+ }
2481
3408
  oldLineNumber = hunkStart.oldStart;
2482
3409
  newLineNumber = hunkStart.newStart;
2483
3410
  insideHunk = true;
@@ -2522,6 +3449,65 @@ function renderDiffContent(text) {
2522
3449
  flushCurrentFile();
2523
3450
  return rendered;
2524
3451
  }
3452
+ function renderBareDiffHunkContent(lines, diffFile = null) {
3453
+ const hunks = [];
3454
+ let currentHunk = [];
3455
+ let oldLineNumber = 0;
3456
+ let newLineNumber = 0;
3457
+ let insideHunk = false;
3458
+ const flushHunk = () => {
3459
+ if (currentHunk.some((line) => line.kind === "diff-add" || line.kind === "diff-remove")) {
3460
+ hunks.push(currentHunk);
3461
+ }
3462
+ currentHunk = [];
3463
+ };
3464
+ for (const rawLine of lines) {
3465
+ const line = stripAnsiForDiff(rawLine);
3466
+ const trimmedStart = line.trimStart();
3467
+ const hunkStart = parseHunkStart(trimmedStart);
3468
+ if (hunkStart) {
3469
+ flushHunk();
3470
+ currentHunk.push({
3471
+ kind: "diff-hunk",
3472
+ text: trimmedStart,
3473
+ ...(diffFile ? { diffFile } : {})
3474
+ });
3475
+ oldLineNumber = hunkStart.oldStart;
3476
+ newLineNumber = hunkStart.newStart;
3477
+ insideHunk = true;
3478
+ continue;
3479
+ }
3480
+ if (!insideHunk) {
3481
+ continue;
3482
+ }
3483
+ if (line.startsWith("+")) {
3484
+ currentHunk.push({
3485
+ kind: "diff-add",
3486
+ text: formatDiffCodeLine(newLineNumber, "+", line.slice(1))
3487
+ });
3488
+ newLineNumber += 1;
3489
+ continue;
3490
+ }
3491
+ if (line.startsWith("-")) {
3492
+ currentHunk.push({
3493
+ kind: "diff-remove",
3494
+ text: formatDiffCodeLine(oldLineNumber, "-", line.slice(1))
3495
+ });
3496
+ oldLineNumber += 1;
3497
+ continue;
3498
+ }
3499
+ if (line.startsWith(" ")) {
3500
+ currentHunk.push({
3501
+ kind: "diff-context",
3502
+ text: formatDiffCodeLine(newLineNumber, " ", line.slice(1))
3503
+ });
3504
+ oldLineNumber += 1;
3505
+ newLineNumber += 1;
3506
+ }
3507
+ }
3508
+ flushHunk();
3509
+ return hunks.flat();
3510
+ }
2525
3511
  function renderDiffFileLine(line) {
2526
3512
  const title = renderDiffFileTitle(line);
2527
3513
  if (!title) {
@@ -2678,6 +3664,9 @@ function classifyRenderedLine(section, line) {
2678
3664
  if (isNoMatchesSummaryLine(trimmed)) {
2679
3665
  return { kind: "summary", text: trimmed };
2680
3666
  }
3667
+ if (isNetworkRecoverySummaryLine(trimmed)) {
3668
+ return { kind: "summary", text: trimmed };
3669
+ }
2681
3670
  if ((isProcessSection || isExplicitErrorLine(trimmed)) &&
2682
3671
  isErrorProcessLine(trimmed) &&
2683
3672
  !isMarkdownListLikeLine(trimmed)) {
@@ -2833,16 +3822,70 @@ function isMarkdownTableSeparator(line) {
2833
3822
  function isMarkdownTableRow(line) {
2834
3823
  return line.startsWith("|") && line.endsWith("|") && line.split("|").length > 2;
2835
3824
  }
2836
- function renderMarkdownTableRow(line) {
3825
+ function collectMarkdownTableBlock(rawLines, startIndex) {
3826
+ const rows = [];
3827
+ let index = startIndex;
3828
+ let consumed = false;
3829
+ while (index < rawLines.length) {
3830
+ const line = stripAnsi(rawLines[index] ?? "").replace(/\r/g, "").trim();
3831
+ if (!line) {
3832
+ break;
3833
+ }
3834
+ if (isMarkdownTableSeparator(line)) {
3835
+ consumed = true;
3836
+ index += 1;
3837
+ continue;
3838
+ }
3839
+ if (!isMarkdownTableRow(line)) {
3840
+ break;
3841
+ }
3842
+ const row = parseMarkdownTableRow(line);
3843
+ if (row.length > 0) {
3844
+ rows.push(row);
3845
+ }
3846
+ consumed = true;
3847
+ index += 1;
3848
+ }
3849
+ if (!consumed) {
3850
+ return null;
3851
+ }
3852
+ return {
3853
+ lines: renderMarkdownTableRows(rows),
3854
+ nextIndex: index
3855
+ };
3856
+ }
3857
+ function parseMarkdownTableRow(line) {
2837
3858
  return line
2838
3859
  .split("|")
2839
3860
  .map((cell) => stripInlineMarkdown(cell.trim()))
2840
- .filter(Boolean)
2841
- .join(" ");
3861
+ .filter(Boolean);
3862
+ }
3863
+ function renderMarkdownTableRows(rows) {
3864
+ if (rows.length === 0) {
3865
+ return [];
3866
+ }
3867
+ const columnWidths = markdownTableColumnWidths(rows);
3868
+ return rows.map((row) => ({
3869
+ kind: "table",
3870
+ text: row
3871
+ .map((cell, index) => index === row.length - 1
3872
+ ? cell
3873
+ : `${cell}${" ".repeat(Math.max(0, (columnWidths[index] ?? 0) - displayWidth(cell)))}`)
3874
+ .join(" ")
3875
+ }));
3876
+ }
3877
+ function markdownTableColumnWidths(rows) {
3878
+ const widths = [];
3879
+ for (const row of rows) {
3880
+ row.forEach((cell, index) => {
3881
+ widths[index] = Math.max(widths[index] ?? 0, displayWidth(cell));
3882
+ });
3883
+ }
3884
+ return widths;
2842
3885
  }
2843
3886
  function stripInlineMarkdown(text) {
2844
3887
  return text
2845
- .replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 <$2>")
3888
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, label, target) => formatMarkdownLink(label, target))
2846
3889
  .replace(/`([^`]+)`/g, "$1")
2847
3890
  .replace(/\*\*([^*]+)\*\*/g, "$1")
2848
3891
  .replace(/__([^_]+)__/g, "$1")
@@ -2850,19 +3893,25 @@ function stripInlineMarkdown(text) {
2850
3893
  .replace(/_([^_]+)_/g, "$1")
2851
3894
  .trim();
2852
3895
  }
2853
- function decodeHtmlEntities(text) {
3896
+ function formatMarkdownLink(label, target) {
3897
+ const text = stripInlineMarkdownLinkText(label);
3898
+ const href = target.trim();
3899
+ if (!href) {
3900
+ return text;
3901
+ }
3902
+ return isExternalMarkdownLinkTarget(href) ? `${text} <${href}>` : text;
3903
+ }
3904
+ function stripInlineMarkdownLinkText(text) {
2854
3905
  return text
2855
- .replace(/&larr;/g, "")
2856
- .replace(/&rarr;/g, "")
2857
- .replace(/&uarr;/g, "")
2858
- .replace(/&darr;/g, "")
2859
- .replace(/&middot;/g, "·")
2860
- .replace(/&nbsp;/g, " ")
2861
- .replace(/&amp;/g, "&")
2862
- .replace(/&lt;/g, "<")
2863
- .replace(/&gt;/g, ">")
2864
- .replace(/&quot;/g, "\"")
2865
- .replace(/&#39;/g, "'");
3906
+ .replace(/`([^`]+)`/g, "$1")
3907
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
3908
+ .replace(/__([^_]+)__/g, "$1")
3909
+ .replace(/\*([^*]+)\*/g, "$1")
3910
+ .replace(/_([^_]+)_/g, "$1")
3911
+ .trim();
3912
+ }
3913
+ function isExternalMarkdownLinkTarget(target) {
3914
+ return /^(?:https?:|mailto:)/i.test(target);
2866
3915
  }
2867
3916
  function renderJsonLine(line) {
2868
3917
  try {
@@ -2873,7 +3922,7 @@ function renderJsonLine(line) {
2873
3922
  kind: "json",
2874
3923
  text: summary.meta
2875
3924
  },
2876
- ...(summary.message ? [{ kind: "json-message", text: summary.message }] : [])
3925
+ ...summary.messages.map((message) => ({ kind: "json-message", text: message }))
2877
3926
  ];
2878
3927
  }
2879
3928
  catch {
@@ -2881,26 +3930,86 @@ function renderJsonLine(line) {
2881
3930
  }
2882
3931
  }
2883
3932
  function summarizeJsonRecord(value) {
2884
- const severity = stringField(value, "severity");
2885
- const id = stringField(value, "id");
2886
- const target = stringField(value, "to") || stringField(value, "from");
3933
+ const state = stringField(value, "status") || stringField(value, "severity");
3934
+ const id = stringField(value, "id") || stringField(value, "finding_id") || stringField(value, "findingId");
3935
+ const to = stringField(value, "to");
3936
+ const from = stringField(value, "from");
3937
+ const direction = from && to ? `${from} -> ${to}` : to ? `to ${to}` : from ? `from ${from}` : "";
2887
3938
  const file = stringField(value, "file") || stringField(value, "path");
2888
- const message = stringField(value, "message") || stringField(value, "summary") || JSON.stringify(value);
2889
- const meta = [
2890
- severity ? `[${severity}]` : "",
2891
- id,
2892
- target ? `to ${target}` : "",
2893
- file
2894
- ].filter(Boolean).join(" ");
3939
+ const line = numberLikeField(value, "line");
3940
+ const column = numberLikeField(value, "column") || numberLikeField(value, "col");
3941
+ const location = formatJsonRecordLocation(file, line, column);
3942
+ const messages = jsonRecordMessages(value);
3943
+ const marker = state ? `[${state}]` : "";
3944
+ const lead = [marker, id].filter(Boolean).join(" ");
3945
+ const details = [direction, location].filter(Boolean);
3946
+ const meta = id
3947
+ ? [lead, ...details].filter(Boolean).join(" · ")
3948
+ : [lead, ...details].filter(Boolean).join(" ");
2895
3949
  return {
2896
3950
  meta: meta || "json",
2897
- message
3951
+ messages: messages.length > 0 ? messages : [JSON.stringify(value)]
2898
3952
  };
2899
3953
  }
2900
3954
  function stringField(value, key) {
2901
3955
  const field = value[key];
2902
3956
  return typeof field === "string" ? field.trim() : "";
2903
3957
  }
3958
+ function jsonRecordMessages(value) {
3959
+ const primary = stringField(value, "message") || stringField(value, "summary");
3960
+ if (primary) {
3961
+ return [primary];
3962
+ }
3963
+ return [
3964
+ ...textFragmentsField(value, "title"),
3965
+ ...textFragmentsField(value, "detail").map((text) => `detail · ${text}`),
3966
+ ...textFragmentsField(value, "details").map((text) => `detail · ${text}`),
3967
+ ...textFragmentsField(value, "recommendation").map((text) => `fix · ${text}`)
3968
+ ].filter(uniqueTextFragmentFilter());
3969
+ }
3970
+ function textFragmentsField(value, key) {
3971
+ return textFragmentsValue(value[key]);
3972
+ }
3973
+ function textFragmentsValue(value) {
3974
+ if (typeof value === "string") {
3975
+ return value.trim() ? [value.trim()] : [];
3976
+ }
3977
+ if (Array.isArray(value)) {
3978
+ return value.flatMap((item) => textFragmentsValue(item));
3979
+ }
3980
+ return [];
3981
+ }
3982
+ function uniqueTextFragmentFilter() {
3983
+ const seen = new Set();
3984
+ return (value) => {
3985
+ const normalized = value.toLowerCase();
3986
+ if (!value || seen.has(normalized)) {
3987
+ return false;
3988
+ }
3989
+ seen.add(normalized);
3990
+ return true;
3991
+ };
3992
+ }
3993
+ function numberLikeField(value, key) {
3994
+ const field = value[key];
3995
+ if (typeof field === "number" && Number.isFinite(field)) {
3996
+ return String(field);
3997
+ }
3998
+ if (typeof field !== "string") {
3999
+ return "";
4000
+ }
4001
+ const trimmed = field.trim();
4002
+ return /^\d+$/.test(trimmed) ? trimmed : "";
4003
+ }
4004
+ function formatJsonRecordLocation(file, line, column) {
4005
+ if (!file) {
4006
+ return "";
4007
+ }
4008
+ if (!line) {
4009
+ return file;
4010
+ }
4011
+ return column ? `${file}:${line}:${column}` : `${file}:${line}`;
4012
+ }
2904
4013
  function stripAnsi(value) {
2905
4014
  return value.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "");
2906
4015
  }
@@ -2918,72 +4027,91 @@ function groupTitle(group) {
2918
4027
  }
2919
4028
  export function workerOutputLineTheme(kind) {
2920
4029
  if (kind === "group") {
2921
- return { backgroundColor: "ansi256(24)", bold: true, color: "white" };
4030
+ return { backgroundColor: TUI_THEME.chrome, bold: true, color: TUI_THEME.text };
2922
4031
  }
2923
4032
  if (kind === "section") {
2924
- return { backgroundColor: "ansi256(236)", color: "yellow" };
4033
+ return { backgroundColor: TUI_THEME.rail, color: TUI_THEME.warning };
4034
+ }
4035
+ if (kind === "content" || kind === "list" || kind === "list-detail" || kind === "ordered-list" || kind === "task") {
4036
+ return { backgroundColor: TUI_THEME.surface, color: TUI_THEME.text };
4037
+ }
4038
+ if (kind === "placeholder") {
4039
+ return workerOutputEmptyFallbackTheme();
2925
4040
  }
2926
4041
  if (kind === "heading") {
2927
- return { backgroundColor: "ansi256(235)", bold: true, color: "white" };
4042
+ return { backgroundColor: TUI_THEME.surface, bold: true, color: TUI_THEME.text };
2928
4043
  }
2929
4044
  if (kind === "quote") {
2930
- return { backgroundColor: "ansi256(235)", color: "gray" };
4045
+ return { backgroundColor: TUI_THEME.surface, color: TUI_THEME.muted };
2931
4046
  }
2932
4047
  if (kind === "table") {
2933
- return { backgroundColor: "ansi256(236)", color: "white" };
4048
+ return { backgroundColor: TUI_THEME.rail, color: TUI_THEME.text };
2934
4049
  }
2935
4050
  if (kind === "rule") {
2936
- return { color: "gray", dimColor: true };
4051
+ return { backgroundColor: TUI_THEME.surface, color: TUI_THEME.muted };
4052
+ }
4053
+ if (kind === "blank") {
4054
+ return { backgroundColor: TUI_THEME.surface };
2937
4055
  }
2938
4056
  if (kind === "code") {
2939
- return { backgroundColor: "ansi256(236)", color: "gray" };
4057
+ return { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted };
2940
4058
  }
2941
4059
  if (kind === "source-line") {
2942
- return { backgroundColor: "ansi256(235)", color: "white" };
4060
+ return { backgroundColor: TUI_THEME.surface, color: TUI_THEME.text };
2943
4061
  }
2944
4062
  if (kind === "summary") {
2945
- return { backgroundColor: "ansi256(236)", color: "gray" };
4063
+ return { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted };
2946
4064
  }
2947
4065
  if (kind === "json") {
2948
- return { backgroundColor: "ansi256(53)", color: "magenta" };
4066
+ return { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent };
2949
4067
  }
2950
4068
  if (kind === "json-message") {
2951
- return { backgroundColor: "ansi256(235)" };
4069
+ return { backgroundColor: TUI_THEME.surface };
2952
4070
  }
2953
4071
  if (kind === "command") {
2954
- return { backgroundColor: "ansi256(17)", color: "cyan" };
4072
+ return { backgroundColor: TUI_THEME.chrome, color: TUI_THEME.accent };
2955
4073
  }
2956
4074
  if (kind === "success") {
2957
- return { backgroundColor: "ansi256(22)", color: "green" };
4075
+ return { backgroundColor: TUI_THEME.successSurface, color: TUI_THEME.success };
2958
4076
  }
2959
4077
  if (kind === "error") {
2960
- return { backgroundColor: "ansi256(52)", color: "red" };
4078
+ return { backgroundColor: TUI_THEME.dangerSurface, color: TUI_THEME.danger };
2961
4079
  }
2962
4080
  if (kind === "diff-file") {
2963
- return { bold: true, color: "cyan" };
4081
+ return { backgroundColor: TUI_THEME.surface, bold: true, color: TUI_THEME.accent };
2964
4082
  }
2965
4083
  if (kind === "diff-summary") {
2966
- return { color: "white" };
4084
+ return { backgroundColor: TUI_THEME.surface, color: TUI_THEME.text };
2967
4085
  }
2968
4086
  if (kind === "diff-hunk" || kind === "diff-meta") {
2969
- return { backgroundColor: "ansi256(236)", color: "gray" };
4087
+ return { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted };
2970
4088
  }
2971
4089
  if (kind === "diff-add") {
2972
- return { backgroundColor: "ansi256(22)", color: "green" };
4090
+ return { backgroundColor: TUI_THEME.successSurface, color: TUI_THEME.success };
2973
4091
  }
2974
4092
  if (kind === "diff-remove") {
2975
- return { backgroundColor: "ansi256(52)", color: "red" };
4093
+ return { backgroundColor: TUI_THEME.dangerSurface, color: TUI_THEME.danger };
2976
4094
  }
2977
4095
  if (kind === "diff-context") {
2978
- return { color: "gray" };
4096
+ return { backgroundColor: TUI_THEME.surface, color: TUI_THEME.muted };
2979
4097
  }
2980
4098
  return {};
2981
4099
  }
4100
+ export function workerOutputEmptyFallbackTheme() {
4101
+ return {
4102
+ backgroundColor: TUI_THEME.surface,
4103
+ color: TUI_THEME.muted
4104
+ };
4105
+ }
4106
+ export function workerOutputLineFillTheme(kind) {
4107
+ const backgroundColor = workerOutputLineTheme(kind).backgroundColor;
4108
+ return backgroundColor ?? null;
4109
+ }
2982
4110
  export function workerOutputLineLayout(kind, text) {
2983
4111
  if (kind === "section") {
2984
- return { gutter: "", body: `file · ${formatWorkerSectionTitle(text)}` };
4112
+ return { gutter: "", body: joinWorkerOutputChromeParts([workerSectionLabel(text), formatWorkerSectionTitle(text)]) };
2985
4113
  }
2986
- if (kind === "content") {
4114
+ if (kind === "content" || kind === "placeholder") {
2987
4115
  return { gutter: "", body: formatPlainDisplayText(text) };
2988
4116
  }
2989
4117
  if (kind === "heading") {
@@ -2999,7 +4127,7 @@ export function workerOutputLineLayout(kind, text) {
2999
4127
  return { gutter: "", body: text };
3000
4128
  }
3001
4129
  if (kind === "quote") {
3002
- return { gutter: "", body: `> ${text}` };
4130
+ return { gutter: "", body: `│ ${text}` };
3003
4131
  }
3004
4132
  if (kind === "table" || kind === "rule") {
3005
4133
  return { gutter: "", body: text };
@@ -3035,7 +4163,7 @@ export function workerOutputLineLayout(kind, text) {
3035
4163
  return { gutter: "", body: `└ ${text}` };
3036
4164
  }
3037
4165
  if (kind === "diff-hunk") {
3038
- return { gutter: "", body: `hunk ${text}` };
4166
+ return { gutter: "", body: text };
3039
4167
  }
3040
4168
  if (kind === "diff-context") {
3041
4169
  return { gutter: "", body: text };
@@ -3052,9 +4180,66 @@ export function workerOutputLineLayout(kind, text) {
3052
4180
  return { gutter: "", body: text };
3053
4181
  }
3054
4182
  function formatWorkerSectionTitle(title) {
4183
+ const featureMatch = title.match(/^features\/([^/]+)\/([^/]+)$/);
4184
+ const turn = featureMatch?.[1] ?? "";
4185
+ const file = (featureMatch?.[2] ?? basename(title)).toLowerCase();
4186
+ if (turn && isTurnScopedWorkerArtifact(file)) {
4187
+ return turn;
4188
+ }
4189
+ if (file === "requirements.md" || file === "plan.md" || file === "acceptance.md" || file === "worklog.md") {
4190
+ return "";
4191
+ }
4192
+ if (file === "patch.diff") {
4193
+ return "patch";
4194
+ }
3055
4195
  return title.replace(/^features\//, "");
3056
4196
  }
4197
+ function workerSectionLabel(title) {
4198
+ const file = basename(title).toLowerCase();
4199
+ if (file === "requirements.md") {
4200
+ return "requirements";
4201
+ }
4202
+ if (file === "plan.md") {
4203
+ return "plan";
4204
+ }
4205
+ if (file === "acceptance.md") {
4206
+ return "acceptance";
4207
+ }
4208
+ if (file === "actor-worklog.md" || file === "worklog.md") {
4209
+ return "worklog";
4210
+ }
4211
+ if (file === "decisions.md") {
4212
+ return "decision";
4213
+ }
4214
+ if (file.endsWith(".diff")) {
4215
+ return "diff";
4216
+ }
4217
+ if (file === "actor-replies.jsonl") {
4218
+ return "mail";
4219
+ }
4220
+ if (file === "critic-findings.jsonl") {
4221
+ return "findings";
4222
+ }
4223
+ if (file.endsWith(".jsonl")) {
4224
+ return "jsonl";
4225
+ }
4226
+ return "file";
4227
+ }
4228
+ function isTurnScopedWorkerArtifact(file) {
4229
+ return file === "actor-worklog.md" ||
4230
+ file === "decisions.md" ||
4231
+ file === "actor-replies.jsonl" ||
4232
+ file === "critic-findings.jsonl";
4233
+ }
3057
4234
  function formatSummaryDisplayText(text) {
4235
+ const networkRecovery = networkRecoverySummaryParts(text);
4236
+ if (networkRecovery) {
4237
+ return [
4238
+ "network recovered",
4239
+ networkRecovery.reconnect,
4240
+ networkRecovery.fallback
4241
+ ].filter(Boolean).join(" · ");
4242
+ }
3058
4243
  const readRun = text.match(/^Collapsed read summaries: (\d+) chunks, (\d+) lines(?: \((.+)\))?$/i);
3059
4244
  if (readRun) {
3060
4245
  return [
@@ -3275,41 +4460,76 @@ function formatFileUrlDisplay(url) {
3275
4460
  const segments = path.split("/").filter(Boolean);
3276
4461
  return segments.slice(-2).join("/") || url;
3277
4462
  }
3278
- function WorkerOutputLine({ line }) {
4463
+ function WorkerOutputLine({ line, selected, width }) {
3279
4464
  const theme = workerOutputLineTheme(line.kind);
4465
+ const fillBackground = workerOutputLineFillTheme(line.kind);
3280
4466
  if (line.kind === "blank") {
3281
- return _jsx(Text, { children: " " });
4467
+ return _jsx(WorkerOutputBlankLine, { width: width });
3282
4468
  }
3283
4469
  if (line.kind === "group") {
3284
- return (_jsx(Box, { children: _jsxs(Text, { ...theme, children: [" ", line.text, " "] }) }));
4470
+ const body = `${selected ? ">" : " "}${line.text} `;
4471
+ return (_jsxs(Box, { children: [_jsx(Text, { ...theme, children: body }), _jsx(WorkerOutputTrailingFill, { backgroundColor: fillBackground, width: width, usedWidth: displayWidth(body) })] }));
3285
4472
  }
3286
4473
  if (line.preformatted) {
3287
- return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { ...theme, wrap: "truncate-end", children: line.text || " " })] }));
4474
+ const body = line.text || " ";
4475
+ return (_jsxs(Box, { children: [_jsx(WorkerOutputIndent, { backgroundColor: fillBackground, selected: selected }), _jsx(Text, { ...theme, wrap: "truncate-end", children: body }), _jsx(WorkerOutputTrailingFill, { backgroundColor: fillBackground, width: width, usedWidth: 2 + displayWidth(body) })] }));
3288
4476
  }
3289
4477
  const layout = workerOutputLineLayout(line.kind, line.text);
3290
4478
  if (line.kind === "code") {
3291
- return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { ...theme, wrap: "wrap", children: line.text || " " })] }));
4479
+ return (_jsxs(Box, { children: [_jsx(WorkerOutputIndent, { backgroundColor: fillBackground, selected: selected }), _jsx(Text, { ...theme, wrap: "wrap", children: line.text || " " }), _jsx(WorkerOutputTrailingFill, { backgroundColor: fillBackground, width: width, usedWidth: 2 + displayWidth(line.text || " ") })] }));
3292
4480
  }
3293
4481
  if (line.kind === "source-line") {
3294
4482
  const sourceParts = sourceDisplayLineParts(line.text);
3295
4483
  if (sourceParts) {
3296
- return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { backgroundColor: WORKER_PANEL_BACKGROUND, color: "gray", children: sourceParts.gutter }), _jsx(Text, { backgroundColor: WORKER_PANEL_BACKGROUND, color: "white", wrap: "truncate-end", children: sourceParts.code })] }));
4484
+ const usedWidth = 2 + displayWidth(sourceParts.gutter) + displayWidth(sourceParts.code || " ");
4485
+ return (_jsxs(Box, { children: [_jsx(WorkerOutputIndent, { backgroundColor: fillBackground, selected: selected }), _jsx(Text, { backgroundColor: TUI_THEME.surface, color: TUI_THEME.muted, children: sourceParts.gutter }), _jsx(Text, { backgroundColor: TUI_THEME.surface, color: TUI_THEME.text, wrap: "truncate-end", children: sourceParts.code }), _jsx(WorkerOutputTrailingFill, { backgroundColor: fillBackground, width: width, usedWidth: usedWidth })] }));
3297
4486
  }
3298
- return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { ...theme, children: line.text || " " })] }));
4487
+ const body = line.text || " ";
4488
+ return (_jsxs(Box, { children: [_jsx(WorkerOutputIndent, { backgroundColor: fillBackground, selected: selected }), _jsx(Text, { ...theme, children: body }), _jsx(WorkerOutputTrailingFill, { backgroundColor: fillBackground, width: width, usedWidth: 2 + displayWidth(body) })] }));
3299
4489
  }
3300
4490
  if (line.continuation && isDiffCodeKind(line.kind)) {
3301
- return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { ...theme, wrap: "truncate-end", children: line.text || " " })] }));
4491
+ const body = line.text || " ";
4492
+ return (_jsxs(Box, { children: [_jsx(WorkerOutputIndent, { backgroundColor: fillBackground, selected: selected }), _jsx(Text, { ...theme, wrap: "truncate-end", children: body }), _jsx(WorkerOutputTrailingFill, { backgroundColor: fillBackground, width: width, usedWidth: 2 + displayWidth(body) })] }));
3302
4493
  }
3303
4494
  const diffCodeLine = diffCodeLineParts(line);
3304
4495
  if (diffCodeLine) {
3305
- return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { ...theme, children: `${diffCodeLine.lineNumber} ${diffCodeLine.sign} ` }), _jsx(Text, { ...theme, wrap: "wrap", children: diffCodeLine.code || " " })] }));
4496
+ const prefix = `${diffCodeLine.lineNumber} ${diffCodeLine.sign} `;
4497
+ const code = diffCodeLine.code || " ";
4498
+ return (_jsxs(Box, { children: [_jsx(WorkerOutputIndent, { backgroundColor: fillBackground, selected: selected }), _jsx(Text, { ...theme, children: prefix }), _jsx(Text, { ...theme, wrap: "wrap", children: code }), _jsx(WorkerOutputTrailingFill, { backgroundColor: fillBackground, width: width, usedWidth: 2 + displayWidth(prefix) + displayWidth(code) })] }));
4499
+ }
4500
+ const gutter = layout.gutter ? formatGutter(layout.gutter) : "";
4501
+ const body = layout.body || " ";
4502
+ return (_jsxs(Box, { children: [_jsx(WorkerOutputIndent, { backgroundColor: fillBackground, selected: selected }), gutter ? _jsx(Text, { ...theme, children: gutter }) : null, _jsx(Text, { ...theme, wrap: "truncate-end", children: body }), _jsx(WorkerOutputTrailingFill, { backgroundColor: fillBackground, width: width, usedWidth: 2 + displayWidth(gutter) + displayWidth(body) })] }));
4503
+ }
4504
+ function WorkerOutputIndent({ backgroundColor, selected }) {
4505
+ if (selected) {
4506
+ return _jsx(Text, { backgroundColor: backgroundColor ?? TUI_THEME.surface, color: TUI_THEME.accent, bold: true, children: "> " });
4507
+ }
4508
+ return backgroundColor
4509
+ ? _jsx(Text, { backgroundColor: backgroundColor, children: " " })
4510
+ : _jsx(Text, { color: TUI_THEME.muted, children: " " });
4511
+ }
4512
+ function WorkerOutputBlankLine({ width }) {
4513
+ const theme = workerOutputLineTheme("blank");
4514
+ const fillWidth = shouldRenderWorkerOutputRailFill() ? Math.max(1, width) : 1;
4515
+ return _jsx(Text, { ...theme, children: " ".repeat(fillWidth) });
4516
+ }
4517
+ function WorkerOutputTrailingFill({ backgroundColor, usedWidth, width }) {
4518
+ if (!backgroundColor || !shouldRenderWorkerOutputRailFill()) {
4519
+ return null;
3306
4520
  }
3307
- return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " " }), layout.gutter ? _jsx(Text, { ...theme, children: formatGutter(layout.gutter) }) : null, _jsx(Text, { ...theme, wrap: "truncate-end", children: layout.body || " " })] }));
4521
+ const fillWidth = Math.max(0, width - usedWidth);
4522
+ return fillWidth > 0 ? _jsx(Text, { backgroundColor: backgroundColor, children: " ".repeat(fillWidth) }) : null;
4523
+ }
4524
+ function shouldRenderWorkerOutputRailFill() {
4525
+ return process.stdout.isTTY === true && typeof process.stdout.columns === "number";
3308
4526
  }
3309
- function WorkerOutputNanoLine({ line, width }) {
4527
+ function WorkerOutputNanoLine({ fillWidth, line, selected, width }) {
3310
4528
  const theme = workerOutputLineTheme(line.kind);
3311
- const text = compactEndByDisplayWidth(workerOutputNanoLineText(line), Math.max(1, width));
3312
- return _jsx(Text, { ...theme, wrap: "truncate-end", children: text || " " });
4529
+ const fillBackground = workerOutputLineFillTheme(line.kind);
4530
+ const text = compactEndByDisplayWidth(`${selected ? "> " : ""}${workerOutputNanoLineText(line)}`, Math.max(1, width));
4531
+ const body = text || " ";
4532
+ return (_jsxs(Box, { children: [_jsx(Text, { ...theme, wrap: "truncate-end", children: body }), _jsx(WorkerOutputTrailingFill, { backgroundColor: fillBackground, width: fillWidth, usedWidth: displayWidth(body) })] }));
3313
4533
  }
3314
4534
  function workerOutputNanoLineText(line) {
3315
4535
  if (line.kind === "blank") {
@@ -3367,9 +4587,9 @@ export function workerOutputDiffColumns(text) {
3367
4587
  code: match[3] ?? ""
3368
4588
  };
3369
4589
  }
3370
- function renderDisplayLines(lines, width = Number(process.stdout.columns) || 120) {
3371
- const contentWidth = Math.max(1, width - 4);
3372
- if (isNanoWorkerOutputWidth(width)) {
4590
+ function renderDisplayLines(lines, width = Number(process.stdout.columns) || 120, options = {}) {
4591
+ const contentWidth = options.contentWidth ?? Math.max(1, width - 4);
4592
+ if (options.nano ?? isNanoWorkerOutputWidth(width)) {
3373
4593
  return renderNanoDisplayLines(lines, contentWidth);
3374
4594
  }
3375
4595
  return lines.flatMap((line) => {
@@ -3442,6 +4662,9 @@ export function workerOutputBodyDisplayLines(kind, text, width) {
3442
4662
  return wrapBodyWithContinuation(body, Math.max(1, width), continuationPrefix);
3443
4663
  }
3444
4664
  function compactWorkerBodyForWidth(kind, body, width) {
4665
+ if (kind === "diff-hunk") {
4666
+ return compactDiffHunkBodyForWidth(body, width);
4667
+ }
3445
4668
  if (kind === "summary") {
3446
4669
  return compactSummaryBodyForWidth(body, width);
3447
4670
  }
@@ -3480,6 +4703,25 @@ function compactWorkerBodyForWidth(kind, body, width) {
3480
4703
  }
3481
4704
  return body;
3482
4705
  }
4706
+ function compactDiffHunkBodyForWidth(body, width) {
4707
+ if (displayWidth(body) <= width) {
4708
+ return body;
4709
+ }
4710
+ const match = body.match(/^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@(?:\s+.*)?$/);
4711
+ if (!match) {
4712
+ return compactEndByDisplayWidth(body, width);
4713
+ }
4714
+ const oldStart = match[1] ?? "0";
4715
+ const newStart = match[2] ?? "0";
4716
+ const candidates = [
4717
+ `@@ -${oldStart} +${newStart} @@`,
4718
+ `-${oldStart} +${newStart}`,
4719
+ `${oldStart}>${newStart}`,
4720
+ `+${newStart}`
4721
+ ];
4722
+ return candidates.find((candidate) => displayWidth(candidate) <= width)
4723
+ ?? compactEndByDisplayWidth(body, width);
4724
+ }
3483
4725
  function compactContextWindowErrorForWidth(body, width) {
3484
4726
  if (width < 7) {
3485
4727
  return "ctx";
@@ -3558,9 +4800,9 @@ function compactNarrowWorkerBody(body, width) {
3558
4800
  if (/^Findings:\s*none$/i.test(body)) {
3559
4801
  return width < 24 ? compactFindingsNoneForWidth(width) : body;
3560
4802
  }
3561
- const findingsLabel = width < 24 ? "findings" : "findings.jsonl";
3562
- const repliesLabel = width < 24 ? "replies" : "replies.jsonl";
3563
- const worklogLabel = width < 24 ? "worklog" : "worklog.md";
4803
+ const findingsLabel = width < 24 ? "findings" : width < 32 ? "findings.jsonl" : "critic-findings.jsonl";
4804
+ const repliesLabel = width < 24 ? "replies" : width < 32 ? "replies.jsonl" : "actor-replies.jsonl";
4805
+ const worklogLabel = width < 24 ? "worklog" : width < 32 ? "worklog.md" : "actor-worklog.md";
3564
4806
  return body
3565
4807
  .replace(/^(•\s+)?npm run dev could not bind\b.*$/i, (_match, marker) => `${marker ?? ""}dev fallback.`)
3566
4808
  .replace(/\bcritic-findings\.jsonl\b/g, findingsLabel)