Skip to content

Studio drafts: one owner for the draft and the outgoing message #205

Closed

Description

@AojdevStudio

Problem

In #201, the review of how a send names its message spun past land-pr's convergence rule. Five review rounds found 12 cases where the window sends a message twice, treats a new message as an old one, or loses or keeps a draft by mistake. One redesign (b35114b) failed review too, so the PR stopped fixing in place.

Root cause

Two parties own the same draft and the same outgoing message.

  • The window (src/studio/StudioApp.tsx) keeps a cache of drafts, a debounce save queue, and per-screen revisions and edit counts. It also builds the outgoing message itself: text, picture imports, and request id.
  • The sidecar stores the drafts and moves the start draft into a new project by matching its text. It removes duplicate sends by request id within one conversation, and it removes a draft only by revision.

Every identity model on the window side had to guess whether its copy still matched the stored draft when a send began. That guess breaks across async saves, project creation, screen changes, a saved state that arrives late, and pastes that land late.

Round Window model Cases it missed
47918da object identity restored pictures get a new request id. A first send leaves its moved start draft in the store
27f2c04 picture paths a picture taken out and put back at the same path, with a changed file, reuses the old id
9168046 saved revision with a fallback a failed draft save reuses the old revision. A start draft changed during project creation takes the first message's revision
a6c2c80 edit count a moved newer draft reuses the old count. A picture-only retry from the start screen makes a second project. A late paste is lost
b35114b revision, known only while unchanged an autosave and the send's own save race, so an unchanged message sends twice. A start-screen retry overwrites the project's newer draft. A late saved state restores pictures under a sent revision. A picture-only first message never moves, because the move matches text and not revision

Direction

Give the draft and the outgoing message one owner, the sidecar. Keep the window's part synchronous and small.

  • The sidecar sends a stored draft revision in one commit. One call creates the project for a start-screen send, moves the draft by revision, copies its pictures, commits the message as draft-<rev>, and removes the draft. Sent again with the same revision, it answers with the project it already used.
  • The window knows only whether the composer changed since its last completed save. It never builds a request id. It never runs two saves of one screen at once.
  • The 12 cases above become window and sidecar tests, written first.

Not in scope

  • The non-draft fixes from the same round: the quit wait, ~/.local/bin for uv, the toolchain check limit, and the pictures cleanup. Each has its own test.

