parallel-codex-tui 0.2.1 → 0.2.2

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.
@@ -4,18 +4,18 @@ defaultMode = "auto"
4
4
  [router.codex]
5
5
  command = "codex"
6
6
  args = ["exec", "--ephemeral", "--ignore-rules", "-c", "model_reasoning_effort=low", "--skip-git-repo-check", "--sandbox", "read-only", "--color", "never", "-"]
7
- timeoutMs = 30000
7
+ timeoutMs = 60000
8
8
  # Stop a silent Router before the total ceiling so one transient retry stays bounded.
9
- firstOutputTimeoutMs = 15000
9
+ firstOutputTimeoutMs = 30000
10
10
  # Reset after every stdout/stderr chunk; timeoutMs remains the hard ceiling.
11
- idleTimeoutMs = 15000
11
+ idleTimeoutMs = 30000
12
12
  # Stop a noisy or malformed classifier before it can grow Router memory without bound (1 KiB..16 MiB).
13
13
  maxOutputBytes = 1048576
14
14
  # Retry one transient Router failure before asking the user; 1 disables automatic retry.
15
15
  maxAttempts = 2
16
16
  retryDelayMs = 500
17
17
  # Active-task follow-ups fail safely to Main after this shorter Codex timeout.
18
- followUpTimeoutMs = 20000
18
+ followUpTimeoutMs = 45000
19
19
  # Non-interactive fallback; the TUI pauses and asks before executing it.
20
20
  fallback = "simple"
21
21
 
package/README.md CHANGED
@@ -6,7 +6,7 @@ Built with Codex-assisted development.
6
6
 
7
7
  ## Current Release
8
8
 
