Skip to content

feat(shell): tabbed central panel with pinned home for embedding hosts#60

Merged
samuelstroschein merged 28 commits into
mainfrom
central-panel-embedding
Jul 21, 2026
Merged

feat(shell): tabbed central panel with pinned home for embedding hosts#60
samuelstroschein merged 28 commits into
mainfrom
central-panel-embedding

Conversation

@samuelstroschein

Copy link
Copy Markdown
Member

What

An opt-in embedding API for hosts that need browser-style tabs in the central panel (LixRay's home flow), while keeping the current single-document slot as the default (FlashType is unaffected — no changes needed there).

createAtelier({
  lix,
  extensions: [homeExtension, dirExtension],
  centralPanel: {
    tabs: true,                          // central tab strip + multiple content views
    home: { extensionId: "lixray_home" } // pinned, non-closable first tab
  },
});

Full write-up in docs/embedding.md.

The tab model (browser-like)

  1. The configured home is pinned first, never closes, never navigates; it compacts to its icon when inactive.
  2. documents.open(path) activates an existing tab for that path, otherwise navigates the active content tab in place (label follows the location). If home is active, a content tab is appended.
  3. documents.open(path, { newTab: true }) (hosts map ⌘-click) and the tab-strip + append new tabs.
  4. Closing a tab activates its neighbor; the last content tab closes back to home.

API surface

  • AtelierOptions.centralPanel (tabs, home)
  • AtelierDocumentOpenOptions.newTab
  • instance.views.open(extensionId, { state, instanceKey, newTab, panel }) — places host extension views (e.g. folder views) as central content tabs; also on AtelierExtensionRuntime.views
  • ExtensionManifest.placement / .hidden — declare central-capable, menu-hidden views
  • central_view_activated event — fired for every active-central-view change so routing hosts can mirror the active tab into the URL

Compatibility

All additive. Without centralPanel the shell behaves exactly as before (verified by the existing 838-test suite); new behavior is covered by src/shell/central-tabs.test.tsx.

🤖 Generated with Claude Code

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 20, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
atelier-preview 5de946d Commit Preview URL

Branch Preview URL
Jul 21 2026, 09:34 PM

samuelstroschein and others added 2 commits July 20, 2026 13:27
Adds an opt-in centralPanel option to createAtelier:
- centralPanel.tabs renders the central tab strip and keeps multiple
  content views open (browser-style: open navigates the active tab in
  place, newTab appends, close lands on the neighbor)
- centralPanel.home pins a host extension as the permanent first tab
  (never closable/draggable; compacts to its icon when inactive)
- documents.open gains a newTab option; new instance.views.open() places
  host extension views (e.g. folder views) as central content tabs
- extension manifests gain placement/hidden so hosts can declare
  central-capable, menu-hidden views
- central_view_activated event lets routing hosts mirror the active
  central view into the URL

Defaults are unchanged: without centralPanel the central island remains
the single-document slot with no tab strip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review-driven hardening of the central-tabs embedding API:

- centralPanel is now a mode-discriminated union ({ mode: "document" } |
  { mode: "tabs", home? }) so a home cannot be configured without tabs
- the tab-model rules (canHost/normalize/place/closeFallback) move into a
  CentralSlotBehavior strategy object with one implementation per mode
  (src/shell/central-slot-behavior.ts), replacing scattered boolean
  branches; the behavior parameter is required so no call site can
  silently fall back to document-slot semantics
- views.open: instanceKey renamed to instanceId (it is the id reported
  back on views and events); "central-home" is rejected for non-home
  extensions; completion now waits for the view to become active
- manifest placement is enforced everywhere: add-view menus, drag&drop,
  and programmatic side-panel opens
- a pinned home forces the Files view into the sidebar instead of
  dropping it under the default landing mode
- document_closed now reports the tab that actually activates (neighbor,
  or null for the home) instead of the last view
- central_view_activated fires in both modes and resets cleanly when the
  central panel empties; reopening an existing tab merges state instead
  of resetting it; extension_opened no longer fires for no-op resyncs
- isPinned is validated on persisted state

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@samuelstroschein
samuelstroschein force-pushed the central-panel-embedding branch from bc7926b to 2f892cd Compare July 20, 2026 17:40
@samuelstroschein

Copy link
Copy Markdown
Member Author

Rebased onto main (post checkpoint-removal) and applied the review round:

  • centralPanel is now a mode-discriminated union: { mode: "document" } | { mode: "tabs", home? } — a pinned home can no longer be configured without tabs.
  • The tab-model rules moved into a CentralSlotBehavior strategy object (src/shell/central-slot-behavior.ts) with one implementation per mode, replacing the boolean-branching policy threading; the behavior parameter is required at every call site.
  • views.open instanceKeyinstanceId (same value the host gets back on central_view_activated), reserved-id guard for central-home, and completion now waits for activation.
  • Fixes from adversarial review: Files view no longer vanishes under landing mode with a pinned home (forced to sidebar); document_closed.nextFilePath reports the tab that actually activates; placement is enforced for side panels (menus, drag&drop, programmatic opens); reopening an existing tab merges state instead of resetting it; central_view_activated fires in both modes; extension_opened no longer fires on no-op resyncs; isPinned is validated on persisted state.

838 tests green, lint clean.

🤖 Generated with Claude Code

samuelstroschein and others added 19 commits July 20, 2026 18:19
Two additions to the slots family, shaped by current host requirements:
- navbarCenter replaces the built-in top-bar title (hosts render e.g. a
  workspace breadcrumb; the read-only chip is preserved)
- centralTabStrip is a headless render-prop for the central tab strip:
  Atelier keeps the tab rules (pinned home, selection, closing, new-tab)
  and exposes them as AtelierTabStripContext; the host owns the chips.
  Custom strips forgo the built-in drag-reorder.

The built-in strip remains the default; hosts that pass neither slot are
unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A custom centralTabStrip now renders outside the island's bordered
container, on the app background — the host's chips float over the
canvas and the island starts below them. The built-in strip is
unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The built-in tab row now renders outside the island's bordered container
for every panel — chips sit on the app background with the island below,
matching the islands design ('its own chip group over its own island').
The row loses its inside-chrome divider and edge padding so chips align
flush with the island edge. Applies to side panels and the central strip
alike; custom central strips already floated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The visible view's chip is always clearly selected — white island-card
look (bg-panel + border) in both active and keyboard-focused states —
matching the floating-strip design.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The toolbar aligns to the content column (same centering as the
ProseMirror body, block-select text flush with the H1) instead of
rendering as a full-width bar with its own border and background.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The add-view trigger stays visible with a muted 'No views available'
item instead of vanishing (e.g. after the bundled History removal left
side panels with nothing addable).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Side-panel chips now match the central spec (12.5px/500 idle, 600 active,
12px padding) and the ✕/+ affordances share the same sizes. The add-view
trigger always renders, disabled with a 'No views available' item when
the registry has nothing placeable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closing the last chip of a side panel collapses the island instead of
removing the view — the view survives, so reopening the panel restores
it (and Files canonicalization no longer silently undoes the close).
Also resolves the active-chip border: border-transparent in the base
classes fought the state border color at equal specificity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Symmetric vertical centering (the pt offset crowded the open pill
against the island edge), the open block-select pill uses the quiet
hover tone, and the whole bar rests at 70% opacity until hovered,
focused, or holding an open menu.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New semantic tokens from the design review: canvas-level hover
(bg-hover-canvas), a quaternary text/icon tier for metadata and quiet
affordances, bg-active for tonal chips, and the comp's secondary-action
border/shadow recipe. Applied to side-panel chips, ✕/+ affordances,
top-bar toggles, and the formatting bar's icon tones.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The comp tints the trigger only on hover; the open state now reads
through the rotated chevron and the popup, not a filled pill.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
min-w-0 on the host-strip wrapper so many tabs shrink/scroll inside
the panel instead of widening it past the viewport.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The cursor is usually still over the trigger right after opening, so
the hover tint read as a stuck pill.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The trigger's -ml-2.25 alignment margin pulled it left of its
overflow-x-auto scroll container's content edge, which clips — the
hover/open pill rendered with its left side sliced flat. The margin
moves to the scroll container so alignment is preserved without
putting a child outside the scrollport.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Back to the text/csv view pattern: an edge-to-edge row at panel-header
height with a hairline bottom border, controls left, copy right — no
content-column centering, no negative-margin alignment, no rest-state
fade. The floating-bar experiments kept producing clipping and
stuck-pill artifacts; the plain header row avoids the whole class.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A | B | + with A active produced A | C | B; new tabs now join at the
end regardless of which tab is active, browser-style.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Removes the document/tabs mode union: the central panel is always
browser-style tabs with a pinned home. Hosts that configure no home
get the Files view as the pinned home tab — same rules, zero config;
a custom home banishes Files to the sidebar.

- centralPanel options lose the mode discriminant (breaking).
- DOCUMENT_SLOT_BEHAVIOR and every tabs/document branch deleted;
  createCentralSlotBehavior is the single strategy.
- documents.startNew reveals a hidden Files home and starts the draft
  there instead of silently creating an untitled file.
- Panel interactions activate the view they happen in, not whatever
  was active (fixes programmatic focus racing tab reveals).
- Compact pinned tabs gain an accessible name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The active chip's permanent selection ring is gone — white card +
  border is the active treatment; keyboard focus keeps focus-visible.
- Central document tabs show their file-type glyph, browser-favicon
  style, instead of the generic extension icon.
- + opens the pinned home view (a view opener, read-safe) instead of
  instantly creating a markdown file; the headless context.newTab
  gets the same semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
samuelstroschein and others added 5 commits July 21, 2026 15:25
The top-bar toggles edge-aligned their icons inside square buttons,
so the hover pill looked off-center. And the central tab strip loses
its + entirely — new tabs come only from explicit intent (⌘-click,
newly created documents); the headless context drops newTab.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Refines the single tabs primitive per host feedback: homeless hosts
(FlashType) get NO pinned center tab — the Files view lives in the
left sidebar (open by default on fresh state), documents open as
central tabs, and closing the last tab shows the empty state. A
configured home (lixray) behaves as before. filesViewMode is now
fully derived; stored panel sizes always win over defaults.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds AtelierOptions.filesView so hosts can feed un-imported "watched"
entries into the bundled Files view and lazily import them on first
interaction:

- filesView.watchEntries subscribes with the currently expanded
  directories (root always included) and is resubscribed whenever the
  expanded set changes; pushed entries merge into the tree as
  source: "watched" with lix rows winning path collisions and missing
  ancestor directories synthesized.
- filesView.resolveFileForInteraction imports a watched path to a
  canonical lix file before open and rename; resolving null is a
  graceful no-op and a newer interaction supersedes an older resolve.
- Watched rows keep the existing non-destructive rendering contract:
  no delete, no drag-and-drop, watched directories stay read-only.
- Hosts without the option see zero behavior change.

This unblocks FlashType deleting its ~5,400-line files-extension fork
that exists only to surface watcher-backed disk files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adding Files from any side panel routes to its canonical left-sidebar
home instead of being silently stripped by canonicalization. Closing a
side chip removes the view — the last one leaves the panel's empty
state with its add-view affordance; only the ensured left Files view
(which removal would resurrect) collapses the island instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The right panel's add-view menu offered Files, but canonicalization
pins it to the left sidebar — the item was a silent no-op (or a
surprising left-sidebar reveal). JSON manifests learn the placement
field, react definitions propagate it, and the bundled Files manifest
declares ["left"], so only menus that can honor it list it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
samuelstroschein and others added 2 commits July 21, 2026 17:31
The continuous canonicalization that resurrected Files made its ✕ a
no-op whenever it shared a panel. Files now seeds once into the fresh
left-sidebar state and behaves like any view: chips remove it, the
left + menu offers it back, and removing a panel's last view also
closes the island. No resurrection, no special cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With the resurrection logic gone there is nothing pinning Files to the
left — both side + menus offer it, and adding it to the right panel
opens a second tree there. The files→left redirect is removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@samuelstroschein
samuelstroschein merged commit 29e5e4a into main Jul 21, 2026
1 check passed
samuelstroschein added a commit to opral/flashtype that referenced this pull request Jul 21, 2026
* feat!: adopt atelier's single central-tabs primitive

Bumps submodule/atelier to 4396919 (central-panel-embedding): the
central panel is now always browser-style tabs with a pinned home.
FlashType configures no custom home, so the Files view becomes the
pinned home tab — documents open as content tabs beside it and closing
the last content tab lands back on Files.

- drop filesViewMode: "sidebar" (derived by atelier now; a no-op)
- closeFileViews closes by path via the new documents.close(path) so
  deleting a file also closes its background tab (the tree lives in the
  pinned home tab, so the doc view is rarely active during deletion)
- drop the revisions wiring deleted upstream by atelier#59 (checkpoint
  removal): history view props, checkpointBranchId, atelier_history
  builtin id; local types keep the optional diff-viewer props compilable
