parallel-codex-tui 0.1.0 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/.parallel-codex/config.example.toml +90 -3
  2. package/README.md +269 -12
  3. package/dist/bootstrap.js +50 -18
  4. package/dist/cli-args.js +96 -14
  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 +40 -0
  10. package/dist/cli.js +291 -35
  11. package/dist/core/app-root.js +8 -0
  12. package/dist/core/collaboration-timeline.js +261 -0
  13. package/dist/core/config-errors.js +14 -0
  14. package/dist/core/config.js +191 -23
  15. package/dist/core/file-store.js +130 -6
  16. package/dist/core/lease-finalization.js +22 -0
  17. package/dist/core/paths.js +10 -0
  18. package/dist/core/process-identity.js +48 -0
  19. package/dist/core/process-mutation-turn.js +128 -0
  20. package/dist/core/process-ownership.js +276 -0
  21. package/dist/core/process-tree.js +90 -0
  22. package/dist/core/router-audit.js +155 -0
  23. package/dist/core/router-redaction.js +31 -0
  24. package/dist/core/router.js +473 -42
  25. package/dist/core/session-index.js +225 -30
  26. package/dist/core/session-manager.js +1182 -44
  27. package/dist/core/task-state-machine.js +17 -0
  28. package/dist/core/workspace-commit-recovery.js +118 -0
  29. package/dist/core/workspace.js +126 -0
  30. package/dist/doctor.js +384 -30
  31. package/dist/domain/schemas.js +127 -6
  32. package/dist/orchestrator/collaboration-channel.js +255 -4
  33. package/dist/orchestrator/feature-plan.js +70 -0
  34. package/dist/orchestrator/judge-artifacts.js +236 -0
  35. package/dist/orchestrator/orchestrator.js +1777 -212
  36. package/dist/orchestrator/prompts.js +126 -2
  37. package/dist/orchestrator/supervisor-summary.js +56 -2
  38. package/dist/orchestrator/workspace-sandbox.js +911 -0
  39. package/dist/tui/App.js +2838 -159
  40. package/dist/tui/AppShell.js +188 -23
  41. package/dist/tui/CollaborationTimelineView.js +327 -0
  42. package/dist/tui/FeatureBoardView.js +227 -0
  43. package/dist/tui/InputBar.js +514 -57
  44. package/dist/tui/RouterDiagnosticsView.js +469 -0
  45. package/dist/tui/StatusBar.js +610 -57
  46. package/dist/tui/TaskSessionsView.js +207 -0
  47. package/dist/tui/TerminalOutput.js +53 -9
  48. package/dist/tui/WorkerOutputView.js +1403 -161
  49. package/dist/tui/WorkerOverviewView.js +250 -0
  50. package/dist/tui/chat-history.js +25 -0
  51. package/dist/tui/chat-input.js +67 -19
  52. package/dist/tui/chat-paste.js +76 -0
  53. package/dist/tui/display-width.js +41 -3
  54. package/dist/tui/incremental-text-file.js +101 -0
  55. package/dist/tui/keyboard.js +46 -0
  56. package/dist/tui/markdown-text.js +14 -0
  57. package/dist/tui/raw-input-decoder.js +3 -0
  58. package/dist/tui/scrolling.js +2 -1
  59. package/dist/tui/status-line.js +318 -11
  60. package/dist/tui/task-memory.js +15 -0
  61. package/dist/tui/task-result.js +105 -0
  62. package/dist/tui/terminal-screen.js +13 -1
  63. package/dist/tui/theme-contrast.js +144 -0
  64. package/dist/tui/theme-preview.js +109 -0
  65. package/dist/tui/theme.js +158 -0
  66. package/dist/version.js +1 -1
  67. package/dist/workers/capabilities.js +212 -0
  68. package/dist/workers/live-probe.js +176 -0
  69. package/dist/workers/mock-adapter.js +39 -6
  70. package/dist/workers/native-attach.js +147 -8
  71. package/dist/workers/native-session-detection.js +17 -0
  72. package/dist/workers/process-adapter.js +580 -81
  73. package/dist/workers/registry.js +4 -2
  74. package/package.json +17 -2
