Skip to content

Ask to save the interview before anything discards it - #115

Merged
alpha5611331 merged 2 commits into
mainfrom
feat/save-history-confirmation
Aug 28, 2026
Merged

Ask to save the interview before anything discards it#115
alpha5611331 merged 2 commits into
mainfrom
feat/save-history-confirmation

Conversation

@alpha5611331

Copy link
Copy Markdown
Member

Closes #114.

What changes

Clear, Start and closing the app all empty the transcript and the suggestions. Those live only in main-process memory - nothing reaches disk until an export - so all three now ask first, through one dialog offering Word, Markdown, discard or cancel.

hasHistory, and why the length check had to go

setPlaceholderState() seeds the panels with one transcript and two suggestions so an empty app has something to show, on launch and again after every Clear. transcripts.length === 0 is therefore never true, and a prompt keyed on it would fire on every Clear of a freshly launched app asking the user to save sample copy.

AppState.hasHistory is derived in withHistory() and never set by a caller. Only the transcript and suggestion services write the three history keys and they only ever write real content, so a write to any of them retires the placeholder and the flag is recomputed from what the write leaves behind. The untouched arrays are emptied in the same write: clearAll() runs before every session so mixed state is not reachable today, but a real transcript beside two lines of sample suggestion copy is the one shape that would carry placeholder text into an exported report.

This fixes a live defect on the export path. exportTranscript guarded on the same lengths, so Export on a machine that had never run an interview passed the guard, billed a summarize call on "Transcripts will be here", and wrote the model's answer into a document presented as a record of the candidate's interview. Both that guard and nothingToExport now read the flag.

The close path

Clear and Start are renderer-initiated and confirm before they act. A close is decided in main - the window button, Cmd+Q, app.quit() - and the renderer would hear about it too late to matter. window-close-guard.ts vetoes the close, sends app:save-history-prompt, and the renderer closes the window itself by replying with window:close-confirmed or window:close-cancelled.

Three pieces of state, each for a failure the others do not cover:

  • closeConfirmed lets the answered close through rather than re-prompting on it.
  • prompting stops a second close - the window button pressed while the dialog is up - stacking another prompt.
  • quitting, set from before-quit, is what makes Cmd+Q work. Vetoing the close cancels the quit, so confirming calls app.quit() again rather than win.close(); otherwise the app sits there with one window fewer. Cancelling resets it, or the next Cmd+Q would take that branch for a session the user just chose to keep.

The guard gives up on a renderer that is destroyed or crashed. Exactly one reply has to come back, and an app that cannot be closed at all is worse than one that closes unasked.

Smaller decisions

  • The dialog is mounted once in MainFrame: the three actions do not share a screen. Stealth mode does not render the control panel, and the close prompt has no component of its own.
  • Cancelling at the system save dialog leaves the prompt open rather than reading as a decision to discard, and so does a failed export - going ahead there would destroy the interview on the one path where keeping it did not work.
  • The export success toast moved to export-success-toast.tsx, shared by the export menu and the prompt, so "where did that file go" has the same answer either way.
  • The start confirmation is asked before the macOS permission gate: a user about to be sent into System Settings should not have answered a question the trip makes moot.

Checks

pnpm lint, both tsc configs, pnpm build and pnpm test:main all pass locally. test/save-history.test.mjs pins the flag through placeholder, real ingest, clear and re-seed, that an export of the placeholder is refused, and (source-level) the four close-guard invariants above. The electron test stub gained BrowserWindow, desktopCapturer and dialog, which named imports need at link time for tools.service to load at all.

🤖 Generated with Claude Code

Clear, Start and closing the app all empty the transcript and the
suggestions, which exist only in main-process memory - nothing reaches
disk until an export. All three now ask first, through one dialog.

The question is only worth asking about a real interview, and the array
lengths cannot tell you that: the panels are seeded with placeholder copy
on launch and again after every Clear, so `transcripts.length === 0` is
never true. `AppState.hasHistory` is derived in main instead and follows
the writes, since only the transcript and suggestion services write those
keys and they only ever write real content.

That also fixes the export path, where the same length check was already
live: Export on a machine that had never run an interview passed it and
billed a summarize call on "Transcripts will be here".

Closing cannot ask on its own behalf - the decision is taken in main - so
the close is vetoed, the renderer is asked, and it closes the window by
replying. Vetoing a close cancels an in-flight quit, so a confirmed close
restarts the quit rather than closing the window.

Closes #114

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gitar-bot

gitar-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

`hasHistory` counted action suggestions, which the export does not
write - `exportTranscript` builds the report from transcripts and live
suggestions alone. A session whose only content was a screenshot
therefore offered to save what the save could not contain, and passed the
export guard onto a billed summarize call over an empty transcript. That
is the failure the guard exists to stop, reached through another door.

And installing an update is a quit the guard must not veto.
`quitAndInstall` launches the installer and requests the quit after it,
so vetoing leaves an installer running against an app that will not exit;
on Windows it ends with the installer killing it. The updater handler now
disarms the guard before installing and re-arms it if nothing was
launched, and `update-notification.tsx` asks in front of the install
instead.

Also: `updateState` strips an incoming `hasHistory` so no caller can set
what the close guard trusts, the guard resets per window, and a renderer
reload clears a pending prompt that died with it - otherwise the veto
stood with nobody left to answer and the window could not be closed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@alpha5611331
alpha5611331 merged commit f2466f6 into main Aug 28, 2026
1 check passed
@alpha5611331
alpha5611331 deleted the feat/save-history-confirmation branch August 28, 2026 02:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Clearing, restarting or closing destroys the interview with no chance to save it

1 participant