Skip to content

feat(desktop): adding ui state into the history stack#967

Open
tellaho wants to merge 3 commits into
mainfrom
tho/feat/history-ui-state
Open

feat(desktop): adding ui state into the history stack#967
tellaho wants to merge 3 commits into
mainfrom
tho/feat/history-ui-state

Conversation

@tellaho

@tellaho tellaho commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator
Screen Recording 2026-06-11 at 12 57 08 AM-v2

Category: improvement
User Impact: Back/forward and page reloads now restore what you were looking at — open thread, profile, or agent session panels in channels (including the profile panel's Memories/Channels sub-view), the selected home inbox item, the Pulse profile panel, and the settings screen with its open section — instead of dropping you back to a bare default view.

Problem: Hitting back or reloading discarded UI state everywhere: thread/profile/agent-session panels in channels, the profile panel's sub-view, the home inbox selection, and the Pulse profile panel all lived in component state, so every navigation silently reset them. During active development this meant chasing down your last UI state after every refresh. (Requested in buzz: buzz://message?channel=d14cd131-6084-4c9c-ba4d-24fb6bfc4263&id=7b077fcec0ad531998b05b4a62d1ed05b9f92aa4d909aab5ee17ce56613bd081)

Solution: Make the URL the source of truth for panel-level UI state. A generic useHistorySearchState(keys) hook backs UI state with route search params and coalesces all setter calls within one event handler into a single navigation — so each user action produces exactly one history entry, even when a handler closes one panel and opens another. Channel routes get thread / profile / profileView / agentSession params, /pulse gets profile / profileView, / gets item for the inbox selection, and settings becomes a /settings?section=… route — closing it goes back to the previous entry, panels and all. One deliberate asymmetry: home's default (automatic) selection stays local-only — only explicit user selections touch the URL, so background feed loads never trigger navigations that could disturb open popovers elsewhere.

File changes

desktop/src/shared/hooks/useHistorySearchState.ts (new)
The generic history-backed state hook: reads the given keys from the route search, exposes a patch API, and microtask-coalesces same-handler patches into one navigate() (push by default, replace opt-in for programmatic corrections).

desktop/src/features/channels/ui/useChannelPanelHistoryState.ts (new)
Channel-route wrapper over the generic hook exposing drop-in setter replacements for the old useState shapes (thread, profile, profileView, agentSession). Opening/switching/closing a profile resets its sub-view in the same coalesced patch.

desktop/src/app/routes/channels.$channelId.tsx, pulse.tsx, index.tsx, settings.tsx (new), routes.ts, routeTree.gen.ts
Search-param validation for the new params on each route, plus the new /settings route registration. The settings route component renders null — settings still renders at the AppShell level (it replaces the chrome/sidebar/outlet wholesale), now keyed off the route instead of local state.

desktop/src/app/AppShell.tsx, desktop/src/app/navigation/useAppNavigation.ts, desktop/src/features/settings/ui/SettingsPanels.tsx
settingsOpen/settingsSection useStates replaced by route-derived state. Opening settings pushes one history entry; closing goes back (landing on the previous entry with its panel params intact); section switches replace rather than push, so back always exits settings in one step and reloads restore the open section. Side effect: navigating anywhere (deep links, notification clicks) now exits settings automatically.

desktop/src/features/channels/ui/ChannelScreen.tsx
Swaps the three panel useStates for the hook. resetComposerTargets now only clears local ephemeral state — channel switches naturally produce entries without panel params. The thread auto-close effect waits for the timeline's first page before declaring a restored thread head missing, so reloads don't wipe the param mid-load.

desktop/src/features/channels/ui/useChannelAgentSessions.ts
Agent session pubkey state lifted to the caller (URL-backed). The auto-close-when-agent-missing effect waits for the agent list to populate and uses replace so corrections don't pollute history.

desktop/src/features/channels/ui/useChannelRouteTarget.ts
Deep-link thread opening writes the thread param with replace: true, so the deep-link entry itself carries the opened panel — back leaves the deep link rather than stripping the panel off it.

desktop/src/features/profile/ui/UserProfilePanel.tsx
The summary/memories/channels sub-view becomes controlled (view/onViewChange props). The render-phase pubkey-change reset becomes a defensive replace-effect (call sites now own the reset).

desktop/src/features/channels/ui/ChannelPane.tsx
Threads profilePanelView/onProfilePanelViewChange through to the profile panel.

desktop/src/features/pulse/ui/PulseScreen.tsx
Profile panel state moves from local useState to profile/profileView params on /pulse.

desktop/src/features/home/ui/HomeView.tsx
Explicit inbox selections are mirrored to ?item=; back/forward and reloads restore the selected detail pane. Default selection and auto-corrections stay local-only (no background navigations). The clear/default effects wait for the feed to load so a restored selection isn't clobbered mid-load.

desktop/src/features/channels/useChannelPaneHandlers.ts
Setter type loosened from Dispatch<SetStateAction> to a plain value setter (no call sites used functional updates).

desktop/playwright.config.ts
Registers navigation.spec.ts in the smoke project — it was added in #259 but never wired into any project, so it has never run in CI.

desktop/tests/e2e/navigation.spec.ts
New coverage: back/forward restores thread panels, back undoes closing a panel, open panels survive reload, home inbox selection survives reload with back restoring the previous selection. One pre-existing test (direct forum thread links close back to the forum route) is marked fixme: enabling the file exposed that the forum "Back to posts" header renders under the fixed top-chrome drag region, which intercepts clicks — same chrome-inset class #941 is reworking.

Reproduction steps

  1. Open any stream channel and open a thread from the timeline (hover a message → Reply). Note the URL gains thread=<id>.
  2. Navigate to another channel, then hit back — the previous channel reopens with the thread panel restored. Forward returns to the other channel without it.
  3. Close the thread panel with its ✕, then hit back — the panel reopens.
  4. Open a profile panel from a message avatar, go into Memories or Channels — the URL gains profileView=. Reload: the panel reopens on that sub-view. Back: returns to the summary.
  5. With any panel open, reload the app — the panel is restored from the URL.
  6. Open settings (Cmd+,), switch to Notifications — the URL is /settings?section=notifications. Reload: settings reopens on that section. Click "Back to app": you return to the exact channel state you left, open panels included.
  7. On Home, click an inbox item — the URL gains item=. Reload restores that selection; back returns to the default selection.
  8. On Pulse, open a profile panel — same back/reload behavior via /pulse?profile=.

Verification

  • tsc --noEmit, biome, 627 unit tests clean.
  • Smoke e2e project: 187 passed (now including the never-before-run navigation suite).
  • Integration project: remaining failures are pre-existing — two need a live local relay; profile.spec.ts:116/:442 are flaky on clean main too (verified by stash-and-rerun).

Known not-covered (deliberate)

  • Members sidebar and channel-management sheet (modal overlays — history-backing them makes back-button behavior worse).
  • Settings sections do not stack history entries (section switches replace) — back always exits settings in one step.
  • Thread panel internals (expanded reply branches, reply target, scroll position).
  • Composer drafts (in-memory; orthogonal localStorage fix).

🤖 Generated with Claude Code

@tellaho tellaho force-pushed the tho/feat/history-ui-state branch from e6a9a67 to 38ceb01 Compare June 11, 2026 07:53
@tellaho tellaho marked this pull request as ready for review June 11, 2026 07:54
@tellaho tellaho changed the title feat(desktop): move auxiliary panel state into the history stack feat(desktop): adding ui state into the history stack Jun 11, 2026
@tellaho

tellaho commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator Author

⚠️ Blocked by #966 — this branch builds and passes CI on its own, but running it locally (just dev / staging recipes) needs the buzz env-var rename fixes from #966. Once #966 merges, everything here should work out of the box.

🤖 Generated with Claude Code

tellaho and others added 3 commits June 11, 2026 01:08
Back/forward now restores the auxiliary panel (thread, profile, agent
session) each history entry was showing, and reloads restore the open
panel from the URL.

- Panel identity moves from ChannelScreen useState into channel-route
  search params (thread, profile, agentSession) via a new
  useChannelPanelHistoryState hook. Setter calls within one event
  handler coalesce into a single navigation, so each user action
  produces exactly one history entry.
- Channel changes no longer need to clear panel state imperatively;
  fresh entries simply carry no panel params.
- Deep-link thread opening (useChannelRouteTarget) writes the thread
  param with replace so back leaves the deep link intact.
- Thread auto-close waits for the timeline to load before declaring a
  restored thread head missing; agent-session auto-close waits for the
  agent list to populate.
- Register navigation.spec.ts in the smoke project (it was never wired
  into playwright.config.ts, so it has never run in CI); fixme the
  pre-existing forum back-link failure where the top chrome drag region
  intercepts the click. Add coverage: back/forward restores thread
  panels, back undoes closing a panel, open panels survive reload.

Requested in buzz: buzz://message?channel=d14cd131-6084-4c9c-ba4d-24fb6bfc4263&id=7b077fcec0ad531998b05b4a62d1ed05b9f92aa4d909aab5ee17ce56613bd081

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…me inbox, and pulse

Follow-ups to the auxiliary-panel history work, covering the remaining
right-panel surfaces:

- Extract the coalescing search-param logic into a generic
  useHistorySearchState(keys) shared hook; useChannelPanelHistoryState
  becomes a thin wrapper over it.
- Profile panel sub-view (summary/memories/channels) moves to a
  `profileView` param on the channel and pulse routes. UserProfilePanel
  becomes controlled (view/onViewChange); opening or switching a profile
  resets the sub-view in the same coalesced patch. The render-phase
  pubkey-change reset becomes a replace-effect.
- Pulse: the profile panel moves from local state to `profile`/
  `profileView` params on /pulse.
- Home: explicit inbox selections are mirrored to an `item` param on /;
  back/forward and reloads restore the selected detail pane. Default
  (auto) selection stays local-only so background feed loads never
  trigger navigations — an effect-driven navigation here disturbed open
  popovers elsewhere in the app (caught by profile e2e).
- New e2e: home inbox selection survives reload, back restores the
  previous selection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Settings was an AppShell-level overlay driven by two useStates, so
reloads dropped out of it and back didn't exit it. It's now /settings
with a `section` search param:

- New /settings route (null component — settings still renders at the
  AppShell level, replacing the chrome/sidebar/outlet wholesale, now
  keyed off the route instead of local state).
- goSettings()/closeSettings() in useAppNavigation. Opening settings
  pushes one entry; closing goes back, landing on the previous entry
  with its panel params intact (e.g. an open thread). Section switches
  replace rather than push, so back always exits settings in one step
  and reloads restore the open section.
- SettingsView's gated-section auto-correct flows through the same
  replace path, keeping effect-driven corrections out of the stack.
- isSettingsSection validator exported from SettingsPanels for route
  search validation.
- e2e: open settings from a channel with a thread panel, switch
  section, reload (section survives), back to app (thread restored).

A side effect worth knowing: navigating anywhere (deep links,
notification clicks) now exits settings automatically, since settings
is itself a route.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tellaho tellaho force-pushed the tho/feat/history-ui-state branch from 38ceb01 to 664f8d5 Compare June 11, 2026 08:08
@tellaho

tellaho commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator Author

✅ Unblocked — #966 merged and this branch is now rebased on top of it (664f8d5a). Local dev recipes work out of the box; typecheck and the navigation e2e suite pass on the rebased branch.

🤖 Generated with Claude Code

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