Olai layouts, proposed

One leaf type. A pane holds tabs, a tab holds a view. A pane lives in the row, or floats, or is pinned to an edge. The layout is a document. The address bar says / and stays there. There is no dock: what the dock did is a pinned pane with tabs, and chat is a view like any other.

This is a working prototype, not a spec. Every flow below runs in the mock shell. Right-click anything openable for the full menu: here, to the right, as a tab, pinned, floating. +click is still "to the right", or press ⌥ held in the fake browser chrome so plain clicks do that. Floats drag by their header. The panel under the shell shows the layout document as it changes, and which store each action touched.

olai.local/unchanged
olai [house] journal ✓ chat ✓ git ✓
history.state · this tabLive layout for this browser tab. One entry per change, so Back and Forward still work. Replaces the URL codec one for one.
host · <vault>/.olai/layout.jsonThe last layout for this vault, written on every change. A fresh tab, another port, or another browser opens here. Not per-tab, so tabs never collide.
preferences · pinned width, sidebar, snapGeometry that belongs to the device, not the work. A pinned pane's width lives here, not in the document.

Events

Layout document (live)


    

The flows

  1. Split a node. ⌥click install the cabinets. A pane opens to the right, zoomed on it. Kept from today.
  2. Chat is a view. Click the door on a node. The chat opens in a pane pinned to the right edge. That is what the dock was: a pinned pane. ⌥click the door and the same chat opens in the row instead.
  3. Pin and unpin. Any pane's header has a pin. Pin Today to the right and it stays through everything the row does. Unpin the chat and it is an ordinary pane.
  4. Tabs in a pane. Right-click a node, choose as a tab here. A second door click into the pinned pane adds a tab rather than replacing the chat. Two chats side by side in one pinned pane is a tab strip.
  5. Floats. Right-click, choose float. The pane floats over the row, drags by its header, minimizes to a pill, and drops back into the row. The minimized chat pill Olai has today is a float with one tab.
  6. Reload. Press ⟳ reload. Row, pins, tabs, floats, and the pill all come back from the tab's history entry. The address bar never moved.
  7. A plugin leaves. Turn the git plugin off while a commit tab is open. The tab stays, titled, and says who is missing. Turn it back on and the commit returns.
  8. A new browser tab. Press +. It opens with the last layout from the host, then has its own history. Two tabs diverge.
  9. Back and forward. Every layout change is a history entry, so the browser's own buttons walk it.
  10. Links and addresses. A link an agent wrote navigates the focused pane. Type /git into the header address box. Addresses open views. They are not where views live.
  11. Closing the last pane. Close everything. The row reseeds with the home outline. A layout is never empty.
  12. Phone. Row panes become a tab strip, the pinned pane becomes a bottom sheet, floats become pills. Same document.

What this changes, and what it keeps

Keeps

  • The pane row in the navigation plugin. Sibling views over one store is the product.
  • Pane chrome in the layout plugin: dividers, rails, drag to reorder, the phone tab strip.
  • The Alt-click rule and the two Link call sites.
  • Routes as the way a link names a page. Vault addresses stay a static grammar.
  • Back and forward, and reload restoration, which the URL gave you.
  • The home outline as what an empty layout becomes.

Changes

  • One leaf: a pane with host row or float, an optional pin, and tabs. A tab is a string kind, serializable params, and a captured title.
  • The dock is gone. Chat's "open in the dock" is "open pinned right", and a second open there is a tab. The app.panel seat is retired.
  • The minimized chat pill is a float with one tab, minimized. Chat stops owning a second surface.
  • The URL codec goes. The workspace is a JSON document in history.state and on the host.
  • Plugins claim a view kind by name. A returning plugin gets its tabs back. An absent one leaves a placeholder.
  • A right-click menu on anything openable: here, to the right, as a tab, pinned, floating. Alt-click stays as the shortcut for "to the right".

The document

{
  "v": 1,
  "panes": [
    { "id": "p1", "host": "row", "pin": null, "size": 0.5,
      "tabs": [ { "view": { "kind": "outline", "file": "house", "node": null }, "title": "house" } ], "active": 0 },
    { "id": "p2", "host": "row", "pin": null, "size": 0.5, "collapsed": false,
      "tabs": [ { "view": { "kind": "outline", "file": "house", "node": "install" }, "title": "install the cabinets" },
                { "view": { "kind": "git", "commit": "head" },                           "title": "Commit" } ], "active": 1 },
    { "id": "p3", "host": "row", "pin": "right",
      "tabs": [ { "view": { "kind": "chat", "agent": "claude-1" }, "title": "Claude Code · install the cabinets" },
                { "view": { "kind": "chat", "agent": "codex-1" },  "title": "Codex · fence posts" } ], "active": 0 },
    { "id": "p4", "host": "float", "rect": { "x": 0.55, "y": 0.2, "w": 0.32, "h": 0.5 }, "min": true,
      "tabs": [ { "view": { "kind": "day", "date": "2026-09-07" }, "title": "Today" } ], "active": 0 }
  ],
  "focus": "p2"
}

Row sizes are fractions of the unpinned row, never pixels. A float's rect is a fraction of the shell. A pinned pane has no size in the document: its width is device geometry and lives in preferences. Titles are captured at open time so a placeholder can still say what it was. A tab's params must be enough to rebuild it with the opener gone, because reload, back, and a plugin returning all rebuild from the document alone.

Where each thing is decided

ConcernOwnerNote
Leaf type, verbs, history entriesnavigationRow, pin, float, tabs, focus. Open-to-the-right skips pinned panes. Equalize sizes only the unpinned row.
View kinds and their rendererseach plugin, on an app.view slotClaimed by a string kind. Sits next to the content provider that already draws routes.
Pinned edge, floats, pills, the phone sheetlayoutAll presentation of one pane type. Pinned width and sidebar stay in preferences.
Chat viewchatContributes the chat kind. Its open verb is "pinned right, as a tab if one is there". Kolu and Odu keep hanging faces on it.
Placeholder for a missing kindnavigationBuilt first. It is what makes the document safe under plugin churn.
Last-layout on the hostvaultOne file per served directory. Follows the vault, not the browser origin.
The open menunavigationOne menu over every Link and follow site: here, right, tab, pinned, float.

Why the host and not localStorage for the last layout

DeepSeek Harness kept user preferences in localStorage, lost them when the server came up on another port, and moved them to a host settings document with a postmortem. The same applies here: the layout belongs to the vault, and the vault is served from the host. Per-tab live state stays in the tab, in history entries, which is the only place that is naturally per tab.

Sequence

  1. On top of PR 557: the leaf becomes a pane of tabs of views, plugins claim kinds, the placeholder for a missing kind. The URL codec still in place, now spelling the document. No user-visible change.
  2. The codec moves from URL text to history entries plus the host file. Address bar pins to /. The thirteen features that assert on the address bar assert on the document instead.
  3. Pins, floats, and tabs draw. The dock seat and the minimized pill are deleted from chat and layout. Chat contributes its view kind and its open verbs become pane opens. The open menu lands on Link.

From DeepSeek Harness: the (kind, params, title) tab record, tabs in a pane, a float as a pane with a different host, fractional sizes, a first-class missing-renderer state, reseeding an emptied root, and the durability-by-ownership rule. Not taken: their fixed shell with chat as the immovable center, their memory-only layout, and their operation log with inverses. A snapshot per history entry is enough here.