diff --git a/README.md b/README.md index 497dc0be..ba7193f4 100644 --- a/README.md +++ b/README.md @@ -1,181 +1,166 @@ -# statewalker-workbench +# statewalker-shell-react -Application shell and fragment platform for the statewalker ecosystem — backbone runtimes, the platform-capability vocabulary, and the canonical workbench substrate (logic + renderer fragments). +## What it is -## Packages +React renderers for the statewalker application shell. Each package here is +the view half of a logic package: the logic package (for example +`@statewalker/shell.core`) holds state, commands and slots with no React, and +the package here (`@statewalker/shell.view.react`) renders them. Together they +give an app a React mount, a dockview-based shell, a settings dialog, a file +explorer, viewers for images, Markdown, PDF and video, and the AI settings +tabs. All packages are published to npm under `@statewalker/*`. -Packages follow the **`@statewalker/${domain}.${aspect}.${modifier}`** convention -(all-dots; `aspect` ∈ `{core, view, browser, node, cloud, feature}`; a `view` leaf -— `*.view.react` / `*.view.shadcn` / `*.view.` — is the only place a UI tech -may be imported). See the `workbench-package-naming` capability spec. +## The shape: fifteen view packages, no apps -### Backbone (independent runtime) - -| Package | Description | -| --- | --- | -| [`@statewalker/backbone.core`](packages/backbone.core) | Backbone primitives: resolver, topo sort, activation, manifest types, vendored logger. | -| [`@statewalker/backbone.node`](packages/backbone.node) | Node bootstrap: resolves `AppManifest` against the filesystem and activates modules. | -| [`@statewalker/backbone.browser`](packages/backbone.browser) | Browser runtime: fragment loader + web-side module activation. | - -### Platform capabilities - -| Package | Description | -| --- | --- | -| [`@statewalker/platform.core`](packages/platform.core) | Type-only command vocabulary: pickers, downloads, clipboard, preferences, URL state. | -| [`@statewalker/platform.browser`](packages/platform.browser) | Browser implementation of the platform commands. | -| [`@statewalker/platform.node`](packages/platform.node) | Node implementation of the platform commands — filesystem-backed durable preferences. | - -### Workspace foundation - -| Package | Description | -| --- | --- | -| [`@statewalker/workspace.core`](packages/workspace.core) | Workspace logic: the `Workspace` class, project/resource model, class-keyed adapters, the `files:*` filesystem commands (`WorkspaceFilesManager`), and the `workspace:change` command. | -| [`@statewalker/workspace.browser`](packages/workspace.browser) | Browser FS-Access lifecycle for the workspace: `WorkspaceShellAdapter` + `workspace:*` commands. | -| [`@statewalker/workspace.view.react`](packages/workspace.view.react) | Workspace renderer: `AppWorkspaceProvider`, `DirectoryPickerEmptyState`, `ReconnectBanner`, switch-workspace header item. | - -### UI substrate - -| Package | Description | -| --- | --- | -| [`@statewalker/ui.view.react`](packages/ui.view.react) | React mount, ``, `core:views` slot, substrate hooks (`useSlot`, `useKeyedSlot`, `useAdapterValue`, `useAdapter`), theme binding. | -| [`@statewalker/ui.view.shadcn`](packages/ui.view.shadcn) | shadcn/ui primitives + `cn()` helper for the workbench substrate. | - -### Shell (application frame) - -| Package | Description | -| --- | --- | -| [`@statewalker/shell.core`](packages/shell.core) | Frame logic: `dock:*` slot keys, `dock:show-panel`/`close-panel`/`focus-panel` commands, dock state. | -| [`@statewalker/shell.view.react`](packages/shell.view.react) | Frame UI: `dockview-react` host, `MainShell`, `ShellHeader`, `JsonPanel` (registers `MainShell` into `core:views`). | - -### Render engine (json-render) - -| Package | Description | -| --- | --- | -| [`@statewalker/render.core`](packages/render.core) | json-render engine state (opaque, no `@json-render` dep): `SpecStore` + `spec:create`/`spec:patch` + `restorePanelSpecsFromLayout`, plus the `json:catalogs` slot. | -| [`@statewalker/render.view.react`](packages/render.view.react) | The sole `@json-render/react` boundary: `` + `defineRegistry`/`schema` re-exports. | - -### Files & MIME viewers - -| Package | Description | -| --- | --- | -| [`@statewalker/mime.core`](packages/mime.core) | Mime-viewer dispatch: `files:visualize`/`files:open`, the `mime:renderers`/`mime-icons`/`editor-factories` slots, `MimeRenderer` + `pickMimeRenderer`. | -| [`@statewalker/mime.view.image`](packages/mime.view.image) | Image viewer renderer. | -| [`@statewalker/mime.view.markdown`](packages/mime.view.markdown) | Markdown viewer renderer. | -| [`@statewalker/mime.view.pdf`](packages/mime.view.pdf) | PDF viewer renderer. | -| [`@statewalker/mime.view.video`](packages/mime.view.video) | Video viewer renderer. | - -### Explorer +``` +packages/ + ui.view.react React mount, , core:views slot, workspace hooks, theme CSS + ui.view.shadcn shadcn/ui primitives and cn() + render.view.react ; re-exports from @json-render/react + shell.view.react dockview host, MainShell, header, the "json" dock panel, tab icons + workspace.view.react workspace label header item, switch-workspace button + settings.view.react settings dialog and settings button + explorer.view.react file-explorer panels (list, breadcrumbs, filter, drag and drop) + inline.view.react and the built-in inline components + mime.view.image image/* viewer + mime.view.markdown text/markdown viewer and a reusable component + mime.view.pdf application/pdf viewer (browser built-in PDF viewer) + mime.view.video video/* viewer + ai-config.view.react "Remote Models" settings tab + ai-local-models.view.react "Local Models" settings tab + webapp.view.react : iframe dock panel for a web app hosted from the workspace +``` -| Package | Description | +| Package | npm | | --- | --- | -| [`@statewalker/explorer.core`](packages/explorer.core) | File-explorer logic: navigation, search controller, tree-state, browser orchestration commands. | -| [`@statewalker/explorer.view.react`](packages/explorer.view.react) | File-explorer renderer: tree, list, drag-and-drop, context menu, breadcrumbs, search panel. | +| [`@statewalker/ui.view.react`](packages/ui.view.react) | [npm](https://www.npmjs.com/package/@statewalker/ui.view.react) | +| [`@statewalker/ui.view.shadcn`](packages/ui.view.shadcn) | [npm](https://www.npmjs.com/package/@statewalker/ui.view.shadcn) | +| [`@statewalker/render.view.react`](packages/render.view.react) | [npm](https://www.npmjs.com/package/@statewalker/render.view.react) | +| [`@statewalker/shell.view.react`](packages/shell.view.react) | [npm](https://www.npmjs.com/package/@statewalker/shell.view.react) | +| [`@statewalker/workspace.view.react`](packages/workspace.view.react) | [npm](https://www.npmjs.com/package/@statewalker/workspace.view.react) | +| [`@statewalker/settings.view.react`](packages/settings.view.react) | [npm](https://www.npmjs.com/package/@statewalker/settings.view.react) | +| [`@statewalker/explorer.view.react`](packages/explorer.view.react) | [npm](https://www.npmjs.com/package/@statewalker/explorer.view.react) | +| [`@statewalker/inline.view.react`](packages/inline.view.react) | [npm](https://www.npmjs.com/package/@statewalker/inline.view.react) | +| [`@statewalker/mime.view.image`](packages/mime.view.image) | [npm](https://www.npmjs.com/package/@statewalker/mime.view.image) | +| [`@statewalker/mime.view.markdown`](packages/mime.view.markdown) | [npm](https://www.npmjs.com/package/@statewalker/mime.view.markdown) | +| [`@statewalker/mime.view.pdf`](packages/mime.view.pdf) | [npm](https://www.npmjs.com/package/@statewalker/mime.view.pdf) | +| [`@statewalker/mime.view.video`](packages/mime.view.video) | [npm](https://www.npmjs.com/package/@statewalker/mime.view.video) | +| [`@statewalker/ai-config.view.react`](packages/ai-config.view.react) | [npm](https://www.npmjs.com/package/@statewalker/ai-config.view.react) | +| [`@statewalker/ai-local-models.view.react`](packages/ai-local-models.view.react) | [npm](https://www.npmjs.com/package/@statewalker/ai-local-models.view.react) | +| [`@statewalker/webapp.view.react`](packages/webapp.view.react) | [npm](https://www.npmjs.com/package/@statewalker/webapp.view.react) | + +Most packages share one source layout: -### Settings & inline content +``` +src/ + index.ts re-exports public/index.ts and the default init + fragment.ts re-exports the default init + public/ exported components, slots, catalogs, init + internal/ implementation and tests; not reachable through exports + styles.css Tailwind v4 @source globs (ui.view.react also defines the theme) +``` -| Package | Description | -| --- | --- | -| [`@statewalker/settings.core`](packages/settings.core) | Settings logic: `settings:*` slots and commands. | -| [`@statewalker/settings.view.react`](packages/settings.view.react) | Settings renderer: dialog + header-items button. | -| [`@statewalker/inline.core`](packages/inline.core) | Inline-content logic: `inline-content:components` descriptor slot + types. | -| [`@statewalker/inline.view.react`](packages/inline.view.react) | Inline-content renderer: `inline-content:renderers` slot, ``, built-ins. | +Each package exports `.` and `./fragment` from `dist/` (JS and `.d.ts`) and, +when it has a stylesheet, `./styles` from `src/styles.css`. +`render.view.react` exports only `.`. `webapp.view.react` has no `./styles` and +keeps its sources directly in `src/`. Packages ship both `dist/` and `src/`. -### AI +The packages use these `@statewalker` packages from npm: `shell.core`, +`workspace.core`, `workspace.browser`, `render.core`, `mime.core`, +`explorer.core`, `inline.core`, `settings.core`, `webapp.browser`, +`ai-config.core`, `ai-local-models.core`, `shared-baseclass`, +`shared-commands`, `shared-registry`, `shared-slots`, `webrun-files` and (in +tests) `webrun-files-mem`. -| Package | Description | -| --- | --- | -| [`@statewalker/ai-agent.core`](packages/ai-agent.core) | Agent runtime logic — sessions, tools, providers. React-free. | -| [`@statewalker/ai-agent-runtime.core`](packages/ai-agent-runtime.core) | Fragment orchestrator wiring the agent runtime into a workspace. | -| [`@statewalker/ai-config.core`](packages/ai-config.core) | Unified AI configuration: connections, models, credentials. Takes a host (`files` + `secrets`) rather than a `Workspace`. | -| [`@statewalker/ai-config.view.react`](packages/ai-config.view.react) | Renderer for the AI connections settings tab. | -| [`@statewalker/ai-local-models.core`](packages/ai-local-models.core) | Local (in-browser) model lifecycle and weight storage. | -| [`@statewalker/ai-local-models.browser`](packages/ai-local-models.browser) | Browser implementation of the local-model runtime. | -| [`@statewalker/ai-local-models.view.react`](packages/ai-local-models.view.react) | Renderer for the Local Models settings tab. | -| [`@statewalker/ai-openai-compat.core`](packages/ai-openai-compat.core) | Wire-format adapter exposing Vercel AI SDK providers over an OpenAI-compatible surface. | - -### Web app hosting - -| Package | Description | -| --- | --- | -| [`@statewalker/webapp.core`](packages/webapp.core) | Web-application hosting logic — serves a built site from the workspace. | -| [`@statewalker/webapp.browser`](packages/webapp.browser) | Browser implementation of the hosting runtime. | -| [`@statewalker/webapp.view.react`](packages/webapp.view.react) | Renderer for the hosted-app surface. | +## How to run it -### Version control +1. Install Node.js 24. +2. Enable corepack so the pinned pnpm 10 is used: `corepack enable`. +3. Install: `pnpm install`. +4. Build every package: `pnpm build`. +5. Run the tests: `pnpm test`. -| Package | Description | -| --- | --- | -| [`@statewalker/workspace-vcs.core`](packages/workspace-vcs.core) | Git as an opt-in project nature — a real native-git-compatible `.git` per project, manual commits, HTTP remotes. Bridges the workspace to [`webrun-vcs`](https://github.com/statewalker/webrun-vcs). | +There is no app in this repository to start. The packages are activated by a +host app; see "How a host app wires the packages" below. -## Backbone independence rule +## Why it is the way it is -The `backbone.*` packages MUST NOT declare a runtime dependency on any other `@statewalker/*` package — including siblings in this monorepo. Backbone vendors the narrow slices it needs (currently `Logger`/`getLogger` from `@statewalker/shared-logger`) into `backbone.core/src/_vendor/`. A CI check (`scripts/check-backbone-isolation.ts`) enforces this invariant on every PR. +### Logic and view live in separate packages -Substrate fragments may depend on backbone, never the reverse. +A `*.view.*` package is the only kind of package allowed to import a UI +library (React, shadcn, the json-render React bindings). The logic packages +stay React-free, so they can be tested without a DOM and a different renderer +could replace these packages. Values that cross from logic to view are data: +a logic package names a component by a string `viewKey`, and the view +package registers the component under that key in the `core:views` slot. -## Substrate fragment shape +### How a host app wires the packages -Every substrate fragment package follows a single canonical layout, enforced by the `workbench-canonical-substrate` capability spec: +Every package with a `./fragment` export has a default export +`init(ctx) => cleanup`. It reads the `Workspace` from `ctx`, registers +components into slots, and returns a function that removes them. ``` -src/ - index.ts # re-exports public/index.js - fragment.ts # re-exports public/init.js's default - public/ # types, commands, slot keys, manager classes, init - internal/ # impl + tests; not reachable through any export sub-path - styles.css # renderer fragments only — Tailwind v4 @source globs +host boot + create Workspace, put it into ctx + init logic fragments (shell.core, settings.core, explorer.core, ...) + init view fragments (ui.view.react, shell.view.react, settings.view.react, ...) + ui.view.react: createRoot(#app).render() + import each package's ./styles once ``` -`package.json#exports` declares exactly `"."` and `"./fragment"` for logic fragments, plus `"./styles"` for renderer fragments (the `*.view.*` leaves). `*.core` (and bare `*.view`) packages do not import React or any `*.view.*` package — the ADR-0002 boundary, keyed on the `view` leaf. - -## Cross-repo dependencies - -This repository depends on: - -| Repository | Packages used | +`ui.view.react` renders whatever is registered under `shell:root` in +`core:views`; `shell.view.react` registers `MainShell` there. Neither imports +the other's components. + +### Dependency specifiers + +Packages inside this repository depend on each other with `workspace:^`. +Everything else comes from the pnpm catalog in `pnpm-workspace.yaml` +(`catalog:`); `react` and `react-dom` are peers (`catalog:peers`, `>=18`). + +## What will surprise you + +- **Components render without colors or spacing.** The host did not import + `@statewalker/ui.view.react/styles` (theme variables) or a package's + `./styles` (Tailwind `@source` globs), so Tailwind did not emit its classes. +- **`useAppWorkspace must be used inside .`** A + component that uses the workspace hooks was rendered outside ``. +- **`No adapter registered for ...`** at fragment init. A view fragment ran + before the logic fragment that installs the adapter it needs (for example + `SpecStore`, `LayoutStore`, `DockHost`). +- **A dock tab shows "Spec ... is missing." or "Catalog ... is not + registered."** The panel was restored from the saved layout, but no + fragment created its spec or registered its catalog. +- **The page stays empty and nothing is logged.** There is no element with + `id="app"`; `ui.view.react`'s init then skips the mount silently. +- **Nothing renders after the folder is opened.** No component is registered + under `shell:root`: `shell.view.react`'s fragment was not activated. +- **Cleanup functions return promises.** Most `init` functions return + `() => Promise`; await them when tearing down. + +## Reference + +### Commands + +| Command | What it does | | --- | --- | -| [`statewalker-fsm`](https://github.com/statewalker/statewalker-fsm) | `@statewalker/fsm` | -| [`statewalker-shared`](https://github.com/statewalker/statewalker-shared) | `@statewalker/shared-adapters`, `@statewalker/shared-baseclass`, `@statewalker/shared-commands`, `@statewalker/shared-ids`, `@statewalker/shared-logger`, `@statewalker/shared-logger-pino`, `@statewalker/shared-registry`, `@statewalker/shared-slots` | -| [`webrun-files`](https://github.com/statewalker/webrun-files) | `@statewalker/webrun-files`, `@statewalker/webrun-files-browser`, `@statewalker/webrun-files-composite`, `@statewalker/webrun-files-mem`, `@statewalker/webrun-files-node` | -| [`webrun-transform`](https://github.com/statewalker/webrun-transform) | `@statewalker/webrun-builder`, `@statewalker/webrun-dataflow`, `@statewalker/webrun-modules` | -| [`webrun-vcs`](https://github.com/statewalker/webrun-vcs) | `@statewalker/vcs-commands`, `@statewalker/vcs-core`, `@statewalker/vcs-store-files`, `@statewalker/vcs-transport`, `@statewalker/vcs-transport-adapters`, `@statewalker/vcs-working-tree`, `@statewalker/vcs-workspace` | -| [`webrun-wire`](https://github.com/statewalker/webrun-wire) | `@statewalker/webrun-site-builder`, `@statewalker/webrun-site-host` | - -Cross-repo dependencies are declared `workspace:*` rather than `catalog:`. This is -deliberate: turbo derives its task graph from `workspace:` specifiers and does **not** -resolve `catalog:`, so a `catalog:` cross-repo dependency is invisible to the scheduler -and its consumer can be built before it. - -## Development - -```sh -pnpm install -pnpm run build -pnpm run test -``` - -## Release - -Releases are managed via [changesets](https://github.com/changesets/changesets): - -```sh -pnpm changeset # describe the change -pnpm version-packages # roll versions + regenerate CHANGELOGs -pnpm release-packages # publish to npm -``` - -## History - -The initial commit on `main` is a fresh template expansion; pre-split history is preserved as archaeology-only branches: +| `pnpm install` | Install dependencies | +| `pnpm build` | Build every package with tsdown | +| `pnpm test` | Run every package's vitest suite | +| `pnpm typecheck` | Type-check every package | +| `pnpm lint` / `pnpm lint:check` | Biome check, with or without fixes | +| `pnpm format` / `pnpm format:check` | Biome format, with or without writes | +| `pnpm changeset` | Add a changeset to pick a version bump and changelog text | -- `history/backbone-common`, `history/backbone-server`, `history/backbone-web` -- `history/shared-views`, `history/shared-dom`, `history/shared-react` -- `history/shared-react-shadcn` (sourced from `shared-react.shadcn/`) -- `history/shared-react-spectrum` (sourced from `shared-react.spectrum/`) -- `history/app-shell-core` (sourced from `workspaces/workspace-explorer/apps/app.shell.core/`; package later folded into `shared-views`) +### Releases -These branches are never merged into `main`; `git log` them when walking blame across the split. +Packages are published to npm from CI with changesets. After CI passes on +`main`, a job adds a patch changeset for each package whose packed contents +differ from npm and opens a "chore: version packages" pull request; merging it +publishes. Add your own changeset with `pnpm changeset` to choose the bump or +the changelog text. Dependency updates come from Renovate. -## License +### License -MIT — see `LICENSE`. +MIT. See [LICENSE](LICENSE). diff --git a/package.json b/package.json index 0c47e2f1..efcedf16 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "private": true, "type": "module", "packageManager": "pnpm@10.16.1", - "description": "Application shell and fragment platform: backbone-common/server/web + shared-dom/views/react renderers", + "description": "React renderers for the statewalker application shell", "bugs": { "url": "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/statewalker/statewalker-shell-react/issues" }, diff --git a/packages/PACKAGE_README.template.md b/packages/PACKAGE_README.template.md index 595e2087..f54f3454 100644 --- a/packages/PACKAGE_README.template.md +++ b/packages/PACKAGE_README.template.md @@ -1,25 +1,36 @@ # {{packageName}} -{{packageDescription}} +## What it is -## Installation + + +## Why it exists + + + +## How to use ```sh pnpm add {{packageName}} ``` -## Usage +Peer dependencies: `react` and `react-dom` (`>=18`). - +| Import | Provides | +| --- | --- | +| `{{packageName}}` | | +| `{{packageName}}/fragment` | | +| `{{packageName}}/styles` | Tailwind v4 `@source` globs | -```ts -import {} from "{{packageName}}"; -``` +## Examples + + -## API +## Internals - + -## Related +## License - +MIT diff --git a/packages/ai-config.view.react/README.md b/packages/ai-config.view.react/README.md index 297c59b6..27f3d61a 100644 --- a/packages/ai-config.view.react/README.md +++ b/packages/ai-config.view.react/README.md @@ -2,11 +2,21 @@ ## What it is -The React renderer for the "Remote Models" AI-connections settings tab. It implements the React-free contract published by `@statewalker/ai-config`: the json-render schema-typed catalog and its component bindings (a controlled `Collapsible`, a status-dot `Tabs`, and a bespoke `FieldInput`), the `AiConfig → state` projection bridge, the `AiConfig` + `Secrets` action handlers, and the settings-tab `ViewComponent` itself. Its fragment init registers the tab into the settings dialog and wires the deep-link command. +The "Remote Models" tab of the settings dialog, where a user adds AI provider +connections (Anthropic, OpenAI-compatible endpoints and other types defined in +`@statewalker/ai-config.core`), enters API keys, tests the connection, and +stars models. The tab is a json-render spec from `@statewalker/ai-config.core` +rendered with shadcn components; this package supplies the React bindings, the +action handlers and the bridge that copies `AiConfig` state into the spec's +state. ## Why it exists -Per ADR 0002 the logic/view split is the package boundary: the opaque json-render `Spec`, the catalog id, and the allowed component/action name-sets live in the React-free `@statewalker/ai-config`; everything that needs React — the schema-typed `defineCatalog`/`defineRegistry` binding (which pulls in `@json-render/react`'s `schema` via `render.view.react`), the actual React components, the action implementations, and the state bridge — lives here. This keeps `@statewalker/ai-config` portable while giving the host a drop-in settings tab. The two packages agree on the catalog id, component vocabulary, and action vocabulary so the spec and these bindings can be validated against each other. +`@statewalker/ai-config.core` holds the `AiConfig` adapter, the tab's spec, +and the allowed component and action names, with no React. Anything that needs +React or json-render's React schema lives here: the typed catalog, the +components, the action implementations and the state bridge. The fragment +plugs the tab into the settings dialog. ## How to use @@ -14,93 +24,132 @@ Per ADR 0002 the logic/view split is the package boundary: the opaque json-rende pnpm add @statewalker/ai-config.view.react ``` -Register both fragments — the logic fragment first, then this renderer — and import its stylesheet: +Peer dependencies: `react` and `react-dom` (`>=18`). + +| Import | Provides | +| --- | --- | +| `@statewalker/ai-config.view.react` | `AiConfigConnectionsTab`, `buildConnectionsRegistry`, `connectionsCatalog`; default export is the fragment init | +| `@statewalker/ai-config.view.react/fragment` | Default export: `init(ctx)` returning `() => Promise` | +| `@statewalker/ai-config.view.react/styles` | An empty stylesheet (comments only), kept as a place for tab-specific styles | + +Activate the logic fragment first, then this one: ```ts -import initAiConfig from "@statewalker/ai-config/fragment"; +import initAiConfig from "@statewalker/ai-config.core/fragment"; import initAiConfigView from "@statewalker/ai-config.view.react/fragment"; -import "@statewalker/ai-config.view.react/styles"; initAiConfig(ctx); -initAiConfigView(ctx); +const cleanup = initAiConfigView(ctx); ``` -The renderer's init: +The init: -1. Registers the connections-tab `ViewComponent` into the `core:views` slot under the `ai-config:connections` viewKey. -2. Contributes the `settings:tabs` entry ("Remote Models", `order: 20`). -3. Listens for `ConfigureAiCommand` and opens the settings dialog on the tab via `OpenSettingsCommand`. +1. Registers `AiConfigConnectionsTab` in `core:views` under + `AI_CONFIG_CONNECTIONS_TAB_VIEW_KEY`. +2. Adds a `settings:tabs` entry titled "Remote Models", `order: 20`. +3. Handles `ConfigureAiCommand` by calling `OpenSettingsCommand` with that tab. ## Examples -### The settings-tab component (the normal entry point) +### Open the tab from anywhere -`AiConfigConnectionsTab` is registered for you by the fragment, but it is exported for direct mounting: +```ts +import { ConfigureAiCommand } from "@statewalker/ai-config.core"; + +await commands.call(ConfigureAiCommand, undefined).promise; +``` + +### Mount the tab directly ```tsx import { AiConfigConnectionsTab } from "@statewalker/ai-config.view.react"; -// Renders inside an AppWorkspace context; resolves AiConfig from the workspace, -// owns a per-mount json-render StateStore, and renders the spec via SpecRenderer. -; +; // needs an ancestor and the AiConfig adapter ``` -### Building the registry directly +### Build the registry ```ts -import { - buildConnectionsRegistry, - connectionsCatalog, -} from "@statewalker/ai-config.view.react"; - -const { registry } = buildConnectionsRegistry({ - actions: handlers, // a ConnectionsActionHandlers map bound to AiConfig + the store -}); -``` +import { buildConnectionsRegistry, connectionsCatalog } from "@statewalker/ai-config.view.react"; -The `handlers` map is produced internally by `buildActionHandlers({ aiConfig, store })`; `AiConfigConnectionsTab` assembles it for you. +const registry = buildConnectionsRegistry({ actions: handlers }); +``` -`connectionsCatalog` is the schema-typed catalog: it extends `@json-render/shadcn`'s stock definitions with the panel's three custom components, plus typed params for each of the seven connection actions. +`buildConnectionsRegistry` returns json-render's `DefineRegistryResult`. +`handlers` must implement the seven actions: `addConnection`, +`connectConnection`, `disconnectConnection`, `removeConnection`, +`toggleModelStar`, `addHeader`, `removeHeader`. The tab builds these +internally; the function is exported for tests and custom hosts. +`connectionsCatalog` extends `@json-render/shadcn`'s component definitions +with the tab's three custom components. ## Internals -### Architectural decisions +### One store per mounted tab -- **Per-mount store, mount-scoped handlers.** `AiConfigConnectionsTab` creates one json-render `StateStore` per mount (`makeConnectionsInitialState()`), builds the action handlers closed over `{ aiConfig, store }`, builds the registry, and wires the bridge — all `useMemo`-stable. The component renders the spec through `SpecRenderer`. -- **The component watches the store from outside json-render's context.** It lives *outside* `SpecRenderer`'s `JSONUIProvider`, so it subscribes to the store directly via `useSyncExternalStore` on `/ui/activeConnectionId` and calls `bridge.sync()` when the active tab changes — there is no json-render state context out there to read. -- **Three custom catalog components** replace stock shadcn variants the panel needs: - - `ControlledCollapsible` — controlled fold via a state `openPath` (`/ui/form/settingsOpen`), so `connectConnection` can collapse the credential form on success and the user can re-expand it. Stock shadcn Collapsible is uncontrolled (`defaultOpen` only). - - `StatusTabs` — a connection-aware `Tabs` whose triggers carry a status dot (connected / testing / error / idle) projected by the bridge. Renders only the tab strip; the active body is a sibling gated on `/persistent/active`, not a `TabsContent` child. - - `FieldInput` — form input with `autoComplete` set explicitly plus `data-1p-ignore`/`data-lpignore` to suppress browser autofill cross-pollution, and a show/hide eye toggle for the API-key field. -- **Credentials are write-only in the UI.** The state bridge seeds `/ui/form/apiKey` blank from the domain (it never reads keys back); `connectConnection` flushes it to `Secrets` via `AiConfig.setApiKey`. The key never lands on `/persistent`, on a `Connection`, or in the persisted config. +`AiConfigConnectionsTab` creates a json-render `StateStore` per mount, builds +the action handlers over `{ aiConfig, store }`, builds the registry, and +renders the spec with `SpecRenderer`. It sits outside the +``, so it watches `/ui/activeConnectionId` in the store +directly and re-syncs the bridge when the selected tab changes. -### Algorithms +### The state bridge -- **State bridge projection.** `createConnectionsBridge(store, aiConfig)` projects `AiConfig.listConnections()` into `/persistent/{hasConnections,tabs,active}` and swaps `/ui/form` to the selected connection's draft. A connection is "connected" once discovery has cached models on it. It re-runs on every `AiConfig` update **and** when the host re-syncs on tab change; the form is re-seeded only when the active connection id actually changes, so in-progress edits survive unrelated config updates. Neither the bridge nor the component subscribes through json-render, avoiding the new-reference notification loop. -- **Action handlers.** `connectConnection` validates a URL when required (`anthropic`/`openai-compatible`), persists the shell, flushes the key, discovers models, and — only when `starredModelIds` is empty — seeds default stars via `applyDefaultStarred`. `removeConnection` gates behind a confirm dialog when the connection has a stored key (`hasKey`), then advances the active tab to a neighbour. `toggleModelStar`, `addHeader`, `removeHeader` are small store/`AiConfig` mutations. +`createConnectionsBridge(store, aiConfig)` writes +`/persistent/{hasConnections,tabs,active}` from `AiConfig.listConnections()` +and loads the selected connection's draft into `/ui/form`. It runs on every +`AiConfig` update and on tab changes. The form is reloaded only when the +selected connection changes, so typing is not lost when an unrelated config +update arrives. A connection counts as connected once models were discovered +for it. -### Constraints +### API keys are write-only -- Renders through `@statewalker/ui.view.shadcn` primitives; `react`/`react-dom` are peer dependencies (`>=18`). The `./styles` export is currently a placeholder hook for panel-specific tweaks. -- The catalog's component and action vocabularies must stay in lockstep with `@statewalker/ai-config`'s `CONNECTIONS_COMPONENTS` / `CONNECTIONS_ACTIONS` name-sets (the logic package's validation test enforces the spec stays within them). +The bridge never reads a key back; the key field always starts empty. +`connectConnection` writes the key to `Secrets` (via `AiConfig.setApiKey`) +only when the field is not empty. An empty field keeps the stored key, so +re-testing does not erase it. -### Dependencies +### What "Connect" does, and what the user sees when it fails + +1. URL required for `anthropic` and `openai-compatible`; otherwise the form + shows `A URL is required for connections.` +2. Saves the connection (name, URL, headers). +3. Saves the key, or, if none is typed and none is stored, shows + `An API key is required.` +4. Fetches the model list. If no models are starred yet, stars the defaults + from `applyDefaultStarred`. +5. If no model is active yet, makes the first starred model active, so a new + workspace can start a chat session. +6. Collapses the form. Any thrown error is shown as its message in the form. + +Removing a connection that has a stored key asks for confirmation first. -- `@statewalker/ai-config` — the logic contract: spec, catalog id, vocabularies, `AiConfig` adapter, `applyDefaultStarred`, `capabilitiesFor`. -- `@json-render/core` / `@json-render/shadcn` — `Spec`/`StateStore`, `defineCatalog`, and the stock shadcn component definitions/bindings extended here. -- `@statewalker/render.core` / `@statewalker/render.view.react` — `SpecRenderer`, `defineRegistry`, `schema`, and the `useStateBinding`/`useBoundProp` hooks the custom components use. -- `@statewalker/settings.core` — `settingsTabSlot`, `OpenSettingsCommand` for the tab contribution and deep-link. -- `@statewalker/ui.view.react` / `@statewalker/ui.view.shadcn` — `coreViewsSlot`/`ViewComponent`, `useAppWorkspace`, and the shadcn primitives (`Collapsible`, `Tabs`, `Input`, `Button`, …). -- `@statewalker/workspace.core` — `getWorkspace`, the adapter host. -- `@statewalker/shared-commands` / `@statewalker/shared-registry` / `@statewalker/shared-slots` — command bus, scoped cleanup, slot registration. -- `lucide-react` (eye icons), `zod` (catalog prop/param schemas). +### Custom components -## Related +- `Collapsible` (controlled): open state lives in `/ui/form/settingsOpen`, so + a successful connect can collapse the form. The stock shadcn one only has + `defaultOpen`. +- `StatusTabs`: tab strip with a status dot (connected, testing, error, idle). +- `FieldInput`: sets `autoComplete`, `data-1p-ignore` and `data-lpignore` to + stop password managers from filling one connection's key into another, and + has a show/hide toggle for the key. + +The component and action names must stay within +`CONNECTIONS_COMPONENTS` / `CONNECTIONS_ACTIONS` from +`@statewalker/ai-config.core`. + +### Dependencies -- `@statewalker/ai-config` — the React-free logic fragment this renderer pairs with. -- `@statewalker/ai-agent-runtime` — consumes the active selection that this panel lets the user pick. -- `@statewalker/render.view.react` — the json-render React renderer this builds on. +- `@statewalker/ai-config.core` — `AiConfig`, the spec, ids, `ConfigureAiCommand`, `applyDefaultStarred`. +- `@json-render/core`, `@json-render/shadcn` — `StateStore`, `defineCatalog`, stock shadcn components. +- `@statewalker/render.view.react` — `SpecRenderer`, `defineRegistry`, `schema`, binding hooks. +- `@statewalker/settings.core` — `settingsTabSlot`, `OpenSettingsCommand`. +- `@statewalker/ui.view.react`, `@statewalker/ui.view.shadcn` — `coreViewsSlot`, `useAppWorkspace`, primitives. +- `@statewalker/workspace.core`, `@statewalker/shared-commands`, `@statewalker/shared-registry`, `@statewalker/shared-slots` — fragment wiring. +- `lucide-react` (eye icons), `zod` (schemas). +- `@statewalker/render.core` — declared but not imported by the source. ## License -MIT — see the monorepo root `LICENSE`. +MIT diff --git a/packages/ai-local-models.view.react/README.md b/packages/ai-local-models.view.react/README.md new file mode 100644 index 00000000..82eabfcd --- /dev/null +++ b/packages/ai-local-models.view.react/README.md @@ -0,0 +1,106 @@ +# @statewalker/ai-local-models.view.react + +## What it is + +The "Local Models" tab of the settings dialog. It lists the models that can +run in the browser, shows each model's description, and lets the user +download weights (with progress), cancel a download, delete weights, and pick +a downloaded model as the active one. The tab is a json-render spec from +`@statewalker/ai-local-models.core`; this package supplies the React +bindings, the action handlers and the bridge from the `LocalModels` adapter +into the spec's state. + +## Why it exists + +`@statewalker/ai-local-models.core` holds the `LocalModels` adapter (catalog +of models, weight storage, downloaded set), the tab spec, and the +`settings:tabs` entry, with no React. This package renders that spec and wires +its buttons to the adapter. + +## How to use + +```sh +pnpm add @statewalker/ai-local-models.view.react +``` + +Peer dependencies: `react` and `react-dom` (`>=18`). + +| Import | Provides | +| --- | --- | +| `@statewalker/ai-local-models.view.react` | `aiLocalModelsCatalog`, types `AiLocalModelsCatalog`, `LocalModelRow`; default export is the fragment init | +| `@statewalker/ai-local-models.view.react/fragment` | Default export: `init(ctx)` returning `() => Promise` | +| `@statewalker/ai-local-models.view.react/styles` | Tailwind v4 `@source` globs | + +Activate the logic fragment first (it contributes the settings tab), then +this one: + +```ts +import initLocalModels from "@statewalker/ai-local-models.core/fragment"; +import initLocalModelsView from "@statewalker/ai-local-models.view.react/fragment"; +import "@statewalker/ai-local-models.view.react/styles"; + +initLocalModels(ctx); +const cleanup = initLocalModelsView(ctx); +``` + +The init: + +1. Registers the tab component in `core:views` under + `LOCAL_MODELS_TAB_VIEW_KEY` (the key used by the logic fragment's + `settings:tabs` entry). +2. Registers a registry for `aiLocalModelsCatalog` in `json:catalogs` under + `AI_LOCAL_MODELS_CATALOG_ID`. + +## Examples + +### Validate a spec against the catalog + +```ts +import { aiLocalModelsCatalog } from "@statewalker/ai-local-models.view.react"; +``` + +`aiLocalModelsCatalog` is `@json-render/shadcn`'s component definitions plus a +`Markdown` component (`{ source: string }`), and four actions, each taking +`{ key: string }`: `downloadLocalModel`, `cancelDownload`, `removeLocalModel`, +`selectLocalModel`. + +`LocalModelRow` is the shape of one entry in `/persistent/localModelsList`. + +## Internals + +### What each action does + +| Action | Effect | +| --- | --- | +| `downloadLocalModel` | Iterates `LocalModels.download(key)` and writes progress to `/ui/downloads/` (`phase`, `progress`, `message`); then `markDownloaded(key)`. An error is written to `/ui/downloads//error`. | +| `cancelDownload` | `LocalModels.cancelDownload(key)` and clears `/ui/downloads/`. | +| `removeLocalModel` | `LocalModels.removeWeights(key)`. | +| `selectLocalModel` | Calls `SelectLocalModelCommand` with `{ modelId: key }`; the logic fragment handles it. | + +### The registry in `json:catalogs` has no working actions + +The tab component builds its own store, handlers and registry per mount and +renders through its own ``. The registry the init puts into +`json:catalogs` is built with an empty handler map. A local-models spec opened +anywhere else (for example as a dock tab) renders, but its buttons log +`No handler registered for action: ...` and do nothing. + +### State + +`bindLocalModels(store, localModels)` writes `/persistent/localModelsList` +from the curated catalog and the downloaded set, and updates it when the +adapter changes. Model descriptions are rendered with `react-markdown` and +`remark-gfm`; an empty description renders nothing. + +### Dependencies + +- `@statewalker/ai-local-models.core` — `LocalModels`, spec, ids, `SelectLocalModelCommand`. +- `@json-render/core`, `@json-render/react`, `@json-render/shadcn`, `zod` — store, rendering, catalog. This package imports `@json-render/react` directly instead of going through `@statewalker/render.view.react`. +- `@statewalker/render.core` — `catalogsSlot`. +- `@statewalker/ui.view.react` — `coreViewsSlot`, `useAppWorkspace`. +- `react-markdown`, `remark-gfm` — model descriptions. +- `@statewalker/workspace.core`, `@statewalker/shared-commands`, `@statewalker/shared-registry`, `@statewalker/shared-slots` — fragment wiring. + +## License + +MIT diff --git a/packages/explorer.view.react/README.md b/packages/explorer.view.react/README.md index 48876ec4..b8ed3541 100644 --- a/packages/explorer.view.react/README.md +++ b/packages/explorer.view.react/README.md @@ -2,23 +2,18 @@ ## What it is -The React renderer for the workbench file explorer. It binds the -`file-explorer` json-render catalog, renders the panel UI -(`FileExplorerPanel`, `FilesListView`, plus internal breadcrumb / file-row / -drag-and-drop), applies the two-pane panel presets into the dock on workspace -open, and serves the `file-explorer:new-panel` command. It is the view half of -`@statewalker/explorer.core`, which owns all the -React-free logic. +A view fragment for file-explorer tabs in the dock. Each tab shows one folder +as a list with breadcrumbs, a filter and drag and drop. The fragment binds the +`file-explorer` json-render catalog to React, opens the panel presets from +`@statewalker/explorer.core` when a workspace opens, and handles the +`file-explorer:new-panel` command. ## Why it exists -Per ADR-0002 the explorer is split into a logic fragment and a renderer -fragment. This package is the renderer: it consumes the models, controller, spec -helpers, and slots from `explorer.core` and turns them into mounted dock panels. -It also holds the schema-typed json-render catalog (`fileExplorerCatalog`), -which must live on the React side because it needs `@json-render/react`'s -`schema` (reached through `@statewalker/render.view.react`, the sole -json-render/react boundary). +`@statewalker/explorer.core` holds the models, the panel controller, the +commands, the slots and the spec helpers, with no React. This package turns +them into dock tabs. The schema-typed catalog (`fileExplorerCatalog`) lives +here because building it needs json-render's React `schema`. ## How to use @@ -26,112 +21,108 @@ json-render/react boundary). pnpm add @statewalker/explorer.view.react ``` -Three entry points: +Peer dependencies: `react` and `react-dom` (`>=18`). -- `@statewalker/explorer.view.react` — public React surface - (`FileExplorerPanel`, `FileExplorerPanelProps`, `FilesListView`). -- `@statewalker/explorer.view.react/fragment` — default-exported - `initFileExplorerReact(ctx)` renderer-fragment init. -- `@statewalker/explorer.view.react/styles` — the bundled `styles.css`. +| Import | Provides | +| --- | --- | +| `@statewalker/explorer.view.react` | `FileExplorerPanel`, `FileExplorerPanelProps`, `FilesListView`; default export is the fragment init | +| `@statewalker/explorer.view.react/fragment` | Default export: `init(ctx)` returning `() => Promise` | +| `@statewalker/explorer.view.react/styles` | Tailwind v4 `@source` globs | -The host app registers the fragment init and imports the styles. The init wires -the catalog, the dock-tab icon, the two-pane preset applier, and the new-panel -command; it returns a `cleanup` thunk. Peer-depends on `react` / `react-dom` >= 18. +Activate it after the `explorer.core`, `render.core` and `shell.core` logic +fragments: + +```ts +import "@statewalker/explorer.view.react/styles"; +import initExplorerView from "@statewalker/explorer.view.react/fragment"; + +const cleanup = initExplorerView(ctx); +``` + +The init: + +1. Registers the catalog under `FILE_EXPLORER_CATALOG_ID` in `json:catalogs`. +2. Adds a folder icon for tabs whose id starts with `file-explorer:`. +3. On workspace open, creates specs for `file-explorer:` tabs in the saved + layout (from the `LayoutStore` adapter), then opens one dock tab per entry + in the `file-explorer:panels` preset slot. +4. Listens for `NewFileExplorerPanelCommand` (`file-explorer:new-panel`). ## Examples -Register the fragment (host wiring) — pairs with the logic fragment: +### Open a new explorer tab ```ts -import initFileExplorer from "@statewalker/explorer.core/fragment"; -import initFileExplorerReact from "@statewalker/explorer.view.react/fragment"; -import "@statewalker/explorer.view.react/styles"; +import { NewFileExplorerPanelCommand } from "@statewalker/explorer.core"; -const cleanupLogic = initFileExplorer(ctx); -const cleanupView = await initFileExplorerReact(ctx); +const { panelId } = await commands.call(NewFileExplorerPanelCommand, { + initialPath: "/docs", + label: "Docs", + position: "right", +}).promise; ``` -Mount a single panel directly (outside the dock): +Defaults: `initialPath` `"/"`, `label` `"Files"`, `position` `"within"`. The +result's `panelId` is the generated id (`panel-<8 hex chars>`); the dock tab id +is `file-explorer:` followed by it. + +### Mount one panel outside the dock ```tsx import { FileExplorerPanel } from "@statewalker/explorer.view.react"; -; +; ``` -The catalog binding means a dock tab built from `makeFileExplorerSpec(panelId)` -(in `explorer.core`) resolves to `FileExplorerView`, which renders a -`FileExplorerPanel`. +`FileExplorerPanelProps`: `panelId` (required), `initialPath`, `label`, +`mainViewerHost`, `folderNavigationHost`. The panel needs an +`` ancestor. + +`FilesListView` (`{ model, panelId, onOpen }`) is the list itself, for hosts +that create their own `FilesListModel`. ## Internals -### Architectural decisions - -- **Catalog binding is the integration seam.** `init.tsx` calls `defineRegistry` - against `fileExplorerCatalog` and registers it under - `FILE_EXPLORER_CATALOG_ID` in `catalogsSlot`, so any spec produced by - `makeFileExplorerSpec` renders a `FileExplorerPanel`. -- **Panels are ordinary dock tabs.** Two-pane presets are applied by dispatching - one `dock:show-panel` (`ShowDockPanelCommand`) per preset, so explorer panels - appear in the central `DockViewHost` exactly like markdown / pdf / image - viewers. A preset's `side` becomes a dock-position hint (`left` / `right` split, - else `within`); the first preset opens at the default position. -- **Spec pre-allocation avoids a flash.** `restorePanelSpecsFromLayout` runs - synchronously at init so every persisted `file-explorer:` panel id has a spec - before `DockView.fromJSON()` runs, preventing a `PanelMissing` placeholder. - When a preset later arrives, an existing pre-allocated spec is `patch`ed up to - the full preset (label, host flags) rather than left at restore defaults. -- **Each panel owns its `PanelController`** (`useMemo` keyed on workspace / - panelId / label / initialPath) and registers an `ActiveFileExplorerPanel` into - `activeFileExplorerPanelsSlot` so the logic-side `files:open` handler can route - navigations to it. Every click / keypress dispatches `files:open` with the - panel id as both `origin` and `target`, so folders navigate in place. -- **`useViewModel`** wraps `useSyncExternalStore` over a `ViewModel`'s - `onUpdate` / `version`, the bridge between the React-free reactive models and - the React render loop. - -### Algorithms - -- **Preset idempotency:** an `opened` set tracks already-mounted preset ids so - hot-added presets (observed via `slots.observe`) don't duplicate tabs; - `workspace.onUnload` clears it. `workspace.onLoad` fires immediately if already - open, so one subscription covers first-load and re-open. -- **`file-explorer:new-panel`** generates a fresh `panel-` id, creates a - persistent spec, shows the dock panel (default position `within`), and resolves - with the new id. - -### Constraints - -- Peer-depends on React 18+; `styles.css` must be imported by the host. -- Folder single-click navigates; files require double-click / Enter (so they stay - draggable). +### How presets become tabs + +Presets are sorted by `order`, then `id`. The first opens at the default +position. Each next one with `side: "left"` or `"right"` splits on that side; +`side: "main"` or no side adds a tab to the previous group. Opened preset ids +are remembered until the workspace unloads, so presets added later (the slot +is observed) do not duplicate tabs. A failed `dock:show-panel` is logged as +`[file-explorer] failed to open dock panel:` and the other presets still open. + +### Why specs are created before the layout is restored + +The dock restores tabs from the saved layout. A tab whose spec does not exist +yet shows `Spec is missing.` To avoid that, the init creates a +default spec for every saved `file-explorer:` tab first. When a preset for the +same id arrives, the spec is patched with the preset's label and flags. + +### Navigation + +Every activation (folder click, double-click, Enter, Backspace, breadcrumb) +calls `files:open` with this panel as both `origin` +and `target`, so folders open in place. One click opens a folder; a file needs +a double-click or Enter, so that a single click can start a drag. Each panel +registers itself in `activeFileExplorerPanelsSlot` so the `files:open` handler +in `explorer.core` can route to it. + +### Not implemented + +There is no tree view, context menu or search panel in this package. ### Dependencies -`@statewalker/explorer.core` (models, controller, commands, slots, spec -helpers), `@statewalker/render.core` (`SpecStore`, `catalogsSlot`, layout -restore), `@statewalker/render.view.react` (`schema`, `defineRegistry`), -`@statewalker/shell.core` (`ShowDockPanelCommand`, `FocusPanelCommand`, -`PanelPosition`), `@statewalker/shell.view.react` (`dockTabIconSlot`), -`@statewalker/ui.view.react` (`useAdapter`, `useAppWorkspace`, -`compareByOrderAndId`), `@statewalker/ui.view.shadcn`, `mime.core` -(`OpenCommand`), `workspace.core` (`getWorkspace`), `shared-commands`, -`shared-registry`, `shared-slots`, `@json-render/core`, `zod`, `lucide-react`. - -## Related - -- `@statewalker/explorer.core` — the paired - React-free logic fragment (the `.core` ↔ `.view.react` pair). -- `@statewalker/render.view.react` — the - json-render/react boundary supplying `schema` / `defineRegistry`. -- `@statewalker/shell.view.react` — dock host - and tab-icon slot. +- `@statewalker/explorer.core` — models, controller, commands, slots, spec helpers. +- `@statewalker/render.core`, `@statewalker/render.view.react` — `SpecStore`, `LayoutStore`, `json:catalogs`, `defineRegistry`, `schema`. +- `@statewalker/shell.core`, `@statewalker/shell.view.react` — `ShowDockPanelCommand`, `dockTabIconSlot`. +- `@statewalker/mime.core` — `OpenCommand` (`files:open`). +- `@statewalker/ui.view.react` — hooks, `compareByOrderAndId`. +- `@statewalker/shared-commands`, `@statewalker/shared-slots`, `@statewalker/shared-registry`, `@statewalker/workspace.core` — fragment wiring. +- `@json-render/core`, `zod`, `lucide-react`. +- `@statewalker/ui.view.shadcn`, `@statewalker/shared-baseclass`, `@statewalker/webrun-files` — declared but not imported by the source. ## License -MIT — see the monorepo root `LICENSE`. +MIT diff --git a/packages/explorer.view.react/package.json b/packages/explorer.view.react/package.json index 34979b21..5419bb25 100644 --- a/packages/explorer.view.react/package.json +++ b/packages/explorer.view.react/package.json @@ -3,7 +3,7 @@ "version": "0.2.1", "private": false, "type": "module", - "description": "File-explorer renderer fragment: tree, list, drag-and-drop, context menu, navigation breadcrumbs, search panel.", + "description": "File-explorer renderer fragment: list view with breadcrumbs, filter and drag-and-drop, dock panel presets, and the file-explorer:new-panel command.", "bugs": { "url": "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/statewalker/statewalker-shell-react/issues" }, diff --git a/packages/inline.view.react/README.md b/packages/inline.view.react/README.md index cf1177bc..f72cc90d 100644 --- a/packages/inline.view.react/README.md +++ b/packages/inline.view.react/README.md @@ -2,24 +2,21 @@ ## What it is -The React renderer for the inline-content subsystem. It owns the -`inline-content:renderers` keyed slot (the rendering lookup table), the -`` resolver that turns an `InlineContentSpec` into a rendered -component, and the five built-in inline components — `MetricCard`, `LineChart`, -`FileCard`, `DirectoryCard`, `ActionButton`. It is the view half of -`@statewalker/inline.core`, which holds the -React-free contract. +React rendering for inline content: small components that a message or +document embeds by id, such as a metric card or a chart. It provides +``, which looks up a component by `spec.componentId` and +renders it with `spec.props`, the `inline-content:renderers` slot that holds +those components, and five built-in components: `metric-card`, `line-chart`, +`file-card`, `directory-card`, `action-button`. ## Why it exists -Per ADR-0002 the inline-content subsystem is split logic / view. This package is -the view: the rendering slot is React-typed, so it can't live in the -framework-neutral logic package. Its init registers each built-in component -under its id into `inline-content:renderers` (so `` can resolve -it) *and* contributes the matching descriptor into the logic-side -`inline-content:components` slot (so the component is discoverable). Plug-in -fragments register the same way, so the chat surface sees plug-in components -indistinguishably from built-ins. +`@statewalker/inline.core` defines the spec type (`InlineContentSpec`) and the +`inline-content:components` slot, where components are listed by id and label +for discovery, with no React. The slot that holds the actual React components +must be typed with React, so it lives here. The fragment registers each +built-in in both slots; plug-in components register the same way and are +rendered exactly like built-ins. ## How to use @@ -27,24 +24,28 @@ indistinguishably from built-ins. pnpm add @statewalker/inline.view.react ``` -Three entry points: +Peer dependencies: `react` and `react-dom` (`>=18`). -- `@statewalker/inline.view.react` — `InlineContent`, - `inlineContentRenderersSlot`, `InlineContentComponent`. -- `@statewalker/inline.view.react/fragment` — default-exported - `initInlineContentReact(ctx)` renderer-fragment init (registers the five - built-ins; returns a `cleanup` thunk). -- `@statewalker/inline.view.react/styles` — the bundled `styles.css`. +| Import | Provides | +| --- | --- | +| `@statewalker/inline.view.react` | `InlineContent`, `inlineContentRenderersSlot`, `InlineContentComponent`; default export is the fragment init | +| `@statewalker/inline.view.react/fragment` | Default export: `init(ctx)` that registers the five built-ins; returns a cleanup function | +| `@statewalker/inline.view.react/styles` | Tailwind v4 `@source` globs | + +```ts +import "@statewalker/inline.view.react/styles"; +import initInlineView from "@statewalker/inline.view.react/fragment"; -Peer-depends on `react` / `react-dom` >= 18. +const cleanup = initInlineView(ctx); +``` ## Examples -Render a spec (typically one carried in an assistant message): +### Render a spec ```tsx -import { InlineContent } from "@statewalker/inline.view.react"; import type { InlineContentSpec } from "@statewalker/inline.core"; +import { InlineContent } from "@statewalker/inline.view.react"; const spec: InlineContentSpec = { componentId: "line-chart", @@ -54,85 +55,68 @@ const spec: InlineContentSpec = { ; ``` -Register a plug-in component (same path the built-ins take): +### Register a plug-in component ```tsx -import { inlineContentRenderersSlot, type InlineContentComponent } from "@statewalker/inline.view.react"; import { inlineComponentSlot } from "@statewalker/inline.core"; +import { type InlineContentComponent, inlineContentRenderersSlot } from "@statewalker/inline.view.react"; import { Slots } from "@statewalker/shared-slots"; -const MyInline: InlineContentComponent = ({ props }) =>
{String(props)}
; +const Greeting: InlineContentComponent = ({ props }) => {String((props as { name: string }).name)}; + const slots = workspace.requireAdapter(Slots); -slots.register(inlineContentRenderersSlot, "my-inline", MyInline); -slots.provide(inlineComponentSlot, { id: "my-inline", label: "My Inline" }); +slots.register(inlineContentRenderersSlot, "greeting", Greeting); +slots.provide(inlineComponentSlot, { id: "greeting", label: "Greeting" }); ``` -Built-in component ids and their props: +### Built-in components -| id | props | +| `componentId` | `props` | | --- | --- | -| `metric-card` | `{ label, value, delta?, trend?: "positive" \| "negative" }` | -| `line-chart` | `{ values: number[], startLabel?, endLabel?, height? }` | -| `file-card` | `{ uri, name?, description? }` — click fires `files:visualize` | -| `directory-card` | `{ uri, name?, entries? }` — lazy-loads via `files:load-directory`; rows fire `files:visualize` | -| `action-button` | `{ label, command, payload?, variant? }` — click fires the named command | +| `metric-card` | `{ label: string, value: string \| number, delta?: string, trend?: "positive" \| "negative" }` | +| `line-chart` | `{ values: number[], startLabel?: string, endLabel?: string, height?: number }` | +| `file-card` | `{ uri: string, name?: string, description?: string }`; a click calls `files:visualize` | +| `directory-card` | `{ uri: string, name?: string, entries?: { name, kind: "file" \| "directory" }[] }`; without `entries` it loads one level with `files:load-directory`; a row click calls `files:visualize` | +| `action-button` | `{ label: string, command: string, payload?: unknown, variant?: "default" \| "primary" \| "destructive" }`; a click calls the command named `command` with `payload` | ## Internals -### Architectural decisions - -- **Two slots, one registration.** `init.ts` walks a `BUILTINS` table and, for - each entry, both `register`s the component into `inline-content:renderers` and - `provide`s the descriptor into `inline-content:components`. Rendering lookup and - discoverability stay in sync. -- **`` subscribes to the slot** via `useKeyedSlot`, so a - late-registered (plug-in) component renders without a remount. -- **Unknown ids are visible, not swallowed.** Because a spec's `componentId` - usually originates from the agent's structured output (the trust boundary), an - unresolved id renders a small inline error chip rather than nothing. Each - built-in independently validates its own props and shows the same chip on a - shape mismatch. -- **`InlineContentComponent` is `ComponentType<{ props: unknown }>`.** The slot - holds components opaquely; each casts `props` to its concrete shape internally, - mirroring the `unknown` props in `InlineContentSpec`. -- **Components fire commands, not callbacks.** `FileCard` / `DirectoryCard` fire - `files:visualize`; `ActionButton` fires an arbitrary command by string key. - This keeps one component reusable across many actions with no per-action React - bindings, and routes everything through the workspace command bus. - -### Algorithms - -- **`LineChart`** scales a numeric series to a fluid SVG `` (min/max - normalised over the configured height, x by index) — no charting dependency. -- **`DirectoryCard`** lazy-loads one level via `files:load-directory` on mount - when `entries` is omitted, tracking `loading` / `ready` / `error` state and - cancelling on unmount; explicit `entries` skip the load. - -### Constraints - -- Peer-depends on React 18+; the host must import `styles.css`. -- `DirectoryCard` is one level deep — sub-directory rows route through - `files:visualize` rather than expanding in place. -- Command-firing components require the workspace `Commands` adapter (via - `useAppWorkspace`). +### Bad input is shown, not hidden -### Dependencies +Specs often come from model output, so ids and props cannot be trusted. -`@statewalker/inline.core` (spec/descriptor types, `inlineComponentSlot`), -`@statewalker/ui.view.react` (`useAdapter`, `useKeyedSlot`, `useAppWorkspace`), -`@statewalker/mime.core` (`VisualizeFileCommand`), `@statewalker/workspace.core` -(`LoadDirectoryCommand`, `DirectoryEntry`, `getWorkspace`), -`@statewalker/workspace.view.react`, `shared-commands` (`Commands`), -`shared-registry`, `shared-slots` (`defineKeyedSlot`, `Slots`). +- Unknown id: `` renders `Unknown inline component: `. +- Props of the wrong shape: the component renders `: invalid props` + (for example `LineChart: invalid props`). -## Related +### `action-button` calls any command by name + +The button builds a command declaration from the `command` string at click +time. A command with no listener does nothing and reports no error. Because +the command name comes from the spec, any registered command can be triggered +by content that reaches ``. + +### Components added later still render + +`` reads the renderers slot with `useKeyedSlot`, so a component +registered after the spec is on screen replaces the "Unknown inline component" +chip without a remount. + +### Other details + +- `line-chart` draws an SVG `` scaled between the min and max values; no chart library. +- `directory-card` shows one level. Sub-folder rows call `files:visualize` + instead of expanding. + +### Dependencies -- `@statewalker/inline.core` — the paired React-free - logic fragment (spec/descriptor contract + discoverability slot); the `.core` - ↔ `.view.react` pair. -- `@statewalker/mime.core` — owns the - `files:visualize` command the file/directory cards fire. +- `@statewalker/inline.core` — `InlineContentSpec`, `inlineComponentSlot`. +- `@statewalker/mime.core` — `VisualizeFileCommand`. +- `@statewalker/workspace.core` — `LoadDirectoryCommand`, `getWorkspace`. +- `@statewalker/ui.view.react` — hooks. +- `@statewalker/shared-commands`, `@statewalker/shared-slots`, `@statewalker/shared-registry` — command bus and slots. +- `@statewalker/workspace.view.react` — declared but not imported by the source. ## License -MIT — see the monorepo root `LICENSE`. +MIT diff --git a/packages/mime.view.image/README.md b/packages/mime.view.image/README.md index 064bbd32..a89dee2c 100644 --- a/packages/mime.view.image/README.md +++ b/packages/mime.view.image/README.md @@ -2,19 +2,18 @@ ## What it is -The React renderer fragment for image files. It contributes an `image/*` -`MimeRenderer` to the `files:mime-renderers` slot of -`@statewalker/mime.core`, registers the -`image-viewer` json-render catalog, and binds the `ImageView` component that -loads a file and renders it as an `` in a dock panel. +A view fragment that opens image files in a dock tab. It adds a +`MimeRenderer` for `image/*` to the `files:mime-renderers` slot of +`@statewalker/mime.core`, registers the `image-viewer` json-render catalog, and +binds its `ImageView` component, which loads the file and +shows it in an ``. ## Why it exists -`mime.core` dispatches a URI to whichever renderer claims its MIME type, but it -holds no React and no concrete viewer. This package is the image half of that -split (ADR-0002): the catalog/spec/ids logic plus the React component that turns -a workspace file into a rendered image. Opening an `image/*` file via -`files:visualize` lands here. +`@statewalker/mime.core` sends a file URI to whichever renderer claims its +MIME type, but contains no viewers. This package is the image viewer. After it is +activated, `files:visualize` (`VisualizeFileCommand`) on a file of type `image/*` +opens a tab here. ## How to use @@ -22,86 +21,88 @@ a workspace file into a rendered image. Opening an `image/*` file via pnpm add @statewalker/mime.view.image ``` -Boot the fragment (default export, also at `@statewalker/mime.view.image/fragment`) -and import its styles once at app start: +Peer dependencies: `react` and `react-dom` (`>=18`). Browser only (uses +`Blob` and `URL.createObjectURL`). + +| Import | Provides | +| --- | --- | +| `@statewalker/mime.view.image` | Catalog and id helpers listed below; default export is the fragment init | +| `@statewalker/mime.view.image/fragment` | Default export: `init(ctx)` returning `() => Promise` | +| `@statewalker/mime.view.image/styles` | Tailwind v4 `@source` globs | ```ts -import initImageViewer from "@statewalker/mime.view.image"; import "@statewalker/mime.view.image/styles"; +import init from "@statewalker/mime.view.image/fragment"; -const cleanup = initImageViewer(ctx); // ctx → getWorkspace(ctx) +const cleanup = init(ctx); // after the mime.core, render.core and shell.core logic fragments ``` -`init` registers the catalog, provides the `image/*` renderer, restores any -`image-viewer:` panels from the persisted dock layout, and adds a `FileImage` -dock-tab icon. After that, `files:visualize({ uri })` on an image file opens it. +The init registers the catalog, provides the `image/*` renderer, adds a `FileImage` +icon for tabs whose id starts with `image-viewer:`, and, each time the workspace +opens, creates specs for `image-viewer:` tabs in the saved layout. ## Examples -The contributed renderer and the spec helpers (also re-exported for hosts that -pre-allocate panels): +### Open a file ```ts -import { - IMAGE_VIEWER_CATALOG_ID, // "image-viewer" - makeImageSpec, - imageViewerPanelId, - imageViewerSpecId, -} from "@statewalker/mime.view.image"; +import { VisualizeFileCommand } from "@statewalker/mime.core"; + +await commands.call(VisualizeFileCommand, { uri: "file:///photos/cat.png" }).promise; +``` +### Build the panel plan yourself + +```ts +import { IMAGE_VIEWER_CATALOG_ID, makeImageSpec, imageViewerPanelId, imageViewerSpecId } from "@statewalker/mime.view.image"; + +const uri = "file:///photos/cat.png"; const plan = { - catalogId: IMAGE_VIEWER_CATALOG_ID, - spec: makeImageSpec("file:///photos/cat.png"), - panelId: imageViewerPanelId("file:///photos/cat.png"), - specId: imageViewerSpecId("file:///photos/cat.png"), + catalogId: IMAGE_VIEWER_CATALOG_ID, // "image-viewer" + spec: makeImageSpec(uri), + panelId: imageViewerPanelId(uri), + specId: imageViewerSpecId(uri), }; ``` -`ImageView` itself is internal — it is bound to the catalog's `ImageView` -component type by `init` and is not a public export. +This is what the registered `MimeRenderer.buildPanel(uri)` returns. Panel +and spec ids are derived from the URI, so opening the same file again focuses +the existing tab. + +Public exports: `imageViewerCatalog` (the typed catalog), `IMAGE_VIEWER_CATALOG_ID`, `makeImageSpec`, `imageViewerPanelId`, `imageViewerSpecId`. +The `ImageView` component itself is not exported. ## Internals -### Architectural decisions +### Loading -- **Catalog + binding in one fragment.** The typed `imageViewerCatalog` (schema: - `{ uri: string }`) and its React binding are registered together; the spec - carries only the URI so `SpecStore` patches stay cheap. -- **URI-keyed deterministic ids.** `imageViewerPanelId` / `imageViewerSpecId` - derive from the URI, so reopening a file focuses the existing tab. -- **Layout restore on boot.** `restorePanelSpecsFromLayout` pre-allocates specs - for any `image-viewer:` tabs in the persisted dock layout so DockView's - `fromJSON()` finds a spec rather than a missing-panel placeholder. +`ImageView` calls `LoadFileCommand` (`files:load-file`) on mount and keeps a +loading / ready / error state. A failed load shows `Failed to load file` and +the error message in the tab. The bytes are wrapped in a `Blob` with the loaded MIME type (`image/png` when unknown) and shown through a `blob:` URL, which is revoked on unmount or when the URI changes. -### Algorithms +### Why specs are created on workspace open -`ImageView` lazy-loads the file via `LoadFileCommand` (`files:load-file`) on -mount, wraps the bytes in a `Blob` using the loaded MIME type (default -`image/png`), and renders the resulting `blob:` URL. The object URL is revoked -on unmount or URI change to avoid leaks. State is a three-way -loading / ready / error union. +The dock restores tabs from the saved layout (read from the `LayoutStore` +adapter). A restored tab without a spec shows `Spec is missing.` The +init therefore creates a spec for every saved `image-viewer:` tab when the workspace +opens. The spec holds only `{ uri }`; the component reads the file itself. ### Constraints -- Renders raster and SVG images the browser can display in an ``; no - zoom/pan controls. -- Requires `globalThis.localStorage` for layout restore and a browser `URL` / - `Blob` runtime — this is a browser fragment. +- Shows what the browser can display in an `` (raster formats and SVG). No zoom or pan. +- The whole file is read into memory. ### Dependencies -`@statewalker/mime.core` (`mimeRenderersSlot`), `@json-render/core` + -`@statewalker/render.core` / `@statewalker/render.view.react` (catalog, specs, -registry), `@statewalker/shell.view.react` (`dockTabIconSlot`), -`@statewalker/workspace.core` / `.view.react` (`Workspace`, `LoadFileCommand`, -`useAppWorkspace`), `@statewalker/ui.view.react`, `lucide-react` (`FileImage`), -`zod`. React 18+ is a peer dependency. - -## Related - -- `@statewalker/mime.core` — the dispatch core this builds on. -- `@statewalker/mime.view.markdown`, `@statewalker/mime.view.pdf`, `@statewalker/mime.view.video` — sibling renderers. +- `@statewalker/mime.core` — `mimeRenderersSlot`. +- `@statewalker/render.core`, `@statewalker/render.view.react`, `@json-render/core`, `zod` — catalog, specs, `SpecStore`, `LayoutStore`, `defineRegistry`. +- `@statewalker/shell.view.react` — `dockTabIconSlot`. +- `@statewalker/workspace.core` — `LoadFileCommand`, `getWorkspace`. +- `@statewalker/ui.view.react` — `useAppWorkspace`. +- `@statewalker/shared-commands`, `@statewalker/shared-slots`, `@statewalker/shared-registry` — fragment wiring. +- `lucide-react` — the `FileImage` icon. +- `@statewalker/workspace.view.react` — declared but not imported by the source. ## License -MIT — see the monorepo root `LICENSE`. +MIT diff --git a/packages/mime.view.markdown/README.md b/packages/mime.view.markdown/README.md index aeb10989..8c9327d5 100644 --- a/packages/mime.view.markdown/README.md +++ b/packages/mime.view.markdown/README.md @@ -2,19 +2,19 @@ ## What it is -The React renderer fragment for Markdown files. It contributes a `text/markdown` -`MimeRenderer` to the `files:mime-renderers` slot of -`@statewalker/mime.core`, registers the -`markdown-viewer` json-render catalog, and binds the `MarkdownView` component -that loads a file, decodes it to text, and renders it with the shared -`Markdown` component in a dock panel. +A view fragment that opens Markdown files in a dock tab, plus the `` +component it renders them with. The fragment adds a `MimeRenderer` for +`text/markdown` to the `files:mime-renderers` slot of `@statewalker/mime.core` +and registers the `markdown-viewer` json-render catalog. `` renders +a Markdown string with GitHub-flavored Markdown, line breaks kept, and +syntax-highlighted code blocks. ## Why it exists -`mime.core` dispatches a URI to whichever renderer claims its MIME type but -holds no React and no concrete viewer. This package is the Markdown half of that -split (ADR-0002). Opening a `text/markdown` file via `files:visualize` lands -here, where the bytes are decoded and rendered as formatted prose. +`@statewalker/mime.core` sends a file URI to whichever renderer claims its +MIME type, but contains no viewers. This package is the Markdown viewer. The +`` component is exported so other views (for example chat output) +render Markdown the same way as files. ## How to use @@ -22,91 +22,116 @@ here, where the bytes are decoded and rendered as formatted prose. pnpm add @statewalker/mime.view.markdown ``` -Boot the fragment (default export, also at -`@statewalker/mime.view.markdown/fragment`) and import its styles once at app -start: +Peer dependencies: `react` and `react-dom` (`>=18`). + +| Import | Provides | +| --- | --- | +| `@statewalker/mime.view.markdown` | `Markdown`, `MarkdownProps`, the catalog and id helpers; default export is the fragment init | +| `@statewalker/mime.view.markdown/fragment` | Default export: `init(ctx)` returning `() => Promise` | +| `@statewalker/mime.view.markdown/styles` | Tailwind v4 `@source` globs | ```ts -import initMarkdownViewer from "@statewalker/mime.view.markdown"; import "@statewalker/mime.view.markdown/styles"; +import initMarkdownViewer from "@statewalker/mime.view.markdown/fragment"; -const cleanup = initMarkdownViewer(ctx); // ctx → getWorkspace(ctx) +const cleanup = initMarkdownViewer(ctx); // after the mime.core, render.core and shell.core logic fragments ``` -`init` registers the catalog, provides the `text/markdown` renderer, restores -any `markdown-viewer:` panels from the persisted dock layout, and adds a -`FileText` dock-tab icon. After that, `files:visualize({ uri })` on a `.md` file -opens it. +The init registers the catalog, provides the `text/markdown` renderer, adds a +`FileText` icon for tabs whose id starts with `markdown-viewer:`, and, each +time the workspace opens, creates specs for `markdown-viewer:` tabs in the +saved layout. ## Examples -The spec helpers (re-exported for hosts that pre-allocate panels): +### Render a Markdown string + +```tsx +import { Markdown } from "@statewalker/mime.view.markdown"; + +{"# Title\n\n- one\n- two\n\n```ts\nconst x = 1;\n```"}; +``` + +`MarkdownProps`: + +- `children: string` — the Markdown source. +- `id?: string` — prefix for block keys; a `useId()` value by default. +- `className?: string` — class of the wrapping `
`. +- `components?: Partial` — `react-markdown` component overrides, + merged over the built-in `code` and `pre`. +- `remarkPlugins?: PluggableList` — added after `remark-gfm` and `remark-breaks`. +- `urlTransform?: UrlTransform` — passed to `react-markdown`; its default + applies when omitted. + +### Open a file + +```ts +import { VisualizeFileCommand } from "@statewalker/mime.core"; + +await commands.call(VisualizeFileCommand, { uri: "file:///docs/readme.md" }).promise; +``` + +### Build the panel plan yourself ```ts import { - MARKDOWN_VIEWER_CATALOG_ID, // "markdown-viewer" + MARKDOWN_VIEWER_CATALOG_ID, makeMarkdownSpec, markdownViewerPanelId, markdownViewerSpecId, } from "@statewalker/mime.view.markdown"; +const uri = "file:///docs/readme.md"; const plan = { - catalogId: MARKDOWN_VIEWER_CATALOG_ID, - spec: makeMarkdownSpec("file:///docs/readme.md"), - panelId: markdownViewerPanelId("file:///docs/readme.md"), - specId: markdownViewerSpecId("file:///docs/readme.md"), + catalogId: MARKDOWN_VIEWER_CATALOG_ID, // "markdown-viewer" + spec: makeMarkdownSpec(uri), + panelId: markdownViewerPanelId(uri), + specId: markdownViewerSpecId(uri), }; ``` -`MarkdownView` is internal — `init` binds it to the catalog's `MarkdownView` -component type; it is not a public export. +This is what the registered `MimeRenderer.buildPanel(uri)` returns. Ids are +derived from the URI, so opening the same file again focuses the existing tab. +`markdownViewerCatalog` (the typed catalog) is exported too. ## Internals -### Architectural decisions +### Why the text is split into blocks -- **Renderer-only fragment.** The paired logic fragment was dropped; the inert - `text/markdown` MIME-pattern data lives inline in `init`, registered alongside - the catalog binding. -- **Reuse the chat Markdown renderer.** Rendering delegates to `Markdown` from - `@repo/chat-mini.chat-react`, so Markdown styling stays consistent with chat - output rather than duplicating a parser. -- **URI-only spec.** The spec carries just the URI; the component fetches the - text itself, keeping `SpecStore` patches cheap and ids deterministic so - reopening a file focuses the existing tab. -- **Layout restore on boot.** `restorePanelSpecsFromLayout` pre-allocates specs - for persisted `markdown-viewer:` tabs so DockView's `fromJSON()` finds them. +`` splits the source into top-level blocks with `marked.lexer` and +renders each block with a memoized `react-markdown` instance. When the text +grows (for example while a chat answer streams in), the text is split again, +but only blocks whose text changed go through `react-markdown` again. -### Algorithms +### Code blocks -`MarkdownView` lazy-loads the file via `LoadFileCommand` (`files:load-file`) on -mount, decodes the bytes with `TextDecoder`, and renders the string through -`` inside a `prose` container. State is a three-way -loading / ready / error union; the effect cancels cleanly on unmount or URI -change. +Fenced code is highlighted with `shiki` (`codeToHtml`, theme `github-light`) +asynchronously, after the first render. The language comes from the +`language-*` class; without one, `plaintext`. Inline code (a code element that +starts and ends on the same line) is a styled ``. The theme does not +change in dark mode. -### Constraints +### File tabs -- Renders Markdown as decoded UTF-8 text; raw bytes are not validated as - Markdown. -- Requires `globalThis.localStorage` for layout restore — this is a browser - fragment. +`MarkdownView` calls `LoadFileCommand` (`files:load-file`), decodes the bytes +as UTF-8 with `TextDecoder`, and renders them inside a `prose` container. A +failed load shows `Failed to load file` and the error message. The dock +restores tabs from the saved layout (read from the `LayoutStore` adapter); a +restored tab without a spec would show `Spec is missing.`, so the init +creates specs for saved `markdown-viewer:` tabs when the workspace opens. ### Dependencies -`@statewalker/mime.core` (`mimeRenderersSlot`), `@repo/chat-mini.chat-react` -(`Markdown`), `@json-render/core` + `@statewalker/render.core` / -`@statewalker/render.view.react` (catalog, specs, registry), -`@statewalker/shell.view.react` (`dockTabIconSlot`), -`@statewalker/workspace.core` / `.view.react` (`Workspace`, `LoadFileCommand`, -`useAppWorkspace`), `@statewalker/ui.view.react`, `lucide-react` (`FileText`), -`zod`. React 18+ is a peer dependency. - -## Related - -- `@statewalker/mime.core` — the dispatch core this builds on. -- `@statewalker/mime.view.image`, `@statewalker/mime.view.pdf`, `@statewalker/mime.view.video` — sibling renderers. +- `react-markdown`, `remark-gfm`, `remark-breaks`, `marked`, `shiki` — parsing, rendering, highlighting. +- `@statewalker/mime.core` — `mimeRenderersSlot`. +- `@statewalker/render.core`, `@statewalker/render.view.react`, `@json-render/core`, `zod` — catalog, specs, `SpecStore`, `LayoutStore`. +- `@statewalker/shell.view.react` — `dockTabIconSlot`. +- `@statewalker/workspace.core` — `LoadFileCommand`, `getWorkspace`. +- `@statewalker/ui.view.react`, `@statewalker/ui.view.shadcn` — `useAppWorkspace`, `cn()`. +- `@statewalker/shared-commands`, `@statewalker/shared-slots`, `@statewalker/shared-registry` — fragment wiring. +- `lucide-react` — the `FileText` icon. +- `@statewalker/workspace.view.react` — declared but not imported by the source. ## License -MIT — see the monorepo root `LICENSE`. +MIT diff --git a/packages/mime.view.pdf/README.md b/packages/mime.view.pdf/README.md index c9956c15..f71b9e82 100644 --- a/packages/mime.view.pdf/README.md +++ b/packages/mime.view.pdf/README.md @@ -2,18 +2,18 @@ ## What it is -The React renderer fragment for PDF files. It contributes an `application/pdf` -`MimeRenderer` to the `files:mime-renderers` slot of -`@statewalker/mime.core`, registers the `pdf-viewer` -json-render catalog, and binds the `PdfView` component that loads a file and -hands it to the browser's built-in PDF viewer via an `` in a dock panel. +A view fragment that opens PDF files in a dock tab. It adds a +`MimeRenderer` for `application/pdf` to the `files:mime-renderers` slot of +`@statewalker/mime.core`, registers the `pdf-viewer` json-render catalog, and +binds its `PdfView` component, which loads the file and +shows it with the browser's built-in PDF viewer (``). ## Why it exists -`mime.core` dispatches a URI to whichever renderer claims its MIME type but -holds no React and no concrete viewer. This package is the PDF half of that -split (ADR-0002). Opening an `application/pdf` file via `files:visualize` lands -here. +`@statewalker/mime.core` sends a file URI to whichever renderer claims its +MIME type, but contains no viewers. This package is the PDF viewer. After it is +activated, `files:visualize` (`VisualizeFileCommand`) on a file of type `application/pdf` +opens a tab here. ## How to use @@ -21,85 +21,89 @@ here. pnpm add @statewalker/mime.view.pdf ``` -Boot the fragment (default export, also at `@statewalker/mime.view.pdf/fragment`) -and import its styles once at app start: +Peer dependencies: `react` and `react-dom` (`>=18`). Browser only (uses +`Blob` and `URL.createObjectURL`). + +| Import | Provides | +| --- | --- | +| `@statewalker/mime.view.pdf` | Catalog and id helpers listed below; default export is the fragment init | +| `@statewalker/mime.view.pdf/fragment` | Default export: `init(ctx)` returning `() => Promise` | +| `@statewalker/mime.view.pdf/styles` | Tailwind v4 `@source` globs | ```ts -import initPdfViewer from "@statewalker/mime.view.pdf"; import "@statewalker/mime.view.pdf/styles"; +import init from "@statewalker/mime.view.pdf/fragment"; -const cleanup = initPdfViewer(ctx); // ctx → getWorkspace(ctx) +const cleanup = init(ctx); // after the mime.core, render.core and shell.core logic fragments ``` -`init` registers the catalog, provides the `application/pdf` renderer, restores -any `pdf-viewer:` panels from the persisted dock layout, and adds a `FileText` -dock-tab icon. After that, `files:visualize({ uri })` on a `.pdf` file opens it. +The init registers the catalog, provides the `application/pdf` renderer, adds a `FileText` +icon for tabs whose id starts with `pdf-viewer:`, and, each time the workspace +opens, creates specs for `pdf-viewer:` tabs in the saved layout. ## Examples -The spec helpers (re-exported for hosts that pre-allocate panels): +### Open a file ```ts -import { - PDF_VIEWER_CATALOG_ID, // "pdf-viewer" - makePdfSpec, - pdfViewerPanelId, - pdfViewerSpecId, -} from "@statewalker/mime.view.pdf"; +import { VisualizeFileCommand } from "@statewalker/mime.core"; + +await commands.call(VisualizeFileCommand, { uri: "file:///docs/report.pdf" }).promise; +``` +### Build the panel plan yourself + +```ts +import { PDF_VIEWER_CATALOG_ID, makePdfSpec, pdfViewerPanelId, pdfViewerSpecId } from "@statewalker/mime.view.pdf"; + +const uri = "file:///docs/report.pdf"; const plan = { - catalogId: PDF_VIEWER_CATALOG_ID, - spec: makePdfSpec("file:///docs/report.pdf"), - panelId: pdfViewerPanelId("file:///docs/report.pdf"), - specId: pdfViewerSpecId("file:///docs/report.pdf"), + catalogId: PDF_VIEWER_CATALOG_ID, // "pdf-viewer" + spec: makePdfSpec(uri), + panelId: pdfViewerPanelId(uri), + specId: pdfViewerSpecId(uri), }; ``` -`PdfView` is internal — `init` binds it to the catalog's `PdfView` component -type; it is not a public export. +This is what the registered `MimeRenderer.buildPanel(uri)` returns. Panel +and spec ids are derived from the URI, so opening the same file again focuses +the existing tab. + +Public exports: `pdfViewerCatalog` (the typed catalog), `PDF_VIEWER_CATALOG_ID`, `makePdfSpec`, `pdfViewerPanelId`, `pdfViewerSpecId`. +The `PdfView` component itself is not exported. ## Internals -### Architectural decisions +### Loading -- **Browser-native viewer.** Rendering delegates to an ``, - i.e. the browser's built-in PDF viewer. PDF.js is explicitly out of scope for - v1 — no rendering engine is bundled. -- **URI-only spec, deterministic ids.** The spec carries just the URI; - `pdfViewerPanelId` / `pdfViewerSpecId` key off it so reopening a file focuses - the existing tab. -- **Layout restore on boot.** `restorePanelSpecsFromLayout` pre-allocates specs - for persisted `pdf-viewer:` tabs so DockView's `fromJSON()` finds a spec - instead of a missing-panel placeholder when the React tree mounts. +`PdfView` calls `LoadFileCommand` (`files:load-file`) on mount and keeps a +loading / ready / error state. A failed load shows `Failed to load file` and +the error message in the tab. The bytes are wrapped in a `Blob` of type `application/pdf` and passed to the `` as a `blob:` URL, which is revoked on unmount or when the URI changes. -### Algorithms +### Why specs are created on workspace open -`PdfView` lazy-loads the file via `LoadFileCommand` (`files:load-file`) on -mount, wraps the bytes in a `Blob` typed `application/pdf`, and points the -`` at the resulting `blob:` URL. The object URL is revoked on unmount or -URI change. State is a three-way loading / ready / error union. +The dock restores tabs from the saved layout (read from the `LayoutStore` +adapter). A restored tab without a spec shows `Spec is missing.` The +init therefore creates a spec for every saved `pdf-viewer:` tab when the workspace +opens. The spec holds only `{ uri }`; the component reads the file itself. ### Constraints -- Relies on the host browser shipping a PDF viewer; no in-app annotation, - search, or page navigation beyond what the browser provides. -- Requires `globalThis.localStorage` for layout restore and a browser `URL` / - `Blob` runtime — this is a browser fragment. +- No PDF engine is bundled. A browser without a built-in PDF viewer (many mobile browsers, some locked-down setups) shows an empty area or a download prompt instead of the document. +- Search, annotation and page navigation are whatever the browser's viewer offers. +- The whole file is read into memory. ### Dependencies -`@statewalker/mime.core` (`mimeRenderersSlot`), `@json-render/core` + -`@statewalker/render.core` / `@statewalker/render.view.react` (catalog, specs, -registry), `@statewalker/shell.view.react` (`dockTabIconSlot`), -`@statewalker/workspace.core` / `.view.react` (`Workspace`, `LoadFileCommand`, -`useAppWorkspace`), `@statewalker/ui.view.react`, `lucide-react` (`FileText`), -`zod`. React 18+ is a peer dependency. - -## Related - -- `@statewalker/mime.core` — the dispatch core this builds on. -- `@statewalker/mime.view.image`, `@statewalker/mime.view.markdown`, `@statewalker/mime.view.video` — sibling renderers. +- `@statewalker/mime.core` — `mimeRenderersSlot`. +- `@statewalker/render.core`, `@statewalker/render.view.react`, `@json-render/core`, `zod` — catalog, specs, `SpecStore`, `LayoutStore`, `defineRegistry`. +- `@statewalker/shell.view.react` — `dockTabIconSlot`. +- `@statewalker/workspace.core` — `LoadFileCommand`, `getWorkspace`. +- `@statewalker/ui.view.react` — `useAppWorkspace`. +- `@statewalker/shared-commands`, `@statewalker/shared-slots`, `@statewalker/shared-registry` — fragment wiring. +- `lucide-react` — the `FileText` icon. +- `@statewalker/workspace.view.react` — declared but not imported by the source. ## License -MIT — see the monorepo root `LICENSE`. +MIT diff --git a/packages/mime.view.video/README.md b/packages/mime.view.video/README.md index 7deea18d..f43f0c11 100644 --- a/packages/mime.view.video/README.md +++ b/packages/mime.view.video/README.md @@ -2,18 +2,18 @@ ## What it is -The React renderer fragment for video files. It contributes a `video/*` -`MimeRenderer` to the `files:mime-renderers` slot of -`@statewalker/mime.core`, registers the -`video-viewer` json-render catalog, and binds the `VideoView` component that -loads a file and plays it with an HTML `