9
- `v0.2.1` is available from [npm](https://www.npmjs.com/package/parallel-codex-tui/v/0.2.1) and as a [GitHub Release](https://github.com/allendred/parallel-codex-tui/releases/tag/v0.2.1). It restores single Up/Down request-history navigation while preserving mouse-wheel chat scrolling, terminal text selection, and the parallel recovery and diagnostics work introduced in `v0.2.0`.
9
+ `v0.2.2` is available from [npm](https://www.npmjs.com/package/parallel-codex-tui/v/0.2.2) and as a [GitHub Release](https://github.com/allendred/parallel-codex-tui/releases/tag/v0.2.2). It automatically inherits the macOS system proxy when explicit proxy variables are absent, expands the default Router cold-start budget, and retains the single Up/Down request-history fix from `v0.2.1`.
10
10
 
11
11
  The release keeps terminal scrolling and copying available at the same time without requiring Shift. Ordinary left-drag selection remains owned by the terminal while alternate-scroll delivers the wheel to chat, Worker logs, and structured views. `Ctrl+Y` copies the visible view as a fallback. It preserves the embedded native Agent scrollback across status-detail round trips and real PTY resizes.
12
12
 
@@ -20,7 +20,7 @@ Highlights:
20
20
  - Named Worker Providers support Codex-compatible, Claude-compatible, OpenAI-compatible, Anthropic-compatible, and custom generic commands with independent role, model, environment, permission, resume, and interactive settings.
21
21
  - Worker overview, Feature board, collaboration timeline, Task center, status details, rendered Markdown/Diff/error logs, Unicode search, keyboard navigation, mouse scrolling, and configurable themes share one terminal UI system.
22
22
 
23
- Release acceptance includes a real three-Feature Tetris task with parallel Actor/Critic waves and final integration review. Real Codex and Claude probes both proved fresh and same-session resume calls, the semantic Router completed a live classification, and one TUI completed Main calls in two workspaces before restoring the first workspace without leaking chat state. PTY coverage runs in Apple Terminal, tmux, and Zellij profiles at narrow and wide sizes, including status/log equivalence and preserving the native output tail across status-detail round trips. The deterministic repository suite passed 1,257 tests across 122 files; one quota-consuming real-Agent test is skipped by default and passes through `npm run test:real-agents`.
23
+ Release acceptance includes a real three-Feature Tetris task with parallel Actor/Critic waves and final integration review. Real Codex and Claude probes both proved fresh and same-session resume calls, the semantic Router completed a live classification, and one TUI completed Main calls in two workspaces before restoring the first workspace without leaking chat state. PTY coverage runs in Apple Terminal, tmux, and Zellij profiles at narrow and wide sizes, including status/log equivalence and preserving the native output tail across status-detail round trips. The deterministic repository suite passed 1,262 tests across 123 files; one quota-consuming real-Agent test is skipped by default and passes through `npm run test:real-agents`.
24
24
 
25
25
  Real Provider probes depend on valid local CLI credentials. In particular, authenticate the Claude CLI before selecting a Claude-compatible Worker, then run `parallel-codex-tui --doctor --probe-agents` to prove fresh and resumed calls on that machine.
26
26
 
@@ -264,17 +264,17 @@ defaultMode = "auto"
264
264
  [router.codex]
265
265
  command = "codex"
266
266
  args = ["exec", "--ephemeral", "--ignore-rules", "-c", "model_reasoning_effort=low", "--skip-git-repo-check", "--sandbox", "read-only", "--color", "never", "-"]
267
- timeoutMs = 30000
268
- firstOutputTimeoutMs = 15000
269
- idleTimeoutMs = 15000
267
+ timeoutMs = 60000
268
+ firstOutputTimeoutMs = 30000
269
+ idleTimeoutMs = 30000
270
270
  maxOutputBytes = 1048576
271
271
  maxAttempts = 2
272
272
  retryDelayMs = 500
273
- followUpTimeoutMs = 20000
273
+ followUpTimeoutMs = 45000
274
274
  fallback = "simple"
275
275
  ```
276
276
 
277
- Set `defaultMode = "simple"` / `defaultMode = "complex"` to force one path. In `auto` mode, routing is semantic through an ephemeral, low-reasoning Codex run. Only `simple` and `complex` are accepted route modes; harmless casing and surrounding whitespace are normalized, while invalid JSON or an unknown mode uses the configured fallback and appears as `invalid output` in the status bar. `fallback = "simple"` or `fallback = "complex"` supplies the non-interactive fallback path; the safe default is `simple` and there is no keyword-only router. `firstOutputTimeoutMs` stops a silent process, `idleTimeoutMs` resets after every stdout or stderr chunk, and `timeoutMs` remains the hard ceiling even while output continues. `maxOutputBytes` bounds combined Router stdout and stderr in memory; exceeding it stops the process tree and reports invalid output instead of waiting for a timeout. The 1 MiB default can be configured from 1 KiB through 16 MiB. The 15-second first-output and idle defaults stay below the 30-second total ceiling, so their failure kinds remain visible. Two repeated silent starts settle in about 30.5 seconds instead of two full 30-second waits; an idle deadline begins after the latest output, while the hard ceiling still bounds every attempt. A watchdog only runs separately when its limit is lower than the active initial or follow-up total timeout, avoiding competing timers at the same deadline.
277
+ Set `defaultMode = "simple"` / `defaultMode = "complex"` to force one path. In `auto` mode, routing is semantic through an ephemeral, low-reasoning Codex run. Only `simple` and `complex` are accepted route modes; harmless casing and surrounding whitespace are normalized, while invalid JSON or an unknown mode uses the configured fallback and appears as `invalid output` in the status bar. `fallback = "simple"` or `fallback = "complex"` supplies the non-interactive fallback path; the safe default is `simple` and there is no keyword-only router. `firstOutputTimeoutMs` stops a silent process, `idleTimeoutMs` resets after every stdout or stderr chunk, and `timeoutMs` remains the hard ceiling even while output continues. `maxOutputBytes` bounds combined Router stdout and stderr in memory; exceeding it stops the process tree and reports invalid output instead of waiting for a timeout. The 1 MiB default can be configured from 1 KiB through 16 MiB. The 30-second first-output and idle defaults stay below the 60-second total ceiling, leaving room for a proxied Codex cold start while preserving distinct failure evidence. An idle deadline begins after the latest output, while the hard ceiling still bounds every attempt. A watchdog only runs separately when its limit is lower than the active initial or follow-up total timeout, avoiding competing timers at the same deadline.
278
278
 
279
279
  On POSIX systems the Router command runs in its own process group. Timeout, cancellation, and stdin failure send `SIGTERM`, wait briefly for graceful cleanup, then use `SIGKILL` when any group member remains. A retry, fallback choice, or completed cancellation is not exposed until that command tree is confirmed stopped, so a previous classifier cannot overlap the next Router or Worker. If termination cannot be verified after `SIGKILL`, the request fails closed instead of continuing around a live process.
280
280
 
@@ -290,7 +290,7 @@ Every new Router fallback persists an authoritative `router_failure_kind` alongs
290
290
 
291
291
  ### Proxy Environment
292
292
 
293
- Some CLI runtimes do not inherit the macOS System Settings proxy. Configure the router explicitly when direct OpenAI connections are blocked:
293
+ On macOS, startup automatically inherits enabled HTTP, HTTPS, SOCKS, and bypass settings from System Settings when the corresponding process environment variables are absent. Explicit shell variables and local config remain authoritative. Set `PARALLEL_CODEX_INHERIT_SYSTEM_PROXY=0` to opt out. Configure provider-specific values explicitly when one agent needs a different route:
294
294
 
295
295
  ```toml
296
296
  [router.codex.env]
@@ -308,7 +308,7 @@ NO_PROXY = "localhost,127.0.0.1"
308
308
 
309
309
  `router.codex.env` applies only to semantic classification. `workers.<id>.model.env` applies to fresh/resumed runs and embedded native attach for that named Worker Provider. Keep these values in local `config.toml`, which is ignored by Git.
310
310
 
311
- Run `parallel-codex-tui --doctor` after changing proxy settings. Doctor checks referenced environment variables, reports when a macOS system proxy is not inherited by Codex subprocesses, and labels configured proxy host/port checks as local-endpoint reachability without printing credentials. Then run `parallel-codex-tui --doctor --probe-router` when you also need to verify the real Codex Router path through that proxy; this explicit probe can take up to `router.codex.timeoutMs`. A timed-out request is identified as `first output timeout`, `idle timeout after stdout/stderr`, or `total timeout`; `via <proxy-host:port>` remains context, not a claim that the proxy caused the failure. The audit never exposes the proxy URL or credentials.
311
+ Run `parallel-codex-tui --doctor` after changing proxy settings. Doctor checks referenced environment variables and labels inherited or configured proxy host/port checks as local-endpoint reachability without printing credentials. If system-proxy detection is unavailable, Doctor reports that the macOS proxy was not inherited and points to the explicit config table. Then run `parallel-codex-tui --doctor --probe-router` when you also need to verify the real Codex Router path through that proxy; this explicit probe can take up to `router.codex.timeoutMs`. A timed-out request is identified as `first output timeout`, `idle timeout after stdout/stderr`, or `total timeout`; `via <proxy-host:port>` remains context, not a claim that the proxy caused the failure. The audit never exposes the proxy URL or credentials.
312
312
 
313
313
  ## Mock Mode
314
314
 
@@ -516,12 +516,12 @@ The release job installs npm `^11.5.1`, runs on Node `24.15.x`, publishes the pr
516
516
  To publish a release, update `package.json` and `src/version.ts` to the same version, then push a matching tag:
517
517
 
518
518
  ```bash
519
- VERSION=0.2.1
519
+ VERSION=0.2.2
520
520
  git tag "v$VERSION"
521
521
  git push origin "v$VERSION"
522
522
  ```
523
523
 
524
- You can also run the Release workflow manually and enter the same tag value. The release tag must match `package.json`; for example, package version `0.2.1` requires tag `v0.2.1`. Published tags such as `v0.2.0` are immutable and must not be moved or reused.
524
+ You can also run the Release workflow manually and enter the same tag value. The release tag must match `package.json`; for example, package version `0.2.2` requires tag `v0.2.2`. Published tags such as `v0.2.1` are immutable and must not be moved or reused.
525
525
 
526
526
  ## Publishing Hygiene
527
527
 
package/dist/cli.js CHANGED
@@ -16,6 +16,7 @@ import { configPath, loadConfig, withUiThemeOverride, writeDefaultConfig } from
16
16
  import { pathExists } from "./core/file-store.js";
17
17
  import { exportDiagnostics } from "./core/diagnostics.js";
18
18
  import { readRouterAudit } from "./core/router-audit.js";
19
+ import { inheritMacSystemProxy } from "./core/system-proxy.js";
19
20
  import { loadTaskSessionDetails as loadPersistedTaskSessionDetails } from "./core/task-session-details.js";
20
21
  import { listWorkspaceChoices } from "./core/workspace.js";
21
22
  import { runDoctor, runRuntimePreflight } from "./doctor.js";
@@ -40,6 +41,9 @@ async function main() {
40
41
  process.exit(1);
41
42
  }
42
43
  const cliArgs = parseCliArgs(rawArgs, process.cwd());
44
+ if (!cliArgs.help && !cliArgs.themes && !cliArgs.version && !cliArgs.init) {
45
+ await inheritMacSystemProxy(process.env);
46
+ }
43
47
  if (!cliArgs.help && !cliArgs.themes && !cliArgs.version) {
44
48
  await prepareAppRoot(cliArgs.appRoot);
45
49
  }
@@ -74,13 +74,13 @@ const CodexRouterConfigSchema = z.object({
74
74
  "never",
75
75
  "-"
76
76
  ]),
77
- timeoutMs: z.number().int().positive().default(30000),
78
- firstOutputTimeoutMs: z.number().int().positive().default(15000),
79
- idleTimeoutMs: z.number().int().positive().default(15000),
77
+ timeoutMs: z.number().int().positive().default(60000),
78
+ firstOutputTimeoutMs: z.number().int().positive().default(30000),
79
+ idleTimeoutMs: z.number().int().positive().default(30000),
80
80
  maxOutputBytes: z.number().int().min(1024).max(16 * 1024 * 1024).default(1024 * 1024),
81
81
  maxAttempts: z.number().int().min(1).max(3).default(2),
82
82
  retryDelayMs: z.number().int().min(0).max(10000).default(500),
83
- followUpTimeoutMs: z.number().int().positive().max(120000).default(20000),
83
+ followUpTimeoutMs: z.number().int().positive().max(120000).default(45000),
84
84
  fallback: z.enum(["simple", "complex"]).default("simple"),
85
85
  env: z.record(z.string()).default({})
86
86
  });
