镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

fix(channels): keep a channel's side panels open when returning to it - #2838

Merged
bostonaholic merged 7 commits into
mainfrom
bostonaholic--channel-panel-restore
Nov 6, 2026
Merged

bostonaholic merged 7 commits into
mainfrom
bostonaholic--channel-panel-restore

Conversation

@bostonaholic

@bostonaholic bostonaholic commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Why

Opening a side panel in a channel, going to another channel, and coming back showed the channel without that panel. Threads already stayed open per channel, but Canvas (and any plugin channelMenu panel) was retired on every channel change, and launcher side panels (Huddles, Todos) were held once for the whole page and cleared whenever the channel changed.

What changes

  • Canvas and channel-menu tabs stay in the channel's workspace when the viewer leaves. They unmount while away and show again on return, after eligibility is checked again. They close when the viewer closes them, the plugin is disabled, or the channel is no longer eligible.
  • Side-column launcher panels (Huddles, Todos) are kept per channel in the same session tab store as threads and Canvas, so they also survive a trip to another page (for example Activity) and back. Closing one whose launcher was replaced since it opened returns focus to the page's fallback target instead of the body. The session store keeps only the panel, channel context and target, no DOM elements.
  • The Terminal bottom drawer is unchanged: it still closes when the viewer leaves the channel. Restoring it would mount the terminal on return, which takes keyboard focus.
  • Reloading Buzz does not restore these panels.
  • Docs: docs/plugin-architecture.md (channel menu panels, channel-header launchers) and docs/buzz-help.md.

Test plan

  • Vitest: per-channel menu tabs keep entries and selection across switches; side panel survives channel switch and page unmount, bottom drawer does not; focus falls back when the trigger is gone; disabling the plugin forgets the panel; a menu tab whose channel became ineligible while away is dropped and does not come back. src/bundled + src/features/panels: 302 files, 5386 tests pass at 45bdb82. tsc clean.
  • Browser (chromium + webkit): channel-header-menu, channel-tabs, todos, huddles, terminal, canvas-history, channel-permissions, channel-modal-dismissal — 87 passed before rebasing onto main; after the rebase, channel-header-menu, channel-tabs, todos — 28 passed at 45bdb82. The Canvas journeys were updated from "leaving retires Canvas" to "Canvas is still open on return with its draft"; the new Canvas journey failed before the fix and passes after (Canvas return check repeated 3× per engine: 12/12). todos.spec.mjs now expects the Todos panel still open after Activity/Bestie → Alpha.
  • Not done: native desktop app check; human test.

Browser cases changed: 2 Canvas journeys and 1 Todos assertion changed to the new behavior; none added or removed.

Noticed, out of scope

  • After returning to a channel, Canvas sometimes shows View and sometimes keeps Edit, depending on whether the return remounts the panel. The draft is kept either way.
  • A side panel whose channel becomes read-only or cached stays hidden instead of closing, and shows again when access returns. Menu tabs close in that case. Documented in docs/plugin-architecture.md.

🤖 Generated with Claude Code

Screenshots

Canvas open in design-review
screenshot-01

Switched to Beta
screenshot-02

Back in design-review: Canvas still open
screenshot-03

After Close and a round trip: stays closed
screenshot-04

Demo (browser fixture, Chromium)

demo.mp4

bostonaholic and others added 6 commits November 5, 2026 18:38
Leaving a channel retired its channel-menu tabs (Canvas and plugin
channelMenu panels), so returning showed the conversation without the
panel the viewer had opened. Keep them in the channel's workspace like
every other tab: they unmount when the viewer leaves, remount after
eligibility is checked again on return, and close only when the viewer
closes them, the plugin is disabled, or the channel stops being eligible.

Co-authored-by: mboston Implementer <932d3b6ebea6cc83e091551ae46d1c7e7f8f7d0fa8e9cef5e6facc8544af413b@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matthew Boston <mboston@squareup.com>
…witches

Channel-header launcher panels (Huddles and Todos in the side column,
Terminal in the bottom drawer) held one opening for the whole page and
cleared it whenever the channel changed. Keep one opening per channel
instead: leaving a channel unmounts its drawer, and returning shows it
again until the viewer closes it. Disabling or replacing the plugin
still closes it in every channel, and re-enabling starts closed.