@@ -1,79 +1,485 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { performance } from "node:perf_hooks";
3
+ import { StringDecoder } from "node:string_decoder";
2
4
  import { RouteDecisionSchema } from "../domain/schemas.js";
3
- export async function routeRequestWithCodex(request, config, runner = runCodexRouterProcess, cwd = config.projectRoot) {
5
+ import { ProcessTreeCleanupError, terminateProcessTree } from "./process-tree.js";
6
+ import { classifyRouterFailure } from "./router-audit.js";
7
+ import { sanitizeRouterText } from "./router-redaction.js";
8
+ export async function routeRequestWithCodex(request, config, runner = runCodexRouterProcess, cwd = config.projectRoot, signal, onProgress) {
9
+ const startedAt = Date.now();
10
+ const telemetryStartedAt = performance.now();
11
+ let dispatchMs;
12
+ if (signal?.aborted) {
13
+ throw cancellationError();
14
+ }
4
15
  if (config.router.defaultMode === "simple") {
5
- return simpleRoute("Forced simple mode from config.", config);
16
+ return annotateRoute(simpleRoute("Forced simple mode from config.", config), "forced", startedAt);
6
17
  }
7
18
  if (config.router.defaultMode === "complex") {
8
- return complexRoute("Forced complex mode from config.", config);
19
+ return annotateRoute(complexRoute("Forced complex mode from config.", config), "forced", startedAt);
9
20
  }
21
+ const proxyContext = routerProxyContext(config.router.codex.env);
22
+ const commandLabel = routerCommandLabel(config.router.codex.command);
23
+ const routerName = routerDisplayName(commandLabel);
10
24
  try {
11
- const output = await runner(buildCodexRouterPrompt(request, config), config, cwd);
12
- const route = parseCodexRoute(output, config);
13
- return RouteDecisionSchema.parse(route);
25
+ const prompt = buildCodexRouterPrompt(request, config);
26
+ dispatchMs = Math.max(0, performance.now() - telemetryStartedAt);
27
+ emitRouterProgress(onProgress, "dispatching");
28
+ const result = normalizeRouterRunnerResult(await runner(prompt, config, cwd, signal, onProgress));
29
+ let route;
30
+ const parseStartedAt = performance.now();
31
+ emitRouterProgress(onProgress, "parsing");
32
+ try {
33
+ route = RouteDecisionSchema.parse(parseCodexRoute(result.output, config));
34
+ }
35
+ catch (error) {
36
+ throw routerExecutionError(error, mergeRouterTelemetry(result.telemetry, {
37
+ router_dispatch_ms: dispatchMs,
38
+ router_parse_ms: Math.max(0, performance.now() - parseStartedAt)
39
+ }), "response");
40
+ }
41
+ return annotateRoute(route, "codex", startedAt, mergeRouterTelemetry(result.telemetry, {
42
+ router_dispatch_ms: dispatchMs,
43
+ router_parse_ms: Math.max(0, performance.now() - parseStartedAt)
44
+ }), proxyContext, commandLabel);
14
45
  }
15
46
  catch (error) {
16
- const fallback = fallbackRoute(config);
47
+ if (error instanceof ProcessTreeCleanupError) {
48
+ throw error;
49
+ }
50
+ if (signal?.aborted || isAbortError(error)) {
51
+ throw cancellationError();
52
+ }
53
+ const context = routerExecutionErrorContext(error);
54
+ const failureSummary = summarizeRouterError(error);
55
+ const fallback = annotateRoute(fallbackRoute(config, routerName), "fallback", startedAt, mergeRouterTelemetry(context.telemetry, {
56
+ router_dispatch_ms: dispatchMs
57
+ }), proxyContext, commandLabel);
17
58
  return {
18
59
  ...fallback,
19
- reason: `Codex router failed: ${error instanceof Error ? error.message : String(error)}. ${fallback.reason}`
60
+ ...(context.stage ? { router_failure_stage: context.stage } : {}),
61
+ router_failure_kind: routerFailureKind(failureSummary, context.stage, context.timeoutKind),
62
+ ...(context.timeoutKind ? { router_timeout_kind: context.timeoutKind } : {}),
63
+ reason: `${routerName} failed: ${failureSummary}. ${fallback.reason}`
20
64
  };
21
65
  }
22
66
  }
23
- export async function runCodexRouterProcess(prompt, config, cwd = config.projectRoot) {
24
- const { command, args, timeoutMs } = config.router.codex;
67
+ function routerFailureKind(summary, stage, timeoutKind) {
68
+ if (timeoutKind) {
69
+ return "timeout";
70
+ }
71
+ const classified = classifyRouterFailure(summary);
72
+ if (classified) {
73
+ return classified;
74
+ }
75
+ if (stage === "spawn") {
76
+ return "unavailable";
77
+ }
78
+ if (stage === "input") {
79
+ return "input";
80
+ }
81
+ if (stage === "exit") {
82
+ return "exit";
83
+ }
84
+ if (stage === "response") {
85
+ return "invalid-output";
86
+ }
87
+ return "unknown";
88
+ }
89
+ function annotateRoute(route, source, startedAt, telemetry, proxyContext, routerCommand) {
90
+ return {
91
+ ...route,
92
+ ...normalizeRouterTelemetry(telemetry),
93
+ ...routerProxyRouteFields(proxyContext),
94
+ ...(routerCommand ? { router_command: routerCommand } : {}),
95
+ source,
96
+ duration_ms: Math.max(0, Date.now() - startedAt)
97
+ };
98
+ }
99
+ export function routerCommandLabel(command) {
100
+ const executable = command.trim().split(/[\\/]/).filter(Boolean).at(-1) ?? "router";
101
+ const safe = sanitizeRouterText(executable)
102
+ .replace(/[\u0000-\u001f\u007f|·]+/g, "-")
103
+ .replace(/\s+/g, "-")
104
+ .replace(/-+/g, "-")
105
+ .replace(/^-|-$/g, "");
106
+ return Array.from(safe || "router").slice(0, 80).join("");
107
+ }
108
+ function routerDisplayName(commandLabel) {
109
+ return /^codex(?:\.exe)?$/i.test(commandLabel) ? "Codex router" : `Router ${commandLabel}`;
110
+ }
111
+ export async function runCodexRouterProcess(prompt, config, cwd = config.projectRoot, signal, onProgress) {
112
+ const { command, args, timeoutMs, firstOutputTimeoutMs, idleTimeoutMs, maxOutputBytes } = config.router.codex;
113
+ const routerName = routerDisplayName(routerCommandLabel(command));
114
+ if (signal?.aborted) {
115
+ throw cancellationError();
116
+ }
117
+ const configuredEnvironment = routerEnvironment(config.router.codex.env);
118
+ const env = {
119
+ ...process.env,
120
+ ...configuredEnvironment
121
+ };
122
+ const proxyConfigured = hasConfiguredProxy(env);
123
+ let progressPhase;
124
+ const reportProgress = (phase) => {
125
+ if (progressPhase === phase) {
126
+ return;
127
+ }
128
+ progressPhase = phase;
129
+ emitRouterProgress(onProgress, phase);
130
+ };
131
+ reportProgress("starting");
25
132
  return new Promise((resolve, reject) => {
133
+ const processStartedAt = Date.now();
134
+ const detached = process.platform !== "win32";
26
135
  const child = spawn(command, args, {
27
136
  cwd,
28
- env: process.env,
29
- stdio: ["pipe", "pipe", "pipe"]
137
+ env,
138
+ stdio: ["pipe", "pipe", "pipe"],
139
+ detached
30
140
  });
31
141
  let stdout = "";
32
142
  let stderr = "";
143
+ const stdoutDecoder = new StringDecoder("utf8");
144
+ const stderrDecoder = new StringDecoder("utf8");
145
+ let stdoutBytes = 0;
146
+ let stderrBytes = 0;
147
+ let spawnMs;
148
+ let firstOutputMs;
149
+ let firstStdoutMs;
150
+ let firstStderrMs;
33
151
  let settled = false;
34
- let timeout;
35
- const finish = (error, output = stdout) => {
152
+ let terminating = false;
153
+ let totalTimeout;
154
+ let firstOutputTimeout;
155
+ let idleTimeout;
156
+ let abortListener;
157
+ const clearRunTimers = () => {
158
+ if (totalTimeout) {
159
+ clearTimeout(totalTimeout);
160
+ totalTimeout = undefined;
161
+ }
162
+ if (firstOutputTimeout) {
163
+ clearTimeout(firstOutputTimeout);
164
+ firstOutputTimeout = undefined;
165
+ }
166
+ if (idleTimeout) {
167
+ clearTimeout(idleTimeout);
168
+ idleTimeout = undefined;
169
+ }
170
+ };
171
+ const telemetry = () => ({
172
+ ...(typeof spawnMs === "number" ? { router_spawn_ms: spawnMs } : {}),
173
+ ...(typeof firstOutputMs === "number" ? { router_first_output_ms: firstOutputMs } : {}),
174
+ ...(typeof firstStdoutMs === "number" ? { router_first_stdout_ms: firstStdoutMs } : {}),
175
+ ...(typeof firstStderrMs === "number" ? { router_first_stderr_ms: firstStderrMs } : {}),
176
+ router_process_ms: Math.max(0, Date.now() - processStartedAt),
177
+ router_stdout_bytes: stdoutBytes,
178
+ router_stderr_bytes: stderrBytes
179
+ });
180
+ const finish = (error, output = stdout, stage, timeoutKind) => {
36
181
  if (settled) {
37
182
  return;
38
183
  }
39
184
  settled = true;
40
- if (timeout) {
41
- clearTimeout(timeout);
185
+ clearRunTimers();
186
+ if (abortListener) {
187
+ signal?.removeEventListener("abort", abortListener);
42
188
  }
43
189
  if (error) {
44
- reject(error);
190
+ reject(stage ? routerExecutionError(error, telemetry(), stage, timeoutKind) : error);
45
191
  }
46
192
  else {
47
- resolve(output);
193
+ resolve({ output, telemetry: telemetry() });
194
+ }
195
+ };
196
+ const terminateThenFinish = (error, output = stdout, stage, timeoutKind) => {
197
+ if (settled || terminating) {
198
+ return;
199
+ }
200
+ terminating = true;
201
+ reportProgress("stopping");
202
+ clearRunTimers();
203
+ if (abortListener) {
204
+ signal?.removeEventListener("abort", abortListener);
205
+ abortListener = undefined;
206
+ }
207
+ void terminateProcessTree(child, {
208
+ processGroup: detached,
209
+ label: `${routerName} process`,
210
+ termGraceMs: 250,
211
+ killWaitMs: 500,
212
+ pollMs: 20
213
+ }).then(() => finish(error, output, stage, timeoutKind), (cleanupError) => finish(cleanupError instanceof ProcessTreeCleanupError
214
+ ? cleanupError
215
+ : new ProcessTreeCleanupError(`${routerName} cleanup failed: ${errorMessage(cleanupError)}`)));
216
+ };
217
+ const timeoutRouter = (kind, limitMs, stage) => {
218
+ if (settled || terminating) {
219
+ return;
220
+ }
221
+ const detail = summarizeRouterProcessDetail(stderr);
222
+ const proxyContext = proxyConfigured && !/\bproxy\b|代理/i.test(detail)
223
+ ? " with proxy configured"
224
+ : "";
225
+ const timeoutLabel = kind === "first-output" ? " first output" : kind === "idle" ? " idle" : "";
226
+ terminateThenFinish(new Error(`${routerName}${timeoutLabel} timed out after ${limitMs}ms${proxyContext}${detail ? `: ${detail}` : ""}`), stdout, stage, kind);
227
+ };
228
+ const stopIfOutputLimitExceeded = () => {
229
+ if (stdoutBytes + stderrBytes <= maxOutputBytes) {
230
+ return false;
48
231
  }
232
+ terminateThenFinish(new Error(`${routerName} output exceeded ${maxOutputBytes} byte limit`), stdout, "response");
233
+ return true;
234
+ };
235
+ const resetIdleTimeout = () => {
236
+ if (idleTimeout) {
237
+ clearTimeout(idleTimeout);
238
+ idleTimeout = undefined;
239
+ }
240
+ if (idleTimeoutMs <= 0 || (timeoutMs > 0 && idleTimeoutMs >= timeoutMs)) {
241
+ return;
242
+ }
243
+ idleTimeout = setTimeout(() => timeoutRouter("idle", idleTimeoutMs, "streaming"), idleTimeoutMs);
244
+ };
245
+ const recordOutputActivity = () => {
246
+ if (firstOutputTimeout) {
247
+ clearTimeout(firstOutputTimeout);
248
+ firstOutputTimeout = undefined;
249
+ }
250
+ resetIdleTimeout();
251
+ };
252
+ abortListener = () => {
253
+ terminateThenFinish(cancellationError());
49
254
  };
255
+ signal?.addEventListener("abort", abortListener, { once: true });
256
+ child.once("spawn", () => {
257
+ if (terminating) {
258
+ return;
259
+ }
260
+ spawnMs = Math.max(0, Date.now() - processStartedAt);
261
+ reportProgress("waiting-output");
262
+ });
50
263
  if (timeoutMs > 0) {
51
- timeout = setTimeout(() => {
52
- child.kill("SIGTERM");
53
- finish(new Error(`Codex router timed out after ${timeoutMs}ms`));
264
+ totalTimeout = setTimeout(() => {
265
+ const stage = stdoutBytes + stderrBytes === 0 ? "waiting-output" : "streaming";
266
+ timeoutRouter("total", timeoutMs, stage);
54
267
  }, timeoutMs);
55
268
  }
269
+ if (firstOutputTimeoutMs > 0 && (timeoutMs <= 0 || firstOutputTimeoutMs < timeoutMs)) {
270
+ firstOutputTimeout = setTimeout(() => timeoutRouter("first-output", firstOutputTimeoutMs, "waiting-output"), firstOutputTimeoutMs);
271
+ }
56
272
  child.stdout.on("data", (chunk) => {
57
- stdout += chunk.toString("utf8");
273
+ if (settled || terminating) {
274
+ return;
275
+ }
276
+ stdoutBytes += chunk.byteLength;
277
+ firstOutputMs ??= Math.max(0, Date.now() - processStartedAt);
278
+ firstStdoutMs ??= Math.max(0, Date.now() - processStartedAt);
279
+ if (stopIfOutputLimitExceeded()) {
280
+ return;
281
+ }
282
+ stdout += stdoutDecoder.write(chunk);
283
+ recordOutputActivity();
284
+ reportProgress("receiving-response");
58
285
  });
59
286
  child.stderr.on("data", (chunk) => {
60
- stderr += chunk.toString("utf8");
287
+ if (settled || terminating) {
288
+ return;
289
+ }
290
+ stderrBytes += chunk.byteLength;
291
+ firstOutputMs ??= Math.max(0, Date.now() - processStartedAt);
292
+ firstStderrMs ??= Math.max(0, Date.now() - processStartedAt);
293
+ if (stopIfOutputLimitExceeded()) {
294
+ return;
295
+ }
296
+ stderr += stderrDecoder.write(chunk);
297
+ recordOutputActivity();
298
+ if (stdoutBytes === 0) {
299
+ reportProgress("receiving-stderr");
300
+ }
61
301
  });
62
302
  child.on("error", (error) => {
63
- finish(error);
303
+ if (terminating) {
304
+ return;
305
+ }
306
+ finish(error, stdout, "spawn");
64
307
  });
65
308
  child.on("close", (code, signal) => {
309
+ if (terminating) {
310
+ return;
311
+ }
312
+ stdout += stdoutDecoder.end();
313
+ stderr += stderrDecoder.end();
66
314
  if (code === 0) {
67
315
  finish(null);
68
316
  return;
69
317
  }
70
318
  const detail = (stderr || stdout).trim();
71
- finish(new Error(`Codex router exited with ${signal ? `signal ${signal}` : `code ${code ?? 1}`}${detail ? `: ${detail}` : ""}`));
319
+ finish(new Error(`${routerName} exited with ${signal ? `signal ${signal}` : `code ${code ?? 1}`}${detail ? `: ${detail}` : ""}`), stdout, "exit");
320
+ });
321
+ child.stdin.once("error", (error) => {
322
+ terminateThenFinish(new Error(`${routerName} input failed: ${error.message}`), stdout, "input");
72
323
  });
73
- child.stdin.write(prompt);
74
- child.stdin.end();
324
+ if (signal?.aborted) {
325
+ abortListener();
326
+ }
327
+ else {
328
+ child.stdin.end(prompt);
329
+ }
330
+ });
331
+ }
332
+ function errorMessage(error) {
333
+ return error instanceof Error ? error.message : String(error);
334
+ }
335
+ function normalizeRouterRunnerResult(result) {
336
+ return typeof result === "string"
337
+ ? { output: result }
338
+ : { output: result.output, telemetry: normalizeRouterTelemetry(result.telemetry) };
339
+ }
340
+ function normalizeRouterTelemetry(telemetry) {
341
+ if (!telemetry) {
342
+ return undefined;
343
+ }
344
+ const normalized = {};
345
+ for (const key of [
346
+ "router_dispatch_ms",
347
+ "router_spawn_ms",
348
+ "router_first_output_ms",
349
+ "router_first_stdout_ms",
350
+ "router_first_stderr_ms",
351
+ "router_process_ms",
352
+ "router_parse_ms",
353
+ "router_stdout_bytes",
354
+ "router_stderr_bytes"
355
+ ]) {
356
+ const value = telemetry[key];
357
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
358
+ continue;
359
+ }
360
+ normalized[key] = key.endsWith("_bytes") ? Math.trunc(value) : value;
361
+ }
362
+ return normalized;
363
+ }
364
+ function mergeRouterTelemetry(telemetry, additions) {
365
+ return normalizeRouterTelemetry({
366
+ ...telemetry,
367
+ ...Object.fromEntries(Object.entries(additions).filter(([, value]) => value !== undefined))
75
368
  });
76
369
  }
370
+ function routerExecutionError(error, telemetry, stage, timeoutKind) {
371
+ const source = error instanceof Error ? error : new Error(String(error));
372
+ const wrapped = new Error(source.message);
373
+ wrapped.name = source.name;
374
+ wrapped.routerTelemetry = normalizeRouterTelemetry(telemetry);
375
+ wrapped.routerFailureStage = stage;
376
+ wrapped.routerTimeoutKind = timeoutKind;
377
+ return wrapped;
378
+ }
379
+ function routerExecutionErrorContext(error) {
380
+ if (!(error instanceof Error)) {
381
+ return {};
382
+ }
383
+ const executionError = error;
384
+ return {
385
+ ...(executionError.routerTelemetry ? { telemetry: executionError.routerTelemetry } : {}),
386
+ ...(executionError.routerFailureStage ? { stage: executionError.routerFailureStage } : {}),
387
+ ...(executionError.routerTimeoutKind ? { timeoutKind: executionError.routerTimeoutKind } : {})
388
+ };
389
+ }
390
+ function routerEnvironment(configured, env = process.env) {
391
+ return Object.fromEntries(Object.entries(configured).map(([name, value]) => [
392
+ name,
393
+ value.replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, variable) => env[variable] ?? "")
394
+ ]));
395
+ }
396
+ export function routerProxyConfigured(configured, env = process.env) {
397
+ return routerProxyContext(configured, env).configured;
398
+ }
399
+ export function routerProxyContext(configured, env = process.env) {
400
+ const configuredEnvironment = routerEnvironment(configured, env);
401
+ const configuredProxy = preferredProxyEntry(configuredEnvironment);
402
+ if (configuredProxy) {
403
+ return {
404
+ configured: true,
405
+ source: "router-config",
406
+ variable: configuredProxy.name,
407
+ endpoint: safeProxyEndpoint(configuredProxy.value)
408
+ };
409
+ }
410
+ const inheritedProxy = preferredProxyEntry({
411
+ ...env,
412
+ ...configuredEnvironment
413
+ });
414
+ if (!inheritedProxy) {
415
+ return { configured: false };
416
+ }
417
+ return {
418
+ configured: true,
419
+ source: "environment",
420
+ variable: inheritedProxy.name,
421
+ endpoint: safeProxyEndpoint(inheritedProxy.value)
422
+ };
423
+ }
424
+ function hasConfiguredProxy(env) {
425
+ return Object.entries(env).some(([name, value]) => (/^(?:HTTP|HTTPS|ALL)_PROXY$/i.test(name) && Boolean(value?.trim())));
426
+ }
427
+ function preferredProxyEntry(env) {
428
+ const candidates = Object.entries(env)
429
+ .filter((entry) => (/^(?:HTTP|HTTPS|ALL)_PROXY$/i.test(entry[0]) && Boolean(entry[1]?.trim())))
430
+ .sort((left, right) => {
431
+ const priority = proxyVariablePriority(left[0]) - proxyVariablePriority(right[0]);
432
+ return priority || left[0].localeCompare(right[0]);
433
+ });
434
+ const selected = candidates[0];
435
+ return selected ? { name: selected[0], value: selected[1].trim() } : null;
436
+ }
437
+ function proxyVariablePriority(name) {
438
+ const normalized = name.toUpperCase();
439
+ return normalized === "HTTPS_PROXY" ? 0 : normalized === "ALL_PROXY" ? 1 : 2;
440
+ }
441
+ function safeProxyEndpoint(value) {
442
+ try {
443
+ const normalized = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(value)
444
+ ? value
445
+ : `http://${value}`;
446
+ const parsed = new URL(normalized);
447
+ return parsed.host || "custom";
448
+ }
449
+ catch {
450
+ return "custom";
451
+ }
452
+ }
453
+ function routerProxyRouteFields(proxyContext) {
454
+ if (!proxyContext) {
455
+ return {};
456
+ }
457
+ if (!proxyContext.configured) {
458
+ return { proxy_configured: false };
459
+ }
460
+ return {
461
+ proxy_configured: true,
462
+ proxy_source: proxyContext.source,
463
+ proxy_variable: proxyContext.variable,
464
+ proxy_endpoint: proxyContext.endpoint
465
+ };
466
+ }
467
+ function emitRouterProgress(listener, phase) {
468
+ try {
469
+ listener?.({ phase });
470
+ }
471
+ catch {
472
+ // UI progress listeners must never change Router execution semantics.
473
+ }
474
+ }
475
+ function cancellationError() {
476
+ const error = new Error("Request cancelled.");
477
+ error.name = "AbortError";
478
+ return error;
479
+ }
480
+ function isAbortError(error) {
481
+ return error instanceof Error && error.name === "AbortError";
482
+ }
77
483
  function buildCodexRouterPrompt(request, config) {
78
484
  return [
79
485
  "You are a routing classifier for parallel-codex-tui.",
@@ -86,11 +492,7 @@ function buildCodexRouterPrompt(request, config) {
86
492
  "JSON schema:",
87
493
  JSON.stringify({
88
494
  mode: "simple|complex",
89
- reason: "short explanation",
90
- suggested_roles: ["judge", "actor", "critic"],
91
- judge_engine: config.pairing.judge,
92
- actor_engine: config.pairing.actor,
93
- critic_engine: config.pairing.critic
495
+ reason: "short explanation"
94
496
  }),
95
497
  "",
96
498
  "User request:",
@@ -98,15 +500,25 @@ function buildCodexRouterPrompt(request, config) {
98
500
  ].join("\n");
99
501
  }
100
502
  function parseCodexRoute(output, config) {
503
+ const routerName = routerDisplayName(routerCommandLabel(config.router.codex.command));
101
504
  const jsonText = extractJsonObject(output);
102
505
  const parsed = JSON.parse(jsonText);
506
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
507
+ throw new Error(`Invalid ${routerName} response object`);
508
+ }
509
+ const record = parsed;
510
+ const mode = typeof record.mode === "string" ? record.mode.trim().toLowerCase() : "";
511
+ if (mode !== "simple" && mode !== "complex") {
512
+ throw new Error(`Invalid ${routerName} mode`);
513
+ }
514
+ const reason = typeof record.reason === "string" ? record.reason.trim() : "";
103
515
  return {
104
- mode: parsed.mode === "complex" ? "complex" : "simple",
105
- reason: parsed.reason || "Codex router decision.",
106
- suggested_roles: parsed.mode === "complex" ? ["judge", "actor", "critic"] : [],
107
- judge_engine: parsed.judge_engine ?? config.pairing.judge,
108
- actor_engine: parsed.actor_engine ?? config.pairing.actor,
109
- critic_engine: parsed.critic_engine ?? config.pairing.critic
516
+ mode,
517
+ reason: reason ? sanitizeRouterText(reason) : `${routerName} decision.`,
518
+ suggested_roles: mode === "complex" ? ["judge", "actor", "critic"] : [],
519
+ judge_engine: config.pairing.judge,
520
+ actor_engine: config.pairing.actor,
521
+ critic_engine: config.pairing.critic
110
522
  };
111
523
  }
112
524
  function extractJsonObject(output) {
@@ -120,14 +532,33 @@ function extractJsonObject(output) {
120
532
  }
121
533
  return match[0];
122
534
  }
123
- function fallbackRoute(config) {
535
+ function fallbackRoute(config, routerName) {
124
536
  if (config.router.codex.fallback === "simple") {
125
- return simpleRoute("Codex router fallback forced simple.", config);
537
+ return simpleRoute(`${routerName} fallback forced simple.`, config);
126
538
  }
127
539
  if (config.router.codex.fallback === "complex") {
128
- return complexRoute("Codex router fallback forced complex.", config);
540
+ return complexRoute(`${routerName} fallback forced complex.`, config);
129
541
  }
130
- return complexRoute("Codex router fallback forced complex.", config);
542
+ return complexRoute(`${routerName} fallback forced complex.`, config);
543
+ }
544
+ function summarizeRouterError(error) {
545
+ const raw = error instanceof Error ? error.message : String(error);
546
+ const meaningful = raw
547
+ .split(/\r?\n/)
548
+ .map((line) => line.trim())
549
+ .find((line) => line && !line.toLowerCase().startsWith("tip:") && !line.toLowerCase().startsWith("usage:"));
550
+ return sanitizeRouterText(meaningful || "unknown router error").replace(/[.。]+$/u, "");
551
+ }
552
+ function summarizeRouterProcessDetail(output) {
553
+ const lines = output
554
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
555
+ .replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
556
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ")
557
+ .split(/\r?\n/)
558
+ .map((line) => line.trim())
559
+ .filter(Boolean);
560
+ const latest = lines.at(-1) ?? "";
561
+ return sanitizeRouterText(latest).slice(0, 240);
131
562
  }
132
563
  function simpleRoute(reason, config) {
133
564
  return {