diff --git a/.changeset/fix-docker-start-exit.md b/.changeset/fix-docker-start-exit.md new file mode 100644 index 00000000..4665829d --- /dev/null +++ b/.changeset/fix-docker-start-exit.md @@ -0,0 +1,8 @@ +--- +"@perstack/tui-components": patch +"perstack": patch +--- + +Short-circuit selection TUI when expert key is provided to fix immediate exit in compiled binaries. + +In Bun-compiled binaries, the first Ink `render()` call for the selection phase caused the event loop to exit before React effects could fire, resulting in `start` exiting silently with code 0. By returning the selection result directly when `initialExpertKey` is known, the Ink render is skipped entirely. diff --git a/packages/tui-components/src/selection/app.tsx b/packages/tui-components/src/selection/app.tsx index fa728915..5988e3c7 100644 --- a/packages/tui-components/src/selection/app.tsx +++ b/packages/tui-components/src/selection/app.tsx @@ -40,8 +40,6 @@ type SelectionAppProps = SelectionParams & { export const SelectionApp = (props: SelectionAppProps) => { const { showHistory, - initialExpertKey, - initialCheckpoint, configuredExperts, recentExperts, historyJobs, @@ -80,16 +78,8 @@ export const SelectionApp = (props: SelectionAppProps) => { } }, [exitResult, onComplete, exit]) - // If expert key is provided, complete immediately (never rendered anything) - useEffect(() => { - if (initialExpertKey) { - onComplete({ - expertKey: initialExpertKey, - checkpoint: initialCheckpoint, - }) - exit() - } - }, [initialExpertKey, initialCheckpoint, onComplete, exit]) + // Note: initialExpertKey shortcut is handled in renderSelection() before + // Ink render is called. This component only renders for interactive selection. // Handlers const completeWithExpert = useCallback( @@ -192,8 +182,8 @@ export const SelectionApp = (props: SelectionAppProps) => { ], ) - // After exitResult is set or initialExpertKey shortcut, render null - if (exitResult || initialExpertKey) { + // After exitResult is set, render null so Ink's final frame is empty + if (exitResult) { return null } diff --git a/packages/tui-components/src/selection/render.tsx b/packages/tui-components/src/selection/render.tsx index af86df6f..18375db5 100644 --- a/packages/tui-components/src/selection/render.tsx +++ b/packages/tui-components/src/selection/render.tsx @@ -6,8 +6,20 @@ import type { SelectionParams, SelectionResult } from "./types.js" /** * Renders the selection TUI phase. * Returns a promise that resolves with the selection result (expert and checkpoint). + * + * When `initialExpertKey` is provided, returns immediately without rendering the TUI. + * This avoids Ink lifecycle issues in compiled binaries where React effects may not + * fire before the process exits. */ export async function renderSelection(params: SelectionParams): Promise { + // Short-circuit: if expert key is already known, skip the TUI entirely + if (params.initialExpertKey) { + return { + expertKey: params.initialExpertKey, + checkpoint: params.initialCheckpoint, + } + } + return new Promise((resolve, reject) => { let selectionResult: SelectionResult | undefined