acryl-development-canvas 0.1.0

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/lib/index.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ /** Cordis Host plugin: Development Canvas PTY table and loopback routes. */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ /** Stable Cordis plugin name. */
4
+ export declare const name = "development-canvas";
5
+ /** Loopback Web server required to publish canvas routes. */
6
+ export declare const inject: string[];
7
+ /**
8
+ * Activate Development Canvas as a neighboring Host plugin.
9
+ * Disable the Loader row to unload routes and kill leftover PTY sessions.
10
+ * @param ctx - Host context for this generation.
11
+ */
12
+ export declare function apply(ctx: Context): void;
package/lib/index.js ADDED
@@ -0,0 +1,473 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { statSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { isAbsolute, join } from "node:path";
6
+ import { spawn } from "node-pty";
7
+ //#region src/canvas-pty-contract.ts
8
+ /** Allowlisted Development Canvas PTY commands and loopback routes. */
9
+ const CANVAS_PTY_COMMAND_IDS = [
10
+ "shell",
11
+ "claude",
12
+ "codex",
13
+ "opencode",
14
+ "gemini",
15
+ "pi",
16
+ "grok",
17
+ "aider",
18
+ "goose",
19
+ "amp",
20
+ "kimi",
21
+ "cursor",
22
+ "hermes",
23
+ "qwen"
24
+ ];
25
+ const CANVAS_PTY_PATH = "/api/development-canvas/pty";
26
+ const CANVAS_PTY_INPUT_PATH = "/api/development-canvas/pty/input";
27
+ const CANVAS_PTY_RESIZE_PATH = "/api/development-canvas/pty/resize";
28
+ const CANVAS_PTY_CLOSE_PATH = "/api/development-canvas/pty/close";
29
+ const COMMAND_IDS = new Set(CANVAS_PTY_COMMAND_IDS);
30
+ /** @param value - unknown command id from JSON. */
31
+ function isCanvasPtyCommandId(value) {
32
+ return typeof value === "string" && COMMAND_IDS.has(value);
33
+ }
34
+ //#endregion
35
+ //#region src/canvas-pty.ts
36
+ /** Host-owned Development Canvas PTY sessions. UI observes; this module owns lifetime. */
37
+ const MAX_OUTPUT_CHARS = 64 * 1024;
38
+ const KILL_GRACE_MS = 1e3;
39
+ const DEFAULT_COLS = 120;
40
+ const DEFAULT_ROWS = 40;
41
+ /**
42
+ * A Finder-launched macOS app gets a minimal PATH (`/usr/bin:/bin:…`), so a
43
+ * bare agent name such as `claude` fails to spawn even when it is installed.
44
+ * Resolve bare commands to an absolute executable by merging `env.PATH` with
45
+ * the user's login-shell PATH (cached per registry).
46
+ */
47
+ function canvasPtySpawnDirs(env, platform) {
48
+ const dirs = [];
49
+ const add = (raw) => {
50
+ if (raw === void 0) return;
51
+ for (const dir of raw.split(platform === "win32" ? ";" : ":")) if (dir.length > 0 && !dirs.includes(dir)) dirs.push(dir);
52
+ };
53
+ add(env.PATH);
54
+ const shell = env.SHELL ?? (platform === "win32" ? env.ComSpec ?? "cmd.exe" : "/bin/sh");
55
+ const probes = platform === "win32" ? [[
56
+ "/d",
57
+ "/s",
58
+ "/c",
59
+ "echo %PATH%"
60
+ ]] : [["-lic", "echo $PATH"], ["-lc", "echo $PATH"]];
61
+ for (const args of probes) try {
62
+ add(execFileSync(shell, args, {
63
+ encoding: "utf8",
64
+ env,
65
+ timeout: 3e3,
66
+ stdio: [
67
+ "ignore",
68
+ "pipe",
69
+ "ignore"
70
+ ]
71
+ }).trimEnd());
72
+ if (dirs.length > 0) break;
73
+ } catch {}
74
+ return dirs;
75
+ }
76
+ /**
77
+ * Return an absolute executable path for `command`, or `undefined` when none of
78
+ * `dirs` contains an executable `command`. Absolute/relative paths pass through.
79
+ */
80
+ function resolveCanvasPtyCommand(command, dirs, platform) {
81
+ if (isAbsolute(command) || command.includes("/") || command.includes("\\")) return command;
82
+ const names = platform === "win32" ? [
83
+ command,
84
+ `${command}.exe`,
85
+ `${command}.cmd`,
86
+ `${command}.bat`
87
+ ] : [command];
88
+ for (const dir of dirs) for (const name of names) try {
89
+ const candidate = join(dir, name);
90
+ const stat = statSync(candidate);
91
+ if (stat.isFile() && (platform === "win32" || (stat.mode & 73) !== 0)) return candidate;
92
+ } catch {}
93
+ }
94
+ /** The spawn cwd: process.cwd() when it exists, else the user's home directory. */
95
+ function defaultSpawnCwd() {
96
+ try {
97
+ if (statSync(process.cwd()).isDirectory()) return process.cwd();
98
+ } catch {}
99
+ return homedir();
100
+ }
101
+ /**
102
+ * Resolve argv for one allowlisted canvas command.
103
+ * @param commandId - catalog id from the tab.
104
+ * @param platform - Host process.platform.
105
+ * @param env - environment used to pick SHELL / ComSpec.
106
+ */
107
+ function planCanvasPtyCommand(commandId, platform, env) {
108
+ if (commandId === "shell") {
109
+ if (platform === "win32") return {
110
+ command: env.ComSpec ?? "cmd.exe",
111
+ args: []
112
+ };
113
+ return {
114
+ command: env.SHELL ?? "/bin/sh",
115
+ args: []
116
+ };
117
+ }
118
+ return {
119
+ command: commandId,
120
+ args: []
121
+ };
122
+ }
123
+ function appendOutput(current, chunk) {
124
+ const next = current + chunk;
125
+ if (next.length <= MAX_OUTPUT_CHARS) return next;
126
+ return next.slice(next.length - MAX_OUTPUT_CHARS);
127
+ }
128
+ /** Table of live native PTY sessions for one Development Canvas Host fiber. */
129
+ var CanvasPtyRegistry = class {
130
+ sessions = /* @__PURE__ */ new Map();
131
+ spawnImpl;
132
+ env;
133
+ cwd;
134
+ platform;
135
+ createId;
136
+ spawnDirs;
137
+ constructor(options = {}) {
138
+ this.spawnImpl = options.spawn ?? defaultSpawn;
139
+ this.env = options.env ?? process.env;
140
+ this.cwd = options.cwd ?? defaultSpawnCwd();
141
+ this.platform = options.platform ?? process.platform;
142
+ this.createId = options.createId ?? (() => `pty_${randomUUID()}`);
143
+ this.spawnDirs = canvasPtySpawnDirs(this.env, this.platform);
144
+ }
145
+ /**
146
+ * Start one allowlisted command inside a real terminal.
147
+ * @param commandId - catalog id from the Terminal/agent tab.
148
+ */
149
+ start(commandId) {
150
+ if (!isCanvasPtyCommandId(commandId)) throw new Error("acryl-development-canvas: unknown canvas PTY command");
151
+ const plan = planCanvasPtyCommand(commandId, this.platform, this.env);
152
+ const id = this.createId();
153
+ let process;
154
+ try {
155
+ const resolved = resolveCanvasPtyCommand(plan.command, this.spawnDirs, this.platform);
156
+ if (resolved === void 0 && !plan.command.includes("/") && !plan.command.includes("\\")) throw new Error(`canvas PTY command not found in PATH: ${plan.command}`);
157
+ process = this.spawnImpl(resolved ?? plan.command, plan.args, {
158
+ cwd: this.cwd,
159
+ env: {
160
+ ...this.env,
161
+ TERM: this.env.TERM ?? "xterm-256color"
162
+ },
163
+ name: "xterm-256color",
164
+ cols: DEFAULT_COLS,
165
+ rows: DEFAULT_ROWS
166
+ });
167
+ } catch (cause) {
168
+ throw new Error(`acryl-development-canvas: canvas PTY spawn failed: ${cause instanceof Error ? cause.message : String(cause)}`);
169
+ }
170
+ const subscriptions = [];
171
+ const session = {
172
+ id,
173
+ process,
174
+ subscriptions,
175
+ status: "running",
176
+ output: "",
177
+ exitCode: null,
178
+ error: null
179
+ };
180
+ subscriptions.push(process.onData((chunk) => {
181
+ session.output = appendOutput(session.output, chunk);
182
+ }));
183
+ subscriptions.push(process.onExit(({ exitCode, signal }) => {
184
+ session.status = "exited";
185
+ session.exitCode = exitCode;
186
+ if (session.error === null && signal !== void 0 && signal !== 0) session.error = `signal ${String(signal)}`;
187
+ }));
188
+ this.sessions.set(id, session);
189
+ return this.view(session);
190
+ }
191
+ /** Write exact terminal input bytes. */
192
+ write(id, data) {
193
+ const session = this.require(id);
194
+ if (session.status !== "running") throw new Error("acryl-development-canvas: canvas PTY is not running");
195
+ session.process.write(data);
196
+ }
197
+ /** Resize one live terminal. */
198
+ resize(id, cols, rows) {
199
+ const session = this.require(id);
200
+ if (session.status !== "running") return;
201
+ session.process.resize(cols, rows);
202
+ }
203
+ /** Snapshot one session for the renderer. */
204
+ read(id) {
205
+ return this.view(this.require(id));
206
+ }
207
+ /** Stop one session. Idempotent. */
208
+ async close(id) {
209
+ const session = this.sessions.get(id);
210
+ if (session === void 0) return;
211
+ await stopPty(session);
212
+ for (const subscription of session.subscriptions) subscription.dispose();
213
+ this.sessions.delete(id);
214
+ }
215
+ /** Stop every session owned by this fiber. */
216
+ async disposeAll() {
217
+ const ids = [...this.sessions.keys()];
218
+ await Promise.all(ids.map(async (id) => this.close(id)));
219
+ }
220
+ require(id) {
221
+ const session = this.sessions.get(id);
222
+ if (session === void 0) throw new Error("acryl-development-canvas: unknown canvas PTY session");
223
+ return session;
224
+ }
225
+ view(session) {
226
+ return {
227
+ id: session.id,
228
+ status: session.status,
229
+ output: session.output,
230
+ exitCode: session.exitCode,
231
+ error: session.error
232
+ };
233
+ }
234
+ };
235
+ function defaultSpawn(command, args, options) {
236
+ return spawn(command, [...args], {
237
+ cwd: options.cwd,
238
+ env: options.env,
239
+ name: options.name,
240
+ cols: options.cols,
241
+ rows: options.rows
242
+ });
243
+ }
244
+ async function stopPty(session) {
245
+ if (session.status !== "running") return;
246
+ await new Promise((resolve) => {
247
+ let done = false;
248
+ const finish = () => {
249
+ if (done) return;
250
+ done = true;
251
+ clearTimeout(timer);
252
+ exitSubscription.dispose();
253
+ resolve();
254
+ };
255
+ const exitSubscription = session.process.onExit(finish);
256
+ const timer = setTimeout(() => {
257
+ try {
258
+ session.process.kill("SIGKILL");
259
+ } catch {}
260
+ finish();
261
+ }, KILL_GRACE_MS);
262
+ timer.unref?.();
263
+ try {
264
+ session.process.kill();
265
+ } catch {
266
+ finish();
267
+ }
268
+ });
269
+ }
270
+ //#endregion
271
+ //#region src/canvas-pty-route.ts
272
+ const MAX_BODY_BYTES = 16 * 1024;
273
+ var BodyTooLargeError = class extends Error {};
274
+ function finishJson(res, statusCode, value, allow) {
275
+ res.statusCode = statusCode;
276
+ res.setHeader("cache-control", "no-store");
277
+ res.setHeader("content-type", "application/json; charset=utf-8");
278
+ res.setHeader("x-content-type-options", "nosniff");
279
+ if (allow !== void 0) res.setHeader("allow", allow);
280
+ res.end(JSON.stringify(value));
281
+ }
282
+ function error(message) {
283
+ return { error: message };
284
+ }
285
+ function isLoopbackHostname(hostname) {
286
+ return hostname === "127.0.0.1" || hostname === "[::1]";
287
+ }
288
+ function isLoopbackAddress(address) {
289
+ if (address === void 0) return false;
290
+ if (address === "::1" || address === "127.0.0.1") return true;
291
+ if (address.startsWith("::ffff:")) return address.slice(7).startsWith("127.");
292
+ return address.startsWith("127.");
293
+ }
294
+ function isSameOriginLoopbackRequest(req, expectedOrigin, mutating) {
295
+ let expected;
296
+ try {
297
+ expected = new URL(expectedOrigin);
298
+ } catch {
299
+ return false;
300
+ }
301
+ if (expected.origin !== expectedOrigin || expected.protocol !== "http:" || expected.username !== "" || expected.password !== "" || !isLoopbackHostname(expected.hostname)) return false;
302
+ if (!isLoopbackAddress(req.socket.remoteAddress)) return false;
303
+ if (req.headers.host?.toLowerCase() !== expected.host.toLowerCase()) return false;
304
+ let origin;
305
+ try {
306
+ origin = req.headers.origin === void 0 ? void 0 : new URL(req.headers.origin).origin;
307
+ if (req.headers.origin !== void 0 && origin !== req.headers.origin) origin = void 0;
308
+ } catch {
309
+ origin = void 0;
310
+ }
311
+ if (origin === expected.origin) return req.headers["sec-fetch-site"] === void 0 || req.headers["sec-fetch-site"] === "same-origin";
312
+ if (mutating) return false;
313
+ try {
314
+ return req.headers["sec-fetch-site"] === "same-origin" && req.headers.referer !== void 0 && new URL(req.headers.referer).origin === expected.origin;
315
+ } catch {
316
+ return false;
317
+ }
318
+ }
319
+ async function readJson(req) {
320
+ const declaredLength = req.headers["content-length"];
321
+ if (declaredLength !== void 0) {
322
+ if (!/^\d+$/.test(declaredLength)) throw new SyntaxError("invalid content length");
323
+ if (Number(declaredLength) > MAX_BODY_BYTES) throw new BodyTooLargeError();
324
+ }
325
+ let size = 0;
326
+ const chunks = [];
327
+ for await (const chunk of req) {
328
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
329
+ size += buffer.byteLength;
330
+ if (size > MAX_BODY_BYTES) throw new BodyTooLargeError();
331
+ chunks.push(buffer);
332
+ }
333
+ if (chunks.length === 0) return {};
334
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
335
+ }
336
+ function isObject(value) {
337
+ return value !== null && typeof value === "object" && !Array.isArray(value);
338
+ }
339
+ /**
340
+ * POST start and GET snapshot for the Canvas PTY route.
341
+ */
342
+ async function handleCanvasPtyRequest(req, res, expectedOrigin, registry, reportError) {
343
+ if (req.method === "GET") {
344
+ if (!isSameOriginLoopbackRequest(req, expectedOrigin, false)) return finishJson(res, 403, error("forbidden"));
345
+ const id = new URL(req.url ?? "", "http://127.0.0.1").searchParams.get("id");
346
+ if (id === null || id.length === 0) return finishJson(res, 400, error("invalid canvas PTY request"));
347
+ try {
348
+ return finishJson(res, 200, registry.read(id), "GET");
349
+ } catch (cause) {
350
+ reportError("read canvas PTY", cause);
351
+ return finishJson(res, 404, error("unknown canvas PTY session"));
352
+ }
353
+ }
354
+ if (req.method !== "POST") return finishJson(res, 405, error("method not allowed"), "POST");
355
+ if (!isSameOriginLoopbackRequest(req, expectedOrigin, true)) return finishJson(res, 403, error("forbidden"));
356
+ let body;
357
+ try {
358
+ body = await readJson(req);
359
+ } catch (cause) {
360
+ if (cause instanceof BodyTooLargeError) return finishJson(res, 413, error("body too large"));
361
+ return finishJson(res, 400, error("invalid canvas PTY request"));
362
+ }
363
+ if (!isObject(body) || !isCanvasPtyCommandId(body.commandId) || Object.keys(body).length !== 1) return finishJson(res, 400, error("invalid canvas PTY request"));
364
+ try {
365
+ return finishJson(res, 200, registry.start(body.commandId));
366
+ } catch (cause) {
367
+ reportError("start canvas PTY", cause);
368
+ return finishJson(res, 500, error(cause instanceof Error ? cause.message : "canvas PTY spawn failed"));
369
+ }
370
+ }
371
+ /** POST stdin to a live session. */
372
+ async function handleCanvasPtyInputRequest(req, res, expectedOrigin, registry, reportError) {
373
+ if (req.method !== "POST") return finishJson(res, 405, error("method not allowed"), "POST");
374
+ if (!isSameOriginLoopbackRequest(req, expectedOrigin, true)) return finishJson(res, 403, error("forbidden"));
375
+ let body;
376
+ try {
377
+ body = await readJson(req);
378
+ } catch {
379
+ return finishJson(res, 400, error("invalid canvas PTY input"));
380
+ }
381
+ if (!isObject(body) || typeof body.id !== "string" || typeof body.data !== "string" || Object.keys(body).length !== 2) return finishJson(res, 400, error("invalid canvas PTY input"));
382
+ try {
383
+ registry.write(body.id, body.data);
384
+ return finishJson(res, 200, { accepted: true });
385
+ } catch (cause) {
386
+ reportError("write canvas PTY", cause);
387
+ return finishJson(res, 404, error("unknown canvas PTY session"));
388
+ }
389
+ }
390
+ /** POST terminal dimensions to a live session. */
391
+ async function handleCanvasPtyResizeRequest(req, res, expectedOrigin, registry, reportError) {
392
+ if (req.method !== "POST") return finishJson(res, 405, error("method not allowed"), "POST");
393
+ if (!isSameOriginLoopbackRequest(req, expectedOrigin, true)) return finishJson(res, 403, error("forbidden"));
394
+ let body;
395
+ try {
396
+ body = await readJson(req);
397
+ } catch {
398
+ return finishJson(res, 400, error("invalid canvas PTY resize"));
399
+ }
400
+ if (!isObject(body) || typeof body.id !== "string" || !Number.isInteger(body.cols) || !Number.isInteger(body.rows) || typeof body.cols !== "number" || typeof body.rows !== "number" || body.cols < 2 || body.cols > 500 || body.rows < 1 || body.rows > 200 || Object.keys(body).length !== 3) return finishJson(res, 400, error("invalid canvas PTY resize"));
401
+ try {
402
+ registry.resize(body.id, body.cols, body.rows);
403
+ return finishJson(res, 200, { accepted: true });
404
+ } catch (cause) {
405
+ reportError("resize canvas PTY", cause);
406
+ return finishJson(res, 404, error("unknown canvas PTY session"));
407
+ }
408
+ }
409
+ /** POST idempotent close. */
410
+ async function handleCanvasPtyCloseRequest(req, res, expectedOrigin, registry, reportError) {
411
+ if (req.method !== "POST") return finishJson(res, 405, error("method not allowed"), "POST");
412
+ if (!isSameOriginLoopbackRequest(req, expectedOrigin, true)) return finishJson(res, 403, error("forbidden"));
413
+ let body;
414
+ try {
415
+ body = await readJson(req);
416
+ } catch {
417
+ return finishJson(res, 400, error("invalid canvas PTY close"));
418
+ }
419
+ if (!isObject(body) || typeof body.id !== "string" || Object.keys(body).length !== 1) return finishJson(res, 400, error("invalid canvas PTY close"));
420
+ try {
421
+ await registry.close(body.id);
422
+ return finishJson(res, 200, { accepted: true });
423
+ } catch (cause) {
424
+ reportError("close canvas PTY", cause);
425
+ return finishJson(res, 500, error("canvas PTY could not be closed"));
426
+ }
427
+ }
428
+ //#endregion
429
+ //#region src/index.ts
430
+ /** Stable Cordis plugin name. */
431
+ const name = "development-canvas";
432
+ /** Loopback Web server required to publish canvas routes. */
433
+ const inject = ["webServer"];
434
+ /**
435
+ * Activate Development Canvas as a neighboring Host plugin.
436
+ * Disable the Loader row to unload routes and kill leftover PTY sessions.
437
+ * @param ctx - Host context for this generation.
438
+ */
439
+ function apply(ctx) {
440
+ if (ctx.webServer.host !== "127.0.0.1") throw new Error("acryl-development-canvas: development canvas requires a loopback Web server");
441
+ const rendererOrigin = `http://127.0.0.1:${String(ctx.webServer.port)}`;
442
+ const reportHostError = (operation, cause) => {
443
+ ctx.logger.error(`acryl-development-canvas: failed to ${operation}: ${cause instanceof Error ? cause.message : String(cause)}`);
444
+ };
445
+ ctx.effect(() => {
446
+ const canvasPty = new CanvasPtyRegistry();
447
+ const releases = [];
448
+ try {
449
+ const ptyRoutes = [
450
+ [CANVAS_PTY_PATH, handleCanvasPtyRequest],
451
+ [CANVAS_PTY_INPUT_PATH, handleCanvasPtyInputRequest],
452
+ [CANVAS_PTY_RESIZE_PATH, handleCanvasPtyResizeRequest],
453
+ [CANVAS_PTY_CLOSE_PATH, handleCanvasPtyCloseRequest]
454
+ ];
455
+ for (const [path, handler] of ptyRoutes) releases.push(ctx.webServer.register({
456
+ kind: "exact",
457
+ path,
458
+ handler: (req, res) => handler(req, res, rendererOrigin, canvasPty, reportHostError)
459
+ }));
460
+ } catch (cause) {
461
+ for (const release of releases.reverse()) release();
462
+ throw cause;
463
+ }
464
+ return async () => {
465
+ for (const release of releases.reverse()) release();
466
+ await canvasPty.disposeAll();
467
+ };
468
+ }, "development-canvas: routes and PTY table");
469
+ }
470
+ //#endregion
471
+ export { apply, inject, name };
472
+
473
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["spawnPty"],"sources":["../src/canvas-pty-contract.ts","../src/canvas-pty.ts","../src/canvas-pty-route.ts","../src/index.ts"],"sourcesContent":["/** Allowlisted Development Canvas PTY commands and loopback routes. */\n\nexport const CANVAS_PTY_COMMAND_IDS = [\n 'shell',\n 'claude',\n 'codex',\n 'opencode',\n 'gemini',\n 'pi',\n 'grok',\n 'aider',\n 'goose',\n 'amp',\n 'kimi',\n 'cursor',\n 'hermes',\n 'qwen',\n] as const\n\nexport type CanvasPtyCommandId = (typeof CANVAS_PTY_COMMAND_IDS)[number]\n\nexport const CANVAS_PTY_PATH = '/api/development-canvas/pty'\nexport const CANVAS_PTY_INPUT_PATH = '/api/development-canvas/pty/input'\nexport const CANVAS_PTY_RESIZE_PATH = '/api/development-canvas/pty/resize'\nexport const CANVAS_PTY_CLOSE_PATH = '/api/development-canvas/pty/close'\n\nconst COMMAND_IDS = new Set<string>(CANVAS_PTY_COMMAND_IDS)\n\n/** @param value - unknown command id from JSON. */\nexport function isCanvasPtyCommandId(value: unknown): value is CanvasPtyCommandId {\n return typeof value === 'string' && COMMAND_IDS.has(value)\n}\n\nexport type CanvasPtyStatus = 'starting' | 'running' | 'exited' | 'error'\n\nexport interface CanvasPtyView {\n readonly id: string\n readonly status: CanvasPtyStatus\n readonly output: string\n readonly exitCode: number | null\n readonly error: string | null\n}\n","/** Host-owned Development Canvas PTY sessions. UI observes; this module owns lifetime. */\n\nimport { execFileSync } from 'node:child_process'\nimport { randomUUID } from 'node:crypto'\nimport { statSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { isAbsolute, join } from 'node:path'\nimport { spawn as spawnPty } from 'node-pty'\nimport type { CanvasPtyCommandId, CanvasPtyStatus, CanvasPtyView } from './canvas-pty-contract.ts'\nimport { isCanvasPtyCommandId } from './canvas-pty-contract.ts'\n\nconst MAX_OUTPUT_CHARS = 64 * 1024\nconst KILL_GRACE_MS = 1_000\nconst DEFAULT_COLS = 120\nconst DEFAULT_ROWS = 40\n\n/**\n * A Finder-launched macOS app gets a minimal PATH (`/usr/bin:/bin:…`), so a\n * bare agent name such as `claude` fails to spawn even when it is installed.\n * Resolve bare commands to an absolute executable by merging `env.PATH` with\n * the user's login-shell PATH (cached per registry).\n */\nexport function canvasPtySpawnDirs(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): string[] {\n const dirs: string[] = []\n const add = (raw: string | undefined): void => {\n if (raw === undefined) return\n for (const dir of raw.split(platform === 'win32' ? ';' : ':')) {\n if (dir.length > 0 && !dirs.includes(dir)) dirs.push(dir)\n }\n }\n add(env.PATH)\n const shell = env.SHELL ?? (platform === 'win32' ? env.ComSpec ?? 'cmd.exe' : '/bin/sh')\n const probes = platform === 'win32'\n ? [['/d', '/s', '/c', 'echo %PATH%']]\n : [['-lic', 'echo $PATH'], ['-lc', 'echo $PATH']]\n for (const args of probes) {\n try {\n const out = execFileSync(shell, args, {\n encoding: 'utf8',\n env,\n timeout: 3_000,\n stdio: ['ignore', 'pipe', 'ignore'],\n })\n add(out.trimEnd())\n if (dirs.length > 0) break\n } catch {\n // Log-in shell not available; keep the env PATH we already have.\n }\n }\n return dirs\n}\n\n/**\n * Return an absolute executable path for `command`, or `undefined` when none of\n * `dirs` contains an executable `command`. Absolute/relative paths pass through.\n */\nexport function resolveCanvasPtyCommand(\n command: string,\n dirs: readonly string[],\n platform: NodeJS.Platform,\n): string | undefined {\n if (isAbsolute(command) || command.includes('/') || command.includes('\\\\')) return command\n const names = platform === 'win32'\n ? [command, `${command}.exe`, `${command}.cmd`, `${command}.bat`]\n : [command]\n for (const dir of dirs) {\n for (const name of names) {\n try {\n const candidate = join(dir, name)\n const stat = statSync(candidate)\n if (stat.isFile() && (platform === 'win32' || (stat.mode & 0o111) !== 0)) return candidate\n } catch {\n // Not present here; keep searching.\n }\n }\n }\n return undefined\n}\n\n/** The spawn cwd: process.cwd() when it exists, else the user's home directory. */\nfunction defaultSpawnCwd(): string {\n try {\n if (statSync(process.cwd()).isDirectory()) return process.cwd()\n } catch {\n // Fall through to the home directory.\n }\n return homedir()\n}\n\nexport interface CanvasPtyProcess {\n onData(listener: (data: string) => void): { dispose(): void }\n onExit(listener: (event: { exitCode: number; signal?: number }) => void): { dispose(): void }\n write(data: string): void\n resize(cols: number, rows: number): void\n kill(signal?: string): void\n}\n\n/** PTY spawn injected so unit tests never launch a real process. */\nexport type CanvasPtySpawn = (\n command: string,\n args: readonly string[],\n options: {\n readonly cwd: string\n readonly env: NodeJS.ProcessEnv\n readonly name: string\n readonly cols: number\n readonly rows: number\n },\n) => CanvasPtyProcess\n\nexport interface CanvasPtySpawnPlan {\n readonly command: string\n readonly args: readonly string[]\n}\n\nexport interface CanvasPtyRegistryOptions {\n readonly spawn?: CanvasPtySpawn\n readonly env?: NodeJS.ProcessEnv\n readonly cwd?: string\n readonly platform?: NodeJS.Platform\n readonly createId?: () => string\n}\n\ninterface LiveSession {\n readonly id: string\n readonly process: CanvasPtyProcess\n readonly subscriptions: readonly { dispose(): void }[]\n status: CanvasPtyStatus\n output: string\n exitCode: number | null\n error: string | null\n}\n\n/**\n * Resolve argv for one allowlisted canvas command.\n * @param commandId - catalog id from the tab.\n * @param platform - Host process.platform.\n * @param env - environment used to pick SHELL / ComSpec.\n */\nexport function planCanvasPtyCommand(\n commandId: CanvasPtyCommandId,\n platform: NodeJS.Platform,\n env: NodeJS.ProcessEnv,\n): CanvasPtySpawnPlan {\n if (commandId === 'shell') {\n if (platform === 'win32') {\n return { command: env.ComSpec ?? 'cmd.exe', args: [] }\n }\n return { command: env.SHELL ?? '/bin/sh', args: [] }\n }\n return { command: commandId, args: [] }\n}\n\nfunction appendOutput(current: string, chunk: string): string {\n const next = current + chunk\n if (next.length <= MAX_OUTPUT_CHARS) return next\n return next.slice(next.length - MAX_OUTPUT_CHARS)\n}\n\n/** Table of live native PTY sessions for one Development Canvas Host fiber. */\nexport class CanvasPtyRegistry {\n private readonly sessions = new Map<string, LiveSession>()\n private readonly spawnImpl: CanvasPtySpawn\n private readonly env: NodeJS.ProcessEnv\n private readonly cwd: string\n private readonly platform: NodeJS.Platform\n private readonly createId: () => string\n private readonly spawnDirs: string[]\n\n constructor(options: CanvasPtyRegistryOptions = {}) {\n this.spawnImpl = options.spawn ?? defaultSpawn\n this.env = options.env ?? process.env\n this.cwd = options.cwd ?? defaultSpawnCwd()\n this.platform = options.platform ?? process.platform\n this.createId = options.createId ?? (() => `pty_${randomUUID()}`)\n this.spawnDirs = canvasPtySpawnDirs(this.env, this.platform)\n }\n\n /**\n * Start one allowlisted command inside a real terminal.\n * @param commandId - catalog id from the Terminal/agent tab.\n */\n start(commandId: string): CanvasPtyView {\n if (!isCanvasPtyCommandId(commandId)) {\n throw new Error('acryl-development-canvas: unknown canvas PTY command')\n }\n const plan = planCanvasPtyCommand(commandId, this.platform, this.env)\n const id = this.createId()\n let process: CanvasPtyProcess\n try {\n const resolved = resolveCanvasPtyCommand(plan.command, this.spawnDirs, this.platform)\n if (resolved === undefined && !plan.command.includes('/') && !plan.command.includes('\\\\')) {\n throw new Error(`canvas PTY command not found in PATH: ${plan.command}`)\n }\n process = this.spawnImpl(resolved ?? plan.command, plan.args, {\n cwd: this.cwd,\n env: { ...this.env, TERM: this.env.TERM ?? 'xterm-256color' },\n name: 'xterm-256color',\n cols: DEFAULT_COLS,\n rows: DEFAULT_ROWS,\n })\n } catch (cause) {\n throw new Error(\n `acryl-development-canvas: canvas PTY spawn failed: ${cause instanceof Error ? cause.message : String(cause)}`,\n )\n }\n const subscriptions: { dispose(): void }[] = []\n const session: LiveSession = {\n id,\n process,\n subscriptions,\n status: 'running',\n output: '',\n exitCode: null,\n error: null,\n }\n subscriptions.push(process.onData((chunk) => {\n session.output = appendOutput(session.output, chunk)\n }))\n subscriptions.push(process.onExit(({ exitCode, signal }) => {\n session.status = 'exited'\n session.exitCode = exitCode\n if (session.error === null && signal !== undefined && signal !== 0) {\n session.error = `signal ${String(signal)}`\n }\n }))\n this.sessions.set(id, session)\n return this.view(session)\n }\n\n /** Write exact terminal input bytes. */\n write(id: string, data: string): void {\n const session = this.require(id)\n if (session.status !== 'running') {\n throw new Error('acryl-development-canvas: canvas PTY is not running')\n }\n session.process.write(data)\n }\n\n /** Resize one live terminal. */\n resize(id: string, cols: number, rows: number): void {\n const session = this.require(id)\n if (session.status !== 'running') return\n session.process.resize(cols, rows)\n }\n\n /** Snapshot one session for the renderer. */\n read(id: string): CanvasPtyView {\n return this.view(this.require(id))\n }\n\n /** Stop one session. Idempotent. */\n async close(id: string): Promise<void> {\n const session = this.sessions.get(id)\n if (session === undefined) return\n await stopPty(session)\n for (const subscription of session.subscriptions) subscription.dispose()\n this.sessions.delete(id)\n }\n\n /** Stop every session owned by this fiber. */\n async disposeAll(): Promise<void> {\n const ids = [...this.sessions.keys()]\n await Promise.all(ids.map(async id => this.close(id)))\n }\n\n private require(id: string): LiveSession {\n const session = this.sessions.get(id)\n if (session === undefined) {\n throw new Error('acryl-development-canvas: unknown canvas PTY session')\n }\n return session\n }\n\n private view(session: LiveSession): CanvasPtyView {\n return {\n id: session.id,\n status: session.status,\n output: session.output,\n exitCode: session.exitCode,\n error: session.error,\n }\n }\n}\n\nfunction defaultSpawn(\n command: string,\n args: readonly string[],\n options: {\n readonly cwd: string\n readonly env: NodeJS.ProcessEnv\n readonly name: string\n readonly cols: number\n readonly rows: number\n },\n): CanvasPtyProcess {\n return spawnPty(command, [...args], {\n cwd: options.cwd,\n env: options.env as Record<string, string>,\n name: options.name,\n cols: options.cols,\n rows: options.rows,\n })\n}\n\nasync function stopPty(session: LiveSession): Promise<void> {\n if (session.status !== 'running') return\n await new Promise<void>((resolve) => {\n let done = false\n const finish = (): void => {\n if (done) return\n done = true\n clearTimeout(timer)\n exitSubscription.dispose()\n resolve()\n }\n const exitSubscription = session.process.onExit(finish)\n const timer = setTimeout(() => {\n try {\n session.process.kill('SIGKILL')\n } catch {\n // The PTY may have exited between the status check and timeout.\n }\n finish()\n }, KILL_GRACE_MS)\n timer.unref?.()\n try {\n session.process.kill()\n } catch {\n finish()\n }\n })\n}\n","/** Same-origin JSON handlers for Development Canvas PTY sessions. */\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { CanvasPtyRegistry } from './canvas-pty.ts'\nimport {\n CANVAS_PTY_CLOSE_PATH,\n CANVAS_PTY_INPUT_PATH,\n CANVAS_PTY_PATH,\n CANVAS_PTY_RESIZE_PATH,\n isCanvasPtyCommandId,\n} from './canvas-pty-contract.ts'\n\nconst MAX_BODY_BYTES = 16 * 1024\n\nclass BodyTooLargeError extends Error {}\n\nfunction finishJson(\n res: ServerResponse,\n statusCode: number,\n value: object,\n allow?: 'GET' | 'POST',\n): void {\n res.statusCode = statusCode\n res.setHeader('cache-control', 'no-store')\n res.setHeader('content-type', 'application/json; charset=utf-8')\n res.setHeader('x-content-type-options', 'nosniff')\n if (allow !== undefined) res.setHeader('allow', allow)\n res.end(JSON.stringify(value))\n}\n\nfunction error(message: string): { readonly error: string } {\n return { error: message }\n}\n\nfunction isLoopbackHostname(hostname: string): boolean {\n return hostname === '127.0.0.1' || hostname === '[::1]'\n}\n\nfunction isLoopbackAddress(address: string | undefined): boolean {\n if (address === undefined) return false\n if (address === '::1' || address === '127.0.0.1') return true\n if (address.startsWith('::ffff:')) return address.slice('::ffff:'.length).startsWith('127.')\n return address.startsWith('127.')\n}\n\nfunction isSameOriginLoopbackRequest(\n req: IncomingMessage,\n expectedOrigin: string,\n mutating: boolean,\n): boolean {\n let expected: URL\n try {\n expected = new URL(expectedOrigin)\n } catch {\n return false\n }\n if (expected.origin !== expectedOrigin || expected.protocol !== 'http:'\n || expected.username !== '' || expected.password !== ''\n || !isLoopbackHostname(expected.hostname)) return false\n if (!isLoopbackAddress(req.socket.remoteAddress)) return false\n if (req.headers.host?.toLowerCase() !== expected.host.toLowerCase()) return false\n let origin: string | undefined\n try {\n origin = req.headers.origin === undefined ? undefined : new URL(req.headers.origin).origin\n if (req.headers.origin !== undefined && origin !== req.headers.origin) origin = undefined\n } catch {\n origin = undefined\n }\n if (origin === expected.origin) {\n return req.headers['sec-fetch-site'] === undefined || req.headers['sec-fetch-site'] === 'same-origin'\n }\n if (mutating) return false\n try {\n return req.headers['sec-fetch-site'] === 'same-origin'\n && req.headers.referer !== undefined\n && new URL(req.headers.referer).origin === expected.origin\n } catch {\n return false\n }\n}\n\nasync function readJson(req: IncomingMessage): Promise<unknown> {\n const declaredLength = req.headers['content-length']\n if (declaredLength !== undefined) {\n if (!/^\\d+$/.test(declaredLength)) throw new SyntaxError('invalid content length')\n if (Number(declaredLength) > MAX_BODY_BYTES) throw new BodyTooLargeError()\n }\n let size = 0\n const chunks: Buffer[] = []\n for await (const chunk of req) {\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array)\n size += buffer.byteLength\n if (size > MAX_BODY_BYTES) throw new BodyTooLargeError()\n chunks.push(buffer)\n }\n if (chunks.length === 0) return {}\n return JSON.parse(Buffer.concat(chunks).toString('utf8')) as unknown\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n\n/**\n * POST start and GET snapshot for the Canvas PTY route.\n */\nexport async function handleCanvasPtyRequest(\n req: IncomingMessage,\n res: ServerResponse,\n expectedOrigin: string,\n registry: CanvasPtyRegistry,\n reportError: (operation: string, cause: unknown) => void,\n): Promise<void> {\n if (req.method === 'GET') {\n if (!isSameOriginLoopbackRequest(req, expectedOrigin, false)) {\n return finishJson(res, 403, error('forbidden'))\n }\n const id = new URL(req.url ?? '', 'http://127.0.0.1').searchParams.get('id')\n if (id === null || id.length === 0) return finishJson(res, 400, error('invalid canvas PTY request'))\n try {\n return finishJson(res, 200, registry.read(id), 'GET')\n } catch (cause) {\n reportError('read canvas PTY', cause)\n return finishJson(res, 404, error('unknown canvas PTY session'))\n }\n }\n if (req.method !== 'POST') return finishJson(res, 405, error('method not allowed'), 'POST')\n if (!isSameOriginLoopbackRequest(req, expectedOrigin, true)) {\n return finishJson(res, 403, error('forbidden'))\n }\n let body: unknown\n try {\n body = await readJson(req)\n } catch (cause) {\n if (cause instanceof BodyTooLargeError) return finishJson(res, 413, error('body too large'))\n return finishJson(res, 400, error('invalid canvas PTY request'))\n }\n if (!isObject(body) || !isCanvasPtyCommandId(body.commandId) || Object.keys(body).length !== 1) {\n return finishJson(res, 400, error('invalid canvas PTY request'))\n }\n try {\n return finishJson(res, 200, registry.start(body.commandId))\n } catch (cause) {\n reportError('start canvas PTY', cause)\n return finishJson(res, 500, error(cause instanceof Error ? cause.message : 'canvas PTY spawn failed'))\n }\n}\n\n/** POST stdin to a live session. */\nexport async function handleCanvasPtyInputRequest(\n req: IncomingMessage,\n res: ServerResponse,\n expectedOrigin: string,\n registry: CanvasPtyRegistry,\n reportError: (operation: string, cause: unknown) => void,\n): Promise<void> {\n if (req.method !== 'POST') return finishJson(res, 405, error('method not allowed'), 'POST')\n if (!isSameOriginLoopbackRequest(req, expectedOrigin, true)) {\n return finishJson(res, 403, error('forbidden'))\n }\n let body: unknown\n try {\n body = await readJson(req)\n } catch {\n return finishJson(res, 400, error('invalid canvas PTY input'))\n }\n if (!isObject(body) || typeof body.id !== 'string' || typeof body.data !== 'string'\n || Object.keys(body).length !== 2) {\n return finishJson(res, 400, error('invalid canvas PTY input'))\n }\n try {\n registry.write(body.id, body.data)\n return finishJson(res, 200, { accepted: true })\n } catch (cause) {\n reportError('write canvas PTY', cause)\n return finishJson(res, 404, error('unknown canvas PTY session'))\n }\n}\n\n/** POST terminal dimensions to a live session. */\nexport async function handleCanvasPtyResizeRequest(\n req: IncomingMessage,\n res: ServerResponse,\n expectedOrigin: string,\n registry: CanvasPtyRegistry,\n reportError: (operation: string, cause: unknown) => void,\n): Promise<void> {\n if (req.method !== 'POST') return finishJson(res, 405, error('method not allowed'), 'POST')\n if (!isSameOriginLoopbackRequest(req, expectedOrigin, true)) {\n return finishJson(res, 403, error('forbidden'))\n }\n let body: unknown\n try {\n body = await readJson(req)\n } catch {\n return finishJson(res, 400, error('invalid canvas PTY resize'))\n }\n if (!isObject(body) || typeof body.id !== 'string'\n || !Number.isInteger(body.cols) || !Number.isInteger(body.rows)\n || typeof body.cols !== 'number' || typeof body.rows !== 'number'\n || body.cols < 2 || body.cols > 500 || body.rows < 1 || body.rows > 200\n || Object.keys(body).length !== 3) {\n return finishJson(res, 400, error('invalid canvas PTY resize'))\n }\n try {\n registry.resize(body.id, body.cols, body.rows)\n return finishJson(res, 200, { accepted: true })\n } catch (cause) {\n reportError('resize canvas PTY', cause)\n return finishJson(res, 404, error('unknown canvas PTY session'))\n }\n}\n\n/** POST idempotent close. */\nexport async function handleCanvasPtyCloseRequest(\n req: IncomingMessage,\n res: ServerResponse,\n expectedOrigin: string,\n registry: CanvasPtyRegistry,\n reportError: (operation: string, cause: unknown) => void,\n): Promise<void> {\n if (req.method !== 'POST') return finishJson(res, 405, error('method not allowed'), 'POST')\n if (!isSameOriginLoopbackRequest(req, expectedOrigin, true)) {\n return finishJson(res, 403, error('forbidden'))\n }\n let body: unknown\n try {\n body = await readJson(req)\n } catch {\n return finishJson(res, 400, error('invalid canvas PTY close'))\n }\n if (!isObject(body) || typeof body.id !== 'string' || Object.keys(body).length !== 1) {\n return finishJson(res, 400, error('invalid canvas PTY close'))\n }\n try {\n await registry.close(body.id)\n return finishJson(res, 200, { accepted: true })\n } catch (cause) {\n reportError('close canvas PTY', cause)\n return finishJson(res, 500, error('canvas PTY could not be closed'))\n }\n}\n\nexport const canvasPtyRoutePaths = {\n pty: CANVAS_PTY_PATH,\n input: CANVAS_PTY_INPUT_PATH,\n resize: CANVAS_PTY_RESIZE_PATH,\n close: CANVAS_PTY_CLOSE_PATH,\n} as const\n","/** Cordis Host plugin: Development Canvas PTY table and loopback routes. */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-host-webserver'\nimport { CanvasPtyRegistry } from './canvas-pty.ts'\nimport {\n CANVAS_PTY_CLOSE_PATH,\n CANVAS_PTY_INPUT_PATH,\n CANVAS_PTY_PATH,\n CANVAS_PTY_RESIZE_PATH,\n} from './canvas-pty-contract.ts'\nimport {\n handleCanvasPtyCloseRequest,\n handleCanvasPtyInputRequest,\n handleCanvasPtyRequest,\n handleCanvasPtyResizeRequest,\n} from './canvas-pty-route.ts'\n\n/** Stable Cordis plugin name. */\nexport const name = 'development-canvas'\n\n/** Loopback Web server required to publish canvas routes. */\nexport const inject = ['webServer']\n\n/**\n * Activate Development Canvas as a neighboring Host plugin.\n * Disable the Loader row to unload routes and kill leftover PTY sessions.\n * @param ctx - Host context for this generation.\n */\nexport function apply(ctx: Context): void {\n if (ctx.webServer.host !== '127.0.0.1') {\n throw new Error('acryl-development-canvas: development canvas requires a loopback Web server')\n }\n const rendererOrigin = `http://127.0.0.1:${String(ctx.webServer.port)}`\n const reportHostError = (operation: string, cause: unknown): void => {\n ctx.logger.error(\n `acryl-development-canvas: failed to ${operation}: ${cause instanceof Error ? cause.message : String(cause)}`,\n )\n }\n ctx.effect(() => {\n const canvasPty = new CanvasPtyRegistry()\n const releases: Array<() => void> = []\n try {\n const ptyRoutes = [\n [CANVAS_PTY_PATH, handleCanvasPtyRequest],\n [CANVAS_PTY_INPUT_PATH, handleCanvasPtyInputRequest],\n [CANVAS_PTY_RESIZE_PATH, handleCanvasPtyResizeRequest],\n [CANVAS_PTY_CLOSE_PATH, handleCanvasPtyCloseRequest],\n ] as const\n for (const [path, handler] of ptyRoutes) {\n releases.push(ctx.webServer.register({\n kind: 'exact',\n path,\n handler: (req, res) => handler(req, res, rendererOrigin, canvasPty, reportHostError),\n }))\n }\n } catch (cause) {\n for (const release of releases.reverse()) release()\n throw cause\n }\n return async () => {\n for (const release of releases.reverse()) release()\n await canvasPty.disposeAll()\n }\n }, 'development-canvas: routes and PTY table')\n}\n"],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /** Allowlisted Development Canvas PTY commands and loopback routes. */
2
+ export declare const CANVAS_PTY_COMMAND_IDS: readonly ["shell", "claude", "codex", "opencode", "gemini", "pi", "grok", "aider", "goose", "amp", "kimi", "cursor", "hermes", "qwen"];
3
+ export type CanvasPtyCommandId = (typeof CANVAS_PTY_COMMAND_IDS)[number];
4
+ export declare const CANVAS_PTY_PATH = "/api/development-canvas/pty";
5
+ export declare const CANVAS_PTY_INPUT_PATH = "/api/development-canvas/pty/input";
6
+ export declare const CANVAS_PTY_RESIZE_PATH = "/api/development-canvas/pty/resize";
7
+ export declare const CANVAS_PTY_CLOSE_PATH = "/api/development-canvas/pty/close";
8
+ /** @param value - unknown command id from JSON. */
9
+ export declare function isCanvasPtyCommandId(value: unknown): value is CanvasPtyCommandId;
10
+ export type CanvasPtyStatus = 'starting' | 'running' | 'exited' | 'error';
11
+ export interface CanvasPtyView {
12
+ readonly id: string;
13
+ readonly status: CanvasPtyStatus;
14
+ readonly output: string;
15
+ readonly exitCode: number | null;
16
+ readonly error: string | null;
17
+ }
@@ -0,0 +1,15 @@
1
+ /** Advanced-mode ADE workspace: one tab fills the main content area. */
2
+ import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
3
+ import type { UseSessions } from '@deepseek-ai/dsh-client-ui-session/client';
4
+ import { type ReactNode } from 'react';
5
+ import { type CanvasPtyApi } from './pty-api.ts';
6
+ export type DevelopmentCanvasProps = Omit<PropsRuntime<'root'>, 'useSessions'> & {
7
+ readonly renderConversation: () => ReactNode;
8
+ readonly ptyApi?: CanvasPtyApi;
9
+ readonly useSessions: UseSessions;
10
+ };
11
+ /**
12
+ * Orca-style tab workspace that replaces the advanced-mode conversation surface.
13
+ * @param props.renderConversation - upstream Chat slot, rendered by the Chat tab.
14
+ */
15
+ export declare function DevelopmentCanvas({ renderConversation, ptyApi, useSessions }: DevelopmentCanvasProps): import("react").JSX.Element;
@@ -0,0 +1,14 @@
1
+ /** Labels for allowlisted Terminal / agent tabs. */
2
+ import type { CanvasPtyCommandId } from '../../canvas-pty-contract.ts';
3
+ export interface CanvasAgentCommand {
4
+ readonly id: CanvasPtyCommandId;
5
+ readonly label: string;
6
+ }
7
+ export interface CanvasSurfaceAction {
8
+ readonly kind: 'pty' | 'file' | 'browser';
9
+ readonly commandId?: CanvasPtyCommandId;
10
+ readonly label: string;
11
+ }
12
+ export declare const CANVAS_SURFACE_ACTIONS: readonly CanvasSurfaceAction[];
13
+ export declare const CANVAS_AGENT_COMMANDS: readonly CanvasAgentCommand[];
14
+ export declare function labelForCommand(id: CanvasPtyCommandId): string;
@@ -0,0 +1,13 @@
1
+ /** Same-origin client for Host canvas PTY sessions. */
2
+ import { type CanvasPtyCommandId, type CanvasPtyView } from '../../canvas-pty-contract.ts';
3
+ type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
4
+ export interface CanvasPtyApi {
5
+ start(commandId: CanvasPtyCommandId): Promise<CanvasPtyView>;
6
+ read(id: string): Promise<CanvasPtyView>;
7
+ write(id: string, data: string): Promise<void>;
8
+ resize(id: string, cols: number, rows: number): Promise<void>;
9
+ close(id: string): Promise<void>;
10
+ }
11
+ /** @param fetcher - injected for tests; defaults to window.fetch. */
12
+ export declare function createCanvasPtyApi(fetcher?: FetchLike): CanvasPtyApi;
13
+ export {};
@@ -0,0 +1,22 @@
1
+ import type { CanvasPtyCommandId, CanvasPtyView } from '../../canvas-pty-contract.ts';
2
+ import type { CanvasPtyApi } from './pty-api.ts';
3
+ /**
4
+ * Client-activation owner for every Host PTY opened by one Canvas contribution.
5
+ * The Cordis declaration effect disposes this module before its slot disappears.
6
+ */
7
+ export declare class CanvasPtyClient implements CanvasPtyApi {
8
+ private readonly api;
9
+ private readonly sessions;
10
+ private readonly starting;
11
+ private disposed;
12
+ private disposePromise;
13
+ constructor(api: CanvasPtyApi);
14
+ start(commandId: CanvasPtyCommandId): Promise<CanvasPtyView>;
15
+ read(id: string): Promise<CanvasPtyView>;
16
+ write(id: string, data: string): Promise<void>;
17
+ resize(id: string, cols: number, rows: number): Promise<void>;
18
+ close(id: string): Promise<void>;
19
+ /** Close all sessions, including starts that settle while disposal is active. */
20
+ dispose(): Promise<void>;
21
+ private settleAndClose;
22
+ }
@@ -0,0 +1,12 @@
1
+ import { DevelopmentCanvasState } from './state.ts';
2
+ export interface CanvasSessionNavigationProjection {
3
+ readonly current: string | undefined;
4
+ readonly blank: boolean | undefined;
5
+ }
6
+ /**
7
+ * Keep DSH session navigation visible inside the Canvas tab workspace.
8
+ * Re-selecting a blank session is the New Session signal even when its id is
9
+ * unchanged. Ordinary updates to a non-blank current session leave the active
10
+ * Canvas tool tab alone.
11
+ */
12
+ export declare function synchronizeCanvasWithSessionNavigation(canvas: DevelopmentCanvasState, previousCurrent: string | undefined, projection: CanvasSessionNavigationProjection): string | undefined;