@zenodinh/pi-render 0.0.0-stage → 0.1.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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -2
  3. package/index.ts +284 -0
  4. package/package.json +70 -5
  5. package/src/commands/canvas.ts +57 -0
  6. package/src/core/code-theme.ts +275 -0
  7. package/src/core/log.ts +57 -0
  8. package/src/core/paint.ts +64 -0
  9. package/src/core/registry.ts +135 -0
  10. package/src/core/settings.ts +119 -0
  11. package/src/core/types/code-theme.ts +24 -0
  12. package/src/core/types/host.ts +169 -0
  13. package/src/core/types/log.ts +28 -0
  14. package/src/core/types/paint.ts +54 -0
  15. package/src/core/types/registry.ts +44 -0
  16. package/src/core/types/settings.ts +31 -0
  17. package/src/core/types.ts +34 -0
  18. package/src/renderers/content/artifacts/cache.ts +234 -0
  19. package/src/renderers/content/artifacts/cards.ts +136 -0
  20. package/src/renderers/content/artifacts/engines.ts +396 -0
  21. package/src/renderers/content/artifacts/local-binary.ts +80 -0
  22. package/src/renderers/content/artifacts/prereqs.ts +128 -0
  23. package/src/renderers/content/artifacts/server.ts +181 -0
  24. package/src/renderers/content/code-panel.ts +161 -0
  25. package/src/renderers/content/image-card.ts +252 -0
  26. package/src/renderers/content/index.ts +79 -0
  27. package/src/renderers/content/json-panel.ts +116 -0
  28. package/src/renderers/content/table.ts +174 -0
  29. package/src/renderers/content/types.ts +20 -0
  30. package/src/renderers/tool/index.ts +113 -0
  31. package/src/renderers/tool/runtime.ts +267 -0
  32. package/src/renderers/tool/specs/bash.ts +168 -0
  33. package/src/renderers/tool/specs/codemode.ts +248 -0
  34. package/src/renderers/tool/specs/edit.ts +213 -0
  35. package/src/renderers/tool/specs/ls.ts +136 -0
  36. package/src/renderers/tool/specs/read.ts +296 -0
  37. package/src/renderers/tool/specs/search.ts +325 -0
  38. package/src/renderers/tool/specs/write.ts +142 -0
  39. package/src/renderers/tool/types.ts +45 -0
  40. package/themes/dracula-soft.json +81 -0
  41. package/themes/one-dark.json +80 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Quan Dinh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,111 @@
