@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,21 @@
1
+ /** Model-facing tools that directly drive the active OpenPencil canvas. */
2
+ import { type JsonValue } from '@deepseek-ai/dsh-tools';
3
+ import type { EditorHostController } from './editor-host.js';
4
+ /** Read the exact selection the user currently has on the editable canvas. */
5
+ export declare function createDesignSelectionTool(editorHost: EditorHostController): import("@deepseek-ai/dsh-tools").ToolDefinition;
6
+ export interface DesignCreateArgs {
7
+ path: string;
8
+ operations: string;
9
+ pageId?: string;
10
+ canvasWidth?: number;
11
+ postProcess?: boolean;
12
+ }
13
+ /** Apply one transactional OpenPencil batch program to generate design nodes. */
14
+ export declare function createDesignCreateTool(editorHost: EditorHostController): import("@deepseek-ai/dsh-tools").ToolDefinition;
15
+ export interface DesignEditArgs {
16
+ path: string;
17
+ nodeId?: string;
18
+ changes: Record<string, JsonValue>;
19
+ }
20
+ /** Patch one explicit node, or the single node the user selected. */
21
+ export declare function createDesignEditTool(editorHost: EditorHostController): import("@deepseek-ai/dsh-tools").ToolDefinition;
@@ -0,0 +1,214 @@
1
+ /** Model-facing tools that directly drive the active OpenPencil canvas. */
2
+ import { defineTool } from '@deepseek-ai/dsh-tools';
3
+ import { resolveInputFile } from './renderer.js';
4
+ import { OPENPENCIL_CREATE_TOOL_NAME, OPENPENCIL_EDIT_TOOL_NAME, OPENPENCIL_SELECTION_TOOL_NAME, } from './tool-names.js';
5
+ const MAX_OPERATIONS_LENGTH = 256 * 1024;
6
+ function sessionWorkspace(exec) {
7
+ return exec.agent?.session.header.cwd ?? process.cwd();
8
+ }
9
+ async function expectedSource(path, exec) {
10
+ return path === undefined ? undefined : resolveInputFile(path, sessionWorkspace(exec));
11
+ }
12
+ function ownerSessionId(exec) {
13
+ return exec.agent === undefined ? undefined : String(exec.agent.id);
14
+ }
15
+ const renderJson = (_args, value) => [{
16
+ type: 'text',
17
+ text: JSON.stringify(value, null, 2),
18
+ }];
19
+ const nodeSchema = {
20
+ type: 'object',
21
+ additionalProperties: false,
22
+ properties: {
23
+ id: { type: 'string', required: true },
24
+ type: { type: 'string' },
25
+ name: { type: 'string' },
26
+ x: { type: 'number' },
27
+ y: { type: 'number' },
28
+ width: { type: 'number' },
29
+ height: { type: 'number' },
30
+ },
31
+ };
32
+ const selectionProperties = {
33
+ sourcePath: { type: 'string', required: true },
34
+ activePageId: { type: 'string', required: true },
35
+ selectedIds: { type: 'array', items: { type: 'string' }, required: true },
36
+ nodes: { type: 'array', items: nodeSchema, required: true },
37
+ count: { type: 'integer', required: true },
38
+ updatedAt: { type: 'integer', required: true },
39
+ };
40
+ function selectionResult(selection) {
41
+ return { ...selection, count: selection.selectedIds.length };
42
+ }
43
+ /** Read the exact selection the user currently has on the editable canvas. */
44
+ export function createDesignSelectionTool(editorHost) {
45
+ return defineTool({
46
+ name: OPENPENCIL_SELECTION_TOOL_NAME,
47
+ description: 'Read the current selection from the live OpenPencil sidebar canvas. '
48
+ + 'Use this before modifying a node. Returns selected node ids, names, types, bounds, and the active page. '
49
+ + 'The editable sidebar must already be open. If path is supplied, the tool refuses to target a different canvas.',
50
+ parameters: {
51
+ path: { type: 'string', description: 'Optional .op path used to assert which live editor is being read.' },
52
+ },
53
+ output: {
54
+ schema: {
55
+ type: 'object', additionalProperties: false, properties: selectionProperties,
56
+ },
57
+ render: renderJson,
58
+ },
59
+ async execute(args, exec) {
60
+ const sourcePath = await expectedSource(args.path, exec);
61
+ const selection = await editorHost.getActiveSelection({ sourcePath, ownerSessionId: ownerSessionId(exec), signal: exec.signal });
62
+ return selectionResult(selection);
63
+ },
64
+ presentCall: () => ({ card: 'generic', title: 'Read OpenPencil selection', kind: 'read' }),
65
+ });
66
+ }
67
+ /** Apply one transactional OpenPencil batch program to generate design nodes. */
68
+ export function createDesignCreateTool(editorHost) {
69
+ return defineTool({
70
+ name: OPENPENCIL_CREATE_TOOL_NAME,
71
+ description: 'Generate or restructure design nodes on an existing active OpenPencil canvas using one transactional batch_design program. '
72
+ + 'Use this only after the editable sidebar is already open. For a new design with no .op file or live editor, use openpencil_new instead. '
73
+ + 'Use concise newline-separated operations: '
74
+ + 'I(parentId, nodeJson) inserts, U(nodeId, patchJson) updates, D(nodeId) deletes, '
75
+ + 'M(nodeId,parentId,index) moves, C(nodeId,parentId,overrides) clones, and R(nodeId,nodeJson) replaces. '
76
+ + 'All operations apply together or none apply. The canvas updates live but remains unsaved until the sidebar Save action.',
77
+ parameters: {
78
+ path: { type: 'string', required: true, description: 'The .op path used to bind this write to the intended live editor.' },
79
+ operations: {
80
+ type: 'string', required: true,
81
+ description: 'Newline-separated batch_design operations, for example I("page-id", {"type":"frame","id":"hero","name":"Hero","x":0,"y":0,"width":1200,"height":800}).',
82
+ },
83
+ pageId: { type: 'string', description: 'Optional page id. Defaults to the live editor active page.' },
84
+ canvasWidth: { type: 'number', description: 'Optional canvas width hint for post-processing.' },
85
+ postProcess: { type: 'boolean', description: 'Run OpenPencil post-processing after the batch. Default false.' },
86
+ },
87
+ output: {
88
+ schema: {
89
+ type: 'object', additionalProperties: false,
90
+ properties: {
91
+ ...selectionProperties,
92
+ applied: { type: 'boolean', const: true, required: true },
93
+ dirty: { type: 'boolean', const: true, required: true },
94
+ saved: { type: 'boolean', const: false, required: true },
95
+ result: { type: 'object', additionalProperties: true },
96
+ note: { type: 'string', required: true },
97
+ },
98
+ },
99
+ render: renderJson,
100
+ },
101
+ async execute(args, exec) {
102
+ if (args.operations.trim().length === 0)
103
+ throw new Error(`${OPENPENCIL_CREATE_TOOL_NAME}: operations must not be empty`);
104
+ if (args.operations.length > MAX_OPERATIONS_LENGTH)
105
+ throw new Error(`${OPENPENCIL_CREATE_TOOL_NAME}: operations are too large`);
106
+ const sourcePath = await expectedSource(args.path, exec);
107
+ const owner = ownerSessionId(exec);
108
+ const before = await editorHost.getActiveSelection({ sourcePath, ownerSessionId: owner, signal: exec.signal });
109
+ const result = await editorHost.callActiveMcp('batch_design', {
110
+ operations: args.operations,
111
+ ...(args.pageId !== undefined && args.pageId.length > 0
112
+ ? { pageId: args.pageId }
113
+ : before.activePageId === '' ? {} : { pageId: before.activePageId }),
114
+ ...(args.canvasWidth === undefined ? {} : { canvasWidth: args.canvasWidth }),
115
+ ...(args.postProcess === undefined ? {} : { postProcess: args.postProcess }),
116
+ }, { sourcePath, ownerSessionId: owner, signal: exec.signal });
117
+ const selection = await editorHost.getActiveSelection({ sourcePath, ownerSessionId: owner, signal: exec.signal });
118
+ return {
119
+ ...selectionResult(selection),
120
+ applied: true,
121
+ dirty: true,
122
+ saved: false,
123
+ ...(typeof result.value === 'object' && result.value !== null && !Array.isArray(result.value)
124
+ ? { result: result.value }
125
+ : {}),
126
+ note: 'Applied to the live OpenPencil canvas. Use Save in the sidebar to persist the .op file.',
127
+ };
128
+ },
129
+ presentCall: () => ({ card: 'generic', title: 'Generate on OpenPencil canvas', kind: 'execute' }),
130
+ });
131
+ }
132
+ /** Patch one explicit node, or the single node the user selected. */
133
+ export function createDesignEditTool(editorHost) {
134
+ return defineTool({
135
+ name: OPENPENCIL_EDIT_TOOL_NAME,
136
+ description: 'Modify one node directly on the active OpenPencil canvas. '
137
+ + 'When nodeId is omitted, this tool targets the single node currently selected by the user; '
138
+ + 'it refuses an empty or multi-selection instead of guessing. Pass rich OpenPencil node fields in changes, '
139
+ + 'for example content, name, x, y, width, height, fill, fontSize, padding, layout, or opacity. '
140
+ + 'The canvas updates live but remains unsaved until the sidebar Save action.',
141
+ parameters: {
142
+ path: { type: 'string', required: true, description: 'The .op path used to bind this write to the intended live editor.' },
143
+ nodeId: { type: 'string', description: 'Explicit target node id. Omit to use the one selected canvas node.' },
144
+ changes: {
145
+ type: 'object', additionalProperties: true, required: true,
146
+ description: 'Shallow OpenPencil node patch. Rich canonical fields are preserved.',
147
+ },
148
+ },
149
+ output: {
150
+ schema: {
151
+ type: 'object', additionalProperties: false,
152
+ properties: {
153
+ ...selectionProperties,
154
+ applied: { type: 'boolean', const: true, required: true },
155
+ dirty: { type: 'boolean', const: true, required: true },
156
+ saved: { type: 'boolean', const: false, required: true },
157
+ targetNodeId: { type: 'string', required: true },
158
+ targetSource: { type: 'string', enum: ['explicit', 'selection'], required: true },
159
+ result: { type: 'object', additionalProperties: true },
160
+ note: { type: 'string', required: true },
161
+ },
162
+ },
163
+ render: renderJson,
164
+ },
165
+ async execute(args, exec) {
166
+ if (Object.keys(args.changes).length === 0)
167
+ throw new Error(`${OPENPENCIL_EDIT_TOOL_NAME}: changes must not be empty`);
168
+ const sourcePath = await expectedSource(args.path, exec);
169
+ const owner = ownerSessionId(exec);
170
+ const before = await editorHost.getActiveSelection({ sourcePath, ownerSessionId: owner, signal: exec.signal });
171
+ const explicit = args.nodeId?.trim();
172
+ let targetNodeId;
173
+ let targetSource;
174
+ if (explicit !== undefined && explicit.length > 0) {
175
+ if (!before.selectedIds.includes(explicit)) {
176
+ throw new Error(`${OPENPENCIL_EDIT_TOOL_NAME}: nodeId ${explicit} is not in the current canvas selection`);
177
+ }
178
+ targetNodeId = explicit;
179
+ targetSource = 'explicit';
180
+ }
181
+ else {
182
+ if (before.selectedIds.length === 0) {
183
+ throw new Error(`${OPENPENCIL_EDIT_TOOL_NAME}: no canvas node is selected; select one in OpenPencil or pass nodeId`);
184
+ }
185
+ if (before.selectedIds.length !== 1) {
186
+ throw new Error(`${OPENPENCIL_EDIT_TOOL_NAME}: ${before.selectedIds.length} nodes are selected; pass nodeId to choose one`);
187
+ }
188
+ targetNodeId = before.selectedIds[0];
189
+ targetSource = 'selection';
190
+ }
191
+ const result = await editorHost.callActiveMcp('update_node', {
192
+ nodeId: targetNodeId,
193
+ data: args.changes,
194
+ ...(before.activePageId === '' ? {} : { pageId: before.activePageId }),
195
+ }, { sourcePath, ownerSessionId: owner, signal: exec.signal });
196
+ const selection = await editorHost.getActiveSelection({ sourcePath, ownerSessionId: owner, signal: exec.signal });
197
+ return {
198
+ ...selectionResult(selection),
199
+ applied: true,
200
+ dirty: true,
201
+ saved: false,
202
+ targetNodeId,
203
+ targetSource,
204
+ ...(typeof result.value === 'object' && result.value !== null && !Array.isArray(result.value)
205
+ ? { result: result.value }
206
+ : {}),
207
+ note: 'Applied to the live OpenPencil canvas. Use Save in the sidebar to persist the .op file.',
208
+ };
209
+ },
210
+ presentCall: (args) => ({
211
+ card: 'generic', title: args.nodeId === undefined ? 'Edit selected OpenPencil node' : `Edit OpenPencil node ${args.nodeId}`, kind: 'execute',
212
+ }),
213
+ });
214
+ }
@@ -0,0 +1,62 @@
1
+ /** Lazy managed OpenPencil editor sessions for the DSH details panel. */
2
+ import type { IncomingMessage, ServerResponse } from 'node:http';
3
+ import { type OpenPencilMcpResult, type OpenPencilSelectionSnapshot } from './mcp-client.js';
4
+ export declare const EDITOR_ROUTE_PREFIX = "/_dsh/dsh-openpencil/editor";
5
+ export interface EditorGrant {
6
+ enabled: true;
7
+ launchUrl: string;
8
+ refreshUrl: string;
9
+ }
10
+ export type OpenPencilLiveTool = 'get_selection' | 'update_node' | 'batch_design';
11
+ export interface ActiveMcpCallOptions {
12
+ /** Refuse to drive a different transcript card's live editor. */
13
+ sourcePath?: string;
14
+ ownerSessionId?: string;
15
+ signal?: AbortSignal;
16
+ }
17
+ export interface CreateDocumentBatchOptions {
18
+ operations: string;
19
+ canvasWidth?: number;
20
+ postProcess?: boolean;
21
+ signal: AbortSignal;
22
+ }
23
+ export interface CreateDocumentBatchResult {
24
+ documentJson: string;
25
+ result: unknown;
26
+ }
27
+ /** Locate the GUI-free managed host used by op-vscode. */
28
+ export declare function findEditorHostBinary(): string | undefined;
29
+ /**
30
+ * Trust the transport peer, never forwarded or caller-controlled host data.
31
+ * Node may expose an IPv4 peer either directly or as an IPv4-mapped IPv6
32
+ * address, including the compact hexadecimal form used by some platforms.
33
+ */
34
+ export declare function isLoopbackRemoteAddress(address: string | undefined): boolean;
35
+ /** Owns opaque launch capabilities and all live managed editor children. */
36
+ export declare class EditorHostController {
37
+ #private;
38
+ readonly binary: string | undefined;
39
+ constructor(masterKey: Buffer);
40
+ get available(): boolean;
41
+ get routeAvailable(): boolean;
42
+ attachRoute(): () => void;
43
+ /** Mint an opaque, runtime-only launch URL; no source path enters metadata. */
44
+ grantFor(sourcePath: string | undefined, sourceSha256: string | undefined): EditorGrant | undefined;
45
+ handle(req: IncomingMessage, res: ServerResponse): Promise<void>;
46
+ dispose(): Promise<void>;
47
+ /**
48
+ * Build one brand-new document without requiring a browser-owned editor.
49
+ * The managed daemon is transient and never enters the visible-session map,
50
+ * so this operation neither depends on nor retires an existing workbench.
51
+ * Callers publish the returned authoritative JSON through DSH's filesystem
52
+ * capability only after the whole batch succeeds.
53
+ */
54
+ createDocumentBatch(options: CreateDocumentBatchOptions): Promise<CreateDocumentBatchResult>;
55
+ /** Current live editor selection, suitable for Agent context and UI chips. */
56
+ getActiveSelection(options?: ActiveMcpCallOptions): Promise<OpenPencilSelectionSnapshot>;
57
+ /**
58
+ * Drive one allowlisted first-party MCP tool on the currently visible
59
+ * editor. The managed daemon token never crosses this controller boundary.
60
+ */
61
+ callActiveMcp(tool: OpenPencilLiveTool, args: Record<string, unknown>, options?: ActiveMcpCallOptions): Promise<OpenPencilMcpResult>;
62
+ }