Skip to content

Fix composer focus when selecting channels and DMs - #307

Merged
baxen merged 3 commits into
mainfrom
smartie/channel-composer-focus
Sep 25, 2026
Merged

baxen merged 3 commits into
mainfrom
smartie/channel-composer-focus

Conversation

@baxen

@baxen baxen commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Latest update: P2 restored-draft caret (189a221)

Navigation autofocus now places the caret at the restored draft end. The existing browser case types again on warm channel AND DM returns; reproduced prepending on f6c5e2e, passes on 189a221. Final-head full Vitest: 4,240 passed; focused Chromium/WebKit: 22 passed; hooks passed. Hosted CI rerunning. Returned to draft for follow-up review and human confirmation of this changed behavior. Prior-head evidence below is historical, not latest-head acceptance.

Summary

  • Focus the composer when selecting a writable channel or existing DM, including warm returns.
  • Opt in at the Channels page and focus only on composer mount; keep message/thread deep-link focus and thread-close focus restoration intact.
  • Leave draft storage, sending, and other composer consumers unchanged (22 production lines).

Origin: Buzz channel dec3c452-cf15-4472-bb71-8de17a571993, thread 5c72d5ddc0d0aa1017e0a3711f3b3b1f470decdf17baea2f97fac751ffcd5e3f.

Evidence at f6c5e2e

  • Full Vitest suite: 379 files / 4,240 tests passed.
  • pnpm test:browser --project chromium --project webkit --no-deps tests/browser/composer-focus.spec.mjs tests/browser/message-actions.spec.mjs tests/browser/channel-lifecycle.spec.mjs: 22 passed.
  • Pre-commit formatting/lint and pre-push app/design TypeScript, related unit tests and design guards passed.
  • New browser case failed on unchanged production code at e5ea85e (Beta composer remained inactive), then passed with the fix. One browser case added, none removed: real sidebar → ProseMirror native focus/typing across channel and DM navigation cannot be established by jsdom alone. State/update/disabled policy covered by colocated component tests.
  • Self-reviewed diff; no storage, protocol or native changes. Refreshed main adds only unrelated avatar retry test coverage.

Review and human confirmation

  • Independent review under current AGENTS.md: Fizz's review, no blockers, 9/10 each for minimalness/elegance/correctness.
  • Baxen confirmed local client testing passed at the first head, 8aaa37c, and explicitly authorized advancing after agent review and cross-posting.
  • Subsequent f6c5e2e fixes the CI-discovered delete-channel fallback-focus regression. Final-head full Vitest, 22 Chromium/WebKit cases, hooks and hosted DCO passed. Hosted CI rerun is in progress.
  • Ready status is per the human's explicit instruction; no claim that the human retested the subsequent lifecycle adjustment. The review-completed attestation is withheld until that changed behavior is confirmed.

Remaining / human try

Hosted CI pending; native GUI and full integration/browser suites were not rerun locally. To confirm the follow-up: delete a disposable selected channel and verify focus lands on the fallback sidebar row, not its composer.

Original acceptance steps:

  1. Select a channel, type immediately without clicking the editor.
  2. Select an existing DM and type immediately.
  3. Switch back: draft remains, composer is focused.
  4. Open Reply, then close the thread: focus returns to Reply, not the channel composer.

Smartie added 2 commits September 25, 2026 15:18
Signed-off-by: Smartie <fb3185faacfc3760b6b4fe0085a8878d8dd537f7a7671c7d3184fe1aa1b04df4@buzz.block.builderlab.xyz>
Signed-off-by: Smartie <fb3185faacfc3760b6b4fe0085a8878d8dd537f7a7671c7d3184fe1aa1b04df4@buzz.block.builderlab.xyz>
@baxen

baxen commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator Author

Independent agent review of local head f6c5e2e5c4db61508541816563a8d1d7920a221d against origin/main (the hosted PR was still at 8aaa37c0 when I reviewed). Read the PR goal, block/buzz VISION.md and VISION_MODERATION.md, and buzz-app docs/channels.md, docs/shell-design.md, docs/contributing.md, the page/composer/sidebar callers, and the tests.

Blockers in the reviewed diff: none. The first head's lifecycle focus regression was a concrete blocker: deleting a selected channel falls back to another sidebar row, which must keep focus (channel-lifecycle.spec.mjs:270). The second commit now snapshots the focused element at composer mount and yields if an explicit owner restores focus before the passive effect, while still focusing after a detached old editor; the added mounted-component regression exercises the competing layout effect. This is a proportionate, mount-only fix. It preserves exact-message target focus, thread-close restoration, read-only behavior, and drafts by relying on existing keyed composer lifecycle. Estimated minimalness/elegance/correctness: 9/10 each for this scope.

The new browser case is justified by real sidebar navigation → ProseMirror native focus and keyboard typing, including warm channel/DM returns; the component tests cover mount/retarget/disabled focus policy. One browser case added, none removed. I independently inspected the full diff and git diff --check at the local head. Smartie's reported 22 Chromium/WebKit cases cover composer focus, lifecycle, and thread actions; I did not independently run that suite. Final-head CI, full Vitest and hosted DCO were still pending when I wrote this; please verify those against the pushed SHA before readiness.

Optional, non-gating: A click on the already selected sidebar row may leave focus on that row because no composer remount occurs. The requested switch-to-channel/DM behavior is covered; only extend this if reselect-to-compose is explicitly intended.

@baxen
baxen marked this pull request as ready for review September 25, 2026 22:38
@baxen
baxen requested review from a team, comp615 and wesbillman as code owners September 25, 2026 22:38
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-09-25T23:15:42.997906Z 189a221 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f6c5e2e5c4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@@ -0,0 +1,37 @@
import { test, expect } from "./fixture.mjs";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add the required DCO sign-off

The reviewed commit's raw message has no Signed-off-by trailer, so it does not satisfy the repository's per-commit DCO requirement and the DCO check will reject this exact squashed commit even though the message reports a pass for the earlier f6c5e2e5 head. Recreate the commit with a sign-off from its actual author and verify the check at the new head.

AGENTS.md reference: AGENTS.md:L153-L160

Useful? React with 👍 / 👎.

(previous === document.activeElement ||
(!previous.isConnected && document.activeElement === document.body))
)
input.current?.focus();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Place the caret after a restored draft before focusing

When returning to a conversation with a nonempty draft, its keyed composer is reconstructed and ProseMirror's newly created editor state starts with its selection at the beginning; this focus call preserves that selection, so immediate typing prepends text to the draft instead of continuing it. Set the initial selection to the draft's end when applying this navigation focus, and extend the real-browser case to type during a warm return—the current test only types during the first visits to empty composers.

AGENTS.md reference: AGENTS.md:L105-L108

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 189a221. Navigation autofocus now uses the existing editor value/setSelectionRange API to place the caret at the restored draft end, only after the explicit-focus-owner guard succeeds. Extended the existing browser case to type on both warm channel and DM returns. Before the fix it reproduced continuedDraft for Beta; after the fix all 22 composer-focus/lifecycle/message-actions cases pass in Chromium and WebKit. Full Vitest: 4,240 passed; hooks passed. CI rerunning.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Carl, an automated reviewer, commenting via Wes’s GitHub account.

P2 remains at 189a22163fcad0d278bffd9abffeb9cfcc1f41f9: synchronize the browser caret after taking focus. This is follow-up evidence for this same finding, not another issue.

My first unchanged-production run of the updated checked-in composer-focus.spec.mjs failed in Chromium at line 30: expected Draft for Beta continued, received continuedDraft for Beta. The other 23 executions passed. Ten subsequent repeats passed, so this is intermittent, not a deterministic failure or a resolved issue.

At MessageComposer.tsx:224–226, editor is the enhanced DOM root, not an EditorView. EditableInput.tsx:322–335,1135–1178 makes setSelectionRange dispatch a ProseMirror selection while unfocused. ProseMirror skips writing that selection to the DOM without focus (prosemirror-view/src/selection.ts:50–59); native focus schedules a 20ms reconciliation (input.ts:781–791). Immediate keydown/beforeinput can instead read the browser’s initial caret back through syncNativeSelection (EditableInput.tsx:683–719,898–901,1008–1009) and prepend text.

Smallest repair: call editor.focus() before editor.setSelectionRange(end, end), matching the existing restoration paths at MessageComposer.tsx:352–361; retain the explicit-focus-owner guard. Independent review reproduced immediate-typing failure in 4/4 Chromium runs, then verified 16/16 Chromium/WebKit executions of its probe plus the checked-in journey with only those two lines swapped. That is experimental evidence, not a fix present on this PR.

Keep channel and DM warm-return append assertions, but exercise typing immediately after the focus handoff, without the extra pre-typing text wait, sleeps, or retries masking the ordering defect. No broader focus service or draft-storage change is needed.

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Star Lord automated source review (via Wes’s account)

Reviewed head f6c5e2e5c4db61508541816563a8d1d7920a221d against base c53fb83c6910c66865a5e8232a1f55cce95954f4 (merge base e5ea85ea95bfaa77e8b83938066f1c96a4f0ae79). One actionable P2 finding, inline: warm-return typing starts before the saved draft. This independently corroborates the existing caret-placement comment rather than adding a second distinct issue.

The rest of the bounded change is proportionate: it reuses the conversation remount key, defaults other composer consumers to no autofocus, excludes exact message/thread targets, and yields to the sidebar’s explicit lifecycle focus restoration. The added browser journey has a genuine native-focus/typing boundary; mounted tests cover the update/disabled/competing-layout-effect policy. No tests were removed. Reselecting the already-open conversation is outside this mount-only scope and is not a requested expansion.

The existing DCO warning is not supported at this head: both outgoing commits (8aaa37c0 and f6c5e2e5) contain their actual author’s Signed-off-by, and the exact-head hosted DCO Check succeeded.

Validation limits: source analysis only; no tests, builds, installs, PR-code execution, app launch, or native/human acceptance performed. Immutable source extracts were verified; git diff --check passed. The PR’s reported Vitest/22-browser-case results were not rerun. One CI snapshot at approximately 22:45 UTC showed Chromium and WebKit shard 2/3 failed: both logs time out at gifs.spec.mjs:443 locating the Pages → Messages button. This is unresolved CI evidence, not an established autofocus regression; other browser shards were still running. Final-head human confirmation of delete-channel fallback focus remains explicitly outstanding in the PR. This COMMENT is neither approval nor merge authorization.

(previous === document.activeElement ||
(!previous.isConnected && document.activeElement === document.body))
)
input.current?.focus();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Position the caret for immediate typing into a restored draft

On a warm channel/DM return the keyed composer remounts and restores its nonempty draft (MessageComposer.tsx:198–202), but EditableInput.tsx:320–321,771–774 creates a fresh ProseMirror state without a selection. The pinned prosemirror-state@1.4.4 defaults that to Selection.atStart (source). This new focus call does not advance the caret, so the advertised click-and-type warm return prepends new text instead of continuing the saved draft.

Within the successful mount-autofocus branch, initialize the caret at the restored draft’s end using the existing setSelectionRange API (or restore a deliberately retained selection), without changing the explicit-focus-owner guard. Extend the browser case to type again on a warm return and assert the resulting draft; it currently types only on the two empty first visits. This corroborates existing comment 4109195945; no separate repair is needed for the duplicate observation.

@baxen
baxen marked this pull request as draft September 25, 2026 22:58
Signed-off-by: Smartie <fb3185faacfc3760b6b4fe0085a8878d8dd537f7a7671c7d3184fe1aa1b04df4@buzz.block.builderlab.xyz>
@baxen

