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.
Files changed (78) hide show
  1. package/.parallel-codex/config.example.toml +136 -3
  2. package/README.md +299 -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-preflight.js +18 -0
  7. package/dist/cli-startup-recovery.js +82 -0
  8. package/dist/cli-workspace-picker.js +330 -0
  9. package/dist/cli-workspace-transition.js +33 -0
  10. package/dist/cli-workspace.js +7 -71
  11. package/dist/cli.js +234 -24
  12. package/dist/core/collaboration-timeline.js +268 -0
  13. package/dist/core/config-errors.js +14 -0
  14. package/dist/core/config.js +297 -109
  15. package/dist/core/file-store.js +119 -6
  16. package/dist/core/lease-finalization.js +22 -0
  17. package/dist/core/paths.js +7 -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 +462 -35
  25. package/dist/core/session-index.js +412 -88
  26. package/dist/core/session-manager.js +1110 -40
  27. package/dist/core/task-session-details.js +175 -0
  28. package/dist/core/task-state-machine.js +18 -0
  29. package/dist/core/workspace-commit-recovery.js +118 -0
  30. package/dist/core/workspace.js +19 -11
  31. package/dist/doctor.js +373 -34
  32. package/dist/domain/schemas.js +142 -7
  33. package/dist/orchestrator/collaboration-channel.js +289 -6
  34. package/dist/orchestrator/feature-plan.js +70 -0
  35. package/dist/orchestrator/final-acceptance.js +86 -0
  36. package/dist/orchestrator/judge-artifacts.js +236 -0
  37. package/dist/orchestrator/orchestrator.js +2086 -203
  38. package/dist/orchestrator/prompts.js +168 -5
  39. package/dist/orchestrator/supervisor-summary.js +56 -2
  40. package/dist/orchestrator/workspace-sandbox.js +927 -0
  41. package/dist/tui/App.js +3187 -161
  42. package/dist/tui/AppShell.js +196 -25
  43. package/dist/tui/CollaborationTimelineView.js +327 -0
  44. package/dist/tui/FeatureBoardView.js +232 -0
  45. package/dist/tui/InputBar.js +581 -57
  46. package/dist/tui/RouterDiagnosticsView.js +469 -0
  47. package/dist/tui/StatusBar.js +610 -57
  48. package/dist/tui/StatusDetailView.js +164 -0
  49. package/dist/tui/TaskSessionDetailView.js +222 -0
  50. package/dist/tui/TaskSessionsView.js +210 -0
  51. package/dist/tui/TerminalOutput.js +53 -9
  52. package/dist/tui/WorkerOutputView.js +1404 -161
  53. package/dist/tui/WorkerOverviewView.js +269 -0
  54. package/dist/tui/chat-history.js +25 -0
  55. package/dist/tui/chat-input.js +67 -19
  56. package/dist/tui/chat-paste.js +76 -0
  57. package/dist/tui/display-width.js +41 -3
  58. package/dist/tui/incremental-text-file.js +101 -0
  59. package/dist/tui/keyboard.js +49 -0
  60. package/dist/tui/markdown-text.js +14 -0
  61. package/dist/tui/raw-input-decoder.js +3 -0
  62. package/dist/tui/scrolling.js +2 -1
  63. package/dist/tui/status-line.js +360 -11
  64. package/dist/tui/task-memory.js +13 -1
  65. package/dist/tui/task-result.js +105 -0
  66. package/dist/tui/terminal-screen.js +13 -1
  67. package/dist/tui/theme-contrast.js +144 -0
  68. package/dist/tui/theme-preview.js +109 -0
  69. package/dist/tui/theme.js +158 -0
  70. package/dist/version.js +1 -1
  71. package/dist/workers/capabilities.js +213 -0
  72. package/dist/workers/live-probe.js +177 -0
  73. package/dist/workers/mock-adapter.js +73 -8
  74. package/dist/workers/native-attach.js +106 -16
  75. package/dist/workers/process-adapter.js +572 -77
  76. package/dist/workers/provider.js +26 -0
  77. package/dist/workers/registry.js +12 -20
  78. package/package.json +11 -2
package/dist/cli-args.js CHANGED
@@ -1,29 +1,40 @@
1
1
  import { resolve } from "node:path";
2
2
  import { homedir } from "node:os";
