Skip to content

Restore thread panel per channel across switches - #5593

Open
aparisesquare wants to merge 1 commit into
block:mainfrom
aparisesquare:aparise/thread-panel-per-channel-memory
Open

Restore thread panel per channel across switches#5593
aparisesquare wants to merge 1 commit into
block:mainfrom
aparisesquare:aparise/thread-panel-per-channel-memory

Conversation

@aparisesquare

Copy link
Copy Markdown

What

Leaving a channel and returning now restores the thread panel the way it was left — open on the same thread, or closed. Previously every channel switch closed the panel: sidebar navigation builds a fresh /channels/$channelId URL with no thread search param, so the URL-backed panel state was lost on every forward navigation (back/forward already restored it per history entry).

How

Panel state stays URL-backed; the change adds a session-scoped per-channel memory that seeds the URL goChannel builds. Four small pieces, no relay/schema changes:

  • channelPanelMemory.ts (new): tri-state per-channel memory — thread head id ("open on this thread"), null ("user left it closed", so a closed panel stays closed on return), no entry ("never visited"). Backed by sessionStorage following the thread-panel width precedent (useThreadPanelWidth): survives a reload, dies with the app session.
  • useChannelThreadMemory.ts (new): continuously mirrors the raw ?thread value into the memory from ChannelScreen (one call line — the file sits near the 1000-line ceiling). Skips huddle transcripts (useHuddleThreadIsolation force-closes threads there) and the forum-post view. Continuous recording makes every exit path correct for free, and turns a thread snap-closed by useThreadTargetSync (deleted/unfetchable head) into memory = null — forgotten, never retried, so no restore loop is possible.
  • goChannel: navigations that carry no explicit target (thread / messageId / agentSession / autoSend) seed thread from memory when building the search params. One choke point covers sidebar, tray, notifications, and DM opens; the URL is right the first time, so a switch stays exactly one history entry, and explicit targets plus back/forward win by construction. A restored thread whose head paged out of the initial window rides the existing deep-link splice (targetThreadRootId ← search.thread in channels.$channelId.tsxChannelRouteScreen fetch).
  • resetChannelPanelMemory() wired into resetCommunityState() and added to the AGENTS.md singleton list — channel ids are community-local, so remembered panel state must not leak across a community switch (memory + its sessionStorage key are both cleared).

Not restored (out of scope): in-panel expanded replies and scroll position (plain React state); profile/agent-session panels (the memory value can grow to a partial record of those keys later without moving any wiring).

Testing

  • channelPanelMemory.test.mjs — tri-state round-trip, per-channel isolation, sessionStorage write-through, redundant-write skip, reset semantics, storage-failure resilience (7 tests).
  • tests/e2e/thread-panel-persistence.spec.ts (registered in the smoke project) — open thread in #general → switch to #random (panel closed there) → return (panel restored, same thread, URL carries thread=) → close panel → leave and return (stays closed).
  • Full desktop unit suite: 4677/4677 pass. Neighboring e2e specs (navigation, channels, messaging, thread-focus-mode, threadpane-ultrawide, thread-unread) pass.
  • biome check, tsc --noEmit, and check-file-sizes clean.

Leaving a channel and returning now restores the thread panel the way it
was left: open on the same thread, or closed. Panel state stays URL-backed
(?thread=<headId>); the change adds a session-scoped per-channel memory
that seeds the URL goChannel builds.

- channelPanelMemory.ts: tri-state per-channel memory (thread id / closed /
  never visited), sessionStorage-backed like the thread-panel width, so it
  survives a reload but not an app restart.
- useChannelThreadMemory.ts: continuously mirrors the raw ?thread value in
  ChannelScreen; skips huddle transcripts and the forum-post view. A thread
  snap-closed by useThreadTargetSync records null, so stale entries are
  forgotten instead of retried.
- goChannel: navigations with no explicit target (thread/messageId/
  agentSession/autoSend) seed thread from memory — the URL is right the
  first time, so a switch stays one history entry and back/forward keeps
  its per-entry params. Restored threads whose head paged out of the
  initial window ride the existing deep-link splice
  (targetThreadRootId <- search.thread).
- resetChannelPanelMemory() wired into resetCommunityState() and added to
  the AGENTS.md singleton list — channel ids are community-local.

Covered by channelPanelMemory.test.mjs and a Playwright spec exercising
restore, per-channel isolation, and closed-stays-closed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Anthony Parise <aparise@squareup.com>
@aparisesquare
aparisesquare requested a review from a team as a code owner August 11, 2026 19:12
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