Activity

  1. pullfrog commented on Nov 7, 2026

    @pullfrog

    Implementation plan

    1. Start from feat(studio): the window works on the sidecar's committed chats, and the claude -p runner is gone #201's eventual head, not current main: main still has the older drafts.set and chat.send contracts, while feat(studio): the window works on the sidecar's committed chats, and the claude -p runner is gone #201 introduces the revision and window save machinery this issue addresses. Add regression tests first for the 13 cases, including delayed saves and pastes, picture-only drafts, project switches, and lost responses.
    2. Change the sidecar protocol and store so a send consumes a stored revision. Commit project identity, message identity (draft-<rev>), and conditional draft removal together; persist the revision-to-project result so a retry after a lost response or restart returns the original project. Keep picture copying and failure recovery consistent with that commit, and never overwrite a newer draft.
    3. Simplify StudioApp.tsx and its IPC wrappers to serialize saves per screen, track edits since the last completed save, and send only the saved revision. Guard late save and paste completions against screen changes and newer edits.
    4. Run the window and sidecar suites, including concurrent sends, save failures, restart/retry, and attachment-copy failure cases. Verify existing drafts migrate safely and the real Studio send flow works.

    No duplicate was confirmed. #201 is the prerequisite and review history; #193 concerns a separate project preview defect.

    Pullfrog  | Implement a fix ➔ | View workflow run | via Pullfrog | Using gpt-6.1-terra | 𝕏

  2. added
    bugSomething isn't working
    rustPull requests that update rust code
    on Nov 7, 2026
  3. AojdevStudio commented on Nov 7, 2026

    @AojdevStudio
    OwnerAuthor

    The one-owner design is built at b4c9bc2, on #201's branch. It is not pushed. The gate is green: 298 window tests, 118 sidecar tests, and TLC matches every recorded verdict with all 10 mutants failing.

    Sol's review of the round, 0aa8402..b4c9bc2, found no duplicate send and no wrong-content send. It did find three P2 draft bugs at the edges:

    1. The start screen can get stuck on a rejected revision. It takes a crash after project creation and before the message commit. Then a retried import shows the project, a newer draft is saved there, and the person goes back to the start screen. Every send then fails with "The draft changed", because the window keeps its old saved revision and never saves again. (src/studio/StudioApp.tsx send catch, about line 440)
    2. A late saved state can bring back a picture that already went out. An import retry's state reply arrives after a successful send. It restores the start screen's saved pictures onto the emptied screen and saves them again. (takeState, about line 186)
    3. A quit can lose text typed during a slow send. The quit flush waits for the send before it saves newer edits. Rust holds a quit for at most 1.5 s, so text typed during a send longer than that is lost. (onQuitFlush, about line 318, and src-tauri/src/studio/quit.rs:15)

    Each fix would be small:

    1. A send that fails with "The draft changed" drops the screen's saved revision, so the next send saves again.
    2. The saved state restores start pictures only on a screen this window has never saved or sent.
    3. The quit saves changed screens at once, without waiting for a send.

    The PR's rule for this round was to stop and report, not patch, so none of these are applied.

  4. AojdevStudio commented on Nov 8, 2026

    @AojdevStudio
    OwnerAuthor

    The fix round for the three edge findings is commit 964383e, not pushed. Sol's review of b4c9bc2..964383e found four P2s. Three come from the fixes themselves:

    1. The revision filter drops an unsent picture. (StudioApp.tsx:186, sentRevs in takeState). The first saved state arrives late, holding picture A at revision 1. Meanwhile the person sends text without A at revision 2. The filter then skips A, though A never went out.
    2. A quit keeps one draft under two screens. (StudioApp.tsx:326). A first send commits but answers slowly, and the person types and quits. The forced save stores the new text under new. When the send answers, the window carries the same text to the project. The start screen's stored copy stays.
    3. A quit can bring back a sent picture. (StudioApp.tsx:326). The forced save during a send includes the outgoing picture under a newer revision. If the window closes before the answer, the relaunch shows that sent picture again.
    4. A recorded start draft does not protect its pasted pictures. (store.ts:279). A newer project draft replaces the moved draft, so only madeFor names the pasted picture. The cleanup at start checks current drafts only, so it deletes the picture after a day, and the resumed send then fails.

    Recommendation for the next fix round:

    • Keep fix 1 (the recorded start draft). Count madeFor drafts' pictures in the start-up cleanup. That closes 4.
    • Replace fix 2 with a narrower one. An import retry refreshes only the project list and the blocked notice, never drafts. The late saved state that brought a sent picture back only exists on that path. The first state merges as before. That closes the b4c9bc2 finding without 1.
    • Revert fix 3. Track "a quit during a send longer than 1.5 s can lose text typed during it" as a known limit. A send finishes in milliseconds, since nothing installs at quit. Fixing it properly means Rust holds a quit for as long as a send runs, which belongs in its own change. That closes 2 and 3.
  5. AojdevStudio commented on Nov 8, 2026

    @AojdevStudio
    OwnerAuthor

    The narrower fix round is pushed as 361b3c5, #201's head. The gate is green: 300 window tests, 120 sidecar tests, and TLC matches every recorded verdict, with all 10 mutants failing.

    Sol's review of 964383e..361b3c5 found no P1 or P2. It found two P3s, both shipped as known:

    1. P3, development builds only. React StrictMode reads the saved state twice at startup. If the second read answers late, after the person sent a start-screen picture, the start-screen picture merge brings that picture back. Sol found no path in the packaged app. Project drafts are not affected. (src/studio/StudioApp.tsx:181)
    2. P3, normal use, disk only. startedDrafts() returns every recorded start draft, completed sends included. So the start-up cleanup keeps a first message's original pasted pictures forever, after the send has copied them into the project. It costs disk space and brings back no draft. (sidecar/src/store.ts:289)

    Both are listed in the A to L report for Astra. A small follow-up would fix 2: drop a recorded draft once its message is committed. It would fix 1 by having the start picture merge take part only in the first state read.

  6. AojdevStudio commented on Nov 8, 2026

    @AojdevStudio
    OwnerAuthor

    The two P3s above also live in #207, which stays open after #201 closes this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingrustPull requests that update rust code

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions