dsh-generative-ui 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,23 @@
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
+ render: (props: {
18
+ code: string;
19
+ streaming: boolean;
20
+ }) => ReactElement;
21
+ scope?: HTMLElement;
22
+ };
23
+ 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,17 @@
1
+ /** Exported for the test: a fresh card in a fresh session should be able to report again. */
2
+ export declare const forgetReportedErrors: () => void;
3
+ export type ErrorReporter = (text: string) => void;
4
+ /**
5
+ * The message body. Kept short and factual: it is spent from the user's context window, and the
6
+ * one thing the model needs is what failed and that nobody typed it.
7
+ *
8
+ * English, like the prompt and the skill it sits beside. This message is the only text this
9
+ * plugin puts into the conversation, and writing it in Chinese did two things: it read as a
10
+ * different voice from everything else the plugin says, and — because a card must be written in
11
+ * the language the USER wrote in — it pushed the model toward answering a Spanish or French
12
+ * speaker in the wrong language for the rest of the turn.
13
+ */
14
+ export declare const reportBody: (message: string, phase: string) => string;
15
+ /** Called when a surface paints. Cancels a report the very next frame made untrue. */
16
+ export declare function cardRendered(): void;
17
+ export declare function reportCardError(send: ErrorReporter | undefined, message: string, phase: string): 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,41 @@
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";
@@ -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;
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Host half — serves the @esm.sh/tsx wasm the browser half compiles TSX with.
3
+ *
4
+ * The shell's /plugins route hard-codes the `/client.js` and `/client.js.map`
5
+ * suffixes and 404s everything else, and dsh-host-frontend-static owns the sole
6
+ * fallback seat (and answers misses with index.html + 200, so dropping the wasm
7
+ * there would fail as a confusing magic-word error). A plugin-owned webServer
8
+ * route is the way to ship bytes; dsh-latex-tools serves MathJax the same way.
9
+ * @module dsh-generative-ui
10
+ */
11
+ import type { IncomingMessage, ServerResponse } from "node:http";
12
+ import type { Context } from "@deepseek-ai/cordis";
13
+ import z from "@deepseek-ai/schemastery";
14
+ export declare const name = "dsh-generative-ui";
15
+ export declare const inject: string[];
16
+ /** The settings section this plugin owns; the key under `dsh-generative-ui:` in settings.yaml. */
17
+ export declare const SETTINGS_NAMESPACE: import("@deepseek-ai/dsh-settings").SettingsNamespace;
18
+ /**
19
+ * Plugin settings. A schemastery schema, not a TypeScript type: the host validates the
20
+ * `settings.yaml` section against it and builds the settings UI from it, so a plain interface
21
+ * would be a switch nobody can find and nobody can check.
22
+ *
23
+ * `allowExec` is off by default and that default is the point. `$dsh/fs` is bounded — it takes a
24
+ * workspace-relative path and runs under the session's sandbox policy, so the worst it reaches is
25
+ * a file the user could have opened anyway. `$dsh/exec` takes an arbitrary command string, and a
26
+ * card is code a MODEL wrote, running in the user's browser, firing on their keystrokes. The
27
+ * sandbox policy still applies, but "whatever the agent's own bash tool may do" is a much larger
28
+ * surface than a path — and the user never approves a card's commands the way they approve the
29
+ * agent's.
30
+ */
31
+ export declare const Config: z<Schemastery.ObjectS<{
32
+ allowExec: z<boolean, boolean>;
33
+ }>, Schemastery.ObjectT<{
34
+ allowExec: z<boolean, boolean>;
35
+ }>>;
36
+ export type Config = ReturnType<typeof Config>;
37
+ export { ASSET_PREFIX, WASM_PATH } from "./contract-assets.ts";
38
+ /**
39
+ * An absolute path to one of the package's import maps, or undefined when it is not there.
40
+ *
41
+ * `existsSync` is the point. `fileURLToPath` only rejects a malformed URL — it happily returns a
42
+ * path to a file that does not exist, which is what this used to do: installed in a shape where
43
+ * the package root is not two levels up, the skill was handed a path that resolves to nothing
44
+ * and told the model to pass it to `-i`. The failure then surfaces as `genui check` reporting
45
+ * `Cannot find module "$dsh/fs"` on correct code, and the model "fixes" imports that were right.
46
+ */
47
+ export declare const resolvedMap: (relative: string, importMetaUrl: string) => string | undefined;
48
+ /** Exported for `test/routes.test.ts`: a prefix route that stops checking its pathname serves the whole prefix. */
49
+ export declare function serveAsset(req: IncomingMessage, res: ServerResponse, file: string): Promise<void>;
50
+ /**
51
+ * Serves one canvas file's current contents, or — with no `id` — the ids of every canvas
52
+ * in the workspace.
53
+ *
54
+ * The client could reconstruct a canvas from `write` tool arguments alone, and does while
55
+ * a write streams — but a model routinely follows a write with several `edit` calls, whose
56
+ * arguments carry a patch rather than the file. Reading the file is the only source that
57
+ * stays correct across every way it can change, including edits made outside the agent.
58
+ *
59
+ * Confined to the canvas directory by construction — the id is a path segment and the path
60
+ * is built from the contract — and to a live session's own workspace by the `cwd` check.
61
+ *
62
+ * That check is the security boundary, not a formality. This route answers any page the
63
+ * user has open: a simple GET triggers no preflight, so without it `?cwd=/anywhere` turns
64
+ * the plugin into a file-existence oracle for the whole disk. The client only ever sends
65
+ * the cwd it read off the current session, so matching against live sessions costs nothing.
66
+ */
67
+ /** Exported for `test/routes.test.ts`: the listing is the launcher's only source of truth and had no test. */
68
+ export declare function serveCanvas(liveWorkspaces: () => ReadonlySet<string>, req: IncomingMessage, res: ServerResponse): Promise<void>;
69
+ /** Context shape for the filesystem route; see the SessionStoreCtx note on why it is local. */
70
+ type FsCtx = {
71
+ fs: {
72
+ resolve: (path: string, opts?: {
73
+ cwd?: string;
74
+ }) => Promise<FsTargetLike>;
75
+ readText: (target: FsTargetLike) => Promise<string>;
76
+ readBytes: (target: FsTargetLike, signal: AbortSignal | undefined, maxBytes: number) => Promise<Uint8Array>;
77
+ listDir: (target: FsTargetLike) => Promise<{
78
+ name: string;
79
+ type?: string;
80
+ size?: number;
81
+ }[]>;
82
+ writeText: (target: FsTargetLike, content: string, expected?: undefined, signal?: AbortSignal, policy?: unknown) => Promise<unknown>;
83
+ };
84
+ sandboxPolicy: {
85
+ resolve: (request?: {
86
+ session?: unknown;
87
+ }) => unknown;
88
+ };
89
+ sessions: {
90
+ list: () => readonly {
91
+ id?: string;
92
+ header: {
93
+ cwd?: string;
94
+ };
95
+ }[];
96
+ };
97
+ };
98
+ type FsTargetLike = {
99
+ targetKey: unknown;
100
+ displayPath: string;
101
+ };
102
+ /**
103
+ * Reads, lists, and writes on behalf of a generated card.
104
+ *
105
+ * Everything goes through the host's `ctx.fs` carrying the session's own
106
+ * `ctx.sandboxPolicy`, so **a card may do exactly what the session may do** — under
107
+ * `read-only` the write is refused by the same fence that refuses the model's, with the
108
+ * same structured denial. Inventing a narrower boundary here would mean a second policy to
109
+ * keep in sync with the one the user actually sees in the composer.
110
+ *
111
+ * The `cwd` allowlist is still required, for the reason the canvas route documents: any page
112
+ * the user has open can call this, so without it the workspace is not the workspace.
113
+ */
114
+ /** Exported for `test/fs-route.test.ts`. */
115
+ export declare function serveFs(ctx: FsCtx, liveWorkspaces: () => ReadonlySet<string>, req: IncomingMessage, res: ServerResponse): Promise<void>;
116
+ /** Context shape for the shell route. `resolve` fills the executor's own defaults and caps. */
117
+ type ExecCtx = {
118
+ shell: {
119
+ resolve: (request: {
120
+ command: string;
121
+ workdir?: string;
122
+ timeoutMs?: number;
123
+ sandboxPolicy?: unknown;
124
+ signal?: AbortSignal;
125
+ }) => unknown;
126
+ run: (spec: unknown) => Promise<{
127
+ exitCode: number | null;
128
+ signal?: string | null;
129
+ timedOut?: boolean;
130
+ stdout: {
131
+ text: string;
132
+ truncated: boolean;
133
+ };
134
+ stderr: {
135
+ text: string;
136
+ truncated: boolean;
137
+ };
138
+ }>;
139
+ };
140
+ sandboxPolicy: {
141
+ resolve: (request?: {
142
+ session?: unknown;
143
+ }) => unknown;
144
+ };
145
+ sessions: {
146
+ list: () => readonly {
147
+ id?: string;
148
+ header: {
149
+ cwd?: string;
150
+ };
151
+ }[];
152
+ };
153
+ };
154
+ /**
155
+ * Runs one command on behalf of a generated card.
156
+ *
157
+ * The whole point is that a card can answer questions only a command can answer — git
158
+ * history, a test run, ripgrep across a big tree — without us re-implementing each one as a
159
+ * route. It carries the session's own sandbox policy, so this opens no door the model's own
160
+ * bash tool does not already have open, and a read-only session gets a read-only shell.
161
+ *
162
+ * A non-zero exit is a RESULT, not an error: a card wants to show `git status` failing in a
163
+ * non-repo as much as it wants to show it succeeding. Only infrastructure failures reject.
164
+ *
165
+ * **Why this does not go through `ctx.approval`, which is the seam for "may this action
166
+ * proceed?".** It is the right question and dsh's own `tool-bash` asks it — but the service
167
+ * cannot answer it here. `approval.request()` takes an `agent` and throws outright when the
168
+ * session has no open turn: *"approval.request() outside an open turn … Ask from inside the turn
169
+ * that needs the decision."* A card's command is the opposite of that — it fires on the reader's
170
+ * keystroke, long after the turn that wrote the card ended, with no agent on whose behalf to ask.
171
+ * `ctx.userQuestions.ask()` DOES work outside a turn (its `agent` is optional), so a per-command
172
+ * prompt is buildable; what stops it is that a card runs one command per keystroke, and a dialog
173
+ * per keystroke is not a safety feature. The setting is therefore about whether the CAPABILITY
174
+ * exists, and the per-command fence remains the session's own sandbox policy, which this passes
175
+ * through unchanged. Anything genuinely destructive belongs in `sendMessage`, where the user's
176
+ * next turn — and with it the whole approval machinery — is what runs it.
177
+ */
178
+ /** Exported for `test/exec-route.test.ts`. */
179
+ export declare function serveExec(ctx: ExecCtx, liveWorkspaces: () => ReadonlySet<string>, req: IncomingMessage, res: ServerResponse): Promise<void>;
180
+ /** Context shape for the search route. Only the two methods it calls, so a fake in a test is small. */
181
+ type WebCtx = {
182
+ web: {
183
+ search: (request: {
184
+ query: string;
185
+ maxResults?: number;
186
+ }, signal?: AbortSignal) => Promise<{
187
+ content?: string;
188
+ sources: readonly {
189
+ url: string;
190
+ title?: string;
191
+ snippet?: string;
192
+ publishedAt?: string;
193
+ }[];
194
+ truncated: boolean;
195
+ }>;
196
+ };
197
+ };
198
+ /**
199
+ * Runs one web search on behalf of a generated card.
200
+ *
201
+ * A card that wants live information — a price, a release date, what a package exports — otherwise
202
+ * has nothing: `fetch` from inside the surface is not the shape (no credentials, no CORS, no
203
+ * provider selection), and routing the question through `$dsh/ai` asks a model to recall rather
204
+ * than to look. `ctx.web` already owns provider selection, the result shape, and the truncation
205
+ * bound, so this forwards and does not re-decide any of it.
206
+ *
207
+ * SEARCH ONLY — see `WEB_SEARCH_PATH` for why `fetch` is not forwarded.
208
+ *
209
+ * `WebError` carries a `code` and the seam's own contract calls that set OPEN: a provider may
210
+ * raise a code this build has never seen. So the error is passed through as text rather than
211
+ * matched on, and the card decides what to show.
212
+ */
213
+ /** Exported for `test/web-search-route.test.ts`. */
214
+ export declare function serveWebSearch(ctx: WebCtx, liveWorkspaces: () => ReadonlySet<string>, req: IncomingMessage, res: ServerResponse): Promise<void>;
215
+ /** Context shape for the two services the AI route needs; see the SessionStoreCtx note. */
216
+ type LlmCtx = {
217
+ llm: {
218
+ stream: (options: {
219
+ provider: string;
220
+ model: string;
221
+ messages: readonly unknown[];
222
+ system?: string;
223
+ signal?: AbortSignal;
224
+ }) => AsyncIterable<{
225
+ type: string;
226
+ text?: string;
227
+ reason?: {
228
+ kind: string;
229
+ failure?: {
230
+ message?: string;
231
+ };
232
+ };
233
+ }>;
234
+ };
235
+ agentDefaultModel: {
236
+ currentSelection: () => {
237
+ provider: string;
238
+ model: string;
239
+ };
240
+ };
241
+ };
242
+ /**
243
+ * Streams one model call on behalf of a generated card.
244
+ *
245
+ * The card cannot call a provider itself — it has no credentials and should never be given
246
+ * any. `ctx.llm` already owns the adapter registry, the retry policy and the keys, and
247
+ * `agentDefaultModel` owns which model the app is set to, so this route is a forwarder:
248
+ * it converts a small JSON request into `llm.stream` and pipes the text deltas back.
249
+ *
250
+ * Same `cwd` allowlist as the canvas route, and for the same reason: any page the user has
251
+ * open can POST here, so without it this is an open model proxy for anything on the machine.
252
+ */
253
+ /** Exported for `test/ai-route.test.ts`. */
254
+ export declare function serveAi(ctx: LlmCtx, liveWorkspaces: () => ReadonlySet<string>, req: IncomingMessage, res: ServerResponse): Promise<void>;
255
+ export declare function apply(ctx: Context, config?: Config): void;
@@ -0,0 +1,13 @@
1
+ export declare const PROMPT_SECTION_NAME = "dsh-generative-ui:inline";
2
+ /** After tool guidance (100–199): this describes an output format, not the harness identity. */
3
+ export declare const PROMPT_SECTION_ORDER = 210;
4
+ /**
5
+ * The section, built for the capabilities this host actually exposes.
6
+ *
7
+ * `allowExec` is not cosmetic here. The closed-set sentence below ("these are the whole set") is
8
+ * load-bearing — it is what stops the model reasoning its way to a plausible sixth import — so it
9
+ * has to name the set that EXISTS. Documenting `$dsh/exec` on a host where the route is not
10
+ * registered teaches the model to write cards whose import fails, and a failed import takes the
11
+ * whole module down: the reader gets a blank card with nothing on screen naming the cause.
12
+ */
13
+ export declare const inlinePrompt: (allowExec?: boolean) => string;
@@ -0,0 +1,27 @@
1
+ export declare const SKILL_NAME = "generative-ui";
2
+ export declare const SKILL_DESCRIPTION = "How to decide between an inline ui4a/tsx block, a canvas file, and plain prose \u2014 and how to lay one out so it reads. Load it **before you decide**, not after \u2014 including when your first instinct is that prose is enough. Most of the questions that should have been an interface do not ask for one.";
3
+ /**
4
+ * The skill body.
5
+ *
6
+ * A function of the import-map path because that path is only known at runtime — the plugin
7
+ * lives wherever the profile installed it, and the model runs the checker from the workspace.
8
+ * Without the map, `check` reports `Cannot find module "$dsh/chat"` on every card that uses
9
+ * one, and a false error is worse than no check: the model goes and "fixes" it.
10
+ */
11
+ /**
12
+ * The paragraph about which import map serves which command.
13
+ *
14
+ * Built here rather than inline: nesting one template interpolation inside another inside the
15
+ * body is how this file broke twice, and the two maps have genuinely different lifetimes —
16
+ * the type one may exist while the stub one does not.
17
+ */
18
+ /** Exported for `test/skill.test.ts`: three states, and this file has broken on them twice. */
19
+ export declare function mapNotes(typesMap: string | undefined, standaloneMap: string | undefined): string;
20
+ /**
21
+ * The skill, for the capabilities this host exposes.
22
+ *
23
+ * With commands off the whole `## Running a command` section is cut rather than softened: it is
24
+ * ~90 lines that all assume `bash()` exists, and half a section describing a capability the host
25
+ * does not have is worse than none — the model reads the surviving half as permission.
26
+ */
27
+ export declare const skillBody: (typesMap: string | undefined, standaloneMap: string | undefined, allowExec?: boolean) => string;