Skip to content

Pop-out windows: Slack-style thread windows at parity with the main view - #2896

Open
jawspeak wants to merge 84 commits into
block:mainfrom
jawspeak:design/popout-window
Open

jawspeak wants to merge 84 commits into
block:mainfrom
jawspeak:design/popout-window

Conversation

@jawspeak

@jawspeak jawspeak commented Nov 6, 2026 •

Copy link
Copy Markdown

tl;dr - carefully implemented to be very similar to slack pop-out windows that people expect. Video demo buzz://message?channel=ce6ec97e-b85e-4c84-8992-938a40185053&id=78a1f2ce060ccaecc049a6a1863f6dff2b5914ab757e7ce63389820ee13fe439&thread=a664e10a8bf8b84272b465aa8b17e7fccdf3a985852639ece895d31faf5933d4

Pop-out windows (Slack-style) — channel, DM, Solo and thread windows

Opens a channel, DM, Solo chat or thread in its own desktop window ("#channel — Buzz", "Thread in #channel — Buzz"), with shared drafts, message actions, member avatars, and live agent previews with Stop. A conversation window drills into one of its threads in place with a < back control, as Slack's does. Design and scope checkpoints: docs/popout-windows-design.md (§10 S1, §11 S2, §12 S3; review logs in §9, §11, and §12). Help text: docs/buzz-help.md → "Pop-out windows".

Model

The pop-out is a companion projection (the Huddle companion-window precedent generalized): the main window is the sole signer, outbox, unread and session owner. The companion renders what main projects and asks main to perform bounded operations against the rows it shows — reply, react, edit/delete/send-to-channel, read state, follow, report, plugin-action handoffs. Every operation ends in explicit terminal evidence acknowledged by the companion; absence is never success.

What's in

  • Native (src-tauri/src/popout_window.rs, capabilities/popout.json, popout.html): per-window binding, bounded DTOs, guarded close (unsent work), ⌘W, focus-main, microphone allowed for pop-out labels on the app origin (Linux/Windows hook).
  • Open from a sidebar row's menu → "Open in new window" or ⌘/Ctrl-click the row (channels and Solo chats), the conversation tab menu → "Open in new window", and a message's ⋯ → "Open thread in new window".
  • Conversation windows: timeline with day dividers, join/leave notices, typing/working status line; drill into a thread in place (< back); files kept across the drill; a recording in progress asks before leaving.
  • Appearance (light/dark, accent, contrast, text scale) mirrored from the main window — also fixes the Huddle window, which had the same defect. A dark window opens dark from its first frame (appearance bootstrap in the companion page + native background colour from the design tokens).
  • Identity: the header shows the conversation's tab icon (hash / lock / DM person / Solo bubble / thread mark) and the title names the kind ("#design", "Ada (DM)", "Planning (Solo chat)", "Thread in …"); DMs are named by their people. Sidebar "Open in new window" carries the same icon and sits after Copy link in every row menu.
  • Parity: reply with draft shared with main, reactions + picker, copy, mark read/unread, follow, report, edit/delete/send-to-channel own messages, delete an owned agent's message after main re-verifies ownership, plugin actions (Start Solo chat, Remind me) handed to main and opened there, View activity beside the conversation in main, voice notes recorded in the pop-out and uploaded by main; buzz:// links labelled as in main (Links plugin renderer, channel names, "‹author› in #‹channel›" bylines resolved by main and projected per row) in messages and composer decorations.
  • Exit holds: switching community / signing out lists pop-outs with unsent work and asks first.

Screenshots (native tryout)

A Solo chat in its own window beside the main window: kind icon, title with the kind, the same messages and composer

A message's menu with Open thread in new window after Follow thread An agent's live work on a request, with Stop

