Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,11 @@ A sandboxed ES-module card rendered in an iframe within Home. Authored by the ag
_Avoid_: Card (ambiguous — the UI has many cards), tile (as the concept name — tile is a size class)

**Sidebar**:
The webview in the VS Code activity bar container, showing project navigation and system status. Contains action buttons (open chat, create project), a session-aware unified project tree (Research Projects with lifecycle metadata expanding into file trees; Dev Projects as plain expandable folders), and a collapsible fleet section (deferred). The sidebar is navigation chrome — it follows the active session's project binding but never drives session switching. Single-clicking a file opens it as a tab in Preview (the multi-document file workspace in the side panel); double-clicking opens a native VS Code editor tab.
The webview in the VS Code activity bar container, showing project navigation and system status. Contains action buttons (open chat, create project), a session-aware unified project tree (Research Projects with lifecycle metadata expanding into file trees; Dev Projects as plain expandable folders), and a collapsible fleet section (deferred). The sidebar is navigation chrome — it follows the active session's project binding but never drives session switching. Single-clicking a file opens it in Preview (the multi-document file workspace in the side panel); double-clicking opens a native VS Code editor tab.
_Avoid_: Explorer (VS Code's native file explorer is separate), Panel (the in-app dismissible drawer is a different concept)

**Preview**:
The multi-document file workspace in the side panel. Holds zero or more files as inner tabs, each rendering its content (markdown rendered with a toggle to edit; text/code files in a CodeMirror editor; images and PDFs inline). Files arrive via Sidebar single-click or a Chat file pill and accumulate as tabs — each closeable and drag-reorderable; opening an already-open file focuses its existing tab rather than duplicating it. A breadcrumb bar under each pane's tab strip shows the active file's project-relative path with interactive sibling navigation. Supports recursive split panes via edge-drop: dragging a tab toward a pane's edge divides the view, and each resulting pane keeps its own tab bar, breadcrumb, zoom, and preview/edit toggle. Empty panes auto-collapse; a minimum pane dimension is enforced so splits can't shrink below a usable size. Opens and activates automatically when the first file is selected; shows a placeholder when no file is open. For committed editing, double-click the Sidebar entry to open a native VS Code tab.
The multi-document file workspace in the side panel. Holds up to eight files as unique inner tabs, each rendering its content (markdown rendered with a toggle to edit; text/code files in a CodeMirror editor; images and PDFs inline). Files arrive via Sidebar single-click or a Chat file pill; opening an already-open file focuses its existing tab rather than duplicating it. Each open file retains its current view while it remains open, including through inner-tab changes, pane moves, and outer-tab changes. A breadcrumb bar under each pane's tab strip provides project-relative sibling navigation. Recursive split panes are created by edge-drop; each pane has its own tab bar, breadcrumb, zoom, and preview/edit toggle. Empty panes auto-collapse, and a minimum pane dimension keeps the layout usable. Preview opens and activates when the first file is selected and shows a placeholder when no file is open. For committed editing, double-click the Sidebar entry to open a native VS Code tab.
_Avoid_: Editor (Preview is a multi-document viewer, not a primary editor — committed editing belongs in a native VS Code tab), File browser (the Sidebar is still the primary project-wide file tree; the breadcrumb is a contextual sibling-navigation aid scoped to the open file, not a second tree)

### Orthogonal axes
Expand Down
47 changes: 39 additions & 8 deletions docs/adr/0013-preview-workspace-split-panes.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,48 @@
# Preview becomes a multi-file split-pane workspace; single-file companion model retired
# Preview becomes a renderer-preserving multi-file split-pane workspace

Status: proposed (2026-09-09)
Status: amended (2026-09-09)

Tracking: harmoniqs/amicode#940 · Glossary update: `CONTEXT.md` (Preview, Sidebar)

Preview — previously a single-file companion viewer that replaced its content on every sidebar click (#931, landed the same day) — becomes a multi-document workspace: files accumulate as closeable inner tabs, a breadcrumb bar under each pane's tab strip provides project-relative path navigation with sibling dropdowns, and tabs can be dragged to edge drop-zones to create recursive split panes, each with independent zoom and preview/edit controls. The outer side-panel tab bar (Home, Files Changed, Context, Pulse Inspector, Preview) is unaffected — Preview remains one tab there; the new tab-and-pane machinery is entirely contained within it.
## Decision

**Why:** The single-file companion model (#931) optimized for a different problem — one file at a time, driven externally, no navigation chrome competing with the Sidebar. In practice, researchers comparing two files (a script and its output, a spec and its implementation) lost their place every time a second file replaced the first. The companion model traded away exactly the capability multi-file work needs. Sidebar remains the project-wide file tree; the breadcrumb is a narrower, contextual navigation aid scoped to the currently open file's siblings, not a second file browser.
Preview remains one outer side-panel tab while becoming a multi-document workspace. Files opened by Sidebar single-click or Chat file pills accumulate as closeable inner tabs. A breadcrumb provides contextual sibling navigation, and dragging an inner tab reorders it, transfers it between panes, or creates a recursive split at a pane edge. The Sidebar remains the project-wide file tree; Preview is not a second file browser.

**Conditions of acceptance:** Files opened via Sidebar single-click or a Chat file pill accumulate as inner tabs in the focused pane rather than replacing the current file; re-opening an already-open file (from any entry point, including breadcrumb sibling navigation) focuses its existing tab instead of duplicating it — enforced workspace-wide, not just within one pane. Tabs close via an explicit control and are drag-reorderable. A breadcrumb bar shows the active file's project-relative path as clickable segments, each expanding to a sibling dropdown. Dragging a tab to a pane's edge splits that pane (horizontal or vertical); splits are recursive, subject to a 150px minimum pane dimension that refuses drops which would violate it and clamps resizes at the same floor. Each pane carries independent zoom and preview/edit-toggle state. The entire workspace — every pane, every tab, all per-tab state including unsaved edits — survives switching to another outer side-panel tab and back. Double-click-to-open-in-VS-Code and the outer tab bar are unchanged.
The workspace is a thin layout shell over the existing Preview renderers. It owns only tab-to-pane assignment, pane geometry, focused-pane state, pane zoom, dirty indicators, and the eight-tab resource limit. Each open file owns one persistent baseline renderer instance. Switching tabs, moving a tab between panes, or switching away from the outer Preview tab preserves that renderer instance rather than reconstructing its document, CodeMirror state, PDF layout, scroll position, or focus.

**Accepted costs:** The workspace reintroduces navigation surface (the breadcrumb) that #931 deliberately removed — a future reader of #931's history will see this as a partial reversal, not a straight line; the ADR exists so that reversal reads as deliberate. Per-pane state (zoom, mode, scroll, unsaved content) roughly doubles or triples the state `SessionPreviewTab` used to hold for a single file, now split across `PreviewWorkspace`/`PreviewPane`. The workspace store must be lifted into the layout context (above `session-side-panel.tsx`'s `<Show when={activeTab() === SESSION_PREVIEW_TAB}>` gate) rather than owned locally, because SolidJS disposes a `<Show>` branch's reactive scope on every toggle — the single-file `SessionPreviewTab` already loses its local state this way today, and a multi-file, multi-pane workspace makes that loss far more costly if inherited unfixed. At the 330px minimum panel width (`WORK_COLUMN_WIDTH_MIN`), only one horizontal split is practically usable before panes drop below a comfortable reading width — accepted as a constraint of the side-panel form factor. Splitting, cross-pane tab transfer, and pane resizing are pointer/drag-only in this version; there is no keyboard-accessible path for any of the three.
The outer Preview content remains mounted while another outer tab is selected. It is hidden and inert rather than disposed. The pane canvas may provide CSS-only overflow when recursive minimum geometry exceeds the Work Column, but it does not observe, store, restore, or otherwise control scroll position.

**Considered:** (A) Extend `SessionPreviewTab` in place with tab/breadcrumb/split logic (rejected: the component is 119 lines today, but tab-bar, pane-tree, and breadcrumb logic are three distinct responsibilities that would tangle together as each grows independently — not a current-size problem but a projected-shape one); (B) **new `PreviewWorkspace`/`PreviewPane`/`PreviewBreadcrumb` component tree** (chosen: clean separation, each component independently testable, `SessionPreviewTab` shrinks to a thin shell); (C) a generic `SplitPaneLayout` primitive built first and specialized for Preview (rejected for now: speculative reuse — no other surface has asked for splitting yet — and the abstraction would be guessed at rather than derived from a second real use; extracting it from B later is straightforward if that need materializes).
## Why

**Flip condition:** If a second surface (e.g. Files Changed) independently needs split-pane viewing, extract the pane-tree logic from `PreviewWorkspace` into the generic primitive considered as option C, rather than duplicating the tree/drag machinery. If the side panel's minimum width increases substantially in a future layout pass, revisit the 150px pane minimum and how many practical splits it should allow.
The single-file companion model (#931) optimized for externally driven, one-file-at-a-time reading. Researchers comparing a script and its output, or a spec and implementation, lost their place whenever the next selection replaced the first. Multi-file work needs retained documents and panes.

The original form of this ADR chose a lifted workspace state store plus a renderer-state hydration adapter. That duplicated ownership already held by `PreviewFileView`, CodeMirror, and the PDF renderer. It introduced a canvas-scroll feedback loop and renderer lifecycle races that broke ordinary scrolling and Markdown editing. Preserving the working renderer instances makes the layout shell smaller and gives each layer one owner.

## Conditions Of Acceptance

- A path has at most one live inner tab across the workspace; re-opening it focuses its existing pane and tab.
- At most eight renderer instances are open. The ninth open requires an explicit close; clean tabs are never silently evicted.
- A live renderer remains intact through inner-tab changes, pane transfer or split, and outer-tab changes. Draft text, local scroll, selection, and loaded content stay with that renderer.
- Dirty state is tab chrome only. Closing a dirty tab offers save, discard, or cancel; the layout shell never stores draft text.
- Drag is the primary path for reorder, transfer, and edge split. A Preview-scoped nested-DnD spike must prove non-interference with outer tabs before that interaction ships.
- Each leaf has a 150px minimum dimension. When the tree exceeds the Work Column, a CSS-only canvas scrolls without persistence or restoration logic.
- Pane zoom and the existing preview/edit controls are pane-scoped. Breadcrumb navigation is added only after tab, renderer, and pane interaction gates pass.
- Double-click-to-open-in-VS-Code and the outer side-panel tab bar remain unchanged.

## Rejected Alternatives

1. **Renderer-state hydration adapter** -- rejected. Capturing and restoring drafts, scroll, and focus creates a second owner for state the renderer already owns.
2. **One active renderer per pane** -- rejected. Inactive tabs lose live editing and view state when their renderer is replaced.
3. **Generic split-pane primitive first** -- deferred. No second surface currently establishes a real reuse boundary.

## Accepted Costs

Keeping renderer instances mounted consumes more memory and background resources than hydration. The workspace therefore caps live tabs at eight and requires explicit closure. Keeping Preview mounted while inactive also retains its renderer resources, but avoids destructive remounting during ordinary navigation.

## Validation

Browser interaction tests are required before a Dev Host build is vendored. They must prove stable Markdown focus after grammar loading, ordinary Preview scrolling, Cmd+S behavior, persistence across outer-tab switches, renderer identity across drag relocation, dirty-close confirmation, and non-interference between Preview and outer-tab DnD. Unit and type tests support but do not replace these gates.

## Flip Condition

If a second surface needs the same renderer-preserving pane behavior, extract only the layout shell after two real uses establish its interface. If eight retained renderers prove insufficient in measured use, revisit the cap with evidence rather than introducing silent eviction or a state adapter.
16 changes: 8 additions & 8 deletions packages/app-bundle/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -968,10 +968,10 @@
"packages/ui/package.json": "b1d168d0371e9094faae1107fc6c00be197f09bc69daa2247a3890d607f4b629",
"packages/app/public/amico.svg": "a14b9d543d895bcdf0758f7b9ef5908ee0acaac794446494af059b159247db8f",
"packages/app/public/oc-theme-preload.js": "27227e802b3494e7c545da903e679efdb30ccc754cd4eb5cdf08005a40d560b6",
"packages/app/src/app.tsx": "a72e7cef35d5de80980fbb1fc26c14d8551d1677821e72798c624842927b55fd",
"packages/app/src/design-polish.css": "42cc6efaefe9a71dedd12fcb0bf2549453d9a46097cb025cf087064b29d3ccce",
"packages/app/src/app.tsx": "91ea1817db2f7e21caae642f3c2e276b4835fcecf499699cb787e7a5521ae20b",
"packages/app/src/design-polish.css": "ee180766e073ef9bf820ead1d2fb02fe92a2e9f73e987d5f161ba2045871c2ef",
"packages/app/src/entry.tsx": "f35e1017f4c9d478d254b2a38043e5750064b6bef25169c3e07ae9f72ff1049c",
"packages/app/src/index.css": "08179e06ce2d419a2d98acc96025f91c7709062ea9f3ad245e88dc35e75ff9f7",
"packages/app/src/index.css": "2c11df3dcebb381358e06626094b5dda3c35688ad7940d643b2cbb6cff1ea9e9",
"packages/app/src/theme-preload.test.ts": "d9e4e96dd39a3491493637682611bedf6ddf4b6e4dbdddcb60bf006211b137a1",
"packages/core/src/config.ts": "0b6b81bea6a3a09285daa4bd70757e6bbe93cf2508121e1062b71bb24654d692",
"packages/core/src/location-mutation.ts": "5fa852c48e98cec513346f848b422c2d5d6d4b753b39da851c73be073a03511a",
Expand Down Expand Up @@ -1074,7 +1074,7 @@
"packages/app/src/context/language.tsx": "cb545e04c128bf981b2b72ac407f9220cf9e80337fe8c87482ffe85407e0cf9a",
"packages/app/src/context/layout-tabs.test.ts": "4d9fdbe963306f164b2f48eac3b6a28c5a703c1758bf14b2cc4796720d8fa72c",
"packages/app/src/context/layout-tabs.ts": "741506ce165f68cdb0b8f2931a2dad3e26c05880f9286c3daf04ec979bac21c5",
"packages/app/src/context/layout.tsx": "cc9dffc8ddacd825038610a9fd4783a41852853f1d2fe1bb65b4e8348db1e78d",
"packages/app/src/context/layout.tsx": "7767c8b5f64efd63390cbe99128686bec2f1da7e8f2b21530df4c3fd67d171bb",
"packages/app/src/context/local-agent.test.ts": "a5a9d60bb4401d409218cc247c1cc06ede8b7878ddf01dc6080328f54eb9cade",
"packages/app/src/context/local-agent.ts": "0aab67e695dc3bb45a733ac0df80a0a5e14cfe29b2375b6dc3cd07fbccea33e2",
"packages/app/src/context/local.tsx": "3ab8b9fc2db082df4ba485373679f00a95d0ffe3c691a0afee1dc53db7eabd9e",
Expand Down Expand Up @@ -1425,12 +1425,12 @@
"packages/app/src/components/session/index.ts": "21473290d4a1a3d0670fd878372ddf21ff22d4a0b30d1aef6c81ea906c1488b4",
"packages/app/src/components/session/panel-menu.tsx": "42f323046b7375ae158023a51506038eb15f2e1545c407f45acd47446a8ef3e0",
"packages/app/src/components/session/pdf-canvas-view.tsx": "02032c64833583c9b0b62ed0ec7dc9770e7fa368b4659db45730118686ca6a27",
"packages/app/src/components/session/preview-file-view.tsx": "6e1949ed11bb3ea65023021aeec86875f2853bb84948d2147436c6f8be395da4",
"packages/app/src/components/session/preview-file-view.tsx": "1ec448960876937e3e740c960c5780f6244dca7f35a2462de699d9eb3dbda798",
"packages/app/src/components/session/session-chats-dropdown.test.ts": "2003d2a15781337ea6bff2c68e730cc5b6df38697f15d927ab26913936444db7",
"packages/app/src/components/session/session-context-tab.tsx": "227243b178b517f067d9ae0ae0eec3c559beeb6681828158b0600a17e98e7f81",
"packages/app/src/components/session/session-header.tsx": "a46591ed1097d0fdcff61fb0c5529396955e8857cbef748747c51e472d5564c5",
"packages/app/src/components/session/session-new-view.tsx": "9510a4f550a3f0d4791e98e8025666f09d70a60fb66f193e48ee61feddae5a57",
"packages/app/src/components/session/session-preview-tab.tsx": "4eb8127182f34aa9d141f5d9cb8eba330592ef0e2cf7c2cfe422e788f2fc67c5",
"packages/app/src/components/session/session-preview-tab.tsx": "3467aa44fa8db70d313e84ed0b31cc4d11616a53060dcf55bc6e5b68f5001c2f",
"packages/app/src/components/session/session-sortable-terminal-tab-v2.tsx": "08db0e378c3e07d243121f40c77e153bafe897e5e2ececd48a3e00786793032b",
"packages/app/src/components/session/use-context-warning.ts": "af7a6d0159a5541aa02ad4d08fd694af1cc4853fc1c0a70763bc264a634d1c53",
"packages/app/src/components/settings-v2/data-storage-controller.ts": "fa5d143cc101f3a3b9d5ad445edddc981e0d02783021d52dbfed6c8d8bf62498",
Expand Down Expand Up @@ -1481,8 +1481,8 @@
"packages/app/src/pages/session/helpers.ts": "8d0106a5ec3f01a666bd840e20b6bfb28d0e88b8c8c51fc1fdd7eaa33e9daafc",
"packages/app/src/pages/session/session-panel-width.test.ts": "9482afba7fbce254cbbd21ef885e9e9620b981616681635376f466478f611155",
"packages/app/src/pages/session/session-panel-width.ts": "02ee3a02ed78db2eac47c2528055cf78aef95f389641e929ddc56c17471dcbe2",
"packages/app/src/pages/session/session-side-panel-structure.test.ts": "c138fe905498c8326f459dccba61b146af6dfb12b78480303723b5a85046931a",
"packages/app/src/pages/session/session-side-panel.tsx": "4c93427154a87f78ac40b2d9b772cade515e5f4b31468fffdc4f5c228a84a908",
"packages/app/src/pages/session/session-side-panel-structure.test.ts": "b2d1d007d9200d0ac4a0889e45c2dbe57a324a10cb26ae9ab8f420d3c890402b",
"packages/app/src/pages/session/session-side-panel.tsx": "120f756cce7139d72a610f9bddb3be861a9a830989ad2af3ef549b2cb5a70f68",
"packages/app/src/pages/session/terminal-panel-v2.tsx": "68dad9307f1d2abf3ff9248e451bd08005acf01ddc46b30a6d01b58f4a90dce0",
"packages/app/src/pages/session/use-amicode-commands.test.ts": "65671d054c404edd2b799b0f6591a9c055e6ecb454a674fa0bd9a7fa6f68e111",
"packages/app/src/pages/session/use-amicode-commands.tsx": "88bfdad9ae6dfd920b8e6a14ffebd2cf9e52b8f67bb9284cb358ad940643ca67",
Expand Down
Loading
Loading