Skip to content

perf(messages): close the editor and delete dialog on submit - #3493

Merged
kalvinnchau merged 1 commit into
mainfrom
peon/perf-close-on-submit
Nov 13, 2026
Merged

kalvinnchau merged 1 commit into
mainfrom
peon/perf-close-on-submit

Conversation

@kalvinnchau

@kalvinnchau kalvinnchau commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Saving an edit kept the composer in edit mode, and confirming a delete kept the alert dialog open, until the relay accepted the operation. The row already showed the change one frame after the press, through the optimistic update, but the editor or dialog stayed up for about one more relay round trip with no visible progress.

Change

  • useMessageEdit.ts: new useCloseAfterPaint(active, close) closes one frame plus one task after an operation starts, so the optimistic row paints before the close render. The editor uses it while its operation is not failed/unknown.
  • MessageManagement.tsx: DeleteMessageDialog uses the same hook in place of its close-on-accepted effect.
  • An empty edit that becomes a delete now hands control back to the composer (draft restored) when the deletion is dispatched, not on relay acceptance. That makes the per-operation acceptance callbacks (completions + observeSend) unused, so they are removed.

Why behavior does not change

Only when the surface closes changes. What is sent, and how it is retried, stay the same.

  • A failure that lands before the close (failed/unknown) keeps the editor or dialog open with its in-place Retry, as before.
  • A failure after the close is shown by MessageManagementStatus. It already outlives a closed dialog and a removed row, and retries the same operation id. That path existed before for presentation loss and navigation.
  • Closing a frame later, not in the same task, keeps the close render out of the press's frame. Closing in the same task raised INP to 112–144 ms, from 64–72 ms, in a local run (n=4).

Evidence

Interleaved A/B on 7 Linux and 7 macOS boxes:

  • 6 rounds × 2 samples per variant on each box, so n=84 per variant per OS.
  • 4× CPU throttle and 150 ms simulated relay latency.
  • Values are the mean of per-box medians.

Timings are taken in the page: the rAF timestamp of the first frame where the condition holds, measured from the input event. "shown"/"gone" is the row showing the edit, or the row being removed. "closed" is the editor or dialog being gone. INP is the Event Timing duration of the press.

The baseline is main (111207e2). The measured change is eed816c3. The final commit adds the empty-edit delete change, the shared hook and tests, and is rebased onto current main.

Edit (Enter in the edit composer)

OS metric main PR Δ boxes better
Linux closed ms 472.1 73.7 −84.4% 7/7
shown ms 67.7 69.9 +3.2% —
INP ms 78.3 78.3 0.0% —
macOS closed ms 427.4 66.2 −84.5% 7/7
shown ms 62.9 62.2 −1.1% —
INP ms 60.0 60.0 0.0% —

Delete (Delete in the confirmation dialog)

OS metric main PR Δ boxes better
Linux closed ms 502.5 112.1 −77.7% 7/7
gone ms 108.6 109.8 +1.1% —
INP ms 104.0 103.4 −0.5% —
macOS closed ms 445.5 77.3 −82.7% 7/7
gone ms 77.5 77.1 −0.5% —
INP ms 65.1 64.6 −0.9% —

LoAF blocking time is flat on both OSes. Script and task time are lower too, but part of that comes from the shorter sample window, so this PR does not claim it.

Video

Recorded in the browser test harness at 150 ms simulated relay latency, with no CPU throttle. Left is main, right is this PR. Both sides are aligned on the edit's Enter. The on-screen timer counts from the press until the editor or dialog closes.

close-on-submit-before-after.mp4

Checks

  • vitest run src/features/messages src/features/conversation: 3329 pass.
  • Two new tests hold animation frames so a failure lands before the deferred close, and they assert that the editor or dialog stays open with Retry, then closes after a successful retry. Both fail when the failed/unknown guard is removed.
  • Updated tests now expect the close on submit and the failure reported by the message status: MessageManagement, MessageComposer, MessageEdit.integration, EmbeddedConversation.
  • Browser specs pass on chromium: message-edit, inbox-edit, message-actions-floating, message-management, owned-agent-message-deletion (35 tests). message-edit and owned-agent-message-deletion now expect the close on submit.
  • bin/pnpm check and tsc are clean.

🤖 Generated with Claude Code

