parallel-codex-tui 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.parallel-codex/config.example.toml +136 -3
- package/README.md +299 -21
- package/dist/bootstrap.js +37 -17
- package/dist/cli-args.js +34 -3
- package/dist/cli-help.js +20 -0
- package/dist/cli-startup-preflight.js +18 -0
- package/dist/cli-startup-recovery.js +82 -0
- package/dist/cli-workspace-picker.js +330 -0
- package/dist/cli-workspace-transition.js +33 -0
- package/dist/cli-workspace.js +7 -71
- package/dist/cli.js +234 -24
- package/dist/core/collaboration-timeline.js +268 -0
- package/dist/core/config-errors.js +14 -0
- package/dist/core/config.js +297 -109
- package/dist/core/file-store.js +119 -6
- package/dist/core/lease-finalization.js +22 -0
- package/dist/core/paths.js +7 -0
- package/dist/core/process-identity.js +48 -0
- package/dist/core/process-mutation-turn.js +128 -0
- package/dist/core/process-ownership.js +276 -0
- package/dist/core/process-tree.js +90 -0
- package/dist/core/router-audit.js +155 -0
- package/dist/core/router-redaction.js +31 -0
- package/dist/core/router.js +462 -35
- package/dist/core/session-index.js +412 -88
- package/dist/core/session-manager.js +1110 -40
- package/dist/core/task-session-details.js +175 -0
- package/dist/core/task-state-machine.js +18 -0
- package/dist/core/workspace-commit-recovery.js +118 -0
- package/dist/core/workspace.js +19 -11
- package/dist/doctor.js +373 -34
- package/dist/domain/schemas.js +142 -7
- package/dist/orchestrator/collaboration-channel.js +289 -6
- package/dist/orchestrator/feature-plan.js +70 -0
- package/dist/orchestrator/final-acceptance.js +86 -0
- package/dist/orchestrator/judge-artifacts.js +236 -0
- package/dist/orchestrator/orchestrator.js +2086 -203
- package/dist/orchestrator/prompts.js +168 -5
- package/dist/orchestrator/supervisor-summary.js +56 -2
- package/dist/orchestrator/workspace-sandbox.js +927 -0
- package/dist/tui/App.js +3187 -161
- package/dist/tui/AppShell.js +196 -25
- package/dist/tui/CollaborationTimelineView.js +327 -0
- package/dist/tui/FeatureBoardView.js +232 -0
- package/dist/tui/InputBar.js +581 -57
- package/dist/tui/RouterDiagnosticsView.js +469 -0
- package/dist/tui/StatusBar.js +610 -57
- package/dist/tui/StatusDetailView.js +164 -0
- package/dist/tui/TaskSessionDetailView.js +222 -0
- package/dist/tui/TaskSessionsView.js +210 -0
- package/dist/tui/TerminalOutput.js +53 -9
- package/dist/tui/WorkerOutputView.js +1404 -161
- package/dist/tui/WorkerOverviewView.js +269 -0
- package/dist/tui/chat-history.js +25 -0
- package/dist/tui/chat-input.js +67 -19
- package/dist/tui/chat-paste.js +76 -0
- package/dist/tui/display-width.js +41 -3
- package/dist/tui/incremental-text-file.js +101 -0
- package/dist/tui/keyboard.js +49 -0
- package/dist/tui/markdown-text.js +14 -0
- package/dist/tui/raw-input-decoder.js +3 -0
- package/dist/tui/scrolling.js +2 -1
- package/dist/tui/status-line.js +360 -11
- package/dist/tui/task-memory.js +13 -1
- package/dist/tui/task-result.js +105 -0
- package/dist/tui/terminal-screen.js +13 -1
- package/dist/tui/theme-contrast.js +144 -0
- package/dist/tui/theme-preview.js +109 -0
- package/dist/tui/theme.js +158 -0
- package/dist/version.js +1 -1
- package/dist/workers/capabilities.js +213 -0
- package/dist/workers/live-probe.js +177 -0
- package/dist/workers/mock-adapter.js +73 -8
- package/dist/workers/native-attach.js +106 -16
- package/dist/workers/process-adapter.js +572 -77
- package/dist/workers/provider.js +26 -0
- package/dist/workers/registry.js +12 -20
- package/package.json +11 -2
package/dist/doctor.js
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
1
2
|
import { constants } from "node:fs";
|
|
2
3
|
import { access } from "node:fs/promises";
|
|
4
|
+
import { createConnection } from "node:net";
|
|
3
5
|
import { delimiter, join } from "node:path";
|
|
4
6
|
import { prepareAppRoot } from "./core/app-root.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
+
import { formatConfigErrorMessage } from "./core/config-errors.js";
|
|
8
|
+
import { configPath, loadConfig, withUiThemeOverride } from "./core/config.js";
|
|
9
|
+
import { ensureDir, pathExists } from "./core/file-store.js";
|
|
10
|
+
import { routerRuntimeDir } from "./core/paths.js";
|
|
11
|
+
import { diagnoseRouterFailure } from "./core/router-audit.js";
|
|
12
|
+
import { routeRequestWithCodex, routerProxyConfigured } from "./core/router.js";
|
|
7
13
|
import { prepareWorkspace } from "./core/workspace.js";
|
|
8
|
-
|
|
14
|
+
import { auditTuiThemeContrast, TUI_THEME_MIN_CONTRAST_RATIO } from "./tui/theme-contrast.js";
|
|
15
|
+
import { formatTuiThemePreview } from "./tui/theme-preview.js";
|
|
16
|
+
import { resolveTuiTheme } from "./tui/theme.js";
|
|
17
|
+
import { diagnoseAgentCapabilities } from "./workers/capabilities.js";
|
|
18
|
+
import { runLiveAgentProbes } from "./workers/live-probe.js";
|
|
19
|
+
import { workerProvider } from "./workers/provider.js";
|
|
20
|
+
export async function runDoctor(appRoot, workspaceRoot, env = process.env, options = {}) {
|
|
9
21
|
const lines = ["parallel-codex-tui doctor"];
|
|
10
22
|
let ok = true;
|
|
11
23
|
await prepareAppRoot(appRoot);
|
|
@@ -15,7 +27,7 @@ export async function runDoctor(appRoot, workspaceRoot, env = process.env) {
|
|
|
15
27
|
}
|
|
16
28
|
else {
|
|
17
29
|
ok = false;
|
|
18
|
-
lines.push(`Node.js: unsupported (${process.versions.node}; need
|
|
30
|
+
lines.push(`Node.js: unsupported (${process.versions.node}; need 24.15+)`);
|
|
19
31
|
}
|
|
20
32
|
lines.push(`workspace: ok (${preparedWorkspace})`);
|
|
21
33
|
const localConfigPath = configPath(appRoot);
|
|
@@ -29,19 +41,78 @@ export async function runDoctor(appRoot, workspaceRoot, env = process.env) {
|
|
|
29
41
|
}
|
|
30
42
|
let config;
|
|
31
43
|
try {
|
|
32
|
-
|
|
44
|
+
const loadedConfig = await loadConfig(appRoot);
|
|
45
|
+
config = withUiThemeOverride(loadedConfig, options.theme ?? null);
|
|
33
46
|
lines.push(`config: ok (${localConfigPath})`);
|
|
47
|
+
lines.push(`theme: ok (${themeSummary(config.ui.theme, loadedConfig.ui.theme, options.theme ?? null)}; ${themeOverrideSummary(config.ui.colors)})`);
|
|
48
|
+
const theme = resolveTuiTheme({ theme: config.ui.theme, colors: config.ui.colors });
|
|
49
|
+
lines.push(`palette: ${themePaletteSummary(theme)}`);
|
|
50
|
+
lines.push(...formatTuiThemePreview(theme));
|
|
51
|
+
lines.push(...formatThemeContrastAudit(auditTuiThemeContrast(theme)));
|
|
34
52
|
}
|
|
35
53
|
catch (error) {
|
|
36
54
|
ok = false;
|
|
37
|
-
lines.push(`config: invalid (${localConfigPath}
|
|
55
|
+
lines.push(`config: invalid (${localConfigPath})`);
|
|
56
|
+
lines.push(...formatConfigErrorMessage(error).split("\n").map((line) => `config error: ${line}`));
|
|
38
57
|
return {
|
|
39
58
|
ok,
|
|
40
59
|
text: `${lines.join("\n")}\n`
|
|
41
60
|
};
|
|
42
61
|
}
|
|
43
|
-
|
|
62
|
+
const includeRouter = config.router.defaultMode === "auto" || options.probeRouter === true;
|
|
63
|
+
if (includeRouter) {
|
|
64
|
+
lines.push(`router retry: ${config.router.codex.maxAttempts} attempts; transient only; ${config.router.codex.retryDelayMs}ms backoff (TUI routing; live probe runs once)`);
|
|
65
|
+
lines.push(`router budget: total ${config.router.codex.timeoutMs}ms; follow-up ${config.router.codex.followUpTimeoutMs}ms; first output ${config.router.codex.firstOutputTimeoutMs}ms; idle ${config.router.codex.idleTimeoutMs}ms`);
|
|
66
|
+
}
|
|
67
|
+
const preflight = await runRuntimePreflight(config, preparedWorkspace, env, {
|
|
68
|
+
includeRouter,
|
|
69
|
+
...(options.capabilityRunner ? { capabilityRunner: options.capabilityRunner } : {}),
|
|
70
|
+
...(options.capabilityTimeoutMs ? { capabilityTimeoutMs: options.capabilityTimeoutMs } : {})
|
|
71
|
+
});
|
|
72
|
+
lines.push(...preflight.lines);
|
|
73
|
+
ok = ok && preflight.ok;
|
|
74
|
+
if (options.probeAgents) {
|
|
75
|
+
if (ok) {
|
|
76
|
+
const liveAgents = await runLiveAgentProbes(config, preparedWorkspace, configuredWorkerEngines(config), options.liveAgentProbeOptions);
|
|
77
|
+
lines.push(...liveAgents.lines);
|
|
78
|
+
ok = ok && liveAgents.ok;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
lines.push("agent live probe: skipped (preflight failed)");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
lines.push("agent live probe: not run (add --probe-agents; may use model quota)");
|
|
86
|
+
}
|
|
87
|
+
if (options.probeRouter) {
|
|
88
|
+
const probe = await runRouterProbe(config, appRoot, options.routeRunner);
|
|
89
|
+
lines.push(probe.line);
|
|
90
|
+
ok = ok && probe.ok;
|
|
91
|
+
}
|
|
92
|
+
else if (config.router.defaultMode === "auto") {
|
|
93
|
+
lines.push("router live probe: not run (add --probe-router)");
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
ok,
|
|
97
|
+
text: `${lines.join("\n")}\n`
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export async function runRuntimePreflight(config, workspaceRoot, env = process.env, options = {}) {
|
|
101
|
+
const includeRouter = options.includeRouter ?? config.router.defaultMode === "auto";
|
|
102
|
+
const lines = [];
|
|
103
|
+
let ok = true;
|
|
104
|
+
try {
|
|
105
|
+
await access(workspaceRoot, constants.R_OK | constants.W_OK | constants.X_OK);
|
|
106
|
+
lines.push("workspace permissions: ok (read/write/search)");
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
ok = false;
|
|
110
|
+
lines.push(`workspace permissions: denied (${workspaceRoot}; need read/write/search)`);
|
|
111
|
+
}
|
|
112
|
+
const availableCommands = new Set();
|
|
113
|
+
for (const command of configuredCommands(config, includeRouter)) {
|
|
44
114
|
if (await commandExists(command, env)) {
|
|
115
|
+
availableCommands.add(command);
|
|
45
116
|
lines.push(`${command}: ok`);
|
|
46
117
|
}
|
|
47
118
|
else {
|
|
@@ -49,7 +120,7 @@ export async function runDoctor(appRoot, workspaceRoot, env = process.env) {
|
|
|
49
120
|
lines.push(`${command}: missing`);
|
|
50
121
|
}
|
|
51
122
|
}
|
|
52
|
-
for (const check of
|
|
123
|
+
for (const check of configuredEnvironmentChecks(config, env, includeRouter)) {
|
|
53
124
|
if (check.ok) {
|
|
54
125
|
lines.push(`${check.label}: ok`);
|
|
55
126
|
}
|
|
@@ -58,39 +129,204 @@ export async function runDoctor(appRoot, workspaceRoot, env = process.env) {
|
|
|
58
129
|
lines.push(`${check.label}: missing env ${check.envName}`);
|
|
59
130
|
}
|
|
60
131
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
132
|
+
const capabilityDiagnostics = await diagnoseAgentCapabilities(config, env, {
|
|
133
|
+
includeRouter,
|
|
134
|
+
workerEngines: configuredWorkerEngines(config),
|
|
135
|
+
availableCommands,
|
|
136
|
+
...(options.capabilityRunner ? { runner: options.capabilityRunner } : {}),
|
|
137
|
+
...(options.capabilityTimeoutMs ? { timeoutMs: options.capabilityTimeoutMs } : {})
|
|
138
|
+
});
|
|
139
|
+
lines.push(...capabilityDiagnostics.lines);
|
|
140
|
+
ok = ok && capabilityDiagnostics.ok;
|
|
141
|
+
const systemProxy = options.systemProxy !== undefined
|
|
142
|
+
? options.systemProxy
|
|
143
|
+
: await detectMacSystemProxy();
|
|
144
|
+
const proxyDiagnostics = await diagnoseProxyEnvironment(config, env, systemProxy, options.proxyConnector ?? canConnectProxy, { includeRouter });
|
|
145
|
+
lines.push(...proxyDiagnostics.lines);
|
|
146
|
+
ok = ok && proxyDiagnostics.ok;
|
|
147
|
+
return { ok, lines };
|
|
148
|
+
}
|
|
149
|
+
export async function diagnoseProxyEnvironment(config, env, systemProxy, connect = canConnectProxy, options = {}) {
|
|
150
|
+
const contexts = [];
|
|
151
|
+
if (options.includeRouter ?? config.router.defaultMode === "auto") {
|
|
152
|
+
contexts.push({ label: "router proxy", env: config.router.codex.env, table: "router.codex.env" });
|
|
153
|
+
}
|
|
154
|
+
for (const engine of configuredWorkerEngines(config)) {
|
|
155
|
+
contexts.push({
|
|
156
|
+
label: `workers.${engine} proxy`,
|
|
157
|
+
env: workerProvider(config, engine).config.model.env,
|
|
158
|
+
table: `workers.${engine}.model.env`
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
const connectionCache = new Map();
|
|
162
|
+
const lines = [];
|
|
163
|
+
let ok = true;
|
|
164
|
+
for (const context of contexts) {
|
|
165
|
+
const endpoint = configuredProxyEndpoint(context.env, env);
|
|
166
|
+
if (!endpoint) {
|
|
167
|
+
if (systemProxy) {
|
|
168
|
+
lines.push(`${context.label}: warning (macOS system proxy ${formatProxyEndpoint(systemProxy)} is not inherited; configure [${context.table}])`);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
lines.push(`${context.label}: direct (no proxy configured)`);
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (endpoint === "invalid") {
|
|
176
|
+
ok = false;
|
|
177
|
+
lines.push(`${context.label}: invalid proxy URL`);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const key = formatProxyEndpoint(endpoint);
|
|
181
|
+
const reachable = await cachedProxyConnection(connectionCache, key, () => connect(endpoint.host, endpoint.port));
|
|
182
|
+
if (reachable) {
|
|
183
|
+
lines.push(`${context.label}: reachable (${key}; local endpoint only)`);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
ok = false;
|
|
187
|
+
lines.push(`${context.label}: unreachable (${key})`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return { ok, lines };
|
|
191
|
+
}
|
|
192
|
+
async function runRouterProbe(config, appRoot, runner) {
|
|
193
|
+
const cwd = routerRuntimeDir(appRoot, config.dataDir);
|
|
194
|
+
await ensureDir(cwd);
|
|
195
|
+
const probeConfig = {
|
|
196
|
+
...config,
|
|
197
|
+
router: {
|
|
198
|
+
...config.router,
|
|
199
|
+
defaultMode: "auto"
|
|
200
|
+
}
|
|
64
201
|
};
|
|
202
|
+
try {
|
|
203
|
+
const route = await routeRequestWithCodex("hello", probeConfig, runner, cwd);
|
|
204
|
+
if (route.source === "codex") {
|
|
205
|
+
const trace = formatRouterProbeTrace(route, false);
|
|
206
|
+
return {
|
|
207
|
+
ok: true,
|
|
208
|
+
line: `router live probe: ok (${route.mode} in ${Math.round(route.duration_ms ?? 0)}ms${trace ? `; ${trace}` : ""})`
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const trace = formatRouterProbeTrace(route, true);
|
|
212
|
+
const diagnosis = diagnoseRouterFailure({
|
|
213
|
+
...route,
|
|
214
|
+
reason: route.reason,
|
|
215
|
+
proxy_configured: routerProxyConfigured(config.router.codex.env)
|
|
216
|
+
});
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
line: `router live probe: failed (${sanitizeDiagnosticText(route.reason)}${trace ? `; ${trace}` : ""}; diagnosis ${diagnosis.summary}; next ${diagnosis.action})`
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
line: `router live probe: failed (${sanitizeDiagnosticText(message)})`
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function formatRouterProbeTrace(route, includeStage) {
|
|
231
|
+
return [
|
|
232
|
+
...(includeStage && route.router_failure_stage ? [`stage ${route.router_failure_stage}`] : []),
|
|
233
|
+
...(includeStage && route.router_timeout_kind ? [`timeout ${route.router_timeout_kind}`] : []),
|
|
234
|
+
...(typeof route.router_dispatch_ms === "number" ? [`dispatch ${Math.round(route.router_dispatch_ms)}ms`] : []),
|
|
235
|
+
...(typeof route.router_spawn_ms === "number" ? [`spawn ${Math.round(route.router_spawn_ms)}ms`] : []),
|
|
236
|
+
...formatRouterProbeFirstOutput(route),
|
|
237
|
+
...(typeof route.router_process_ms === "number" ? [`process ${Math.round(route.router_process_ms)}ms`] : []),
|
|
238
|
+
...(typeof route.router_parse_ms === "number" ? [`parse ${Math.round(route.router_parse_ms)}ms`] : []),
|
|
239
|
+
...(typeof route.duration_ms === "number" ? [`total ${Math.round(route.duration_ms)}ms`] : []),
|
|
240
|
+
...(typeof route.router_stdout_bytes === "number" ? [`stdout ${formatRouterProbeBytes(route.router_stdout_bytes)}`] : []),
|
|
241
|
+
...(typeof route.router_stderr_bytes === "number" ? [`stderr ${formatRouterProbeBytes(route.router_stderr_bytes)}`] : [])
|
|
242
|
+
].join("; ");
|
|
243
|
+
}
|
|
244
|
+
function formatRouterProbeFirstOutput(route) {
|
|
245
|
+
const streams = [
|
|
246
|
+
...(typeof route.router_first_stdout_ms === "number"
|
|
247
|
+
? [{ at: route.router_first_stdout_ms, text: `first stdout ${Math.round(route.router_first_stdout_ms)}ms` }]
|
|
248
|
+
: []),
|
|
249
|
+
...(typeof route.router_first_stderr_ms === "number"
|
|
250
|
+
? [{ at: route.router_first_stderr_ms, text: `first stderr ${Math.round(route.router_first_stderr_ms)}ms` }]
|
|
251
|
+
: [])
|
|
252
|
+
];
|
|
253
|
+
if (streams.length > 0) {
|
|
254
|
+
return streams.sort((left, right) => left.at - right.at).map((stream) => stream.text);
|
|
255
|
+
}
|
|
256
|
+
if (typeof route.router_first_output_ms === "number") {
|
|
257
|
+
return [`first output ${Math.round(route.router_first_output_ms)}ms`];
|
|
258
|
+
}
|
|
259
|
+
return route.router_failure_stage === "waiting-output" ? ["first output none"] : [];
|
|
260
|
+
}
|
|
261
|
+
function formatRouterProbeBytes(bytes) {
|
|
262
|
+
if (bytes < 1024) {
|
|
263
|
+
return `${Math.round(bytes)}B`;
|
|
264
|
+
}
|
|
265
|
+
return `${(bytes / 1024).toFixed(bytes < 10 * 1024 ? 1 : 0)}KB`;
|
|
266
|
+
}
|
|
267
|
+
function sanitizeDiagnosticText(value) {
|
|
268
|
+
return value
|
|
269
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
270
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
271
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ")
|
|
272
|
+
.replace(/\b([a-z][a-z0-9+.-]*:\/\/)([^@\s/]+)@/gi, "$1***@")
|
|
273
|
+
.replace(/\b(api[-_\s]?key|token|authorization)(\s*[:=]\s*)(\S+)/gi, "$1$2***")
|
|
274
|
+
.replace(/\s+/g, " ")
|
|
275
|
+
.trim()
|
|
276
|
+
.slice(0, 400) || "unknown error";
|
|
65
277
|
}
|
|
66
278
|
export function isSupportedNodeVersion(version) {
|
|
67
279
|
const [majorRaw = "0", minorRaw = "0"] = version.split(".");
|
|
68
280
|
const major = Number.parseInt(majorRaw, 10);
|
|
69
281
|
const minor = Number.parseInt(minorRaw, 10);
|
|
70
|
-
return major >=
|
|
282
|
+
return major > 24 || (major === 24 && minor >= 15);
|
|
71
283
|
}
|
|
72
|
-
function configuredCommands(config) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
284
|
+
function configuredCommands(config, includeRouter) {
|
|
285
|
+
const commands = includeRouter ? [config.router.codex.command] : [];
|
|
286
|
+
for (const engine of configuredWorkerEngines(config)) {
|
|
287
|
+
const worker = config.workers[engine];
|
|
288
|
+
commands.push(worker.command);
|
|
289
|
+
if (worker.nativeSession.enabled) {
|
|
290
|
+
commands.push(worker.interactive.command);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return [...new Set(commands.filter((command) => command && command !== "mock"))];
|
|
76
294
|
}
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
295
|
+
function themeOverrideSummary(colors) {
|
|
296
|
+
const entries = Object.entries(colors).sort(([left], [right]) => left.localeCompare(right));
|
|
297
|
+
if (entries.length === 0) {
|
|
298
|
+
return "no color overrides";
|
|
80
299
|
}
|
|
81
|
-
|
|
82
|
-
|
|
300
|
+
return `colors: ${entries.map(([key, value]) => `${key}=${value}`).join(", ")}`;
|
|
301
|
+
}
|
|
302
|
+
function themePaletteSummary(theme) {
|
|
303
|
+
return [
|
|
304
|
+
`chrome=${theme.chrome}`,
|
|
305
|
+
`surface=${theme.surface}`,
|
|
306
|
+
`rail=${theme.rail}`,
|
|
307
|
+
`accent=${theme.accent}`
|
|
308
|
+
].join(", ");
|
|
309
|
+
}
|
|
310
|
+
function formatThemeContrastAudit(audit) {
|
|
311
|
+
if (audit.issues.length === 0) {
|
|
312
|
+
return [`theme contrast: ok (minimum ${formatContrastRatio(audit.minimumRatio)}:1 across ${audit.measurements.length} rendered pairs)`];
|
|
83
313
|
}
|
|
84
|
-
|
|
85
|
-
|
|
314
|
+
return [
|
|
315
|
+
`theme contrast: warning (${audit.issues.length} of ${audit.measurements.length} rendered pairs below ${TUI_THEME_MIN_CONTRAST_RATIO}:1)`,
|
|
316
|
+
...audit.issues.map(({ foreground, background, ratio }) => `theme contrast issue: ${foreground}/${background} ${formatContrastRatio(ratio)}:1`)
|
|
317
|
+
];
|
|
318
|
+
}
|
|
319
|
+
function formatContrastRatio(ratio) {
|
|
320
|
+
return ratio.toFixed(2);
|
|
321
|
+
}
|
|
322
|
+
function themeSummary(effectiveTheme, configTheme, cliTheme) {
|
|
323
|
+
if (cliTheme && cliTheme !== configTheme) {
|
|
324
|
+
return `${effectiveTheme} via --theme; config ${configTheme}`;
|
|
86
325
|
}
|
|
87
|
-
return
|
|
326
|
+
return effectiveTheme;
|
|
88
327
|
}
|
|
89
|
-
function
|
|
328
|
+
function configuredWorkerEngines(config) {
|
|
90
329
|
const engines = new Set();
|
|
91
|
-
if (config.router.defaultMode === "auto" && options.includeRouter) {
|
|
92
|
-
engines.add("router-codex");
|
|
93
|
-
}
|
|
94
330
|
if (config.router.defaultMode === "auto") {
|
|
95
331
|
engines.add(config.pairing.main);
|
|
96
332
|
engines.add(config.pairing.judge);
|
|
@@ -105,15 +341,23 @@ function configuredEngines(config, options) {
|
|
|
105
341
|
engines.add(config.pairing.actor);
|
|
106
342
|
engines.add(config.pairing.critic);
|
|
107
343
|
}
|
|
108
|
-
return
|
|
344
|
+
return [...engines].filter((engine) => engine !== "mock");
|
|
109
345
|
}
|
|
110
|
-
function
|
|
111
|
-
return configuredEngines(config, { includeRouter: false }).filter((engine) => engine === "codex" || engine === "claude");
|
|
112
|
-
}
|
|
113
|
-
function configuredWorkerModelEnvChecks(config, env) {
|
|
346
|
+
function configuredEnvironmentChecks(config, env, includeRouter) {
|
|
114
347
|
const checks = [];
|
|
348
|
+
if (includeRouter) {
|
|
349
|
+
for (const [key, value] of Object.entries(config.router.codex.env)) {
|
|
350
|
+
for (const envName of referencedEnvNames(value)) {
|
|
351
|
+
checks.push({
|
|
352
|
+
label: `router.codex.env.${key}`,
|
|
353
|
+
envName,
|
|
354
|
+
ok: Boolean(env[envName])
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
115
359
|
for (const engine of configuredWorkerEngines(config)) {
|
|
116
|
-
const worker = engine
|
|
360
|
+
const worker = workerProvider(config, engine).config;
|
|
117
361
|
for (const [key, value] of Object.entries(worker.model.env)) {
|
|
118
362
|
for (const envName of referencedEnvNames(value)) {
|
|
119
363
|
checks.push({
|
|
@@ -153,3 +397,98 @@ async function canExecute(path) {
|
|
|
153
397
|
return false;
|
|
154
398
|
}
|
|
155
399
|
}
|
|
400
|
+
function configuredProxyEndpoint(configured, processEnvironment) {
|
|
401
|
+
const effective = {
|
|
402
|
+
...processEnvironment,
|
|
403
|
+
...Object.fromEntries(Object.entries(configured).map(([name, value]) => [name, renderEnvironmentValue(value, processEnvironment)]))
|
|
404
|
+
};
|
|
405
|
+
const value = [
|
|
406
|
+
effective.HTTPS_PROXY,
|
|
407
|
+
effective.https_proxy,
|
|
408
|
+
effective.ALL_PROXY,
|
|
409
|
+
effective.all_proxy,
|
|
410
|
+
effective.HTTP_PROXY,
|
|
411
|
+
effective.http_proxy
|
|
412
|
+
].find((candidate) => candidate?.trim())?.trim();
|
|
413
|
+
if (!value) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
const url = new URL(value.includes("://") ? value : `http://${value}`);
|
|
418
|
+
const port = url.port ? Number(url.port) : defaultProxyPort(url.protocol);
|
|
419
|
+
if (!url.hostname || !Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
420
|
+
return "invalid";
|
|
421
|
+
}
|
|
422
|
+
return { host: url.hostname, port };
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
return "invalid";
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function renderEnvironmentValue(value, env) {
|
|
429
|
+
return value.replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => env[name] ?? "");
|
|
430
|
+
}
|
|
431
|
+
function defaultProxyPort(protocol) {
|
|
432
|
+
if (protocol === "https:") {
|
|
433
|
+
return 443;
|
|
434
|
+
}
|
|
435
|
+
if (protocol.startsWith("socks")) {
|
|
436
|
+
return 1080;
|
|
437
|
+
}
|
|
438
|
+
return 80;
|
|
439
|
+
}
|
|
440
|
+
function formatProxyEndpoint(endpoint) {
|
|
441
|
+
const host = endpoint.host.includes(":") ? `[${endpoint.host}]` : endpoint.host;
|
|
442
|
+
return `${host}:${endpoint.port}`;
|
|
443
|
+
}
|
|
444
|
+
async function cachedProxyConnection(cache, key, connect) {
|
|
445
|
+
const existing = cache.get(key);
|
|
446
|
+
if (existing) {
|
|
447
|
+
return existing;
|
|
448
|
+
}
|
|
449
|
+
const pending = connect();
|
|
450
|
+
cache.set(key, pending);
|
|
451
|
+
return pending;
|
|
452
|
+
}
|
|
453
|
+
async function detectMacSystemProxy() {
|
|
454
|
+
if (process.platform !== "darwin") {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
const output = await execFileText("scutil", ["--proxy"]);
|
|
458
|
+
if (!output) {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
const https = parseScutilProxy(output, "HTTPS");
|
|
462
|
+
return https ?? parseScutilProxy(output, "HTTP");
|
|
463
|
+
}
|
|
464
|
+
function parseScutilProxy(output, prefix) {
|
|
465
|
+
const enabled = output.match(new RegExp(`${prefix}Enable\\s*:\\s*(\\d+)`))?.[1] === "1";
|
|
466
|
+
const host = output.match(new RegExp(`${prefix}Proxy\\s*:\\s*(\\S+)`))?.[1];
|
|
467
|
+
const port = Number(output.match(new RegExp(`${prefix}Port\\s*:\\s*(\\d+)`))?.[1]);
|
|
468
|
+
return enabled && host && Number.isInteger(port) && port > 0 ? { host, port } : null;
|
|
469
|
+
}
|
|
470
|
+
function execFileText(command, args) {
|
|
471
|
+
return new Promise((resolve) => {
|
|
472
|
+
execFile(command, args, { timeout: 1000 }, (error, stdout) => {
|
|
473
|
+
resolve(error ? "" : stdout);
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
function canConnectProxy(host, port) {
|
|
478
|
+
return new Promise((resolve) => {
|
|
479
|
+
const socket = createConnection({ host, port });
|
|
480
|
+
let settled = false;
|
|
481
|
+
const finish = (reachable) => {
|
|
482
|
+
if (settled) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
settled = true;
|
|
486
|
+
socket.destroy();
|
|
487
|
+
resolve(reachable);
|
|
488
|
+
};
|
|
489
|
+
socket.setTimeout(750);
|
|
490
|
+
socket.once("connect", () => finish(true));
|
|
491
|
+
socket.once("error", () => finish(false));
|
|
492
|
+
socket.once("timeout", () => finish(false));
|
|
493
|
+
});
|
|
494
|
+
}
|