docs/craft/features/universal-panel/2026-05-28-universal-panel-refactor-design.md
The current BuildOutputPanel (web/src/app/craft/components/OutputPanel.tsx)
hosts three pinned tabs — Preview, Files, Artifacts — and a separate
pinned-vs-file-tabs system where opened files render as additional tabs at
the panel header level (the filePreviewTabs array, ~line 500). This is
already close to a universal tab system, but file-specific in shape.
A planned subagent-view feature wants to add another transient tab kind ("show me the live transcript of subagent X in the side panel"). Rather than special-case the panel's tab system per kind, generalize it once so future view kinds can plug in cleanly.
The goal: make the side panel a single surface that can render any "viewable thing," with a small set of pinned tabs that are always present and a stack of transient tabs that the user opens and closes per session.
This refactor is a prerequisite for the subagent-view feature
(docs/craft/features/subagents/2026-05-28-subagents-view-design.md) but
stands on its own value: it tightens the panel's typing, removes
file-specific assumptions, and lays groundwork for future view kinds.
OutputPanel.tsx:500-505
renders filePreviewTabs after a separator from the pinned trio. The
refactor doesn't invent a new structure — it generalizes the existing
one.useOutputPanelOpen / useToggleOutputPanel are wired in ChatPanel.tsx
(the chat-header toggle) and v1/page.tsx (panel mount). There's a
300ms slide-out animation already in place (ChatPanel.tsx:90-95).useBuildSessionStore.ts holds filePreviewTabs,
activeFilePreviewPath, and tabHistory (the pinned-vs-file
switcher). These become a single typed panelTabs list once
generalized.Today the panel implicitly has two kinds of tabs:
filePreviewTabs.After the refactor:
kind:
{ kind: "file", path: string } — replaces today's
filePreviewTabs entries; no behavior change.{ kind: "subagent", subagentId: string } — placeholder for the
follow-up subagent PR. Not rendered or createable in this PR; the
union just leaves the door open.The rendering layer switches on kind to pick label, icon, and body
component. Future kinds (search results, diff viewer, log viewer, etc.)
add a new union member and a new body component; no other plumbing.
useBuildSessionStore.ts)filePreviewTabs: FilePreviewTab[] → panelTabs: PanelTab[] where
PanelTab = { kind: "file", path: string } (subagent kind added in
the subagent PR).activeFilePreviewPath: string | null → activePanelTabId: string | null. The ID is a derived key (e.g. "file:<path>") so each kind
has a unique namespace.tabHistory (the pinned-vs-file recency stack) generalizes the same
way — it tracks pinned tabs and active transient tab IDs.openFilePreview, closeFilePreview,
setActiveFilePreviewPath) generalize to take a PanelTab /
tabId. The file-specific helpers can stay as thin wrappers
internally if call sites are easier to migrate that way.OutputPanel.tsx)panelTabs and switches on
kind to render the tab chrome. For kind: "file", behavior is
unchanged (file icon + filename + close ×).<FilesTab> / <PreviewTab> /
<ArtifactsTab> render based on activeOutputTab and
<FilePreviewContent> renders for an active file path) becomes a
single switch on the active tab ID — if the active is a pinned tab,
render the corresponding pinned component; if a transient
kind: "file", render <FilePreviewContent>.ChatPanel.tsx)toggleOutputPanel. Visual: accent-tinted
when panel is open, neutral when closed.When the session's first webapp artifact lands (the moment the Preview
tab becomes meaningful), set outputPanelOpen = true if it isn't
already. This teaches users where the panel lives without needing a
permanent header button.
Trigger detection: piggyback on the existing webappNeedsRefresh /
artifact-creation signal. Only fire once per session — if the user has
manually closed the panel, don't re-open it on later refreshes.
Existing inline file references in chat (e.g., the file paths shown in
tool-call cards) become click targets that open the panel to that
file. Most of this already works — the file is opened in
filePreviewTabs and the panel surfaces it — but verify the path is
clean and the panel auto-opens if currently closed.
New / changed:
useBuildSessionStore.ts (changed) — generalize
filePreviewTabs → panelTabs, rename associated action and
selector hooks. Add migration shims if any consumer outside the
panel reads the old field names.OutputPanel.tsx (changed) — switch the tab-row map and body
switch onto the generalized model. Add pin indicator on pinned
tabs.ChatPanel.tsx (changed) — audit chat-header for any
redundant launcher buttons; ensure single panel toggle. Implement
auto-open-on-first-preview behavior.PanelTab interfaces / types (new or moved) — define
the discriminated-union type in web/src/app/craft/types/ next
to the existing display types.filePreviewTabs field outside the panel.
Likely candidates: the file-preview modal, the chat panel itself if
it surfaces "X files open" anywhere. Grep for filePreviewTabs,
activeFilePreviewPath, openFilePreview — update each call site.Single layer: Playwright E2E. This is fundamentally a UI plumbing refactor; the meaningful behavior is the integration between store, tab rendering, and chat-header toggle.
web/tests/e2e/craft-side-panel.spec.ts — drive a Craft conversation
through these gates:
No unit tests proposed: the refactor is largely structural — type-check and the E2E catch the regressions worth catching. No external-dependency unit tests proposed: nothing in this refactor touches the DB or backend services.