Co-authored-by: mboston Implementer <932d3b6ebea6cc83e091551ae46d1c7e7f8f7d0fa8e9cef5e6facc8544af413b@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matthew Boston <mboston@squareup.com>
The Canvas journeys asserted that leaving a channel retired its Canvas
tab. Assert the new behavior instead: the tab is still open and selected
on return and Edit still holds the channel's draft. The assertions do not
depend on which Canvas view shows first, since a return can either keep
or remount the panel.

Co-authored-by: mboston Implementer <932d3b6ebea6cc83e091551ae46d1c7e7f8f7d0fa8e9cef5e6facc8544af413b@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matthew Boston <mboston@squareup.com>
The previous commit kept every launcher drawer open per channel in page
state. That restored the Terminal drawer on return, which takes keyboard
focus on mount, and it still lost the drawer on a trip to another page.

Keep only side-column panels (Huddles, Todos) open, and keep them in the
channel's session tab store next to its threads and Canvas, so they also
survive leaving the page. The bottom drawer closes on leaving the channel
as before. Closing a restored panel whose launcher was replaced focuses
the page's fallback target instead of the body. A menu tab whose channel
became ineligible while the viewer was away is dropped on return.

Co-authored-by: mboston Implementer <932d3b6ebea6cc83e091551ae46d1c7e7f8f7d0fa8e9cef5e6facc8544af413b@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matthew Boston <mboston@squareup.com>
The retained side panel opening carried its launcher element into the
session-lifetime tab store, which retains no DOM elements by contract and
would keep a detached page alive after the viewer left it. Store only the
panel, context and target; the trigger stays in a page-local map, and a
close after the page was replaced falls back to the page's focus target.

Co-authored-by: mboston Implementer <932d3b6ebea6cc83e091551ae46d1c7e7f8f7d0fa8e9cef5e6facc8544af413b@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matthew Boston <mboston@squareup.com>
… Canvas journey

main now labels the channel heading as a Manage members button, not a tab.

Co-authored-by: mboston Implementer <932d3b6ebea6cc83e091551ae46d1c7e7f8f7d0fa8e9cef5e6facc8544af413b@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matthew Boston <mboston@squareup.com>
@bostonaholic

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 6, 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 ✅ Completed 2026-11-06T16:57:59.860480Z 5672c42 Manual request
ℹ️ 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.

@bostonaholic
bostonaholic marked this pull request as ready for review November 6, 2026 16:43
@bostonaholic
bostonaholic requested a review from a team as a code owner November 6, 2026 16:43

@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: 45bdb82a7d

ℹ️ 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".

The history snapshot treated a side pane holding only a retained launcher
drawer (Todos, Huddles) as empty and recorded it closed, so Back to that
visit hid the drawer even though it was never closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bostonaholic

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: 5672c42c46

ℹ️ 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".

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Reviewed at 5672c42. No actionable defects remain; the drawer-only history regression is fixed. Chromium and WebKit lifecycle checks pass. Inactive tabs within the current workspace retain the existing eager-mount behavior; native Huddles/terminal integration and sustained performance benchmarks remain unverified.

@bostonaholic
bostonaholic merged commit 1db835e into main Nov 6, 2026
119 of 121 checks passed
@bostonaholic
bostonaholic deleted the bostonaholic--channel-panel-restore branch November 6, 2026 18:39
wpfleger96 added a commit that referenced this pull request Nov 6, 2026
…leanup

