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.
- package/README.md +17 -4
- package/lib/client.js +484 -98
- package/lib/client.js.map +19 -17
- package/lib/index.js +523 -34
- package/lib/types/card-failure.d.ts +88 -0
- package/lib/types/client/canvas/CanvasLauncher.d.ts +6 -0
- package/lib/types/client/canvas/CanvasPanel.d.ts +88 -0
- package/lib/types/client/canvas/collect.d.ts +45 -0
- package/lib/types/client/canvas/index.d.ts +43 -0
- package/lib/types/client/canvas/mount.d.ts +30 -0
- package/lib/types/client/canvas/panel-css.d.ts +1 -0
- package/lib/types/client/canvas/read.d.ts +12 -0
- package/lib/types/client/canvas/subpages.d.ts +20 -0
- package/lib/types/client/canvas/useDismissable.d.ts +15 -0
- package/lib/types/client/index.d.ts +20 -0
- package/lib/types/client/runtime/GenUISurface.d.ts +189 -0
- package/lib/types/client/runtime/bindings.d.ts +143 -0
- package/lib/types/client/runtime/compiler.d.ts +35 -0
- package/lib/types/client/runtime/inline-fence.d.ts +50 -0
- package/lib/types/client/runtime/observe.d.ts +30 -0
- package/lib/types/client/runtime/register.d.ts +2 -0
- package/lib/types/client/runtime/registry.d.ts +7 -0
- package/lib/types/client/runtime/report-error.d.ts +64 -0
- package/lib/types/client/runtime/segments.d.ts +18 -0
- package/lib/types/client/runtime/state.d.ts +18 -0
- package/lib/types/client/runtime/uno-config.d.ts +16 -0
- package/lib/types/client/runtime/uno.d.ts +50 -0
- package/lib/types/client/session.d.ts +26 -0
- package/lib/types/contract-assets.d.ts +51 -0
- package/lib/types/contract.d.ts +56 -0
- package/lib/types/index.d.ts +298 -0
- package/lib/types/prompt.d.ts +13 -0
- package/lib/types/skill.d.ts +27 -0
- package/package.json +8 -4
- package/src/card-failure.ts +106 -0
- package/src/client/canvas/CanvasPanel.tsx +10 -4
- package/src/client/canvas/index.ts +1 -1
- package/src/client/index.ts +22 -9
- package/src/client/runtime/GenUISurface.tsx +172 -48
- package/src/client/runtime/bindings.ts +3 -2
- package/src/client/runtime/compiler.ts +24 -2
- package/src/client/runtime/inline-fence.ts +185 -8
- package/src/client/runtime/register.ts +16 -4
- package/src/client/runtime/report-error.ts +89 -34
- package/src/client/runtime/state.ts +13 -1
- package/src/client/runtime/uno-config.ts +13 -0
- package/src/client/session.ts +10 -3
- package/src/contract-assets.ts +11 -0
- package/src/index.ts +113 -10
- package/src/prompt.ts +154 -10
- package/src/skill.ts +296 -22
- 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,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;
|