pane_chrome_and_promotion_spec - mark-ik/graphshell GitHub Wiki
Date: 2026-02-28 (revised 2026-04-06) Status: Canonical interaction contract Priority: Implementation-ready
Related:
WORKBENCH.mdworkbench_frame_tile_interaction_spec.md-
pane_presentation_and_locking_spec.mdโ canonical authority forPaneLock(ยง8 here defers to it) -
2026-03-03_pane_opening_mode_and_simplification_suppressed_plan.mdโ canonical authority forPaneOpeningModeandSimplificationSuppressed -
../../archive_docs/checkpoint_2026-04-02/graphshell_docs/implementation_strategy/workbench/2026-02-22_workbench_tab_semantics_overlay_and_promotion_plan.mdโ archived execution note for the completedFrameTabSemanticsrollout; canonical semantic-tab contract lives in../graph/multi_view_pane_spec.md ยง7 -
../canvas/node_badge_and_tagging_spec.mdโ badge strip rendering contract referenced in ยง4.1 -
../shell/2026-04-03_shell_command_bar_execution_plan.mdโ the Shell command bar cleanup that motivated the graduated chrome model -
../../../TERMINOLOGY.mdโTiled Pane,Docked Pane,Pane Presentation Mode,Tile
This spec defines the canonical contracts for:
- Pane Presentation Mode โ the graduated chrome model and its per-mode rendering rules.
- Tile Viewer Chrome Strip โ the per-pane viewer toolbar rendered for Tiled panes.
- Compatibility mode โ Wry as a local tile chrome affordance for web compat fallback.
- Tab selector overlay โ when and how the tile-selection chrome renders.
- Pane opening mode boundary โ where graph-citizenship decisions stop and chrome behavior begins.
- Presentation-mode transitions โ moving panes between Tiled, Docked, and Floating presentation.
- Tab ordering and reorder โ drag-reorder semantics within a Tile.
- Pane locking โ preventing accidental reflow.
- Floating pane promotion โ the canonical path from ephemeral overlay pane to graph-backed tile.
This spec does not define duplicated cross-context appearances as
presentation-instances of one shared node. Reuse across frames/graphlets is
handled by explicit node operations (Move, Associate, Copy) in graph /
workbench authority. Navigator lifecycle acts on node-bearing container entries,
not on bare pane instances.
Chrome affordances graduate with a pane's lifecycle status in the graph-backed workbench. A Pane is an ephemeral content carrier, not yet a graph citizen. A Tile is a graph-backed workbench citizen. Only Tiles expose viewer affordances. The Shell command bar and workbench host chrome are for navigation structure, arrangement, frames, and context โ not per-viewer command ownership.
Per-viewer controls (Back, Forward, Reload, Zoom, compatibility mode) live in the Tile Viewer Chrome Strip (ยง3), rendered per-pane and scoped to the pane's own content. This replaces any prior model where viewer controls were hosted in a global toolbar.
Every pane has a Pane Presentation Mode (also called Pane Chrome Mode) that controls its chrome, mobility, and locking behavior. This is distinct from both the pane's content and the pane's Pane Opening Mode (the graph-citizenship decision that determines whether the pane exists only as an ephemeral open surface or as a graph-backed tile).
PanePresentationMode =
| Tiled -- full chrome; normal tile-tree mobility
| Docked -- reduced chrome; position-locked
| Floating -- chromeless overlay; ephemeral by default; promotable to Tiled
| Fullscreen -- content-only; all chrome hidden (future; not in current scope)
Invariant: PanePresentationMode is workbench-owned state. It does not affect graph content identity. Changing the mode of a pane must not create or delete graph nodes, write addresses, append traversal history, or otherwise mutate graph data.
Exception โ promotion: transitioning a Floating pane to Tiled via the
promote action (ยง2.3, ยง6.3) is the one presentation-mode transition that does
cross the graph-citizenship boundary. It is the canonical Promotion event:
graph citizenship is written, a node is created or reused, and a PaneId plus
any required container membership are assigned. All other mode transitions
remain graph-neutral.
The primary presentation mode. Tiled panes are graph-backed workbench citizens with full viewer affordances.
โโ Tab Bar (full: favicon, title, frame chip, close) โโโ
โโ Tile Viewer Chrome Strip โโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ [<] [>] [R] example.com/page [1:1] [-][+] [Compat] โ
โโ Viewer Content Area โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ (Servo composited texture or Wry overlay) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- Renders with tile-selector chrome: tab bar strip, split/close affordances, resize handles.
- Renders a Tile Viewer Chrome Strip (ยง3) between the tab bar and the viewer content area, carrying per-pane navigation, zoom, and compatibility mode controls.
- Participates in all tile-tree mobility operations: split, move, reorder, close, open into a separate frame.
- Normal drag-and-drop target and source.
Docked panes are graph-backed but present with reduced chrome. They are intended for reference content, pinned panels, and secondary views that should not compete visually with the active browsing context.
โโ Tab Bar (compact: title + close) โโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ (content, no viewer chrome strip) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- Renders with compact tab chrome: title, favicon, and close button. No frame chip, split offer, or resize handles.
- No Tile Viewer Chrome Strip. Navigation, zoom, and compatibility mode controls are not rendered. Users access these via keyboard shortcuts, the command palette, or the graph.
- Position-locked: drag-to-reorder is disabled. User cannot accidentally drag a docked pane out of its position.
- Docked panes are still closeable via their title-bar close button or keyboard shortcut.
- Docked panes are eligible for focus; focus behavior follows the Focus subsystem contract.
- A docked pane may be explicitly restored to
Tiledpresentation by the user (see ยง6.1).
Rationale: Docked presentation reduces visual noise and accidental reflow for auxiliary surfaces (e.g., a persistent diagnostics panel, a side-by-side reference node). Omitting the viewer chrome strip reinforces the visual distinction between the active browsing context (Tiled) and reference content (Docked).
A Floating pane is an ephemeral content carrier rendered over the tile
surface. It is not a graph citizen: it has no PaneId and no
ArrangementRelation edge until the user explicitly promotes it.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ (content, chromeless) โ
โ โ
โ [Promote โ] [Dismiss ร] โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Chrome contract:
- No tab bar, tab strip, or title.
- No Tile Viewer Chrome Strip. No Back/Forward, no zoom, no compatibility toggle.
- No drag handle. Edge-drag resize is allowed (the user can adjust dimensions freely after open).
-
SimplificationSuppressedis set automatically when the pane opens and cleared on promote or dismiss. - Two affordances only:
| Control | Label | Action |
|---|---|---|
| Promote | Promote | Promotes pane to graph-backed Tiled mode via PromoteEphemeralPane (see ยง6.3) |
| Dismiss | X | Demotes node to cold and closes pane without graph write |
These controls render as a compact bar above the content area. They are always visible (not hover-gated) to ensure discoverability in the prototype.
Lifetime contract:
- A
Floatingpane's lifetime is scoped to the hosting surface/context it overlays. That host may be a tile surface, graph surface, frame context, or split context. If that host closes, the floating pane is discarded without any graph write โ it was never graph-citizened. - A
Floatingpane that is dismissed via X produces no graph node, no address write, and no traversal edge. - A
Floatingpane is not a member of any Tile or tile tree container. It floats above the tile tree render layer until promoted or dismissed. - Use cases: link-follows, previews, ephemeral content inspection.
Rationale: This formalises the "chromeless dialog/temp window" UX that previously appeared as an undefined side effect. The Floating mode makes it a managed, intentional surface with predictable lifecycle and a clear upgrade path to full graph citizenship.
The Tile Viewer Chrome Strip is a per-pane horizontal toolbar rendered between
the tab bar and the viewer content area. It is the canonical home for
per-viewer navigation and rendering controls. It renders only in Tiled
presentation mode.
The strip renders as a single ui.horizontal() row. Left-aligned controls
provide navigation; right-aligned controls provide zoom and compatibility mode.
A compact URL display occupies the center.
[<] [>] [R] | example.com/path/to/page | [1:1] [-] [+] [Compat]
โ nav โ โ โ url โ โ โ zoom โ โ compat โ
separator right-to-left layout
| Control | Label | Command | Hover text |
|---|---|---|---|
| Back | < |
BrowserCommand::Back |
"Back" |
| Forward | > |
BrowserCommand::Forward |
"Forward" |
| Reload | R |
BrowserCommand::Reload |
"Reload" |
Navigation commands are routed via
graph_app.request_browser_command(ChromeProjection { fallback_node: Some(node_key) }, command).
This targets the pane's own node without requiring EmbedderWindow access from
the tile behavior context.
The pane's current node URL is shown as a compact, read-only label truncated to fit the available width. The URL is for orientation only; address editing is handled by the Shell omnibar.
| Control | Label | Command | Hover text |
|---|---|---|---|
| Reset | 1:1 |
BrowserCommand::ZoomReset |
"Reset zoom" |
| Zoom out | - |
BrowserCommand::ZoomOut |
"Zoom out" |
| Zoom in | + |
BrowserCommand::ZoomIn |
"Zoom in" |
Zoom commands use the same ChromeProjection routing as navigation.
Wry is framed as a compatibility mode โ a local tile chrome affordance for sites that don't render correctly in the default Servo-based renderer.
| State | Label | Tooltip |
|---|---|---|
| Inactive | Compat |
"Load in compatibility mode (Wry) for sites that don't render correctly" |
| Active | Compat * |
"Using compatibility renderer (Wry). Click to switch back." |
| Unavailable |
Compat (disabled) |
Reason from wry_unavailable_reason (feature disabled, capability missing, or preference) |
Clicking the toggle swaps between the Wry viewer backend (viewer:wry) and
clearing the viewer override (returning to automatic resolution). The toggle
replaces the prior "Render With: Auto/WebView/Wry" selector.
When TileRenderMode::NativeOverlay is active (Wry overlay), the OS window
covers the pane's content rect. The Tile Viewer Chrome Strip is rendered by
egui above the compositor rect allocation โ it is not covered by the
overlay. The content area's allocate_exact_size(ui.available_size()) call
naturally shrinks to accommodate the strip.
The chrome strip is rendered by render_tile_viewer_chrome_strip() in
shell/desktop/workbench/tile_behavior/node_pane_ui.rs, called from the
PanePresentationMode::Tiled branch of render_node_pane_impl().
Each Tile that contains multiple node entries renders a tab bar strip (legacy term: Workbar for frames; now: frame tabs in a workbench-scoped Navigator host or per-tile tab strips for multi-node tiles). The tab strip:
- Shows one tab entry per child tile with: title, badge strip (compact, per
../canvas/node_badge_and_tagging_spec.md ยง3.5), close button. - Active tile's tab is highlighted.
- Tab bar is scrollable horizontally if tab count overflows available width.
When the cursor hovers over a non-active pane within a multi-node Tile, a tile-selection affordance is shown:
- A hover ring or highlight border on the pane boundary.
- The tab bar scrolls to make the hovered pane's tab entry visible.
- Clicking the pane body (not an interactive element within it) activates that pane.
Invariant: The hover affordance must not interfere with content interaction within the pane. Pointer events must be forwarded to the pane content when hovering over content areas.
The active tab renders a distinct visual indicator (accent underline or fill) that is visible at all zoom levels and in reduced-motion mode. The indicator must not rely on animation alone to convey active state.
This spec does not define the full Pane Opening Mode model. It defines the boundary between opening semantics and chrome semantics so they do not get conflated.
Canonical boundary:
- Pane Opening Mode decides whether opening content creates graph citizenship.
- Pane Presentation Mode decides how an already-open pane renders and how much tile chrome it exposes.
Rules:
- Opening a pane in an ephemeral mode may create a visible pane without writing any graph node.
- Creating graph citizenship (for example, writing a pane address into the graph and turning it into a graph-backed tile) is an opening-mode concern, not a chrome-mode concern โ except for
Floatingpane promotion (ยง6.3), which is the explicit crossing of this boundary by user intent. - Moving a node into another frame, associating it with another graphlet, or copying it into another context are explicit node operations and are outside the scope of pane chrome. This spec must not describe those operations as generic "open elsewhere" behavior.
- Once a pane is already graph-backed, switching between
TiledandDockedchanges only presentation and lock affordances. - Internal surfaces that are graph-backed at creation time (for example
verso://tool/*,verso://view/*,verso://frame/<FrameId>) are already across the opening-mode boundary before this spec applies. -
Floatingpanes opened inQuarterPaneorHalfPaneopening mode begin as ephemeral. TheirPaneOpeningModetransitions toTileonly when the user triggers promotion (ยง6.3).
Compatibility note:
- Older docs may still use
graphshell://...for these same internal surfaces. - Treat
graphshell://...as the legacy alias; runtime canonical formatting isverso://....
This separation is required by the address-as-identity model in TERMINOLOGY.md: graph citizenship follows address write and node existence, not the presence or absence of tab chrome.
Triggers:
- Right-click on a docked pane's title bar -> context menu "Show Tile Chrome"
- Keyboard shortcut (configurable; default unbound)
- Command palette: "Show Tile Chrome"
Effect:
- Pane
PanePresentationModechanges toTiled. - Pane is inserted into the tile tree at its current position (it was already in the tree; only chrome mode changes).
- Full tile-selector chrome โ including the Tile Viewer Chrome Strip (ยง3) โ is restored.
- Workbench emits a
PanePresentationModeChangedsignal for observability.
Triggers:
- Right-click on a tab entry -> context menu "Dock pane"
- Keyboard shortcut (configurable; default unbound)
- Command palette: "Dock pane"
Effect:
- Pane
PanePresentationModechanges toDocked. - Position in tile tree is preserved (pane is not moved; only chrome mode changes).
- Tile Viewer Chrome Strip is removed; compact tab chrome applied; drag affordances hidden.
- Workbench emits a
PanePresentationModeChangedsignal for observability.
Invariant: Presentation-mode transitions (ยง6.1, ยง6.2) never move or remove the pane from the tile tree. They only change the presentation mode. Content and graph state are unaffected. The sole exception is ยง6.3 (Floating โ Tiled promotion), which is a graph-citizenship transition by design.
Trigger: User clicks the Promote control on a Floating pane.
Effect (in order):
- Emit
PromoteFloatingPane { target_tile_context }. - In
apply_intents():- Resolve the pane's content address and write it through the canonical graph write path.
- Create or reuse a graph node according to address-as-identity rules.
- Assign a stable
PaneId. - Assert an
ArrangementRelationedge (sub-kind determined by target tile context โ see below). - Transition
PaneOpeningModefromQuarterPane/HalfPanetoTile.
- In
reconcile_webview_lifecycle()/ workbench mutation:- Insert the pane into the tile tree at the target position (see placement rules below).
- Discard the floating geometry.
- Switch
PanePresentationModetoTiled. - Clear
SimplificationSuppressed.
- Workbench emits
PanePresentationModeChangedsignal.
Placement rules:
| Context the floating pane overlays | Placement on promotion |
|---|---|
| Inside or over an existing Tile | New node entry in that Tile; ArrangementRelation sub-kind tile-member when tile membership is graph-rooted for that context |
| Over a split (horizontal or vertical tile, no enclosing multi-node Tile) | New split at current tile tree level; ArrangementRelation sub-kind split-pair
|
| Over the bare graph canvas (no workbench tiles open) | New solo tile; no default ArrangementRelation edge is required |
Invariant: The floating pane's content and address are preserved through promotion. No content reload occurs. The pane receives its tab handle at its insertion position in the tile tree.
Trigger: User clicks the โ dismiss control on a Floating pane (hover-only, top-right corner), or closes the enclosing surface.
Effect:
- Pane is removed from the workbench render layer.
- No graph node is created. No address is written. No traversal edge is appended.
-
SimplificationSuppressedis cleared. - If the enclosing surface closed (not an explicit dismiss click), the floating pane is discarded silently โ no signal, no undo entry.
- If the user clicked โ explicitly, workbench emits
PaneDiscardedsignal (for observability; no undo entry since the pane was never graph-backed).
Non-goal clarification: Dismiss here is pane-surface discard only. It is
not the same as Navigator DismissNode, which removes a node from its current
container and may demote or delete that node depending on lifecycle state.
Within a Tile, tabs may be reordered by drag-and-drop.
- Drag target: the tab entry in the tab strip (not the pane body).
- Drop target: any position in the same tab strip (reorder within the same Tile).
- Cross-Tile drag: drops a tab into a different Tile (moves the tile membership, not just reorders).
Invariant: Tab reorder within a Tile only changes the ordered node-entry list in that container. It does not change tile tree depth or split geometry. No graph data is affected.
Docked panes are not draggable by the user. The drag affordance is hidden in docked chrome. Programmatic reorder (via intent) is still possible; only the user-interactive drag is blocked.
When a drag completes successfully, the tab animates to its new position (120 ms ease-out; respects prefers-reduced-motion). If the drag is cancelled (Esc or released outside a valid drop zone), the tab returns to its original position.
Canonical authority:
pane_presentation_and_locking_spec.mdowns the fullPaneLockcontract, invariants, diagnostics channel table, and test requirements. This section is a cross-reference summary only.
A pane may be locked to prevent user-initiated reflow operations while preserving focus and content interaction.
PaneLock =
| Unlocked
| PositionLocked -- cannot be moved/reordered; can be closed
| FullyLocked -- cannot be moved, reordered, or closed by user
Key rules (full contract in pane_presentation_and_locking_spec.md ยง3):
-
Dockedpanes are implicitlyPositionLockedfrom the user's perspective; theirPaneLockfield is nonetheless separate fromPanePresentationModeand may be set independently. -
FullyLockedis reserved for system-owned panes (e.g., a required diagnostics pane during a critical operation). It is not user-assignable through normal settings. - Lock state is workbench-owned; it does not affect graph content or node identity.
- Lock state changes must route through explicit
GraphIntentvariants; no direct field mutation from UI callsites. - Forbidden operations on locked panes must produce explicit feedback โ silent failure is forbidden.
| Criterion | Verification |
|---|---|
| Tiled pane renders Tile Viewer Chrome Strip | Test: mode = Tiled โ chrome strip with nav, URL, zoom, compat visible between tab bar and content |
| Docked pane does not render Tile Viewer Chrome Strip | Test: mode = Docked โ no chrome strip; compact tab bar only |
| Floating pane renders only Promote + Dismiss | Test: mode = Floating โ only Promote and Dismiss controls rendered; no nav, zoom, or compat |
| Chrome strip does not render for Fullscreen | Test: mode = Fullscreen โ no chrome strip rendered |
| Criterion | Verification |
|---|---|
Back button sends BrowserCommand::Back to pane node |
Test: click < โ request_browser_command(ChromeProjection { fallback_node: node_key }, Back) called |
Forward button sends BrowserCommand::Forward to pane node |
Test: click > โ request_browser_command(ChromeProjection { fallback_node: node_key }, Forward) called |
Reload button sends BrowserCommand::Reload to pane node |
Test: click R โ request_browser_command(ChromeProjection { fallback_node: node_key }, Reload) called |
| URL display shows truncated node URL | Test: node URL longer than display limit โ truncated with ellipsis |
| Zoom in/out/reset route to pane node | Test: click +/-/1:1 โ corresponding BrowserCommand sent to pane node |
| Compat toggle activates Wry viewer | Test: click Compat when inactive โ viewer_id_override set to viewer:wry
|
| Compat toggle deactivates Wry viewer | Test: click Compat * when active โ viewer_id_override cleared |
| Compat toggle disabled when Wry unavailable | Test: wry_unavailable_reason returns Some โ toggle disabled with reason tooltip |
| NativeOverlay chrome strip visible above overlay | Test: TileRenderMode::NativeOverlay โ chrome strip renders above compositor rect; not covered by OS overlay |
| Criterion | Verification |
|---|---|
| Docked pane hides split/move affordances | Test: mode = Docked โ split and drag handles not rendered |
| Docked pane is closeable | Test: mode = Docked โ close button present and functional |
| Pane presentation change does not move pane in tile tree | Test: switch Docked -> Tiled โ pane TileId remains at same tree position |
| Pane presentation change does not affect graph data | Test: switch Docked <-> Tiled โ no graph node create/delete, address writes, or traversal appends |
Tab reorder changes Vec<TileId> order only |
Test: drag tab to new position โ only container child order changed; no depth change |
| Docked pane is not user-draggable | Test: mode = Docked โ drag attempt has no effect |
| Active tab indicator visible without animation | Test: prefers-reduced-motion set โ active tab indicator renders distinctly |
| Cross-Tile drop moves tab membership | Test: drag tab to different Tile โ node entry moves to new container |
PanePresentationModeChanged signal emitted on mode switch |
Test: switch presentation mode โ PanePresentationModeChanged signal present in signal log |
| Criterion | Verification |
|---|---|
| Floating pane renders no tab bar or viewer chrome strip | Test: mode = Floating โ no tab bar, no chrome strip |
| Floating pane dismiss produces no graph write | Test: click Dismiss on Floating pane โ no graph node created, no address written, no traversal edge appended |
| Floating pane dismissed when enclosing surface closes | Test: close host Tile containing a Floating pane โ pane is discarded; no graph write |
Floating pane promotion creates graph node and PaneId
|
Test: click Promote on Floating pane โ graph node created, PaneId assigned, ArrangementRelation edge asserted |
| Promoted pane inserted into Tile as new tab | Test: promote Floating pane overlaying a Tile โ pane appears as a new tab in that tile with tab handle |
| Promoted pane loses floating geometry | Test: promote Floating pane โ floating overlay removed; pane occupies tile tree position |
SimplificationSuppressed cleared after promotion |
Test: promote Floating pane โ SimplificationSuppressed not set on resulting Tiled pane |
SimplificationSuppressed cleared after dismiss |
Test: dismiss Floating pane โ SimplificationSuppressed cleared before removal |