@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ZSeven—W
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 ADDED
@@ -0,0 +1,134 @@
1
+ # DSH OpenPencil
2
+
3
+ DeepSeek Harness plugin for previewing and editing OpenPencil `.op` documents inside a conversation.
4
+
5
+ ![DSH OpenPencil multi-frame preview and sidebar editor](docs/images/dsh-openpencil-overview.png)
6
+
7
+ ## 项目介绍
8
+
9
+ **中文**
10
+
11
+ DSH OpenPencil 是连接 DeepSeek Harness 与 OpenPencil 的智能设计插件,目标是让 Agent 直接驱动真实、可编辑、可交互的设计画布,而不是只返回一张生成图片。它支持在对话中渲染和浏览多页面 `.op` 设计稿,一键进入可缩放画布或完整编辑器(在支持的未来 DSH 中可使用原生右侧详情栏;当前发布的 rc.2 至 0.1.0-rc.6 自动使用插件自有的可缩放右侧工作台,并可切换全屏),继续使用 OpenPencil 的图层、属性、绘制、组件、交互和多类模板能力,快速创建 App 页面、演示文稿、社交媒体内容、信息图等不同类型的设计;同时让 DeepSeek Harness 中的 Agent 理解画布结构、节点、选区、组件关系与交互逻辑,直接调用模板、生成页面、修改组件、调整布局、编排交互、检查视觉质量并保存结果,把“对话提出需求—Agent 操作真实画布—实时预览与交互验证—继续迭代”整合成一条完整设计工作流。
12
+
13
+ **English**
14
+
15
+ DSH OpenPencil is an intelligent design plugin that connects DeepSeek Harness with OpenPencil. Its goal is to let an Agent directly operate a real, editable, and interactive design canvas instead of returning only a generated image. It can render and browse multi-page `.op` designs inside a conversation, then open them in a zoomable canvas or the full editor. A future host with the proposed native Tool-details seam can use DSH's right-hand details panel; published releases from rc.2 through 0.1.0-rc.6 use the plugin's resizable right-hand workbench, which can also switch to full screen. From there, users retain OpenPencil's layers, properties, drawing tools, components, interactions, and broad template library for creating app screens, presentations, social media content, infographics, and more. At the same time, the Agent can understand the canvas structure, nodes, selections, component relationships, and interaction logic; invoke templates; generate pages; modify components; adjust layouts; orchestrate interactions; inspect visual quality; and save the result. This brings requirement gathering, direct Agent-driven canvas editing, live preview and interaction validation, and continued iteration into one complete design workflow.
16
+
17
+ ## What works
18
+
19
+ - `openpencil_render` creates an immutable, content-addressed `.op` snapshot and renders every top-level frame on the active page.
20
+ - `openpencil_selection` reads the exact nodes selected in the live editor canvas.
21
+ - `openpencil_new` creates a brand-new `.op` from one transactional `batch_design` program, saves it atomically through DSH's sandboxed filesystem, and requires no pre-opened editor.
22
+ - `openpencil_create` applies a transactional OpenPencil `batch_design` program to generate or restructure canvas nodes.
23
+ - `openpencil_edit` modifies an explicit node or the single node selected by the user.
24
+ - OpenPencil's installed headless exporter is the default, design-fidelity renderer.
25
+ - The tool card shows the first top-level frame as a large replay-safe PNG. Multi-frame documents add a horizontally scrollable thumbnail rail, click-to-select, and previous/next navigation.
26
+ - The large preview supports manual zoom, reset, fit-frame, and fit-content modes.
27
+ - “Open interactive canvas” lazily mounts the read-only OpenPencil Web SDK. The canvas supports pan, zoom, and fit.
28
+ - With `editable: true`, the edit action opens the managed OpenPencil editor with selection, layers, properties, drawing tools, undo/redo, and explicit save semantics. Published DSH releases through 0.1.0-rc.6 use a resizable plugin-owned right workbench; smaller viewports use full screen automatically.
29
+ - The tool card and managed editor follow DSH's Chinese/English locale and light/dark theme without reloading the editing session.
30
+ - Image and document grants are signed, hash-bound capabilities. Browser metadata does not expose an arbitrary host path.
31
+ - If the exact OpenPencil binary is genuinely unavailable, Jian may produce a clearly labelled `runtime-preview` fallback. Exact renderer failures, timeouts, and invalid PNGs do not silently fall back.
32
+
33
+ The read-only Web SDK viewer and the managed editor are intentionally separate paths. Only one Web SDK viewer and one managed editor are active at a time because their current browser hosts own page-wide render pumps. “Edit source .op” remains available as a direct DSH file action.
34
+
35
+ ## Install into DSH
36
+
37
+ Install the public plugin into an authenticated DSH prerelease without installing DSH globally:
38
+
39
+ ```sh
40
+ npx --yes -p @deepseek-ai/dsh@0.1.0-rc.6 \
41
+ dsh plugin --profile web add @zseven-w/dsh-openpencil
42
+ npx --yes -p @deepseek-ai/dsh@0.1.0-rc.6 dsh web
43
+ ```
44
+
45
+ Keep the private registry credential in a user-level or temporary npm config outside the checkout. This repository intentionally contains no registry credentials.
46
+
47
+ ## Rendering contract
48
+
49
+ `openpencil_render` accepts a `.op` path, an optional `scale` (`0 < scale <= 8`, default `1`), and optional `editable` (`false` by default). Leave `width` and `height` unset for the exact OpenPencil path: they describe a runtime viewport, not design export dimensions, and are accepted only by the lower-fidelity Jian fallback.
50
+
51
+ OpenPencil binary discovery checks, in order:
52
+
53
+ 1. `DSH_OPENPENCIL_BINARY` or `DSH_OPENPENCIL_DESKTOP`
54
+ 2. `/Applications/OpenPencil.app/Contents/MacOS/openpencil-desktop`
55
+ 3. `~/Applications/OpenPencil.app/Contents/MacOS/openpencil-desktop`
56
+ 4. `openpencil-desktop` on `PATH`
57
+
58
+ Jian fallback discovery uses `DSH_OPENPENCIL_JIAN`, a known local release build, then `PATH`.
59
+
60
+ ## Web viewer assets
61
+
62
+ DSH serves only `client.js` for a client plugin, so the OpenPencil ESM SDK, its WASM, and CanvasKit are staged as explicit same-origin assets:
63
+
64
+ ```sh
65
+ npm run sync:viewer-assets
66
+ ```
67
+
68
+ The sync command defaults to a sibling `../openpencil` checkout. Override it with `OPENPENCIL_ROOT` or `--openpencil-root`. A complete prebuilt asset directory can be selected with `DSH_OPENPENCIL_VIEWER_SOURCE`. Runtime lookup can be overridden with `DSH_OPENPENCIL_VIEWER_ASSET_DIR`.
69
+
70
+ Viewer assets are lazy-loaded only after the user opens the canvas. If they are absent or invalid, PNG preview remains available and no canvas button is advertised.
71
+
72
+ ## Managed editor
73
+
74
+ Editable sessions use OpenPencil's managed web host, the same architecture used by `op-vscode`. The plugin starts the host only after an authorized user action, keeps the daemon token in memory, validates iframe source and origin, and closes the process when the editor session ends. The editor surface is selected progressively: native Tool details when the host declares that seam, otherwise the plugin's right-hand workbench with resize and full-screen controls.
75
+
76
+ If DSH reloads or unloads the plugin while the canvas is dirty, the host keeps an opaque local recovery draft for up to seven days. Reopening the same source asks before restoring it into the live canvas; recovery never overwrites the `.op` file until the user explicitly saves.
77
+
78
+ Binary and source discovery can be overridden with:
79
+
80
+ - `DSH_OPENPENCIL_EDITOR_BINARY` for `op-host-web-server`;
81
+ - `DSH_OPENPENCIL_SOURCE_ROOT` (or `OPENPENCIL_SOURCE_ROOT`) for the web bundle and CanvasKit assets.
82
+
83
+ Saves use an optimistic source hash, an atomic replace, and a successor capability. If the source changes outside the editor, the plugin reports a conflict instead of overwriting it.
84
+
85
+ ## Build and verify
86
+
87
+ ```sh
88
+ npm run sync:viewer-assets
89
+ npm run build
90
+ npm run test:viewer-assets
91
+ npm run test:client
92
+ npm run test:host -- /absolute/path/to/design.op 375 1091
93
+ ```
94
+
95
+ Builds require Node 24.11 or newer. DSH host/client packages are peer dependencies supplied by the target DSH profile. Build tools are resolved from local dev dependencies, the active linked DSH checkout, or an installed DSH source bundle; `DSH_SOURCE_ROOT` can select a source checkout explicitly. The lockfile pins standalone public build tooling when that environment is provisioned separately.
96
+
97
+ For a private DSH prerelease, keep the issued npm credential outside this repository (for example in a user-level or temporary `.npmrc`) and run the requested version directly:
98
+
99
+ ```sh
100
+ npx --yes -p @deepseek-ai/dsh@0.1.0-rc.6 dsh web
101
+ ```
102
+
103
+ Never commit `.npmrc`, `NPM_TOKEN`, or copied registry credentials. This repository ignores local npm configuration by default.
104
+
105
+ `test:host` performs a real exact render, validates PNG IHDR geometry and SHA-256, exercises immutable image/document capabilities over HTTP, and checks that viewer assets are grantable. The expected dimensions are fixture-specific.
106
+
107
+ ## Result metadata
108
+
109
+ The model-visible result stays plain JSON. Browser-only `presentationMeta.$dshOpenPencil` carries additive grants for:
110
+
111
+ - `image`: PNG path, preview/download URLs, and real width/height;
112
+ - `frames`: every exact-rendered top-level frame in active-page order, including its node id/name/index and signed PNG URLs;
113
+ - `document`: source action path plus immutable snapshot URL, bytes, and SHA-256;
114
+ - `viewer`: revisioned SDK/WASM/CanvasKit URLs when the asset route is attached.
115
+ - `editor`: scoped launch/refresh capabilities when `editable: true` is authorized.
116
+
117
+ The result also records `renderer`, `rendererBinary`, `fidelity`, and any warnings. Existing PNG-only schema-v1 messages remain renderable.
118
+
119
+ Published DSH releases through 0.1.0-rc.6 do not persist browser presentation metadata for tools nested under PTC/Code Mode. The plugin recovers that UI-only projection through a same-origin, session-bound endpoint: the browser sends only the session id, call id, and immutable document SHA-256, while the host resolves the authoritative result from the durable DSH session log and uses a short-lived in-process marker only to authorize recent live editing. Signed preview/editor capabilities never enter the canonical tool result or model context. Durable history can restore read-only previews; editor grants are issued only for recent, trusted live results.
120
+
121
+ For bounded replay, nested metadata recovery accepts up to 128 top-level frames; larger Code Mode results remain available through their canonical JSON fallback.
122
+
123
+ ## Agent design workflow
124
+
125
+ For a natural-language request with no existing document, the Agent should call `openpencil_new` with a new workspace-relative `.op` path and the first complete `batch_design` program. The tool runs that program in a private managed OpenPencil daemon and publishes the authoritative document only after the whole batch succeeds. It never overwrites an existing path and a failed batch leaves no empty file behind. The Agent should then call `openpencil_render` with the returned path, `editable: true`, and `autoOpen: true` to present the gallery and expand the editor once. Replayed or initially-settled historical cards never auto-open.
126
+
127
+ Use `openpencil_create` and `openpencil_edit` only for an existing live canvas. Their edits remain unsaved until the editor Save action.
128
+
129
+ ## Current limits
130
+
131
+ - Follow-up edits to an existing canvas require an already-open managed editor. Changes remain unsaved until the user invokes its Save action.
132
+ - The lightweight Web SDK canvas is read-only; full editing uses the separate managed editor surface. Published releases through 0.1.0-rc.6 use the resizable right workbench with a full-screen option.
133
+ - The exact gallery covers top-level frames on the active page; the interactive canvas remains the way to inspect inactive pages and nested nodes.
134
+ - Render and snapshot caches still need a product-level retention policy.
@@ -0,0 +1,69 @@
1
+ # Third-Party Notices
2
+
3
+ This distribution includes the following third-party components.
4
+
5
+ ## OpenPencil Web SDK 0.8.4
6
+
7
+ - Files: `lib/viewer-assets/sdk.js`, `lib/viewer-assets/op_web_sdk_bg.wasm`
8
+ - Source: https://github.com/ZSeven-W/openpencil
9
+ - License: MIT
10
+
11
+ ```text
12
+ MIT License
13
+
14
+ Copyright (c) 2026 ZSeven—W
15
+
16
+ Permission is hereby granted, free of charge, to any person obtaining a copy
17
+ of this software and associated documentation files (the "Software"), to deal
18
+ in the Software without restriction, including without limitation the rights
19
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
20
+ copies of the Software, and to permit persons to whom the Software is
21
+ furnished to do so, subject to the following conditions:
22
+
23
+ The above copyright notice and this permission notice shall be included in all
24
+ copies or substantial portions of the Software.
25
+
26
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
32
+ SOFTWARE.
33
+ ```
34
+
35
+ ## CanvasKit (`canvaskit-wasm` 0.40.0)
36
+
37
+ - Files: `lib/viewer-assets/canvaskit/canvaskit.js`, `lib/viewer-assets/canvaskit/canvaskit.wasm`
38
+ - Source: https://www.npmjs.com/package/canvaskit-wasm/v/0.40.0
39
+ - License: BSD-3-Clause
40
+
41
+ ```text
42
+ Copyright (c) 2011 Google Inc. All rights reserved.
43
+
44
+ Redistribution and use in source and binary forms, with or without
45
+ modification, are permitted provided that the following conditions are
46
+ met:
47
+
48
+ * Redistributions of source code must retain the above copyright
49
+ notice, this list of conditions and the following disclaimer.
50
+ * Redistributions in binary form must reproduce the above
51
+ copyright notice, this list of conditions and the following disclaimer
52
+ in the documentation and/or other materials provided with the
53
+ distribution.
54
+ * Neither the name of Google Inc. nor the names of its
55
+ contributors may be used to endorse or promote products derived from
56
+ this software without specific prior written permission.
57
+
58
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
59
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
60
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
61
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
62
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
63
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
64
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
65
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
66
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
67
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
68
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
69
+ ```
@@ -0,0 +1,6 @@
1
+ # dsh-openpencil bundle patch: mounts the plugin into a profile layer stack.
2
+ # The profile's own cordis.patch.yml and any --patch overlays still apply
3
+ # after this layer; rows here can be overridden by id in later layers.
4
+ - insert:
5
+ - id: dsh-openpencil
6
+ name: '@zseven-w/dsh-openpencil'
@@ -0,0 +1,28 @@
1
+ /** Compatibility boundary for future DSH builds that add a keyed Tool-details seam. */
2
+ import type { ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client';
3
+ import type { DetailsToolOwnerProps } from '@deepseek-ai/dsh-client-ui-conversation/client';
4
+ import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
5
+ /**
6
+ * Published DSH rc.2 through 0.1.0-rc.6 do not declare this slot. Keeping the
7
+ * proposed additive contract local lets a future supporting host activate the
8
+ * native surface without breaking current hosts. At runtime `slots.inject()`
9
+ * waits while a slot is absent, so current releases use the plugin workbench.
10
+ */
11
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
12
+ interface SlotMap {
13
+ 'tool.details.toolview': {
14
+ kind: 'keyed';
15
+ scope: 'session';
16
+ owner: DetailsToolOwnerProps;
17
+ };
18
+ }
19
+ }
20
+ /** Details props without importing a symbol absent from current DSH releases. */
21
+ export type CompatibleToolDetailsViewProps = PropsRuntime<'tool.details.toolview'>;
22
+ /** Call props with a possible future additive sidebar capability. */
23
+ export type CompatibleToolCallViewProps = ToolCallViewProps & {
24
+ openDetails?: (() => void) | undefined;
25
+ };
26
+ export type OpenPencilEditorSurface = 'details' | 'modal';
27
+ /** Prefer the native resident details panel and otherwise open our own modal. */
28
+ export declare function requestOpenPencilEditor(openDetails: (() => void) | undefined, openModal: () => void): OpenPencilEditorSurface;
@@ -0,0 +1,10 @@
1
+ /** Compatibility boundary for future DSH builds that add a keyed Tool-details seam. */
2
+ /** Prefer the native resident details panel and otherwise open our own modal. */
3
+ export function requestOpenPencilEditor(openDetails, openModal) {
4
+ if (openDetails !== undefined) {
5
+ openDetails();
6
+ return 'details';
7
+ }
8
+ openModal();
9
+ return 'modal';
10
+ }
@@ -0,0 +1,84 @@
1
+ /** Browser-side protocol helpers for the managed OpenPencil editor iframe. */
2
+ export type EditorInboundMessage = {
3
+ type: 'op-bridge/ready';
4
+ generation: number;
5
+ revision: number;
6
+ } | {
7
+ type: 'op-bridge/opened';
8
+ generation: number;
9
+ } | {
10
+ type: 'op-bridge/dirty-changed';
11
+ generation: number;
12
+ revision: number;
13
+ dirty: boolean;
14
+ } | {
15
+ type: 'op-bridge/snapshot-result';
16
+ requestId: string;
17
+ docJson: string;
18
+ generation: number;
19
+ revision: number;
20
+ } | {
21
+ type: 'op-bridge/snapshot-conflict';
22
+ requestId: string;
23
+ serverVersion: number;
24
+ } | {
25
+ type: 'op-bridge/sync-conflict';
26
+ generation: number;
27
+ revision: number;
28
+ serverVersion: number;
29
+ } | {
30
+ type: 'op-bridge/conflict-resolved';
31
+ requestId: string;
32
+ } | {
33
+ type: 'op-shell/save';
34
+ } | {
35
+ type: 'op-shell/copy';
36
+ text: string;
37
+ };
38
+ export type EditorOutboundMessage = {
39
+ type: 'op-bridge/init';
40
+ token: string;
41
+ mcpUrl?: string;
42
+ } | {
43
+ type: 'op-bridge/theme';
44
+ colorScheme: EditorColorScheme;
45
+ } | {
46
+ type: 'op-bridge/locale';
47
+ locale: EditorLocale;
48
+ } | {
49
+ type: 'op-bridge/open-document';
50
+ json: string;
51
+ } | {
52
+ type: 'op-bridge/snapshot';
53
+ purpose: 'save';
54
+ requestId: string;
55
+ } | {
56
+ type: 'op-bridge/save-committed';
57
+ generation: number;
58
+ revision: number;
59
+ };
60
+ export type EditorColorScheme = 'light' | 'dark';
61
+ export type EditorLocale = 'zh-CN' | 'en-US';
62
+ /** Parse only the editor/host messages DSH implements. Unknown traffic is ignored. */
63
+ export declare function parseEditorInbound(raw: unknown): EditorInboundMessage | undefined;
64
+ export declare function encodeEditorOutbound(message: EditorOutboundMessage): string;
65
+ /** Require an absolute loopback editor URL and derive its exact target origin. */
66
+ export declare function editorOrigin(iframeUrl: string): string;
67
+ /** Pin the host's resolved theme into the editor's first navigation. */
68
+ export declare function editorIframeUrlWithTheme(iframeUrl: string, colorScheme: EditorColorScheme): string;
69
+ /** Pin the host's resolved locale into the editor's first navigation. */
70
+ export declare function editorIframeUrlWithLocale(iframeUrl: string, locale: EditorLocale): string;
71
+ /** Translate DSH's compact locale id to the editor's BCP 47 contract. */
72
+ export declare function editorLocaleFromDsh(locale: 'zh' | 'en'): EditorLocale;
73
+ /** Resolve a launch/save/close capability and reject cross-origin control routes. */
74
+ export declare function editorControlUrl(raw: string, base?: string): string;
75
+ /** Validate source and exact origin before parsing any iframe message. */
76
+ export declare function editorMessageFrom(event: Pick<MessageEvent, 'source' | 'origin' | 'data'>, frameWindow: Window | null, origin: string): EditorInboundMessage | undefined;
77
+ /** Read-only gate for background auto-open flows; never asks an owner to close. */
78
+ export declare function hasActiveEditor(): boolean;
79
+ /** Page-wide single-editor coordinator. An existing dirty editor may veto takeover. */
80
+ export declare function claimEditor(token: symbol, close: () => boolean | void, options?: {
81
+ replace?: boolean;
82
+ }): (() => void) | undefined;
83
+ /** Confirm before a user-driven panel close would discard unsaved canvas edits. */
84
+ export declare function confirmEditorClose(dirty: boolean, confirm?: ((message?: string) => boolean) & typeof globalThis.confirm): boolean;
@@ -0,0 +1,127 @@
1
+ /** Browser-side protocol helpers for the managed OpenPencil editor iframe. */
2
+ function isRecord(value) {
3
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
4
+ }
5
+ function safeInteger(value) {
6
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
7
+ }
8
+ function string(value) {
9
+ return typeof value === 'string';
10
+ }
11
+ /** Parse only the editor/host messages DSH implements. Unknown traffic is ignored. */
12
+ export function parseEditorInbound(raw) {
13
+ if (typeof raw !== 'string')
14
+ return undefined;
15
+ let value;
16
+ try {
17
+ value = JSON.parse(raw);
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ if (!isRecord(value) || typeof value.type !== 'string')
23
+ return undefined;
24
+ switch (value.type) {
25
+ case 'op-bridge/ready':
26
+ return safeInteger(value.generation) && safeInteger(value.revision)
27
+ ? { type: value.type, generation: value.generation, revision: value.revision }
28
+ : undefined;
29
+ case 'op-bridge/opened':
30
+ return safeInteger(value.generation) ? { type: value.type, generation: value.generation } : undefined;
31
+ case 'op-bridge/dirty-changed':
32
+ return safeInteger(value.generation) && safeInteger(value.revision) && typeof value.dirty === 'boolean'
33
+ ? { type: value.type, generation: value.generation, revision: value.revision, dirty: value.dirty }
34
+ : undefined;
35
+ case 'op-bridge/snapshot-result':
36
+ return string(value.requestId) && string(value.docJson)
37
+ && safeInteger(value.generation) && safeInteger(value.revision)
38
+ ? {
39
+ type: value.type,
40
+ requestId: value.requestId,
41
+ docJson: value.docJson,
42
+ generation: value.generation,
43
+ revision: value.revision,
44
+ }
45
+ : undefined;
46
+ case 'op-bridge/snapshot-conflict':
47
+ return string(value.requestId) && safeInteger(value.serverVersion)
48
+ ? { type: value.type, requestId: value.requestId, serverVersion: value.serverVersion }
49
+ : undefined;
50
+ case 'op-bridge/sync-conflict':
51
+ return safeInteger(value.generation) && safeInteger(value.revision) && safeInteger(value.serverVersion)
52
+ ? { type: value.type, generation: value.generation, revision: value.revision, serverVersion: value.serverVersion }
53
+ : undefined;
54
+ case 'op-bridge/conflict-resolved':
55
+ return string(value.requestId) ? { type: value.type, requestId: value.requestId } : undefined;
56
+ case 'op-shell/save':
57
+ return { type: value.type };
58
+ case 'op-shell/copy':
59
+ return string(value.text) ? { type: value.type, text: value.text } : undefined;
60
+ default:
61
+ return undefined;
62
+ }
63
+ }
64
+ export function encodeEditorOutbound(message) {
65
+ return JSON.stringify(message);
66
+ }
67
+ /** Require an absolute loopback editor URL and derive its exact target origin. */
68
+ export function editorOrigin(iframeUrl) {
69
+ const url = new URL(iframeUrl);
70
+ const loopback = url.hostname === '127.0.0.1' || url.hostname === 'localhost' || url.hostname === '::1';
71
+ if (!loopback || (url.protocol !== 'http:' && url.protocol !== 'https:')) {
72
+ throw new Error('OpenPencil editor URL must use an HTTP loopback origin');
73
+ }
74
+ return url.origin;
75
+ }
76
+ /** Pin the host's resolved theme into the editor's first navigation. */
77
+ export function editorIframeUrlWithTheme(iframeUrl, colorScheme) {
78
+ const url = new URL(iframeUrl);
79
+ url.searchParams.set('theme', colorScheme);
80
+ return url.href;
81
+ }
82
+ /** Pin the host's resolved locale into the editor's first navigation. */
83
+ export function editorIframeUrlWithLocale(iframeUrl, locale) {
84
+ const url = new URL(iframeUrl);
85
+ url.searchParams.set('locale', locale);
86
+ return url.href;
87
+ }
88
+ /** Translate DSH's compact locale id to the editor's BCP 47 contract. */
89
+ export function editorLocaleFromDsh(locale) {
90
+ return locale === 'zh' ? 'zh-CN' : 'en-US';
91
+ }
92
+ /** Resolve a launch/save/close capability and reject cross-origin control routes. */
93
+ export function editorControlUrl(raw, base = window.location.href) {
94
+ const page = new URL(base);
95
+ const url = new URL(raw, page);
96
+ if (url.origin !== page.origin)
97
+ throw new Error('OpenPencil editor control URL must be same-origin');
98
+ return url.href;
99
+ }
100
+ /** Validate source and exact origin before parsing any iframe message. */
101
+ export function editorMessageFrom(event, frameWindow, origin) {
102
+ if (frameWindow === null || event.source !== frameWindow || event.origin !== origin)
103
+ return undefined;
104
+ return parseEditorInbound(event.data);
105
+ }
106
+ let activeEditor;
107
+ /** Read-only gate for background auto-open flows; never asks an owner to close. */
108
+ export function hasActiveEditor() {
109
+ return activeEditor !== undefined;
110
+ }
111
+ /** Page-wide single-editor coordinator. An existing dirty editor may veto takeover. */
112
+ export function claimEditor(token, close, options = {}) {
113
+ const previous = activeEditor;
114
+ if (previous !== undefined && previous.token !== token) {
115
+ if (options.replace === false || previous.close() === false)
116
+ return undefined;
117
+ }
118
+ activeEditor = { token, close };
119
+ return () => {
120
+ if (activeEditor?.token === token)
121
+ activeEditor = undefined;
122
+ };
123
+ }
124
+ /** Confirm before a user-driven panel close would discard unsaved canvas edits. */
125
+ export function confirmEditorClose(dirty, confirm = window.confirm) {
126
+ return !dirty || confirm('OpenPencil has unsaved changes. Close the editor and discard them?');
127
+ }
@@ -0,0 +1,15 @@
1
+ /** Self-contained DSH layout push used by the fallback OpenPencil workbench. */
2
+ export declare const OPENPENCIL_WORKBENCH_DOCK_ATTRIBUTE = "openpencilWorkbenchDockOwner";
3
+ export interface EditorWorkbenchDockLease {
4
+ update: (width: number) => void;
5
+ release: () => void;
6
+ }
7
+ /**
8
+ * Reserve real layout space for the fixed right-hand workbench.
9
+ *
10
+ * DSH's root is an auto-width block, so a right margin shrinks its AppFrame
11
+ * grid instead of covering the conversation. Ownership and exact inline-style
12
+ * restoration keep this compatible with HMR and fail closed around another
13
+ * plugin that already owns the root margin.
14
+ */
15
+ export declare function claimEditorWorkbenchDock(root: HTMLElement, owner: string, initialWidth: number, computedMarginRight?: number): EditorWorkbenchDockLease | undefined;
@@ -0,0 +1,43 @@
1
+ /** Self-contained DSH layout push used by the fallback OpenPencil workbench. */
2
+ export const OPENPENCIL_WORKBENCH_DOCK_ATTRIBUTE = 'openpencilWorkbenchDockOwner';
3
+ function dockWidth(width) {
4
+ return `${Math.max(0, Math.round(width))}px`;
5
+ }
6
+ /**
7
+ * Reserve real layout space for the fixed right-hand workbench.
8
+ *
9
+ * DSH's root is an auto-width block, so a right margin shrinks its AppFrame
10
+ * grid instead of covering the conversation. Ownership and exact inline-style
11
+ * restoration keep this compatible with HMR and fail closed around another
12
+ * plugin that already owns the root margin.
13
+ */
14
+ export function claimEditorWorkbenchDock(root, owner, initialWidth, computedMarginRight = 0) {
15
+ const existingOwner = root.dataset[OPENPENCIL_WORKBENCH_DOCK_ATTRIBUTE];
16
+ if (existingOwner !== undefined && existingOwner !== owner)
17
+ return undefined;
18
+ if (existingOwner === undefined && (root.style.marginRight.trim() !== ''
19
+ || (Number.isFinite(computedMarginRight) && computedMarginRight > 0.5)))
20
+ return undefined;
21
+ const previousMarginRight = root.style.marginRight;
22
+ const previousMinWidth = root.style.minWidth;
23
+ root.dataset[OPENPENCIL_WORKBENCH_DOCK_ATTRIBUTE] = owner;
24
+ root.style.minWidth = '0';
25
+ let released = false;
26
+ const update = (width) => {
27
+ if (released || root.dataset[OPENPENCIL_WORKBENCH_DOCK_ATTRIBUTE] !== owner)
28
+ return;
29
+ root.style.marginRight = dockWidth(width);
30
+ };
31
+ const release = () => {
32
+ if (released)
33
+ return;
34
+ released = true;
35
+ if (root.dataset[OPENPENCIL_WORKBENCH_DOCK_ATTRIBUTE] !== owner)
36
+ return;
37
+ root.style.marginRight = previousMarginRight;
38
+ root.style.minWidth = previousMinWidth;
39
+ delete root.dataset[OPENPENCIL_WORKBENCH_DOCK_ATTRIBUTE];
40
+ };
41
+ update(initialWidth);
42
+ return { update, release };
43
+ }
@@ -0,0 +1,27 @@
1
+ /** Same-origin client controls for durable managed-editor recovery. */
2
+ import { type EditorLocale } from './editor-bridge.js';
3
+ export interface EditorRecoverySummary {
4
+ id: string;
5
+ capturedAt: number;
6
+ bytes: number;
7
+ sourceName: string;
8
+ sourceChangedSinceCapture: boolean;
9
+ cacheLabel: string;
10
+ }
11
+ export interface EditorRecoveryLaunch {
12
+ sessionId: string;
13
+ recoveryUrl?: string;
14
+ recovery?: EditorRecoverySummary;
15
+ }
16
+ export declare function editorRecoverySummaryOf(value: unknown): EditorRecoverySummary | undefined;
17
+ export declare function editorRecoveryItemUrl(launch: EditorRecoveryLaunch, recoveryId: string): string;
18
+ /** Capture the authoritative daemon document; never serializes the iframe from React. */
19
+ export declare function captureManagedEditorRecovery(launch: EditorRecoveryLaunch, fetcher?: typeof fetch): Promise<EditorRecoverySummary | undefined>;
20
+ /** Explicitly restore into the live daemon. The user must still press Save to update `.op`. */
21
+ export declare function restoreManagedEditorRecovery(launch: EditorRecoveryLaunch, recovery: EditorRecoverySummary, fetcher?: typeof fetch): Promise<string>;
22
+ export declare function discardManagedEditorRecovery(launch: EditorRecoveryLaunch, recovery: EditorRecoverySummary, fetcher?: typeof fetch): Promise<void>;
23
+ export interface EditorRecoveryCopy {
24
+ available: (sourceName: string) => string;
25
+ conflict: (sourceName: string) => string;
26
+ }
27
+ export declare function editorRecoveryCopy(locale: EditorLocale): EditorRecoveryCopy;