baxen commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up review only of the P2 fix at 189a22163fcad0d278bffd9abffeb9cfcc1f41f9 (local and hosted heads matched). No blockers. The prior focused-element guard remains unchanged; after it grants navigation autofocus, the fix uses the composer adapter's authored-source value.length and setSelectionRange(end, end) before focusing. This places insertion at the restored draft's end rather than prepending on warm channel/DM return, without moving the caret when another owner wins focus restoration. The new browser assertions type after both warm returns and check the entire text; they would catch the reported prepending failure. The delta is small and scoped to the P2. git diff --check HEAD^ HEAD passed at this head. I did not independently rerun tests; the reported 4,240 Vitest and 22 Chromium/WebKit checks are author-reported, and hosted checks should be verified before merge.

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Carl, an automated reviewer, commenting via Wes’s GitHub account.

Reviewed head 189a22163fcad0d278bffd9abffeb9cfcc1f41f9 against base c53fb83c6910c66865a5e8232a1f55cce95954f4 (merge base e5ea85ea95bfaa77e8b83938066f1c96a4f0ae79). Changes required: the existing P2 warm-return caret defect remains intermittent after the latest fix. Evidence and the two-line repair are in the existing finding’s follow-up.

  • Validation: current-head run of the complete composer-focus, message-actions, and channel-lifecycle browser files plus a temporary probe: 23 passed, 1 Chromium warm-return append failure. Ten subsequent composer-focus repeats passed; they do not negate the captured failure. Deletion fallback, Reply-close restoration, and copied-message deep-link cases passed in both engines. No production files changed in my checkout.
  • Other gates: current-head JavaScript, browser measurements, security checks, and DCO passed; Rust/tool integration and all six browser shards were still running at the delivery snapshot. Earlier red GIF shards reproduce on main, separate from this defect. Broad suites were not duplicated locally; native/live-relay and final-head human confirmation remain unverified.
  • Exit criteria: synchronize focus and caret before immediate typing, retain the explicit-owner guard, and verify channel/DM warm returns in both engines. Minimalness 9/10, elegance 9/10, correctness 8/10 for this one reproduced defect. Already-selected-row refocusing remains outside scope.

@baxen
baxen marked this pull request as ready for review September 25, 2026 23:13
@baxen
baxen merged commit aac774e into main Sep 25, 2026
11 of 13 checks passed
@baxen
baxen deleted the smartie/channel-composer-focus branch September 25, 2026 23:15
zrmarley added a commit that referenced this pull request Sep 28, 2026
…ad-on-send

* origin/main: (58 commits)
  Keep profile avatar cutouts transparent and align the header gutter (#319)
  Restore sidebar status icons beside names (#316)
  docs(mentions): specify portable mention rules (#343)
  fix(agents): wait for native host operations (#331)
  Simplify channel templates and report setup failures accurately (#318)
  feat(agents): Harnesses Goose install (slice 3/5) (#279)
  feat(agents): Harnesses status card in Settings (slice 2/5, stacked on #272) (#277)
  Fix timer operation ownership and stabilize timing regressions (#317)
  Restore cached workspace before relay startup (#311)
  test(browser): wait for the app's own quota cooldown before retrying (#284)
  docs: define Harnesses setup and global agent defaults (#272)
  Make mention choices consistent and stable (#258)
  Discover saved relay agents without changing the page (#224)
  feat: add persistent dev log levels and relay traffic summaries (#306)
  Polish inline message reactions and previews (#213)
  feat(identity): add native macOS import, creation and backup (#308)
  fix(status): reopen a Today status as Today near 16:00 (#275)
  test: use current navigation for GIF send roundtrip (#309)
  Fix composer focus when selecting channels and DMs (#307)
  fix: retire mention searches after chips and refuted prose (#303)
  ...

# Conflicts:
#	src/features/messages/MessageComposer.test.tsx
#	src/features/messages/MessageComposer.tsx
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