- e2e: ensureFilesViewOpenInLeftPanel -> ensureFilesHomeTabActive; the
  single-central-document invariant becomes "pinned Files home first,
  one active document content tab beside it"; tree interactions
  re-activate the home tab; the central empty state is gone (landing on
  the Files home instead); the two checkpoint-diff tests are skipped
  with pointers to atelier#59
- unit mocks: documents.close added, revisions removed, closeFileViews
  assertions carry filePath
- pnpm-lock: additive update for new atelier deps

BLOCKED on opral/atelier#60 — the submodule pointer targets the
central-panel-embedding branch, so the submodule-branch validation in CI
fails until it merges; bump to the merge commit afterwards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat!: remove the checkpoint feature, mirroring atelier#59

Atelier removed its checkpoint implementation upstream (opral/atelier#59,
pulled in by the central-tabs submodule bump); FlashType mirrors that
removal instead of keeping a dormant, half-working feature.

Deleted:
- src/extensions/history/ — the checkpoint UI (list, create, rename,
  switch, delete) and its registration
- electron/checkpoint-name.mjs (+test), the
  terminal:generateCheckpointName IPC handler, preload method, and
  DesktopGenerateCheckpointName* types
- src/shell/checkpoint-diff.ts (+test) and
  src/extension-runtime/checkpoint-diff.ts