saving an edit or confirming a delete kept the composer editor or the
dialog open until the relay accepted the operation, about one round trip
after the row already showed the change. close them on submit instead,
a frame after the optimistic row update so the press paints first
(useCloseAfterPaint). a failure that arrives before the close keeps them
open with retry; a later one is reported by the existing message status,
which already survives a closed dialog and a removed row and retries the
same operation. an empty edit that becomes a delete now returns the
composer to its draft when the deletion is sent, so the per-operation
acceptance callbacks are removed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Kalvin Chau <[email protected]>
@kalvinnchau
kalvinnchau marked this pull request as ready for review November 13, 2026 20:26
@kalvinnchau
kalvinnchau requested a review from a team as a code owner November 13, 2026 20:26

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 looks good. the shared close hook cancels both the frame and the timer if a failure lands first, so in-place Retry survives, and MessageManagementStatus still owns edits and deletes that fail after the close. two nonblocking notes:

  • nonblocking: since Save and Delete now close before the relay confirms, a later failure only shows up as Retry message update outside the editor or dialog. might be worth a line in docs/buzz-help.md so that recovery path is discoverable.

}}
dispatched={(dialog) =>
// The row hides now; MessageManagementStatus owns a failed delete.
selection.done?.(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nonblocking: on the empty-edit → delete path, done runs editing.close() → restore() synchronously inside the Delete click, so the composer's draft-restore render lands in the press task. that's the same-task close the PR measured as raising INP, and only the dialog close is deferred here. could route done through the same after-paint close if it shows up in the numbers

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Agreed, this is accurate. On the empty-edit → delete path, the draft restore still renders inside the Delete click. That path is rare and the A/B did not cover it, so I have no numbers for it. Routing done through the same after-paint close is a small follow-up. I will add it to this PR if the owner wants it before merge.

@kalvinnchau

Copy link
Copy Markdown
Contributor Author

🤖 Thanks. On the docs/buzz-help.md note: agreed that a failed save after the close now shows only as Retry message update outside the editor or dialog. I am holding off on a docs edit until the owner decides whether it belongs in this PR.

@kalvinnchau
kalvinnchau merged commit f901786 into main Nov 13, 2026
64 checks passed
@kalvinnchau
kalvinnchau deleted the peon/perf-close-on-submit branch November 13, 2026 20:43
joshfriend added a commit that referenced this pull request Nov 13, 2026
* origin/main: (153 commits)
  feat(agents): preview where interrupted agents were working before resume (#3494)
  perf(messages): close the editor and delete dialog on submit (#3493)
  feat(messages): quote selected message text into the composer (#2392)
  perf(toast): scope docked clearance to the toast host (#3489)
  fix(unread): undo exact captured ordinary reads conditionally (#3473)
  fix(solo): keep navigation responsive during chat deletion (#3488)
  fix(channels): keep custom thread tab names until the tab closes (#3458)
  perf(relay): verify cached history in the native host (#3426)
  fix(settings): refine claude and typesafe key setup dialogs (#3391)
  Update bundled help for Bestie and agent settings (#3194)
  feat(profiles): share and show a local time zone (#3332)
  Revert "feat(link-previews): unfurl web links below messages by default" (#3480)
  Show disabled plugin chats in Solo (#3456)
  perf(messages): host one context menu per message list (#3465)
  test(claude): keep the quit-release test copy above the minimum version (#3477)
  fix(profiles): restore verified names from the device cache (#3382)
  fix(mentions): show addressed agents first in Solo audience picker (#2856)
  perf: count member access once and scope find highlights to painting (#3464)
  perf(messages): prepare a message once for its renderer and link previews (#3466)
  fix(composer): speed up long pastes and paste office text as text (#3441)
  ...

# Conflicts:
#	docs/plugin-authoring-guide.md
joshfriend added a commit that referenced this pull request Nov 13, 2026
…when-ready

* origin/main: (21 commits)
  fix(agents): report a conversation wake that fails before its turn starts (#2192)
  fix(usage): read Claude transcripts over 50 MB a page at a time (#3498)
  feat(agents): preview where interrupted agents were working before resume (#3494)
  perf(messages): close the editor and delete dialog on submit (#3493)
  feat(messages): quote selected message text into the composer (#2392)
  perf(toast): scope docked clearance to the toast host (#3489)
  fix(unread): undo exact captured ordinary reads conditionally (#3473)
  fix(solo): keep navigation responsive during chat deletion (#3488)
  fix(channels): keep custom thread tab names until the tab closes (#3458)
  perf(relay): verify cached history in the native host (#3426)
  fix(settings): refine claude and typesafe key setup dialogs (#3391)
  Update bundled help for Bestie and agent settings (#3194)
  feat(profiles): share and show a local time zone (#3332)
  Revert "feat(link-previews): unfurl web links below messages by default" (#3480)
  Show disabled plugin chats in Solo (#3456)
  perf(messages): host one context menu per message list (#3465)
  test(claude): keep the quit-release test copy above the minimum version (#3477)
  fix(profiles): restore verified names from the device cache (#3382)
  fix(mentions): show addressed agents first in Solo audience picker (#2856)
  perf: count member access once and scope find highlights to painting (#3464)
  ...
joshfriend added a commit that referenced this pull request Nov 13, 2026
…n-on-github

* origin/main: (21 commits)
  fix(agents): report a conversation wake that fails before its turn starts (#2192)
  fix(usage): read Claude transcripts over 50 MB a page at a time (#3498)
  feat(agents): preview where interrupted agents were working before resume (#3494)
  perf(messages): close the editor and delete dialog on submit (#3493)
  feat(messages): quote selected message text into the composer (#2392)
  perf(toast): scope docked clearance to the toast host (#3489)
  fix(unread): undo exact captured ordinary reads conditionally (#3473)
  fix(solo): keep navigation responsive during chat deletion (#3488)
  fix(channels): keep custom thread tab names until the tab closes (#3458)
  perf(relay): verify cached history in the native host (#3426)
  fix(settings): refine claude and typesafe key setup dialogs (#3391)
  Update bundled help for Bestie and agent settings (#3194)
  feat(profiles): share and show a local time zone (#3332)
  Revert "feat(link-previews): unfurl web links below messages by default" (#3480)
  Show disabled plugin chats in Solo (#3456)
  perf(messages): host one context menu per message list (#3465)
  test(claude): keep the quit-release test copy above the minimum version (#3477)
  fix(profiles): restore verified names from the device cache (#3382)
  fix(mentions): show addressed agents first in Solo audience picker (#2856)
  perf: count member access once and scope find highlights to painting (#3464)
  ...
joshfriend added a commit that referenced this pull request Nov 13, 2026
* origin/main: (31 commits)
  Let plugin panels name their tab with panels.useTabTitle (#3251)
  fix(agents): report a conversation wake that fails before its turn starts (#2192)
  fix(usage): read Claude transcripts over 50 MB a page at a time (#3498)
  feat(agents): preview where interrupted agents were working before resume (#3494)
  perf(messages): close the editor and delete dialog on submit (#3493)
  feat(messages): quote selected message text into the composer (#2392)
  perf(toast): scope docked clearance to the toast host (#3489)
  fix(unread): undo exact captured ordinary reads conditionally (#3473)
  fix(solo): keep navigation responsive during chat deletion (#3488)
  fix(channels): keep custom thread tab names until the tab closes (#3458)
  perf(relay): verify cached history in the native host (#3426)
  fix(settings): refine claude and typesafe key setup dialogs (#3391)
  Update bundled help for Bestie and agent settings (#3194)
  feat(profiles): share and show a local time zone (#3332)
  Revert "feat(link-previews): unfurl web links below messages by default" (#3480)
  Show disabled plugin chats in Solo (#3456)
  perf(messages): host one context menu per message list (#3465)
  test(claude): keep the quit-release test copy above the minimum version (#3477)
  fix(profiles): restore verified names from the device cache (#3382)
  fix(mentions): show addressed agents first in Solo audience picker (#2856)
  ...
philazar added a commit that referenced this pull request Nov 13, 2026
…ve-client

* origin/main:
  feat(solo): smooth chat recency reordering (#3502)
  Reject self-mentions that cannot wake another session (#3513)
  feat(status): offer don't clear for every status (#3515)
  Let plugin panels name their tab with panels.useTabTitle (#3251)
  fix(agents): report a conversation wake that fails before its turn starts (#2192)
  fix(usage): read Claude transcripts over 50 MB a page at a time (#3498)
  feat(agents): preview where interrupted agents were working before resume (#3494)
  perf(messages): close the editor and delete dialog on submit (#3493)
  feat(messages): quote selected message text into the composer (#2392)
  perf(toast): scope docked clearance to the toast host (#3489)
  fix(unread): undo exact captured ordinary reads conditionally (#3473)

Signed-off-by: Philip Azar <[email protected]>
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