3
- const allowedValueOptions = new Set(["--app-root", "--workspace", "-w", "--task", "-t"]);
4
- const allowedBooleanOptions = new Set(["--doctor", "--help", "-h", "--init", "--version", "-v"]);
3
+ import { normalizeTuiThemeName, TUI_THEME_NAMES } from "./tui/theme.js";
4
+ import { TaskIdSchema } from "./domain/schemas.js";
5
+ const allowedValueOptions = new Set(["--app-root", "--workspace", "-w", "--task", "-t", "--theme"]);
6
+ const allowedBooleanOptions = new Set(["--doctor", "--help", "-h", "--init", "--probe-agents", "--probe-router", "--themes", "--version", "-v"]);
5
7
  export function parseCliArgs(args, cwd) {
6
8
  const optionArgs = argsBeforeTerminator(args);
7
9
  const appRootFlagIndex = lastFlagIndex(optionArgs, (arg) => arg === "--app-root" || arg.startsWith("--app-root="));
8
10
  const workspaceFlagIndex = lastFlagIndex(optionArgs, (arg) => arg === "--workspace" || arg.startsWith("--workspace=") || arg === "-w" || arg.startsWith("-w="));
9
11
  const taskFlagIndex = lastFlagIndex(optionArgs, (arg) => arg === "--task" || arg.startsWith("--task=") || arg === "-t" || arg.startsWith("-t="));
12
+ const themeFlagIndex = lastFlagIndex(optionArgs, (arg) => arg === "--theme" || arg.startsWith("--theme="));
10
13
  const doctor = optionArgs.includes("--doctor");
11
14
  const help = optionArgs.includes("--help") || optionArgs.includes("-h");
12
15
  const init = optionArgs.includes("--init");
16
+ const probeAgents = optionArgs.includes("--probe-agents");
17
+ const probeRouter = optionArgs.includes("--probe-router");
18
+ const themes = optionArgs.includes("--themes");
13
19
  const version = optionArgs.includes("--version") || optionArgs.includes("-v");
14
20
  const appRootValue = flagValue(optionArgs, appRootFlagIndex);
15
21
  const appRoot = appRootValue ? resolvePathArg(cwd, appRootValue) : cwd;
16
22
  const explicitWorkspace = flagValue(optionArgs, workspaceFlagIndex);
17
23
  const workspaceRoot = explicitWorkspace ? resolvePathArg(cwd, explicitWorkspace) : cwd;
18
24
  const taskId = flagValue(optionArgs, taskFlagIndex);
25
+ const theme = cliThemeValue(flagValue(optionArgs, themeFlagIndex));
19
26
  return {
20
27
  appRoot,
21
28
  doctor,
22
29
  explicitWorkspace,
23
30
  help,
24
31
  init,
32
+ probeAgents,
33
+ probeRouter,
25
34
  workspaceRoot,
26
35
  taskId,
36
+ theme,
37
+ themes,
27
38
  version
28
39
  };
29
40
  }
@@ -38,7 +49,8 @@ function resolvePathArg(cwd, value) {
38
49
  }
