dsh-generative-ui 0.0.1 → 0.0.3

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.
Files changed (52) hide show
  1. package/README.md +17 -4
  2. package/lib/client.js +484 -98
  3. package/lib/client.js.map +19 -17
  4. package/lib/index.js +523 -34
  5. package/lib/types/card-failure.d.ts +88 -0
  6. package/lib/types/client/canvas/CanvasLauncher.d.ts +6 -0
  7. package/lib/types/client/canvas/CanvasPanel.d.ts +88 -0
  8. package/lib/types/client/canvas/collect.d.ts +45 -0
  9. package/lib/types/client/canvas/index.d.ts +43 -0
  10. package/lib/types/client/canvas/mount.d.ts +30 -0
  11. package/lib/types/client/canvas/panel-css.d.ts +1 -0
  12. package/lib/types/client/canvas/read.d.ts +12 -0
  13. package/lib/types/client/canvas/subpages.d.ts +20 -0
  14. package/lib/types/client/canvas/useDismissable.d.ts +15 -0
  15. package/lib/types/client/index.d.ts +20 -0
  16. package/lib/types/client/runtime/GenUISurface.d.ts +189 -0
  17. package/lib/types/client/runtime/bindings.d.ts +143 -0
  18. package/lib/types/client/runtime/compiler.d.ts +35 -0
  19. package/lib/types/client/runtime/inline-fence.d.ts +50 -0
  20. package/lib/types/client/runtime/observe.d.ts +30 -0
  21. package/lib/types/client/runtime/register.d.ts +2 -0
  22. package/lib/types/client/runtime/registry.d.ts +7 -0
  23. package/lib/types/client/runtime/report-error.d.ts +64 -0
  24. package/lib/types/client/runtime/segments.d.ts +18 -0
  25. package/lib/types/client/runtime/state.d.ts +18 -0
  26. package/lib/types/client/runtime/uno-config.d.ts +16 -0
  27. package/lib/types/client/runtime/uno.d.ts +50 -0
  28. package/lib/types/client/session.d.ts +26 -0
  29. package/lib/types/contract-assets.d.ts +51 -0
  30. package/lib/types/contract.d.ts +56 -0
  31. package/lib/types/index.d.ts +298 -0
  32. package/lib/types/prompt.d.ts +13 -0
  33. package/lib/types/skill.d.ts +27 -0
  34. package/package.json +8 -4
  35. package/src/card-failure.ts +106 -0
  36. package/src/client/canvas/CanvasPanel.tsx +10 -4
  37. package/src/client/canvas/index.ts +1 -1
  38. package/src/client/index.ts +22 -9
  39. package/src/client/runtime/GenUISurface.tsx +172 -48
  40. package/src/client/runtime/bindings.ts +3 -2
  41. package/src/client/runtime/compiler.ts +24 -2
  42. package/src/client/runtime/inline-fence.ts +185 -8
  43. package/src/client/runtime/register.ts +16 -4
  44. package/src/client/runtime/report-error.ts +89 -34
  45. package/src/client/runtime/state.ts +13 -1
  46. package/src/client/runtime/uno-config.ts +13 -0
  47. package/src/client/session.ts +10 -3
  48. package/src/contract-assets.ts +11 -0
  49. package/src/index.ts +113 -10
  50. package/src/prompt.ts +154 -10
  51. package/src/skill.ts +296 -22
  52. package/types/fs.d.ts +15 -1
