Skip to content

Inbox: reply drafts and the Draft label - #2028

Open
backnotprop wants to merge 19 commits into
mainfrom
inbox-reply-drafts
Open

backnotprop wants to merge 19 commits into
mainfrom
inbox-reply-drafts

Conversation

@backnotprop

Copy link
Copy Markdown
Owner

Words typed in a thread's reply box are kept per thread as a server draft (inbox/reply-drafts/<thread id>.json, outside the event log, compare-and-set on updated_at). They are saved while typing and put back in the box, without focus, when the thread opens again, after a reload and after a restart. Cancel keeps them, Discard drops them, and the Send that carries them clears them (clear_draft on the reply).

"Draft" replaces "Not sent":

  • the card's tag and footer ("Picked 4:35 PM · in your draft")
  • the thread's foot ("Draft · 2 picks", then Send and Edit draft)
  • a soft "Draft" pill with "Saved 4:43 PM · Discard" above the box
  • the list row's count slot ("Draft · Your reply", from the row's additive unsent)

A thread holding unsent words, picks or comments never sinks to Quiet unless archived. It stays in New, not bold when only the person's own words wait. The sidebar's Drafts count includes these threads, and the Drafts page lists them under "Replies not sent". The phone's door can read a draft (contract 7.61).

Approval: .product/approved/inbox-reply-drafts/ (variant Label).

The window reads the list and the Drafts again on event: drafts, on a debounce timer of its own, so a draft save never cancels the reads a just-arrived message needs.

Tests: typecheck; the Inbox DOM tests; the shared, core and server inbox tests; and the Playwright e2e on the compiled binary, including the new inbox-reply-drafts.spec.ts and inbox-dictation-paste.spec.ts. The dictation spec now opens the reply box through a helper, because a kept draft reopens the box by itself; inbox-tags.spec.ts does the same.

ramos-plannotator and others added 19 commits October 20, 2026 17:18
Words typed in a thread's reply box are kept per thread as a server
draft (inbox/reply-drafts/<thread id>.json, outside the event log, a
compare-and-set on updated_at), saved while typing and put back in the
box, without focus, when the thread opens again: across leaving the
thread, a reload and a restart. Cancel keeps them, Discard drops them,
and the Send that carries them clears them (clear_draft on the reply).

