Repository navigation
Conversation
| // recipient filter above applies again on the next send. | ||
| notifyRecovered(session, recoveryKey, { | ||
| draft: captured, | ||
| picks: capturedPicks, |
There was a problem hiding this comment.
🤖 [P2] Retain recovered picks until an unmounted composer returns
If a user selects a model/effort, starts an attachment send, leaves the conversation, and the upload fails before they return, this picks snapshot reaches no mounted listener. notifyRecovered retains missed recovery only when preparationError is present, but ordinary upload failures and cancellation recover without it. On remount, the caption and attachment return while picks initialize empty, so retry falls back to defaults.
Moving the added test’s remount until after upload rejection reproduces the missing selection; its current remount-before-rejection ordering passes. Retain the pick snapshot with the unconsumed recovery record, replay only against the matching saved revision, and retire it on consumption/replacement. Add the unmount → reject → remount regression case.
wesbillman
left a comment
There was a problem hiding this comment.
Changes needed: retain recovered model/effort picks when the upload fails while its composer is unmounted. I independently traced and corroborated the existing P2 finding; the new regression remounts before failure, so it does not cover failure before remount. Please address that finding and add the missing ordering case; no additional findings.
Star Lord’s automated source review via Wes’s account. Head 95fea73970c541c0ce63fe2510b0f519547386dd; base 63f7e9032e531191b119241b256f660f8d10e66e. No tests or app execution; author-reported results were not independently run. Inspected the PR description and sampled frames from its fixture video; this does not establish live-app behavior.
…is recovered A failed or cancelled attachment send restores its draft and files but not the model/effort picks captured for it, so a resend went out at the agents' defaults while the picker showed unpicked. Carry the captured picks in the recovery record and apply them wherever the composer ends up holding the recovered revision unedited: the accepted-draft load, the explicit recovery load, and the usual case where the storage listener has already loaded it before its notification arrives. Conflict paths never take them. Picks are still not persisted with the draft and do not survive a reload. Follow-up to #856: #856 (comment) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
95fea73 to
098745f
Compare
The reconcileDraft parameter shadowed the picks state declared earlier in the component; capturedPicks reads the state, so one identifier meant two things. Rename the parameter to recoveredPicks. No behavior change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
wesbillman
left a comment
There was a problem hiding this comment.
Changes still needed: the naming-only follow-up does not resolve the existing P2 recovery finding. Current source still drops model/effort picks when an upload fails or is cancelled while the composer is unmounted. Retain unconsumed recovery metadata for the matching draft revision and cover unmount → failure → remount; the added test still remounts before failure. No additional findings.
Star Lord’s automated source review via Wes’s account. Head ad32b5a3f4888c8c3f00df6ec52cc46940c787d8; base ecfbb8a5835dd0fb04e48111ef406a975f2c7715. Source-only: no tests or app execution, and no independent CI/runtime verification. PR description and sampled fixture-video frames inspected; not a full-frame video audit.
…urns Recovery records were retained for replay only when they carried a preparation error, so an ordinary upload failure or cancel that landed while the composer was unmounted reached no listener and its pick snapshot was dropped; on return the caption and files came back from storage but the picker showed unpicked and a retry used the defaults. Retain every recovery record that no mounted composer has consumed. The existing remount replay still applies it only while the saved draft is the recovered revision and retires it otherwise, and consumption clears it as before. Add the unmount -> fail -> remount case, which fails without this change. #887 (comment) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
wesbillman
left a comment
There was a problem hiding this comment.
No changes requested: the prior P2 is addressed by retaining unconsumed recovery picks until a matching-draft remount, with the missing failure-before-remount test case now included. I found no new actionable defect in this fix.
Star Lord’s automated source review via Wes’s account; head 66605f9d5f41b846db2e3793b662bfc2c030c289, base ecfbb8a5835dd0fb04e48111ef406a975f2c7715. Source-only: no tests/app execution or independent CI verification; PR description and sampled fixture-video frames inspected, not a full-frame audit or desktop validation.
Follow-up to the #856 review thread: #856 (comment) (Codex P2, "Restore run picks with failed attachment sends").
Problem
In
MessageComposer.tsx,send()captures the model/effortpicks, hands attachments tosendInBackground(...), and synchronously clearssetPicks({}). When that job later fails (upload or publish failure, session abort) or is cancelled, therecovercallback restores the draft and the attachment store re-adopts the files, but the picks are never restored. The gauge button shows unpicked and a resend goes out at the agents' default model and effort — the one place where the composer promises restoration and under-delivers.Fix
RecoveredSendcarriespicks?: MentionPicks; therecovercallback passescapturedPicks(not the recipient-filteredsentPicks— the restored draft has its original recipients and the filter re-applies on the next send).reconcileDraft(restored?, picks?)appliessetPicks(picks ?? {})only when the composer ends up holding exactly the recovered revision, unedited: theacceptedload, the explicit-recovery load, and — the usual case — when thesubscribeViewlistener already loaded that revision synchronously duringreplaceView, so the explicit notification arrives at thecurrent === revision.currentearly return. Conflict paths never take them, so an old pick cannot attach to a later local edit.receiveRecoveryand the deferred-edit effect forward the record's picks.The last bullet is why the production diff is a little larger than the ~10 lines estimated in the review thread: the recovered revision is normally loaded by the storage listener's argument-less
reconcileDraft()beforenotifyRecoveredruns, so the picks have to be applied at that early return as well.Deliberately not done
MentionDraftor draft storage. They remainuseStateonly and still do not survive a reload or a remount without an in-session recovery, by design.Validation
restores the model and effort picks of a failed background upload (remounted: false|true)inMessageComposer.test.tsx: mention the viewer's agent on the real-session upload harness, pick effort High, attach, send, reject the upload, assert the draft and file are restored and the picker hasdata-selectedwith tooltipNext run: High, then Retry + Send and assert the published kind-9 event carries["effort", <agent>, "high"]. The remounted variant covers picks riding in the module-level record across a composer remount. The harness gainedagents2andmembersoptions (mentioned pubkeys must be channel members on the real session).toHaveAttribute("data-selected"); with it, they pass.bin/pnpm vitest run src/features/messages/MessageComposer.test.tsx— 270/270 passed.bin/pnpm typecheck(tsc --noEmit) — clean.biome check --error-on-warningson both changed files — clean.To try it in the app
Before / after
Left:
mainat f9a5e6f (the #856 merge). Right: this PR. Same script on both: mention the agent, set effort to High, attach a file, Send, press Cancel on the upload. Watch the gauge button after the draft and file come back: on the left its tooltip reverts to the default and the popover's slider is back at "Default", and the resent message has noefforttag; on the right the tooltip reads "Next run: High", the slider stays at High, and the published event carries["effort", <agent>, "high"].Recorded in headless WebKit (Playwright) on the
relay-composerbrowser fixture (realChannelsPage/MessageComposer, real upload fetch,buzz.mentionscompletions) with a fixtureAgents2provider for the viewer's agent "Honey" offering models/efforts, since agent types with models only register under Tauri — not the desktop app. Fixture data only.before-after.mp4
🤖 Generated with Claude Code