1
- # Temporary Holding Version
1
+ # pi-render
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A render-only presentation layer for **pi**: styled tool rows and a richer answer
4
+ canvas, with zero tool-name registrations. pi-render draws; the host and its peers
5
+ execute.
6
+
7
+ It registers one row renderer resolver and one markdown transformer, draws eight
8
+ built-in tool rows collapsed to a single line, and renders tables, code/JSON
9
+ panels, diagram fences, and images inside assistant answers. It adds no tool
10
+ schema, no prompt text, and no execution wrapper.
11
+
12
+ Replaces [pi-pretty-tui](https://github.com/zenodinh/pi-pretty-tui), fixing
13
+ [#21](https://github.com/zenodinh/pi-pretty-tui/issues/21) (expand freeze) and
14
+ [#22](https://github.com/zenodinh/pi-pretty-tui/issues/22) (table frame now takes
15
+ its color from the host theme, at 4.5:1 or better).
16
+
17
+ ## Requirements
18
+
19
+ pi 1.0.4 or later.
20
+
21
+ pi-render and pi-pretty-tui must never be active together: the two do not share a
22
+ session. One leaves pi's installed-packages list as the other enters.
23
+
24
+ ## Install
25
+
26
+ Installing is one edit to pi's installed-packages list. Both commands write
27
+ `~/.pi/agent/settings.json`.
28
+
29
+ ```bash
30
+ pi list # pi-pretty-tui's entry appears on its own line (a path)
31
+ pi remove <exactly what pi list printed for it> # a path, absolute or relative — not the npm: prefix
32
+ pi install npm:@zenodinh/pi-render
33
+ ```
34
+
35
+ Then re-enable the built-in `codemode` tool, which the predecessor displaced —
36
+ delete this entry from `~/.pi/agent/settings.json` if it is present:
37
+
38
+ ```json
39
+ "extensions": ["-builtin:codemode"]
40
+ ```
41
+
42
+ Start a new pi session. The `packages` list now holds the new entry and no
43
+ predecessor entry:
44
+
45
+ ```json
46
+ "packages": ["npm:@zenodinh/pi-render"]
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ Every tool row renders collapsed to one line, with errors summarized on that line.
52
+ `ctrl+o` — or a click — expands the row; expanding a call again reuses the cached
53
+ render, so there is no re-parse and no freeze.
54
+
55
+ `/canvas` reports the canvas surfaces and the artifact cache, and opens the
56
+ artifact browser in a TUI host. In a non-TUI host it prints a summary instead.
57
+
58
+ ## What you get, and what changes
59
+
60
+ | | pi-pretty-tui | pi-render |
61
+ |---|---|---|
62
+ | Tool rows | `bash`, `ls`, `read`, `edit`, `write`, `codemode`, `find`, `grep` | same eight, drawn through the resolver chain |
63
+ | Search execution | `find`/`grep` re-registered and routed to FFF via a library shim | the host's built-in `find`/`grep`; `@ff-labs/pi-fff` is an optional peer extension |
64
+ | Answer canvas | tables, panels, diagram fences, images | same, plus a host-theme-driven table frame |
65
+ | Chrome (user-message marker, prompt icon, working shimmer, thinking label) | drawn here | not drawn — install pi-zentui independently, or keep host defaults |
66
+ | Tool registrations | 8 | 0 |
67
+ | Diagnostics log file | yes | gone (removed with the slow-startup root cause) |
68
+
69
+ ## Themes
70
+
71
+ Two host theme definitions ship with the package and register additively: **Dracula
72
+ Soft** and **One Dark**. Pick either in pi's theme picker; installing the package
73
+ never changes your active theme.
74
+
75
+ Code blocks highlight with one of two shiki themes, `dracula-soft` (default) and
76
+ `one-dark-pro`.
77
+
78
+ ## Search
79
+
80
+ pi 1.0.4 already ships built-in `find` and `grep`, so search works after the
81
+ cutover with nothing else installed. `@ff-labs/pi-fff` is **optional**: it adds
82
+ FFF's frecency and fuzzy index on top.
83
+
84
+ ```bash
85
+ pi install npm:@ff-labs/pi-fff
86
+ ```
87
+
88
+ pi-fff owns the names and the execution; pi-render only styles the rows it
89
+ registers (`find`/`grep`, or `fffind`/`ffgrep` in its non-override modes).
90
+
91
+ ## Rollback
92
+
93
+ Reinstall the predecessor — the same hard switch, in reverse:
94
+
95
+ ```bash
96
+ git clone https://github.com/zenodinh/pi-pretty-tui ~/personal/pi-pretty-tui
97
+ cd ~/personal/pi-pretty-tui && npm install --omit=dev --omit=peer
98
+ pi remove npm:@zenodinh/pi-render
99
+ pi install ~/personal/pi-pretty-tui
100
+ ```
101
+
102
+ The predecessor loads pi-fff as a library and registers `find`/`grep` itself, so
103
+ also remove pi-fff as a separate extension if you installed it above:
104
+
105
+ ```bash
106
+ pi remove npm:@ff-labs/pi-fff
107
+ ```
108
+
109
+ ## License
110
+
111
+ MIT — see [LICENSE](LICENSE).
package/index.ts ADDED
@@ -0,0 +1,284 @@
1
+ /**
2
+ * index.ts — the composition root: the package's only file that calls pi.*.
3
+ *
4
+ * Why one file: every lane ends in a plain value (a resolver, a transformer, a command), so boot is a
5
+ * table of contents — build the registry, compose the two tables, register once per seam, and warm the
6
+ * engines the synchronous seams call. Each lane installs in its own try/catch: a broken lane costs
7
+ * itself, never the session (SA §2). The predecessor's ordering lore does not port — with zero
8
+ * registrations there is no inter-extension race left to sequence around.
9
+ *
10
+ * Boundary: the host arrives as the parameter. Nothing here parses markdown, draws a row or decides
11
+ * module state; the one adapter this file owns is the artifacts fence pass, which no lane supplies.
12
+ *
13
+ * shape: none — wiring only: `piRender` is the runtime unit, and every install below is straight-line.
14
+ */
15
+
16
+ import { readFileSync } from "node:fs";
17
+ import { isAbsolute, resolve } from "node:path";
18
+ import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
19
+ import { installCommands } from "./src/commands/canvas.ts";
20
+ import { CODE_THEME_MODULE, createCodeTheme, createShikiEngine } from "./src/core/code-theme.ts";
21
+ import { createLogger } from "./src/core/log.ts";
22
+ import { createContentPaint, createRowPaint } from "./src/core/paint.ts";
23
+ import { createRegistry } from "./src/core/registry.ts";
24
+ import { createSettingsStore } from "./src/core/settings.ts";
25
+ import type {
26
+ CommandContext,
27
+ ContentPaint,
28
+ ExtensionApi,
29
+ Logger,
30
+ ModuleDescriptor,
31
+ Registry,
32
+ Surface,
33
+ } from "./src/core/types.ts";
34
+ import { type ArtifactCache, createArtifactCache, renderCacheKey } from "./src/renderers/content/artifacts/cache.ts";
35
+ import { artifactTitle, renderArtifactCard } from "./src/renderers/content/artifacts/cards.ts";
36
+ import {
37
+ type DiagramForm,
38
+ type EngineStatus,
39
+ renderDiagram,
40
+ status,
41
+ warmup,
42
+ } from "./src/renderers/content/artifacts/engines.ts";
43
+ import { type ArtifactServer, createArtifactServer } from "./src/renderers/content/artifacts/server.ts";
44
+ import { createCodePanel, mapFencedBlocks, panelWidth } from "./src/renderers/content/code-panel.ts";
45
+ import { createImageCardSurface } from "./src/renderers/content/image-card.ts";
46
+ import { type ContentSurfaces, createContentTransformer } from "./src/renderers/content/index.ts";
47
+ import { createJsonPanel } from "./src/renderers/content/json-panel.ts";
48
+ import { table } from "./src/renderers/content/table.ts";
49
+ import { createMasterResolver, type SpecEntry } from "./src/renderers/tool/index.ts";
50
+ import { bashRow } from "./src/renderers/tool/specs/bash.ts";
51
+ import {
52
+ descriptor as codemodeModule,
53
+ name as codemodeName,
54
+ spec as codemodeSpec,
55
+ } from "./src/renderers/tool/specs/codemode.ts";
56
+ import { editRow } from "./src/renderers/tool/specs/edit.ts";
57
+ import { lsRow } from "./src/renderers/tool/specs/ls.ts";
58
+ import { readRow } from "./src/renderers/tool/specs/read.ts";
59
+ import { SEARCH_SPECS } from "./src/renderers/tool/specs/search.ts";
60
+ import { writeRow } from "./src/renderers/tool/specs/write.ts";
61
+
62
+ const SCOPE = "boot";
63
+
64
+ /** Relative transcript paths resolve against the launch directory — the transform context carries no cwd. */
65
+ const PROJECT_DIR = process.cwd();
66
+
67
+ /** The ten rendered names: the specs own them, so a pi-fff rename fails here instead of falling to the host. */
68
+ const ROW_RECORDS = [
69
+ bashRow,
70
+ lsRow,
71
+ readRow,
72
+ editRow,
73
+ writeRow,
74
+ { name: codemodeName, spec: codemodeSpec, descriptor: codemodeModule },
75
+ ...SEARCH_SPECS,
76
+ ];
77
+
78
+ /** Region 1's table: tool name → spec plus governing key (explicit, because search's four share one key). */
79
+ const ROW_SPECS: Record<string, SpecEntry> = Object.fromEntries(
80
+ ROW_RECORDS.map((row): [string, SpecEntry] => [row.name, { spec: row.spec, key: row.descriptor.key }]),
81
+ );
82
+
83
+ /**
84
+ * Region 2's modules: one per surface slot, keyed as the transformer polls them ("content." + slot,
85
+ * SA §6). The surface lanes export `rewrite` only, so their descriptors live here.
86
+ */
87
+ const CONTENT_MODULES: readonly ModuleDescriptor[] = [
88
+ { key: "content.table", name: "Table blocks", defaultEnabled: true, settings: [] },
89
+ { key: "content.codePanel", name: "Code panels", defaultEnabled: true, settings: [] },
90
+ { key: "content.jsonPanel", name: "JSON panels", defaultEnabled: true, settings: [] },
91
+ { key: "content.imageCard", name: "Image cards", defaultEnabled: true, settings: [] },
92
+ { key: "content.artifacts", name: "Artifact cards", defaultEnabled: true, settings: [] },
93
+ ];
94
+
95
+ /** Every module this package exposes — what the /render panel and both render seams read. */
96
+ const MODULES: readonly ModuleDescriptor[] = uniqueModules([
97
+ ...ROW_RECORDS.map((row) => row.descriptor),
98
+ CODE_THEME_MODULE,
99
+ ...CONTENT_MODULES,
100
+ ]);
101
+
102
+ /** shape: none — one dedupe pass over a fixed list; search's four names carry one shared descriptor. */
103
+ function uniqueModules(descriptors: readonly ModuleDescriptor[]): ModuleDescriptor[] {
104
+ const byKey = new Map<string, ModuleDescriptor>();
105
+ for (const descriptor of descriptors) byKey.set(descriptor.key, descriptor);
106
+ return [...byKey.values()];
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // The artifacts fence pass — the one adapter this file owns (SA §6)
111
+ // ---------------------------------------------------------------------------
112
+
113
+ /** Raster width the cache key is computed from; pi-tui's own image width (P transformer.ts:68). */
114
+ const ARTIFACT_WIDTH_CELLS = 60;
115
+ const ARTIFACT_MODE = "image";
116
+
117
+ /** shape: none — one membership test narrowing a fence language to a form the pass can render. */
118
+ function isArtifactForm(lang: string): lang is DiagramForm {
119
+ return lang === "plantuml" || lang === "dot" || lang === "svg" || lang === "html";
120
+ }
121
+
122
+ // ported from pi-pretty-tui/src/features/canvas/transformer.ts:134-226 — survives because: fence → card
123
+ // with a raw fence on failure is the whole artifacts surface; its settings gate and inline pixels do not.
124
+ /**
125
+ * shape: closure returning an object literal — trigger #4, one stateless Surface over the captured cache,
126
+ * server and log.
127
+ */
128
+ function createArtifactsSurface(cache: ArtifactCache, server: ArtifactServer, log: Logger): Surface {
129
+ /** One fence to one card, or undefined so the raw fence stays byte-identical. */
130
+ const card = (form: DiagramForm, source: string, width: number, paint: ContentPaint): string | undefined => {
131
+ try {
132
+ const key = renderCacheKey(form, source, ARTIFACT_MODE, ARTIFACT_WIDTH_CELLS);
133
+ const result = renderDiagram(form, source, {
134
+ mode: ARTIFACT_MODE,
135
+ widthCells: ARTIFACT_WIDTH_CELLS,
136
+ cache,
137
+ log,
138
+ });
139
+ // A diagram opens its vector; html renders to a raster the cache exposes only through get(), so
140
+ // its saved source is the open target (P fenceCard).
141
+ const path =
142
+ (result.kind === "svg" ? cache.vectorPath(key) : undefined) ??
143
+ cache.putSource(key, source, form === "plantuml" ? "puml" : form);
144
+ if (path === undefined) return undefined;
145
+ return renderArtifactCard(server, { typeLabel: form, title: artifactTitle(form, source), path, width }, paint);
146
+ } catch (error) {
147
+ // A missing binary or an unloadable engine: raw fence plus one line, never a throw into the transform.
148
+ log.logOnce(`artifact:${form}`, SCOPE, `artifact ${form} not rendered: ${messageOf(error)}`);
149
+ return undefined;
150
+ }
151
+ };
152
+
153
+ return {
154
+ rewrite(markdown, ctx, paint) {
155
+ const width = panelWidth(ctx.availableWidth);
156
+ const { text, changed } = mapFencedBlocks(markdown, (lang, code) =>
157
+ isArtifactForm(lang) ? card(lang, code, width, paint) : undefined,
158
+ );
159
+ return changed ? text : markdown;
160
+ },
161
+ };
162
+ }
163
+
164
+ /** The JSON pane's reader: a relative link resolves from the launch directory; every miss is undefined. */
165
+ // shape: none — one guarded read; the pane owns the degrade.
166
+ function readLinkedFile(href: string): string | undefined {
167
+ try {
168
+ return readFileSync(isAbsolute(href) ? href : resolve(PROJECT_DIR, href), "utf8");
169
+ } catch {
170
+ return undefined;
171
+ }
172
+ }
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // Lanes — one install per seam, each settled on its own
176
+ // ---------------------------------------------------------------------------
177
+
178
+ /** Region 1: the ten-name table behind one resolver — the package's only registerToolRenderer call. */
179
+ function installRows(pi: ExtensionApi, registry: Registry, log: Logger): void {
180
+ pi.registerToolRenderer(createMasterResolver(registry, ROW_SPECS, { paint: createRowPaint, log }));
181
+ }
182
+
183
+ /**
184
+ * The artifacts lane: warm the engines the synchronous fence pass calls, bind the one server, then hand
185
+ * the pass back. A failed bind degrades every card to a file:// link, so only a throw costs this slot.
186
+ */
187
+ async function installArtifacts(log: Logger, deps: BootDeps): Promise<Surface> {
188
+ await (deps.warmArtifactEngines ?? warmup)(log);
189
+ const server = (deps.createArtifactServer ?? ((logger: Logger) => createArtifactServer({ log: logger })))(log);
190
+ await server.start();
191
+ return createArtifactsSurface(createArtifactCache({ cacheDir: deps.cacheDir, log }), server, log);
192
+ }
193
+
194
+ /** Region 2: code theme, five surfaces, one transformer — the only registerMarkdownTransformer call. */
195
+ async function installContent(
196
+ pi: ExtensionApi,
197
+ registry: Registry,
198
+ log: Logger,
199
+ artifacts: Surface | undefined,
200
+ ): Promise<void> {
201
+ // The panel seam is synchronous while every engine start-up is not, so the highlight core is warmed
202
+ // here, before this registration can be reached.
203
+ const codeTheme = createCodeTheme(await createShikiEngine(log), { registry, log });
204
+ const surfaces: ContentSurfaces = {
205
+ table,
206
+ codePanel: createCodePanel(codeTheme),
207
+ jsonPanel: createJsonPanel(codeTheme, { read: readLinkedFile, log }),
208
+ imageCard: createImageCardSurface({
209
+ projectDir: PROJECT_DIR,
210
+ log: (line) => log.logLine(SCOPE, line),
211
+ }),
212
+ artifacts,
213
+ };
214
+ // The host's markdown theme is live per call, so its closures are captured once, here.
215
+ pi.registerMarkdownTransformer(
216
+ createContentTransformer(registry, surfaces, { paint: createContentPaint(getMarkdownTheme()), log }),
217
+ );
218
+ }
219
+
220
+ /** The /canvas view: what the artifact engines can draw this session. The browser pane is T-32's. */
221
+ function canvasView(ctx: CommandContext): Promise<void> {
222
+ const ready = Object.entries(status())
223
+ .filter(([, on]) => on)
224
+ .map(([id]) => id);
225
+ ctx.ui.notify(`canvas: artifact engines ready — ${ready.join(", ") || "none"}`);
226
+ return Promise.resolve();
227
+ }
228
+
229
+ /** Region 9: the one command, which reaches the rest of the package through the registry alone (SA §7). */
230
+ function installCanvasCommand(pi: ExtensionApi, registry: Registry): void {
231
+ installCommands(pi, { registry, canvasView });
232
+ }
233
+
234
+ /** shape: none — one guarded call; a lane reports itself and boot carries on. */
235
+ async function settle<T>(lane: string, log: Logger, install: () => T | Promise<T>): Promise<T | undefined> {
236
+ try {
237
+ return await install();
238
+ } catch (error) {
239
+ log.logOnce(`boot:${lane}`, SCOPE, `${lane} lane failed: ${messageOf(error)}`);
240
+ return undefined;
241
+ }
242
+ }
243
+
244
+ /** shape: none — one message extraction for a caught value of unknown type. */
245
+ function messageOf(error: unknown): string {
246
+ return error instanceof Error ? error.message : String(error);
247
+ }
248
+
249
+ /**
250
+ * The composition root's injected collaborators. Production passes none; a boot test replaces the heavy
251
+ * or fallible ones — the settings document, the wasm warm-up, the loopback bind, the cache directory —
252
+ * and can throw from one to prove a failed lane costs only itself.
253
+ */
254
+ export interface BootDeps {
255
+ /** Settings document path. Optional; the default is the host's agent directory. */
256
+ settingsPath?: string;
257
+ /** Artifact engine warm-up; resolves once the four engines are ready. Optional. */
258
+ warmArtifactEngines?: (log: Logger) => Promise<EngineStatus>;
259
+ /** Artifact server factory. Optional; the default binds one loopback port. */
260
+ createArtifactServer?: (log: Logger) => ArtifactServer;
261
+ /** Rendered-artifact cache directory. Optional; the default is the agent cache directory. */
262
+ cacheDir?: string;
263
+ }
264
+
265
+ /** shape: none — the boot sequence itself; every runtime lives in the lane that installs it. */
266
+ export default async function piRender(pi: ExtensionApi, deps: BootDeps = {}): Promise<void> {
267
+ const log = createLogger();
268
+ const registry = createRegistry(createSettingsStore({ path: deps.settingsPath, logger: log }), log);
269
+ for (const descriptor of MODULES) registry.defineModule(descriptor);
270
+
271
+ // Lane by lane: the artifacts pass resolves before content, because the transformer dispatches it.
272
+ await settle("rows", log, () => installRows(pi, registry, log));
273
+ const artifacts = await settle("artifacts", log, () => installArtifacts(log, deps));
274
+ await settle("content", log, () => installContent(pi, registry, log, artifacts));
275
+ await settle("commands", log, () => installCanvasCommand(pi, registry));
276
+
277
+ // Collapse-first reading model (owner decision): every tool row starts on one line.
278
+ // The host exposes this on the per-event UI context (ctx.ui), not on the pi API — only
279
+ // interactive mode implements it (the runner's non-UI fallback is a no-op stub), so the
280
+ // call rides session_start: real in TUI, harmless in text/rpc.
281
+ pi.on("session_start", (_event, ctx) => {
282
+ ctx.ui?.setToolsExpanded?.(false);
283
+ });
284
+ }
package/package.json CHANGED
@@ -1,6 +1,71 @@
1
1
  {
2
- "name": "@zenodinh/pi-render",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@zenodinh/pi-render",
3
+ "private": false,
4
+ "description": "Render-only presentation layer for pi: tool rows and content surfaces, zero tool registrations.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": ">=26"
9
+ },
10
+ "files": [
11
+ "index.ts",
12
+ "src",
13
+ "themes",
14
+ "!src/**/*.test.ts",
15
+ "!src/**/__snapshots__",
16
+ "!test",
17
+ "!themes/*.test.ts"
18
+ ],
19
+ "scripts": {
20
+ "check": "tsc --noEmit",
21
+ "lint": "biome check .",
22
+ "lint:fix": "biome check --write .",
23
+ "test": "vitest run --passWithNoTests",
24
+ "test:coverage": "vitest run --coverage --passWithNoTests",
25
+ "scan": "node scripts/security-scan.ts"
26
+ },
27
+ "pi": {
28
+ "extensions": [
29
+ "./index.ts"
30
+ ],
31
+ "themes": [
32
+ "themes/dracula-soft.json",
33
+ "themes/one-dark.json"
34
+ ]
35
+ },
36
+ "dependencies": {
37
+ "@resvg/resvg-wasm": "^2.6.2",
38
+ "@shikijs/cli": "^4.5.0",
39
+ "@shikijs/themes": "^4.5.0",
40
+ "@viz-js/viz": "^3.31.0",
41
+ "canvaskit-wasm": "^0.42.0",
42
+ "shiki": "4.5.0"
43
+ },
44
+ "peerDependencies": {
45
+ "@earendil-works/pi-ai": "*",
46
+ "@earendil-works/pi-coding-agent": "*",
47
+ "@earendil-works/pi-tui": "*"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "@earendil-works/pi-ai": {
51
+ "optional": true
52
+ },
53
+ "@earendil-works/pi-coding-agent": {
54
+ "optional": true
55
+ },
56
+ "@earendil-works/pi-tui": {
57
+ "optional": true
58
+ }
59
+ },
60
+ "devDependencies": {
61
+ "@biomejs/biome": "2.5.15",
62
+ "@earendil-works/pi-ai": "^1.0.0",
63
+ "@earendil-works/pi-coding-agent": "^1.0.0",
64
+ "@earendil-works/pi-tui": "^1.0.0",
65
+ "@types/node": "^26.6.4",
66
+ "@vitest/coverage-v8": "^5.0.3",
67
+ "typescript": "^7.0.2",
68
+ "vitest": "^5.0.3"
69
+ },
70
+ "version": "0.1.2"
71
+ }
@@ -0,0 +1,57 @@
1
+ // ported from pi-pretty-tui/src/features/canvas/index.ts:90-100 — survives because: the predecessor's
2
+ // /canvas registerCommand shape — a thin handler that opens the artifact through an injected seam and
3
+ // notifies instead of throwing when it cannot — is exactly the command contract pi-render needs. Its
4
+ // shortcuts, cmux routing, warmup and cache-dir plumbing stay behind (registration-adjacent chrome).
5
+
6
+ /**
7
+ * canvas.ts — the package's one command: /canvas opens the artifact browser, or reports its status.
8
+ *
9
+ * Boundary: `ctx` arrives from the host and is read as core/types/host.ts declares it — nothing here
10
+ * narrows or re-declares a host shape. The injected `deps.registry` is the command's ONLY channel to
11
+ * the rest of the package (SA §7), and it is read-only: /canvas reports module state and never writes
12
+ * it, so a disabled module's command cannot act anyway.
13
+ *
14
+ * Why `canvasView` is injected: `index.ts` (T-28) is the only file that composes lanes, so the real
15
+ * browser arrives as a parameter; this module never imports a renderer and its tests drive a fake view.
16
+ *
17
+ * shape: none — trigger #1 dispatch object does not apply: one command, two guards (host mode, module
18
+ * enabled) resolved in sequence, no per-kind handler table.
19
+ */
20
+
21
+ import type { CommandContext, ExtensionApi, Registry } from "../core/types.ts";
22
+
23
+ /**
24
+ * The content lane's artifacts module key (SA §6). A literal, not an import: `commands/` may not
25
+ * reach into a renderer lane for its constant (SA §1 rule 3).
26
+ */
27
+ const ARTIFACTS_KEY = "content.artifacts";
28
+
29
+ /** What /canvas needs from its callers: module state to report, and the view to open. */
30
+ export interface CommandsDeps {
31
+ /** Registry the command reads, e.g. isEnabled("content.artifacts"). Required; never written here. */
32
+ readonly registry: Registry;
33
+ /** Opens the artifact browser. Required; index.ts injects the real view, tests a fake one. */
34
+ readonly canvasView: (ctx: CommandContext) => Promise<void>;
35
+ }
36
+
37
+ /** shape: none — one interpolated line; a status table would restate the same registry read. */
38
+ function summarize(mode: CommandContext["mode"], enabled: boolean): string {
39
+ if (!enabled) return "canvas: content.artifacts is disabled — enable it to open the artifact browser";
40
+ return `canvas: content.artifacts is enabled — the artifact browser needs a TUI host (mode: ${mode})`;
41
+ }
42
+
43
+ /** shape: none — one guarded registration; the handler below is this module's only command. */
44
+ export function installCommands(pi: ExtensionApi, deps: CommandsDeps): void {
45
+ pi.registerCommand("canvas", {
46
+ description: "canvas: open the artifact browser, or report its status",
47
+ handler: async (_args, ctx) => {
48
+ const enabled = deps.registry.isEnabled(ARTIFACTS_KEY);
49
+ // A non-TUI host cannot draw the view and a disabled module has nothing to open: both report.
50
+ if (ctx.mode !== "tui" || !enabled) {
51
+ ctx.ui.notify(summarize(ctx.mode, enabled));
52
+ return;
53
+ }
54
+ await deps.canvasView(ctx);
55
+ },
56
+ });
57
+ }