@@ -0,0 +1,88 @@
1
+ /**
2
+ * What the model is told about a card that will not render.
3
+ *
4
+ * Split in two, deliberately, because the two halves want opposite lifetimes:
5
+ *
6
+ * - **The detail is state.** It goes out as a runtime-context snapshot, re-evaluated on every
7
+ * assembly and superseded by the next one. A card that gets fixed simply stops being mentioned.
8
+ * As a chat message it was the opposite — permanent, and still there three turns after the card
9
+ * started working.
10
+ * - **The nudge is an event.** One short line through `followup`, whose only job is to open a turn
11
+ * so the model looks at the detail now rather than whenever the user next types.
12
+ *
13
+ * Neither is a user-role message any more, and that removes the paragraph the old body had to
14
+ * carry: *"This was sent by the renderer, not by the user — nobody typed it, so do not apologise
15
+ * or address it as a request."* That existed because the report arrived wearing the user's face.
16
+ * A `kind: "plugin"` source does not, so the model is told what happened and nothing else — the
17
+ * disclaimer cost more tokens than the error it was wrapped around.
18
+ */
19
+ /** A card failure as the browser half reported it. */
20
+ export type CardFailure = {
21
+ readonly message: string;
22
+ readonly phase: string;
23
+ };
24
+ /** The runtime-context section name. Stable: it is how a later snapshot supersedes an earlier one. */
25
+ export declare const CARD_FAILURE_CONTEXT = "ui4a:card-failure";
26
+ /**
27
+ * Ordered after the plugin's own guidance so the model reads how cards work before it reads that
28
+ * one is broken.
29
+ */
30
+ export declare const CARD_FAILURE_CONTEXT_ORDER = 250;
31
+ /**
32
+ * The detail, as the model sees it.
33
+ *
34
+ * It says it is current state rather than an event on purpose: the same text is re-delivered on
35
+ * every step while the card stays broken, and a model that reads it as a fresh report tries to
36
+ * fix the card again on each one.
37
+ */
38
+ export declare const failureText: (failure: CardFailure) => string;
39
+ /**
40
+ * The one line that opens a turn. The detail is already in context; this only asks for attention.
41
+ *
42
+ * The second sentence is not padding — it closes a race the split design makes possible. The nudge
43
+ * is an event and cannot be recalled once queued; the detail is state and is re-evaluated when the
44
+ * turn assembles. A card that recovers in between (a retry succeeding is the common way) clears
45
+ * the state, so the turn opens with "go read the runtime context" pointing at a context that says
46
+ * nothing about a card.
47
+ *
48
+ * Seen in a real session: two notices, and the SECOND one had no card-failure section anywhere in
49
+ * the turn that followed it. Without a defined outcome for that, the model has been told something
50
+ * is broken, cannot find it, and goes looking — in that session it rewrote a card that was fine.
51
+ */
52
+ export declare const WAKE_TEXT = "A ui4a card you wrote is not rendering \u2014 the failure is in the runtime context. If there is no card failure there, it recovered on its own between this notice and now: say nothing about it and carry on with what you were doing.";
53
+ /** Shown as the context row's label in the transcript, so a reader can see what fired without opening it. */
54
+ export declare const WAKE_SUMMARY = "ui4a card failed to render";
55
+ /**
56
+ * The current failing card per session.
57
+ *
58
+ * Per session rather than per card: the nudge exists to get one turn spent on the problem, and a
59
+ * reply that breaks three cards does not want three turns. The newest failure wins because it is
60
+ * the one the model just wrote.
61
+ */
62
+ export declare class CardFailures {
63
+ private readonly bySession;
64
+ /**
65
+ * Record a failure.
66
+ *
67
+ * @returns whether the session went from HEALTHY to failing — the caller wakes the model only
68
+ * then. Dedup lives here rather than in the browser half because that half is reloaded by every
69
+ * navigation, and a dedup set that resets on reload wakes the model again for a card it already
70
+ * knows about.
71
+ *
72
+ * **Not "is this a different message".** That was the first version, and it is wrong for a
73
+ * nudge whose entire payload is *go read the context*: two different failures produce two
74
+ * BYTE-IDENTICAL notices, so a reader watching a long turn sees the same sentence queued twice
75
+ * and the model gets two turns pointed at one current-state snapshot. Measured on a real
76
+ * session — three genuinely different failures (`@react-three/fiber` unresolvable, `maps is not
77
+ * defined`, `@react-three/cannon` unresolvable) over eighty minutes, each "news" by the old
78
+ * test, and the third's notice was still sitting unsent in the composer beside the second.
79
+ *
80
+ * While a failure is already recorded the model is either about to look or has looked, and the
81
+ * detail it will read is re-evaluated every step anyway — so a changed message needs no second
82
+ * nudge. `clear()` on recovery is what re-arms this.
83
+ */
84
+ set(session: string, failure: CardFailure): boolean;
85
+ clear(session: string): void;
86
+ /** The context text for one session; empty contributes nothing to the assembly. */
87
+ text(session: string | undefined): string;
88
+ }
@@ -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?: (restored: boolean) => 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?: (restored: boolean) => 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,189 @@
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, by rendering through partial-react's stable slot
8
+ * wrapper so hooks stay on the same fiber. Off, every recompile renders a freshly
9
+ * compiled function type, which React remounts — a running timer restarts at zero.
10
+ *
11
+ * It does NOT risk dropping an edit: the renderer's two reuse branches only fire in
12
+ * `push` mode, so a whole-file replacement always renders.
13
+ */
14
+ preserveState?: boolean;
15
+ /** Real compile diagnostics. A streaming frame never reaches this — see `errorAction`. */
16
+ onError?: (error: Error, phase: "transform" | "compile" | "render") => void;
17
+ /** Fires whenever a frame actually painted. Use it to clear a previously shown error. */
18
+ onRendered?: (restored: boolean) => void;
19
+ className?: string;
20
+ };
21
+ /** Paired with `disposeCompiler`: the shared instance must go with the wasm behind it. */
22
+ export declare const dropSharedCompiler: () => void;
23
+ export declare const compiler: () => import("./compiler.ts").TsxCompiler;
24
+ /**
25
+ * A dependency that failed to arrive, not code that is wrong. esm.sh cold-starts and the
26
+ * network drops, and the symptom is identical to a broken component — a blank surface — so
27
+ * it is worth a few retries before anyone concludes the model wrote something wrong.
28
+ */
29
+ /** Exported for `test/transient.test.ts`. */
30
+ export declare const TRANSIENT_LOAD: RegExp;
31
+ /**
32
+ * Whether a reported error is worth another attempt.
33
+ *
34
+ * Extracted because it is the whole of the decision and none of the React: three conditions,
35
+ * each of which sends the reader somewhere different when it is wrong. Exported for
36
+ * `test/retry.test.ts`.
37
+ */
38
+ export declare const shouldRetry: (message: string, phase: string, streaming: boolean, attempts: number) => boolean;
39
+ /**
40
+ * What to do with a frame, given what the surface already holds.
41
+ *
42
+ * `pushCode` APPENDS but a session event carries the whole prefix so far, so the difference
43
+ * between these four answers is the difference between a correct surface and one whose buffer
44
+ * doubles on every frame. Pure, because the decision is, and because a state machine that only
45
+ * runs inside an effect with three refs is one nothing ever checks.
46
+ *
47
+ * - `nothing` the frame adds no text, or a settled frame re-delivers what is already painted
48
+ * - `replace` settled: render the whole thing outright
49
+ * - `append` streaming and the buffer is a prefix of this frame: push only the delta
50
+ * - `restart` the prefix was rewritten (a re-delivered history page, or an edit)
51
+ */
52
+ export type Delivery = {
53
+ do: "nothing";
54
+ } | {
55
+ do: "replace";
56
+ code: string;
57
+ } | {
58
+ do: "append";
59
+ delta: string;
60
+ } | {
61
+ do: "restart";
62
+ code: string;
63
+ };
64
+ export declare const deliveryFor: (code: string, delivered: string, streaming: boolean) => Delivery;
65
+ /**
66
+ * Route a `Delivery` to the renderer. Returns whether anything was delivered, which is what tells
67
+ * the caller to advance its `delivered` marker.
68
+ *
69
+ * Split from the effect so the three calls can be constrained: `render` replaces the buffer,
70
+ * `pushCode` appends to it, and `clear({ preserveVisualState: true })` starts over WITHOUT
71
+ * blanking what is on screen. Getting `render` and `pushCode` the wrong way round doubles the
72
+ * buffer on every streamed frame, and the difference is one word inside an effect.
73
+ */
74
+ export declare const deliver: (renderer: RendererCalls, delivery: Delivery) => boolean;
75
+ /**
76
+ * What the card imports, as one string — the key the import-map probe is cached against.
77
+ *
78
+ * Compared by value rather than by set, so re-ordering the same imports re-probes. That is
79
+ * deliberate: the probe is cheap and cached downstream, and a set comparison here would be more
80
+ * code to get wrong than the re-probe costs.
81
+ */
82
+ export declare const importSignature: (code: string) => string;
83
+ /**
84
+ * Whether a failure suppressed during streaming still has to be told to someone.
85
+ *
86
+ * The hole this closes: `errorAction` ignores every streaming frame (a truncated one is not a
87
+ * broken card), and `deliveryFor` answers `nothing` when the settled frame is byte-identical to
88
+ * the last streamed one. Both are right on their own, and together they mean a card that really
89
+ * is broken recompiles never and reports never.
90
+ *
91
+ * Pure, and exported, because it is three conditions and each one is a distinct bug when wrong:
92
+ * without `!streaming` it fires mid-stream and undoes the fix it belongs to; without `stranded`
93
+ * it reports nothing; without the `reportedFor` guard a settled card re-rendered by every later
94
+ * frame of the transcript reports on each one.
95
+ */
96
+ export declare const reportStranded: (streaming: boolean, stranded: unknown, code: string, reportedFor: string) => boolean;
97
+ /** Only what `deliver` touches — the real renderer has far more. */
98
+ export type RendererCalls = {
99
+ render: (code: string) => void;
100
+ pushCode: (delta: string) => void;
101
+ clear: (options: {
102
+ preserveVisualState: boolean;
103
+ }) => void;
104
+ };
105
+ /**
106
+ * What to do about an error the renderer reported.
107
+ *
108
+ * - `ignore` the stream is not finished; the next frame supersedes this
109
+ * - `retry` a dependency failed to arrive, and busting the import URLs is the fix
110
+ * - `report` tell the reader
111
+ *
112
+ * **A streaming frame is never reported, whatever it says.** This used to test the message
113
+ * against `TRANSIENT` (`No default export found`, unexpected EOF) and report anything else, on
114
+ * the theory that a truncated frame fails to parse. It does not have to: a cut that lands
115
+ * mid-identifier leaves valid syntax and throws at module evaluation instead. Measured on one
116
+ * real session — five consecutive reports, five regenerations, and every final card was fine:
117
+ * `Mouse is not defined` from a card whose only such name is `MousePointer2`, `type is not
118
+ * defined` from `type ToolGroup =`, and `icon is not defined` from a card containing no `icon` at
119
+ * all. The frame, not the card, was broken. There is no message that distinguishes the two, so
120
+ * the phase does it: only a settled surface has anything worth saying about.
121
+ */
122
+ export declare const errorAction: (message: string, phase: string, streaming: boolean, attempts: number) => "ignore" | "retry" | "report";
123
+ /**
124
+ * What `onError` DOES with the three outcomes, separated from where they come from. The decision
125
+ * was already a pure function; the dispatch was not, and the mutation audit could not constrain
126
+ * it — swapping `ignore` for `retry` survived every test, because the only caller lives inside a
127
+ * `GenUIRenderer.create` callback that needs a DOM to reach.
128
+ *
129
+ * Each branch is one line and each is load-bearing: `ignore` must not touch the counter (a
130
+ * streaming frame is not a failed attempt), `retry` must increment BEFORE scheduling (the delay
131
+ * is a function of the count), and `report` must not increment at all.
132
+ */
133
+ /**
134
+ * What to do when an import probe settles. Three outcomes and every one is a bug if wrong:
135
+ *
136
+ * - **stale** — a later frame's probe won the race. Applying this one reverts the map to an
137
+ * older import set, and the newer frame's packages go missing.
138
+ * - **redeliver** — nothing else is going to apply the map. Always true of a settled surface,
139
+ * and true while streaming too when no frame has been delivered since the probe fired:
140
+ * `setImportMap` only stores, so that buffer stays compiled against a map without these
141
+ * entries — and an unresolvable bare specifier kills the whole module graph, so the card is
142
+ * blank rather than wrong. Measured 2026-08-27 on a card importing `@radix-ui/react-tabs`:
143
+ * one delivery with `streaming: true` renders 0 characters **for good**, while the identical
144
+ * code with `streaming: false` renders. The error is swallowed on top of it — `errorAction`
145
+ * answers `ignore` while streaming, so the reader gets an empty card and the model gets no
146
+ * report. A cold probe is 1.0s and the import behind it another 2.4s, so the window this
147
+ * covers is seconds wide, not a frame.
148
+ * - **store** — a newer frame has already been delivered, and it will apply the map itself.
149
+ * Re-delivering here instead would replace the buffer with the prefix that was current when
150
+ * the probe fired, truncating the stream mid-flight.
151
+ *
152
+ * `redeliver` renders `deliveredRef.current`, which is read at SETTLE time — the newest buffer,
153
+ * not the captured one — so the append the next frame computes still lines up.
154
+ *
155
+ * The `delivered !== ""` part is not defensive: re-rendering an empty buffer clears the surface.
156
+ */
157
+ export declare const probeOutcome: (signature: string, current: string, streaming: boolean, delivered: string, probed: string) => "stale" | "redeliver" | "store";
158
+ export declare const dispatchError: (action: "ignore" | "retry" | "report", effects: {
159
+ attempts: () => number;
160
+ setAttempts: (n: number) => void;
161
+ schedule: (ms: number) => void;
162
+ report: () => void;
163
+ }) => void;
164
+ /**
165
+ * The same import map with a fresh query on every fetched entry.
166
+ *
167
+ * Only `https://esm.sh/` URLs are touched. Local blob URLs are minted per render and already
168
+ * fresh, and appending a query to a `blob:` URL makes it unresolvable — which would break every
169
+ * card rather than fixing one.
170
+ */
171
+ /**
172
+ * The same import map with `bundle`/`external` dropped from every esm.sh entry.
173
+ *
174
+ * esm.sh serves two different builds and only one of them can fail: `?bundle` runs esbuild over
175
+ * the package's whole tree, and a version skew anywhere in it is a hard 500. Measured on
176
+ * `mermaid`, three attempts, deterministic — `?bundle&target=es2022&external=react,react-dom,scheduler`
177
+ * answers **500 `esbuild: No matching export in "node_modules/d3/src/index.js" for import
178
+ * "curveBumpX"`**, while the plain `https://esm.sh/mermaid?target=es2022` answers 200 and the
179
+ * module imports fine. The card meanwhile renders **completely blank with nothing in the
180
+ * console**, because an unresolvable import kills the whole module graph.
181
+ *
182
+ * So the retry that only busts the query re-requests the same broken build. Unbundling is a
183
+ * genuinely different artefact on esm.sh's side, which is what makes it a second chance rather
184
+ * than a second identical failure. It is not the first choice — the bundled build is one request
185
+ * instead of a waterfall — which is why this is on the retry path and not on the happy one.
186
+ */
187
+ export declare const unbundleFetchedImports: (imports: Record<string, string>) => Record<string, string>;
188
+ export declare const bustFetchedImports: (imports: Record<string, string>, attempt: number) => Record<string, string>;
189
+ export declare function GenUISurface({ code, streaming, preserveState, onError, onRendered, className }: GenUISurfaceProps): import("react").JSX.Element;