Tabbed views and artifact viewer API
Status: Merged — 2026-09-24. UI PR #439
merged at 92f236e0, preserving reference notation, artifact routing and the
Gallery release. UIKit PR #216
merged at c34c934; its documentation is published. The owner requested release
without waiting for CI; CI completion is not claimed. The view API is internal
TypeScript, not an SDK or HTTP API. Reference click changes and note-fragment
navigation remain deferred.
Layout owns view tabs
A page layout contains panel regions. A panel shows a tab strip when it has multiple views. Every singleton, including note editors, artifact viewers and web previews, retains a plain eyebrow/header with no visible tab chrome or extra row. This supersedes both the original always-visible proposal and the interim single-editor opt-in presentation following owner feedback. The internal tab model remains layout-owned; additional tabs appear only when views share a panel. The note name and copyable resource ID from the eyebrow are the tab content; view-specific sharing and connection controls sit at the right of the same horizontal header row. Titles stay on one line with no fixed width cap, using the available space and clipping at the controls boundary. Multiple tabs scroll within that same bounded header space; switching or docking tabs preserves both the toolbar and editor instances.
The layout owns selection, ordering, movement, splitting and closing. Clicking
a note in the note-detail catalog, project folder, or bindr list opens a tab in the content region;
clicking it again selects the existing instance, including after docking. Project
route selection opens a stable note leaf and portal rather than replacing the
previous preview subject. Closing the selected tab and clicking the same list row
reopens it. Initial note URLs use the same path; resource detail can be selected
without discarding open notes. Dragging a tab to an edge
creates a panel; a center drop joins tabs. The header has no View actions +
button. Opening, reordering and splitting remain available through the internal
PageViews API. Unrelated resource-add buttons, filters and spacing are preserved.
Closing a tab never deletes its resource. Views remain mounted in connected DOM across selection, reordering, moves and splits. The recursive layout owns geometry; stable sibling surfaces own view lifetime, including iframe documents. Inactive surfaces are hidden and inert. Notes with unverified or pending edits refuse close, and keep their existing recovery controls. No RTC algorithms, operation IDs, ordering or replay semantics change. Local browser acceptance uses the real CodeMirror and note-pane layout with a transport stub to check body bounds, wheel scrolling, and text/selection retention across header resize and tab docking. The read-only/loading title also clips to its fixed-height row, so long names cannot paint over metadata or editor content.
The gap below the bindr row is 20px tighter with layout-owned tabs. A project
selection distinguishes loading (undefined) from intentional resource detail
(null), so delayed bindr hydration cannot select an extra project tab over a
note opened by its URL.
Local development note-body reads keep the API authorization step and transport the returned signed URL through a fixed-origin, read-only Vite proxy. This avoids the production bucket's localhost CORS restriction without changing bucket permissions or note content. Production requests retain their direct signed URL.
The raw-Markdown regression with live preview selected reproduced in the browser:
Vite loaded distinct CodeMirror language modules from the app and live UIKit
worktree. The preview plugin was active, but its syntaxTree() read an empty
tree from a different language state field. Dedupe @codemirror/language and
@codemirror/view alongside the existing state singleton. Browser checks cover
preview/source toggling, resize, tab selection and docking without changing text
or selection. This fixes module identity; no RTC behavior changes.
Existing code to build on
- Studio:
studio-app/components/panels/WebPreviewPanel.tsxseparates a shared provider, header and body. Its header offers location, reload, external-open and zoom controls. Use that separation for the artifact view's content and toolbar; do not copy its internal tab ownership. - Studio:
studio-app/components/panels/preview-tabs.tsseparates the loaded location from the live location so updating the address does not reload the frame. Studio currently mounts only the active preview frame; its remount-on-tab policy is not the proposed lifetime policy for stateful DreamLake views. - Studio:
studio-app/components/StudioRoot.tsxdispatchespreviewto the header and body and mounts the provider above the layout. - UIKit:
packages/uikit/src/components/PanelLayout/panel-tree.tsalready hasleaf,splitandgroupnodes.panelGroup([oneLeaf])currently returns the bare leaf. The always-visible single-tab requirement needs a deliberate layout change; a group builder alone does not implement it. - DreamLake:
components/notes/notePanels.tssupplies note identity and opening decisions;components/notes/panePortal.tsxpreserves mounted pane content while docking. Generalize those boundaries without changing RTC algorithms. - DreamLake:
components/artifacts/ArtifactPreview.tsxandlib/artifacts/useArtifactFrame.tssupply the isolated artifact renderer. Reuse its authorized content loading and frame boundary.
Inspected sources: Studio workspace 21042f125b318ed764c3798e1675d032404cc429;
UIKit 0ea21ec0aaf86270e16eccffab4db7aca006acb7. Local work may advance independently.
Internal host API
The implementation is components/layout/views/{pageViews,PageViewLayout}.tsx
(pageViews is a .ts module). This API belongs to the page-layout host. It does not introduce server endpoints,
store tab state in collaborative note text, or grant resource permissions.
The fragment field contains a URL fragment without the leading #.
reuse: 'existing' searches the page for an already-open resource, selects its
containing panel/tab and records the requested target. A note's identity is its
full global ID; an artifact's identity is the namespace plus artifact ID.
A fragment is a location within that resource, not another resource identity.
new-instance explicitly requests a separate view instance of the same resource;
its editor/viewer state must remain separate. It is not a default duplication
policy. Web previews normalize with new URL(...).href and reuse exact normalized URLs;
only HTTP(S) URLs without embedded username/password are accepted.
Until navigation is wired, a supplied fragment is retained as target data only. Opening a view through this API is not evidence that reference clicks already call it or that the view can scroll to the fragment.
Open a note and an artifact in one panel
The following functions use the internal page host: views is
layoutRef.current.views, and panelId is a live panel/leaf address from that
host. These are not SDK calls. A note namespace selects its display/read context;
its global note ID determines reuse.
Retain a slide or section target
Move the artifact beside the note
Reference and URL examples
Primary content stays in brackets. These examples identify the desired resource and optional location; they do not prescribe click handling:
Corresponding location examples:
Use #/3 only when the artifact actually implements that hash route; never infer
slide numbering or invent an element ID. Preserve the exact raw reference and
its percent encoding. Named secondary attributes belong in braces; raw IDs in
braces are not the preferred directive convention. Existing saved hash-prefixed
and named-attribute forms remain compatible; no bulk note migration is planned.
Static API HTML retains the full token as an atomic source mapping and does not
resolve private resources. A tab target or reference never substitutes for the
server's permission check. Do not persist bearer/share tokens as view identity.
Implementation and acceptance boundary
- UIKit adds opt-in
tabbed,showSingleTab,renderTab, persistent connected surfaces, user-close delegation, and splitting a tab out of its own group. Existing non-tabbed consumers retain their rendering behavior. PageViewLayoutintegrates note, authorized artifact, HTTP(S) web preview and notes-list views. Artifact location/reload/zoom/external-open controls share the web-preview toolbar pattern and reuse the existing isolated artifact frame.- Artifact read grants are passed per view/request so simultaneous viewers do not borrow another view's share token. Layout descriptors contain no grants.
- Pure host operations support reuse, independent instances, selection, indexed movement, splitting and guarded close. Fragments are retained as target data, and are not dispatched to navigation by this API.
snapshotPageViews/restorePageViewsvalidate bounded descriptor-only trees, reject duplicate IDs/unknown view kinds/invalid split sizes, and strip unrelated fields. Automatic page persistence is not enabled. Legacy project render slots are host-owned and cannot be exported as registered resource descriptors.- Closing the last view shows an empty region with an open/move hint. Close is disposal only, never a resource delete. Direct route navigation still has its existing page lifetime; opening tabs within the page preserves view instances.
Local automated evidence covers tree/API behavior, plain-singleton to multi-tab header transitions, keyboard selection, dirty-close refusal, descriptor restoration, real CodeMirror identity/undo/subscriptions, concurrent artifact authorization, and mocked browser note-list reuse, tab edge dragging, iframe document retention and inaccessible resources. Browser fixtures intercept resource APIs and block real RTC connections; no production note was edited. Owner visual acceptance and live RTC acceptance remain separate from these local checks.
Delivery: local implementation and tests only; no merge, UIKit publication, docs deployment, app deployment or skill publication. Public skill impact: none for this internal page-host API and UI gesture change; no public SDK/CLI operation, reference grammar or agent authoring procedure changes. The existing reference and fragment experiments remain a separate workstream.