feat(suggestions): optional professional mode for at-a-glance hints - #97
Conversation
Live suggestions arrived as full sentences and rendered as one whitespace-pre-wrap block. During a real interview the candidate has a couple of seconds to glance at the panel while the interviewer is watching, and a paragraph does not fit in that window. Professional mode asks the backend for hints instead: a bold one-line core answer plus 3-5 keyword bullets. Off by default, so nothing changes for anyone who does not turn it on - the migration IIFE backfills it as false, and test/config-store.test.mjs pins that an upgrading install reads back off. Toggle lives in the control panel next to the LLM button and is deliberately not disabled while running: it is a mid-interview control and only affects the next suggestion. Ctrl+Shift+F7 does the same, which keeps it reachable in stealth mode where the panel is hidden. F7 rather than P because globalShortcut claims accelerators system-wide, and Ctrl+Shift+P would take the command palette away from every editor on the machine for as long as the app runs. Each LiveSuggestion carries the mode it was generated under, and the panel picks its renderer from that rather than from the live setting. Otherwise toggling mid-interview would reformat cards already on screen, parsing prose as Markdown. Professional answers render through SafeMarkdown, the component the action panel already uses. Also adds a rejection handler to the fire-and-forget generateSuggestion call, matching the action service: the config read now sits above the try that guards it, so a throw there would otherwise leak an abort-map entry. Backend support: PowerInterviewAI/backend#48. The mode field defaults to normal server-side, so this is safe against an older deployment. Closes #96 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gitar flagged the assertion as tautological, which was right about the symptom. Its suggested fix - asserting on getStoredRuntime instead - turns out to pass just as unconditionally, because updateConfig re-spreads DEFAULT_RUNTIME_CONFIG on every write, so the key reaches disk whether or not the migration branch runs. Verified both directions by breaking each mechanism in turn: with the migration branch deleted the assertion still passes on the default spread, and with the default flipped to true it still passes because the migration pins false independently. So the two mechanisms are genuinely redundant, and no single assertion can isolate one while both hold. That redundancy is worth keeping - if professional mode ever becomes the default for new installs, the migration is what keeps existing users on prose. Left the code alone and rewrote the comment to say what the assertion actually guards rather than claiming to pin the backfill. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks @gitar-bot - right about the symptom, and I went and checked the proposed fix before applying it. It does not hold either, for a different reason.
I verified this by breaking each mechanism in turn rather than reasoning about it:
So the two mechanisms are genuinely redundant, and while both hold no single assertion can isolate one. I could have written a second seeded scenario with every other migration-guarded key present to isolate the branch, but that pins implementation rather than behaviour - the key materialising on disk has no functional consequence, since The redundancy is worth keeping, though, and the second row above is why: if professional mode ever becomes the default for new installs, the migration branch is what keeps existing users on prose. That is a real behavioural guarantee, just not one this fixture can currently distinguish. Left the code as-is and rewrote the comment in 99ebab9 to state what the assertion actually guards - the invariant itself - instead of claiming to pin the backfill. The misleading comment was the real defect here. |
Self-review: side effects checkedWent back over the diff for things neither CI nor Gitar would catch. Four checked and clear, two residual risks worth knowing about. ClearImport cycle. Request construction sites. Only two exist ( Abort-map cleanup. The new Export. Residual risks1. The backend prompt guards this explicitly ("emit it alone, bare, with no bold, no bullet, no punctuation"), and PowerInterviewAI/backend#48 pins that the instruction survives in both modes. But that is prompt adherence, not a guarantee. I deliberately did not add client-side stripping: it would change the filter for normal mode too, on a hypothesis. This is the first thing to watch in manual testing - if it shows up, normalising before the prefix check is the fix. 2. Markdown re-parses on every stream chunk. Both are inherent to the "prompt change, no post-processing" approach and are best judged against a real interview once backend#48 deploys. |
|
Confirmed — checked Your empirical check (deleting the migration branch, then flipping the default) is the right way to confirm two mechanisms are redundant rather than reasoning about the spread order in the abstract, and the forward-compatibility argument for keeping both is solid — the migration branch is what protects existing installs if |
Update: residual risk 1 is resolvedAbove I flagged the Tested it against the real free-tier model rather than leaving it to first deploy. A filler-only transcript ( So no client-side normalisation is needed, which is the outcome I was hoping for - the alternative would have changed the filter's behaviour for normal mode too. Detail and the other probes are on PowerInterviewAI/backend#48. Summary of what the model actually produced in professional mode: Four bullets, 35 words. That renders through Residual risk 2 (per-chunk Markdown re-parse) still stands and is unchanged - at 35 words the parse is trivial, but it is a real new per-chunk cost on the live path. Worth a look at CPU during a long session. Still not covered by any of this: the rendered result in the actual app. The control panel is behind login, so I have not put eyes on the new button or the Markdown card. That is the remaining manual check. |
Two conflicts, both additive: - SPEC.md: professional mode and the new session-window section were each inserted before "Interview Config Sync". Kept both. - app-state.service.ts: the import line gained SuggestionMode on this branch and refreshWindowSurfaces on main. Kept both. The two features touch app-state.service in different places - setPlaceholderState tags the placeholder card with its mode, updateState refreshes the window surfaces on a running transition - so there is no behavioural interaction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Professional mode asks the model for a bold headline on line 1, so a model that carries the format over to the sentinel emits **NO_SUGGESTION_NEEDED**. The literal prefix match missed that and left the sentinel itself sitting in the panel as a card, mid-interview. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The wrapped forms have to be suppressed and real answers have to survive; loosening the match too far would swallow an answer with no trace. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SafeMarkdown renders paragraphs semibold, so the bold headline sat one weight step from the bullets under it - the line the whole mode exists to make readable in a glance was the hardest one to pick out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The normal-mode prompt asks for plain text with light formatting, so bold or a bullet reached the panel as literal asterisks under whitespace-pre-wrap. Both modes now go through SafeMarkdown, the component the action panel already uses. Prose is passed through withHardBreaks() first, since Markdown folds a single newline into a space and the previous rendering showed every one of them. The wand keeps a column of its own rather than being prepended to the content, where it would swallow whatever structure the answer opens with. The stopped marker moves into the content so it stays inline at the end of the last line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Code Review ✅ Approved 1 resolved / 1 findingsAdds optional professional mode for concise, bulleted live suggestions rendered as Markdown. Addressed the tautological migration test finding. ✅ 1 resolved✅ Quality: Migration test is tautological, doesn't verify backfill
OptionsAuto-apply is off → Gitar will not commit updates to this branch. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Gitar |
Closes #96
Backend half: PowerInterviewAI/backend#48
What
An opt-in mode that switches live and triggered suggestions from full sentences to hints - a bold one-line core answer plus 3-5 keyword bullets:
Off by default. The migration IIFE backfills
professionalMode: false, so upgrading installs and fresh installs both keep prose.test/config-store.test.mjspins that a runtime seeded without the key reads back off, and that an explicittruesurvives an unrelated write.Safe to merge before the backend deploys:
modedefaults tonormalserver-side, so an older deployment ignores the field.Three decisions worth reviewing
The toggle is not disabled while running. Unlike the LLM and audio buttons, this is a mid-interview control - the whole point is flipping it when a question turns out to need a different format. It only affects the next suggestion; in-flight streams are untouched.
Ctrl+Shift+F7, notCtrl+Shift+P.globalShortcutclaims accelerators system-wide, so binding P would take the command palette away from every editor on the machine for as long as this app runs. F7 also sits with the existing F8-F12 block, and a function key keeps it reachable in stealth mode where the control panel is hidden - the same reasoning the F8 comment already gives.Each
LiveSuggestioncarries the mode it was generated under, and the panel picks its renderer from that rather than from the current setting. Reading the live setting would reformat every card on screen the moment the user toggles, parsing already-delivered prose as Markdown. Professional answers render throughSafeMarkdown, the component the action panel already uses, so no new styling was needed.Incidental fix
generateSuggestionis called fire-and-forget, and its abort-map cleanup lives in its ownfinally. The config read now sits above thetrythat guards it, so a throw there would leak an entry and leave a dead controller. Added the same.catchthe action service already carries for exactly this reason.Verification
pnpm lintcleanpnpm exec tscclean on bothtsconfig.app.jsonandtsconfig.electron.jsonpnpm buildsucceedspnpm test:main- all checks pass, including three new config-store assertionsNot yet exercised end-to-end against a running backend with professional mode enabled, since PowerInterviewAI/backend#48 is not deployed. Worth a manual pass once it is: toggle mid-interview and confirm the existing card keeps its rendering while the next one switches.
🤖 Generated with Claude Code