@zseven-w/dsh-openpencil 0.1.0-rc.1

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 (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/THIRD_PARTY_NOTICES.md +69 -0
  4. package/cordis.patch.yml +6 -0
  5. package/docs/images/dsh-openpencil-overview.png +0 -0
  6. package/lib/client/details-compat.d.ts +28 -0
  7. package/lib/client/details-compat.js +10 -0
  8. package/lib/client/editor-bridge.d.ts +84 -0
  9. package/lib/client/editor-bridge.js +127 -0
  10. package/lib/client/editor-dock-layout.d.ts +15 -0
  11. package/lib/client/editor-dock-layout.js +43 -0
  12. package/lib/client/editor-recovery.d.ts +27 -0
  13. package/lib/client/editor-recovery.js +83 -0
  14. package/lib/client/editor-successor.d.ts +28 -0
  15. package/lib/client/editor-successor.js +109 -0
  16. package/lib/client/presentation-hydration.d.ts +36 -0
  17. package/lib/client/presentation-hydration.js +168 -0
  18. package/lib/client/selection-polling.d.ts +24 -0
  19. package/lib/client/selection-polling.js +79 -0
  20. package/lib/client/selection-store.d.ts +26 -0
  21. package/lib/client/selection-store.js +85 -0
  22. package/lib/client.js +4033 -0
  23. package/lib/design-tools.d.ts +21 -0
  24. package/lib/design-tools.js +214 -0
  25. package/lib/editor-host.d.ts +62 -0
  26. package/lib/editor-host.js +1147 -0
  27. package/lib/editor-recovery.d.ts +38 -0
  28. package/lib/editor-recovery.js +319 -0
  29. package/lib/index.d.ts +24 -0
  30. package/lib/index.js +132 -0
  31. package/lib/mcp-client.d.ts +47 -0
  32. package/lib/mcp-client.js +148 -0
  33. package/lib/new-tool.d.ts +23 -0
  34. package/lib/new-tool.js +136 -0
  35. package/lib/presentation-hydration.d.ts +55 -0
  36. package/lib/presentation-hydration.js +561 -0
  37. package/lib/renderer.d.ts +203 -0
  38. package/lib/renderer.js +772 -0
  39. package/lib/tool-names.d.ts +13 -0
  40. package/lib/tool-names.js +19 -0
  41. package/lib/tool.d.ts +27 -0
  42. package/lib/tool.js +221 -0
  43. package/lib/types/client/details-compat.d.ts +28 -0
  44. package/lib/types/client/editor-bridge.d.ts +84 -0
  45. package/lib/types/client/editor-dock-layout.d.ts +15 -0
  46. package/lib/types/client/editor-modal.d.ts +54 -0
  47. package/lib/types/client/editor-panel.d.ts +127 -0
  48. package/lib/types/client/editor-recovery.d.ts +27 -0
  49. package/lib/types/client/editor-successor.d.ts +28 -0
  50. package/lib/types/client/editor-workbench-host.d.ts +43 -0
  51. package/lib/types/client/frame-gallery.d.ts +98 -0
  52. package/lib/types/client/index.d.ts +176 -0
  53. package/lib/types/client/presentation-hydration.d.ts +36 -0
  54. package/lib/types/client/selection-dock.d.ts +18 -0
  55. package/lib/types/client/selection-polling.d.ts +24 -0
  56. package/lib/types/client/selection-store.d.ts +26 -0
  57. package/lib/types/tool-names.d.ts +13 -0
  58. package/lib/viewer-assets/canvaskit/canvaskit.js +288 -0
  59. package/lib/viewer-assets/canvaskit/canvaskit.wasm +0 -0
  60. package/lib/viewer-assets/manifest.json +22 -0
  61. package/lib/viewer-assets/op_web_sdk_bg.wasm +0 -0
  62. package/lib/viewer-assets/sdk.js +225 -0
  63. package/lib/viewer-assets.d.ts +74 -0
  64. package/lib/viewer-assets.js +303 -0
  65. package/package.json +95 -0
  66. package/scripts/build-client.mjs +27 -0
  67. package/scripts/run-tool.mjs +63 -0
  68. package/scripts/sync-viewer-assets.mjs +304 -0
  69. package/scripts/test-host.mjs +311 -0
  70. package/scripts/test-viewer-assets.mjs +54 -0
  71. package/src/client/details-compat.ts +44 -0
  72. package/src/client/editor-bridge.ts +170 -0
  73. package/src/client/editor-dock-layout.ts +56 -0
  74. package/src/client/editor-modal.tsx +607 -0
  75. package/src/client/editor-panel.tsx +823 -0
  76. package/src/client/editor-recovery.ts +116 -0
  77. package/src/client/editor-successor.ts +136 -0
  78. package/src/client/editor-workbench-host.tsx +280 -0
  79. package/src/client/frame-gallery.tsx +546 -0
  80. package/src/client/index.tsx +987 -0
  81. package/src/client/presentation-hydration.ts +210 -0
  82. package/src/client/react-dom-compat.d.ts +22 -0
  83. package/src/client/selection-dock.tsx +89 -0
  84. package/src/client/selection-polling.ts +93 -0
  85. package/src/client/selection-store.ts +108 -0
  86. package/src/design-tools.ts +242 -0
  87. package/src/editor-host.ts +1238 -0
  88. package/src/editor-recovery.ts +374 -0
  89. package/src/index.ts +219 -0
  90. package/src/mcp-client.ts +193 -0
  91. package/src/new-tool.ts +160 -0
  92. package/src/presentation-hydration.ts +634 -0
  93. package/src/renderer.ts +884 -0
  94. package/src/tool-names.ts +21 -0
  95. package/src/tool.ts +258 -0
  96. package/src/viewer-assets.ts +353 -0
@@ -0,0 +1,148 @@
1
+ /** Strict loopback JSON-RPC client for one managed OpenPencil editor. */
2
+ const MCP_TIMEOUT_MS = 20_000;
3
+ const MAX_MCP_RESPONSE_BYTES = 2 * 1024 * 1024;
4
+ function isRecord(value) {
5
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
6
+ }
7
+ function finite(value) {
8
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
9
+ }
10
+ function optionalString(value) {
11
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
12
+ }
13
+ function textFromContent(value) {
14
+ if (!Array.isArray(value))
15
+ return '';
16
+ const parts = [];
17
+ for (const item of value) {
18
+ if (isRecord(item) && item.type === 'text' && typeof item.text === 'string')
19
+ parts.push(item.text);
20
+ }
21
+ return parts.join('\n');
22
+ }
23
+ /** Decode a JSON-RPC tools/call envelope, including MCP's HTTP-200 errors. */
24
+ export function parseOpenPencilMcpResponse(tool, value) {
25
+ if (!isRecord(value))
26
+ throw new Error(`OpenPencil MCP ${tool} returned an invalid JSON-RPC response`);
27
+ if (isRecord(value.error)) {
28
+ const message = optionalString(value.error.message) ?? JSON.stringify(value.error);
29
+ throw new Error(`OpenPencil MCP ${tool} failed: ${message}`);
30
+ }
31
+ if (!isRecord(value.result))
32
+ throw new Error(`OpenPencil MCP ${tool} omitted its result`);
33
+ const text = textFromContent(value.result.content);
34
+ if (value.result.isError === true) {
35
+ throw new Error(`OpenPencil MCP ${tool} failed${text === '' ? '' : `: ${text}`}`);
36
+ }
37
+ let decoded = text;
38
+ if (text !== '') {
39
+ try {
40
+ decoded = JSON.parse(text);
41
+ }
42
+ catch {
43
+ // Some first-party tools intentionally return human-readable text.
44
+ }
45
+ }
46
+ if (isRecord(decoded) && decoded.applied === false) {
47
+ const details = Array.isArray(decoded.errors)
48
+ ? decoded.errors.map(error => typeof error === 'string' ? error : JSON.stringify(error)).join('; ')
49
+ : text;
50
+ throw new Error(`OpenPencil MCP ${tool} did not apply${details === '' ? '' : `: ${details}`}`);
51
+ }
52
+ return { tool, value: decoded, text };
53
+ }
54
+ /** Call a first-party tool on a managed editor without exposing its daemon token. */
55
+ export async function callOpenPencilMcp(options) {
56
+ const origin = new URL(options.baseUrl);
57
+ const loopback = origin.hostname === '127.0.0.1' || origin.hostname === 'localhost' || origin.hostname === '::1';
58
+ if (!loopback || (origin.protocol !== 'http:' && origin.protocol !== 'https:')) {
59
+ throw new Error('OpenPencil MCP endpoint must use an HTTP loopback origin');
60
+ }
61
+ if (origin.pathname !== '/' || origin.search !== '' || origin.hash !== '') {
62
+ throw new Error('OpenPencil MCP base URL must be an origin');
63
+ }
64
+ const fetcher = options.fetcher ?? fetch;
65
+ const timeout = AbortSignal.timeout(MCP_TIMEOUT_MS);
66
+ const signal = options.signal === undefined ? timeout : AbortSignal.any([options.signal, timeout]);
67
+ const response = await fetcher(new URL('/mcp', origin).href, {
68
+ method: 'POST',
69
+ headers: {
70
+ authorization: `Bearer ${options.token}`,
71
+ 'x-openpencil-token': options.token,
72
+ 'content-type': 'application/json',
73
+ accept: 'application/json',
74
+ },
75
+ body: JSON.stringify({
76
+ jsonrpc: '2.0',
77
+ id: `dsh-${Date.now().toString(36)}`,
78
+ method: 'tools/call',
79
+ params: { name: options.tool, arguments: options.arguments ?? {} },
80
+ }),
81
+ signal,
82
+ });
83
+ const bytes = Buffer.from(await response.arrayBuffer());
84
+ if (bytes.length > MAX_MCP_RESPONSE_BYTES)
85
+ throw new Error(`OpenPencil MCP ${options.tool} response is too large`);
86
+ if (!response.ok)
87
+ throw new Error(`OpenPencil MCP ${options.tool} request failed (${response.status})`);
88
+ let value;
89
+ try {
90
+ value = JSON.parse(bytes.toString('utf8'));
91
+ }
92
+ catch {
93
+ throw new Error(`OpenPencil MCP ${options.tool} returned invalid JSON`);
94
+ }
95
+ return parseOpenPencilMcpResponse(options.tool, value);
96
+ }
97
+ /** Probe the daemon document version before and after a write. */
98
+ export async function getOpenPencilMcpVersion(options) {
99
+ const origin = new URL(options.baseUrl);
100
+ const loopback = origin.hostname === '127.0.0.1' || origin.hostname === 'localhost' || origin.hostname === '::1';
101
+ if (!loopback || (origin.protocol !== 'http:' && origin.protocol !== 'https:')) {
102
+ throw new Error('OpenPencil MCP endpoint must use an HTTP loopback origin');
103
+ }
104
+ const timeout = AbortSignal.timeout(options.timeoutMs ?? MCP_TIMEOUT_MS);
105
+ const signal = options.signal === undefined ? timeout : AbortSignal.any([options.signal, timeout]);
106
+ const response = await (options.fetcher ?? fetch)(new URL('/api/mcp/version', origin).href, {
107
+ headers: { authorization: `Bearer ${options.token}`, 'x-openpencil-token': options.token },
108
+ signal,
109
+ });
110
+ if (!response.ok)
111
+ throw new Error(`OpenPencil MCP version probe failed (${response.status})`);
112
+ const value = await response.json();
113
+ if (!isRecord(value) || typeof value.version !== 'number' || !Number.isSafeInteger(value.version) || value.version < 0) {
114
+ throw new Error('OpenPencil MCP version probe returned an invalid result');
115
+ }
116
+ return value.version;
117
+ }
118
+ function nodeSummary(value) {
119
+ if (!isRecord(value) || typeof value.id !== 'string' || value.id.length === 0)
120
+ return undefined;
121
+ return {
122
+ id: value.id,
123
+ ...(optionalString(value.type) === undefined ? {} : { type: optionalString(value.type) }),
124
+ ...(optionalString(value.name) === undefined ? {} : { name: optionalString(value.name) }),
125
+ ...(finite(value.x) === undefined ? {} : { x: finite(value.x) }),
126
+ ...(finite(value.y) === undefined ? {} : { y: finite(value.y) }),
127
+ ...(finite(value.width) === undefined ? {} : { width: finite(value.width) }),
128
+ ...(finite(value.height) === undefined ? {} : { height: finite(value.height) }),
129
+ };
130
+ }
131
+ /** Project get_selection into the bounded shape shared with the DSH client. */
132
+ export function selectionSnapshotFromMcp(sourcePath, value, updatedAt = Date.now()) {
133
+ if (!isRecord(value))
134
+ throw new Error('OpenPencil get_selection returned an invalid result');
135
+ const selectedIds = Array.isArray(value.selectedIds)
136
+ ? value.selectedIds.filter((id) => typeof id === 'string' && id.length > 0)
137
+ : [];
138
+ const nodes = Array.isArray(value.nodes)
139
+ ? value.nodes.map(nodeSummary).filter((node) => node !== undefined)
140
+ : [];
141
+ return {
142
+ sourcePath,
143
+ activePageId: optionalString(value.activePageId) ?? '',
144
+ selectedIds,
145
+ nodes,
146
+ updatedAt,
147
+ };
148
+ }
@@ -0,0 +1,23 @@
1
+ /** Create a brand-new OpenPencil document from one transactional design batch. */
2
+ import type FileSystem from '@deepseek-ai/dsh-fs';
3
+ import type { FsObservation, FsTarget } from '@deepseek-ai/dsh-fs';
4
+ import type SandboxPolicyService from '@deepseek-ai/dsh-sandbox-policy';
5
+ import { type ToolRunContext } from '@deepseek-ai/dsh-tools';
6
+ import type { EditorHostController } from './editor-host.js';
7
+ export interface DesignNewArgs {
8
+ path: string;
9
+ operations: string;
10
+ canvasWidth?: number;
11
+ postProcess?: boolean;
12
+ }
13
+ export interface DesignNewServices {
14
+ fs: FileSystem;
15
+ sandboxPolicy: SandboxPolicyService;
16
+ observe(target: FsTarget, observation: FsObservation, exec: ToolRunContext): void;
17
+ }
18
+ /**
19
+ * Build and atomically publish a new `.op` document. The first batch runs in
20
+ * a private managed daemon, so no existing file or browser-owned sidebar is
21
+ * required and a failed design never leaves an empty target behind.
22
+ */
23
+ export declare function createDesignNewTool(editorHost: EditorHostController, services: DesignNewServices): import("@deepseek-ai/dsh-tools").ToolDefinition;
@@ -0,0 +1,136 @@
1
+ /** Create a brand-new OpenPencil document from one transactional design batch. */
2
+ import { createHash } from 'node:crypto';
3
+ import { lstat } from 'node:fs/promises';
4
+ import { basename, dirname, extname, isAbsolute } from 'node:path';
5
+ import { defineTool } from '@deepseek-ai/dsh-tools';
6
+ import { OPENPENCIL_NEW_TOOL_NAME } from './tool-names.js';
7
+ const MAX_OPERATIONS_LENGTH = 256 * 1024;
8
+ const ROOT_OPERATION_EXAMPLE = 'root=I(null,{"type":"frame","name":"Home","width":390,"height":844,"layout":"vertical","gap":24,"padding":24,"fill":"#F7F1E7","children":[{"type":"text","name":"Title","content":"Forage","fontSize":32,"fontWeight":700,"fill":"#173C2B"}]})';
9
+ const IMAGE_OPERATION_EXAMPLE = 'slot=I(root,{"type":"frame","name":"Hero photo","width":"fill_container","height":240,"cornerRadius":20,"clipContent":true,"children":[]})\nphoto=G(slot,"search","seasonal food photography")';
10
+ function renderJson(_args, value) {
11
+ return [{ type: 'text', text: JSON.stringify(value, null, 2) }];
12
+ }
13
+ function isRecord(value) {
14
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
15
+ }
16
+ /**
17
+ * Build and atomically publish a new `.op` document. The first batch runs in
18
+ * a private managed daemon, so no existing file or browser-owned sidebar is
19
+ * required and a failed design never leaves an empty target behind.
20
+ */
21
+ export function createDesignNewTool(editorHost, services) {
22
+ return defineTool({
23
+ name: OPENPENCIL_NEW_TOOL_NAME,
24
+ description: 'Create and save a brand-new OpenPencil .op design from one transactional batch_design program. '
25
+ + 'Use this for natural-language requests to make a new design when no .op file or live editor exists. '
26
+ + 'Do not inspect or hand-write .op JSON and do not ask the user to open a sidebar. '
27
+ + 'Choose a concise workspace-relative .op filename when the user did not specify one. '
28
+ + 'This is a local OpenPencil-host operation and requires the session Workspace Write permission. '
29
+ + 'The target must not already exist and its parent directory must exist. '
30
+ + `Start with this valid pattern: ${ROOT_OPERATION_EXAMPLE}. `
31
+ + 'A complete new design must create at least one renderable root frame. Prefer nested children for a cohesive initial screen and prefer at most 25 top-level operations. '
32
+ + `For requested photography, add these valid lines in the same batch: ${IMAGE_OPERATION_EXAMPLE}. `
33
+ + 'Use OpenPencil image search or an editable placeholder without pausing for external image-service setup; do not ask image-provider questions unless the user explicitly requested external generation. '
34
+ + 'All operations apply together and the resulting document is saved atomically. '
35
+ + 'After success, immediately call openpencil_render with the returned path, editable=true, and autoOpen=true to show and automatically expand the editor once.',
36
+ parameters: {
37
+ path: {
38
+ type: 'string', required: true,
39
+ description: 'New workspace-relative or absolute .op path. Choose a useful filename without asking when the user omitted one. Existing targets are never overwritten.',
40
+ },
41
+ operations: {
42
+ type: 'string', required: true,
43
+ description: `A complete batch_design program, preferably at most 25 top-level newline-separated operations. Begin with ${ROOT_OPERATION_EXAMPLE}. Use returned bindings for later I/U/G operations. For photography use ${IMAGE_OPERATION_EXAMPLE}.`,
44
+ },
45
+ canvasWidth: { type: 'number', description: 'Optional canvas-width hint for OpenPencil post-processing.' },
46
+ postProcess: { type: 'boolean', description: 'Run OpenPencil post-processing after the batch. Default false.' },
47
+ },
48
+ output: {
49
+ schema: {
50
+ type: 'object', additionalProperties: false,
51
+ properties: {
52
+ path: { type: 'string', required: true },
53
+ filename: { type: 'string', required: true },
54
+ bytes: { type: 'integer', required: true },
55
+ sha256: { type: 'string', required: true },
56
+ created: { type: 'boolean', const: true, required: true },
57
+ applied: { type: 'boolean', const: true, required: true },
58
+ saved: { type: 'boolean', const: true, required: true },
59
+ result: { type: 'object', additionalProperties: true },
60
+ note: { type: 'string', required: true },
61
+ },
62
+ },
63
+ render: renderJson,
64
+ },
65
+ async execute(args, exec) {
66
+ const requestedPath = args.path.trim();
67
+ if (requestedPath.length === 0)
68
+ throw new Error(`${OPENPENCIL_NEW_TOOL_NAME}: path is required`);
69
+ if (extname(requestedPath).toLowerCase() !== '.op') {
70
+ throw new Error(`${OPENPENCIL_NEW_TOOL_NAME}: path must end in .op`);
71
+ }
72
+ if (args.operations.trim().length === 0)
73
+ throw new Error(`${OPENPENCIL_NEW_TOOL_NAME}: operations must not be empty`);
74
+ if (args.operations.length > MAX_OPERATIONS_LENGTH)
75
+ throw new Error(`${OPENPENCIL_NEW_TOOL_NAME}: operations are too large`);
76
+ if (args.canvasWidth !== undefined && (!Number.isFinite(args.canvasWidth) || args.canvasWidth <= 0 || args.canvasWidth > 16_384)) {
77
+ throw new Error(`${OPENPENCIL_NEW_TOOL_NAME}: canvasWidth must be greater than 0 and at most 16384`);
78
+ }
79
+ const sandboxPolicy = services.sandboxPolicy.resolve({ session: exec.agent?.session });
80
+ if (services.fs.sandboxMode !== undefined && sandboxPolicy.mode === 'read-only') {
81
+ throw new Error(`${OPENPENCIL_NEW_TOOL_NAME}: creating a design requires Workspace Write access; switch this session from Read Only to Workspace Write and retry`);
82
+ }
83
+ const resolveOptions = { cwd: sandboxPolicy.workspaceRoot, signal: exec.signal };
84
+ const pathInfo = await services.fs.lstat(requestedPath, { cwd: sandboxPolicy.workspaceRoot }, exec.signal);
85
+ if (pathInfo !== undefined)
86
+ throw new Error(`${OPENPENCIL_NEW_TOOL_NAME}: target already exists: ${requestedPath}`);
87
+ const target = await services.fs.resolve(requestedPath, resolveOptions);
88
+ const processPath = services.fs.processPath(target);
89
+ if (extname(processPath).toLowerCase() !== '.op') {
90
+ throw new Error(`${OPENPENCIL_NEW_TOOL_NAME}: resolved target must end in .op`);
91
+ }
92
+ if (!isAbsolute(processPath))
93
+ throw new Error(`${OPENPENCIL_NEW_TOOL_NAME}: the DSH filesystem did not provide an absolute host path`);
94
+ let parentInfo;
95
+ try {
96
+ parentInfo = await lstat(dirname(processPath));
97
+ }
98
+ catch {
99
+ throw new Error(`${OPENPENCIL_NEW_TOOL_NAME}: the target parent is not available to the local OpenPencil host; use an existing directory in a local DSH workspace`);
100
+ }
101
+ if (!parentInfo.isDirectory()) {
102
+ throw new Error(`${OPENPENCIL_NEW_TOOL_NAME}: target parent must be a local directory`);
103
+ }
104
+ const resolvedInfo = await services.fs.stat(target, exec.signal);
105
+ if (resolvedInfo !== undefined) {
106
+ services.observe(target, { kind: 'present', version: resolvedInfo.version }, exec);
107
+ throw new Error(`${OPENPENCIL_NEW_TOOL_NAME}: target already exists: ${requestedPath}`);
108
+ }
109
+ services.observe(target, { kind: 'absent' }, exec);
110
+ const batch = await editorHost.createDocumentBatch({
111
+ operations: args.operations,
112
+ ...(args.canvasWidth === undefined ? {} : { canvasWidth: args.canvasWidth }),
113
+ ...(args.postProcess === undefined ? {} : { postProcess: args.postProcess }),
114
+ signal: exec.signal,
115
+ });
116
+ const outcome = await services.fs.writeText(target, batch.documentJson, { kind: 'createIfAbsent' }, exec.signal, sandboxPolicy);
117
+ services.observe(target, { kind: 'present', version: outcome.version }, exec);
118
+ const bytes = Buffer.byteLength(outcome.after);
119
+ const sha256 = createHash('sha256').update(outcome.after).digest('hex');
120
+ return {
121
+ path: processPath,
122
+ filename: basename(processPath),
123
+ bytes,
124
+ sha256,
125
+ created: true,
126
+ applied: true,
127
+ saved: true,
128
+ ...(isRecord(batch.result) ? { result: batch.result } : {}),
129
+ note: `Created and saved ${processPath}. Call openpencil_render with this path, editable=true, and autoOpen=true now.`,
130
+ };
131
+ },
132
+ presentCall: (args) => ({
133
+ card: 'generic', title: `Create ${args.path}`, kind: 'execute', locations: [{ path: args.path }],
134
+ }),
135
+ });
136
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Safe recovery of browser-only presentation metadata for nested Code Mode
3
+ * tool calls.
4
+ *
5
+ * DSH persists a nested `openpencil_render` outcome as a
6
+ * `tool/code-dispatch` event, but published DSH through 0.1.0-rc.6 omits the native
7
+ * `tool/result` presentation metadata from that event. The browser can ask
8
+ * this same-origin endpoint to re-project that metadata without submitting
9
+ * any path or tool result of its own. Live results are remembered briefly so
10
+ * an explicitly requested editor grant can be restored; replayed durable
11
+ * events are preview-only.
12
+ *
13
+ * @module dsh-openpencil/presentation-hydration
14
+ */
15
+ import type { IncomingMessage, ServerResponse } from 'node:http';
16
+ import { type SessionStore } from '@deepseek-ai/dsh-session';
17
+ import type { ToolExecution, ToolExecutionResult } from '@deepseek-ai/dsh-tools';
18
+ import { type EditorHostController } from './editor-host.js';
19
+ import { RenderAccessController, type RenderResult } from './renderer.js';
20
+ import type { ViewerAssetController } from './viewer-assets.js';
21
+ /** Exact same-origin endpoint used by the client to recover nested metadata. */
22
+ export declare const PRESENTATION_HYDRATION_ROUTE = "/_dsh/dsh-openpencil/presentation";
23
+ interface PresentationHydrationOptions {
24
+ ttlMs?: number;
25
+ maxEntries?: number;
26
+ maxRecordBytes?: number;
27
+ maxBytes?: number;
28
+ now?: () => number;
29
+ }
30
+ interface PresentationHydrationDependencies {
31
+ sessions: Pick<SessionStore, 'get'>;
32
+ render: RenderAccessController;
33
+ viewer?: Pick<ViewerAssetController, 'viewerGrant'>;
34
+ editor?: Pick<EditorHostController, 'grantFor'>;
35
+ /** DSH Web authorities derived from `webRuntime.trustedHosts`. */
36
+ trustedHosts?: readonly string[] | (() => readonly string[]);
37
+ }
38
+ /**
39
+ * Accept only the canonical, content-addressed result shape emitted by this
40
+ * plugin. Hydration never signs a legacy absolute-path-only render.
41
+ */
42
+ export declare function parseHydratableRenderResult(value: unknown): RenderResult | undefined;
43
+ /** TTL/LRU cache plus fail-closed hydration endpoint. */
44
+ export declare class PresentationHydrationController {
45
+ #private;
46
+ private readonly dependencies;
47
+ constructor(dependencies: PresentationHydrationDependencies, options?: PresentationHydrationOptions);
48
+ /** Observe one trusted in-process result before Code Mode drops its meta. */
49
+ observeToolResult(exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): void;
50
+ /** Drop all live editor-capable records owned by a disposed session. */
51
+ forgetSession(sessionId: string): void;
52
+ /** Handle the exact same-origin POST hydration route. */
53
+ handle(req: IncomingMessage, res: ServerResponse): Promise<void>;
54
+ }
55
+ export {};