dsh-generative-ui 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  # dsh-generative-ui
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/dsh-generative-ui)](https://www.npmjs.com/package/dsh-generative-ui)
3
4
  [![pkg.pr.new](https://pkg.pr.new/badge/CNSeniorious000/dsh-generative-ui)](https://pkg.pr.new/~/CNSeniorious000/dsh-generative-ui)
4
5
  [![MIT](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
5
6
 
@@ -16,20 +17,25 @@ The `ui4a` in that fence is the harness this implements — **UI for Agent**, fr
16
17
 
17
18
  ## Install
18
19
 
19
- **Not on npm yet**, so install the preview build published on every push:
20
+ ```sh
21
+ dsh plugin --profile web add dsh-generative-ui
22
+ ```
23
+
24
+ Every release is published from CI over OIDC, so the tarball carries npm provenance. For an
25
+ unreleased commit there is a preview build of every push:
20
26
 
21
27
  ```sh
22
28
  dsh plugin --profile web add https://pkg.pr.new/CNSeniorious000/dsh-generative-ui@main
23
29
  ```
24
30
 
25
- Or, working on it locally, point the profile at your checkout — `lib/` is built by `prepare`, so
31
+ And working on it locally, point the profile at your checkout — `lib/` is built by `prepare`, so
26
32
  the profile does not care that this package uses bun and dsh uses pnpm:
27
33
 
28
34
  ```sh
29
35
  dsh plugin --profile web add link:/path/to/dsh-generative-ui
30
36
  ```
31
37
 
32
- `dsh plugin` forwards to the profile's package manager, so either form installs the package. Mounting it also takes one line in `~/.dsh/profiles/web/package.json` — the profile's bundle list is what dsh actually boots:
38
+ `dsh plugin` forwards to the profile's package manager, so any of these installs the package. Mounting it also takes one line in `~/.dsh/profiles/web/package.json` — the profile's bundle list is what dsh actually boots:
33
39
 
34
40
  ```json
35
41
  {
@@ -0,0 +1,6 @@
1
+ export type CanvasLauncherProps = {
2
+ /** Every canvas in the workspace, including ones this session never wrote. */
3
+ ids: readonly string[];
4
+ onOpen: (id: string) => void;
5
+ };
6
+ export declare function CanvasLauncher({ ids, onOpen }: CanvasLauncherProps): import("react").JSX.Element;
@@ -0,0 +1,88 @@
1
+ export type Canvas = {
2
+ id: string;
3
+ code: string;
4
+ streaming: boolean;
5
+ };
6
+ export type CanvasPanelProps = {
7
+ canvases: readonly Canvas[];
8
+ /** Every canvas in the workspace, so the header can reach ones this session never wrote. */
9
+ offerable: readonly string[];
10
+ /** The session's workspace, needed to fetch a canvas's sub-page files. */
11
+ cwd: string | undefined;
12
+ onOpen: (id: string) => void;
13
+ onClose: () => void;
14
+ /** Reports the panel's width so the host frame can reserve matching space. */
15
+ onWidth: (width: number) => void;
16
+ /** A canvas that failed to compile; see `runtime/report-error.ts`. */
17
+ onCardError?: (message: string, phase: string) => void;
18
+ /** A canvas that painted; see `runtime/report-error.ts`. */
19
+ onCardRendered?: () => void;
20
+ };
21
+ /**
22
+ * Rewrites a canvas's relative sub-page imports into blob URLs before it is compiled.
23
+ *
24
+ * Only for a settled canvas: mid-stream the sibling files are usually not written yet, and a
25
+ * canvas does not stream under the default PTC mode anyway (see CLAUDE.md §3.6). Until the
26
+ * rewrite lands the original source is rendered, which fails exactly as it does today rather
27
+ * than blanking a canvas that was working.
28
+ */
29
+ export declare function useSubPages(cwd: string | undefined, canvas: Canvas | undefined): string;
30
+ /**
31
+ * Revoke every blob URL in a list, exactly once each.
32
+ *
33
+ * Both callers hand it the SAME array — the disposer, and the resolve that lands after it. The
34
+ * array is filled by `inlineSubPages` as it goes, so the two can see different lengths, and a
35
+ * URL revoked twice is harmless while one revoked never is a leak per edit. Emptying the list
36
+ * as it goes makes the pair idempotent regardless of which runs first or what arrived between.
37
+ */
38
+ /**
39
+ * Whether this canvas needs the sub-page pass at all. Extracted from the effect because the
40
+ * mutation audit could not constrain it there — flipping `streaming` or dropping the
41
+ * `importsSibling` check survived every test, and both are load-bearing: resolving mid-stream
42
+ * inlines a prefix that the next frame supersedes, and a canvas with no sibling imports would
43
+ * pay a compile per frame to produce the code it already had.
44
+ *
45
+ * The `undefined` checks stay in the effect: a type predicate narrows only one parameter, and
46
+ * moving them here would cost the compiler its knowledge that `cwd` and `id` are strings after.
47
+ */
48
+ export declare const needsResolve: (code: string, streaming: boolean | undefined) => boolean;
49
+ export declare const revokeAll: (urls: string[]) => void;
50
+ /** Cheap gate: only a canvas that actually writes a relative import pays for the pass. */
51
+ declare const MIN_WIDTH = 320;
52
+ declare const MAX_WIDTH = 720;
53
+ /** Exported for `test/panel-css.test.ts`: the default must be draggable-to. */
54
+ export { MIN_WIDTH as MIN_WIDTH_FOR_TEST, MAX_WIDTH as MAX_WIDTH_FOR_TEST };
55
+ /**
56
+ * Must equal the `--dgu-panel-width` fallback in `panel.css`.
57
+ *
58
+ * The CSS default is what the panel is painted at before React's inline style lands; this is
59
+ * what the resize state starts from. Different values mean the panel visibly jumps on its first
60
+ * frame — a `panel-css.test.ts` assertion holds them together, since a stylesheet cannot import
61
+ * a constant.
62
+ */
63
+ export declare const DEFAULT_WIDTH = 420;
64
+ /**
65
+ * The panel's width for a pointer at `clientX`, clamped to what is usable.
66
+ *
67
+ * Extracted from the drag handler because it is the whole of the arithmetic and none of the
68
+ * DOM: a swapped bound or a flipped subtraction gives a panel that snaps shut or eats the
69
+ * conversation, and the drag itself cannot be exercised without a browser.
70
+ */
71
+ /**
72
+ * Which canvas the panel shows, and which ids the "other canvases" menu offers.
73
+ *
74
+ * Both are pure functions of the props and both decide what the reader is looking at, so they
75
+ * are here rather than inline: the fallback is what covers a selected canvas disappearing
76
+ * mid-stream (no cleanup needed — clearing the state as well would be a second render saying
77
+ * the same thing), and `offerable` minus the tabs is what keeps the menu from listing what is
78
+ * already on screen.
79
+ */
80
+ export declare const activeCanvas: (canvases: readonly Canvas[], activeId: string | null) => Canvas;
81
+ export declare const otherCanvases: (canvases: readonly Canvas[], offerable: readonly string[]) => string[];
82
+ export declare const widthForPointer: (clientX: number, viewportWidth: number) => number;
83
+ /** Drag-to-resize on the panel's left edge, mirroring the host's own invisible hit strip. */
84
+ export declare function useResize(initial: number): {
85
+ width: number;
86
+ start: (event: React.PointerEvent<HTMLDivElement>) => void;
87
+ };
88
+ export declare function CanvasPanel({ canvases, offerable, cwd, onOpen, onClose, onWidth, onCardError, onCardRendered }: CanvasPanelProps): import("react").JSX.Element;
@@ -0,0 +1,45 @@
1
+ import type { Canvas } from "./CanvasPanel.tsx";
2
+ export type ToolCallView = {
3
+ name: string;
4
+ argsRaw: string;
5
+ settled: boolean;
6
+ };
7
+ /**
8
+ * One call in the tree a `tool-call` node owns.
9
+ *
10
+ * A running call carries its arguments inline; a settled one wraps them in `call`. Either
11
+ * may own `subCalls` — `run_code` dispatches the file tools as children, so a canvas
12
+ * written from inside a code block is nested rather than top-level.
13
+ */
14
+ export type CallBlock = {
15
+ kind?: string;
16
+ name?: string;
17
+ argsRaw?: string;
18
+ call?: {
19
+ name?: string;
20
+ argsRaw?: string;
21
+ };
22
+ subCalls?: readonly CallBlock[];
23
+ };
24
+ /**
25
+ * Every call a `tool-call` node contains, parents before children.
26
+ *
27
+ * Flattening rather than reading only the root is what lets a canvas written through
28
+ * `run_code` (or any other dispatching tool) be found: the write is a sub-call, and the
29
+ * root's own arguments are the code that ran, not the file.
30
+ */
31
+ export declare function toolCallsOf(data: unknown): ToolCallView[];
32
+ /**
33
+ * Latest canvas per id, in first-seen order.
34
+ * @param calls - every tool call in the transcript, oldest first.
35
+ */
36
+ export type CollectedCanvases = {
37
+ canvases: Canvas[];
38
+ /**
39
+ * Canvases whose newest change was a patch, mapped to how many patches they have seen.
40
+ * The count is a cache version: the caller re-reads the file when it changes, and
41
+ * reuses what it already has when it does not.
42
+ */
43
+ stale: Map<string, number>;
44
+ };
45
+ export declare function collectCanvases(calls: readonly ToolCallView[]): CollectedCanvases;
@@ -0,0 +1,43 @@
1
+ import { type Canvas } from "./CanvasPanel.tsx";
2
+ import { type ToolCallView } from "./collect.ts";
3
+ export type CanvasHostOptions = {
4
+ /** Every tool call in the current session, oldest first. */
5
+ calls: () => readonly ToolCallView[];
6
+ /** The current session's workspace directory, when it has one. */
7
+ cwd: () => string | undefined;
8
+ /** Identity of the current session; dismissals are remembered against it. */
9
+ sessionId: () => string;
10
+ /**
11
+ * Where a canvas's compile failure goes. Optional because a host may have no chat channel;
12
+ * absent, the error is still painted for the reader and simply reaches nobody else.
13
+ */
14
+ onCardError?: (message: string, phase: string) => void;
15
+ /** A canvas that painted; cancels a deferred error report the next frame made untrue. */
16
+ onCardRendered?: () => void;
17
+ };
18
+ /**
19
+ * A call whose arguments carry executable code rather than a file operation.
20
+ *
21
+ * Matched on the argument text, not the tool name, for the same reason `collect.ts` classifies
22
+ * by shape: the day the host renames `run_code`, a name list stops matching and canvases
23
+ * silently stop appearing.
24
+ *
25
+ * It must also mention the canvases directory. All 29 opaque canvas writes in the corpus do,
26
+ * even the 27 whose path is built from a variable — and without that clause a session doing
27
+ * ordinary shell work re-lists once per command (measured: median 0, but one session hit 94).
28
+ */
29
+ /**
30
+ * What a sweep would paint, as a string. Equal signatures mean nothing to do.
31
+ *
32
+ * The observer fires on every streamed token, so this is what stands between the panel and a
33
+ * React render per token. `code.length` rather than the code: a growing card changes length on
34
+ * every frame, which is exactly when it should repaint, and comparing megabytes of source per
35
+ * token would cost more than the render. The offerable list belongs in it too — closing the
36
+ * last canvas changes nothing about the visible list, and without it the launcher never paints.
37
+ */
38
+ export declare const paintSignature: (canvases: readonly Canvas[], offerable: readonly string[]) => string;
39
+ export declare const OPAQUE_WRITE: RegExp;
40
+ export declare function mountCanvasHost({ calls, cwd, sessionId, onCardError, onCardRendered }: CanvasHostOptions): {
41
+ dispose: () => void;
42
+ show: (id: string) => void;
43
+ };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Injects the panel stylesheet.
3
+ *
4
+ * The `data-plugin` attribute is required, not decorative: the client module loader
5
+ * claims every `style:not([data-plugin])` for whichever plugin is currently
6
+ * materializing, so an unmarked sheet gets adopted by a stranger and torn out with them.
7
+ */
8
+ export declare function injectStyles(css: string): () => void;
9
+ /**
10
+ * Resolves the frame element, waiting for it when the shell has not painted yet.
11
+ * @param onFound - called once with the container; not called if disposed first.
12
+ */
13
+ export declare function whenFrameReady(onFound: (frame: HTMLElement) => void): () => void;
14
+ /**
15
+ * Creates the panel's host element and makes room for it.
16
+ *
17
+ * The frame's own columns are sized to fill the viewport exactly, so an extra flex child
18
+ * is laid out past the right edge no matter where it is inserted — present in the DOM,
19
+ * correctly sized, and entirely off screen (that is what the first attempt did). Instead
20
+ * the panel is fixed to the right edge and the frame is given matching right padding, so
21
+ * the conversation reflows into the remaining width and nothing overlaps.
22
+ *
23
+ * @param frame - the host's AppFrame element.
24
+ * @param width - the panel's current width in pixels.
25
+ */
26
+ export declare function createColumn(frame: HTMLElement): {
27
+ element: HTMLElement;
28
+ setWidth: (width: number) => void;
29
+ remove: () => void;
30
+ };
@@ -0,0 +1 @@
1
+ export declare const PANEL_CSS = "/*\n * The canvas column. Everything visual comes from the host's design tokens, so the panel\n * tracks theme changes for free and never introduces a second palette\n * (docs/web-styling.md: feature packages consume aliases, they do not define colors).\n */\n.dgu-panel {\n --dgu-panel-width: 420px;\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n z-index: 20;\n display: flex;\n flex-direction: column;\n width: var(--dgu-panel-width);\n background: var(--dsw-alias-bg-base);\n border-left: 1px solid var(--dsw-alias-border-l1);\n color: var(--dsw-alias-label-primary);\n font-family: var(--dsw-font-family);\n overflow: hidden;\n}\n\n.dgu-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 0 8px 0 14px;\n height: 44px;\n flex: none;\n}\n\n/* No hairline under either the title bar or the tab strip, and that is the host's own language,\n * not a preference: measured on dsh web's conversation header (`wSkVaW_header`), the computed\n * `border-bottom-color` is `rgba(0, 0, 0, 0)` on a transparent background with no box-shadow \u2014\n * it separates itself from the scroll area with 76px of height and nothing else. Two `border-l1`\n * rules here put lines across a panel that sits inside that same frame, which reads as chrome the\n * app does not otherwise have. */\n\n.dgu-title {\n font-size: var(--dsw-font-s-14-font-size);\n line-height: var(--dsw-font-s-14-line-height);\n font-weight: var(--dsw-font-s-strong-14-font-weight);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.dgu-spacer {\n flex: 1 1 auto;\n}\n\n.dgu-icon-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n flex: none;\n border: 0;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n.dgu-icon-button:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n.dgu-icon-button:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: -2px;\n}\n\n/* Tabs only appear from the second canvas on: one canvas needs no chooser. */\n.dgu-tabs {\n display: flex;\n gap: 2px;\n padding: 6px 8px;\n flex: none;\n overflow-x: auto;\n scrollbar-width: none;\n}\n.dgu-tabs::-webkit-scrollbar {\n display: none;\n}\n\n.dgu-tab {\n padding: 4px 10px;\n border: 0;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-family: inherit;\n font-size: var(--dsw-font-xs-13-font-size);\n line-height: var(--dsw-font-xs-13-line-height);\n white-space: nowrap;\n cursor: pointer;\n}\n.dgu-tab:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.dgu-tab[aria-selected=\"true\"] {\n background: var(--dsw-specific-sidebar-nav-item-active);\n color: var(--dsw-alias-label-primary);\n}\n\n/*\n * The canvas owns this space entirely \u2014 the panel already provides the frame, so the body\n * adds no padding and no background of its own. Generated UI is told to fill it.\n */\n.dgu-body {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n}\n.dgu-body > * {\n min-height: 100%;\n}\n\n.dgu-empty {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n padding: 24px;\n text-align: center;\n color: var(--dsw-alias-label-secondary);\n font-size: var(--dsw-font-xs-13-font-size);\n line-height: var(--dsw-font-xs-13-line-height);\n}\n\n/* Invisible hit strip, matching how the host draws its own sidebar boundary. */\n.dgu-resize {\n position: absolute;\n top: 0;\n bottom: 0;\n width: 8px;\n cursor: col-resize;\n z-index: 1;\n}\n.dgu-resize:hover,\n.dgu-resize[data-dragging] {\n /* The foreground token on purpose: this wants a wash that contrasts with whatever is\n behind it, and `brand-primary` tracks the body text colour in both themes. */\n background: var(--dsw-alias-brand-primary);\n opacity: 0.12;\n}\n\n/*\n * Sits where the panel's left edge was, so reopening is a click in the place it closed\n * from. Fixed like the panel because it shares the same \"outside the frame\" problem \u2014\n * an extra flex child in the frame's row lands off screen.\n */\n.dgu-launcher-dock {\n position: fixed;\n top: 50%;\n right: 0;\n transform: translateY(-50%);\n z-index: 20;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.dgu-launcher {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 8px 6px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-right: 0;\n border-radius: 8px 0 0 8px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-secondary);\n font-family: var(--dsw-font-family);\n font-size: var(--dsw-font-xs-13-font-size);\n line-height: var(--dsw-font-xs-13-line-height);\n cursor: pointer;\n}\n.dgu-launcher:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n.dgu-launcher:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: -2px;\n}\n\n.dgu-launcher-count {\n font-variant-numeric: tabular-nums;\n}\n\n/* Opens leftward: the dock is already against the right edge, so there is nowhere else. */\n.dgu-launcher-menu {\n display: flex;\n flex-direction: column;\n gap: 2px;\n margin: 0;\n padding: 4px;\n max-height: 60vh;\n overflow-y: auto;\n list-style: none;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n /* A floating surface has to occlude: `bg-layer-1` is a tint over whatever it sits on, so\n it needs the panel's own opaque ground underneath before the tint reads as a menu. */\n background: var(--dsw-alias-bg-base);\n background-image: linear-gradient(var(--dsw-alias-bg-layer-1), var(--dsw-alias-bg-layer-1));\n box-shadow: 0 8px 24px rgb(0 0 0 / 0.24);\n}\n\n.dgu-launcher-item {\n display: block;\n width: 100%;\n padding: 5px 10px;\n border: 0;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-primary);\n font-family: var(--dsw-font-family);\n font-size: var(--dsw-font-xs-13-font-size);\n line-height: var(--dsw-font-xs-13-line-height);\n text-align: left;\n white-space: nowrap;\n cursor: pointer;\n}\n.dgu-launcher-item:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n/* Anchors the header's canvas picker; the menu drops below the button. */\n.dgu-picker {\n position: relative;\n display: flex;\n}\n\n.dgu-picker-menu {\n position: absolute;\n top: calc(100% + 4px);\n right: 0;\n z-index: 1;\n}\n";
@@ -0,0 +1,12 @@
1
+ /** Every canvas in the workspace, including ones this session never wrote. */
2
+ export declare function listCanvasIds(cwd: string): Promise<readonly string[]>;
3
+ export declare function readCanvasFile(cwd: string, id: string): Promise<string | null>;
4
+ /**
5
+ * Reads one of a canvas's sub-page files, named by the relative specifier as written in the
6
+ * canvas source. The server resolves it through the contract; anything outside this canvas's
7
+ * own child directory comes back 400 and reads here as null.
8
+ */
9
+ export declare function readCanvasChild(cwd: string, id: string, specifier: string, from: string): Promise<{
10
+ source: string;
11
+ filename: string;
12
+ } | null>;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Whether the card imports a sibling at all — the cheap question `CanvasPanel` asks before
3
+ * paying for a resolve pass. It had its own copy of the regex, identical but for the `g` flag;
4
+ * a widening applied to one and not the other means the panel never calls `inlineSubPages` and
5
+ * the card silently renders without its sub-pages. Sharing one pattern fixed that and introduced
6
+ * the `lastIndex` bug above — hence two derived regexes rather than two literals.
7
+ */
8
+ export declare const importsSibling: (code: string) => boolean;
9
+ /**
10
+ * @param read fetches one child by its specifier, returning its source and the real filename
11
+ * it was found under — the compiler picks its syntax from the extension, and a specifier is
12
+ * written without one, so passing the specifier makes a `.ts` file fail to parse.
13
+ * @param compile turns one child's TSX into JS. Children go through the same compiler as the
14
+ * card, so a sub-page may be TSX and may itself import a sibling.
15
+ * @param urls collects every blob created, so the caller can revoke them with the surface.
16
+ */
17
+ export declare function inlineSubPages(code: string, entry: string, read: (specifier: string, from: string) => Promise<{
18
+ source: string;
19
+ filename: string;
20
+ } | null>, compile: (filename: string, source: string) => Promise<string>, urls: string[]): Promise<string>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Closes on a pointerdown anywhere but `anchor`, while `open`. Returns its own disposer.
3
+ *
4
+ * Split out of the hook so the four things that matter are testable without a renderer: that it
5
+ * does not listen while closed, that an outside press closes, that a press inside the anchor
6
+ * does **not**, and that it unsubscribes. A `pointerdown` listener on the document rather than a
7
+ * backdrop element, because a backdrop would sit over the canvas and swallow the first click
8
+ * into it.
9
+ */
10
+ export declare function dismissOnOutsidePointer(open: boolean, anchor: HTMLElement | null, close: () => void): (() => void) | undefined;
11
+ export declare function useDismissable(): {
12
+ open: boolean;
13
+ setOpen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
14
+ anchor: import("react").RefObject<HTMLDivElement>;
15
+ };
@@ -0,0 +1,20 @@
1
+ import type { ClientContext } from "@deepseek-ai/dsh-client-runtime/client";
2
+ import { localImports } from "./runtime/bindings.ts";
3
+ import { type ChatNodeView } from "./session.ts";
4
+ export declare const inject: string[];
5
+ /** Re-exported so `bun run smoke` can build the synthesized blob modules and parse them. */
6
+ export { localImports };
7
+ export declare function apply(ctx: ClientContext): void;
8
+ /** All assistant prose in one node, concatenated; empty for every other node kind. */
9
+ export declare function textOf(node: ChatNodeView): string;
10
+ /**
11
+ * Cache key for a `tool-call` node: how far its arguments have streamed, plus which calls
12
+ * have settled. Read straight off the node rather than through `toolCallsOf`, so computing
13
+ * the key does not repeat the work the cache exists to avoid.
14
+ *
15
+ * The settled flags are part of the key, not a detail: a call's `argsRaw` is already
16
+ * complete when `tool-result` arrives, so a length-only key would never invalidate and the
17
+ * cached view would claim `streaming` forever — a canvas that never stops pulsing and an
18
+ * `edit` that never marks it stale.
19
+ */
20
+ export declare function callsKeyOf(node: ChatNodeView): string;
@@ -0,0 +1,159 @@
1
+ export type GenUISurfaceProps = {
2
+ /** Full source when settled; the growing prefix while streaming. */
3
+ code: string;
4
+ /** True while `code` is still a prefix, so partial frames get normalized before compiling. */
5
+ streaming?: boolean;
6
+ /**
7
+ * Keep React state across recompiles. Right for a growing stream, where each frame is
8
+ * the previous one plus more text. Wrong for a whole-file replacement: the renderer
9
+ * decides reuse from the hook signature, so a rewrite that keeps the same hooks — an
10
+ * edited canvas usually does — is silently dropped rather than rendered.
11
+ */
12
+ preserveState?: boolean;
13
+ /** Real compile diagnostics. Transient streaming frames are filtered out — see TRANSIENT below. */
14
+ onError?: (error: Error, phase: "transform" | "compile" | "render") => void;
15
+ /** Fires whenever a frame actually painted. Use it to clear a previously shown error. */
16
+ onRendered?: () => void;
17
+ className?: string;
18
+ };
19
+ /** Paired with `disposeCompiler`: the shared instance must go with the wasm behind it. */
20
+ export declare const dropSharedCompiler: () => void;
21
+ export declare const compiler: () => import("./compiler.ts").TsxCompiler;
22
+ /**
23
+ * Mid-stream frames legitimately fail: a prefix that has not reached `export default`
24
+ * yet, or a half-written expression. partial-react treats these as transient and keeps
25
+ * the last good frame, so surfacing them would just make the UI flash errors while the
26
+ * model types. Only a failure that survives settling is the caller's business.
27
+ */
28
+ /** Exported for `test/transient.test.ts`: this decides whether the reader sees an error. */
29
+ export declare const TRANSIENT: RegExp;
30
+ /**
31
+ * A dependency that failed to arrive, not code that is wrong. esm.sh cold-starts and the
32
+ * network drops, and the symptom is identical to a broken component — a blank surface — so
33
+ * it is worth a few retries before anyone concludes the model wrote something wrong.
34
+ */
35
+ /** Exported for `test/transient.test.ts`. */
36
+ export declare const TRANSIENT_LOAD: RegExp;
37
+ /**
38
+ * Whether a reported error is worth another attempt.
39
+ *
40
+ * Extracted because it is the whole of the decision and none of the React: three conditions,
41
+ * each of which sends the reader somewhere different when it is wrong. Exported for
42
+ * `test/retry.test.ts`.
43
+ */
44
+ /**
45
+ * Whether a mid-stream error is the stream not being finished yet.
46
+ *
47
+ * Both patterns come from the parse stages — `No default export found` is thrown inside
48
+ * `importCompiledComponent` (compile), and an unexpected EOF is the transform rejecting a
49
+ * prefix. A card whose own render throws a message that happens to match is a real error, so
50
+ * the phase is part of the question rather than the message alone.
51
+ */
52
+ export declare const isUnfinishedFrame: (message: string, phase: string, streaming: boolean) => boolean;
53
+ export declare const shouldRetry: (message: string, phase: string, streaming: boolean, attempts: number) => boolean;
54
+ /**
55
+ * What to do with a frame, given what the surface already holds.
56
+ *
57
+ * `pushCode` APPENDS but a session event carries the whole prefix so far, so the difference
58
+ * between these four answers is the difference between a correct surface and one whose buffer
59
+ * doubles on every frame. Pure, because the decision is, and because a state machine that only
60
+ * runs inside an effect with three refs is one nothing ever checks.
61
+ *
62
+ * - `nothing` the frame adds no text, or a settled frame re-delivers what is already painted
63
+ * - `replace` settled: render the whole thing outright
64
+ * - `append` streaming and the buffer is a prefix of this frame: push only the delta
65
+ * - `restart` the prefix was rewritten (a re-delivered history page, or an edit)
66
+ */
67
+ export type Delivery = {
68
+ do: "nothing";
69
+ } | {
70
+ do: "replace";
71
+ code: string;
72
+ } | {
73
+ do: "append";
74
+ delta: string;
75
+ } | {
76
+ do: "restart";
77
+ code: string;
78
+ };
79
+ export declare const deliveryFor: (code: string, delivered: string, streaming: boolean) => Delivery;
80
+ /**
81
+ * Route a `Delivery` to the renderer. Returns whether anything was delivered, which is what tells
82
+ * the caller to advance its `delivered` marker.
83
+ *
84
+ * Split from the effect so the three calls can be constrained: `render` replaces the buffer,
85
+ * `pushCode` appends to it, and `clear({ preserveVisualState: true })` starts over WITHOUT
86
+ * blanking what is on screen. Getting `render` and `pushCode` the wrong way round doubles the
87
+ * buffer on every streamed frame, and the difference is one word inside an effect.
88
+ */
89
+ export declare const deliver: (renderer: RendererCalls, delivery: Delivery) => boolean;
90
+ /**
91
+ * What the card imports, as one string — the key the import-map probe is cached against.
92
+ *
93
+ * Compared by value rather than by set, so re-ordering the same imports re-probes. That is
94
+ * deliberate: the probe is cheap and cached downstream, and a set comparison here would be more
95
+ * code to get wrong than the re-probe costs.
96
+ */
97
+ export declare const importSignature: (code: string) => string;
98
+ /** Only what `deliver` touches — the real renderer has far more. */
99
+ export type RendererCalls = {
100
+ render: (code: string) => void;
101
+ pushCode: (delta: string) => void;
102
+ clear: (options: {
103
+ preserveVisualState: boolean;
104
+ }) => void;
105
+ };
106
+ /**
107
+ * What to do about an error the renderer reported.
108
+ *
109
+ * - `ignore` the stream is not finished; the next frame supersedes this
110
+ * - `retry` a dependency failed to arrive, and busting the import URLs is the fix
111
+ * - `report` tell the reader
112
+ *
113
+ * Only a SETTLED surface retries: while streaming, the next frame re-delivers on its own, and a
114
+ * retry there would replace the growing buffer with a stale prefix. Compile phase only — a failed
115
+ * dependency import is reported there (`importCompiledComponent` runs inside the compile `catch`,
116
+ * `partial-react/src/runtime.ts:338`), whereas the same message from the RENDER phase is the
117
+ * card's own `fetch` throwing inside its body, where re-importing changes nothing and costs three
118
+ * retries and 2.4 seconds of blank surface before the reader is told anything.
119
+ */
120
+ export declare const errorAction: (message: string, phase: string, streaming: boolean, attempts: number) => "ignore" | "retry" | "report";
121
+ /**
122
+ * What `onError` DOES with the three outcomes, separated from where they come from. The decision
123
+ * was already a pure function; the dispatch was not, and the mutation audit could not constrain
124
+ * it — swapping `ignore` for `retry` survived every test, because the only caller lives inside a
125
+ * `GenUIRenderer.create` callback that needs a DOM to reach.
126
+ *
127
+ * Each branch is one line and each is load-bearing: `ignore` must not touch the counter (a
128
+ * streaming frame is not a failed attempt), `retry` must increment BEFORE scheduling (the delay
129
+ * is a function of the count), and `report` must not increment at all.
130
+ */
131
+ /**
132
+ * What to do when an import probe settles. Three outcomes and every one is a bug if wrong:
133
+ *
134
+ * - **stale** — a later frame's probe won the race. Applying this one reverts the map to an
135
+ * older import set, and the newer frame's packages go missing.
136
+ * - **redeliver** — a settled surface has no next frame. `setImportMap` only stores; it
137
+ * schedules nothing, so without a re-render the card stays blank for good.
138
+ * - **store** — while streaming, the very next frame applies the map. Re-delivering here instead
139
+ * would replace the buffer with whatever prefix was current when the probe fired, truncating
140
+ * the stream mid-flight.
141
+ *
142
+ * The `delivered !== ""` part is not defensive: re-rendering an empty buffer clears the surface.
143
+ */
144
+ export declare const probeOutcome: (signature: string, current: string, streaming: boolean, delivered: string) => "stale" | "redeliver" | "store";
145
+ export declare const dispatchError: (action: "ignore" | "retry" | "report", effects: {
146
+ attempts: () => number;
147
+ setAttempts: (n: number) => void;
148
+ schedule: (ms: number) => void;
149
+ report: () => void;
150
+ }) => void;
151
+ /**
152
+ * The same import map with a fresh query on every fetched entry.
153
+ *
154
+ * Only `https://esm.sh/` URLs are touched. Local blob URLs are minted per render and already
155
+ * fresh, and appending a query to a `blob:` URL makes it unresolvable — which would break every
156
+ * card rather than fixing one.
157
+ */
158
+ export declare const bustFetchedImports: (imports: Record<string, string>, attempt: number) => Record<string, string>;
159
+ export declare function GenUISurface({ code, streaming, preserveState, onError, onRendered, className }: GenUISurfaceProps): import("react").JSX.Element;
@@ -0,0 +1,143 @@
1
+ import { usePersistedState } from "./state.ts";
2
+ /** What the plugin's client half lends to generated code. Registered once, at apply. */
3
+ export type Ui4aHost = {
4
+ /** Sends a prompt into the current session, exactly as the composer would. */
5
+ send: (text: string) => void;
6
+ /** The current session's workspace, which the AI route authorizes against. */
7
+ cwd: () => string | undefined;
8
+ /** The open session, so a write runs under the access mode the composer shows. */
9
+ sessionId: () => string;
10
+ };
11
+ /** Named so the round trip can be tested against the real code rather than a copy of it. */
12
+ export declare function decodeBase64(base64: string): Uint8Array<ArrayBuffer>;
13
+ export declare function registerUi4aHost(next: Ui4aHost): () => void;
14
+ /**
15
+ * The capability surface, one group per `$dsh/<group>` module.
16
+ *
17
+ * A function rather than a constant so the host can be swapped (or torn down) without the
18
+ * already-imported blob modules going stale — they close over `bind`, not over a host.
19
+ */
20
+ export declare function bind(): {
21
+ chat: {
22
+ /**
23
+ * Drives the next turn from inside a card. The text is what the user would have typed:
24
+ * it lands in the transcript as their message, because a turn nobody can see arriving
25
+ * reads as the app talking to itself.
26
+ */
27
+ sendMessage: (text: string) => void;
28
+ };
29
+ ai: {
30
+ /**
31
+ * Streams text from the app's own model, one piece per network chunk.
32
+ *
33
+ * Nothing here holds a credential: the Node half forwards to `ctx.llm`, which owns the
34
+ * provider route and the keys. Reach for it when the *content* is the variable part —
35
+ * the recipe, the five candidate names — and skip it when the data is genuinely fixed.
36
+ */
37
+ streamText: (options: Ui4aStreamOptions | string) => AsyncIterable<string>;
38
+ };
39
+ fs: {
40
+ /** The file's text. Throws when it does not exist or the session may not read it. */
41
+ readFile: (path: string) => Promise<string>;
42
+ /**
43
+ * Directory entries: the name, whether it is a file or a directory, and a file's size.
44
+ *
45
+ * An array of objects rather than of names, because without `type` a card cannot draw a
46
+ * tree — it would have to probe every entry with a second call to find out whether it
47
+ * can be descended into. The host has all three already; we used to drop two of them.
48
+ */
49
+ readdir: (path: string) => Promise<Ui4aDirEntry[]>;
50
+ /**
51
+ * The file's bytes, for anything that is not text.
52
+ *
53
+ * `readFile` decodes as UTF-8, so a .mid, a wav or a png read that way comes back with
54
+ * every byte above 0x7f replaced by U+FFFD — corrupt, and silently so. Anything handed to
55
+ * `decodeAudioData`, a MIDI parser or an image decoder has to come through here.
56
+ */
57
+ readBytes: (path: string) => Promise<Uint8Array<ArrayBuffer>>;
58
+ /**
59
+ * Writes the file, subject to the session's own access mode.
60
+ *
61
+ * Under `Read Only` this rejects exactly as the model's own `write` would — the fence
62
+ * is the host's, not ours, so what a card may do never diverges from what the composer
63
+ * says the session may do.
64
+ */
65
+ writeFile: (path: string, content: string) => Promise<void>;
66
+ };
67
+ exec: {
68
+ /**
69
+ * Runs one command in the workspace and resolves with its output.
70
+ *
71
+ * A non-zero exit resolves rather than rejects: `git status` failing outside a repo is
72
+ * something a card wants to show, not an outage. Only a failure to run at all rejects.
73
+ * The session's own sandbox mode applies, so this is no wider than the model's own bash.
74
+ */
75
+ bash: (command: string, options?: {
76
+ signal?: AbortSignal;
77
+ }) => Promise<Ui4aExecResult>;
78
+ };
79
+ web: {
80
+ /**
81
+ * One web search, through whichever provider the host composed.
82
+ *
83
+ * Search only: `ctx.web` also does `fetch`, and this deployment turns that off for its own
84
+ * tools because the local backend can reach private-network addresses. A card wanting a page
85
+ * body should ask the user for it or search for a quotable source instead.
86
+ */
87
+ search: (query: string, options?: {
88
+ maxResults?: number;
89
+ signal?: AbortSignal;
90
+ }) => Promise<Ui4aSearchResult>;
91
+ };
92
+ state: {
93
+ usePersistedState: typeof usePersistedState;
94
+ };
95
+ };
96
+ /** What one search returns. Mirrors the seam's `WebSearchResult`, which is what the route forwards. */
97
+ export type Ui4aSearchResult = {
98
+ /** A provider-generated answer or summary, when the provider makes one (Exa and DeepSeek do not). */
99
+ content?: string;
100
+ sources: readonly {
101
+ url: string;
102
+ title?: string;
103
+ snippet?: string;
104
+ publishedAt?: string;
105
+ }[];
106
+ /** True when the seam cut `sources` down to the requested bound. */
107
+ truncated: boolean;
108
+ };
109
+ /** What a command left behind. `truncated` means the output was cut, not that it failed. */
110
+ export type Ui4aExecResult = {
111
+ stdout: string;
112
+ stderr: string;
113
+ exitCode: number | null;
114
+ truncated: {
115
+ stdout: boolean;
116
+ stderr: boolean;
117
+ };
118
+ timedOut: boolean;
119
+ };
120
+ /** One entry of a directory listing. `size` is absent for directories. */
121
+ export type Ui4aDirEntry = {
122
+ name: string;
123
+ type?: "file" | "directory";
124
+ size?: number;
125
+ };
126
+ /** One user turn plus an optional system prompt — see the route's note on why not more. */
127
+ export type Ui4aStreamOptions = {
128
+ prompt: string;
129
+ system?: string;
130
+ signal?: AbortSignal;
131
+ };
132
+ export declare function bindingImports(): Record<string, string>;
133
+ export declare function releaseBindings(): void;
134
+ /**
135
+ * Every module generated code can import without reaching the network: the shell's React
136
+ * family, plus the `$dsh/*` capabilities.
137
+ *
138
+ * Exported from the plugin's client entry as well, so `bun run smoke` can build the blob
139
+ * modules and parse them. They are synthesized strings that nothing type-checks, and a
140
+ * syntax error in one fails the way an unresolvable import does — the whole module graph
141
+ * dies and the card renders blank with no console error.
142
+ */
143
+ export declare function localImports(): Record<string, string>;