- e2e/branch-switcher.spec.ts — all four tests exercised checkpoint
  UI or checkpoint diffs (incl. the two previously skipped)

Stripped checkpoint branches from the Files view (checkpoint-diff
virtual tree, checkpointBranchId/checkpointDiff context, the
"checkpoint-diff" entry source), the file tree (reviewStatuses map,
"recreated" status styling), the legacy MarkdownView/CsvView
(checkpoint-diff rendering; commit-snapshot/diff modes stay), the top
bar (isReviewingCheckpoint badge), and editor-revision-state
(checkpoint state keys).

Kept: shared lix branch plumbing (createBranch/switchBranch/
syncDiskToLix, the lix_workspace_branch_id active-branch key) and the
agent-review machinery — separate features that read them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: bump atelier to central-panel-embedding tip

Picks up the built-in strip UX parity (file-type glyphs, no active
ring) and the central + removal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat!: adopt the final central-panel shape — Files stays in the sidebar

Bumps submodule/atelier to c8c9a58 (central-panel-embedding tip):
pinned home only when configured. FlashType passes no centralPanel.home,
so the Files view lives in the left sidebar (open by default on fresh
state), the central panel is tabs of documents only, and closing the
last tab shows the central empty state again.

Restores the e2e specs to their origin/main shape — the sidebar-Files
assumptions (tree visible beside documents, "Start writing" empty
state, in-place tab navigation) hold again under the tab model — minus
the deleted checkpoint helper. The documents.close(path) deletion fix
stays.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor!: replace the files-extension fork with atelier's bundled Files view