Also in this PR

  • Member avatars in the window header (agents for a Solo chat), opening the members dialog / Share access in the main window.
  • Agents at work show as main's live bubble: under the request (or its thread root in a channel window) and in the footer for requests the window doesn't show; Stop is admitted by main and tracked with main's own state owner.
  • Plugin chat icons (e.g. GitHub PR state) in a Solo window's header, through a new describe on registerChatIcon.
  • "Open in new window" with its icon, right after Copy link, in every conversation menu (sidebar rows, Solo header, side-pane header).
  • buzz:// links label as in main; dark windows open dark; the Solo header's and side pane's menus; organization sign-out names held pop-out drafts.

Not in a pop-out (documented)

The members/access dialog opens in the main window. Pop-outs do not restore after restart. Canvas, pages, Settings, and browser/mobile pop-outs are outside this change. Plugin chat icons apply to Solo chats; the community name is not shown in the header/title.

Verification

  • Independent agent review (jaw-gpt56sol) of every slice; findings and closures logged in docs/popout-windows-design.md §9. Reviewer notes under the reviewer's workspace.
  • Published snapshot: 8eecc7e, rebased onto main 741f3a8. The branch owner reported passing pre-push checks (types-and-related-unit-tests, design-system, rust-clippy) and targeted Rust window/microphone tests. Related-unit-test hooks do not establish a full Vitest-suite pass. The owner reported native_mcp::tests::test_overlay_preserves_normal_capability_selection also fails locally on clean origin/main; that check remains unresolved locally.
  • Human (jaw) tested every slice in the native app over 2026-11-09/10 (see the checklist below).

Pre-PR checklist (AGENTS.md)

  1. Agent review ran on every slice (jaw-gpt56sol); all blockers fixed, each closure logged in docs/popout-windows-design.md.
  2. Agent exercise of the native flow: skipped by the human author (allowed by the checklist); the human test below covers the same flows.
  3. Human tested in the native app (jaw, 2026-11-09/10): open channel/DM/Solo/thread windows from every entry point, reply/react/edit/delete/mark read/follow, plugin actions and View activity handed to main, voice note, buzz:// link formatting, dark-mode open, member avatars, agents at work + Stop, plugin chat icon, menus, sign-out with a held draft.

buzz-review-completed

Generated with Codex

@jawspeak
jawspeak force-pushed the design/popout-window branch from d1ff354 to 8ee3b9d Compare November 7, 2026 03:17
@jawspeak
jawspeak force-pushed the design/popout-window branch 5 times, most recently from 4a130fb to 8eecc7e Compare November 10, 2026 23:04
@jawspeak
jawspeak marked this pull request as ready for review November 10, 2026 23:05
@jawspeak
jawspeak requested a review from a team as a code owner November 10, 2026 23:05
@jawspeak

Copy link
Copy Markdown
Author

🤖 @block/buzz-oss-team Could a core maintainer pick up code-owner review for this PR and approve the pending fork CI run? The author cannot add the team through GitHub’s reviewer picker due to permissions. CI run: https://github.com/block/buzz-app/actions/runs/42920275467

jawspeak and others added 20 commits November 11, 2026 13:04
Proposal only; no behavior change. Compares Slack's Open in new window,
records Buzz's Huddle companion precedent and main-only native authority,
and recommends a sliced presentation-companion approach.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
Adopt the composer's existing draft-conflict contract, make Open in main a
focus (not a move), add per-window binding/incarnation, a real thread read
projection, reading-lease based unread, and cite Slack's official help.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
…exits, delayed reading IPC

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
Presentation-only companion windows (popout-<uuid>) modeled on the Huddle
window: main owns identity, relay, signing and read state; each window is
bound to one target and session incarnation; watch/action/download are
gated per label; guarded close asks the companion; Cmd+W closes a focused
pop-out; a main renderer reload detaches companions instead of leaving
them able to act.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
Main-window owner projects a session-owned thread view into a pop-out
companion, reusing the Huddle composer bridge (now with thread reply and
thread draft keys) and a shared companion row projection/renderer. The
companion mounts the real composer, reports held work for native close
guards, and measures reading with useReading over a lease proxy whose
leases live in main. Entry point: message menu → Open thread in new window.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
…d S1 deviations

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
- Reading moves to a request/response BroadcastChannel bridge: a companion
  observation completes only when main's read-state save is durable, so the
  earned lease outlives the save; a disposed lease answers no late request.