@@ -165,13 +165,13 @@ export function defaultConfig(projectRoot) {
165
165
  "never",
166
166
  "-"
167
167
  ],
168
- timeoutMs: 30000,
169
- firstOutputTimeoutMs: 15000,
170
- idleTimeoutMs: 15000,
168
+ timeoutMs: 60000,
169
+ firstOutputTimeoutMs: 30000,
170
+ idleTimeoutMs: 30000,
171
171
  maxOutputBytes: 1024 * 1024,
172
172
  maxAttempts: 2,
173
173
  retryDelayMs: 500,
174
- followUpTimeoutMs: 20000,
174
+ followUpTimeoutMs: 45000,
175
175
  fallback: "simple",
176
176
  env: {}
177
177
  }
@@ -0,0 +1,79 @@
1
+ import { execFile } from "node:child_process";
2
+ const proxyEnvironmentNames = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"];
3
+ export async function inheritMacSystemProxy(env = process.env, options = {}) {
4
+ if ((options.platform ?? process.platform) !== "darwin" || systemProxyInheritanceDisabled(env)) {
5
+ return [];
6
+ }
7
+ const output = await (options.readSettings ?? readMacSystemProxySettings)();
8
+ const settings = parseMacSystemProxySettings(output);
9
+ if (!settings.http && !settings.https && !settings.socks) {
10
+ return [];
11
+ }
12
+ const inherited = {
13
+ HTTP_PROXY: settings.http ? proxyUrl("http", settings.http) : null,
14
+ HTTPS_PROXY: settings.https ? proxyUrl("http", settings.https) : null,
15
+ ALL_PROXY: settings.socks ? proxyUrl("socks5h", settings.socks) : null
16
+ };
17
+ const applied = [];
18
+ for (const name of proxyEnvironmentNames) {
19
+ const value = inherited[name];
20
+ if (!value || environmentHasName(env, name)) {
21
+ continue;
22
+ }
23
+ env[name] = value;
24
+ applied.push(name);
25
+ }
26
+ if (settings.exceptions.length > 0 && !environmentHasName(env, "NO_PROXY")) {
27
+ env.NO_PROXY = settings.exceptions.join(",");
28
+ applied.push("NO_PROXY");
29
+ }
30
+ return applied;
31
+ }
32
+ export function parseMacSystemProxySettings(output) {
33
+ return {
34
+ http: parseProxyEndpoint(output, "HTTP"),
35
+ https: parseProxyEndpoint(output, "HTTPS"),
36
+ socks: parseProxyEndpoint(output, "SOCKS"),
37
+ exceptions: parseProxyExceptions(output)
38
+ };
39
+ }
40
+ function parseProxyEndpoint(output, prefix) {
41
+ const enabled = output.match(new RegExp(`\\b${prefix}Enable\\s*:\\s*(\\d+)`))?.[1] === "1";
42
+ const host = output.match(new RegExp(`\\b${prefix}Proxy\\s*:\\s*(\\S+)`))?.[1];
43
+ const port = Number(output.match(new RegExp(`\\b${prefix}Port\\s*:\\s*(\\d+)`))?.[1]);
44
+ return enabled && host && Number.isInteger(port) && port > 0 ? { host, port } : null;
45
+ }
46
+ function parseProxyExceptions(output) {
47
+ const block = output.match(/ExceptionsList\s*:\s*<array>\s*\{([\s\S]*?)\n\s*\}/)?.[1] ?? "";
48
+ const exceptions = [...block.matchAll(/^\s*\d+\s*:\s*(.+?)\s*$/gm)]
49
+ .map((match) => normalizeProxyException(match[1] ?? ""))
50
+ .filter((value) => Boolean(value));
51
+ return [...new Set(exceptions)];
52
+ }
53
+ function normalizeProxyException(value) {
54
+ const normalized = value.trim();
55
+ if (!normalized) {
56
+ return null;
57
+ }
58
+ if (normalized === "<local>") {
59
+ return "localhost";
60
+ }
61
+ return normalized.startsWith("*.") ? normalized.slice(1) : normalized;
62
+ }
63
+ function proxyUrl(protocol, endpoint) {
64
+ const host = endpoint.host.includes(":") ? `[${endpoint.host}]` : endpoint.host;
65
+ return `${protocol}://${host}:${endpoint.port}`;
66
+ }
67
+ function environmentHasName(env, name) {
68
+ return Object.entries(env).some(([candidate, value]) => (candidate.toUpperCase() === name && Boolean(value?.trim())));
69
+ }
70
+ function systemProxyInheritanceDisabled(env) {
71
+ return /^(?:0|false|no|off)$/i.test(env.PARALLEL_CODEX_INHERIT_SYSTEM_PROXY?.trim() ?? "");
72
+ }
73
+ function readMacSystemProxySettings() {
74
+ return new Promise((resolve) => {
75
+ execFile("scutil", ["--proxy"], { timeout: 1000 }, (error, stdout) => {
76
+ resolve(error ? "" : stdout);
77
+ });
78
+ });
79
+ }
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = "0.2.1";
1
+ export const version = "0.2.2";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parallel-codex-tui",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "A TypeScript TUI wrapper for routed parallel coding with Codex and Claude workers.",
5
5
  "license": "MIT",
6
6
  "type": "module",