From 8e48b082b35b5bd6b55ac8f82f158af4ff3b3e65 Mon Sep 17 00:00:00 2001 From: Aymeric Rabot Date: Mon, 12 Oct 2026 15:21:13 -0400 Subject: [PATCH 1/2] feat(editor): a plugin's start screen in a host chat; IconRefGlyph exported PluginChatExtension.start is a lazy component the host's chat shows while its thread is empty. It acts through ChatStartProps only: send a message (pictures by reference), attach files to the composer, or requirePro() before a step that needs the level the plugin's tools run at. IconRefGlyph and IconRefImage are exported so a host renders registry icons the way the editor does. Co-Authored-By: Claude Opus 5.5 --- packages/editor/src/index.tsx | 2 ++ packages/editor/src/lib/plugin-chat.ts | 14 ++++++++++++++ wiki/architecture/plugin-authoring.md | 2 ++ 3 files changed, 18 insertions(+) diff --git a/packages/editor/src/index.tsx b/packages/editor/src/index.tsx index d8888cdb66..2722c069d9 100644 --- a/packages/editor/src/index.tsx +++ b/packages/editor/src/index.tsx @@ -253,6 +253,7 @@ export { TerrainSculptPanel } from './components/ui/controls/terrain-sculpt-pane export { ToggleControl } from './components/ui/controls/toggle-control' export { ToolOptionsPanel } from './components/ui/controls/tool-options-panel' export { FloatingLevelSelector } from './components/ui/floating-level-selector' +export { IconRefGlyph, IconRefImage } from './components/ui/icon-ref' export { CATALOG_ITEMS } from './components/ui/item-catalog/catalog-items' // A base plate's panel (one connected footprint's floor and foundation); the // slab kind's panel hands base plates to it. @@ -725,6 +726,7 @@ export { resolvePlanarCursorPosition, } from './lib/planar-cursor-placement' export type { + ChatStartProps, ChatToolCardProps, ChatToolOutput, PluginChatExtension, diff --git a/packages/editor/src/lib/plugin-chat.ts b/packages/editor/src/lib/plugin-chat.ts index d7edfe821c..956e1485a6 100644 --- a/packages/editor/src/lib/plugin-chat.ts +++ b/packages/editor/src/lib/plugin-chat.ts @@ -28,6 +28,18 @@ export type ChatToolCardProps = { } } +/** + * What a plugin's start screen does through the host's chat, before anything is said. `send` sends + * a message now, its pictures by reference (a host-stored image); `attach` puts files on the next + * message, in the composer; `requirePro` moves the chat to the level that runs the plugin's tools, + * and answers false, after offering the upgrade, when the person's plan does not include it. + */ +export type ChatStartProps = { + send(text: string, images?: readonly { url: string; name: string }[]): void + attach(files: readonly File[]): void + requirePro(): boolean +} + /** * A plugin's contribution to a host's chat, exported from its browser entry beside its panels and * views; its tool contracts are its `./agent-tools` export. Which tools a turn offers, and to whom, @@ -47,4 +59,6 @@ export type PluginChatExtension = { actions?: readonly { id: string; label: string; hint: string; prompt: string }[] /** File extensions or MIME types the plugin's tools read, which the composer then accepts. */ accepts?: readonly string[] + /** What the chat shows while its thread is empty, loaded lazily. */ + start?: () => Promise<{ default: ComponentType }> } diff --git a/wiki/architecture/plugin-authoring.md b/wiki/architecture/plugin-authoring.md index 1579bc093c..bcc17e4866 100644 --- a/wiki/architecture/plugin-authoring.md +++ b/wiki/architecture/plugin-authoring.md @@ -358,6 +358,7 @@ export const signageChat: PluginChatExtension = { cards: { signage_schedule: () => import('./schedule-card') }, actions: [{ id: 'exit-signs', label: 'Exit signs', hint: 'Sign every exit', prompt: 'Place exit signs on every level.' }], accepts: ['.csv'], + start: () => import('./chat-start'), } ``` @@ -368,6 +369,7 @@ export const signageChat: PluginChatExtension = { - **The host decides.** Which tools a turn offers, to whom and at what cost, whether a `chatOnly` request is honoured, and how far `timeoutMs` may go are host policy, like a panel's `access`. Being in `installedPlugins` is necessary, not sufficient. These packages define the shapes; a host keeps its own catalog of the plugins it serves. - **Trusted code.** These exports run with the application's privileges: a zod schema is code, and an executor or a card runs in the page. Hosts load reviewed, pinned plugins. The purity rule keeps contracts loadable in a sandboxed workflow; it does not isolate them. - **Cards and outputs.** A card renders from the persisted call alone (`toolName`, `input`, `output`, `state`), so a reloaded chat shows the same card; its `actions` open a view, select nodes or send a prompt. A `ChatToolOutput` carries images by reference (`ref`), never bytes. `actions` add entries to the composer's `/` menu; `accepts` lists the file types the plugin's tools read. +- **A start screen.** `start` is what the host's chat shows while its thread is empty. It acts only through its `ChatStartProps`: `send` a message (pictures by reference), `attach` files to the composer, or `requirePro()` before a step that needs the level the plugin's tools run at, which answers false after the host offered the upgrade. Render registry icons with `IconRefGlyph`, as the editor does. The rules both surfaces follow for these tools are in [agent surfaces](agent-surfaces.md#plugin-tools). From c3481011992016e7ddef145f183923a30ea7e7d4 Mon Sep 17 00:00:00 2001 From: Aymeric Rabot Date: Tue, 13 Oct 2026 00:27:26 -0400 Subject: [PATCH 2/2] fix(editor): a start's send and attach run at the plugin's level; requirePro only asks requirePro() moved the chat to Pro as a side effect, so a step that only needs the plan (opening a picture studio) changed the effort the next message ran at. It now only asks, offering the upgrade on a plan without it; send and attach are what run at the level of the plugin's tools. The doc says that "Pro" is the hosted product's word for that level. Co-Authored-By: Claude Opus 5.5 --- packages/editor/src/lib/plugin-chat.ts | 7 ++++--- wiki/architecture/plugin-authoring.md | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/editor/src/lib/plugin-chat.ts b/packages/editor/src/lib/plugin-chat.ts index 956e1485a6..9833c2a612 100644 --- a/packages/editor/src/lib/plugin-chat.ts +++ b/packages/editor/src/lib/plugin-chat.ts @@ -30,9 +30,10 @@ export type ChatToolCardProps = { /** * What a plugin's start screen does through the host's chat, before anything is said. `send` sends - * a message now, its pictures by reference (a host-stored image); `attach` puts files on the next - * message, in the composer; `requirePro` moves the chat to the level that runs the plugin's tools, - * and answers false, after offering the upgrade, when the person's plan does not include it. + * a message now, its pictures by reference (a host-stored image), and `attach` puts files on the + * next message, in the composer: both at the level that runs the plugin's tools. `requirePro` only + * asks whether the person's plan includes that level ("Pro" is the hosted product's name for it): + * it answers false after offering the upgrade, and changes nothing in the chat. */ export type ChatStartProps = { send(text: string, images?: readonly { url: string; name: string }[]): void diff --git a/wiki/architecture/plugin-authoring.md b/wiki/architecture/plugin-authoring.md index bcc17e4866..0b45be1d67 100644 --- a/wiki/architecture/plugin-authoring.md +++ b/wiki/architecture/plugin-authoring.md @@ -369,7 +369,7 @@ export const signageChat: PluginChatExtension = { - **The host decides.** Which tools a turn offers, to whom and at what cost, whether a `chatOnly` request is honoured, and how far `timeoutMs` may go are host policy, like a panel's `access`. Being in `installedPlugins` is necessary, not sufficient. These packages define the shapes; a host keeps its own catalog of the plugins it serves. - **Trusted code.** These exports run with the application's privileges: a zod schema is code, and an executor or a card runs in the page. Hosts load reviewed, pinned plugins. The purity rule keeps contracts loadable in a sandboxed workflow; it does not isolate them. - **Cards and outputs.** A card renders from the persisted call alone (`toolName`, `input`, `output`, `state`), so a reloaded chat shows the same card; its `actions` open a view, select nodes or send a prompt. A `ChatToolOutput` carries images by reference (`ref`), never bytes. `actions` add entries to the composer's `/` menu; `accepts` lists the file types the plugin's tools read. -- **A start screen.** `start` is what the host's chat shows while its thread is empty. It acts only through its `ChatStartProps`: `send` a message (pictures by reference), `attach` files to the composer, or `requirePro()` before a step that needs the level the plugin's tools run at, which answers false after the host offered the upgrade. Render registry icons with `IconRefGlyph`, as the editor does. +- **A start screen.** `start` is what the host's chat shows while its thread is empty. It acts only through its `ChatStartProps`: `send` a message (pictures by reference) or `attach` files to the composer, both at the level the plugin's tools run at, and `requirePro()` before a step that needs that level: it answers false after the host offered the upgrade, and changes nothing in the chat. Render registry icons with `IconRefGlyph`, as the editor does. The rules both surfaces follow for these tools are in [agent surfaces](agent-surfaces.md#plugin-tools).