- Replies are offered and admitted only once the exact verified root of the
  thread is loaded in its channel; channel availability stays separate.
- A window is owned before native creation: scope change or disposal during
  the await retires it and destroys what native created; failed creation
  releases the reader and composer.
- Web links in a pop-out go to the OS opener; only buzz: links go to main.
- Image review hides Copy image (no media_copy_image authority).
- Linux/Windows: closing main destroys unguarded companions and asks
  guarded ones first; main destruction closes every companion.
- Native tests: caller gating, per-label download isolation, guarded
  close interception, capability contents.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
… before closing any companion

A companion lease request carries the read-intent revision its event saw;
main grants it only while that revision is current, so intent formed before
a manual unread cannot become a new read when it arrives late. Views carry
main's revision and refresh on unread changes. On Linux/Windows main close
asks guarded companions first and closes nothing until accepted.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
… thread tabs

FOUNDATION edit authorized by jaw (thread buzz-feature-popout-window,
2026-11-04): conversation.registerTabAction adds an entry to a conversation
tab's menu after the host's own actions, shown while its matcher passes for
that tab's {channelId, threadRootId?} target. The Pop-out windows plugin
registers Open in new window for thread tabs.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
…; pop-out windows register theirs

FOUNDATION edits authorized by jaw (thread buzz-feature-popout-window,
2026-11-04). The communities service owns exit holds: select() waits as a
pending selection while a registered feature holds work a community exit
would discard, and runs its continuation only once selected. The shell
shows one Switch community? dialog for pending selections and, through the
existing single-slot departure guard composed with unsaved Settings, for
navigations that would change community before their route commits; Keep
working preserves route and community. Navigation-driven selection forces
because the guard already asked; the invite path sequences its open after
selection; the sign-out confirmation lists the holds. Pop-out windows
report held companions (native now forwards the guard action) and
register them through communityReader.holdExit.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
…vigation; join keeps selection pending; organization sign-out asks

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
…ts session revision

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
…enabled by default

The desktop catalog comes from the native plugin manager, not the frontend
bundled list; without this the plugin never activated on desktop.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
jawspeak and others added 23 commits November 11, 2026 13:06
…app resolves

The dark window background used the hover step (#2e2e2e); dark --bg-app is
--neutral-2 (#101010). The pinning test now resolves --bg-app's alias chain
per mode from tokens.css instead of searching for one token anywhere.
The command-capability test gains popout.json, which it had omitted since
the pop-out commands were added.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
…nce too

No second copy of the colour literals: a view reporting only dark builds a
bare Appearance and asks it, so the pinned test covers that path as well.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
The header shows the icon the conversation's tab has (hash, lock, the DM's
person, the Solo bubble; the thread mark for a thread frame) with an
accessible kind label, and a DM is named by its people as tabs and Inbox
name it. The native title carries the kind where no icon can: "#design",
"Ada (DM)", "Planning (Solo chat)", "Thread in …". Main projects kind and
the DM picture; the native DTO validates both.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
…lugin chat icons

Signed-off-by: Jonathan Andrew Wolter <[email protected]>
Main projects a bounded member preview (count, up to three to picture —
agents for a Solo chat, people otherwise, pictures first, names through
the identity service) and requests their profiles as its own header does.
The companion renders the same avatar stack; choosing it opens the
conversation's members in the main window (the conversation itself for a
Solo chat, which has no Members surface). New companion action openMembers.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
…ed before filtering

