import type { ComponentType, MutableRefObject } from 'react'; import type { FileSystemProvider } from '../filesystem/types'; import { TilingWorkspace } from './tiling/tiling'; import { ZenWorkspace } from './zen/zen'; import { WorkspaceActiveProvider } from './barSlot'; import type { TilingHandle } from '../actions'; export type WorkspaceKind = 'tiling' | 'zen'; /** * Intent from the shell (e.g. sidebar) asking the active workspace to open a * document. * * TODO(naming): `providerId` / `entryId` come from the FileSystemProvider * abstraction in `src/filesystem/types.ts`. As we add non-filesystem sources * (search results, recent items, deep links) these names start to lie. Options * we may want to workshop: `sourceId` + `documentId`, a single opaque `uri` * (e.g. `habitat-docs://`), or a tagged union per source kind. Leaving * the FS-flavoured names for now since the only source today *is* the * filesystem sidebar. */ export type OpenDocRequest = { /** * Identifies the source that owns the document. Today this matches both * `FileSystemProvider.id` and `DocumentProvider.id` (e.g. `"habitat-docs"`). */ providerId: string; /** * Identifies the specific document within that source. For * `habitat-docs` this is an AT-protocol URI; for future providers it could * be a file path, blob CID, etc. Opaque to the workspace. */ entryId: string; /** Human-readable label used for tab/title chrome. */ title: string; }; export type WorkspaceProps = { /** * Whether this workspace is the one currently shown. Inactive workspaces stay * mounted so their tile layout and tabs survive a switch, but they ignore * open intents and do not paint the bottom bar. */ active: boolean; /** * Stable per-vault key (the vault's provider id). Used as the persistence * namespace so each vault restores its own layout. */ instanceKey: string; /** * Monotonically bumped each time the shell issues a new open intent. * Workspaces detect "a new open arrived" by comparing against their * last-seen id, which avoids re-opening on unrelated re-renders. */ openRequestId: number; openRequest: OpenDocRequest | null; /** * Look up a FileSystemProvider by `OpenDocRequest.providerId`. Workspaces * open documents via `provider.documents` when present. */ resolveProvider: (providerId: string) => FileSystemProvider | undefined; /** * Mutable ref for TilingWorkspace to expose its split handle. * Only meaningful for the tiling workspace; other kinds ignore it. */ tilingHandleRef?: MutableRefObject; /** * Create a new file in the selected source and open it in the active tile. * Surfaced by a tile's empty-state "Open a new file" action. Absent when no * source is selected. */ onNewFile?: () => void; /** * Open a document (by provider + entry) in the active tile. Same channel the * sidebar uses; a workspace calls this to follow an internal link from within * a tile. Because the clicked tile is already active, the open lands in it. */ onOpenEntry?: (request: OpenDocRequest) => void; /** * Report which document this workspace is currently showing, or null for * none. Only the `active` workspace reports; inactive ones stay mounted and * stay quiet. * * This is the shell's ONLY source of truth for "the current document", and * the sidebar highlight is rendered from it. It is deliberately a report of * *state* rather than a notification fired by each navigation action: a new * way to reach a document (wikilinks, internal links, quick-open, tab * cycling, restoring a layout) cannot forget to announce itself, because * there is nothing to announce — the workspace simply keeps reporting what * it shows. */ onActiveDocumentChange?: (request: OpenDocRequest | null) => void; }; export type WorkspaceComponent = ComponentType; export type WorkspaceDescriptor = { kind: WorkspaceKind; label: string; description: string; Component: WorkspaceComponent; }; export const WORKSPACE_DESCRIPTORS: readonly WorkspaceDescriptor[] = [ { kind: 'tiling', label: 'Tiling', description: 'Split-pane editor with resizable tiles and split controls.', Component: TilingWorkspace, }, { kind: 'zen', label: 'Zen', description: 'Single editor focused on one document at a time.', Component: ZenWorkspace, }, ]; const BY_KIND = new Map(WORKSPACE_DESCRIPTORS.map((d) => [d.kind, d])); export const DEFAULT_WORKSPACE_KIND: WorkspaceKind = 'tiling'; export function getWorkspaceDescriptor( kind: WorkspaceKind, ): WorkspaceDescriptor { const d = BY_KIND.get(kind); if (!d) throw new Error(`Unknown workspace kind: ${kind}`); return d; } export function isWorkspaceKind(value: unknown): value is WorkspaceKind { return typeof value === 'string' && BY_KIND.has(value as WorkspaceKind); } type WorkspaceHostProps = { kind: WorkspaceKind; /** Provider ids of the mounted vaults, one workspace instance per vault. */ vaultIds: readonly string[]; /** The currently selected vault's provider id. */ selectedVaultId: string; openRequestId: number; openRequest: OpenDocRequest | null; resolveProvider: (providerId: string) => FileSystemProvider | undefined; tilingHandleRef?: MutableRefObject; onNewFile?: () => void; onOpenEntry?: (request: OpenDocRequest) => void; onActiveDocumentChange?: (request: OpenDocRequest | null) => void; }; /** * Mounts one workspace instance per (kind × vault) and shows only the one * matching the current kind and selected vault. Keeping every instance mounted * preserves each vault's tile layout and tabs (and its open documents), so * switching vaults — or modes — restores that vault's setup exactly. Inactive * instances are hidden, ignore open intents (`active=false`), and suppress * their bottom-bar portal. */ export function WorkspaceHost({ kind, vaultIds, selectedVaultId, openRequestId, openRequest, resolveProvider, tilingHandleRef, onNewFile, onOpenEntry, onActiveDocumentChange, }: WorkspaceHostProps) { // Always render at least one slot — even before any vault is mounted — so the // empty start page is visible. Guarantee exactly one active vault key. const vaultKeys = vaultIds.length > 0 ? vaultIds : ['']; const activeVaultKey = vaultKeys.includes(selectedVaultId) ? selectedVaultId : vaultKeys[0]; return ( <> {vaultKeys.flatMap((vaultKey) => WORKSPACE_DESCRIPTORS.map(({ kind: descriptorKind, Component }) => { const isActive = descriptorKind === kind && vaultKey === activeVaultKey; return (
); }), )} ); }