@vibe-x/dsh-html-workbench 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.
@@ -0,0 +1,6 @@
1
+ # HTML Workbench 插件层:给 DSH Web profile 增加右侧栏 HTML 预览/编辑面板。
2
+ # 作为一层 profile 装载:一个 node half(拉起/复用 html-workbench 服务 + 资产追踪),
3
+ # 加一个 client bundle(在浏览器里挂载右侧面板,无核心改动)。
4
+ - insert:
5
+ - id: html-workbench
6
+ name: '@vibe-x/dsh-html-workbench'
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "@vibe-x/dsh-html-workbench",
3
+ "version": "0.1.0",
4
+ "main": "./src/index.js",
5
+ "description": "在 DeepSeek Harness 右侧栏预览并可视化编辑 agent 生成的 HTML:复用/拉起本地 html-workbench 服务,在 iframe 中内嵌 GrapesJS 编辑器,插件卸载时停掉自己启动的服务。",
6
+ "engines": {
7
+ "dsh": ">=0.0.1"
8
+ },
9
+ "contributes": {
10
+ "tools": [],
11
+ "skills": []
12
+ },
13
+ "client": {
14
+ "main": "./lib/client.js"
15
+ }
16
+ }
package/lib/client.js ADDED
@@ -0,0 +1,267 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@vibe-x/dsh-html-workbench",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+
7
+ // Closure symbols — the same names the dynamic runner injects.
8
+ const React = require('react');
9
+ const styles = {
10
+ insert(css) {
11
+ if (typeof document === 'undefined') return () => {};
12
+ const id = 'html-workbench-dsh-plugin-styles';
13
+ if (document.getElementById(id)) return () => {};
14
+ const el = document.createElement('style');
15
+ el.id = id;
16
+ el.textContent = css;
17
+ document.head.appendChild(el);
18
+ return () => { if (el.parentNode) el.parentNode.removeChild(el); };
19
+ },
20
+ };
21
+ const host = {
22
+ call(method, args) {
23
+ const a = args || {};
24
+ let url = '/html-workbench/' + method;
25
+ if (method === 'open' && a.file) url += '?file=' + encodeURIComponent(a.file);
26
+ return fetch(url).then((r) => r.json());
27
+ },
28
+ };
29
+
30
+ const plugin = (() => {
31
+ /**
32
+ * HTML Workbench DSH plugin — Client half (function body).
33
+ *
34
+ * This file is the plain-JavaScript function body consumed by DeepSeek
35
+ * Harness's dynamic Cordis plugin loader: pass this exact text as `code.client`
36
+ * to `cordis_define`. The static client bundle wraps this same body in
37
+ * `window.__ModuleLoader__.load(...)` at build time.
38
+ *
39
+ * Responsibilities (runs in the browser):
40
+ * - Register a right-side panel + corner trigger in `shell.overlay`.
41
+ * - Preview takes the full panel height; the HTML-asset picker is a compact,
42
+ * collapsible dock at the bottom (a <select> switcher + expandable list).
43
+ * - Open a file by calling Host `open`, then embed the workbench URL in an
44
+ * iframe. Left edge is draggable to resize the panel width.
45
+ */
46
+
47
+ return {
48
+ inject: ['timer'],
49
+ apply(ctx) {
50
+ const slots = ctx.get('slots')
51
+ if (slots === undefined) return
52
+
53
+ styles.insert(`
54
+ html #root { margin-right: var(--hwb-panel-width, 0px); transition: margin-right var(--ds-transition-duration-slow, 200ms) ease; }
55
+ body[data-hwb-dragging] #root { transition: none; }
56
+ body[data-hwb-dragging] { user-select: none; }
57
+ .hwb-panel, .hwb-panel * { box-sizing: border-box; }
58
+ .hwb-panel { position: fixed; top: 0; right: 0; bottom: 0; width: 820px; max-width: calc(100vw - 24px); display: flex; flex-direction: column; background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); border-left: 1px solid var(--dsw-alias-border-l1); box-shadow: var(--dsw-shadow-lv2); z-index: 9999; font-family: var(--dsw-font-family, system-ui, sans-serif); font-size: 13px; line-height: 1.5; pointer-events: auto; }
59
+ .hwb-resize { position: absolute; left: -4px; top: 0; bottom: 0; width: 9px; cursor: col-resize; z-index: 3; touch-action: none; }
60
+ .hwb-resize::after { content: ""; position: absolute; left: 4px; top: 0; bottom: 0; width: 2px; background: transparent; transition: background .15s; }
61
+ .hwb-resize:hover::after, .hwb-resize:active::after { background: var(--dsw-alias-interactive-bg-hover-accent); }
62
+ .hwb-head { display: flex; align-items: center; gap: 10px; padding: 10px 14px; border-bottom: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1); flex: none; }
63
+ .hwb-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--dsw-alias-state-error-primary); flex: none; }
64
+ .hwb-dot.on { background: var(--dsw-alias-state-success-primary); }
65
+ .hwb-titlewrap { min-width: 0; display: flex; flex-direction: column; }
66
+ .hwb-title { font-weight: 600; font-size: 13px; line-height: 1.2; white-space: nowrap; }
67
+ .hwb-sub { font-size: 11px; color: var(--dsw-alias-label-tertiary); line-height: 1.2; }
68
+ .hwb-spacer { flex: 1; }
69
+ .hwb-close { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border: none; background: transparent; color: var(--dsw-alias-label-secondary); cursor: pointer; border-radius: 6px; flex: none; }
70
+ .hwb-close:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); }
71
+ .hwb-preview { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; background: #fff; }
72
+ .hwb-frame { flex: 1; border: 0; width: 100%; }
73
+ .hwb-hint { flex: 1; display: flex; align-items: center; justify-content: center; color: var(--dsw-alias-label-tertiary); text-align: center; padding: 16px; }
74
+ .hwb-error { padding: 10px 14px; color: var(--dsw-alias-state-error-primary); font-size: 12px; }
75
+ .hwb-dock { flex: none; border-top: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); display: flex; flex-direction: column; }
76
+ .hwb-dockbar { display: flex; align-items: center; gap: 8px; padding: 6px 10px; }
77
+ .hwb-select { flex: 1; min-width: 0; height: 28px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 6px; background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; padding: 0 8px; cursor: pointer; }
78
+ .hwb-count { font-size: 11px; color: var(--dsw-alias-label-tertiary); flex: none; white-space: nowrap; }
79
+ .hwb-docktoggle { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border: none; background: transparent; color: var(--dsw-alias-label-secondary); cursor: pointer; border-radius: 6px; flex: none; }
80
+ .hwb-docktoggle:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); }
81
+ .hwb-docklist { flex: none; max-height: 38vh; overflow-y: auto; border-top: 1px solid var(--dsw-alias-border-l2); }
82
+ .hwb-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-bottom: 1px solid var(--dsw-alias-border-l2); cursor: pointer; }
83
+ .hwb-item:hover { background: var(--dsw-alias-interactive-bg-hover); }
84
+ .hwb-item.active { background: var(--dsw-alias-interactive-bg-active); }
85
+ .hwb-item-main { flex: 1; min-width: 0; }
86
+ .hwb-item-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
87
+ .hwb-item-path { font-size: 11px; color: var(--dsw-alias-label-tertiary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
88
+ .hwb-item-btn { border: 1px solid var(--dsw-alias-border-l2); background: transparent; color: var(--dsw-alias-label-secondary); border-radius: 6px; padding: 2px 9px; cursor: pointer; font-size: 12px; flex: none; }
89
+ .hwb-item-btn:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
90
+ .hwb-dockempty { padding: 14px; color: var(--dsw-alias-label-tertiary); text-align: center; font-size: 12px; }
91
+ .hwb-trigger { position: fixed; top: 10px; right: 12px; z-index: 10000; width: 36px; height: 36px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-secondary); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; border-radius: 8px; pointer-events: auto; }
92
+ .hwb-trigger:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); }
93
+ `)
94
+
95
+ const DEFAULT_WIDTH = 820
96
+ const MIN_WIDTH = 480
97
+
98
+ const store = {
99
+ open: false,
100
+ panelWidth: DEFAULT_WIDTH,
101
+ expanded: false,
102
+ assets: [],
103
+ running: false,
104
+ current: null,
105
+ loading: false,
106
+ error: null,
107
+ listeners: [],
108
+ subscribe(fn) { this.listeners.push(fn); return () => { this.listeners = this.listeners.filter((f) => f !== fn) } },
109
+ set(p) { Object.assign(this, p); this.listeners.forEach((fn) => { try { fn() } catch (e) {} }) },
110
+ }
111
+
112
+ const useStore = () => {
113
+ const [, force] = React.useState(0)
114
+ React.useEffect(() => store.subscribe(() => force((x) => x + 1)), [])
115
+ return store
116
+ }
117
+
118
+ const basename = (p) => {
119
+ const parts = String(p).split('/')
120
+ return parts[parts.length - 1] || p
121
+ }
122
+
123
+ const refresh = () => {
124
+ host.call('list').then((res) => {
125
+ if (res && res.ok) store.set({ assets: res.assets || [], running: !!res.running })
126
+ else store.set({ error: (res && res.error) || 'list failed' })
127
+ }).catch((e) => store.set({ error: String(e && e.message ? e.message : e) }))
128
+ }
129
+
130
+ const openFile = (path) => {
131
+ store.set({ loading: true, current: { path: path, url: null }, error: null })
132
+ host.call('open', { file: path }).then((res) => {
133
+ if (res && res.ok) store.set({ loading: false, current: { path: path, url: res.url }, running: true })
134
+ else store.set({ loading: false, error: (res && res.error) || 'open failed' })
135
+ }).catch((e) => store.set({ loading: false, error: String(e && e.message ? e.message : e) }))
136
+ }
137
+
138
+ const startResize = (e) => {
139
+ e.preventDefault()
140
+ document.body.setAttribute('data-hwb-dragging', '')
141
+ const maxW = Math.max(MIN_WIDTH, window.innerWidth - 60)
142
+ const onMove = (ev) => {
143
+ const w = window.innerWidth - ev.clientX
144
+ store.set({ panelWidth: Math.max(MIN_WIDTH, Math.min(w, maxW)) })
145
+ }
146
+ const onUp = () => {
147
+ document.body.removeAttribute('data-hwb-dragging')
148
+ document.removeEventListener('mousemove', onMove)
149
+ document.removeEventListener('mouseup', onUp)
150
+ }
151
+ document.addEventListener('mousemove', onMove)
152
+ document.addEventListener('mouseup', onUp)
153
+ }
154
+
155
+ const chevronUp = React.createElement('svg', { width: 14, height: 14, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
156
+ React.createElement('path', { d: 'M18 15l-6-6-6 6' }))
157
+ const chevronDown = React.createElement('svg', { width: 14, height: 14, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
158
+ React.createElement('path', { d: 'M6 9l6 6 6-6' }))
159
+
160
+ const Panel = () => {
161
+ const s = useStore()
162
+ React.useEffect(() => {
163
+ const root = document.documentElement
164
+ root.style.setProperty('--hwb-panel-width', s.open ? (s.panelWidth + 'px') : '0px')
165
+ return () => { root.style.setProperty('--hwb-panel-width', '0px') }
166
+ }, [s.open, s.panelWidth])
167
+ React.useEffect(() => {
168
+ if (!s.open) return
169
+ refresh()
170
+ const dispose = ctx.interval(refresh, 3000)
171
+ return () => { if (dispose) dispose() }
172
+ }, [s.open])
173
+ if (!s.open) return null
174
+
175
+ const assets = s.assets || []
176
+ const currentPath = s.current && s.current.path ? s.current.path : ''
177
+ const opts = assets.map((a) => React.createElement('option', { key: a.id || a.path, value: a.path }, basename(a.path)))
178
+ const items = assets.map((a) => {
179
+ const active = currentPath === a.path
180
+ return React.createElement('div', { key: a.id || a.path, className: 'hwb-item' + (active ? ' active' : '') },
181
+ React.createElement('div', { className: 'hwb-item-main' },
182
+ React.createElement('div', { className: 'hwb-item-name' }, basename(a.path)),
183
+ React.createElement('div', { className: 'hwb-item-path', title: a.path }, a.path),
184
+ ),
185
+ React.createElement('button', { type: 'button', className: 'hwb-item-btn', onClick: () => openFile(a.path) }, '打开'),
186
+ )
187
+ })
188
+
189
+ return React.createElement('div', { className: 'hwb-panel', style: { width: s.panelWidth + 'px' }, role: 'dialog', 'aria-label': 'HTML Workbench' },
190
+ React.createElement('div', { className: 'hwb-resize', title: '拖动调整宽度', onMouseDown: startResize }),
191
+ React.createElement('div', { className: 'hwb-head' },
192
+ React.createElement('span', { className: 'hwb-dot' + (s.running ? ' on' : '') }),
193
+ React.createElement('div', { className: 'hwb-titlewrap' },
194
+ React.createElement('div', { className: 'hwb-title' }, 'HTML Workbench'),
195
+ React.createElement('div', { className: 'hwb-sub' }, s.running ? '服务运行中 · 拖动左边缘调整宽度' : '服务未就绪'),
196
+ ),
197
+ React.createElement('span', { className: 'hwb-spacer' }),
198
+ React.createElement('button', { type: 'button', className: 'hwb-close', title: '关闭', onClick: () => store.set({ open: false }) },
199
+ React.createElement('svg', { width: 14, height: 14, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round' },
200
+ React.createElement('path', { d: 'M18 6L6 18' }),
201
+ React.createElement('path', { d: 'M6 6l12 12' }),
202
+ ),
203
+ ),
204
+ ),
205
+ React.createElement('div', { className: 'hwb-preview' },
206
+ s.loading ? React.createElement('div', { className: 'hwb-hint' }, '正在启动 Workbench…')
207
+ : s.current && s.current.url ? React.createElement('iframe', { className: 'hwb-frame', src: s.current.url, title: 'HTML Workbench' })
208
+ : React.createElement('div', { className: 'hwb-hint' }, '从下方选择一个 HTML 文件开始编辑与预览'),
209
+ ),
210
+ React.createElement('div', { className: 'hwb-dock' },
211
+ React.createElement('div', { className: 'hwb-dockbar' },
212
+ React.createElement('select', {
213
+ className: 'hwb-select',
214
+ value: currentPath,
215
+ onChange: (e) => { const v = e.target.value; if (v) openFile(v) },
216
+ },
217
+ React.createElement('option', { value: '', disabled: true }, assets.length ? '选择 HTML 文件…' : '暂无 HTML 产物'),
218
+ opts,
219
+ ),
220
+ React.createElement('span', { className: 'hwb-count' }, assets.length + ' 个'),
221
+ React.createElement('button', { type: 'button', className: 'hwb-docktoggle', title: s.expanded ? '收起列表' : '展开列表', onClick: () => store.set({ expanded: !s.expanded }) },
222
+ s.expanded ? chevronDown : chevronUp,
223
+ ),
224
+ ),
225
+ s.expanded ? React.createElement('div', { className: 'hwb-docklist' },
226
+ items.length ? items : React.createElement('div', { className: 'hwb-dockempty' }, '暂无 HTML 产物 — 代理 write/edit 的 .html 文件会出现在这里。'),
227
+ s.error ? React.createElement('div', { className: 'hwb-error' }, s.error) : null,
228
+ ) : null,
229
+ ),
230
+ )
231
+ }
232
+
233
+ const Trigger = () => {
234
+ const s = useStore()
235
+ if (s.open) return null
236
+ return React.createElement('button', {
237
+ type: 'button',
238
+ className: 'hwb-trigger',
239
+ title: 'HTML Workbench',
240
+ 'aria-expanded': s.open,
241
+ onClick: () => { store.set({ open: true }); refresh() },
242
+ }, React.createElement('svg', { width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
243
+ React.createElement('rect', { x: 2, y: 4, width: 20, height: 14, rx: 2 }),
244
+ React.createElement('path', { d: 'M8 21h8' }),
245
+ React.createElement('path', { d: 'M12 18v3' }),
246
+ ))
247
+ }
248
+
249
+ slots.inject('shell.overlay', () => slots.register(
250
+ { name: 'shell.overlay', id: 'html-workbench-panel', order: 60, label: 'HTML Workbench' },
251
+ () => React.createElement(Panel),
252
+ ))
253
+
254
+ slots.inject('shell.overlay', () => slots.register(
255
+ { name: 'shell.overlay', id: 'html-workbench-trigger', order: 59, label: 'HTML Workbench 入口' },
256
+ () => React.createElement(Trigger),
257
+ ))
258
+ },
259
+ }
260
+
261
+ })();
262
+
263
+ exports.inject = plugin.inject ?? [];
264
+ exports.apply = plugin.apply;
265
+ return module.exports;
266
+ },
267
+ });
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@vibe-x/dsh-html-workbench",
3
+ "version": "0.1.0",
4
+ "description": "DeepSeek Harness 插件:在右侧栏预览并可视化编辑 agent 生成的 HTML(复用/拉起本地 html-workbench 服务)。",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "src/index.js",
8
+ "exports": {
9
+ ".": "./src/index.js",
10
+ "./client": "./lib/client.js",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "dsh": {
14
+ "bundle": {
15
+ "patch": "./cordis.patch.yml"
16
+ },
17
+ "client": {
18
+ "platform": "web",
19
+ "inject": [
20
+ "@deepseek-ai/dsh-client-runtime"
21
+ ]
22
+ }
23
+ },
24
+ "files": [
25
+ "src",
26
+ "lib/client.js",
27
+ "scripts/workbench.py",
28
+ "assets/workbench.html",
29
+ "cordis.patch.yml",
30
+ "dsh.plugin.json"
31
+ ],
32
+ "scripts": {
33
+ "build": "node build.mjs",
34
+ "prepublishOnly": "node build.mjs"
35
+ },
36
+ "keywords": [
37
+ "dsh",
38
+ "deepseek-harness",
39
+ "plugin",
40
+ "cordis",
41
+ "html",
42
+ "editor",
43
+ "workbench"
44
+ ],
45
+ "engines": {
46
+ "node": ">=20"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ }
51
+ }