AvatarStack takes an optional total so a sample of a larger set shows the
right overflow without placeholder items. The projection requests the
bounded roster's profiles as main's header does, so a late agent flag or
picture changes the preview and count.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
Main projects the agents working in a frame's conversation from the same
live evidence its indicator reads (current activity, stop controls, step
and status text, thread scope), bounded for the native DTO. The companion
renders the same incoming-message placeholder with the current step, a row
per agent (View activity opens in main) and Stop. Stop is a composer-bridge
operation admitted by the top frame, which re-derives the controls at that
moment and refuses work that has already ended.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
…main's state owner

A conversation frame now includes work in its threads, as main's channel
indicator does (only a row's own live bubble takes it over there, which
companion rows never have). The stop operation answers as main's interrupt
does ({lifecycle, running}) and the companion drives useWorkStops with it:
lifecycle stops complete, hosted work waits the bounded retry window with
main's notices, nothing-running is reported.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
A plugin's chat icon can now describe itself in data (ChatIcon.describer:
describe + watch), so a host can draw it where the component cannot run.
registerChatIcon takes the describer as a second argument and validates
it. The GitHub plugin describes its linked pull request's state; the
pop-out projects a Solo chat's first describing contribution, watches it
while the frame lives, and the companion draws the design-system artwork
with the sidebar's tone colours.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
…n; watches and lookup made safe

Only the first chat icon contribution is shown, as in the sidebar; one
that describes nothing leaves the default. Watches are reconciled on every
projection, so a Solo chat converted to a channel releases its watch at
once. The catalog lookup is an own-property check, so a prototype key in a
projected token draws the default instead of failing the renderer. Merged
pull requests take the sidebar's purple.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
…ontract now requires

Post-rebase fixup: main's ReadStateStorage gained capture().

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
…ut confirmation too

While server discovery is pending or failed, the root account's own row
is the only one shown; it now receives the exit holds like the listed
rows, so a pop-out's unsent reply is named before signing out.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
…indow

The channel header menu shared the sidebar's action builder and already
had it; the Solo header built its own list. Contributed conversation
actions now render through one shared ConversationActionItems in the Solo
header and the Solo sidebar rows, right after Copy link in each.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
… too

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
… as its header does

The Solo access dialog lives in the main-view header, not on a route. The
pop-out shows the chat in main, then hands the header a session-fenced
request (soloAccessRequests, like message-action handoffs) that only the
header showing that chat in that session takes.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
… it, as main's row does

Main presents work tied to a request message as a live bubble under that
row (step, status, Stop) and keeps its footer indicator for agents no row
presents; the pop-out projected only the footer, so a hosted agent's work
on a request showed as "Only typing updates" with no Stop. Rows a live
turn or local request names as trigger now carry live[], computed with
main's requestWork/workStop; the companion renders the same bubble under
the row; the footer leaves those agents to their rows; Stop admits row
controls too.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
stopWork searched only the replies for row controls; a hosted turn whose
retained trigger is the thread root showed Stop but refused it once its
raw records were trimmed. The root is now searched with the rows; test
covers a hosted turn with a retained trigger and no raw records.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
…channel window; the page no longer scrolls past its content

