feat(components): let a host contribute tabs to the session tab strip - #13
Draft
pythonlearner1025 wants to merge 1 commit into
Draft
feat(components): let a host contribute tabs to the session tab strip#13pythonlearner1025 wants to merge 1 commit into
pythonlearner1025 wants to merge 1 commit into
Conversation
A host embedding the session viewer may own surfaces that belong beside the conversation rather than in a second tab strip of their own. This fills a hole that already exists rather than cutting a new one: `SessionTabBar` already carries `viewerTabs`, `activeViewerTabId`, `onViewerTabSelect` and `onViewerTabClose`, the `viewer` arm of `SortableItemData` is implemented, and the one production call site passes `variant="session"` and no viewer tabs at all. `variant="viewer"` is declared and cannot be used, because `parentSession` is required by a strip that variant tells not to draw. `SessionTabBar`: - `ViewerTabItem.type` gains `'custom'` and the item gains an optional `icon`, so the bar draws and sorts a host tab exactly like a file or diff tab without knowing what is behind it. - `parentSession` becomes optional, and the two places that read it are guarded, so `variant="viewer"` is finally usable. `SessionDetail` gains six props: `surfaceTabs`, `activeSurfaceTabId`, `onSurfaceTabSelect`, `onSurfaceTabClose`, `onSessionTabSelect` and `onSessionMissing`. The host owns the list, the selection and both verbs; the page owns the drawing and the layout, and tells the host when its own selection has taken the view back. Two of those need saying: - An active host tab hides every conversation surface, so the host's selection has to end when the page selects a conversation tab — and that selection is `useState`, per parent session, not in the URL. Ten call sites move it, so the notification is a wrapper around the setter rather than a call at each site, and not an effect on the value: an effect fires on a CHANGE, and the click that most needs the call changes nothing, because the parent tab is already selected while a host tab covers it. The three writers that keep the raw setter are corrections rather than selections (the render-time session-switch reset and the two `?tab=` syncs), and their signature difference — an updater, not an id — makes the exclusion structural. - `SessionDetail` returns above the tab strip on the not-found branch, and that return takes every host tab with it. `onSessionMissing` is how the host hears it. The call sits above the once-per-session-id analytics gate deliberately: what a host does with this is move an address, and an address can come back. It is read from a ref so a fresh host closure does not re-run the effect. `content` is a `ReactNode` mounted inline and hidden, not a portal host: React remounts a portal whose container identity changes, so the container swap a ref-callback host was meant to avoid happens anyway. The memo that maps `surfaceTabs` sits beside `viewerTabItems`, above every early return, because this component returns early below it. The mobile drawer is deliberately not changed: `MobileSessionTabSheet` keeps its own kind enum, and the props are inert there. With every new prop absent, both components render exactly what they rendered before, and no existing call site passes one. Model: claude-opus-5[1m]
pythonlearner1025
force-pushed
the
blitz/seam-5-host-surface-tabs
branch
from
September 2, 2026 01:25
1644870 to
4f24bbd
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A host embedding the session viewer may own surfaces that belong beside the
conversation rather than in a second tab strip of their own. This fills a hole
that already exists rather than cutting a new one:
SessionTabBaralreadycarries
viewerTabs,activeViewerTabId,onViewerTabSelectandonViewerTabClose, theviewerarm ofSortableItemDatais implemented, andthe one production call site passes
variant="session"and no viewer tabs atall.
variant="viewer"is declared and cannot be used, becauseparentSessionis required by a strip that variant tells not to draw.
SessionTabBar:ViewerTabItem.typegains'custom'and the item gains an optionalicon,so the bar draws and sorts a host tab exactly like a file or diff tab without
knowing what is behind it.
parentSessionbecomes optional, and the two places that read it are guarded,so
variant="viewer"is finally usable.SessionDetailgains six props:surfaceTabs,activeSurfaceTabId,onSurfaceTabSelect,onSurfaceTabClose,onSessionTabSelectandonSessionMissing. The host owns the list, the selection and both verbs; thepage owns the drawing and the layout, and tells the host when its own selection
has taken the view back.
Two of those need saying:
has to end when the page selects a conversation tab — and that selection is
useState, per parent session, not in the URL. Ten call sites move it, so thenotification is a wrapper around the setter rather than a call at each site,
and not an effect on the value: an effect fires on a CHANGE, and the click that
most needs the call changes nothing, because the parent tab is already
selected while a host tab covers it. The three writers that keep the raw setter
are corrections rather than selections (the render-time session-switch reset
and the two
?tab=syncs), and their signature difference — an updater, not anid — makes the exclusion structural.
SessionDetailreturns above the tab strip on the not-found branch, and thatreturn takes every host tab with it.
onSessionMissingis how the host hearsit. The call sits above the once-per-session-id analytics gate deliberately:
what a host does with this is move an address, and an address can come back.
It is read from a ref so a fresh host closure does not re-run the effect.
contentis aReactNodemounted inline and hidden, not a portal host: Reactremounts a portal whose container identity changes, so the container swap a
ref-callback host was meant to avoid happens anyway. The memo that maps
surfaceTabssits besideviewerTabItems, above every early return, becausethis component returns early below it.
The mobile drawer is deliberately not changed:
MobileSessionTabSheetkeeps itsown kind enum, and the props are inert there.
With every new prop absent, both components render exactly what they rendered
before, and no existing call site passes one.
Compatibility
Every change is additive at its default. With the new prop, parameter or flag
absent, the touched components render and behave exactly as they do today, and
no existing call site in this repository passes one.
Testing
packages/componentstypecheck and the full vitest suite pass. No new test: both components need a workspace runtime. The one defect worth a regression test — theuseMemoplacement, which must sit above every early return or React reports "Rendered more hooks than during the previous render" — is pinned downstream against a live daemon and does not translate to this repo's harness.Notes for the reviewer
The largest of this series and the one most worth arguing with. Two things to look at first:
setActiveTabSessionId). It exists because an active host tab hides every conversation surface, so the host's selection has to end when this page selects one — and that selection isuseState, not a URL field. An effect on the value cannot do it: the click that most needs the call changes nothing.onSessionMissing.SessionDetailreturns above the tab strip on the not-found branch and every host tab goes with it.If you would rather grow a host-tab concept of your own, all of this is replaceable by it.
Review metadata
BlitzOS fork only. Delete this section before sending the PR to
LodyAI/Lody.blitz/seam-5-host-surface-tabsf3474894 (the pinned upstream commit)