kind:"web" . Its canonical id is w:<url>; a fork uses nextPopupWebTabId to form w:<url>:popup:…. Floating does not remove the leaf.
tabs[] / groups[] / splitWebTabId / activeId
Engineering record · Synchronized with current code · Implemented
This document answers where a web leafis currently displayed, who controls it and what the Agent can do with it. Interaction rules follow one invariant rather than separate patches.
Thepairing between session S and page W is the sole first-class relationship. Split and preview are two presentations of that pairing. A WebTab leaf has only one native WebContentsView rectangle at a time; chat PiP directly presents that Page's interactive native view. When the same leaf is active as a normal top tab, hide the chat preview and its reserved space and let the native page fill the content area. The page tab and PiP must not appear simultaneously or replace the page with a mask or screenshot.
Pairing is established when an Agent opens W in S, the user drags W into S's split/group, the user collapses it, or an Agent operates an unpaired page.show() and registerPipPair write to the pairing table; the coverage function only decides which host renders that pairing now. Standalone pages have no pairing before Agent interaction and therefore no collapse-to-preview action.
kind:"web" . Its canonical id is w:<url>; a fork uses nextPopupWebTabId to form w:<url>:popup:…. Floating does not remove the leaf.
tabs[] / groups[] / splitWebTabId / activeId
pairedOwnerByTabId. A session may pair with multiple pages; closing either side clears it. Each session has one preview slot showing the Agent's current working page; other paired pages remain in the tab bar.
Module-level Map, registered through show() / registerPipPair
(tabId, ownerTabId): which paired page currently occupies the preview slot. Switching sessions does not change the binding;hide() moves it into background memory. Rebinding another page does not erase the old pairing.
useWebTabPip singleton, not persisted
pipCoversCenter(tabId, ownerTabId, center) as a pure function of binding × active tab. Components do not store a separate visibility state.
pane · chat PiP · none
First-class state therefore consists of pairing, layout (tab-bar membership, groups and active tab), active binding and background memory. Split and floating views are two presentations of one pairing, not separate relationships.
Compound group groups[] At most two members, projected as one tab-bar entry. User-created groups use groupTab; PiP Expand to split uses setSplitWebTab to pair the current session and leaf.splitWebTabId is a compatibility pointer, materialized into a group during normalization only when the leaf is ungrouped and the active tab is a session.
When an Agent repeatedly invokes open / activate on pages in one session, the preview slot follows attention: the new working page show enters the slot (or revealAgentWebTab inserts an unpaired page or a page paired with the current session but not visible, recording pairing), and the previous page returns to the tab bar. Pages paired with other sessions are excluded and not taken over.
The same binding,ex.dev owned by session A, under different activeId values. Select a scenario to inspect which coverage condition excludes its host.
end() clears only the binding; the leaf stays in center-tabs store unless closeTab. Switching sessions is not a transition; the coverage function returns a different value for the same binding.| Field / action | Meaning |
|---|---|
tabId · ownerTabId | Active binding. Both values are present or both null;pipBoundTabId() uses them to determine coverage. |
backgroundTabId · backgroundOwnerTabId | hide() Restoration record left by hide. Its sole reader is WebPreviewChip. |
rect | Preview geometry, memory only.clampPipRect Constrains it within .center-pane-chat , minimum 240×160.hide/end do not clear rect. Before dragging, default placement is upper-right below the session header (.chat-view-controls: perspective switch + ⋯); after dragging, inline rect takes precedence. |
show(tabId, ownerTabId) | Registers pairedOwnerByTabId, establishes the active binding and clears background. The global singleton slot replaces the old binding but preserves the old page's pairing. |
hide() | Clears active binding and writes background. Meaning: the user temporarily hides the preview and can restore it in one action. |
end() | Clears both binding and memory. Meaning: this page is no longer any session's preview. |
pipCoversCenterThe preview renders if and only if pipCoversCenter holds:pipCoverBase and owner checks are true, and activeId is the owner session. Split retains exclusive native bounds (the preview does not render whensplitWebTabId === tabId ). Opening the page tab makes coverage false and hides preview while retaining binding.
kind === "web"; visibleIds : split already renders it, so no second view is overlaid;splitWebTabId !== tabId; activeId === ownerTabId.PiP floats only over chat, inside .center-pane-chat. When the page tab is active, the component returns null without reserving native docking space. The store subscribes to center-tabs: owner or leaf deletion → end(); invalid background reference → clear memory.
The pairing table records multiple pages but only one binding is active. Presentation is the output of pipCoversCenter (condition 5). After switching to B, the store still holds (ex.dev, A), but the component does not render because live === false ; returning to A makes the same function true again. These are not two events.
Reappearing on return applies only to anactive binding. Clicking ✕ calls hide() and moves binding to background; returning to A shows Show page preview in the composer, not an automatically reopened window.
pipCoversCenter Is false when activeId === tabId . Binding (tabId / ownerTabId) and follow preference remain.WebTabPane retains the full toolbar, and.webFrame fills the available content area without PiP docking space or rendering PipBoundMask. Returning to the owner makes coverage true unless the user invoked hide().
ensureWebView; native bounds come only from the measured .webFrame.CollapseToPipButton and BrowserMenu's collapseToPip use the same resolution: bound → Return to X's preview; unbound with target → Float to X; no target → no button or menu item. Standalone pages have no such action.show(id, B) overwrites the global binding. After opening, wait for waitForWebTabReady (2s)→ bridge.webTab.activate → webtab_result. On non-split failure, restorePriorActiveTabAfterFailedWebOpen restores focus.closeAgentWebTabResult(tabId, tabs, groups) Is a pure function:
| Condition | Result |
|---|---|
| Leaf missing or not web | {ok:false, reason:"tab_not_found"} |
Appears in any group's memberIds | {ok:false, reason:"tab_in_user_layout"} |
| Others (standalone tab, PiP binding, background) | {ok:true} → closeTab; if it is the PiP leaf, the subscription then invokes end() |
A leaf entering a user layout (group) is treated as user-owned. An Agent-opened leaf loses close permission once dragged into a group. Conversely, an Agent can close a user-opened standalone page; implementation does not track the opener.
surfaceRefForChat(sessionId) Priority agrees with the host model: visible web pane in the session group → splitWebTabId when the session is active → PiP leaf when the active session is owner. If none exist or bounds are unreported, omit the surface.visibleWebTab / visibleWebTabById Uses peekLiveWebTabPipId; a binding on a non-owner session is excluded from Agent-visible inventory.op=preview/activate 's target is invisible,revealAgentWebTab when the active center page is a session and the target is a web tab unpaired or paired with that session, invokes show to place it in that session's preview slot, also establishing pairing if needed, and waits for webview readiness before execution. Reject pages paired to other sessions without takeover or operation. After split opening, registerPipPair records ownership without passing through show().
| Rule / concept | Implementation location |
|---|---|
Pairing table, active binding, background,show/hide/end | apps/web/lib/browser/web-tab-pip-store.ts · pairedOwnerByTabId / pipPairedOwnerFor / useWebTabPip |
| R1 Disappear on leave / reappear on return | Same as above · pipCoverBase / pipCoversCenter / peekLiveWebTabPipId |
| R3 fork | Same as above · pipOpenMustFork; leaf creation is in center-tabs-store.ts 's ensureWebTab / ensureExclusiveWebTab; ids in center-tab-ids.ts 's webTabId / nextPopupWebTabId |
| R5 Collapse target | pipCollapseTargetFor(same-group session → pairing table → null) / collapseWebTabToPip; without a target, CollapseToPipButton returns null and BrowserMenu omits collapseToPip |
| Agent-interaction reveal (unpaired or current-session pairing) | Same as above · revealAgentWebTab; desktop-bridge.ts 's op=preview/activate After successful reveal, waitForWebTabReady executes. Split path: registerPipPair |
| Clear binding and pairing when owner / leaf closes | web-tab-pip-store.ts At the end of useCenterTabs.subscribe |
| Preview: live view, dragging, bounds, screenshots, expand / fullscreen / ✕ | apps/web/components/center-tabs/web-tab-pip.tsx · WebTabPip; mounted inside app-shell.tsx 's .center-body , default .webPip upper-right,top:78px(below header buttons) |
| R2 Opening page hides preview | web-tab-pip-store.ts 's pipCoversCenter True only in the owner session;web-tab-pane.tsx 's .webFrame fills the content area without a docking portal |
| Screenshot cache | usePipSnapshots / getSnapshot / setSnapshot; only legacy bridges without native-view support fall back to capture; modern desktop PiP does not poll screenshots |
| Agent open / close / receipts | apps/web/lib/desktop/desktop-bridge.ts · installDesktopMenuHandlers Internal WS branch;closeAgentWebTabResult; restorePriorActiveTabAfterFailedWebOpen |
| Session↔page pairing and visibility | Same as above · surfaceRefForChat(PiP fallback reads peekWebTabPipId / peekWebTabPipOwnerId)/ visibleWebTab / visibleWebTabById(uses peekLiveWebTabPipId)/ browserPageInventory |
| Desktop split availability | app-shell.tsx Calls setDesktopSplitLayoutAvailable(isDesktop && activeKind==="session" && isSplitLayoutAvailable(width)); threshold in split-layout.ts (360+480+6) |
| Restore entry after ✕ | apps/web/components/chat/composer/environment-row/chips/web-preview-chip.tsx · WebPreviewChip |
| Split / group model | center-tabs-store.ts (groupTab / ungroupTab / setSplitWebTab / openWebTabInSplit); center-tab-groups.ts (MAX_CENTER_TAB_GROUP_MEMBERS = 2); center-tabs-persistence.ts Normalization |
| Behavior checks | apps/web/scripts/runtime/check-web-split.mjs(coverage, collapse, fork, close and surface) |
These are existing differences between code and product semantics, not newly proposed work.
Fork ignores memory after hide.pipOpenMustFork Checks only active binding. After A clicks ✕ and B opens the same URL, ensureWebTab reuses the leaf and show(id, B)——show clears background, silently removing A's restore chip. No takeover precisely covers currently displayed previews, not a session's preview resources.
Fork recognizes only canonical leaves.The condition is webTabId(url) === boundTabId. When a third party opens an already-forked popup id, the condition is false andensureWebTab returns another canonical leaf. The outcome happens to be correct, but the check protects id shape rather than every bound same-URL leaf. Each fork leaves an orphan leaf, cleaned by op=close or manual user closure.
Close does not track the opener.The product says an Agent can close tabs it opened but not tabs in user layouts. Implementation checks only group membership. User-opened standalone pages can be closed; Agent-opened pages dragged into groups cannot.
Fork is absent from the split path.On a wide desktop layout with an existing web member,openWebTabInSplit changes that pane's URL without calling pipOpenMustFork. Same-URL conflicts apply only to the PiP branch.
No takeover protects leaves, not slots.R3's fork gives B a new instance, but the same show(id, B) overwrites the sole global binding. A's canonical leaf remains but its binding disappears, so returning to A does not restore preview. Collapsing another page to preview similarly replaces the old binding silently. If no takeover means retaining A's preview resource, implementation does not satisfy it.
Show does not ungroup.When an Agent binds a canonical leaf already in another group's layout as its own PiP, it does not call ungroupTab. Returning to that group retains the member; if the leaf is activeId, coverage is false, preview hides and the native page fills the content area.
Region definitions differ.surfaceRefForChat Marks ungrouped paired pages, including PiP, as region:"right"; browserPageInventory Marks ungrouped visible pages as "center". Agent-visible left/right/center positions disagree with inventory.
Condition 5 implies condition 4.Every owner in show is a session tab, andactiveId === ownerTabId already guarantees kind === "session".pipCoverBase . The condition remains so WebPreviewChip can independently reuse pipCoversCenter before restoring as a fallback.
PiP is absent from version-2 payload.Refresh loses binding, background and preview position; leaves return to normal center tabs. Reappearing on owner return applies only within the same renderer lifetime.
Back, Forward, and New tab each use a 28px-wide button with an 8px gap. An empty tab-list container takes no layout space. New tab has no left divider.
The final visible tab can close completely. Its window and empty tab strip remain open, and the center content area stays blank. The plus button explicitly opens a new tab. Session-owned hidden pages remain available in Resources and never become the close fallback. Close selects visible neighbors after normalizing split groups. The empty state persists across route changes and reloads; it does not automatically create a draft conversation. Moving tabs between windows retains its existing window lifecycle.
Acceptance: close the final tab through the close button or keyboard command; no visible tab or center content remains, and explicit open actions create the requested tab.
The supplied Codex screenshot is a visual reference; its internals are not established by the image. Adopt continuous visible content, direct input and frame-coalesced dragging. Reuse the existing Electron WebContentsView and visible-bounds collection instead of creating another Page or forwarding image coordinates. The native view remains displayed during movement. Each animation frame updates the host transform and native bounds; only release persists geometry. Native input keeps the existing browser control and permission behavior.
The plain browser uses a sandboxed iframe for HTTP(S) pages. It is a separate embedded browser document, not a connection to the Desktop Page; login state and frame-ancestor restrictions follow browser policy. Restricted URLs display an unavailable message and retain Open page. Older Desktop bridges without visible-view support keep a labeled image fallback. Hidden, switched or unmounted previews release bounds and observers without closing their Page. Menus and dialogs occlude native content.
Acceptance: the public WebTabPip mounts the exact tab id, performs no capture on a current Desktop bridge, updates native bounds during drag without recreating or hiding the page, preserves pointer cleanup and session isolation, and exposes iframe interaction without a body click overlay. Native App acceptance verifies a changing counter, clicking, typing, scrolling and continued updates during drag.
Source implementation passes independent specification and quality review. Installed-App acceptance and verification results are recorded separately; a source review does not establish native rendering performance.
Pressing a tab uses the same background and backdrop blur as dragging, without an additional pressed outline or shadow, and does not change the active page, route or conversation. Keyboard focus retains its focus-visible indicator. A release without a drag activates it through the ordinary click path. A drag, Escape, cancellation or lost focus consumes the pending click. Inside and outside the tab strip, tab dragging and pane-label drag previews use the same background, backdrop-filter and shadow CSS variables. In-strip reorder and detach intent do not override these effects. Settings → General provides transparency (0–100%), backdrop blur (0–40px) and shadow strength (0–100%), a live preview and Restore defaults. Defaults are 20% transparency, 16px backdrop blur, 1.2 saturation and 25% of the original shadow alpha. Label text, icons and controls stay opaque; only the content behind the preview is blurred. Local storage persists the normalized numeric values, the pre-paint theme script applies them before hydration, and storage events synchronize same-origin windows. Theme changes preserve these preferences. Invalid or missing fields use defaults, finite numeric values are clamped, and zero blur or shadow disables that effect. Ordinary buttons and Resources outlines are unaffected. Dragging within the strip preserves reorder; releasing outside it preserves the existing transactional window transfer.
The New window cue stays inside the top tab strip, above the native webpage bounds. It does not follow the pointer into a WebContentsView. CSS stacking cannot place renderer content above an Electron child view; hiding the page or creating another native overlay is unnecessary for a strip-level cue. Existing pointer capture and desktop transfer transactions remain authoritative.
A single webpage tab dragged onto Resources in an existing visible conversation attaches that exact Page to the conversation on release. The Resources navigation entry works even while another sidebar view is selected; the open Resources body is also a target. When Resources is not selected, only its navigation row accepts and highlights a drop; the other sidebar view is not a Resources target. When Resources is expanded, its body also accepts drops and the preview highlights that body below the navigation row. The row and body are never combined into one rectangle. Feedback uses one border without blurring the contents. Its rectangle matches the target's outer bounds with no inset; the navigation-row outline uses the same corner radius as the button. Success opens Resources while preserving the active conversation, webpage identity, top tab and original associations. Attachment grants use through the existing resource governance; it does not interrupt or override another execution's active operation. Compound groups, unsent drafts and other tab kinds are not attachment sources or destinations.
The owner-authorized server verifies the session and exact live window/tab/native-target identity from the registered desktop connection before retaining the association. It reuses BrowserResourceStore rather than creating a URL copy. Failure reports an error and does not navigate, detach or fabricate a successful binding. Native connection changes reject stale results. Drag cancellation sends no attachment command. Keyboard activation and context menus retain their existing behavior.
Behavioral tests cover press versus release, drag/cancel click suppression, Resources targeting, same-Page attachment, unauthorized and stale identities, failure and duplicate attachment. Existing Web, desktop, resource and contract checks remain required, followed by installed-App acceptance. The source implements these paths; independent reviews and the listed acceptance gates are required before release.
The supplied desktop screenshot establishes the visible contract: chat on the left, a document on the right, an output file card, and an explicit refresh action after changes. Reuse existing CenterTab groups and DocumentWindow; preserve global widths, font tokens, resizing, navigation and user-owned layouts.
Completed live assistant replies expose local Markdown file links and send_file attachments as compact document rows. The first supported output opens automatically beside its own currently focused conversation. Historical hydration, failed or cancelled replies and background conversations never open a preview. An existing manual split or dirty preview is retained; output rows remain available for explicit opening. Preview tabs are session-attributed, and reopen the same authorized file instead of accumulating duplicate panes.
Project-contained output files use the existing project document owner. Other absolute output paths use the existing session-checked read-only attachment owner. File content is never executed, embedded as raw HTML, or authorized by a model-generated pathname. New preview controls use the installed animated Lucide icon set, keyboard focus and reduced-motion behavior.
A visible document checks lightweight, owner-authorized filesystem metadata. A changed version displays File changed with Refresh. Refresh also remains available at all times, reads current disk bytes without browser caching, and updates the preview only after success. Local edits, pending publications and rich-editor changes are never discarded by Refresh. The existing explicit discard and conflict flows retain their behavior. Hidden panes stop polling; failed metadata or content reads retain the old display and provide a retry.
Acceptance uses the public reply finalization, output rows, tab store and DocumentWindow: a newly completed file output creates chat-left/file-right; same-path output does not duplicate; changing files preserves old tabs and drafts; background/history/remount cannot take over; actual PDF/text preview changes only after Refresh. Metadata shares the existing root checks and disclosure bounds.
Logical file tabs that reference the same project document share one persistent physical editor. Switching conversations moves that existing pane into the active layout and retains its native engine and draft. Closing a tab closes the controller only when no retained file tab references that document. If duplicate aliases are visible together, the other pane offers Show here to move the existing editor without mounting a second one.
Output cards, owner-bound splits and draft-safe refresh use the shared tab layout and document controller. Component and browser checks cover these behaviors; independent review, release gates and installed-App evidence are maintained in the task verification record.
The 40 CSS pixel desktop tab row, navigation arrows and tab controls share one vertical center. macOS uses native hiddenInset controls with a 16 DIP button frame. Electron positions those controls in device-independent pixels, while Chromium scales the tab row with renderer zoom. A fixed native inset therefore drifts from the row after zooming.
The main process reads the owning renderer's current zoom factor and sets the native top inset to round((40 × zoom − 16) / 2), clamped to zero. The horizontal inset is the top inset plus 1 DIP. The current macOS native red circle is inset one DIP farther vertically than horizontally within its control frame; this correction gives equal visible top and left margins. Both axes follow renderer zoom; tab horizontal spacing stays unchanged. Window creation, document load, renderer resize and exit from fullscreen refresh the position. The preload sends a payload-free resize notification; the main process accepts only the owning BrowserWindow's main document. Embedded websites, subframes and overlay views cannot move window controls. No polling, persisted geometry or zoom reset is added. Windows retains its existing Window Controls Overlay; Linux retains native decorations.
Adopt Electron's setWindowButtonPosition and getZoomFactor. Do not rely solely on zoom-changed, whose documented event concerns mouse-wheel zoom requests. Renderer resize covers menu and keyboard zoom without duplicating zoom state. The supplied screenshot is a visual alignment reference, not evidence of another product's internal implementation.
Acceptance: native control centers and navigation/tab centers differ by at most one DIP at ordinary supported zoom, and the visible red-circle top and left margins differ by at most one DIP; native rounding is expected. Initial load, resize, zoom and leaving fullscreen preserve this result in each window. At extreme zoom where the row is shorter than the native control frame, the native inset is zero. Existing controls, tabs, window lifecycle and non-macOS behavior retain their contracts.
The shared desktop chrome module implements native alignment. Production checks cover actual preload resize notifications, sender ownership, zoom geometry and lifecycle wiring. Independent review, final verification and installed-App visual acceptance are separate release gates.
Every application route, including Settings, Agents, Abilities, Applications, conversation History and Scheduler, is a visit in the selected tab. The visible tab label follows the current page. Management routes use their page or settings-section label; conversation, file and webpage titles remain their existing content metadata.
Back and Forward restore the selected tab's own route and label. Switching tabs restores that tab's current page. Reload and transfer preserve management visits instead of removing them. Visiting a page does not rename the underlying conversation or change another tab.
A split holds only chat-area content. Visiting a page from a tab inside a split opens that page in a new top-level tab placed after the split, and the split keeps its members and layout. A restored split member that already carries a page route leaves its split.
Reuse navigationRoute and pageHistory. Route labels are derived for presentation, with no second title or history store. Unknown route labels use their route path rather than a stale content title.
Acceptance: navigate a single tab through a conversation, Agents, Abilities, Applications and Settings sections; check visible names, Back/Forward, switching to another tab, persistence and transfer. Background conversation-title updates must remain available when returning to the conversation.
Restoring a detail route from another page first loads its exported host route, then replaces the temporary host URL with the saved detail URL before recording the visit. Duplicate activation requests share one navigation. If a newer tab activation occurs before the host commits, the late commit restores the latest target and is not recorded as a visit. Provider detail routes, skill detail routes and plugin detail routes must never trigger a document reload when switching tabs.
Provider selection derives from the current route parameter. A late list response may choose a fallback only for the unqualified provider page; it cannot overwrite a URL-selected detail. Changing the detail does not reload the provider catalog.
Dynamic route labels and retained page visits pass independent specification and quality review. Store and React navigation checks cover current names, independent histories and persistence. Frontend builds, static checks and bilingual documentation checks pass; installed-App acceptance is recorded separately.