Main's channel view shows a thread's live work on the thread root; the
pop-out matched only the exact request message, which is not a channel
row, so a request made in a thread had no bubble or Stop there. Live
requests are now mapped to the shown root in conversation frames, with
Stop scoped to the exact request. The companion document is pinned to the
viewport (html/body/#root overflow hidden) so only the message list
scrolls.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
With a waiting and a working request in one thread, the bubble showed the
working one while Stop addressed the first local entry. requestScope now
prefers working, then waiting, then failed, as requestWork does.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
… it, as main does

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
…editor once it connects

The companion composer connects to main asynchronously, so a focus moved
when the thread frame appeared landed on the log. The thread frame now
rests focus on its log and passes that owner through MessageComposer's
autoFocus handoff, which focuses the editor when it is ready and yields to
any focus move meanwhile; covered with the real owner/client bridge.

Co-authored-by: Jaw-fable51-pi <27b9430246cf506d04fedbbb39d0126ad17496a0e41bd82d39e35860de80f3d0@buzz.block.builderlab.xyz>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
Main saves each open pop-out's target, drilled frame and screen frame per
origin:viewer partition and reopens them unfocused where they were once the
relay session is ready and the roster still has the conversation. Closing a
window forgets it; quitting keeps it. Native reports moves and resizes after
they settle and sends no closure notice while the app exits, so a quit never
clears the saved list.

Co-authored-by: Jaw-fable51-claude <7b068d2e5c4ec472d16a8b1241b9d1431d28caef5fa40cd5c0f9c12344b9a08d@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
Co-authored-by: jaw-gpt56sol <ea4d48fe3c0a6de67bdc4128679b983e511642f2579429941cfcdf0eede927fb@buzz.block.builderlab.xyz>
@jawspeak
jawspeak force-pushed the design/popout-window branch from 8eecc7e to 7792ded Compare November 11, 2026 22:24
The pop-out showed every working agent's row and Stop in an always-visible
card under the response preview. Main shows only the compact step cue and
opens its "Working now" chooser on hover or click. Move that chooser out of
TypingIndicator into a shared AgentChooser and render it from
CompanionWorking, so both windows share one implementation and the same
rows, Stop controls and dismissal.

Co-authored-by: Jaw-fable51-claude <7b068d2e5c4ec472d16a8b1241b9d1431d28caef5fa40cd5c0f9c12344b9a08d@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Jonathan Andrew Wolter <[email protected]>
kazemek21 added a commit that referenced this pull request Nov 12, 2026
…ments (#3322)

## TL;DR

Plugins can now register a headless composer annotator with
`ctx.conversation.registerAnnotator`. The host gives it the focused
draft, validates the ranges it publishes, draws the marks, and replaces
text only when the person picks one of the offered replacements, as one
undo step. Checking, language, scheduling, timeouts and wording all stay
in the plugin, so a spelling, grammar or style checker can ship as a
standalone plugin.

## API

```ts
ctx.conversation.registerAnnotator({ id, title, component });
// component: ComponentType<CompletionContext & {
//   observation: { revision, text, prose: { start, end }[] };   // frozen, one per document
//   publish(annotations: ComposerAnnotation[]): (() => void) | false;
// }>
// ComposerAnnotation  = { start, end, text, label, kind?: "spelling" | "grammar", replacements?: { text, label? }[] }
```

The types are exported from `@buzz/author`. The full contract is in
`docs/plugin-architecture.md` under "Composer annotators".

**Who owns what.**

| Host | Plugin |
|---|---|
| Mounting per focused composer, destination and enablement; revoking
`publish` on change, removal or render failure | What to check,
language, ignore rules, tokenization |
| Draft projection, prose ranges and UTF-16 mapping | Debounce,
timeouts, retries, failure policy |
| Validating revision, expected text, prose containment, grapheme
boundaries, labels and replacements; caps | Results, labels, replacement
choice and ranking |
| Underline, `aria-invalid`, right-click / ⌘. menu, focus return,
one-step Undo, IME and read-only refusal | — |

**Disclosure.** An annotator sees what completion providers already see,
under the same eligibility (focused, enabled, outbox-capable composer),
plus the prose ranges. It never sees the caret, and no observation is
offered while unfocused, disabled, read-only or composing.

**Publishing.** `publish` is bound to one observation. It returns
`false` once the draft changed, the destination or enablement changed,
the annotator was removed or failed to render, or the value is not an
array. Otherwise it replaces that annotator's marks and returns a
disposer that withdraws only them. A disposer for an earlier draft
leaves the mapped underlines in place until the next publication, so
per-observation cleanup does not flicker. The menu closes when its
annotation is withdrawn or replaced, even for the same draft.

**Validation and bounds.**
- An annotation must match its text exactly, lie inside one prose range,
not split a surrogate pair or grapheme cluster, and carry a 1–120 unit
label.
- Replacements: at most 5, distinct, at most 64 units, different from
the marked text. An empty replacement deletes the range and needs a
label.
- No control, bidi or line-separator characters and no lone surrogates.
- At most 200 annotations per publication and 400 marks per editor;
earlier annotators win overlaps; at most one redraw per animation frame.

**Applying a replacement.** The host rechecks revision, range, text,
editability and composition; refuses mixed formatting; keeps the range's
formatting; and closes history on both sides so typing, the replacement
and further typing undo as three steps.

## Review notes

- Adds an extension point to three `FOUNDATION` files:
`conversation/contracts.ts`, `conversation/service.tsx` and
`plugins/author.ts`. The change is additive and needs FOUNDATION review.
- The native spellcheck default and autocorrection preference are
unchanged.
- Related open work this does not overlap: native spellcheck default
(#2201, #2546), a generic draft read/replace API (#2656), other composer
work (#2983, #3312). Other open PRs touching the same composer files:
#2988, #3260, #3257, #2896.

## Tests

- `composer-annotations.test.ts` (Node): prose-range exclusions,
same-text revisions, range/grapheme/label/replacement validation and
caps, overlap and merge, mark-preserving replacement and deletion with
Undo/Redo, stale and mixed-format refusal, composition mapping, the
hidden mark on the word being typed.
- `ComposerAnnotators.test.tsx` (jsdom, annotator registered through the
real `PluginRuntime` and `ConversationService`): observation shape and
context, emoji offsets, keyboard and right-click menus, Escape and focus
return, replacement with Undo/Redo, `aria-invalid` kinds, stale and
invalid publications, disposer ownership, menu closing on same-draft
withdrawal or replacement, earlier underlines kept across a stale
disposer, an immediately publishing annotator surviving a destination
change, same-text structural change, retirement on disable, destination
change, plugin removal and render failure, IME composition, disabled and
read-only editors, unmount.
- `MessageComposer.test.tsx`: the real composer wiring, including
retirement on a destination change.
- No browser journey was added; jsdom covers the contract and the
remaining gaps are native-only.

On a Linux remote box against this head rebased on current `main`: `pnpm
check` (Biome, `tsc`, design typecheck and design checks) passed, and
`vitest run` over `src/features/conversation`, `src/plugins`,
`EditableInput`, `MessageComposer` and `composer-annotations` passed (35
files, 1374 tests). The four new lifecycle tests fail against the
previous source and pass with the fix. The local pre-push hook was
skipped because CI runs the same checks.

**Not yet verified:**
- right-click selection and context-menu behavior in native WebKit;
- whether ⌘. reaches the webview on macOS;
- screen-reader announcement of `aria-invalid`;
- real IME composition in the desktop app;
- both themes, narrow and wide widths, and enlarged text, with a real
annotator installed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Kyle Zemek <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
@cvigrassblock

Copy link
Copy Markdown

Just stopping by to say I would love this feature and hope it gets approved. :-)

matt2e added a commit that referenced this pull request Nov 14, 2026
## Why

`user-status.spec.mjs`, "statuses edit, synchronize, clear, reject stale
traffic and retain failed drafts", fails intermittently on WebKit with
an unexplained page error: `Unhandled Promise Rejection: TypeError:
Importing a module script failed.`

| Sighting | Lane |
|---|---|
| [9 Nov,
PR](https://github.com/block/buzz-app/actions/runs/42573912834/job/129017542663),
and again on [its
re-run](https://github.com/block/buzz-app/actions/runs/42573912834/job/129030209036)
| Flake check, WebKit 2/11 |
| [10 Nov,
main](https://github.com/block/buzz-app/actions/runs/42766553248/job/129664645706)
| Browser journeys, WebKit 9/18 |
| [11 Nov,
PR](https://github.com/block/buzz-app/actions/runs/42981535351/job/130393321764)
| Flake check, WebKit 2/12 |

The spec's check for unexplained page errors is deliberate and correct.
The bug is in the product.

## Root cause

`useReactionName` and `ReactionEmoji`'s `revealName` both ran `void
import("./reaction-name").then(...)` with no rejection handler.
`reaction-name.ts` statically imports the emoji-mart native table, so
the first Unicode status or reaction name starts a large chunk load.
When a navigation lands while that load is in flight, WebKit cancels the
request, the `import()` rejects, and the page reports an unhandled
rejection.

In the journey, the click that opens a free-text status starts the load
and the next `page.goto` cancels it. The traces show the chunk request
failing with "Load request cancelled" and the page error 4 ms after the
navigation. The unguarded import arrived with #2384 and moved into
`useReactionName` in #2774.

## Fix

Handle the rejection at both sites.

- A failed load leaves the name unresolved. In the hook the next mount
tries again. `ReactionEmoji` keeps the raw content and the next hover or
focus tries again.
- Both handlers `console.warn` with the error, so a chunk that keeps
failing, such as a 404 after a deploy with a stale shell, stays visible.
The comments note that only a new document fetches the chunk again,
because some engines keep a failed module fetch for the document's life.

The warning does not affect browser checks: the fixture records only
error-level console messages, and the page-error helper watches
`pageerror`. There is no spec or fixture change, and the page-error
check stays as it is.

The second commit is the review follow-up that replaced the first
commit's empty handlers with the warning and made the two unit tests
wait for it.

## Tests

- `use-reaction-name.test.tsx` (new) and a `MessageReactions.test.tsx`
case mock `./reaction-name` to reject. They assert the name stays
unresolved, that the warning is logged, and that no unhandled rejection
reaches the process. They then unmock and show the next mount or reveal
resolves.
- On the unfixed code both fail with the unhandled rejection.

## Evidence

| Scope | Result |
|---|---|
| This branch's head, macOS: `bin/pnpm exec vitest run
src/features/messages/use-reaction-name.test.tsx
src/features/messages/MessageReactions.test.tsx` | 2 files, 29 passed |
| Before the split from a combined branch, Linux: the same two files |
27 passed in 8 of 8 runs, no stderr output |
| Linux, WebKit: `user-status.spec.mjs`, `--workers=1 --repeat-each=8` |
8/8 passed, 11.7 to 12.7 s each |
| Triage, WebKit, with the native table request held across the
navigation | unfixed 3/3 failed with the CI error, fixed 3/3 passed |
| Before the split, Linux: Biome on the touched files, `tsc --noEmit` |
clean |

The count is 29 here and 27 in the commit messages because #3397 has
since added tests to `MessageReactions.test.tsx`.

## Not reproduced, not run

- The cancelled chunk did not reproduce on an idle host without holding
the request, so the WebKit repeat guards against regression rather than
reproducing CI timing.
- Not tried in the desktop app. The user-visible change is the absence
of an unhandled rejection, plus a console warning when the chunk fails.
- The local pre-push checks for this branch (whole-project type check,
related unit tests, design-system check) were not run. The two-file run
in the first row is the only check run on this branch's head. The type
check and Biome results are from the combined branch, where the same
change passed.
- No human has tested this yet, so `buzz-review-completed` is not set.

## Left alone

- A second, unrelated failure of the same journey ("Duration: Today"
read as "8 hours" at exactly 16:00 UTC) is fixed in its own PR.
- In the persistent-failure case each hover or new mount logs another
warning. That is bounded and only visible in developer tools.
- Open PRs #2896, #2120, #2085 and #2009 edit other parts of
`MessageReactions.tsx`, and #2794, #2154 and #2120 edit its test file. A
trial merge of each against this change is clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: mtoohey <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
Co-authored-by: Claude Fable 5.1 <[email protected]>

This branch has not been deployed

No deployments
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