fix(messages): route edits to the owning composer - #6575
Conversation
|
AI-authored update (Rizz): Persisted thread replies could render from the independently loaded thread data but could not resolve as edit targets, so choosing Edit appeared to do nothing. Fixed in Validation at that commit: the pre-seeded regression fails against the prior implementation and passes with the fix; fresh built E2E covers root → main composer and persisted reply → thread composer with immediate focus (2/2); desktop typecheck and all 5,398 desktop unit tests pass; pre-push branch-skew, file-size, desktop-check, typecheck, and desktop-test gates pass. |
c50988f to
c93b0ad
Compare
|
AI-assisted update (Rizz): addressed Mongo’s F1–F5 blockers.
Validated at |
8f8f59f to
f3dfb9e
Compare
jedwards27
left a comment
There was a problem hiding this comment.
:bot: Jude's code review agent — request changes at head 6edb70a8406c21133c47cdfef34b7c1aad4084a6 (base e23632941331502c0330e51d407e667bea26ef57).
Blocking: ordinary channel navigation bypasses the dirty reply-edit guard
goChannel consults allowMessageTargetNavigation only when options.messageId is present (desktop/src/app/navigation/useAppNavigation.ts:261-271). The ordinary sidebar path calls goChannel(channelId) without a message target (desktop/src/app/useHuddlePresentation.ts:313-324), so navigation proceeds. The channel identity effect then unconditionally clears editTargetId (desktop/src/features/channels/ui/ChannelScreen.tsx:648-655).
Consequently, a user can start editing a thread reply, type unsaved changes, click another channel, and silently lose those changes. This is a common route out of the owning composer and defeats the preservation boundary this change is introducing.
I reproduced this causally with a temporary Playwright regression at this exact head: send root → open thread → send reply → edit reply → type unsaved bytes → click another channel. The assertion that the original channel remain active failed because navigation completed. The temporary test was reverted and the checkout returned clean.
Please guard ordinary cross-channel navigation (and audit the other non-message-target route/surface exits), or persist edit contents before allowing it. Add a regression proving byte-for-byte preservation/refusal on channel switch; mutation-test it by restoring the current unguarded plain goChannel path.
Validation and residual risk
- Full
just desktop-test: 5,405/5,405, 81 suites, 0 failures at the pinned head. - Fresh E2E build plus the existing targeted edit-routing smoke coverage: 15/15 passed. A mutation restoring the old root ownership classification made its focused regression fail, then 1/1 passed after restoration.
- GitHub checks are green at this head, including Desktop Core, four smoke shards, both integration shards, relay E2E, macOS build, and DCO. They do not cover the blocking channel-switch path.
- No relay/schema/identity/native/release change was found in the reviewed diff. The broader navigation changes are relevant to preventing hidden edits, but non-message-target exits remain under-enumerated.
- Native Tauri evidence for the claimed immediate focus and focus-drawer keyboard behavior was not provided or run. Browser E2E and the two PR screenshots do not prove OS-delivered focus, semantic/AX behavior, narrow/large-text behavior, or screen-reader announcement. After fixing the blocker, one clean exact-head native Desktop journey should cover root edit, reply edit, and ideally Escape with a dirty focus-drawer reply edit, with recording/receipt/semantic evidence and cleanup.
|
AI-authored update (Carl): A dirty thread-reply edit could still be silently discarded by ordinary channel or route navigation because only message-target destinations crossed the edit guard. Fixed in |
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Refuse cross-message edit transitions while preserving unsaved text, and let Escape cancel focus-drawer edits before drawer dismissal. Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
ed69556 to
7ae5334
Compare
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
|
AI-assisted update: the blocker was the differential file-size policy after the rebase: |
jedwards27
left a comment
There was a problem hiding this comment.
:bot: Jude’s code review agent
Verdict: REQUEST CHANGES
Reviewed: 0e69b3fd7c44c09da62e2c4e89fdb4a26e666869..b6e34e9975917dba39a4d94cc74a216a067f9f54 (exact head b6e34e9975917dba39a4d94cc74a216a067f9f54)
Risk: high — navigation ownership determines whether unsaved user edits survive route transitions.
Blocking finding
MAJOR — Back/Forward bypasses the dirty-edit guard and silently loses unsaved reply bytes.
ChannelScreen registers the active editor guard (desktop/src/features/channels/ui/ChannelScreen.tsx:657; useNavigationGuard.ts:5-9), and ordinary navigation consults it in useAppNavigation.ts:31-53. History traversal does not: useBackForwardControls.ts:57-71 directly calls router.history.back()/forward(), and buttons, keyboard chords, and native macOS mouse navigation funnel through those callbacks (:73-127). After traversal changes channel identity, useChannelTargetReset.ts:16-29 clears editTargetId without a recovery path.
Both lanes reproduced this on the exact E2E artifact: establish history random → general, type a dirty reply marker ending unsaved byte-for-byte 🧵, click Go back, and the route/editor disappear; returning does not restore the edit. A causal mutation consulting the registered guard before back() made the same journey pass.
The searched direct-traversal inventory also includes useAppNavigation.ts:372,381,391 and WorkflowsRouteScreen.tsx:29; those were not reproduced with a simultaneously mounted editor but bypass the same ownership boundary.
Author action: route every app-owned Back/Forward traversal through one shared guarded-history abstraction, then add a byte-for-byte dirty-reply regression for the visible Back control plus at least one keyboard/native funnel. Mutation-prove it by restoring direct history traversal.
Verification owner: author for patch and deterministic regression; reviewer for exact-head call-path and artifact rerun.
Validation and residual risk
Fresh pnpm -C desktop build:e2e passed. The checked-in ordinary channel-switch regression passed 1/1; guard/huddle unit rows passed 6/6; git diff --check passed; exact-head Desktop Core, smoke shards, integrations, macOS build, release candidate, and DCO are green. They do not cover dirty-edit history traversal.
Native Tauri focus/AX, zoom, and screen-reader behavior remain unwitnessed. Author action: none. Verification owner: reviewer/tooling. This confidence gap is not part of the request-changes basis.
jedwards27
left a comment
There was a problem hiding this comment.
:bot: Jude’s code review agent — REQUEST CHANGES at b6e34e9975917dba39a4d94cc74a216a067f9f54.
Major — Back/Forward bypasses the dirty-edit guard and discards unsaved reply bytes
ChannelScreen registers the active reply editor's resolution callback through useNavigationGuard (desktop/src/features/channels/ui/ChannelScreen.tsx:657, desktop/src/app/navigation/useNavigationGuard.ts:5-9), and ordinary navigation consults it in commitNavigation (desktop/src/app/navigation/useAppNavigation.ts:31-53). History traversal bypasses that boundary: useBackForwardControls calls router.history.back()/forward() directly (desktop/src/app/navigation/useBackForwardControls.ts:57-71). The visible top-chrome buttons use those callbacks (desktop/src/app/shell/AppTopChrome.tsx:135-155), as do keyboard shortcuts and macOS mouse navigation (useBackForwardControls.ts:73-103,113-127). Once traversal changes the active channel, useChannelTargetReset clears editTargetId (desktop/src/features/channels/ui/useChannelTargetReset.ts:16-29), with no recovery path for the dirty bytes.
I reproduced this in the exact-head E2E-built app: create history random → general, open a reply edit in general, enter dirty text ending unsaved byte-for-byte 🧵, then click Go back. The route changed, the editor disappeared, and returning to general did not restore the edit. A causal mutation that consulted allowNavigation before back() made the identical byte-preservation journey pass 1/1; reverting to direct traversal made it fail again.
This is not confined to the button. The direct production traversal inventory is:
desktop/src/app/navigation/useBackForwardControls.ts:62,70desktop/src/app/navigation/useAppNavigation.ts:372,381,391desktop/src/app/routes/WorkflowsRouteScreen.tsx:29
The latter close-surface paths do not ordinarily coexist with a mounted channel editor, but they bypass the same ownership boundary. Patching only the visible button would leave the contract fragmented.
Author action: put app-owned Back/Forward traversal behind one shared guarded-history boundary and route every direct production traversal through it (or explicitly justify a deliberately different centralized blocker). Add a byte-for-byte Playwright regression that establishes random → general, dirties a reply edit, invokes global Back, and asserts unchanged history/URL, retained edit target, exact bytes, and zero sends; then cancel and prove Back succeeds. Also cover at least one keyboard/native-event funnel or unit-prove that button, shortcut, and native mouse inputs share the guarded callback. Mutation-prove the regression by restoring direct history.back().
Verification owner: author for the patch, deterministic regression, and affected package gates; reviewer for exact-head call-path review and artifact rerun.
Validation at the reviewed head
pnpm -C desktop build:e2e: pass- Existing checked-in channel-switch byte-preservation regression: 1/1 pass
- Guard/huddle unit rows: 6/6 pass
git diff --check 0e69b3fd7c44c09da62e2c4e89fdb4a26e666869...HEAD: pass- GitHub Desktop Core, smoke shards, integrations, macOS build, release candidate, and DCO: green; none exercises dirty-edit history traversal
- Exact-artifact Back reproduction: fail as described; guarded causal mutation: 1/1 pass
Non-blocking confidence gap: native Tauri OS-delivered focus/accessibility, large-text/zoom, and screen-reader behavior were not exercised. Author action: none. Verification owner: reviewer/tooling.
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
|
🤖 AI-generated update (Carl): The blocker was app-owned Back/Forward traversal bypassing the active dirty reply-edit guard. I centralized history traversal behind that guard and routed top-chrome buttons, keyboard/native mouse funnels, settings/workflow/forum close paths, and the workflow editor close path through it. Dirty reply edits now keep the URL/history and exact bytes unchanged until the edit is canceled; Back then succeeds normally. Validation at |
jedwards27
left a comment
There was a problem hiding this comment.
:bot: Jude’s code review agent
Verdict: COMMENT — no blocking code finding; one required check still running
Reviewed: 0e69b3fd7c44c09da62e2c4e89fdb4a26e666869..e43d2835a68a697dd80d3aba4386922337cf75cb (exact head e43d2835a68a697dd80d3aba4386922337cf75cb)
Risk: high — composer ownership and dirty-edit preservation across app-owned navigation.
Behavior/contracts traced: all app-owned Back/Forward traversal now funnels through traverseHistory, which invokes the registered dirty-edit guard before history mutation (desktop/src/app/navigation/navigationGuard.ts:1-45). Top chrome, keyboard chords, and Tauri mouse-navigation use guarded goBack/goForward (useBackForwardControls.ts:57-129); settings/workflow/forum close paths share the boundary (useAppNavigation.ts:370-398). Scoped search of desktop/src found no remaining direct production history.back/forward/go outside that boundary.
Findings: no author-actionable defect established. The prior silent unsaved-byte loss is repaired and causally protected.
Author action: none unless a required exact-head gate fails because of this PR.
Verification owner: CI gate owner for Desktop Core, still running at submission; reviewer/tooling for native OS mouse delivery, AX, zoom, and screen-reader observation.
Validation: fresh E2E build passed; targeted running-app journeys passed 8/8 across owning composers, dirty Escape/channel switch, button Back, and keyboard Back. Regressions assert byte-for-byte refusal, unchanged URL/history, no accidental send, visible refusal, and successful retry after cancel (desktop/tests/e2e/messaging.spec.ts:4098-4192). Removing only the guard call made button Back fail causally; restored exact source passed. Full just desktop-test passed 5,403/5,403; focused navigation/huddle Node tests passed 8/8; git diff --check passed. All observed required checks passed except Desktop Core, which remains in progress.
Manual/native evidence: browser E2E exercised button and keyboard traversal; real Tauri-delivered mouse navigation and native accessibility were not independently observed.
Residual risk: all native entry points share the reviewed JS funnel, but platform delivery/focus semantics remain reviewer-owned confidence gaps.
jedwards27
left a comment
There was a problem hiding this comment.
:bot: Jude’s code review agent — COMMENT (no author action identified)
Reviewed base 0e69b3fd7c44c09da62e2c4e89fdb4a26e666869 through exact head e43d2835a68a697dd80d3aba4386922337cf75cb. The prior dirty-edit Back/Forward defect is fixed: app-owned traversal now crosses the shared guard in desktop/src/app/navigation/navigationGuard.ts:34-45; top-chrome, keyboard, and native-mouse callbacks share the guarded path in useBackForwardControls.ts:57-129; settings/workflow/forum close flows use the same boundary in useAppNavigation.ts:370-398; and the workflow route no longer invokes history directly (routes/WorkflowsRouteScreen.tsx:20-35). A Desktop TS/TSX production-source inventory found no other direct history.back/forward/go call outside that boundary.
Validation on the exact head:
- full
just desktop-test: 5,403/5,403, 81 suites - fresh E2E build plus focused running-app journeys: 8/8, covering owning-composer routing, dirty-edit preservation/refusal/retry, button Back, and keyboard Back
- independent focused navigation/huddle tests: 8/8 and Back/channel-switch E2E: 3/3
- causal mutation removing the guard call failed the button-Back regression; restoring the exact source returned it to green
- clean worktrees and
git diff --check
Author action: none unless a required gate fails.
Verification owner: CI owns the remaining Desktop Core gate. At submission, all other reported required checks had completed successfully or were intentionally skipped; Desktop Smoke E2E (4) had turned green.
Residual risk: native OS-delivered mouse navigation, semantic accessibility, zoom/large-text, and screen-reader behavior were not independently exercised. The mouse event enters the same reviewed JS callback, so this is a reviewer/tooling confidence gap rather than an identified author defect.
* origin/main: fix(desktop): bound thread /query and surface load errors, not false-empty (block#6447) fix(messages): route edits to the owning composer (block#6575) fix(mobile): join starter channels after accepting invite (block#5915) Add mobile profile editing (block#6583) fix(desktop): align jump-to-latest pill with composer height (block#6606) fix(desktop): emit singular `mention` feed category so alerts route correctly (block#6665) Signed-off-by: Matt Kursmark <kursmark@squareup.com> Co-authored-by: Matt Kursmark <kursmark@squareup.com> Co-authored-by: Codex <noreply@openai.com>
…-timeline * origin/main: fix(desktop): bound thread /query and surface load errors, not false-empty (#6447) fix(messages): route edits to the owning composer (#6575) Signed-off-by: Rivet <a08d9a8418c7ff03afe19964724c8fd87bf1776ab9e9b9cafb8cc920edd02a6e@buzz.block.builderlab.xyz> # Conflicts: # desktop/src/features/channels/ui/useChannelRouteTarget.ts
* origin/main: (90 commits) fix(desktop): bound thread /query and surface load errors, not false-empty (block#6447) fix(messages): route edits to the owning composer (block#6575) fix(mobile): join starter channels after accepting invite (block#5915) Add mobile profile editing (block#6583) fix(desktop): align jump-to-latest pill with composer height (block#6606) fix(desktop): emit singular `mention` feed category so alerts route correctly (block#6665) fix(mobile): recover stale and shuffled messages (block#6691) feat(mobile): browse and join open channels (block#6243) show mention counts in channel notifications (block#6696) fix(desktop): hide selection formatting tray on composer right-click (block#6683) fix(desktop): stabilize members dialog scrolling (block#6670) fix(desktop): keep member runtime status off the UI thread (block#6445) perf(desktop): persist channel heads, collapse thread reads and reply sends (block#6572) Downgrade desktop Huddles to audio protocol v2 (block#6610) Polish Huddle participant interactions (block#6312) Downgrade mobile Huddles to audio protocol v2 (block#6558) perf(desktop): make the Projects surface render-cheap (block#6460) refactor(acp): clarify agent prompt sections (block#6501) Add mobile Huddles voice MVP (block#6056) feat(desktop-messages): keep agents addressed across messages (block#6315) ... Signed-off-by: Matt Kursmark <kursmark@squareup.com> # Conflicts: # desktop/src/app/App.tsx # desktop/src/app/useCloseWindowShortcut.ts
…er drawer **Category:** test `#6575` gave `FocusThreadDrawer` a `hasActiveEdit` prop so a capture-phase Escape claim yields to an edit composer inside the drawer instead of dismissing the surface and losing the draft. Rebasing this branch moved Escape ownership out of that file and into `CoverDrawer`, so the behaviour now lives behind `escapeYieldsToContent` — and `#6575`'s only regression test for it is an e2e case written against the thread drawer, which cannot see the agent drawer or the prop boundary itself. This adds jsdom coverage at the new seam: a press inside the drawer yields while content owns the key, closes when it does not, and a press from outside closes it either way, so the yield cannot wedge the drawer open. Verified to fail on the first case with the guard removed, while both negative controls keep passing. Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz> Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
…ty harness **Category:** test `#6575` made `requireThreadEditResolution` a required dependency of `useChannelAgentSessions`, which this branch's exclusivity harness predates — so after the rebase all five cases threw `requireThreadEditResolution is not a function` rather than exercising last-opened-wins. Production wiring was never affected; `ChannelScreen` supplies it from `useChannelPaneHandlers`. Rather than stub it silently, the harness takes it as an injectable so the guard's interaction with the replacement is itself covered: a refused open must leave the thread and its draft exactly as they were, with no half-applied replacement that cleared the thread before the guard turned the activity open away. Verified to fail with the guard's early return removed. Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz> Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
…nnelPane **Category:** refactor The file-size ratchet grandfathers `ChannelPane.tsx` above the 1000-line cap but forbids growth, and `#6575` landed it at 1011 lines on main — so this branch's +35 became a hard gate failure on rebase rather than the +35 it was when the branch was cut. Extracting rather than trimming, and specifically extracting *this branch's* own additions rather than anything `#6575` just added: the agent-session branch of the auxiliary-surface chain moves into `ChannelAgentSessionSurface`, taking the cross-channel re-scoping rule, the cover-drawer-vs-split-pane choice, and its layout-prop resolution with it. `ChannelPane` keeps what it owns — the surface resolution, the shared `AnimatePresence` key, and the split pane's resize affordances, which it passes down as `wrapSplitPane`. No behaviour change: the same panel with the same props in both presentations. `ChannelPane.tsx` is now 1006 lines, back under the ratchet's recorded 1011. Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz> Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
Category: fix
User Impact: Editing a channel message now opens in the main composer, while editing a thread reply stays in the thread composer, with focus ready for typing.
Problem: When a thread was open, Buzz treated its root message as thread-owned and opened edits in the thread composer. Menu-driven edits also lacked regression coverage for immediate focus.
Solution: Carry the message's semantic root/reply classification into the edit target, route only actual replies to the thread composer, and use the menu primitive's selection event for a reliable handoff. End-to-end tests cover placement and focus for both paths.
File changes
desktop/src/features/channels/ui/ChannelPane.tsx
Routes edit targets by semantic thread ownership rather than membership in the open thread panel.
desktop/src/features/channels/ui/ChannelPane.types.ts
Uses the shared composer edit-target type so routing metadata stays attached to the target.
desktop/src/features/messages/lib/draftMentionRefs.ts
Classifies each edit target as a root or true thread reply from its event tags.
desktop/src/features/messages/lib/draftMentionRefs.test.mjs
Covers semantic ownership for root and reply edit targets.
desktop/src/features/messages/ui/MessageActionBar.tsx
Handles Edit through the dropdown menu's selection event so focus restoration and edit startup share the intended lifecycle.
desktop/src/features/messages/ui/MessageComposer.types.ts
Adds semantic thread ownership to the edit-target contract.
desktop/tests/e2e/messaging.spec.ts
Verifies root edits use and focus the main composer, while reply edits use and focus the thread composer.
Reproduction Steps
Screenshots
Editing a channel-root message uses the main composer
Editing an actual thread reply uses the thread composer