"Draft" replaces "Not sent": the card's tag and footer, the thread's
foot ("Draft · 2 picks" before Send and Edit draft), a soft pill with
"Saved 4:43 PM · Discard" above the box, and the list row's count slot
("Draft · Your reply", from the row's additive `unsent`). A thread
holding unsent work (words, picks, comments) never sinks to Quiet
unless archived: it sits in New, not bold for the person's own words.
The sidebar's Drafts row counts these threads and its page lists them
under "Replies not sent". The phone's door reads a draft (7.61).

Approval "inbox-reply-drafts" (variant Label).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	AGENTS.md
#	packages/inbox/api.ts
#	packages/inbox/components/InboxList.tsx
#	packages/server/inbox.ts
#	packages/shared/inbox/store.ts
The reply box now reopens by itself with the thread's kept words, without
focus, and the foot's button reads Edit draft while picks wait, so the spec
opens the box through one helper that handles both and focuses it as a
click on Reply does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An earlier test's words come back as the thread's reply draft and reopen
the box by itself, so the phone test found no Reply button and waited out
its timeout. The helper now takes the box either way and starts it empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The drafts event shared the record event's 80 ms debounce timer, so a draft
saved just after an agent's message arrived cleared the pending reads,
the open thread's included: the list showed the new message and the thread
did not (seen in inbox-replaced.spec.ts under load). It has its own timer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes for #2028:
- A Send waits for the draft's saves before it posts, and each thread has a
  generation that Send and Discard raise, so a save queued earlier does
  nothing: a slow reply can no longer bring the words back as a draft.
- clear_draft names the updated_at the sent words were saved as; the server
  clears that draft only, so a newer draft from another window stays. The
  foot's Send (box closed) reads the draft again, and an event: drafts for
  the open thread refreshes its copy.
- A save sent as the page goes may name base_body (the words the window
  saved last), so a base a step behind no longer drops it.
- Agents read a thread's section without the person's unsent work.
- Search drops a thread's cached facts when its draft or a comment changes.
- A row's unsent comment count is kept per thread instead of scanned.
- inbox-reply-drafts.ts names contract 7.61.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-ask (#2018) took contract 7.61, so the reply draft's door row is 7.62.
A Re-ask that waits keeps its row in Sent (or Quiet after a Mark read) even
when the thread holds unsent work; the row still shows its Draft label.
… not marked again

- list.ts test for the merged rule: a Re-ask that waits keeps the row in
  Sent (Quiet after a Mark read) whatever the draft; unsent work keeps it
  out of Quiet otherwise.
- A thread whose Mark read still stands is no longer offered Mark read
  (the row's hover, Mark all as read): unsent work can keep it in New, and
  marking it again then undoing dropped the earlier Mark read (found by
  the project menu's property test once Re-ask and drafts met).
- store.test.ts: a brace the merge dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es over

Re-review fixes for #2028:
- A Send claims the thread when it starts: the words waiting are saved,
  every save in flight is answered, the generation is raised and no save of
  that thread runs until the Send ends or fails (then the words are saved
  again). The box is read-only meanwhile. Typing during a slow Send no
  longer brings back a draft holding the sent words.
- A save the server refuses (409) is never retried over the newer state:
  the window adopts the draft as the server holds it and keeps its words,
  not saved, with Keep mine / Use theirs. A tab whose draft another tab
  sent no longer re-creates it. Bases are kept per thread, so the save on
  leaving a thread no longer fetches the latest base and writes over it.
- inbox-mobile.md: the draft's allowlist row names 7.62.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ict is smaller

Round 3 fixes for #2028:
- reload (on event: drafts) takes the box's words: unchanged, nothing moves;
  no edits since the last save or read, the box takes the new words or
  closes when the draft is gone; edits, the base stays, the save is
  refused and the conflict shows. Sent words no longer come back, and
  another window's newer words are no longer overwritten.
- The conflict shows only when both windows hold words: one line above the
  box with Keep mine and Discard (only this window's words go). It is said
  in the thread's live region, named by the textarea's aria-describedby,
  focus returns to the box after either button, and the buttons sit far
  enough apart that their hit areas never meet.
- An empty box refused shows nothing and never deletes the other draft. A
  Send after a conflict leaves the other window's draft showing.
- A Send whose answer is lost after it went is treated as sent for the
  draft (read back by its idempotency key): its words are never saved
  again by themselves; Send again replays it.
- The restart guard and a beforeunload question ask while words are not
  saved (replyDraftUnsaved, now read by unsentReply).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	packages/inbox/components/InboxList.tsx
#	tests/e2e/playwright.config.ts
…lost

A beforeunload listener on every page made Chromium run it before each
navigation, which changed the order of requests around a reload (seen in
inbox-send-now.spec.ts) and keeps some browsers from caching the page for
Back. unsent.ts now adds it only while files pasted into the box, a save
on its way or a conflict's words would be lost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ds outlive the pane

Round 4 fixes for #2028:
- A Send whose reply may have gone (its answer lost) keeps its exact
  requests per thread (keys, targets, comments, clear_draft); Send again
  with the same words posts them unchanged, so the server replays what went
  and never takes a second reply, comments included.
- When it is not known whether the reply went (the Inbox was down), the
  draft is released, not forgotten: it stays on the server, and the retry's
  clear_draft names it, so no draft of the sent words is left behind.
- Words a conflict left unsaved are kept per thread at module level, so
  closing or switching the thread never loses them; they come back with the
  line when it reopens.
- The conflict's second button reads Discard mine.
- Contract 7.62: the door forwards clear_draft unchanged, so the phone must
  not send it for words it did not prefill from that draft.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	tests/e2e/playwright.config.ts
Round 5 fix for #2028. The retry plan replayed old requests whenever the
words matched, so a later reply could go to an old message, a newly pasted
file could be dropped and a new comment left out. It is gone. After a
Send whose answer is lost, the thread is read for the reply's key: found,
the Send is finished (the box closes); not there, the next Send is an
ordinary new one. When the thread cannot be read either (the Inbox is
down), the key, words and carried uploads are kept for 10 minutes, and the
next Send of the same words looks for that reply first: found, nothing goes
again; not there, an ordinary new Send of what is in the box now.

The files a Send leaves the box with are those its requests carried.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s added is named

Round 6 fixes for #2028:
- The remembered unconfirmed Send is forgotten as soon as a thread read
  shows its reply landed (finishing the Send while the box still holds its
  words), when the words change or the box empties, when the person leaves
  the thread, and after 10 minutes. A later reply of the same words is
  always an ordinary Send.
- When the next Send finds the old reply, anything added since (comments,
  picks, files it did not carry) stays for an ordinary Send and is named:
  "Your reply went. Not sent yet: 1 pick. Press Send to send it." Said in
  the live region on every path.
- A thread read answered 404 says the thread was deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y is

Round 7 for #2028:
- When the words change or empty after a lost Send, the error is replaced
  by an honest line once the thread is read ("Your earlier reply went;
  Send sends your edited words as another reply.", or "may have gone"),
  and a save refused because this window's own Send cleared the draft
  saves the edited words as the new draft, never a conflict line about
  another window (expectOwnClear).
- A thread read that finds the reply names what was added since in the
  live region, the box closed too.
- Cancel after a lost Send reads the reply back; when it went, the draft
  copy is read again, so no stale "Draft · Your reply" foot stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

2 participants