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,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>;
@@ -0,0 +1,35 @@
1
+ import { type RendererImportMap } from "partial-react/import-map";
2
+ export type CompileOptions = {
3
+ importMap?: RendererImportMap;
4
+ partial?: boolean;
5
+ previousCode?: string;
6
+ filename?: string;
7
+ };
8
+ export type CompileResult = {
9
+ code: string;
10
+ source: string;
11
+ changed: boolean;
12
+ };
13
+ export type TsxCompiler = {
14
+ compile: (code: string, options?: CompileOptions) => Promise<CompileResult>;
15
+ };
16
+ /**
17
+ * Starts loading the 2.6 MB wasm file so it is warm before the first real frame (a cold init
18
+ * costs 400-500 ms). The *file* is 2.6 MB; an instantiated compiler costs roughly 16 MB of
19
+ * heap, which is why `disposeCompiler` exists — the two numbers have been confused before.
20
+ *
21
+ * Never throws: `apply()` calls this and nothing else awaits it, so a
22
+ * synchronous failure inside `initTsx` — an unfetchable wasm path, say — would otherwise
23
+ * take the whole plugin's registration down with it and leave the shell loading forever.
24
+ * A cold compile on the first card is a far better outcome than no plugin at all.
25
+ */
26
+ export declare const warmCompiler: () => Promise<unknown>;
27
+ /**
28
+ * Drops the wasm instance so GC can take it. `@esm.sh/tsx` exports no dispose — only
29
+ * `init`/`initSync`/`transform` — so releasing the reference is the whole of what we can do.
30
+ * Measured 2026-08-23: an instance costs ~16MB, and each HMR round made a fresh one while the
31
+ * previous stayed reachable through this module-level promise. Dev-only, but a dozen reloads
32
+ * is 200MB.
33
+ */
34
+ export declare function disposeCompiler(): void;
35
+ export declare function createBrowserTsxCompiler(): TsxCompiler;
@@ -0,0 +1,50 @@
1
+ import type { ReactElement } from "react";
2
+ import { type Ui4aSegment } from "./segments.ts";
3
+ /** Split out from `hasPainted` so the rule can be tested without a DOM. */
4
+ export declare const isPaintedText: (text: string) => boolean;
5
+ export declare const hasPainted: (mount: HTMLElement) => boolean;
6
+ /** CodeBlock trims one trailing newline for display, so compare on trimmed ends. */
7
+ export declare const sameCode: (a: string, b: string) => boolean;
8
+ /**
9
+ * The segment a rendered block belongs to.
10
+ *
11
+ * Mid-stream the block shows a prefix of its segment; once settled the two are equal.
12
+ */
13
+ export declare const matchSegment: (segments: readonly Ui4aSegment[], rendered: string) => Ui4aSegment | undefined;
14
+ export type InlineFenceOptions = {
15
+ /** Every ui4a segment currently in the transcript, in document order. */
16
+ segments: () => readonly Ui4aSegment[];
17
+ /** `last` answers whether this card is still the transcript's newest — asked at report time, not render time. */
18
+ render: (props: {
19
+ code: string;
20
+ streaming: boolean;
21
+ last: () => boolean;
22
+ }) => ReactElement;
23
+ scope?: HTMLElement;
24
+ };
25
+ /**
26
+ * Whether this card's code is the last ui4a segment in the transcript.
27
+ *
28
+ * **Only the last block's failure is worth telling the model about.** An earlier card that cannot
29
+ * render stays in the transcript and re-renders on every later frame, so it fails again, and
30
+ * again, for the rest of the session — measured on a real one where the model wrote a card
31
+ * importing `Github` from `lucide-react` (removed upstream), was told, fixed it to `GitBranch` in
32
+ * the very next reply, and the failure notice kept coming back from the reply it had already
33
+ * superseded. The model cannot act on it: editing a message it already sent is not a thing it
34
+ * can do.
35
+ *
36
+ * Evaluated when the report is about to be SENT, not when the error is raised. A card is the last
37
+ * one at the moment it breaks and stops being so as soon as the next card appears, which is
38
+ * exactly the second in between.
39
+ *
40
+ * **Segments, not mounts.** Comparing DOM nodes was wrong three separate ways, all of them
41
+ * silent: `release` calls `claim.mount.remove()` on every markdown re-render, so a broken card
42
+ * followed by any trailing prose compared a detached node and dropped its own report; `defer`
43
+ * parks a far-offscreen block without ever creating a mount, so a reader scrolled up while the fix
44
+ * streams left an older card holding the last mount and reporting as though it were newest; and an
45
+ * empty node list has to mean false, since a session switch inside the settle window empties it
46
+ * while `sendToModel` reads `currentSession()` at send time — session A's error into session B's
47
+ * store. The segment list is the transcript's own order and survives all three.
48
+ */
49
+ export declare const isLastSegment: (segments: readonly Ui4aSegment[], code: string) => boolean;
50
+ export declare function claimInlineFences({ segments, render, scope }: InlineFenceOptions): () => void;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * One document observer, shared by every consumer that reacts to transcript mutations.
3
+ *
4
+ * Both the inline-fence claimer and the canvas host are driven by the same event — a
5
+ * streamed token landing in the chat DOM — so a second observer over the same subtree only
6
+ * doubles the browser's mutation bookkeeping and the number of frames scheduled. The
7
+ * coalescing is not optional either: a streaming reply mutates the transcript dozens of
8
+ * times per second, and one sweep per mutation is how a renderer melts the main thread.
9
+ */
10
+ type Listener = () => void;
11
+ declare const schedule: () => void;
12
+ /**
13
+ * Runs `listener` at most once per frame while the document changes, starting immediately.
14
+ * @returns a disposer that also tears the observer down once nothing is left listening.
15
+ */
16
+ export declare function observeTranscript(listener: Listener): () => void;
17
+ /** Requests a frame outside a mutation — for state that changed without the DOM changing. */
18
+ export declare const scheduleSweep: typeof schedule;
19
+ /**
20
+ * Drop every listener and tear the observer down.
21
+ *
22
+ * The set above is module scope, so it is shared by everything in a process — which is right in
23
+ * a browser (one transcript, one observer) and is a trap in a test run, where a listener left by
24
+ * one file goes on being swept by every later one. A sweep captures its root at registration, so
25
+ * a stale one runs against a document that has since been replaced.
26
+ *
27
+ * Nothing in the plugin calls this: the shell disposes each host and that is the real path.
28
+ */
29
+ export declare function resetTranscriptObservers(): void;
30
+ export {};
@@ -0,0 +1,2 @@
1
+ export declare function registerRuntimeModules(): void;
2
+ export declare const hostReactVersion: string;
@@ -0,0 +1,7 @@
1
+ /** Exported for `test/registry.test.ts`: it generates code, so a bug here is a blank card with an empty console. */
2
+ export declare function buildModuleSource(specifier: string): string;
3
+ export declare function registerModules(modules: Record<string, Record<string, unknown>>): void;
4
+ export declare function moduleUrl(specifier: string): string;
5
+ export declare const registryImports: () => Record<string, string>;
6
+ /** Drops every synthesized blob. The plugin's dispose path must call this or each HMR round leaks one URL per specifier. */
7
+ export declare function disposeRegistry(): void;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Sends a card's compile error back to the model as a chat message.
3
+ *
4
+ * Until this existed, `onError` had no consumer at all: a card that failed to compile painted a
5
+ * red panel for the reader and the model never learned anything. Measured on a real session —
6
+ * a card imported `MonacoEditor` from `modern-monaco`, the surface reported *"has no export named
7
+ * 'MonacoEditor'; module exports: Workspace, errors, hydrate, init, lazy"*, and the transcript
8
+ * ends there: `MonacoEditor` appears 6 times in the records after that point and `no export named`
9
+ * zero. The reader saw the answer on screen; the one party who could act on it did not.
10
+ *
11
+ * Four constraints, each of which this got wrong in an obvious first version:
12
+ *
13
+ * - **Only errors that survived.** `GenUISurface` already separates a mid-stream prefix failure
14
+ * and a retryable network blip from a real one — the `report` branch of its error action. That
15
+ * branch is the only caller, so a half-written expression never reaches the model.
16
+ *
17
+ * It is not sufficient on its own, though. `isUnfinishedFrame` excludes the RENDER phase by
18
+ * design — a card whose own render throws is usually a real error — but a half-written
19
+ * component rendering mid-stream throws too, and the next frame is fine. Measured in a browser:
20
+ * a card that ultimately rendered correctly reported `Cannot read properties of undefined
21
+ * (reading 'getCurrentStack')`, a React internal, to the model. Painting a red panel for a
22
+ * frame costs nothing; sending the model a message about a card that then worked costs it a
23
+ * turn spent fixing what is not broken. So the send is DEFERRED, and a paint cancels it.
24
+ * - **Once per card, not once per frame.** A settled card that fails re-renders on every later
25
+ * frame of the transcript, and a turn per render is a loop the user has to kill. Deduplication
26
+ * lives HOST-side now (`CardFailures.set`), because this half is thrown away by every
27
+ * navigation and a dedup set that resets on reload wakes the model about a card it already
28
+ * knows. What stays here is the settling, which is about frames, not about turns.
29
+ * - **Recovery is reported too.** The report is state, not an event: a card that starts working
30
+ * sends `null`, and the host drops it out of the model's context. As a chat message that was
31
+ * impossible, which is why the old body had to carry a paragraph explaining that nobody had
32
+ * typed it.
33
+ * - **Only the newest card counts.** An earlier card that cannot render stays in the transcript
34
+ * and re-renders on every later frame, so it goes on failing for the rest of the session.
35
+ * Measured on a real one: the model wrote a card importing `Github` from `lucide-react`
36
+ * (removed upstream), was told, fixed it to `GitBranch` in its very next reply — and the
37
+ * failure notice kept coming back from the message it had already superseded. There is nothing
38
+ * the model can do with that; it cannot edit a reply it has sent. `isLastSegment` is the gate, on both the report and the retraction.
39
+ */
40
+ /** Exported for the test: a fresh card in a fresh session should be able to report again. */
41
+ export declare const forgetReportedErrors: () => void;
42
+ /** `null` means the card recovered. */
43
+ export type ErrorReporter = (report: {
44
+ message: string;
45
+ phase: string;
46
+ } | null) => void;
47
+ /** Drop a report the page is going away before it can deliver. NOT a recovery — nothing is fixed. */
48
+ export declare function cancelPendingReport(): void;
49
+ /**
50
+ * Called when a surface paints. Cancels a report the very next frame made untrue.
51
+ *
52
+ * **`restored` is not a detail.** partial-react answers a render throw by re-mounting the LAST
53
+ * GOOD component (`runtime.ts:416-419`, whenever `preserve` is on — which is every inline card),
54
+ * and that re-mount paints, and painting used to cancel the report that the very same throw had
55
+ * just armed. So the one case the reporting exists for — a card that worked, then broke on an
56
+ * edit — showed the reader stale content and told the model nothing. A paint only means the card
57
+ * is fine when it is the NEW code that painted.
58
+ */
59
+ /**
60
+ * @param current whether the painting card is the one the gate would let report. Defaults to
61
+ * permissive for the canvas path, which shows one card at a time.
62
+ */
63
+ export declare function cardRendered(restored?: boolean, current?: () => boolean): void;
64
+ export declare function reportCardError(send: ErrorReporter | undefined, message: string, phase: string, current?: () => boolean): void;
@@ -0,0 +1,18 @@
1
+ export type Ui4aSegment = {
2
+ code: string;
3
+ complete: boolean;
4
+ };
5
+ /**
6
+ * Tool-call markup the model leaked into its own prose. The reply ends mid-fence with the
7
+ * closing tags glued to the last line of TSX, so the body reaches the compiler with those tags
8
+ * in it and fails to parse — the whole card is lost, not just the closing fence.
9
+ *
10
+ * Two spellings, and the rarer one was found first: `</parameter></invoke>` appeared once in
11
+ * the corpus, while the model's own `</||DSML||parameter>` form accounts for three more and
12
+ * was invisible to a regex written from that single sample. Those full-width bars are U+FF5C,
13
+ * not ASCII `|`. Only stripped at the very end of an unterminated body, where nothing
14
+ * legitimate can follow — which is true of a closed fence too: the model leaks the tags and then
15
+ * still writes the closing fence, and stripping only the unterminated case loses that card outright.
16
+ */
17
+ export declare const TOOL_CALL_MARKUP: RegExp;
18
+ export declare function parseUi4aSegments(text: string): Ui4aSegment[];
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `$dsh/state` — the one capability the model asks for without being told it exists.
3
+ *
4
+ * Three runs of a habit-tracker prompt each wrote `import { usePersistedState } from "$dsh/state"`
5
+ * against a module that did not exist, which does not degrade: the browser refuses the module and
6
+ * the card renders blank. Rewording the skill to deny it did not help — the prior survives the
7
+ * denial. So the module exists now, with the signature all three runs assumed.
8
+ *
9
+ * Unlike the other capabilities this needs nothing from the host: `localStorage` and React are
10
+ * both already there. That is also why it is worth having — the alternative the skill used to
11
+ * prescribe is fifteen lines of try/catch that every card rewrites and half of them skip.
12
+ */
13
+ import * as React from "react";
14
+ /**
15
+ * `useState`, except the value survives a reload — and, more often, survives the remount that
16
+ * every canvas revision and every inline transcript re-render causes.
17
+ */
18
+ export declare function usePersistedState<T>(key: string, initial: T | (() => T)): [T, React.Dispatch<React.SetStateAction<T>>];
@@ -0,0 +1,16 @@
1
+ import type { UserConfig } from "@unocss/core";
2
+ /**
3
+ * Two things the host forces on this config, both non-negotiable:
4
+ *
5
+ * `important` receives a SELECTOR STRING, which is how UnoCSS scopes: every rule comes out
6
+ * `.ui4a-root :is(.gap-4){…}`. The runtime sheet is appended to `<head>` last, so an unscoped
7
+ * `hidden` written by a card would win over the shell's own `hidden` and make part of the app
8
+ * vanish. The playground has that bug on record (a sidebar disappearing); we start scoped.
9
+ *
10
+ * `preflights: { reset: false }` drops presetWind4's global reset — 3.5KB of `*, ::before,
11
+ * ::after { margin: 0; border: 0 solid }` that would land on the HOST's DOM, not just ours.
12
+ * The `theme` layer survives it and is the part we need: `--spacing` and `--radius-*`, which
13
+ * every `gap-*` and `rounded-*` resolves against. Without preflights entirely those rules
14
+ * generate but compute to nothing.
15
+ */
16
+ export declare const unoConfig: (scope: string) => UserConfig;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Runtime UnoCSS for generated cards.
3
+ *
4
+ * A build-time pass would scan OUR source, and the classes a card is written with do not exist
5
+ * there — they are typed by the model seconds ago. Responsive is where that shows worst: not one
6
+ * `@container` breakpoint would be generated, so every card would be single-column at any width.
7
+ * The CSS therefore has to be produced in the browser, as the code streams in.
8
+ *
9
+ * Accumulate rather than replace: several cards share one document, and each one's classes must
10
+ * stay in the sheet after another card is added.
11
+ */
12
+ /**
13
+ * The class every generated rule is prefixed with, and the one the surface carries.
14
+ *
15
+ * Named after the contract rather than after this plugin: the same class exists in
16
+ * `ui4a-playground` under the same constant name, so the two runtimes can be diffed line for
17
+ * line. It is also the only marker on the surface node — a second `data-*` hook naming the same
18
+ * thing was removed because nothing read it.
19
+ */
20
+ export declare const UI4A_ROOT_CLASS = "ui4a-root";
21
+ /**
22
+ * Per frame this does the two cheap things only: EXTRACT the class names out of the code
23
+ * (no CSS generated), and generate CSS for the ones not seen before.
24
+ *
25
+ * The expensive spellings, both measured in the playground this is ported from:
26
+ * `uno.generate(code)` regenerates every class in the file each time — 119s of main thread over
27
+ * one streaming canvas; and regenerating the whole accumulated token set on each new class costs
28
+ * more the longer the file gets. Throttling does not help when a single call is what is
29
+ * expensive.
30
+ *
31
+ * Appended rules sort after existing ones, so two same-priority utilities can resolve differently
32
+ * than a single authoritative pass would. Once the stream settles we regenerate the whole set to
33
+ * restore that order.
34
+ */
35
+ export declare function ensureUnoStyles(code: string, streaming?: boolean): Promise<void>;
36
+ /**
37
+ * Split a rule whose selector list mixes vendor pseudo-elements into one rule per vendor.
38
+ *
39
+ * UnoCSS merges selectors that share a declaration, so a card styling a slider for both engines
40
+ * gets `…::-moz-range-thumb, …::-webkit-slider-thumb { height: … }` as ONE rule — and Chromium
41
+ * drops the whole rule because it does not recognise the `-moz-` half. Measured: the browser
42
+ * parsed 75 of the 87 rules in a real card's sheet, the slider came out `height: 0px`, and the
43
+ * card shipped three invisible controls. Order does not matter and neither does which vendor is
44
+ * first; one unknown pseudo-element poisons the list.
45
+ *
46
+ * The model is doing the right thing by writing both prefixes, so the fix belongs here.
47
+ */
48
+ export declare function splitVendorRules(css: string): string;
49
+ /** Drops the sheet and the generator. HMR reloads the module; the old sheet must not survive it. */
50
+ export declare function disposeUnoStyles(): void;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Reading the current session's chat nodes.
3
+ *
4
+ * Both consumers — inline fences and canvases — need the same unwrap, and both run it on
5
+ * every frame while a reply streams. Sharing it keeps the guards in one place, and lets
6
+ * the per-node work be cached against a node's identity rather than redone per frame.
7
+ */
8
+ import type { ClientContext } from "@deepseek-ai/dsh-client-runtime/client";
9
+ export type ChatNodeView = {
10
+ readonly kind: string;
11
+ readonly data: unknown;
12
+ readonly anchorSeq: number;
13
+ };
14
+ /** The current session's chat nodes, or an empty list when no session is open. */
15
+ export declare function chatNodes(ctx: ClientContext): readonly ChatNodeView[];
16
+ /**
17
+ * Derives a value per chat node, reusing the previous result when the node has not changed.
18
+ *
19
+ * A sweep runs on every frame of a streaming reply, but only the tail node is actually
20
+ * growing — re-deriving finished nodes means re-scanning the whole transcript dozens of
21
+ * times a second, which grows with session length rather than with what changed.
22
+ *
23
+ * @param key - identity of a node's current content; equal keys must mean equal results.
24
+ * @param derive - the per-node work to memoize.
25
+ */
26
+ export declare function perNode<T>(key: (node: ChatNodeView) => string, derive: (node: ChatNodeView) => T): (nodes: readonly ChatNodeView[]) => T[];
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Asset URLs shared by both halves. Kept apart from index.ts so the browser half
3
+ * can import them without dragging node:fs and createRequire into its bundle.
4
+ */
5
+ export declare const ASSET_PREFIX = "/dsh-generative-ui/assets";
6
+ export declare const WASM_PATH = "/dsh-generative-ui/assets/tsx_bg.wasm";
7
+ /** Reads one canvas file from the session's workspace: `?cwd=<workspace>&id=<canvas>`. */
8
+ export declare const CANVAS_READ_PATH = "/dsh-generative-ui/canvas";
9
+ /**
10
+ * Streams one model call for a generated card: POST `{prompt|messages, system?}`.
11
+ *
12
+ * The host owns the credentials and the provider route, so this forwards to `ctx.llm`
13
+ * rather than carrying a key of its own.
14
+ */
15
+ export declare const AI_STREAM_PATH = "/dsh-generative-ui/ai";
16
+ /**
17
+ * Filesystem access for a generated card: `?cwd=<workspace>&path=<path>`.
18
+ *
19
+ * GET reads (or lists, with `?list=1`), POST writes. Both go through the host's `ctx.fs`
20
+ * and carry the session's own sandbox policy, so what a card may do is exactly what the
21
+ * session may do — `read-only` denies the write at the fence rather than here.
22
+ */
23
+ export declare const FS_PATH = "/dsh-generative-ui/fs";
24
+ /**
25
+ * Runs one command for a generated card: `?cwd=<workspace>&session=<id>`, POST `{command}`.
26
+ *
27
+ * Under the session's own sandbox policy, exactly as `FS_PATH` is — a read-only session gets
28
+ * a read-only shell rather than a different fence. Foreground only: a card that wants a
29
+ * long-running process wants a different product.
30
+ */
31
+ export declare const EXEC_PATH = "/dsh-generative-ui/exec";
32
+ /**
33
+ * One web search for a generated card: `?cwd=<workspace>`, POST `{query, maxResults?}`.
34
+ *
35
+ * Search only. `ctx.web` also exposes `fetch`, and this deliberately does not forward it: the
36
+ * deployment's own `tool-web` is configured `fetch: false`, and the doc says why — *"the local
37
+ * backend does not block private-network targets; do not enable web_fetch where it can reach
38
+ * sensitive internal ones."* A card is model-written code firing on a reader's keystrokes, so
39
+ * re-opening from here what the host closed for its own tools is not ours to do.
40
+ */
41
+ export declare const WEB_SEARCH_PATH = "/dsh-generative-ui/web-search";
42
+ /**
43
+ * A card's surviving failure, reported by the browser half: `?session=<id>`, POST
44
+ * `{message, phase}` to set it and `{}` to clear it.
45
+ *
46
+ * The detail does NOT come back as a chat message. It becomes a runtime-context snapshot
47
+ * (`ui4a:card-failure`), which is re-evaluated per assembly and superseded by the next one, so a
48
+ * card that gets fixed stops being mentioned instead of leaving a stale complaint in history. The
49
+ * route only carries the state; `wakeAgent` is what asks the model to look at it.
50
+ */
51
+ export declare const CARD_ERROR_PATH = "/dsh-generative-ui/card-error";
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The ui4a path contract — the single place that decides what counts as a canvas.
3
+ * Shared by both halves; never re-derive these patterns with an inline regex.
4
+ */
5
+ /**
6
+ * Where generated files live, under the workspace's own dsh directory.
7
+ *
8
+ * `.dsh/` is the harness's project convention, not ours — `dsh-skill-filesystem` reads
9
+ * `join(projectRoot, ".dsh/skills")` and labels that source `project-dsh`. Sitting beside
10
+ * it keeps a plain `ls` of the user's repo clean and puts our files where they would look
11
+ * for anything dsh wrote. `ui4a` beneath it names the format, which is the honest nesting:
12
+ * this is a dsh plugin writing ui4a files, not a ui4a project with a dsh corner.
13
+ */
14
+ export declare const UI4A_DIR = ".dsh/ui4a";
15
+ export declare const CANVAS_DIR = ".dsh/ui4a/canvases";
16
+ export declare const CANVAS_SUFFIX = ".ui4a.tsx";
17
+ /**
18
+ * Info string of an inline fence, as the model writes it. Slash, not dash — matches
19
+ * ui4a-playground. Note the host's markdown renderer truncates it at the first
20
+ * non-identifier character, so it reaches the DOM as `ui4a`; nothing matches on it.
21
+ */
22
+ export declare const FENCE_LANG = "ui4a/tsx";
23
+ /**
24
+ * Import prefix for the capabilities the plugin lends to generated code.
25
+ *
26
+ * `$dsh/`, not `$ui4a/`: what these expose is the harness — the conversation, its model,
27
+ * its filesystem — and none of it is part of the ui4a rendering contract that `FENCE_LANG`
28
+ * and the canvas paths above define. A card written against them only runs inside dsh.
29
+ */
30
+ export declare const CAPABILITY_PREFIX = "$dsh";
31
+ export declare const capabilityModule: (group: string) => string;
32
+ export declare const isCanvasId: (id: string) => boolean;
33
+ export declare const canvasPath: (id: string) => string;
34
+ export declare const canvasChildDir: (id: string) => string;
35
+ /**
36
+ * Resolves a relative specifier written inside a canvas to a workspace path.
37
+ *
38
+ * `from` is the path the specifier was written in — the canvas file itself, or one of its
39
+ * children — because **a relative specifier is relative to its importer, not to the canvas
40
+ * root**. The entry writes `./<id>/board`; a child of that entry writes `./types` for its
41
+ * sibling, and resolving both against the canvases directory sends the second one nowhere.
42
+ * Measured on a real split: the model produced 7 files whose cross-imports are all sibling
43
+ * form, and every one of them resolved to null before `from` existed.
44
+ *
45
+ * Every segment goes through the same exclusion test as an id, and the result must stay
46
+ * inside `canvasChildDir(id)` — `..` is rejected outright rather than normalised, so there
47
+ * is no arithmetic that could walk out.
48
+ *
49
+ * Returns null for anything outside that shape rather than throwing: the caller is a route
50
+ * answering an arbitrary page, and a bad specifier is a 400, not a crash.
51
+ */
52
+ export declare function canvasChildPath(id: string, specifier: string, from?: string): string | null;
53
+ /** The canvas id of an entry path, or null when the path is not a canvas entry. */
54
+ export declare function canvasIdOf(path: string): string | null;
55
+ /** The owning canvas of any path under the contract — entry file or child module. */
56
+ export declare function owningCanvasIdOf(path: string): string | null;