Bumps submodule/atelier to 207d18e, which adds the host watched-files
data source (createAtelier filesView options), and deletes FlashType's
~5,450-line files-extension fork — it existed to surface watcher-backed
disk files for transient workspaces, which the bundled view now covers.

- src/lib/atelier-files-view.ts relocates the transient-workspace
  machinery: watchEntries bridges setEphemeralWatchedDirectories
  (keyed to the tree's expanded directories) plus the
  onEphemeralWatchedFileTreeChanged subscription, and
  resolveFileForInteraction imports a watched path into lix before
  open/rename. Only ephemeral workspaces pass filesView.
- Review overlays and deletion-closes-tab are native to the bundled
  view (it computes pending review paths itself and closes by path),
  so no host wiring remains.
- Deletes src/extensions/files/*, the fork-only ExtensionContext/
  WorkspaceContext/PanelSide types, and the dead src/queries.ts.
- e2e: the bundled tree auto-reveals the active document's directory,
  so the grouped-transient-workspace test no longer clicks it (the
  click would collapse it); all other selectors held.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: bump atelier — side-panel add/close semantics fix

Adding Files from a side panel focuses the left sidebar; closing the
last removable side view leaves the empty state instead of collapsing
the island.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: bump atelier — Files left-only placement

The right panel's add-view menu no longer offers Files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: bump atelier — Files is a normal, removable view

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: bump atelier — Files placeable in both side panels

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: point submodule/atelier at main

atelier#60 merged; the pointer moves to the squash commit on main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant