Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,11 @@ function runWizard(
} catch (error) {
analytics.captureException(error as Error);
}
// tui.unmount() drains any post-exit message stashed via
// setPostExitMessage(...) to scrollback as part of its cleanup
// (see start-tui.ts + lib/post-exit-message.ts), so any
// copy-paste-ready prompt the workflow stashed lands in the
// user's terminal regardless of which screen exits the process.
tui.unmount();
process.exit(0);
} catch (err) {
Expand Down
46 changes: 46 additions & 0 deletions src/lib/__tests__/post-exit-message.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { buildSession } from '../wizard-session';
import {
POST_EXIT_MESSAGE_KEY,
getPostExitMessage,
setPostExitMessage,
} from '../post-exit-message';

describe('post-exit-message accessors', () => {
it('round-trips set + get on the same session', () => {
const session = buildSession({});
setPostExitMessage(session, 'hello terminal scrollback');
expect(getPostExitMessage(session)).toBe('hello terminal scrollback');
});

it('returns undefined when nothing has been stashed', () => {
expect(getPostExitMessage(buildSession({}))).toBeUndefined();
});

it('returns undefined when frameworkContext holds a non-string at the key', () => {
// Defense-in-depth: the type guard rejects bogus shapes rather than
// returning garbage to the cleanup printer.
const session = buildSession({});
session.frameworkContext[POST_EXIT_MESSAGE_KEY] = { not: 'a string' };
expect(getPostExitMessage(session)).toBeUndefined();
});

it('survives shallow setKey clones of the top-level session', () => {
// The whole point of using frameworkContext: agent-runner.ts mutates
// session.outroData on a stale reference (the atom replaces session
// via setKey during the run), and the mutation goes to a stranded
// object. frameworkContext is shared by reference across those
// setKey shallow-spreads, so a direct mutation on it survives a
// top-level replacement of the session — which is exactly what
// happens in production. Simulate that here.
const original = buildSession({});
setPostExitMessage(original, 'lives in shared framework context');

// Simulate setKey: a NEW top-level session object that shallow-copies
// each top-level key from the original (so frameworkContext is the
// SAME reference as before).
const replaced = { ...original, lastStatus: 'something changed' };
expect(getPostExitMessage(replaced)).toBe(
'lives in shared framework context',
);
});
});
32 changes: 32 additions & 0 deletions src/lib/post-exit-message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Stash text to print to the user's scrollback after the wizard exits.
* Read by `start-tui.ts`'s cleanup handler, AFTER `releaseTerminal()` —
* so the message survives any exit path (bin.ts unmount, screens that
* call `process.exit` directly, error paths).
*
* Why frameworkContext (not session.outroData):
* `agent-runner.ts` mutates `session.outroData` on a STALE session
* reference — by the time the mutation happens, `setKey` calls during
* the agent run have replaced the atom's top-level session, so the
* write goes to a stranded object. `frameworkContext` is the same
* reference across `setKey` shallow-spreads, so a direct mutation on
* it survives (until anyone calls `store.setFrameworkContext`, which
* clones it). Same pattern as
* `posthog-integration/handoff.ts`'s handoff-status accessor.
*/

import type { WizardSession } from './wizard-session.js';

export const POST_EXIT_MESSAGE_KEY = 'pendingPostExitMessage';

export function setPostExitMessage(
session: WizardSession,
message: string,
): void {
session.frameworkContext[POST_EXIT_MESSAGE_KEY] = message;
}

export function getPostExitMessage(session: WizardSession): string | undefined {
const v = session.frameworkContext?.[POST_EXIT_MESSAGE_KEY];
return typeof v === 'string' ? v : undefined;
}
Loading