Ask to save the interview before anything discards it - #115
Merged
Conversation
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>
`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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 gosetPlaceholderState()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 === 0is 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.hasHistoryis derived inwithHistory()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.
exportTranscriptguarded 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 andnothingToExportnow 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.tsvetoes the close, sendsapp:save-history-prompt, and the renderer closes the window itself by replying withwindow:close-confirmedorwindow:close-cancelled.Three pieces of state, each for a failure the others do not cover:
closeConfirmedlets the answered close through rather than re-prompting on it.promptingstops a second close - the window button pressed while the dialog is up - stacking another prompt.quitting, set frombefore-quit, is what makes Cmd+Q work. Vetoing the close cancels the quit, so confirming callsapp.quit()again rather thanwin.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
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.export-success-toast.tsx, shared by the export menu and the prompt, so "where did that file go" has the same answer either way.startconfirmation 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, bothtscconfigs,pnpm buildandpnpm test:mainall pass locally.test/save-history.test.mjspins 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 gainedBrowserWindow,desktopCaptureranddialog, which named imports need at link time fortools.serviceto load at all.🤖 Generated with Claude Code