* origin/main:
  feat(agents): let app-hosted agents read their owner's channels and members (#2745)
  NEXUS-4271: Expose installed plugin status (#2754)
  fix(menu): stop clipping lifecycle actions behind a hidden scrollbar (#2716)
  feat(agents): give approved external apps the selected agent's full Buzz tools (#2707)
  AIDA-2549: Keep Claude settings drafts in sync (#2841)
  fix(channels): keep a channel's side panels open when returning to it (#2838)
  fix(builderlab): clarify remote-agent account availability (#2764)
  fix(sidebar): rename Remove from Team to Close conversation (#2845)
  perf(channels): cut header and name-matching work in large channels (#2726)
  fix(browser): silence a browser page once its tab goes away (#2721)
  Community Mesh: shared compute plugin (including agent type) (#941)
  perf(relay): cut main-thread work from profile updates (#2730)
  fix(composer): lead the channel placeholder with its sidebar icon (#2829)
  test(activity): scope preview assertions to the request row (#2843)
  perf(search): fold people names once across palette keystrokes (#2570)
  fix(github): keep PR watches running when localStorage is full (#2699)
  Make drafts and sent part of Messages (#2728)
  fix(canvas): render every Canvas within the save limit (#2839)

# Conflicts:
#	.github/workflows/release.yml
cheng-block added a commit to cheng-block/buzz-app that referenced this pull request Nov 6, 2026
…ime-hint

* upstream/main:
  ci: give the native job room for a cold Rust cache (block#2867)
  fix(pairing): refuse unknown communities natively and name the requesting process (block#2835)
  Add per-interest Attention pause and management controls (block#2847)
  feat(drafts): show where unsent drafts wait (block#2853)
  feat(agents): let app-hosted agents read their owner's channels and members (block#2745)
  NEXUS-4271: Expose installed plugin status (block#2754)
  fix(menu): stop clipping lifecycle actions behind a hidden scrollbar (block#2716)
  feat(agents): give approved external apps the selected agent's full Buzz tools (block#2707)
  AIDA-2549: Keep Claude settings drafts in sync (block#2841)
  fix(channels): keep a channel's side panels open when returning to it (block#2838)
  fix(builderlab): clarify remote-agent account availability (block#2764)
  fix(sidebar): rename Remove from Team to Close conversation (block#2845)
  perf(channels): cut header and name-matching work in large channels (block#2726)
  fix(browser): silence a browser page once its tab goes away (block#2721)
  Community Mesh: shared compute plugin (including agent type) (block#941)
  perf(relay): cut main-thread work from profile updates (block#2730)
  fix(composer): lead the channel placeholder with its sidebar icon (block#2829)
  test(activity): scope preview assertions to the request row (block#2843)
  perf(search): fold people names once across palette keystrokes (block#2570)
  fix(github): keep PR watches running when localStorage is full (block#2699)

Signed-off-by: Cheng Peng <cheng@block.xyz>
Co-authored-by: Cursor <cursoragent@cursor.com>
cheng-block added a commit to cheng-block/buzz-app that referenced this pull request Nov 6, 2026
…ions-literal-markdown

* upstream/main: (38 commits)
  fix(composer): let Option+Up/Down switch conversations from an empty composer (block#2832)
  fix(reactions): Scope timed reactions to the selected video (block#2796)
  fix(communities): name what the Join dialog is doing instead of "Working…" (block#2822)
  docs(agents): explain the per-conversation MCP cost of Claude agents (block#2683)
  fix(avatars): keep unsaved avatar drafts and focus, and validate the URL late (block#2714)
  fix(agents): offer stream and forum connection setup only to channel members (block#2749)
  Describe Attention as what the agent does, not its tools (block#2865)
  Let a local Codex agent join its owner's huddle by voice (block#2321)
  ci: give the native job room for a cold Rust cache (block#2867)
  fix(pairing): refuse unknown communities natively and name the requesting process (block#2835)
  Add per-interest Attention pause and management controls (block#2847)
  feat(drafts): show where unsent drafts wait (block#2853)
  feat(agents): let app-hosted agents read their owner's channels and members (block#2745)
  NEXUS-4271: Expose installed plugin status (block#2754)
  fix(menu): stop clipping lifecycle actions behind a hidden scrollbar (block#2716)
  feat(agents): give approved external apps the selected agent's full Buzz tools (block#2707)
  AIDA-2549: Keep Claude settings drafts in sync (block#2841)
  fix(channels): keep a channel's side panels open when returning to it (block#2838)
  fix(builderlab): clarify remote-agent account availability (block#2764)
  fix(sidebar): rename Remove from Team to Close conversation (block#2845)
  ...

Signed-off-by: Cheng Peng <cheng@block.xyz>
Co-authored-by: Cursor <cursoragent@cursor.com>
matt2e added a commit that referenced this pull request Nov 9, 2026
…ter Back

Follow-up review for the Close all tabs side-panel fix.

- useConversationTabs: the Close all tabs case now also runs with the
  pane closed. The side panel stays hidden and its launcher reads
  unpressed. This fails if closeTabs drops the paneOpen guard.
- todos.spec.mjs: the journey goes Back to the channel visit. Since
  #2838 a channel visit showing Todos saves an open pane, so Back shows
  Todos with the retained thread tab. Hiding the pane there saves it
  closed: the next Back hides Todos with the launcher unpressed, one
  press shows Todos and the thread tab again, and Close all tabs from
  that channel visit keeps Todos shown, with the launcher pressed. This
  fails if closeTabs no longer keeps the pane open.
- todos.spec.mjs: a channel visit showing Todos beside a retained thread
  saves an open pane when a second thread opens. Hiding the pane before
  Close all tabs still returns there with Todos hidden, and one press
  shows it. This fails if the return reapplies that saved open pane.
- shell-design.md: Close all tabs leaves a side panel as it was, shown
  in an open pane or hidden in a closed one. Hiding the pane hides the
  panel without closing it. The launcher reads pressed only while the
  pane shows the panel, and one press brings back the pane with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: mtoohey <mtoohey@squareup.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
baxen pushed a commit that referenced this pull request Nov 9, 2026
…2803)

## Problem

Close all tabs (right-click a tab, ⌘⇧W, or the macOS **Close All Tabs**
menu item) closes the whole right pane. A launcher's side panel such as
Todos goes with it, even though it isn't a tab.

- Its launcher still reads pressed (`aria-expanded="true"`), so the
first press "closes" the hidden panel and nothing visible happens. Only
the second press brings Todos back.
- **Toggle tab pane** and **Back** to the channel visit end the same
way: Todos is hidden and the launcher reads pressed. Close all tabs from
that earlier channel visit also hides Todos.

## Fix

Close all tabs now leaves a launcher's side panel as it was: shown in an
open pane, hidden in a closed one. The launcher reads pressed only while
the pane shows the panel, so one press always brings it back. Production
change: 23 lines added and 5 removed, in 4 files.

1. **`closeTabs`** (`useChannelTabState.ts`): `paneOpen: side ||
(s.paneOpen && !!s.sidePanel)`. A pane showing a launcher's panel stays
open, and a closed pane stays closed.
2. **`onCloseTabs`** (`ChannelsPage.tsx`): when nothing is kept and a
launcher's side panel is selected, shown or hidden, it dismisses toward
the `"side"` pane, not `"main"`.
- `dismissContent` then records the existing side-panel survivor from
#1767, so the return to the channel visit keeps Todos. That's the same
path that keeps Todos when its last tab closes with X.
- The kept-return marker records the pane's pre-close visibility: a kept
tab shows, and a side panel keeps `paneOpen` as it was. The matching
return applies it after `enterVisit`, so a channel visit that saved an
open pane (a retained thread or Todos was in it) doesn't reveal the
Todos the user had hidden before ⌘⇧W.
3. **Launcher `pressed` and `toggle` via `shown`**
(`useChannelPanels.tsx`): a side panel counts as `shown` only while the
pane is open, and `pressed` and `toggle` use `shown`.
- On a hidden panel, one press goes through `onOpen`
(`setPaneOpen(true)`) and shows the pane with the panel.
- `selected` is unchanged, so the panel stays retained while the pane is
hidden.
- `ChannelPanelTabs` gains `paneOpen`. `useConversationTabs.tsx` passes
`tabState.paneOpen`, the flag its tab-placed `pressed` already reads.

Close other tabs on a main-pane tab now keeps the side panel beside it
too.

**Preserved:**
- [Navigation
history](https://github.com/block/buzz-app/blob/main/docs/navigation-history.md):
closed thread visits are still retired together. Back and Forward still
skip them, and no visit's saved pane state changes. Back to a channel
visit still restores that visit's saved pane. Where it was hidden, Todos
stays hidden, but the launcher now reads unpressed and one press shows
it.
- No visual change. Pressed now matches what's on screen.

**Docs:**
- `shell-design.md` describes Close all tabs and hiding the pane for
side panels.
- `plugin-architecture.md` defines `pressed` for `channelPlacement:
"side"` as "shown in the open pane", not merely selected.

## Tests

**Vitest** (`useConversationTabs.test.tsx`; `launcherHarness` gains a
`placement` parameter):
- "Close all tabs leaves a launcher's side panel as the open/closed pane
showed it":
  - The open case fails on main.
- The closed case fails if `closeTabs` drops the `s.paneOpen &&` guard.
- "a side panel's launcher reads unpressed while the closed pane hides
it, and one press shows it": catches the launcher change.
- "Close other tabs on a main-pane tab keeps a launcher's side panel
open beside it."

**Journey** (`tests/browser/todos.spec.mjs`): "Todos isn't a tab: Close
all tabs keeps it, and its launcher tracks the pane". This is one new
browser case; none were removed.
- **Why it needs a browser:** Close all tabs retires the thread's
history visit, and the page's return to the channel visit has to keep
the side panel. jsdom page tests don't route visits through navigation.
- **What it covers:**
1. Toggle tab pane hides Todos with the launcher unpressed, and one
press shows it.
  2. Close all tabs from a thread visit keeps Todos.
3. Back to a channel visit that saved a closed pane hides Todos, one
press shows it, and Close all tabs from there keeps it. Since #2838 a
channel visit showing Todos saves an open pane, so the journey hides the
pane there first.
4. ⌘⇧W from a thread visit with the pane closed doesn't reveal Todos,
even when the channel visit it returns to saved an open pane.
- **What fails without each part:**
  - On main, it fails at the first launcher check after Toggle tab pane.
- With the `closeTabs` change reverted, it fails at Close all tabs from
the channel visit.
  - With only the launcher change, Todos is gone after Close all tabs.
- If the return drops the kept-return marker's `paneOpen`, it fails at
the ⌘⇧W step from a closed pane.

**Fail before, pass after:**
- With only the first commit's tests applied to main, Vitest fails 2 of
42. The journey fails in Chromium and WebKit, where `aria-expanded` is
"true" after Toggle tab pane.
- With the fix:
- 139 Vitest tests pass across `useConversationTabs`,
`useConversationTabs.panels`, `useChannelPanels`, `useChannelTabState`
and `ChannelsPage.tabs`.
- `todos.spec.mjs`, `tab-close-others.spec.mjs` and
`right-pane-dismissal.spec.mjs` pass in Chromium and WebKit, 32 tests in
all. So does the layout.spec case "Todos stacks beside threads".
  - `tsc --noEmit` and `biome check --error-on-warnings` are clean.
- These local runs were on the same three patches before the rebase onto
067db60. That rebase brought in only #2587 (inbox, thread and unread
files), which touches none of this PR's files. The later rebase onto
be67480 brought in #2838 (retained side panels); the journey's Back
setups were reconciled with it in 2a30ea9 and have not been run
locally since.

## Not checked

- The native ⌘⇧W menu. The browser runs dispatch `buzz:close-all-tabs`,
the window event the native menu sends. The page handled it
(`preventDefault`), and the result matched the right-click menu.
- The desktop app.

## Related

- **#2415** reworks the same dismissal-return code. This change depends
on `dismissContent` keeping `(pane === "side" && tabState.sidePanel)`.
Whichever of the two lands second must keep that condition.
- **#1793** (draft, conflicting) rewrites `closeTabs` and changes
`onCloseTabs`. If it's revived, it needs the same two rules:
  - A pane showing a launcher's side panel stays open on Close all tabs.
- `onCloseTabs` dismisses toward the side pane while a launcher's side
panel is selected, and the return reapplies the pane's pre-close
visibility.

## Before and after

Both runs used the same scripted steps on the same timeline. Each run
checked the expected state at every step, and both passed.

| Step (starts at) | Before: main @ 3bcf1c4 | After: this branch @
2c81a4905 |
|---|---|---|
| 1. Press **Toggle channel todos**, then open the thread from its reply
chip (3.6 s) | Thread tab above Todos. Launcher pressed. | Same. |
| 2. Right-click the thread tab, choose **Close all tabs**, hold about
3.5 s (10.0 s) | The whole pane closes, and Todos goes with it. The
launcher still reads pressed, so the badge turns red. | The tab closes.
Todos stays and fills the pane. Launcher pressed. |
| 3. Open the thread again, then ⌘⇧W (16.2 s) | Reopening brings back
the thread with Todos, because Todos was never deselected. ⌘⇧W then
hides both, and the launcher still reads pressed. | ⌘⇧W closes the tab
and Todos stays. Launcher pressed. |
| 4. Open the thread again, then **Toggle tab pane** (23.2 s) | Pane and
Todos hidden. Launcher **still pressed**. | Pane and Todos hidden.
Launcher **not pressed**. |
| 5. Press the Todos launcher once (30.0 s) | Nothing visible happens.
The launcher flips to not pressed and Todos stays hidden. | The pane
comes back with the thread tab above Todos. Launcher pressed. |

2c81a4905 has the same fix and tests as this PR, from before the rebase.
The only later commit changes a comment and docs. The video was recorded
in Chromium against the fixture relay, with ⌘⇧W sent as the
`buzz:close-all-tabs` event. Back, and Close all tabs from the earlier
channel visit, are covered by the journey but not shown in the video.


https://github.com/user-attachments/assets/c40bd79d-7668-4511-b065-915e533ea6e6

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

---------

Signed-off-by: mtoohey <mtoohey@squareup.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
matt2e added a commit that referenced this pull request Nov 9, 2026
…l switches (#3060)

## Why

Since #2838 keeps a channel's side panels open when you return to it,
the GitHub tab survives a channel switch but its panel is unmounted and
remounted. Coming back to the channel therefore started the pull request
panel over: **Changes** fell back to **Summary**, and a **Submit
review** draft (decision, text, and the head it was written against) was
lost.

## What changes

The GitHub plugin owns a small per-session panel memory, created once in
`apply` beside the pull-state store
(`src/bundled/github/panel-memory.ts`):

- **View** (Summary/Changes) is remembered per channel + pull request
URL. A first open from another channel starts on Summary with the title.
- **One review draft per pull request URL**, from any channel, matching
GitHub's one pending review per pull request.
- The draft **leaves memory when a submit starts** and **returns only if
the submit fails** (unless another panel has started a new draft since).
A panel remounted mid-submit, or a second tab on the same pull request,
cannot offer the in-flight review again. `Use latest commit` is disabled
while a submit is in flight and the write effect skips while busy.
- **Text-less decisions are not remembered.** A Request changes or
Approve with no comment stays in the mounted panel only.
- A **text-less Approve still sees the "New commits were pushed"
notice** after a push, with Submit disabled, as on main. Only an empty
Comment silently follows the latest head.
- Memory lasts the **app session** per plugin activation: a reload or
turning the plugin off and on forgets it.
- **Scroll position is not restored**; that needs a host change.

`docs/buzz-help.md` gains a bullet describing this.

#1793 (restore tab workspaces across refresh) is rebase-only against
this; there is no behavioural interaction.

## Before and after

Before is on the left at `origin/main` `a905c4865f` (the fix commit's
parent); After is on the right at `a5ad4ba667`. Watch the GitHub panel
on the right of each half.


https://github.com/user-attachments/assets/713b8646-29e0-4447-a96b-5b27b8a58a72

![Caption 7, reopening Submit review after the round trip: Before opens
an empty Comment with Submit disabled; After reopens on Request changes
with the typed sentence and the diff still behind
it.](https://github.com/user-attachments/assets/2c56a70a-0cc6-4c60-b811-5399ef10653a)

| Moment | Before (`a905c4865f`) | After (`a5ad4ba667`) |
|---|---|---|
| Open the PR from a message link in #Alpha, switch to Changes, open
Submit review, choose Request changes, type a sentence, Escape, switch
to #Beta | Same on both sides | Same on both sides |
| Back in #Alpha (~15 s) | **Summary**, description from the top, no
diff | **Changes** selected, diff showing |
| Reopen Submit review (~18 s) | **Comment**, empty box, Submit disabled
| **Request changes** with the sentence, Submit enabled |
| Submit (After) or Escape (Before) | Nothing to submit | Submits;
"Review submitted" toast |
| #Beta and back, reopen Submit review | Empty Comment | Empty Comment:
the sent draft is gone |

The recording is the production browser build with the plugin's `gh`
client and diff reader swapped at build time for an in-memory stub so
Changes and Submit review are available outside the desktop app; nothing
else is faked.

## Test plan

- `GitHubPanel.test.tsx` 52 → 60 passing; GitHub plugin suite 345 → 355
passing across 21 files. `tsc --noEmit` clean; Biome clean on changed
files.
- Each new test was confirmed to fail with its fix removed: the remount
case on main; the mid-submit remount cases without the clear-at-start /
put-back-on-failure; the fresh-open-from-another-channel case with a
view keyed by URL alone; the whitespace-only draft case with the old
empty rule; the text-less Approve push case without the head rule; the
two-panel case without the re-read branch; the mid-submit push case with
the busy guards removed.
- Not checked by hand in the desktop app with `gh` signed in.

## Known limits

- Two tabs on the same pull request share one draft; the last one to
type wins and the other panel takes over the draft when its popover
reopens.
- Closing a tab on purpose and reopening the pull request in the same
channel still restores the remembered view.
- A draft on a pull request that merges or closes first lingers unseen
until the session ends, since Submit review goes away once closed.
- No scroll restore.

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

---------

Signed-off-by: mtoohey <mtoohey@squareup.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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