39
50
  export function validateCliArgs(args) {
40
51
  const errors = [];
41
- for (const arg of argsBeforeTerminator(args)) {
52
+ const optionArgs = argsBeforeTerminator(args);
53
+ for (const arg of optionArgs) {
42
54
  if (!arg.startsWith("-") || arg === "-") {
43
55
  continue;
44
56
  }
@@ -52,6 +64,22 @@ export function validateCliArgs(args) {
52
64
  }
53
65
  errors.push(`Unknown option: ${arg}`);
54
66
  }
67
+ const themeFlagIndex = lastFlagIndex(optionArgs, (arg) => arg === "--theme" || arg.startsWith("--theme="));
68
+ const rawThemeValue = flagValue(optionArgs, themeFlagIndex);
69
+ if (rawThemeValue?.trim() && !normalizeTuiThemeName(rawThemeValue)) {
70
+ errors.push(`Invalid --theme: ${rawThemeValue.trim()} (expected ${TUI_THEME_NAMES.join(", ")})`);
71
+ }
72
+ const taskFlagIndex = lastFlagIndex(optionArgs, (arg) => arg === "--task" || arg.startsWith("--task=") || arg === "-t" || arg.startsWith("-t="));
73
+ const rawTaskId = flagValue(optionArgs, taskFlagIndex);
74
+ if (rawTaskId && !TaskIdSchema.safeParse(rawTaskId).success) {
75
+ errors.push("Invalid --task: expected task- followed by letters, numbers, dot, underscore, or hyphen");
76
+ }
77
+ if (optionArgs.includes("--probe-router") && !optionArgs.includes("--doctor")) {
78
+ errors.push("--probe-router requires --doctor");
79
+ }
80
+ if (optionArgs.includes("--probe-agents") && !optionArgs.includes("--doctor")) {
81
+ errors.push("--probe-agents requires --doctor");
82
+ }
55
83
  return errors;
56
84
  }
57
85
  function argsBeforeTerminator(args) {
@@ -75,3 +103,6 @@ function flagValue(args, flagIndex) {
75
103
  const value = flagIndex >= 0 ? args[flagIndex + 1] : null;
76
104
  return value && !value.startsWith("-") ? value : null;
77
105
  }
106
+ function cliThemeValue(value) {
107
+ return normalizeTuiThemeName(value);
108
+ }
@@ -0,0 +1,20 @@
1
+ import { TUI_THEME_NAMES } from "./tui/theme.js";
2
+ export function buildCliHelpText(themeNames = TUI_THEME_NAMES) {
3
+ return `Usage: parallel-codex-tui [options]
4
+
5
+ Options:
6
+ -w, --workspace <path> Project workspace for worker sessions and edits
7
+ --app-root <path> App root for configuration lookup
8
+ -t, --task <id> Open an existing task session
9
+ --theme <name> Temporarily use a TUI theme: ${themeNames.join(", ")}
10
+ --themes List built-in TUI theme palettes; combine with --theme to filter
11
+ --init Write .parallel-codex/config.toml if missing
12
+ --doctor Check config, agent commands, and theme palette preview
13
+ --probe-agents With --doctor, run fresh + resume probes (uses model quota)
14
+ --probe-router With --doctor, run one live Codex Router request
15
+ -v, --version Print the current version
16
+ -h, --help Print this help message
17
+
18
+ Options with values also accept --name=value and -x=value forms.`;
19
+ }
20
+ export const helpText = buildCliHelpText();
@@ -0,0 +1,18 @@
1
+ export function startupPreflightMessages(preflight) {
2
+ if (preflight.ok) {
3
+ return [];
4
+ }
5
+ const issues = preflight.lines.filter(isStartupPreflightIssue);
6
+ if (issues.length === 0) {
7
+ return [];
8
+ }
9
+ const summary = issues.slice(0, 4).join(" · ");
10
+ const remainder = issues.length > 4 ? ` · ${issues.length - 4} more` : "";
11
+ return [{
12
+ from: "system",
13
+ text: `Startup preflight needs attention · ${summary}${remainder} · run parallel-codex-tui --doctor before starting workers`
14
+ }];
15
+ }
16
+ function isStartupPreflightIssue(line) {
17
+ return /(?:^|: )(?:warning\b|missing\b|incompatible\b|unreachable\b|invalid\b|denied\b|failed\b)/i.test(line);
18
+ }
@@ -0,0 +1,82 @@
1
+ export function startupRecoveryMessages(recoveredTasks, activeTaskId, pendingTaskCreations, sessionIndexRecovery) {
2
+ return [
3
+ ...sessionIndexRecoveryMessages(sessionIndexRecovery),
4
+ ...interruptedTaskRecoveryMessages(recoveredTasks, activeTaskId),
5
+ ...pendingTaskCreationMessages(pendingTaskCreations)
6
+ ];
7
+ }
8
+ function sessionIndexRecoveryMessages(recovery) {
9
+ if (!recovery) {
10
+ return [];
11
+ }
12
+ return [{
13
+ from: "system",
14
+ text: recovery.source === "backup"
15
+ ? `Recovered session catalog from the last healthy SQLite backup · task files reindexed · corrupt copy kept at ${recovery.quarantinedPath}`
16
+ : `Rebuilt session catalog from task files after SQLite integrity failure · corrupt copy kept at ${recovery.quarantinedPath}`
17
+ }];
18
+ }
19
+ function interruptedTaskRecoveryMessages(recoveredTasks, activeTaskId) {
20
+ if (recoveredTasks.length === 0) {
21
+ return [];
22
+ }
23
+ const active = activeTaskId
24
+ ? recoveredTasks.find((recovery) => recovery.taskId === activeTaskId)
25
+ : null;
26
+ if (active) {
27
+ const restoredTurns = restoredTurnCount(active);
28
+ const archived = archivedTurnDetail(active);
29
+ if (restoredTurns > 0) {
30
+ return [{
31
+ from: "system",
32
+ text: `Recovered ${restoredTurns === 1 ? "follow-up turn" : `${restoredTurns} follow-up turns`} #${compactStartupTaskId(active.taskId)} · request and route kept${archived} · checkpoints kept · Ctrl+R resume`
33
+ }];
34
+ }
35
+ if (active.previousState === "done") {
36
+ return [{
37
+ from: "system",
38
+ text: `Recovered incomplete task #${compactStartupTaskId(active.taskId)} · completion evidence missing${archived} · checkpoints kept · Ctrl+R rebuild`
39
+ }];
40
+ }
41
+ const workerLabel = `${active.workersRecovered} ${active.workersRecovered === 1 ? "worker" : "workers"}`;
42
+ return [{
43
+ from: "system",
44
+ text: `Recovered interrupted task #${compactStartupTaskId(active.taskId)} · ${workerLabel} stopped${archived} · checkpoints kept · Ctrl+R resume`
45
+ }];
46
+ }
47
+ const restoredTurns = recoveredTasks.reduce((total, recovery) => total + restoredTurnCount(recovery), 0);
48
+ const turnDetail = restoredTurns > 0
49
+ ? ` · ${restoredTurns} ${restoredTurns === 1 ? "turn" : "turns"} restored`
50
+ : "";
51
+ return [{
52
+ from: "system",
53
+ text: `Recovered ${recoveredTasks.length} interrupted ${recoveredTasks.length === 1 ? "task" : "tasks"}${turnDetail} · checkpoints kept · Ctrl+T inspect`
54
+ }];
55
+ }
56
+ function pendingTaskCreationMessages(recovery) {
57
+ if (!recovery || (recovery.abandoned === 0 && recovery.active === 0)) {
58
+ return [];
59
+ }
60
+ const details = [];
61
+ if (recovery.abandoned > 0) {
62
+ details.push(`${recovery.abandoned} incomplete task ${recovery.abandoned === 1 ? "creation" : "creations"} archived`);
63
+ }
64
+ if (recovery.active > 0) {
65
+ details.push(`${recovery.active} task ${recovery.active === 1 ? "creation" : "creations"} active in another TUI`);
66
+ }
67
+ return [{
68
+ from: "system",
69
+ text: `Startup cleanup · ${details.join(" · ")}`
70
+ }];
71
+ }
72
+ function restoredTurnCount(recovery) {
73
+ return (recovery.turnsPublished ?? 0) + (recovery.turnsRepaired ?? 0);
74
+ }
75
+ function archivedTurnDetail(recovery) {
76
+ const count = recovery.turnsAbandoned ?? 0;
77
+ return count > 0 ? ` · ${count} ${count === 1 ? "fragment" : "fragments"} archived` : "";
78
+ }
79
+ function compactStartupTaskId(taskId) {
80
+ const parts = taskId.split("-");
81
+ return parts.length > 2 ? parts.slice(-2).join("-") : taskId;
82
+ }
@@ -0,0 +1,330 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { basename } from "node:path";
4
+ import { Box, Text, render, useInput } from "ink";
5
+ import { resolveWorkspacePath } from "./core/workspace.js";
6
+ import { compactEndByDisplayWidth, compactTailByDisplayWidth, displayWidth } from "./tui/display-width.js";
7
+ import { TUI_THEME } from "./tui/theme.js";
8
+ const WORKSPACE_SHORTCUT_DELAY_MS = 500;
9
+ export class WorkspaceSelectionCancelledError extends Error {
10
+ constructor() {
11
+ super("Workspace selection cancelled.");
12
+ this.name = "WorkspaceSelectionCancelledError";
13
+ }
14
+ }
15
+ export async function promptForWorkspaceTui(input) {
16
+ let resolveSelection = () => undefined;
17
+ let rejectSelection = () => undefined;
18
+ const selection = new Promise((resolve, reject) => {
19
+ resolveSelection = resolve;
20
+ rejectSelection = reject;
21
+ });
22
+ const cancelSelection = () => rejectSelection(new WorkspaceSelectionCancelledError());
23
+ let instance = null;
24
+ process.on("SIGINT", cancelSelection);
25
+ try {
26
+ instance = render(_jsx(WorkspacePicker, { cwd: input.cwd, choices: input.choices, invalidExplicitWorkspace: input.invalidExplicitWorkspace, terminalHeight: input.stdout.rows ?? 24, terminalWidth: input.stdout.columns ?? 80, onCancel: cancelSelection, onSelect: resolveSelection }), {
27
+ stdin: input.stdin,
28
+ stdout: input.stdout,
29
+ stderr: input.stdout,
30
+ exitOnCtrlC: false,
31
+ patchConsole: false
32
+ });
33
+ void instance.waitUntilExit().catch((error) => {
34
+ rejectSelection(error instanceof Error ? error : new Error(String(error)));
35
+ });
36
+ return await selection;
37
+ }
38
+ finally {
39
+ process.off("SIGINT", cancelSelection);
40
+ instance?.clear();
41
+ instance?.unmount();
42
+ }
43
+ }
44
+ export function WorkspacePicker({ cwd, choices, invalidExplicitWorkspace, terminalHeight, terminalWidth, onCancel, onSelect }) {
45
+ const options = useMemo(() => workspacePickerOptions(choices, invalidExplicitWorkspace), [choices, invalidExplicitWorkspace]);
46
+ const defaultWorkspace = invalidExplicitWorkspace?.reason === "missing"
47
+ ? invalidExplicitWorkspace.path
48
+ : null;
49
+ const [mode, setMode] = useState(options.some((option) => option.kind === "workspace") ? "list" : "path");
50
+ const [selectedIndex, setSelectedIndex] = useState(0);
51
+ const [pathValue, setPathValue] = useState("");
52
+ const [openingPath, setOpeningPath] = useState(null);
53
+ const pathValueRef = useRef("");
54
+ const settledRef = useRef(false);
55
+ const shortcutBufferRef = useRef("");
56
+ const shortcutTimerRef = useRef(null);
57
+ const width = Math.max(1, terminalWidth - 1);
58
+ const visibleRows = Math.max(1, Math.min(9, terminalHeight - (invalidExplicitWorkspace ? 7 : 6)));
59
+ const visible = workspacePickerWindow(options, selectedIndex, visibleRows);
60
+ const contentRows = 1
61
+ + (invalidExplicitWorkspace ? 1 : 0)
62
+ + 1
63
+ + (mode === "list" ? visible.items.length + 1 : 2);
64
+ const trailingRows = Math.max(0, terminalHeight - contentRows);
65
+ useEffect(() => () => {
66
+ if (shortcutTimerRef.current) {
67
+ clearTimeout(shortcutTimerRef.current);
68
+ }
69
+ }, []);
70
+ function clearShortcutBuffer() {
71
+ shortcutBufferRef.current = "";
72
+ if (shortcutTimerRef.current) {
73
+ clearTimeout(shortcutTimerRef.current);
74
+ shortcutTimerRef.current = null;
75
+ }
76
+ }
77
+ function finish(value) {
78
+ if (settledRef.current) {
79
+ return;
80
+ }
81
+ settledRef.current = true;
82
+ clearShortcutBuffer();
83
+ setOpeningPath(value);
84
+ onSelect(value);
85
+ }
86
+ function replacePathValue(value) {
87
+ pathValueRef.current = value;
88
+ setPathValue(value);
89
+ }
90
+ function submitPath(value) {
91
+ const requested = value.trim() || defaultWorkspace || cwd;
92
+ finish(resolveWorkspacePath(cwd, requested));
93
+ }
94
+ function openOption(option) {
95
+ clearShortcutBuffer();
96
+ if (!option || option.kind === "new" || !option.path) {
97
+ replacePathValue("");
98
+ setMode("path");
99
+ return;
100
+ }
101
+ finish(option.path);
102
+ }
103
+ function selectNumericShortcut(digits) {
104
+ const candidate = `${shortcutBufferRef.current}${digits}`;
105
+ const exactIndex = options.findIndex((option) => option.shortcut === candidate);
106
+ const hasLonger = options.some((option) => (option.shortcut.length > candidate.length && option.shortcut.startsWith(candidate)));
107
+ if (exactIndex < 0 && !hasLonger) {
108
+ clearShortcutBuffer();
109
+ return;
110
+ }
111
+ shortcutBufferRef.current = candidate;
112
+ if (exactIndex >= 0) {
113
+ setSelectedIndex(exactIndex);
114
+ }
115
+ if (exactIndex >= 0 && !hasLonger) {
116
+ openOption(options[exactIndex]);
117
+ return;
118
+ }
119
+ if (shortcutTimerRef.current) {
120
+ clearTimeout(shortcutTimerRef.current);
121
+ }
122
+ shortcutTimerRef.current = setTimeout(() => {
123
+ const pending = shortcutBufferRef.current;
124
+ clearShortcutBuffer();
125
+ openOption(options.find((option) => option.shortcut === pending));
126
+ }, WORKSPACE_SHORTCUT_DELAY_MS);
127
+ }
128
+ useInput((input, key) => {
129
+ if (settledRef.current) {
130
+ return;
131
+ }
132
+ const inputHasReturn = /[\r\n]/.test(input);
133
+ const printableInput = input.replace(/[\u0000-\u001f\u007f]/g, "");
134
+ if (key.ctrl && input.toLowerCase() === "c") {
135
+ if (!settledRef.current) {
136
+ settledRef.current = true;
137
+ onCancel();
138
+ }
139
+ return;
140
+ }
141
+ if (mode === "path") {
142
+ clearShortcutBuffer();
143
+ if (key.escape) {
144
+ if (options.some((option) => option.kind === "workspace")) {
145
+ setMode("list");
146
+ }
147
+ else {
148
+ onCancel();
149
+ }
150
+ return;
151
+ }
152
+ if (key.return) {
153
+ submitPath(pathValueRef.current);
154
+ return;
155
+ }
156
+ if (key.backspace || key.delete) {
157
+ replacePathValue(Array.from(pathValueRef.current).slice(0, -1).join(""));
158
+ return;
159
+ }
160
+ if (key.ctrl && input.toLowerCase() === "u") {
161
+ replacePathValue("");
162
+ return;
163
+ }
164
+ if (!key.ctrl && !key.meta) {
165
+ const nextValue = `${pathValueRef.current}${printableInput}`;
166
+ if (inputHasReturn) {
167
+ submitPath(nextValue);
168
+ }
169
+ else if (printableInput) {
170
+ replacePathValue(nextValue);
171
+ }
172
+ }
173
+ return;
174
+ }
175
+ if (key.escape) {
176
+ clearShortcutBuffer();
177
+ onCancel();
178
+ return;
179
+ }
180
+ if (key.upArrow || (key.tab && key.shift)) {
181
+ clearShortcutBuffer();
182
+ setSelectedIndex((current) => (current - 1 + options.length) % options.length);
183
+ return;
184
+ }
185
+ if (key.downArrow || key.tab) {
186
+ clearShortcutBuffer();
187
+ setSelectedIndex((current) => (current + 1) % options.length);
188
+ return;
189
+ }
190
+ if (key.return) {
191
+ const pending = shortcutBufferRef.current;
192
+ if (pending) {
193
+ clearShortcutBuffer();
194
+ openOption(options.find((option) => option.shortcut === pending));
195
+ return;
196
+ }
197
+ openOption(options[selectedIndex]);
198
+ return;
199
+ }
200
+ const newPath = printableInput.match(/^n(?:ew)?\s+(.+)$/i)?.[1]?.trim();
201
+ if (newPath) {
202
+ submitPath(newPath);
203
+ return;
204
+ }
205
+ if (/^n(?:ew)?$/i.test(printableInput)) {
206
+ clearShortcutBuffer();
207
+ replacePathValue("");
208
+ setMode("path");
209
+ return;
210
+ }
211
+ if (/^\d+$/.test(printableInput)) {
212
+ selectNumericShortcut(printableInput);
213
+ return;
214
+ }
215
+ if (printableInput) {
216
+ clearShortcutBuffer();
217
+ if (inputHasReturn) {
218
+ submitPath(printableInput);
219
+ }
220
+ else {
221
+ replacePathValue(printableInput);
222
+ setMode("path");
223
+ }
224
+ }
225
+ });
226
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(WorkspacePickerHeader, { width: width }), invalidExplicitWorkspace ? (_jsx(WorkspacePickerNotice, { invalid: invalidExplicitWorkspace, width: width })) : null, _jsx(WorkspacePickerTitle, { count: choices.length, mode: mode, openingPath: openingPath, width: width }), mode === "list" ? (_jsxs(_Fragment, { children: [visible.items.map(({ option, index }) => (_jsx(WorkspacePickerOptionRow, { option: option, selected: index === selectedIndex, width: width }, `${option.kind}-${option.path ?? "new"}`))), _jsx(WorkspacePickerFooter, { text: openingPath
227
+ ? workspacePickerOpeningStatus(openingPath)
228
+ : workspacePickerListStatus(selectedIndex, options.length), width: width })] })) : (_jsxs(_Fragment, { children: [_jsx(WorkspacePickerPathRow, { defaultWorkspace: defaultWorkspace ?? cwd, locked: Boolean(openingPath), value: pathValue, width: width }), _jsx(WorkspacePickerFooter, { text: openingPath
229
+ ? workspacePickerOpeningStatus(openingPath)
230
+ : choices.length > 0 ? `${choices.length} recent` : "new workspace", width: width })] })), Array.from({ length: trailingRows }, (_, index) => (_jsx(FilledText, { text: "", width: width, backgroundColor: TUI_THEME.surface, color: TUI_THEME.text }, `workspace-fill-${index}`)))] }));
231
+ }
232
+ function WorkspacePickerHeader({ width }) {
233
+ const fullBrand = " parallel-codex-tui";
234
+ const fullSection = " · workspace";
235
+ const brand = width >= displayWidth(`${fullBrand}${fullSection}`)
236
+ ? fullBrand
237
+ : width >= 9 ? " pct" : compactEndByDisplayWidth("pct", width);
238
+ const section = width >= displayWidth(`${brand}${fullSection}`)
239
+ ? fullSection
240
+ : width >= displayWidth(`${brand} · ws`) ? " · ws" : "";
241
+ const fill = Math.max(0, width - displayWidth(brand) - displayWidth(section));
242
+ return (_jsxs(Text, { children: [_jsx(Text, { backgroundColor: TUI_THEME.chrome, color: TUI_THEME.accent, bold: true, children: brand }), _jsx(Text, { backgroundColor: TUI_THEME.chrome, color: TUI_THEME.muted, children: section }), _jsx(Text, { backgroundColor: TUI_THEME.chrome, children: " ".repeat(fill) })] }));
243
+ }
244
+ function WorkspacePickerNotice({ invalid, width }) {
245
+ const label = invalid.reason === "missing" ? "Workspace does not exist" : "Workspace is not a directory";
246
+ const text = compactEndByDisplayWidth(` ${label}: ${invalid.path}`, width);
247
+ return _jsx(FilledText, { text: text, width: width, backgroundColor: TUI_THEME.dangerSurface, color: TUI_THEME.danger });
248
+ }
249
+ function WorkspacePickerTitle({ count, mode, openingPath, width }) {
250
+ const rawLabel = openingPath ? " Opening project" : mode === "list" ? " Open project" : " Workspace path";
251
+ const rawMeta = !openingPath && mode === "list" && count > 0 ? ` ${count} recent` : "";
252
+ const label = compactEndByDisplayWidth(rawLabel, width);
253
+ const meta = displayWidth(label) + displayWidth(rawMeta) <= width ? rawMeta : "";
254
+ const fill = Math.max(0, width - displayWidth(label) - displayWidth(meta));
255
+ return (_jsxs(Text, { children: [_jsx(Text, { backgroundColor: TUI_THEME.surface, color: TUI_THEME.text, bold: true, children: label }), _jsx(Text, { backgroundColor: TUI_THEME.surface, color: TUI_THEME.muted, children: meta }), _jsx(Text, { backgroundColor: TUI_THEME.surface, children: " ".repeat(fill) })] }));
256
+ }
257
+ function WorkspacePickerOptionRow({ option, selected, width }) {
258
+ const backgroundColor = selected ? TUI_THEME.rail : TUI_THEME.surface;
259
+ const rawName = option.kind === "new" ? "New project" : basename(option.path ?? "") || option.path || "workspace";
260
+ if (width < 12) {
261
+ return (_jsx(FilledText, { text: `${selected ? ">" : " "} ${rawName}`, width: width, backgroundColor: backgroundColor, color: selected ? TUI_THEME.accent : TUI_THEME.text }));
262
+ }
263
+ const indicator = selected ? " > " : " ";
264
+ const shortcut = `${option.shortcut.padStart(2)} `;
265
+ const status = option.kind === "workspace" && !option.exists && width >= 20 ? " new" : "";
266
+ const fixedWidth = displayWidth(indicator) + displayWidth(shortcut) + displayWidth(status);
267
+ const showPath = option.kind === "workspace" && width >= 46;
268
+ const nameBudget = Math.max(4, Math.min(24, width - fixedWidth - (showPath ? 14 : 0)));
269
+ const name = compactEndByDisplayWidth(rawName, nameBudget);
270
+ const pathBudget = Math.max(0, width - fixedWidth - displayWidth(name) - (showPath ? 2 : 0));
271
+ const path = showPath ? compactTailByDisplayWidth(option.path ?? "", pathBudget) : "";
272
+ const gap = path ? " " : "";
273
+ const used = displayWidth(indicator) + displayWidth(shortcut) + displayWidth(name) + displayWidth(gap) + displayWidth(path) + displayWidth(status);
274
+ const fill = Math.max(0, width - used);
275
+ return (_jsxs(Text, { children: [_jsx(Text, { backgroundColor: backgroundColor, color: selected ? TUI_THEME.accent : TUI_THEME.muted, bold: selected, children: indicator }), _jsx(Text, { backgroundColor: backgroundColor, color: TUI_THEME.muted, children: shortcut }), _jsx(Text, { backgroundColor: backgroundColor, color: TUI_THEME.text, bold: selected, children: name }), gap ? _jsx(Text, { backgroundColor: backgroundColor, children: gap }) : null, path ? _jsx(Text, { backgroundColor: backgroundColor, color: TUI_THEME.muted, children: path }) : null, status ? _jsx(Text, { backgroundColor: backgroundColor, color: TUI_THEME.warning, children: status }) : null, fill > 0 ? _jsx(Text, { backgroundColor: backgroundColor, children: " ".repeat(fill) }) : null] }));
276
+ }
277
+ function WorkspacePickerPathRow({ defaultWorkspace, locked, value, width }) {
278
+ if (width < 4) {
279
+ return _jsx(FilledText, { text: locked ? "·" : "|", width: width, backgroundColor: TUI_THEME.rail, color: locked ? TUI_THEME.muted : TUI_THEME.accent });
280
+ }
281
+ const prefix = locked ? " " : " > ";
282
+ const cursor = locked ? "" : "|";
283
+ const valueWidth = Math.max(1, width - displayWidth(prefix) - displayWidth(cursor));
284
+ const visibleValue = value
285
+ ? compactTailByDisplayWidth(value, valueWidth)
286
+ : compactTailByDisplayWidth(defaultWorkspace, valueWidth);
287
+ const valueColor = value ? TUI_THEME.text : TUI_THEME.muted;
288
+ const used = displayWidth(prefix) + displayWidth(visibleValue) + displayWidth(cursor);
289
+ const fill = Math.max(0, width - used);
290
+ return (_jsxs(Text, { children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: locked ? TUI_THEME.muted : TUI_THEME.accent, bold: !locked, children: prefix }), _jsx(Text, { backgroundColor: TUI_THEME.rail, color: valueColor, children: visibleValue }), cursor ? _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: cursor }) : null, fill > 0 ? _jsx(Text, { backgroundColor: TUI_THEME.rail, children: " ".repeat(fill) }) : null] }));
291
+ }
292
+ function WorkspacePickerFooter({ text, width }) {
293
+ return _jsx(FilledText, { text: ` ${text}`, width: width, backgroundColor: TUI_THEME.chrome, color: TUI_THEME.muted });
294
+ }
295
+ function FilledText({ backgroundColor, color, text, width }) {
296
+ const visible = compactEndByDisplayWidth(text, width);
297
+ const fill = Math.max(0, width - displayWidth(visible));
298
+ return (_jsxs(Text, { children: [_jsx(Text, { backgroundColor: backgroundColor, color: color, children: visible }), fill > 0 ? _jsx(Text, { backgroundColor: backgroundColor, children: " ".repeat(fill) }) : null] }));
299
+ }
300
+ function workspacePickerOptions(choices, invalidExplicitWorkspace) {
301
+ const explicit = invalidExplicitWorkspace?.reason === "missing" ? invalidExplicitWorkspace.path : null;
302
+ const options = [];
303
+ if (explicit && !choices.some((choice) => choice.path === explicit)) {
304
+ options.push({ kind: "workspace", shortcut: "+", path: explicit, exists: false });
305
+ }
306
+ options.push(...choices.map((choice, index) => ({
307
+ kind: "workspace",
308
+ shortcut: String(index + 1),
309
+ path: choice.path,
310
+ exists: choice.exists
311
+ })));
312
+ options.push({ kind: "new", shortcut: "n", path: null, exists: false });
313
+ return options;
314
+ }
315
+ function workspacePickerWindow(options, selectedIndex, limit) {
316
+ const maxStart = Math.max(0, options.length - limit);
317
+ const start = Math.min(maxStart, Math.max(0, selectedIndex - Math.floor(limit / 2)));
318
+ const end = Math.min(options.length, start + limit);
319
+ return {
320
+ start,
321
+ end,
322
+ items: options.slice(start, end).map((option, offset) => ({ option, index: start + offset }))
323
+ };
324
+ }
325
+ function workspacePickerListStatus(selectedIndex, total) {
326
+ return `${Math.min(total, selectedIndex + 1)} / ${total}`;
327
+ }
328
+ function workspacePickerOpeningStatus(path) {
329
+ return `opening ${basename(path) || path}`;
330
+ }
@@ -0,0 +1,33 @@
1
+ export function commitWorkspaceTransition(input) {
2
+ try {
3
+ input.render(input.next);
4
+ }
5
+ catch (renderError) {
6
+ try {
7
+ input.close(input.next);
8
+ }
9
+ catch (cleanupError) {
10
+ deferCloseSafely(input, input.next, cleanupError);
11
+ throw new Error(`${errorMessage(renderError)}; prepared workspace cleanup failed: ${errorMessage(cleanupError)}`, { cause: new AggregateError([renderError, cleanupError]) });
12
+ }
13
+ throw renderError;
14
+ }
15
+ try {
16
+ input.close(input.previous);
17
+ }
18
+ catch (cleanupError) {
19
+ deferCloseSafely(input, input.previous, cleanupError);
20
+ }
21
+ return input.next;
22
+ }
23
+ function deferCloseSafely(input, state, error) {
24
+ try {
25
+ input.deferClose(state, error);
26
+ }
27
+ catch {
28
+ // The rendered workspace is already committed; do not turn cleanup bookkeeping into a failed switch.
29
+ }
30
+ }
31
+ function errorMessage(error) {
32
+ return error instanceof Error ? error.message : String(error);
33
+ }