3.5.0
Released
Features
The Quest Coach interview has a Draft it now button, so the user can stop answering and have the coach draft with what it has (#878). It sits under the answer field — on the keyboard hint's row in the transcript, full width in the phone panel's one-question view (#853/#854) — from the first answer until the conversation has a draft, disabled while a turn is in flight or an earlier answer is being changed, and it sends a visible user turn ("Please draft the quest now with what you have, and fill in anything I haven't said.") flagged
draftNow: true; anything typed in the field stays there.POST /api/coach/interviewthen ends its system prompt with a server-side nudge (COACH_DRAFT_NOW_NUDGE, never stored in the history) — in the one leading system message, not as a second system turn after the history, which the free router and DeepSeek forward as-is and some models ignore — asking for the closing reply — one sentence and the complete JSON block, keeping what the user said or accepted and filling in the rest — and skips the runaway retry; a reply that is still a question goes straight to the structured repair (extractCoachDraft()) under the sameMIN_EXTRACTION_MS/RESPONSE_RESERVE_MSbudget, told to fill in what was never said, and its question is replaced by a closing sentence rather than kept as the draft's lead-in — likewise when the question came with a broken payload or alongside a draft that parsed, since the composer is gone once a draft lands. The draft lands as any closing turn does — Continue / Start over, "Your quest is ready.", "Ask for changes" — in the same interview, with no additional charge. If the repair cannot draft, the question is returned and the interview says the coach couldn't finish the draft. The fill-in-the-gaps wording and the task-details rule are now one shared sentence each incoaching.ts, used by "Draft it for me" (#876) and "Draft it now".The Quest Coach interview now drafts the quest for the user instead of asking them to write it (#877). After the goal — and, only when it is unclear, whether this is a one-off or a routine — every turn is a proposal: a short lead-in, a numbered list (2–4 points for the context, 2–4 for the constraints, 3–5 steps for the tasks, or the 2–4 steps of one session for a routine) and "Does that fit, or what would you change?". The system prompt's "Do not answer on the user's behalf" is replaced with "propose on the user's behalf and let them correct you; never explain the subject, only propose the quest", and the closing draft carries the proposals as accepted — no step added, dropped or reworded; a change that would leave fewer than two or more than five steps is not accepted but answered with steps proposed again within that range; for a routine, a change that puts a frequency into a step ("stretch every morning") is taken as the cadence and the steps are proposed again without it, so the cadence stripping from #858 never alters an accepted step — with
detailswritten by the coach for every task, never null;COACH_QUEST_DRAFT_SCHEMAnow asks fordetailsas a string, and its parser rejects a draft that does not fit the schema's whole shape — a blank goal, context or constraints, fewer than two or more than five tasks, or a bare step (coachDraftGaps()), judged on the raw payload as well as the normalized draft, so a task entry the parser would drop (no title, or not an object) and a missing or unknownrepeatcadence count as gaps rather than reading as a shorter one-off draft — which matters most on the free router's prompt-and-parse path, where that parser is the only validation; the lenient parser still reads an older draft with a null. A closing reply whose draft parses but has any of those gaps goes through the same structured repair as a malformed one, with a request naming each gap; if that repair cannot finish it, the parsed draft is returned as it is rather than leaving the interview with nothing to continue. A repaired draft — incomplete or a broken payload — is also written back into the returned reply (withDraftPayload()), so the coach's stored turn, which the transcript shows and "Ask for changes" redrafts from, matches the draft in the form. The runaway guard learned the difference between a proposal and a lecture:CoachReplyParts.kindis'proposal'when a reply ends in a question, holds one numbered list of 2–5 single-line items of at most 200 characters each — five, the most tasks a draft holds, and 200, a task title's limit, so every step the user accepts survives into the draft verbatim — with the question after it, and has no heading or bullet list, andisRunawayCoachReply()lets a proposal carry its list and up to 1,400 characters, while a reply with no question, a heading, a bullet list, a numbered list that is not a proposal (a single point included), or a question over 700 characters is still regenerated. A broken proposal is retried with its own nudge and a 400-token ceiling (coachRetryPlan()), since the lecture retry's 200 tokens would cut a list off; the lecture nudge no longer forbids lists, so a lecture where a proposal was due can come back as one.COACH_MAX_TOKENSstays at 1,200, which still covers a proposal turn and a five-task draft with details. In the transcript and in the one-question view a proposal's list renders as a numbered list (the bubble renderer reads lists with the samecoachTextBlocks()the guard does), and Looks good (sends "Yes, that works.") and Change it (focuses the answer field) sit above the composer under a proposal, withdrawn while a turn is in flight or an earlier answer is being changed; a quick reply leaves anything typed in the field where it was. In the one-question view an answered proposal is listed under what was proposed ("Here's what I'd suggest for the constraints") rather than under its "Does that fit?", and a correction to it names the proposal the same way. The progress copy keeps its five steps.The New Quest page's AI Coach can draft a whole quest from a title alone (#876). The coach card is title-first: a Title field with Draft it for me as the primary action and Start interview below it as the other way in ("Or answer a few questions"), the theme button where it was. Beside the form's own Title field — and below it on phones — Fill in the rest with AI appears while the title is 3–120 characters — the range the route drafts from, so a longer title (quests allow 200) gets no button rather than one certain to be refused — and only while the coach is idle and no draft is in the form. Either one fills the goal, context, constraints and 3–5 first steps, every step with details, in one call, keeps the title as typed, and collapses the card to "Quest drafted by AI Coach", with the Year Theme the draft was requested under — the theme itself, not the toggle — carried into the interview that continues it even if the toggle changed mid-request; anything typed in the form while the draft was being written is kept over the draft — and in the closing turn "Ask for changes" continues from, so a redraft does not put the replaced values back — and the form's own fields (status, due date, links, schedule) survive it landing; a substitute model is followed in the persona chip only if the chip still shows the persona the draft was sent with, so a pick made mid-request is not overwritten; a routine-shaped title ("Daily stretching") drafts with a cadence and lands in the recurring form as an interview's routine does (#858). Ask for changes then continues the same interview the call opened, so a redraft is not charged again.
POST /api/coach/interviewtakes this as a second body shape,{ seed: { title, note? }, modelPersonaKey?, useYearTheme? }with nomessages, sharing the interview's free-tier cap, persona selection with entitlement and affordability, charge (onecoach_interviewviastart_coach_interview— no new credit kind), refund on failure, repricing on a substitute and route budget, and responding{ draft, interviewId, servedPersonaKey, messages }, wheremessagesis the request and a closing reply carrying the draft that the interview continues from. The draft comes fromgenerateStructured()directly against the newCOACH_SEED_DRAFT_SCHEMA— the interview's schema with 3–5 tasks anddetailsrequired — so a draft with fewer steps or a bare one walks the fallback chain rather than reaching the form. The title (3–120 characters) and note (at most 500) are quoted in the user turn and never put in the system prompt. A 402 or 429 is shown under the field that asked, with the interview's copy; a 500, after the charge is refunded, offers "Try the interview instead". The interview prompt's payload example, task rules and Routines-vs-projects section are now shared with the newCOACH_DRAFT_FROM_SEED_PROMPT, so the two cannot drift; the seed prompt's copy of the example shows every step with details, as its schema requires. There is no new feature flag: it ships withNEXT_PUBLIC_FEATURE_COACHING_MODE.The investigation dialog keeps every number and warning in view and moves its explanations one tap away. The footer's five lines of cost prose are one line — "2–19 credits · up to 3 web searches", "Free · no web search" for a free persona, "no web search" for one without a search tool — with a "How it's priced" toggle that expands the two explanations as they read before; the short-of-hold warning, the "if your balance can't cover it" caveat and the free-model fallback stay always visible, since each changes what clicking Investigate does. Investigate carries the ceiling the user is agreeing to — "Investigate · up to 19 credits", or "Investigate · free" for a free persona and for an inherited one the balance cannot cover, which the route runs on the free model. Reference links are a "+ Add reference links" row that opens the editor, with the explanation of what can be read behind a "What can be read?" toggle on the editor's own "N of 10 added." line (
ImageUrlListgained ahelpActionslot); the quest's saved links and attachments are folded, counted rows ("Links on this quest (3)") that share one cost sentence once open. A section with something chosen in it cannot be folded: its toggle becomes a count ("Reference links (2)", "Links on this quest (1 of 3 chosen)"), because those references are being sent and charged for, and the range and ceiling move as they are added. Nothing is gated on a breakpoint, so the phone and the desktop card are the same layout (#881).The investigation dialog's starters take one row on a phone, and fold away once the question has text (#880). Below
smthe four starters are a single row that scrolls sideways, snapping to each chip, with the next one peeking past the edge; wrapped, at 390px none of them shared a line and the four-row stack was the tallest thing in the dialog, above the field it serves. Once the field has text — when the keyboard is up and room is scarce — the row becomes one "Starters" toggle in the same slot; expanding it and picking a starter still replaces the text and focuses the field, never submits, and folds the row away again. An empty field shows the starters as before, and fromsmthey wrap as they did. The row stays above the field, so a starter is still the first thing a touch device focuses and the keyboard waits.On a phone, Investigate stays on screen with the keyboard up (#879). Below
lgthe task investigation dialog is now a full-screen sheet sized to the visual viewport (--vvh/--vvtfromuseVisualViewportVars, as the coach panel has since #853), so its footer — the cost note, Cancel and Investigate — sits on top of the keyboard and only the body scrolls; iOS Safari does not shrink the layout viewport for the keyboard, so the old card, bounded by afixed inset-0overlay, kept its footer underneath it. The page behind is locked while the sheet is open, the body keeps the field being typed in on screen as the viewport resizes, and a ✕ in the header closes it (refused mid-submit, like Escape). On a touch device the dialog no longer focuses the question field on open, so the keyboard waits until a starter or the field is tapped; with a mouse and keyboard it still does. The overlay also ignores a parent'sspace-ymargin: on the quest page it sat 20px down, with its bottom edge — Investigate included — off the screen. Closing the dialog now returns focus to the Ask button that opened it; it used to be lost, because focus was recorded after the field had taken it. Atlgit is the centred card it was.
Technical changes· 6 entries for database, tooling, CI, tests and docs
Tests
route.test.tscovers Draft it now: the nudge ending the one system message and never echoed back, a compliant reply returned without a repair, a question reply replaced by a structured extraction told to fill in the gaps (with the closing sentence in place of the question, including a question that came with a broken payload, a complete draft or an incomplete one, while a broken reply's own closing sentence and a compliant reply are kept, and an ordinary turn is left alone), no runaway retry spent on it, an incomplete draft's repair told both its gaps and to fill in, the question returned when the extraction fails, no new charge and the same interview id on a continuation, and a non-trueflag read as an ordinary turn.coaching.test.tspins the nudge's messages and its shared fill-in and details rules with the seed prompt.coaching-interview.test.tsxcovers the button disabled before the first answer and mid-turn, its flagged request and the draft it holds, the field kept on failure, the error when a question comes back, its absence once a draft exists and on a title-only draft, and its place in both views;quest-coach-draft.spec.tsandquest-coach.mobile.spec.tsdrive it at desktop width and in the phone panel with the keyboard up (#878). Theapps/webcoverage floors move up to 95.6 / 91.8 / 95.4 / 95.6 (statements / branches / functions / lines), measured at 95.88 / 92.27 / 95.75 / 95.88 across 5381 tests.coaching.test.tscovers the proposal kind and the guard on fixtures: a context, a constraints (spaced out with blank lines) and a four-step task proposal read as proposals and pass, a proposal between 700 and 1,400 characters passes and one over 1,400 does not, and the Everest article, a list with no question, a headed reply, a one-point and a six-point list (five points still pass), a point over 200 characters (200 still passes), steps running over several lines, a question inside the list and a bullet list are all still regenerated; it also pinscoachTextBlocks(), the proposal's question, preamble and label fromsplitCoachQuestion(), the retry plan for a lecture and for a broken proposal, the prompt's new propose-on-their-behalf rule and draft rules, the re-proposal of a step list a change pushed outside 2–5, the re-proposal of a routine step a change gave a frequency (with a cadence-free accepted step surviving a routine draft verbatim), and the schema's stringdetails—coachInterviewDraftFromValuerejecting a null or blank one, zero, one or six tasks, a blank goal, context or constraints, a missing, null, empty or unknownrepeat, and a task the parser would drop even with 2–5 left, withcoachDraftGaps()naming each gap — with the parser still reading a null.route.test.tsalso checks a closing draft with a bare step is repaired with a request for the details, that a one-task draft with blank constraints is repaired with a request naming both gaps, that a draft with an unknown cadence and a titleless task is repaired with a request naming both, and that a complete one is not repaired, that blank details count as missing, that the parsed draft is still returned when the repair fails, and that a repaired draft — details or broken payload — replaces the payload in the returned reply.route.test.tschecks a proposal is returned without a retry and a broken one is retried on the proposal nudge with a ceiling above the lecture's and below a full turn;coaching-interview.test.tsxchecks the list renders as an<ol>in both views, Looks good and Change it appear under a proposal and not under the opening question, Looks good advances the interview by sending "Yes, that works." after the proposal and leaves the field's text alone, Change it focuses the field, the quick replies are withdrawn mid-turn, and an accepted proposal is listed under its lead-in (#877).route.test.tscovers the draft-from-title path: one structured call on the seed schema and no interview turn, the typed title kept, a returned history whose closing turn parses back to the same draft, the title and an instruction-shaped note quoted in the user turn only, exactly onestart_coach_interviewcharge, "Ask for changes" continuing the same interview uncharged on the interview prompt, a routine draft, the Year Theme section, 400s for an unusable seed, 402 for an unentitled persona, an unaffordable one and a refused charge, 429 at the free-tier cap, refund and 500 when every model fails, repricing when a cheaper model answers, and the route budget.coaching.test.tsruns five sample titles' payloads through the seed schema (schema-valid, 3–5 tasks with details, no frequency words in task titles, the title absent from the system prompt), pins the schema to OpenAI strict mode, the seed validation, the quoting and the shared prompt sections.coaching-entry-lifecycle.test.tsx,coaching-quest-creator.test.tsxandcoaching-interview.test.tsxcover the title-first card, the form's button and its three-character threshold, errors under the control that asked, the 120-character upper bound, the request-time Year Theme surviving a toggle flipped mid-request, form edits made during the request surviving the draft and its closing turn (landDraftOnEditedForminform-carryover.test.tstoo), the continuation's theme after the toggle goes off, the seed prompt's example never showing a nulldetails, a persona picked mid-request surviving a fallback (and a fallback still followed otherwise), and the seeded conversation;e2e/quest-coach-draft.spec.tsdrives both buttons, Ask for changes, a routine title and a 429 on the coach fixture route (#876).investigation-prompt-dialog.test.tsxcovers the one-line cost note and its toggle (folded by default,aria-controlsonly while expanded, the old wording behind it), the ceiling on Investigate for paid, free and fallback personas, the warnings showing without expanding, the dialog holding only the starters, the field and one reference row while nothing is chosen, each reference disclosure opening, staying open and counted once something is chosen and offering its toggle again when emptied, the shared cost sentence, the "What can be read?" explanation on the editor's count line, and every toggle locking mid-submit; the shared ten-link budget tests open the sections and still pass.image-url-list.test.tsxpins thehelpActionslot, ande2e/investigation-dialog.mobile.spec.tschecks at 390px that the dialog opens folded with a one-line cost note and "Investigate · up to 19 credits", that the pricing explanation expands, and that ticking a saved link keeps its section open, counted, and moves the ceiling to 20 (#881).investigation-prompt-dialog.test.tsxcovers the sheet's classes, the scroll lock and viewport variables (and their cleanup), the header close and its mid-submit refusal, no autofocus on a touch device, the focused field being kept in view as the body shrinks, the overlay's margin reset and focus returning to the trigger;e2e/investigation-dialog.mobile.spec.ts(themobileproject, on the newe2e-fixtures/investigationroute) opens the dialog on a Pixel 7, taps a starter, shrinks the viewport to a keyboard-up height and checks the field and Investigate are inside the visual viewport before submitting, closes it with ✕ and Escape back to the Ask button, and checks the card is unchanged atlg(#879).investigation-prompt-dialog.test.tsxcovers the starters' one-row-below-smclasses, the fold into a "Starters" toggle once the field has text (not for whitespace alone), expanding, collapsing, a pick replacing the text and focusing the field without submitting, the row returning when the field is cleared, and both controls locking mid-submit;e2e/investigation-dialog.mobile.spec.tschecks at 390px that the four starters share one row that scrolls sideways without widening the body, that typing folds them and a pick from the toggle fills and focuses the field, and that they wrap at 768px (#880).