Skip to content

Keep the research composer visible as one surface - #355

Merged
MaggieAppleton merged 8 commits into
mainfrom
design/research-composer-placement
Oct 8, 2026
Merged

MaggieAppleton merged 8 commits into
mainfrom
design/research-composer-placement

Conversation

@MaggieAppleton

@MaggieAppleton MaggieAppleton commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Why

The /research composer could open above the visible document pane and hide its focused field. Nested borders also made it look like a card inside a card.

What changed

  • Place the draft beside its anchor, flipping or pinning it within the visible scroller when space is tight. Keep its width aligned with prose.
  • Reveal the anchor and draft after opening or a focused viewport resize, while yielding immediately to reader input and reduced-motion settings.
  • Make the composer one surface with inline status and errors, and restore focus for retry after a failed start.
  • Restore a saved scroll offset only when the scroller is created, so normal smooth scrolling is not cancelled.
  • Apply the reviewed design-contract hashes for the placement and scroll changes.

Known follow-up

Pressing Enter can still jump the document scroller because of Lexical caret scrolling. This PR keeps the research composer visible when that happens; it does not change the underlying jump.

Verification

The 36 focused placement and research tests, bun run types, and bun run ci passed locally on the rebased commits. Required GitHub checks will run on the final published head.

Visuals

Before: Composer above visible pane

After: Composer near top of pane

Near bottom: Composer flips above anchor

Phone: Composer in phone viewport

@MaggieAppleton

Copy link
Copy Markdown
Collaborator Author

Review: Changes needed (one small visual bug; the rest is good)

Reviewed in a real browser (headless Playwright against a fake-GitHub server on this branch), desktop 1440x900 and phone 390x844, plus a 30-80ms frame sequence. CI: only the design-contract review hashes fail (research.tsx, plan-editor.tsx), which is expected and safe; e2e and container pass.

Findings

  1. Draft rides over the tab bar when its anchor is partly scrolled out (medium). placement.ts placeDraft: when anchor.bottom < topEdge it returns side: "away" at top: below, with no clip. The draft is position: fixed; z-50, so while the anchor's bottom is between the scroller top (96px) and the viewport top, the whole card draws over the Document/Decisions tab bar (measured: anchor bottom 28px, draft 28-147px; screenshot D-partial.png shows the "Document" tab covered). The PR text says the draft "goes with" the anchor, but it escapes the pane. Suggested fix: clip the draft to the scroller rect (e.g. clip-path: inset(...) computed in place()), or hide it (visibility: hidden) once its top is above the scroller top and the anchor's last line is out of view.
  2. Opening a draft whooshes the document (low, inherited from the known Enter-jump). Enter in the slash menu jumps the scroller to the bottom, then the reveal smooth-scrolls back thousands of px (e.g. 3901 -> 1181 in about 850ms). The draft's enter fade (150ms) finishes off-screen, so the user never sees the scale/fade. Whatever scroll position the reader started at, the final state is the anchor line flush at the top edge (draft y=128), not where they were. Suggest: record scroller.scrollTop at open and use it as the reveal target if the draft fits there (or scroll instantly when the distance is over about one viewport). Fixing the jump itself makes this moot.
  3. Table cell (nit). In a cell the draft anchors to the block before the table (a list, in my test) and sits above the table, covering the column rail. Same anchor logic as main, so not a regression; just noting.

Verified OK

  • Placement near the top, middle and bottom of a long document, in a list, and in a table cell: always fully inside the pane; below the anchor when it fits, content space is reserved so prose is not covered.
  • Scrolling while open: the draft tracks its anchor exactly (draft top = anchor bottom at every offset) except finding 1.
  • Window resize 1440x900 -> 1000x600 -> back: the draft stays inside the pane and re-places.
  • Phone 390x844, then shrunk to 470px (simulated keyboard): the draft stays visible under its anchor at full column width; recovers when the viewport grows back.
  • Long brief (1230 chars): the textarea scrolls internally and the card does not grow, so it can never exceed the pane.
  • Failure (forced 500, and a real server refusal): inline red message in the footer, send stays enabled, focus returns to the textarea.
  • Escape and the x button both close the draft and return focus to the editor. The x is top-right and send bottom-right, consistent with the composer.
  • Reduced motion: no transition, and the reveal scroll is instant.
  • Single surface: hairline edge, --radius-lg, focus ring on the card, quiet footer. It reads consistently with the research card and composer.
  • PlanEditor scroll-restore: switching Document -> Decisions -> Document restores the offset (900 -> 0 -> 900), and switching documents resets to 0 exactly as on main (checked against the 8795 reference). No regression.
  • Not verified live: the successful submit (card appears and stays), because the fake server runs with the Planner off (port 8797 busy) and the request is refused. The new e2e covers placement but I could not confirm the success card here.

@MaggieAppleton

Copy link
Copy Markdown
Collaborator Author

Thanks. Addressed in 0f995d2.

  1. Draft over the tab bar. Fixed. A new pure helper, draftOverflow() (unit-tested), measures how far the fixed draft runs past the visible scroller. place() clips that part with clip-path: inset(...). A partly scrolled-out draft now ends cleanly at the pane edge and reappears as you scroll back. The new e2e test checks this: it wheels the anchor 40px past the top, then asserts the draft is clipped and nothing shows above the scroller.
  2. Whoosh.
    • Checked without End: place the caret by clicking, wait about 300ms, then type /research and press Enter. The scroller doesn't move (431 → 431), and the draft opens in place at y=345.
    • The jump reproduces only when End is pressed, or when Enter comes a few ms after the click (scripted timing). That matches Guard editor scroll position across Enter #368.
    • Defence anyway: revealTarget() (unit-tested). The opening reveal returns to the scroll offset the reader had at OPEN_RESEARCH_COMMAND if the draft fits there. Otherwise it scrolls the minimum, and it jumps instantly instead of gliding when the trip is longer than the pane.
    • The new e2e now places its caret by clicking the end of the line instead of pressing End.
  3. Table-cell anchor. Noted. The anchor logic is unchanged from main, so I left it out of scope.

Also from the earlier round: any key, pointer, touch or wheel input now ends the reveal, so it never fights the reader. CI: e2e passed, 388 tests with 1 unrelated flaky, responsive-workspace.e2e.ts:324. The only CI failures are the expected design-contract hashes. research.tsx is now 9efb66508a77c1cce3bf057c61f62f50db047ab318b9928238aefc922a5afa4c; plan-editor.tsx is unchanged at 48d50ab1….

MaggieAppleton and others added 7 commits October 8, 2026 14:48
Place the /research draft with a unit-tested placeDraft(): below its anchor
block when it fits, flipped above near the bottom edge, pinned when neither
fits, and clamped to the visible scroller inside the visual viewport. On open
and on viewport resize (on-screen keyboard) the scroller reveals the anchor
line and the draft.

The composer is now a single field surface with the focus ring on the card,
inline status and error text in its footer, and popover enter/exit motion.
Focus returns to the question after a failed start, and a draft whose
reference the server rejected (document rebuilt right after placement)
reopens with an inline error instead of vanishing.

The plan scroller no longer writes back every reported scroll offset, which
cancelled smooth scrolls; it restores the saved offset only on (re)mount.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rebuild could misfire it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fast click elsewhere right after opening could be pulled back by the
remaining reveal window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A draft whose anchor is partly scrolled out no longer draws over the
document tabs. The opening reveal goes back to the scroll offset the
reader had when the draft opened if it fits there, and jumps instead of
gliding when the trip is longer than the pane. The new e2e places its
caret by clicking, since End scrolls the document on macOS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaggieAppleton
MaggieAppleton force-pushed the design/research-composer-placement branch from 4f4906b to 243ec28 Compare October 8, 2026 13:49
@MaggieAppleton
MaggieAppleton enabled auto-merge (rebase) October 8, 2026 13:49
@MaggieAppleton
MaggieAppleton merged commit 03595a8 into main Oct 8, 2026
4 checks passed
@MaggieAppleton
MaggieAppleton deleted the design/research-composer-placement branch October 8, 2026 14:17
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.

1 participant