justskill-design 0.0.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +235 -0
- package/README.md +184 -11
- package/SKILL.md +435 -0
- package/canvas/app.mjs +2870 -0
- package/canvas/groups.mjs +204 -0
- package/canvas/index.html +141 -0
- package/canvas/styles.css +1032 -0
- package/canvas/tree.mjs +249 -0
- package/cordis.patch.yml +8 -0
- package/design.mjs +172 -0
- package/docs/dsh-workbench.png +0 -0
- package/dsh/client.js +864 -0
- package/dsh/index.js +354 -0
- package/dsh/scan.js +139 -0
- package/dsh/skill.js +41 -0
- package/lib/base-css.mjs +92 -0
- package/lib/detect.mjs +123 -0
- package/lib/diagnostics.mjs +382 -0
- package/lib/export-html.mjs +309 -0
- package/lib/export-svg.mjs +111 -0
- package/lib/fragment.mjs +702 -0
- package/lib/geometry.mjs +827 -0
- package/lib/icons.mjs +106 -0
- package/lib/levenshtein.mjs +133 -0
- package/lib/node/cli.mjs +387 -0
- package/lib/node/export-svg.mjs +75 -0
- package/lib/node/fs.mjs +309 -0
- package/lib/node/op-json.mjs +75 -0
- package/lib/node/raster-run.mjs +115 -0
- package/lib/node/server.mjs +1264 -0
- package/lib/node/static.mjs +206 -0
- package/lib/node/verbs/add.mjs +220 -0
- package/lib/node/verbs/export.mjs +161 -0
- package/lib/node/verbs/format.mjs +106 -0
- package/lib/node/verbs/import.mjs +126 -0
- package/lib/node/verbs/ls.mjs +98 -0
- package/lib/node/verbs/migrate.mjs +236 -0
- package/lib/node/verbs/move.mjs +76 -0
- package/lib/node/verbs/new.mjs +284 -0
- package/lib/node/verbs/remove.mjs +41 -0
- package/lib/node/verbs/rename.mjs +87 -0
- package/lib/node/verbs/serve.mjs +164 -0
- package/lib/node/verbs/set.mjs +150 -0
- package/lib/node/verbs/show.mjs +122 -0
- package/lib/node/verbs/tokens.mjs +167 -0
- package/lib/node/verbs/validate.mjs +130 -0
- package/lib/node/write-verbs.mjs +215 -0
- package/lib/normalize.mjs +288 -0
- package/lib/ops.mjs +2063 -0
- package/lib/parse.mjs +997 -0
- package/lib/raster.mjs +186 -0
- package/lib/readers/pen.mjs +1684 -0
- package/lib/receipt.mjs +203 -0
- package/lib/render.mjs +503 -0
- package/lib/serialize.mjs +441 -0
- package/lib/spans.mjs +177 -0
- package/lib/svg-path.mjs +483 -0
- package/lib/table.mjs +1047 -0
- package/lib/tokens.mjs +295 -0
- package/lib/validate.mjs +929 -0
- package/package.json +68 -6
- package/references/cookbook.md +156 -0
- package/references/images.md +171 -0
- package/references/product-flow.md +408 -0
- package/references/schema.md +90 -0
- package/references/styling.md +181 -0
- package/scripts/install.mjs +95 -0
package/dsh/client.js
ADDED
|
@@ -0,0 +1,864 @@
|
|
|
1
|
+
/* eslint-disable no-undef */
|
|
2
|
+
/**
|
|
3
|
+
* Browser half of justSkill Design for DeepSeek Harness.
|
|
4
|
+
*
|
|
5
|
+
* Two surfaces, one flow:
|
|
6
|
+
*
|
|
7
|
+
* `sidebar.panellist` + `main` JustSkill Design 工作台 — every workspace's
|
|
8
|
+
* design files, grouped by workspace.
|
|
9
|
+
* `sidebarRightTabs.register` Canvas tab — the file's live canvas in the
|
|
10
|
+
* session's right sidebar.
|
|
11
|
+
*
|
|
12
|
+
* Clicking a design file creates a session in that file's workspace, opens it,
|
|
13
|
+
* opens the Canvas tab and sends the /justskill-design context message. The
|
|
14
|
+
* canvas itself is the existing loopback service, iframed with `embed=1`.
|
|
15
|
+
*/
|
|
16
|
+
window.__ModuleLoader__.load({
|
|
17
|
+
id: 'justskill-design',
|
|
18
|
+
factory(require) {
|
|
19
|
+
const React = require('react');
|
|
20
|
+
const h = React.createElement;
|
|
21
|
+
const { useCallback, useEffect, useMemo, useRef, useState } = React;
|
|
22
|
+
|
|
23
|
+
const ROUTE = 'justskill-design';
|
|
24
|
+
const PANEL_ID = 'justskill-design-workbench';
|
|
25
|
+
const TAB_ID = 'justskill-design/canvas';
|
|
26
|
+
const TAB_KIND = 'justskill-canvas';
|
|
27
|
+
const SKILL = 'justskill-design';
|
|
28
|
+
|
|
29
|
+
let runtime = null;
|
|
30
|
+
|
|
31
|
+
/* ------------------------------------------------------------------ *
|
|
32
|
+
* Small shared helpers
|
|
33
|
+
* ------------------------------------------------------------------ */
|
|
34
|
+
|
|
35
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
36
|
+
|
|
37
|
+
function errorText(error) {
|
|
38
|
+
if (error instanceof Error) return error.message;
|
|
39
|
+
const message = error && typeof error === 'object' ? error.message : undefined;
|
|
40
|
+
return typeof message === 'string' && message !== '' ? message : String(error);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function basename(file) {
|
|
44
|
+
const parts = String(file).split(/[\\/]/);
|
|
45
|
+
return parts[parts.length - 1] || String(file);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function post(path, payload) {
|
|
49
|
+
const response = await fetch(new URL(`api/${ROUTE}/${path}`, document.baseURI), {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'content-type': 'application/json' },
|
|
52
|
+
body: JSON.stringify(payload),
|
|
53
|
+
cache: 'no-store',
|
|
54
|
+
});
|
|
55
|
+
let body = null;
|
|
56
|
+
try {
|
|
57
|
+
body = await response.json();
|
|
58
|
+
} catch {
|
|
59
|
+
body = null;
|
|
60
|
+
}
|
|
61
|
+
if (!response.ok || body === null || body.ok !== true) {
|
|
62
|
+
const code = body && body.error && body.error.code ? body.error.code : `http-${response.status}`;
|
|
63
|
+
const message = body && body.error && body.error.message ? body.error.message : `request failed (${response.status})`;
|
|
64
|
+
const error = new Error(message);
|
|
65
|
+
error.code = code;
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
return body.value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatSize(bytes) {
|
|
72
|
+
if (typeof bytes !== 'number' || !Number.isFinite(bytes)) return '';
|
|
73
|
+
if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
74
|
+
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
75
|
+
return `${bytes} B`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatTime(ms) {
|
|
79
|
+
if (typeof ms !== 'number' || !Number.isFinite(ms)) return '';
|
|
80
|
+
const delta = Date.now() - ms;
|
|
81
|
+
if (delta < 60_000) return '刚刚';
|
|
82
|
+
if (delta < 3_600_000) return `${Math.floor(delta / 60_000)} 分钟前`;
|
|
83
|
+
if (delta < 86_400_000) return `${Math.floor(delta / 3_600_000)} 小时前`;
|
|
84
|
+
if (delta < 7 * 86_400_000) return `${Math.floor(delta / 86_400_000)} 天前`;
|
|
85
|
+
const date = new Date(ms);
|
|
86
|
+
return `${date.getMonth() + 1}月${date.getDate()}日`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readWorkspaceItems() {
|
|
90
|
+
const workspaces = runtime === null ? undefined : runtime.get('workspaces');
|
|
91
|
+
if (workspaces === undefined || workspaces.list === undefined) return [];
|
|
92
|
+
const snapshot = workspaces.list.getSnapshot();
|
|
93
|
+
const items = snapshot && Array.isArray(snapshot.items) ? snapshot.items : [];
|
|
94
|
+
return items.map((item) => ({
|
|
95
|
+
id: item.workspaceId,
|
|
96
|
+
path: item.path,
|
|
97
|
+
title: item.title || basename(item.path),
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function workspaceSignature(items) {
|
|
102
|
+
return items.map((item) => `${item.id}\u0000${item.path}`).join('\u0001');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function useWorkspaces() {
|
|
106
|
+
const [items, setItems] = useState(() => readWorkspaceItems());
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const workspaces = runtime === null ? undefined : runtime.get('workspaces');
|
|
109
|
+
if (workspaces === undefined || workspaces.list === undefined) return undefined;
|
|
110
|
+
const read = () => setItems(readWorkspaceItems());
|
|
111
|
+
read();
|
|
112
|
+
return workspaces.list.subscribe(read);
|
|
113
|
+
}, []);
|
|
114
|
+
return items;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readTabInfo(props) {
|
|
118
|
+
try {
|
|
119
|
+
return props && typeof props.useTabInfo === 'function' ? props.useTabInfo() : null;
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// The Canvas tab keeps the file it picked in module state, keyed by the tab
|
|
126
|
+
// id, so the body and its tab title agree without rewriting the tab's
|
|
127
|
+
// navigation params. A workbench-opened tab still gets its file from
|
|
128
|
+
// `navigation.params`; a tab opened from the right-sidebar guide starts with
|
|
129
|
+
// the workspace picker and switches to the picked file here.
|
|
130
|
+
const tabFiles = new Map();
|
|
131
|
+
const tabFileListeners = new Set();
|
|
132
|
+
|
|
133
|
+
function setTabFile(tabId, file) {
|
|
134
|
+
if (typeof tabId !== 'string' || tabId === '') return;
|
|
135
|
+
if (typeof file === 'string' && file !== '') tabFiles.set(tabId, file);
|
|
136
|
+
else tabFiles.delete(tabId);
|
|
137
|
+
for (const listener of tabFileListeners) listener();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function useTabFile(tabId) {
|
|
141
|
+
const [, force] = useState(0);
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (typeof tabId !== 'string' || tabId === '') return undefined;
|
|
144
|
+
const listener = () => force((value) => value + 1);
|
|
145
|
+
tabFileListeners.add(listener);
|
|
146
|
+
return () => tabFileListeners.delete(listener);
|
|
147
|
+
}, [tabId]);
|
|
148
|
+
return typeof tabId === 'string' && tabId !== '' ? tabFiles.get(tabId) : undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function readSessionWorkspace(sessionId) {
|
|
152
|
+
const sessions = runtime === null ? undefined : runtime.get('sessions');
|
|
153
|
+
const workspaces = runtime === null ? undefined : runtime.get('workspaces');
|
|
154
|
+
const session = sessionId && sessions && sessions.list
|
|
155
|
+
? sessions.list.getSnapshot()?.byId?.[sessionId]
|
|
156
|
+
: undefined;
|
|
157
|
+
const cwd = session && typeof session.cwd === 'string' ? session.cwd : undefined;
|
|
158
|
+
const items = workspaces && workspaces.list
|
|
159
|
+
? (workspaces.list.getSnapshot()?.items ?? [])
|
|
160
|
+
: [];
|
|
161
|
+
const matched = items.find((item) => Array.isArray(item.sessionIds) && item.sessionIds.includes(sessionId))
|
|
162
|
+
?? items.find((item) => cwd !== undefined && item.path === cwd);
|
|
163
|
+
if (matched !== undefined) {
|
|
164
|
+
return {
|
|
165
|
+
id: matched.workspaceId,
|
|
166
|
+
path: matched.path,
|
|
167
|
+
title: matched.title || basename(matched.path),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (typeof cwd === 'string' && cwd !== '') {
|
|
171
|
+
return { id: cwd, path: cwd, title: basename(cwd) };
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function openWorkbench() {
|
|
177
|
+
try {
|
|
178
|
+
runtime?.get('layout')?.selectPanel?.(PANEL_ID);
|
|
179
|
+
return true;
|
|
180
|
+
} catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* ------------------------------------------------------------------ *
|
|
186
|
+
* Icons
|
|
187
|
+
* ------------------------------------------------------------------ */
|
|
188
|
+
|
|
189
|
+
// Icons are written as `svg(child, child, size)`. The size is optional and
|
|
190
|
+
// last, so the helper pulls it off the argument list instead of mistaking the
|
|
191
|
+
// first child for it (which rendered `width="[object Object]"` and showed a
|
|
192
|
+
// giant clipped glyph in the right-sidebar tab title).
|
|
193
|
+
const svg = (...args) => {
|
|
194
|
+
const size = typeof args[args.length - 1] === 'number' ? args.pop() : 16;
|
|
195
|
+
return h('svg', {
|
|
196
|
+
width: size,
|
|
197
|
+
height: size,
|
|
198
|
+
viewBox: '0 0 20 20',
|
|
199
|
+
fill: 'none',
|
|
200
|
+
stroke: 'currentColor',
|
|
201
|
+
strokeWidth: 1.7,
|
|
202
|
+
strokeLinecap: 'round',
|
|
203
|
+
strokeLinejoin: 'round',
|
|
204
|
+
'aria-hidden': 'true',
|
|
205
|
+
}, ...args);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const icon = {
|
|
209
|
+
logo: (size = 22) => h('svg', { width: size, height: size, viewBox: '0 0 20 20', fill: 'none', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': 'true' },
|
|
210
|
+
h('rect', { x: 3, y: 4.2, width: 14, height: 10.6, rx: 2.2 }),
|
|
211
|
+
h('path', { d: 'M3 8h14M6.5 4.2v10.6' }),
|
|
212
|
+
h('path', { d: 'M11.4 10.4l3.2 1.3-1.4.8-.8 1.4z', fill: 'currentColor', stroke: 'none' })),
|
|
213
|
+
search: (size = 15) => svg(h('circle', { cx: 9, cy: 9, r: 4.6 }), h('path', { d: 'M12.6 12.6l3.4 3.4' }), size),
|
|
214
|
+
refresh: (size = 15) => svg(h('path', { d: 'M15.6 8.2A6 6 0 1 0 16 11.4' }), h('path', { d: 'M16 4.6v3.9h-3.9' }), size),
|
|
215
|
+
chevron: (size = 13) => svg(h('path', { d: 'M6 8l4 4 4-4' }), size),
|
|
216
|
+
folder: (size = 13) => svg(h('path', { d: 'M3.6 5.6h4l1.6 2h6v7H3.6z' }), size),
|
|
217
|
+
canvas: (size = 15) => svg(h('rect', { x: 3.2, y: 4.4, width: 13.6, height: 10.4, rx: 2.2 }), h('path', { d: 'M3.2 8.1h13.6M6.6 4.4v10.4' }), size),
|
|
218
|
+
arrow: (size = 15) => svg(h('path', { d: 'M5 10h10M11 6l4 4-4 4' }), size),
|
|
219
|
+
alert: (size = 15) => svg(h('circle', { cx: 10, cy: 10, r: 6.4 }), h('path', { d: 'M10 8.6v4.4M10 6v.1' }), size),
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
function renderFileCard(file, workspace, busy, activate) {
|
|
223
|
+
const recent = typeof file.mtime === 'number' && Date.now() - file.mtime < 5 * 60_000;
|
|
224
|
+
return h('button', {
|
|
225
|
+
type: 'button',
|
|
226
|
+
key: file.path,
|
|
227
|
+
className: 'jsk-card',
|
|
228
|
+
disabled: busy !== null,
|
|
229
|
+
title: `${file.path}\n在 ${workspace.title || basename(workspace.path)} 新建会话并打开画布`,
|
|
230
|
+
onClick: () => activate(file),
|
|
231
|
+
},
|
|
232
|
+
h('span', { className: 'jsk-card-thumb' },
|
|
233
|
+
h('span', { className: 'jsk-card-screens' },
|
|
234
|
+
h('span', { className: 'jsk-card-mini jsk-mini-a' }),
|
|
235
|
+
h('span', { className: 'jsk-card-mini' }),
|
|
236
|
+
h('span', { className: 'jsk-card-mini jsk-mini-warm' })),
|
|
237
|
+
recent ? h('span', { className: 'jsk-card-tag' }, '刚刚编辑') : null,
|
|
238
|
+
h('span', { className: 'jsk-card-open' },
|
|
239
|
+
busy === file.path ? h('span', { className: 'jsk-spinner jsk-spinner-dark' }) : icon.arrow(14))),
|
|
240
|
+
h('span', { className: 'jsk-card-main' },
|
|
241
|
+
h('span', { className: 'jsk-card-name' }, file.name),
|
|
242
|
+
h('span', { className: 'jsk-card-meta' },
|
|
243
|
+
typeof file.pages === 'number' ? h('span', { className: 'jsk-pill' }, `${file.pages} 页`) : null,
|
|
244
|
+
h('span', null, formatSize(file.size)),
|
|
245
|
+
h('span', null, formatTime(file.mtime))),
|
|
246
|
+
file.title ? h('span', { className: 'jsk-card-title' }, file.title) : null));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function WorkbenchPage() {
|
|
250
|
+
const workspaces = useWorkspaces();
|
|
251
|
+
const workspaceKey = useMemo(() => workspaceSignature(workspaces), [workspaces]);
|
|
252
|
+
const [state, setState] = useState({ phase: 'loading', workspaces: [], scannedAt: 0, incomplete: false });
|
|
253
|
+
const [error, setError] = useState(null);
|
|
254
|
+
const [notice, setNotice] = useState(null);
|
|
255
|
+
const [query, setQuery] = useState('');
|
|
256
|
+
const [collapsed, setCollapsed] = useState(() => new Set());
|
|
257
|
+
const [busy, setBusy] = useState(null);
|
|
258
|
+
const request = useRef(0);
|
|
259
|
+
|
|
260
|
+
const load = useCallback(async (refresh) => {
|
|
261
|
+
if (workspaces.length === 0) {
|
|
262
|
+
setState({ phase: 'empty', workspaces: [], scannedAt: 0, incomplete: false });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const ticket = ++request.current;
|
|
266
|
+
setState((current) => ({ ...current, phase: current.workspaces.length === 0 ? 'loading' : 'refreshing' }));
|
|
267
|
+
try {
|
|
268
|
+
const value = await post('scan', {
|
|
269
|
+
workspaces: workspaces.map((item) => ({ id: item.id, path: item.path, title: item.title })),
|
|
270
|
+
refresh: refresh === true,
|
|
271
|
+
});
|
|
272
|
+
if (ticket !== request.current) return;
|
|
273
|
+
setError(null);
|
|
274
|
+
setState({
|
|
275
|
+
phase: 'ready',
|
|
276
|
+
workspaces: Array.isArray(value.workspaces) ? value.workspaces : [],
|
|
277
|
+
scannedAt: value.scannedAt || Date.now(),
|
|
278
|
+
incomplete: value.incomplete === true,
|
|
279
|
+
});
|
|
280
|
+
} catch (cause) {
|
|
281
|
+
if (ticket !== request.current) return;
|
|
282
|
+
setError(errorText(cause));
|
|
283
|
+
setState((current) => ({ ...current, phase: current.workspaces.length === 0 ? 'error' : 'ready' }));
|
|
284
|
+
}
|
|
285
|
+
}, [workspaces]);
|
|
286
|
+
|
|
287
|
+
useEffect(() => {
|
|
288
|
+
load(false);
|
|
289
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
290
|
+
}, [workspaceKey]);
|
|
291
|
+
|
|
292
|
+
const groups = useMemo(() => {
|
|
293
|
+
const needle = query.trim().toLowerCase();
|
|
294
|
+
return state.workspaces
|
|
295
|
+
.map((workspace) => {
|
|
296
|
+
const workspaceMatches = needle === '' || `${workspace.title} ${workspace.path}`.toLowerCase().includes(needle);
|
|
297
|
+
const files = (Array.isArray(workspace.files) ? workspace.files : [])
|
|
298
|
+
.filter((file) => workspaceMatches || `${file.name} ${file.title || ''}`.toLowerCase().includes(needle))
|
|
299
|
+
.map((file) => ({ ...file, workspaceId: workspace.id, workspaceTitle: workspace.title }));
|
|
300
|
+
return { ...workspace, files };
|
|
301
|
+
})
|
|
302
|
+
.filter((workspace) => workspace.files.length > 0);
|
|
303
|
+
}, [state.workspaces, query]);
|
|
304
|
+
|
|
305
|
+
const totals = useMemo(() => {
|
|
306
|
+
let files = 0;
|
|
307
|
+
let newest = 0;
|
|
308
|
+
for (const workspace of state.workspaces) {
|
|
309
|
+
for (const file of Array.isArray(workspace.files) ? workspace.files : []) {
|
|
310
|
+
files += 1;
|
|
311
|
+
if (typeof file.mtime === 'number' && file.mtime > newest) newest = file.mtime;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return { workspaces: state.workspaces.length, files, newest };
|
|
315
|
+
}, [state.workspaces]);
|
|
316
|
+
|
|
317
|
+
const toggleGroup = (id) => {
|
|
318
|
+
setCollapsed((current) => {
|
|
319
|
+
const next = new Set(current);
|
|
320
|
+
if (next.has(id)) next.delete(id);
|
|
321
|
+
else next.add(id);
|
|
322
|
+
return next;
|
|
323
|
+
});
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const activate = useCallback(async (file) => {
|
|
327
|
+
if (busy !== null) return;
|
|
328
|
+
setBusy(file.path);
|
|
329
|
+
setError(null);
|
|
330
|
+
setNotice(null);
|
|
331
|
+
try {
|
|
332
|
+
const sessions = runtime === null ? undefined : runtime.get('sessions');
|
|
333
|
+
const uiWorkspace = runtime === null ? undefined : runtime.get('uiWorkspace');
|
|
334
|
+
const sidebarRight = runtime === null ? undefined : runtime.get('sidebarRight');
|
|
335
|
+
if (sessions === undefined || typeof sessions.create !== 'function') {
|
|
336
|
+
throw new Error('DSH sessions 服务不可用,无法新建会话');
|
|
337
|
+
}
|
|
338
|
+
if (sidebarRight === undefined || typeof sidebarRight.openTab !== 'function') {
|
|
339
|
+
throw new Error('DSH 右侧栏服务不可用,无法打开画布');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const sessionId = await sessions.create({ workspaceId: file.workspaceId });
|
|
343
|
+
try {
|
|
344
|
+
uiWorkspace?.openSession?.(sessionId);
|
|
345
|
+
} catch {
|
|
346
|
+
/* navigation is best effort; the tab is the important half */
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const prompt = [
|
|
350
|
+
`/${SKILL}`,
|
|
351
|
+
'',
|
|
352
|
+
`Use justSkill tools. Current file ${file.uri}`,
|
|
353
|
+
'',
|
|
354
|
+
'The canvas is already open in the right-sidebar Canvas tab.',
|
|
355
|
+
'Do NOT run `node design.mjs serve` and do NOT open a browser for the canvas.',
|
|
356
|
+
'Wait for my adjustment request.',
|
|
357
|
+
].join('\n');
|
|
358
|
+
|
|
359
|
+
const prompting = typeof sessions.using === 'function'
|
|
360
|
+
? sessions.using(sessionId, { source: SKILL }, async (reference) => {
|
|
361
|
+
const session = reference && reference.binding ? reference.binding.session : undefined;
|
|
362
|
+
if (session === undefined || typeof session.prompt !== 'function') {
|
|
363
|
+
throw new Error('无法向新会话发送初始消息');
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
await session.rename?.(`${basename(file.name).replace(/\.html?$/i, '')} · 设计调整`);
|
|
367
|
+
} catch {
|
|
368
|
+
/* a title is a convenience, never a blocker */
|
|
369
|
+
}
|
|
370
|
+
await session.prompt([{ type: 'text', text: prompt }], 'queue');
|
|
371
|
+
})
|
|
372
|
+
: Promise.reject(new Error('DSH sessions.using 不可用'));
|
|
373
|
+
|
|
374
|
+
const mounted = await waitForMounted(sidebarRight, sessionId, 5000);
|
|
375
|
+
if (!mounted) throw new Error('新会话没有切换成功,画布暂未打开');
|
|
376
|
+
await openCanvasTab(sidebarRight, file.path);
|
|
377
|
+
setNotice({ sessionId, file: file.path });
|
|
378
|
+
prompting.catch((cause) => {
|
|
379
|
+
console.error('[justskill-design] initial prompt failed:', cause);
|
|
380
|
+
});
|
|
381
|
+
} catch (cause) {
|
|
382
|
+
setError(errorText(cause));
|
|
383
|
+
} finally {
|
|
384
|
+
setBusy(null);
|
|
385
|
+
}
|
|
386
|
+
}, [busy]);
|
|
387
|
+
|
|
388
|
+
const totalLabel = totals.files === 1 ? '1 个设计文件' : `${totals.files} 个设计文件`;
|
|
389
|
+
const workspaceLabel = totals.workspaces === 1 ? '1 个工作区' : `${totals.workspaces} 个工作区`;
|
|
390
|
+
|
|
391
|
+
return h('div', { className: 'jsk-page' },
|
|
392
|
+
h('header', { className: 'jsk-head' },
|
|
393
|
+
h('div', { className: 'jsk-brand' },
|
|
394
|
+
h('span', { className: 'jsk-brand-icon' }, icon.logo(24)),
|
|
395
|
+
h('div', null,
|
|
396
|
+
h('h1', null, 'JustSkill Design 工作台'),
|
|
397
|
+
h('p', null, '扫描所有工作区里的设计文件,点开就在该工作区新建会话,并在右侧栏打开画布。'))),
|
|
398
|
+
h('div', { className: 'jsk-actions' },
|
|
399
|
+
h('label', { className: 'jsk-search' },
|
|
400
|
+
icon.search(),
|
|
401
|
+
h('input', {
|
|
402
|
+
value: query,
|
|
403
|
+
placeholder: '搜索设计文件、工作区…',
|
|
404
|
+
spellCheck: false,
|
|
405
|
+
onChange: (event) => setQuery(event.target.value),
|
|
406
|
+
})),
|
|
407
|
+
h('button', {
|
|
408
|
+
type: 'button',
|
|
409
|
+
className: 'jsk-button jsk-button-primary',
|
|
410
|
+
disabled: state.phase === 'loading' || state.phase === 'refreshing',
|
|
411
|
+
onClick: () => load(true),
|
|
412
|
+
}, icon.refresh(), state.phase === 'refreshing' ? '扫描中…' : '刷新'))),
|
|
413
|
+
error !== null ? h('div', { className: 'jsk-error' }, icon.alert(), h('span', null, error)) : null,
|
|
414
|
+
notice !== null ? h('div', { className: 'jsk-notice' },
|
|
415
|
+
h('span', null, '已新建会话并打开画布:', h('b', null, basename(notice.file))),
|
|
416
|
+
h('button', {
|
|
417
|
+
type: 'button',
|
|
418
|
+
className: 'jsk-link',
|
|
419
|
+
onClick: () => {
|
|
420
|
+
try {
|
|
421
|
+
runtime.get('sessions')?.open?.(notice.sessionId);
|
|
422
|
+
} catch {
|
|
423
|
+
/* ignored */
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
}, '查看会话')) : null,
|
|
427
|
+
h('div', { className: 'jsk-stats' },
|
|
428
|
+
h('span', null, h('b', null, totals.workspaces), ' 个工作区'),
|
|
429
|
+
h('i', { className: 'jsk-sep' }),
|
|
430
|
+
h('span', null, h('b', null, totals.files), ' 个设计文件'),
|
|
431
|
+
h('i', { className: 'jsk-sep' }),
|
|
432
|
+
h('span', null, '最近更新 ', h('b', null, totals.newest === 0 ? '—' : formatTime(totals.newest))),
|
|
433
|
+
h('span', { className: 'jsk-stats-right' }, '上次扫描 ', formatTime(state.scannedAt || Date.now())),
|
|
434
|
+
state.incomplete ? h('span', { className: 'jsk-badge-warn' }, '部分工作区超时') : null),
|
|
435
|
+
h('div', { className: 'jsk-list' },
|
|
436
|
+
state.phase === 'loading' ? h('div', { className: 'jsk-empty' }, h('span', { className: 'jsk-spinner' }), '正在扫描工作区…') : null,
|
|
437
|
+
state.phase === 'empty' ? h('div', { className: 'jsk-empty' },
|
|
438
|
+
h('strong', null, '还没有工作区'),
|
|
439
|
+
h('span', null, '在 DSH 左侧栏添加一个工作区后,这里会列出其中的设计文件。')) : null,
|
|
440
|
+
state.phase === 'error' ? h('div', { className: 'jsk-empty' }, h('strong', null, '扫描失败'), h('span', null, error || '请稍后重试')) : null,
|
|
441
|
+
groups.length === 0 && state.phase === 'ready' ? h('div', { className: 'jsk-empty' },
|
|
442
|
+
h('strong', null, query.trim() === '' ? '没有找到设计文件' : '没有匹配的设计文件'),
|
|
443
|
+
h('span', null, query.trim() === '' ? '设计文件需要带有 <meta name="design-schema"> 标记;让 agent 用 justSkill 新建一个 design.html 试试。' : '换一个关键词,或清空搜索。')) : null,
|
|
444
|
+
groups.map((workspace) => {
|
|
445
|
+
const isCollapsed = collapsed.has(workspace.id);
|
|
446
|
+
return h('section', { className: 'jsk-group', key: workspace.id },
|
|
447
|
+
h('button', { type: 'button', className: 'jsk-group-head', onClick: () => toggleGroup(workspace.id) },
|
|
448
|
+
h('span', { className: `jsk-chevron ${isCollapsed ? 'jsk-chevron-closed' : ''}` }, icon.chevron()),
|
|
449
|
+
h('span', { className: 'jsk-ws-badge' }, (workspace.title || '?').slice(0, 1)),
|
|
450
|
+
h('span', { className: 'jsk-ws-name' }, workspace.title || basename(workspace.path)),
|
|
451
|
+
h('span', { className: 'jsk-ws-path' }, workspace.path),
|
|
452
|
+
h('span', { className: 'jsk-count' }, `${workspace.files.length} 个文件`)),
|
|
453
|
+
isCollapsed ? null : h('div', { className: 'jsk-grid' },
|
|
454
|
+
workspace.files.length === 0
|
|
455
|
+
? h('div', { className: 'jsk-row-empty' }, '这个工作区里还没有设计文件')
|
|
456
|
+
: workspace.files.map((file) => renderFileCard(file, workspace, busy, activate))));
|
|
457
|
+
}),
|
|
458
|
+
),
|
|
459
|
+
h('footer', { className: 'jsk-foot' },
|
|
460
|
+
h('span', { className: 'jsk-step' }, h('i', null, '1'), '点设计文件'),
|
|
461
|
+
h('span', { className: 'jsk-arrow' }, '→'),
|
|
462
|
+
h('span', { className: 'jsk-step' }, h('i', null, '2'), '该工作区自动新建会话'),
|
|
463
|
+
h('span', { className: 'jsk-arrow' }, '→'),
|
|
464
|
+
h('span', { className: 'jsk-step' }, h('i', null, '3'), '右侧栏打开画布,边聊边改')));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function waitForMounted(sidebarRight, sessionId, timeoutMs) {
|
|
468
|
+
// `sidebarRight.mounted` is the "session on screen" snapshot store; the
|
|
469
|
+
// right sidebar's commands target exactly that session. Without this
|
|
470
|
+
// wait, openTab would land on the previous session (or throw while the
|
|
471
|
+
// new session's store is being minted).
|
|
472
|
+
const deadline = Date.now() + timeoutMs;
|
|
473
|
+
while (Date.now() < deadline) {
|
|
474
|
+
try {
|
|
475
|
+
if (sidebarRight.mounted?.getSnapshot?.() === sessionId) return true;
|
|
476
|
+
} catch {
|
|
477
|
+
/* the store may not be ready yet */
|
|
478
|
+
}
|
|
479
|
+
await sleep(40);
|
|
480
|
+
}
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function openCanvasTab(sidebarRight, file) {
|
|
485
|
+
let last = null;
|
|
486
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
487
|
+
try {
|
|
488
|
+
sidebarRight.openTab(TAB_KIND, { params: { file } });
|
|
489
|
+
return;
|
|
490
|
+
} catch (cause) {
|
|
491
|
+
last = cause;
|
|
492
|
+
await sleep(40);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
throw new Error(`右侧栏画布打开失败${last === null ? '' : `:${errorText(last)}`}`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/* ------------------------------------------------------------------ *
|
|
499
|
+
* Canvas right-sidebar tab
|
|
500
|
+
* ------------------------------------------------------------------ */
|
|
501
|
+
|
|
502
|
+
function CanvasPicker({ sessionId, onPick }) {
|
|
503
|
+
const [state, setState] = useState({ phase: 'loading' });
|
|
504
|
+
const [ticket, setTicket] = useState(0);
|
|
505
|
+
const workspace = useMemo(() => readSessionWorkspace(sessionId), [sessionId]);
|
|
506
|
+
|
|
507
|
+
useEffect(() => {
|
|
508
|
+
if (workspace === null) {
|
|
509
|
+
setState({ phase: 'no-workspace' });
|
|
510
|
+
return undefined;
|
|
511
|
+
}
|
|
512
|
+
let cancelled = false;
|
|
513
|
+
setState({ phase: 'loading' });
|
|
514
|
+
post('scan', {
|
|
515
|
+
workspaces: [{ id: workspace.id, path: workspace.path, title: workspace.title }],
|
|
516
|
+
}).then(
|
|
517
|
+
(value) => {
|
|
518
|
+
if (cancelled) return;
|
|
519
|
+
const entry = Array.isArray(value.workspaces) ? value.workspaces[0] : undefined;
|
|
520
|
+
setState({ phase: 'ready', files: entry && Array.isArray(entry.files) ? entry.files : [], incomplete: entry?.incomplete === true });
|
|
521
|
+
},
|
|
522
|
+
(cause) => {
|
|
523
|
+
if (!cancelled) setState({ phase: 'error', message: errorText(cause) });
|
|
524
|
+
},
|
|
525
|
+
);
|
|
526
|
+
return () => {
|
|
527
|
+
cancelled = true;
|
|
528
|
+
};
|
|
529
|
+
}, [workspace?.id, workspace?.path, ticket]);
|
|
530
|
+
|
|
531
|
+
return h('div', { className: 'jsk-picker' },
|
|
532
|
+
h('div', { className: 'jsk-picker-head' },
|
|
533
|
+
h('div', { className: 'jsk-picker-heading' },
|
|
534
|
+
h('div', { className: 'jsk-picker-title' }, workspace === null ? '当前工作区' : workspace.title),
|
|
535
|
+
h('div', { className: 'jsk-picker-sub' }, workspace === null ? '无法确定当前会话的工作区' : workspace.path)),
|
|
536
|
+
h('button', {
|
|
537
|
+
type: 'button',
|
|
538
|
+
className: 'jsk-picker-workbench',
|
|
539
|
+
title: '打开 JustSkill Design 工作台',
|
|
540
|
+
onClick: () => openWorkbench(),
|
|
541
|
+
}, icon.logo(14), '工作台')),
|
|
542
|
+
state.phase === 'loading' ? h('div', { className: 'jsk-picker-state' }, h('span', { className: 'jsk-spinner' }), '正在读取当前工作区的设计文件…') : null,
|
|
543
|
+
state.phase === 'error' ? h('div', { className: 'jsk-picker-state' },
|
|
544
|
+
h('strong', null, '读取失败'),
|
|
545
|
+
h('span', null, state.message),
|
|
546
|
+
h('button', { type: 'button', className: 'jsk-button', onClick: () => setTicket((value) => value + 1) }, '重试')) : null,
|
|
547
|
+
state.phase === 'no-workspace' ? h('div', { className: 'jsk-picker-state' },
|
|
548
|
+
h('strong', null, '找不到当前工作区'),
|
|
549
|
+
h('span', null, '这个标签页里没有会话工作区信息;请从 JustSkill Design 工作台点一个设计文件。')) : null,
|
|
550
|
+
state.phase === 'ready' && state.files.length === 0 ? h('div', { className: 'jsk-picker-state' },
|
|
551
|
+
h('strong', null, '这个工作区还没有设计文件'),
|
|
552
|
+
h('span', null, '用 justSkill 新建一个 design.html,再回到这里选择它。')) : null,
|
|
553
|
+
state.phase === 'ready' && state.files.length > 0 ? h('div', { className: 'jsk-picker-list' },
|
|
554
|
+
state.incomplete ? h('div', { className: 'jsk-picker-note' }, '部分目录扫描超时,列表可能不完整') : null,
|
|
555
|
+
state.files.map((file) => h('button', {
|
|
556
|
+
type: 'button',
|
|
557
|
+
key: file.path,
|
|
558
|
+
className: 'jsk-pick-card',
|
|
559
|
+
title: file.path,
|
|
560
|
+
onClick: () => onPick(file.path),
|
|
561
|
+
},
|
|
562
|
+
h('span', { className: 'jsk-pick-thumb' },
|
|
563
|
+
h('span', { className: 'jsk-card-screens' },
|
|
564
|
+
h('span', { className: 'jsk-card-mini jsk-mini-a' }),
|
|
565
|
+
h('span', { className: 'jsk-card-mini' }),
|
|
566
|
+
h('span', { className: 'jsk-card-mini jsk-mini-warm' })),
|
|
567
|
+
h('span', { className: 'jsk-pick-arrow' }, icon.arrow(12))),
|
|
568
|
+
h('span', { className: 'jsk-pick-name' }, file.name),
|
|
569
|
+
h('span', { className: 'jsk-pick-meta' },
|
|
570
|
+
typeof file.pages === 'number' ? h('span', { className: 'jsk-pill' }, `${file.pages} 页`) : null,
|
|
571
|
+
h('span', null, formatSize(file.size))),
|
|
572
|
+
h('span', { className: 'jsk-pick-time' }, formatTime(file.mtime))))) : null);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function CanvasBody(props) {
|
|
576
|
+
const info = readTabInfo(props);
|
|
577
|
+
const tabId = info && info.tab ? info.tab.id : undefined;
|
|
578
|
+
const paramsFile = info && info.tab && info.tab.navigation && info.tab.navigation.params
|
|
579
|
+
? info.tab.navigation.params.file
|
|
580
|
+
: undefined;
|
|
581
|
+
const storedFile = useTabFile(tabId);
|
|
582
|
+
const file = typeof storedFile === 'string' && storedFile !== '' ? storedFile : paramsFile;
|
|
583
|
+
const [state, setState] = useState({ phase: file ? 'loading' : 'pick' });
|
|
584
|
+
const [ticket, setTicket] = useState(0);
|
|
585
|
+
|
|
586
|
+
useEffect(() => {
|
|
587
|
+
if (typeof file !== 'string' || file === '') {
|
|
588
|
+
setState({ phase: 'pick' });
|
|
589
|
+
return undefined;
|
|
590
|
+
}
|
|
591
|
+
let cancelled = false;
|
|
592
|
+
setState({ phase: 'loading' });
|
|
593
|
+
post('canvas', { file }).then(
|
|
594
|
+
(value) => {
|
|
595
|
+
if (cancelled) return;
|
|
596
|
+
const url = String(value.url);
|
|
597
|
+
setState({ phase: 'ready', url: `${url}${url.includes('?') ? '&' : '?'}embed=1` });
|
|
598
|
+
},
|
|
599
|
+
(cause) => {
|
|
600
|
+
if (!cancelled) setState({ phase: 'error', message: errorText(cause) });
|
|
601
|
+
},
|
|
602
|
+
);
|
|
603
|
+
return () => {
|
|
604
|
+
cancelled = true;
|
|
605
|
+
};
|
|
606
|
+
}, [file, ticket]);
|
|
607
|
+
|
|
608
|
+
useEffect(() => () => {
|
|
609
|
+
if (typeof tabId === 'string' && tabId !== '') tabFiles.delete(tabId);
|
|
610
|
+
}, [tabId]);
|
|
611
|
+
|
|
612
|
+
if (typeof file !== 'string' || file === '') {
|
|
613
|
+
return h(CanvasPicker, {
|
|
614
|
+
sessionId: props.sessionId,
|
|
615
|
+
onPick: (picked) => setTabFile(tabId, picked),
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (state.phase === 'ready') {
|
|
620
|
+
const fromPicker = !(typeof paramsFile === 'string' && paramsFile !== '') && storedFile === file;
|
|
621
|
+
return h(React.Fragment, null,
|
|
622
|
+
h('iframe', {
|
|
623
|
+
className: 'jsk-frame',
|
|
624
|
+
src: state.url,
|
|
625
|
+
title: `JustSkill Design canvas — ${basename(file)}`,
|
|
626
|
+
referrerPolicy: 'no-referrer',
|
|
627
|
+
}),
|
|
628
|
+
fromPicker ? h('button', {
|
|
629
|
+
type: 'button',
|
|
630
|
+
className: 'jsk-switch',
|
|
631
|
+
title: '换一个设计文件',
|
|
632
|
+
onClick: () => setTabFile(tabId, undefined),
|
|
633
|
+
}, icon.folder(13), '设计文件') : null);
|
|
634
|
+
}
|
|
635
|
+
return h('div', { className: 'jsk-canvas-state' },
|
|
636
|
+
state.phase === 'loading' ? h(React.Fragment, null, h('span', { className: 'jsk-spinner' }), '正在打开画布…') : null,
|
|
637
|
+
state.phase === 'error' ? h(React.Fragment, null,
|
|
638
|
+
h('strong', null, '画布打开失败'),
|
|
639
|
+
h('span', null, state.message),
|
|
640
|
+
h('button', { type: 'button', className: 'jsk-button', onClick: () => setTicket((value) => value + 1) }, '重试')) : null);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function CanvasTitle(props) {
|
|
644
|
+
const info = readTabInfo(props);
|
|
645
|
+
const tabId = info && info.tab ? info.tab.id : undefined;
|
|
646
|
+
const paramsFile = info && info.tab && info.tab.navigation && info.tab.navigation.params
|
|
647
|
+
? info.tab.navigation.params.file
|
|
648
|
+
: undefined;
|
|
649
|
+
const storedFile = useTabFile(tabId);
|
|
650
|
+
const file = typeof storedFile === 'string' && storedFile !== '' ? storedFile : paramsFile;
|
|
651
|
+
return h('span', { className: 'jsk-tab-title' }, icon.canvas(14), h('span', null, typeof file === 'string' && file !== '' ? basename(file) : 'Canvas'));
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function PanelIcon() {
|
|
655
|
+
return h('span', { className: 'jsk-panel-icon', title: 'JustSkill Design 工作台' }, icon.logo(20));
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/* ------------------------------------------------------------------ *
|
|
659
|
+
* Styles
|
|
660
|
+
* ------------------------------------------------------------------ */
|
|
661
|
+
|
|
662
|
+
const CSS = `
|
|
663
|
+
.jsk-page{height:100%;min-height:0;display:flex;flex-direction:column;padding:24px 32px 0;background:var(--dsw-alias-bg-layer-1,#fbfbfc);color:var(--dsw-alias-label-primary,#1b1f26);font:13px/1.5 -apple-system,BlinkMacSystemFont,"PingFang SC","Hiragino Sans GB","Segoe UI",sans-serif;box-sizing:border-box}
|
|
664
|
+
.jsk-page *{box-sizing:border-box}
|
|
665
|
+
.jsk-head{display:flex;align-items:flex-start;justify-content:space-between;gap:20px}
|
|
666
|
+
.jsk-brand{display:flex;align-items:center;gap:13px;min-width:0}
|
|
667
|
+
.jsk-brand-icon{width:46px;height:46px;border-radius:14px;display:grid;place-items:center;color:#fff;flex:none;background:linear-gradient(135deg,#5a73ff,#93a2ff);box-shadow:0 6px 16px rgba(79,107,254,.22)}
|
|
668
|
+
.jsk-brand h1{margin:0;font-size:20px;line-height:1.25;letter-spacing:-.2px}
|
|
669
|
+
.jsk-brand p{margin:5px 0 0;color:var(--dsw-alias-label-secondary,#68707d);font-size:12.5px;line-height:1.45}
|
|
670
|
+
.jsk-actions{display:flex;align-items:center;gap:9px;padding-top:2px;flex:none}
|
|
671
|
+
.jsk-search{width:280px;height:35px;display:flex;align-items:center;gap:8px;padding:0 11px;border:1px solid var(--dsw-alias-border-l2,#e7e9ee);border-radius:9px;background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-tertiary,#9aa2af)}
|
|
672
|
+
.jsk-search input{width:100%;border:0;outline:0;background:none;color:var(--dsw-alias-label-primary,#1b1f26);font:inherit}
|
|
673
|
+
.jsk-search input::placeholder{color:var(--dsw-alias-label-tertiary,#9aa2af)}
|
|
674
|
+
.jsk-button{height:35px;display:inline-flex;align-items:center;gap:7px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2,#e7e9ee);border-radius:9px;background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#3c4452);font:inherit;font-weight:500;cursor:pointer;white-space:nowrap}
|
|
675
|
+
.jsk-button:hover{background:var(--dsw-alias-interactive-bg-hover,#f3f4f7)}
|
|
676
|
+
.jsk-button:disabled{opacity:.55;cursor:default}
|
|
677
|
+
.jsk-button-primary{background:var(--dsw-alias-brand-primary,#4f6bfe);border-color:var(--dsw-alias-brand-primary,#4f6bfe);color:var(--dsw-alias-label-primary-foreground,#fff);box-shadow:0 2px 6px rgba(79,107,254,.22)}
|
|
678
|
+
.jsk-error,.jsk-notice{margin-top:14px;display:flex;align-items:center;gap:8px;padding:9px 12px;border-radius:9px;font-size:12.5px}
|
|
679
|
+
.jsk-error{background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#c0392b) 7%,transparent);border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary,#c0392b) 24%,transparent);color:var(--dsw-alias-state-error-primary,#a33)}
|
|
680
|
+
.jsk-notice{background:color-mix(in srgb,var(--dsw-alias-brand-primary,#4f6bfe) 7%,transparent);border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary,#4f6bfe) 24%,transparent);color:var(--dsw-alias-label-secondary,#4d5563)}
|
|
681
|
+
.jsk-link{background:none;border:0;padding:0;color:var(--dsw-alias-brand-primary,#4f6bfe);font:inherit;font-weight:600;cursor:pointer}
|
|
682
|
+
.jsk-stats{display:flex;align-items:center;gap:15px;margin:18px 0 13px;color:var(--dsw-alias-label-secondary,#68707d);font-size:12px;white-space:nowrap}
|
|
683
|
+
.jsk-stats b{color:var(--dsw-alias-label-primary,#3c4452);font-weight:600}
|
|
684
|
+
.jsk-sep{width:1px;height:11px;background:var(--dsw-alias-border-l2,#e7e9ee)}
|
|
685
|
+
.jsk-stats-right{margin-left:auto;color:var(--dsw-alias-label-tertiary,#9aa2af)}
|
|
686
|
+
.jsk-badge-warn{color:#a06a12;background:#fdf3e3;border-radius:5px;padding:1px 6px;font-size:11px}
|
|
687
|
+
.jsk-list{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:12px;padding-bottom:8px}
|
|
688
|
+
.jsk-group{flex:none;border:1px solid var(--dsw-alias-border-l2,#e7e9ee);border-radius:13px;background:var(--dsw-alias-bg-layer-1,#fff);overflow:hidden;box-shadow:0 1px 2px rgba(16,24,40,.03)}
|
|
689
|
+
.jsk-group-head{width:100%;height:44px;display:flex;align-items:center;gap:9px;padding:0 14px;background:var(--dsw-alias-bg-layer-2,#fcfcfd);border:0;border-bottom:1px solid var(--dsw-alias-border-l2,#f0f2f5);color:inherit;font:inherit;text-align:left;cursor:pointer}
|
|
690
|
+
.jsk-group-head:hover{background:var(--dsw-alias-interactive-bg-hover,#f6f7f9)}
|
|
691
|
+
.jsk-chevron{display:grid;place-items:center;color:var(--dsw-alias-label-tertiary,#a3aab5);transition:transform .15s ease}
|
|
692
|
+
.jsk-chevron-closed{transform:rotate(-90deg)}
|
|
693
|
+
.jsk-ws-badge{width:22px;height:22px;border-radius:6px;display:grid;place-items:center;background:color-mix(in srgb,var(--dsw-alias-brand-primary,#4f6bfe) 10%,transparent);color:var(--dsw-alias-brand-primary,#4658d9);font-size:10px;font-weight:700;flex:none}
|
|
694
|
+
.jsk-ws-name{font-size:12.5px;font-weight:600;white-space:nowrap}
|
|
695
|
+
.jsk-ws-path{font-size:11.5px;color:var(--dsw-alias-label-tertiary,#9aa2af);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
|
|
696
|
+
.jsk-count{margin-left:auto;color:var(--dsw-alias-label-secondary,#68707d);background:var(--dsw-alias-bg-layer-2,#f4f5f8);border-radius:6px;padding:2px 7px;font-size:11.5px;white-space:nowrap;flex:none}
|
|
697
|
+
.jsk-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(208px,1fr));gap:12px;padding:12px}
|
|
698
|
+
.jsk-row-empty{padding:14px;color:var(--dsw-alias-label-tertiary,#9aa2af);font-size:12px;grid-column:1/-1}
|
|
699
|
+
.jsk-pill{color:var(--dsw-alias-label-secondary,#7a8290);background:var(--dsw-alias-bg-layer-2,#f4f5f8);border-radius:5px;padding:1px 5px;font-size:10.5px}
|
|
700
|
+
.jsk-card{display:flex;flex-direction:column;gap:9px;min-width:0;padding:10px;border:1px solid var(--dsw-alias-border-l2,#eaecf1);border-radius:11px;background:var(--dsw-alias-bg-layer-1,#fff);color:inherit;font:inherit;text-align:left;cursor:pointer;transition:border-color .15s ease, box-shadow .15s ease, transform .15s ease}
|
|
701
|
+
.jsk-card:hover{border-color:color-mix(in srgb,var(--dsw-alias-brand-primary,#4f6bfe) 45%,var(--dsw-alias-border-l2,#eaecf1));box-shadow:0 8px 22px -14px rgba(16,24,40,.28);transform:translateY(-1px)}
|
|
702
|
+
.jsk-card:disabled{opacity:.6;cursor:progress}
|
|
703
|
+
.jsk-card-thumb{position:relative;display:flex;align-items:center;justify-content:center;height:112px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2,#eef0f4);background:radial-gradient(circle at 1px 1px,#e2e6ee 1px,transparent 0) 0 0/14px 14px,linear-gradient(135deg,#f7f8fb,#f1f3f8);overflow:hidden}
|
|
704
|
+
.jsk-card-screens{display:flex;align-items:center;gap:5px;transform:translateY(2px)}
|
|
705
|
+
.jsk-card-mini{position:relative;width:27px;height:82px;border-radius:5px;background:#fff;border:1px solid #e2e6ee;box-shadow:0 4px 10px -6px rgba(16,24,40,.25);overflow:hidden}
|
|
706
|
+
.jsk-card-mini:nth-child(2){height:92px}
|
|
707
|
+
.jsk-card-mini:after{content:"";position:absolute;left:3px;right:3px;top:5px;height:5px;border-radius:2px;background:#d9deea}
|
|
708
|
+
.jsk-card-mini.jsk-mini-a:after{background:#c7d2ff}
|
|
709
|
+
.jsk-card-mini.jsk-mini-warm:after{background:#f3d9b8}
|
|
710
|
+
.jsk-card-tag{position:absolute;left:8px;top:8px;padding:1px 6px;border-radius:5px;background:color-mix(in srgb,var(--dsw-alias-brand-primary,#4f6bfe) 92%,transparent);color:#fff;font-size:10px;font-weight:600}
|
|
711
|
+
.jsk-card-open{position:absolute;right:8px;bottom:8px;width:24px;height:24px;border-radius:7px;display:grid;place-items:center;background:color-mix(in srgb,var(--dsw-alias-bg-layer-1,#fff) 88%,transparent);color:var(--dsw-alias-label-secondary,#68707d);backdrop-filter:blur(4px);border:1px solid var(--dsw-alias-border-l2,#e7e9ee)}
|
|
712
|
+
.jsk-card:hover .jsk-card-open{background:var(--dsw-alias-brand-primary,#4f6bfe);border-color:var(--dsw-alias-brand-primary,#4f6bfe);color:#fff}
|
|
713
|
+
.jsk-card-main{display:flex;flex-direction:column;gap:3px;min-width:0;padding:0 2px 2px}
|
|
714
|
+
.jsk-card-name{font-size:13px;font-weight:600;letter-spacing:-.1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
715
|
+
.jsk-card-meta{display:flex;align-items:center;gap:7px;font-size:11px;color:var(--dsw-alias-label-secondary,#68707d);white-space:nowrap;overflow:hidden}
|
|
716
|
+
.jsk-card-title{font-size:11px;color:var(--dsw-alias-label-tertiary,#9aa2af);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
717
|
+
.jsk-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;color:var(--dsw-alias-label-secondary,#68707d);font-size:12.5px;text-align:center;padding:32px}
|
|
718
|
+
.jsk-empty strong{color:var(--dsw-alias-label-primary,#3c4452);font-size:13.5px;font-weight:600}
|
|
719
|
+
.jsk-foot{flex:none;display:flex;align-items:center;gap:9px;margin:6px 0 16px;padding:10px 14px;border:1px dashed var(--dsw-alias-border-l2,#d9dee8);border-radius:11px;background:var(--dsw-alias-bg-layer-2,#f8f9fc);color:var(--dsw-alias-label-secondary,#68707d);font-size:12px;white-space:nowrap;overflow:hidden}
|
|
720
|
+
.jsk-step{display:inline-flex;align-items:center;gap:7px;color:var(--dsw-alias-label-secondary,#4d5563);font-weight:500}
|
|
721
|
+
.jsk-step i{width:17px;height:17px;border-radius:50%;display:grid;place-items:center;background:color-mix(in srgb,var(--dsw-alias-brand-primary,#4f6bfe) 10%,transparent);color:var(--dsw-alias-brand-primary,#4658d9);font-size:10px;font-style:normal;font-weight:700}
|
|
722
|
+
.jsk-arrow{color:var(--dsw-alias-label-tertiary,#b3b9c4)}
|
|
723
|
+
.jsk-spinner{width:14px;height:14px;border:2px solid color-mix(in srgb,var(--dsw-alias-brand-primary,#4f6bfe) 25%,transparent);border-top-color:var(--dsw-alias-brand-primary,#4f6bfe);border-radius:50%;display:inline-block;animation:jsk-spin .8s linear infinite;flex:none}
|
|
724
|
+
.jsk-spinner-dark{border-color:rgba(255,255,255,.35);border-top-color:#fff;width:12px;height:12px}
|
|
725
|
+
@keyframes jsk-spin{to{transform:rotate(360deg)}}
|
|
726
|
+
.jsk-panel-icon{display:grid;place-items:center;width:100%;height:100%}
|
|
727
|
+
.jsk-tab-title{display:inline-flex;align-items:center;gap:6px;min-width:0;max-width:180px}
|
|
728
|
+
.jsk-tab-title>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
729
|
+
.jsk-canvas{position:absolute;inset:0;background:var(--dsw-alias-bg-layer-2,#f1efec)}
|
|
730
|
+
.jsk-frame{display:block;width:100%;height:100%;border:0;background:#f1efec}
|
|
731
|
+
.jsk-canvas-state{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:9px;padding:24px;color:var(--dsw-alias-label-secondary,#68707d);font-size:12.5px;text-align:center;background:var(--dsw-alias-bg-layer-1,#fbfbfc)}
|
|
732
|
+
.jsk-canvas-state strong{color:var(--dsw-alias-label-primary,#3c4452);font-size:13.5px;font-weight:600}
|
|
733
|
+
.jsk-canvas-state strong{color:var(--dsw-alias-label-primary,#3c4452);font-size:13.5px;font-weight:600}
|
|
734
|
+
.jsk-picker{height:100%;display:flex;flex-direction:column;min-height:0;background:var(--dsw-alias-bg-layer-1,#fbfbfc);color:var(--dsw-alias-label-primary,#1b1f26);font:13px/1.5 -apple-system,BlinkMacSystemFont,"PingFang SC","Hiragino Sans GB","Segoe UI",sans-serif}
|
|
735
|
+
.jsk-picker *{box-sizing:border-box}
|
|
736
|
+
.jsk-picker-head{flex:none;display:flex;align-items:center;gap:8px;padding:12px 12px 10px;border-bottom:1px solid var(--dsw-alias-border-l2,#eceef2)}
|
|
737
|
+
.jsk-picker-heading{min-width:0;flex:1}
|
|
738
|
+
.jsk-picker-title{font-size:12.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
739
|
+
.jsk-picker-sub{font-size:10.5px;color:var(--dsw-alias-label-tertiary,#9aa2af);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:2px}
|
|
740
|
+
.jsk-picker-workbench{flex:none;display:inline-flex;align-items:center;gap:5px;height:27px;padding:0 8px;border:1px solid var(--dsw-alias-border-l2,#e7e9ee);border-radius:7px;background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-secondary,#4d5563);font:inherit;font-size:11px;cursor:pointer;white-space:nowrap}
|
|
741
|
+
.jsk-picker-workbench:hover{background:var(--dsw-alias-interactive-bg-hover,#f3f4f7)}
|
|
742
|
+
.jsk-picker-list{flex:1;min-height:0;overflow:auto;padding:8px;display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px;align-content:start}
|
|
743
|
+
.jsk-picker-note{grid-column:1/-1;margin:0 0 2px;padding:5px 7px;border-radius:6px;background:#fdf3e3;color:#a06a12;font-size:10.5px}
|
|
744
|
+
.jsk-picker-state{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:24px;text-align:center;color:var(--dsw-alias-label-secondary,#68707d);font-size:12px}
|
|
745
|
+
.jsk-picker-state strong{color:var(--dsw-alias-label-primary,#3c4452);font-size:13px;font-weight:600}
|
|
746
|
+
.jsk-pick-card{display:flex;flex-direction:column;gap:5px;min-width:0;padding:8px;border:1px solid var(--dsw-alias-border-l2,#eaecf1);border-radius:9px;background:var(--dsw-alias-bg-layer-1,#fff);color:inherit;font:inherit;text-align:left;cursor:pointer;transition:border-color .15s ease, box-shadow .15s ease, transform .15s ease}
|
|
747
|
+
.jsk-pick-card:hover{border-color:color-mix(in srgb,var(--dsw-alias-brand-primary,#4f6bfe) 45%,var(--dsw-alias-border-l2,#eaecf1));box-shadow:0 6px 18px -12px rgba(16,24,40,.26);transform:translateY(-1px)}
|
|
748
|
+
.jsk-pick-thumb{position:relative;display:flex;align-items:center;justify-content:center;height:84px;border-radius:7px;border:1px solid var(--dsw-alias-border-l2,#eef0f4);background:radial-gradient(circle at 1px 1px,#e2e6ee 1px,transparent 0) 0 0/12px 12px,linear-gradient(135deg,#f7f8fb,#f1f3f8);overflow:hidden}
|
|
749
|
+
.jsk-pick-thumb .jsk-card-screens{gap:4px;transform:translateY(1px)}
|
|
750
|
+
.jsk-pick-thumb .jsk-card-mini{width:20px;height:58px}
|
|
751
|
+
.jsk-pick-thumb .jsk-card-mini:nth-child(2){height:66px}
|
|
752
|
+
.jsk-pick-arrow{position:absolute;right:5px;bottom:5px;width:20px;height:20px;border-radius:5px;display:grid;place-items:center;background:color-mix(in srgb,var(--dsw-alias-bg-layer-1,#fff) 86%,transparent);border:1px solid var(--dsw-alias-border-l2,#e7e9ee);color:var(--dsw-alias-label-secondary,#68707d)}
|
|
753
|
+
.jsk-pick-card:hover .jsk-pick-arrow{background:var(--dsw-alias-brand-primary,#4f6bfe);border-color:var(--dsw-alias-brand-primary,#4f6bfe);color:#fff}
|
|
754
|
+
.jsk-pick-name{font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
755
|
+
.jsk-pick-meta{display:flex;align-items:center;gap:5px;font-size:10.5px;color:var(--dsw-alias-label-secondary,#68707d);white-space:nowrap;overflow:hidden}
|
|
756
|
+
.jsk-pick-time{font-size:10.5px;color:var(--dsw-alias-label-tertiary,#9aa2af);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
757
|
+
.jsk-switch{position:absolute;right:10px;bottom:10px;z-index:3;display:inline-flex;align-items:center;gap:5px;height:28px;padding:0 9px;border:1px solid var(--dsw-alias-border-l2,#e7e9ee);border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-1,#fff) 88%,transparent);color:var(--dsw-alias-label-secondary,#4d5563);font:inherit;font-size:11px;cursor:pointer;backdrop-filter:blur(6px);box-shadow:0 2px 8px rgba(0,0,0,.08)}
|
|
758
|
+
.jsk-switch:hover{background:var(--dsw-alias-bg-layer-1,#fff)}
|
|
759
|
+
`;
|
|
760
|
+
|
|
761
|
+
/* ------------------------------------------------------------------ *
|
|
762
|
+
* Plugin body
|
|
763
|
+
* ------------------------------------------------------------------ */
|
|
764
|
+
|
|
765
|
+
return {
|
|
766
|
+
inject: ['slots'],
|
|
767
|
+
apply(ctx) {
|
|
768
|
+
runtime = ctx;
|
|
769
|
+
|
|
770
|
+
ctx.effect(() => {
|
|
771
|
+
const style = document.createElement('style');
|
|
772
|
+
style.dataset.dshPlugin = 'justskill-design';
|
|
773
|
+
style.textContent = CSS;
|
|
774
|
+
document.head.appendChild(style);
|
|
775
|
+
return () => style.remove();
|
|
776
|
+
}, 'justskill-design: stylesheet');
|
|
777
|
+
|
|
778
|
+
ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
|
|
779
|
+
name: 'sidebar.panellist',
|
|
780
|
+
id: PANEL_ID,
|
|
781
|
+
order: 50,
|
|
782
|
+
label: () => 'JustSkill',
|
|
783
|
+
}, PanelIcon));
|
|
784
|
+
|
|
785
|
+
ctx.slots.inject('main', () => ctx.slots.register({
|
|
786
|
+
name: 'main',
|
|
787
|
+
key: PANEL_ID,
|
|
788
|
+
}, WorkbenchPage));
|
|
789
|
+
|
|
790
|
+
ctx.inject(['sidebarRightTabs'], (scope) => {
|
|
791
|
+
const tabs = scope.get('sidebarRightTabs');
|
|
792
|
+
if (tabs === undefined || typeof tabs.register !== 'function') return;
|
|
793
|
+
|
|
794
|
+
scope.effect(() => tabs.register({
|
|
795
|
+
id: TAB_ID,
|
|
796
|
+
kind: TAB_KIND,
|
|
797
|
+
priority: 'extension',
|
|
798
|
+
keepMounted: true,
|
|
799
|
+
title: () => 'Canvas',
|
|
800
|
+
guide: [{
|
|
801
|
+
id: 'canvas',
|
|
802
|
+
order: 100,
|
|
803
|
+
title: () => 'JustSkill Canvas',
|
|
804
|
+
description: () => '从工作台点开设计文件后,这里显示它的实时画布',
|
|
805
|
+
}],
|
|
806
|
+
}), 'justskill-design: canvas tab type');
|
|
807
|
+
|
|
808
|
+
ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({
|
|
809
|
+
name: 'sidebar.right.pane.tab',
|
|
810
|
+
key: TAB_ID,
|
|
811
|
+
}, CanvasBody));
|
|
812
|
+
|
|
813
|
+
ctx.slots.inject('sidebar.right.pane.tab.title', () => ctx.slots.register({
|
|
814
|
+
name: 'sidebar.right.pane.tab.title',
|
|
815
|
+
key: TAB_ID,
|
|
816
|
+
}, CanvasTitle));
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
// Host tool → browser handoff. `justskill_canvas` leaves a session-scoped
|
|
820
|
+
// request in the Host; this watcher is the only thing that can turn it
|
|
821
|
+
// into a right-sidebar tab. It is deliberately not tied to the workbench
|
|
822
|
+
// page, so it also works from an ordinary chat session.
|
|
823
|
+
ctx.effect(() => {
|
|
824
|
+
let stopped = false;
|
|
825
|
+
let busy = false;
|
|
826
|
+
|
|
827
|
+
const tick = async () => {
|
|
828
|
+
if (stopped || busy) return;
|
|
829
|
+
const sidebarRight = runtime === null ? undefined : runtime.get('sidebarRight');
|
|
830
|
+
if (sidebarRight === undefined || typeof sidebarRight.openTab !== 'function') return;
|
|
831
|
+
let sessionId;
|
|
832
|
+
try {
|
|
833
|
+
sessionId = sidebarRight.mounted?.getSnapshot?.();
|
|
834
|
+
} catch {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (typeof sessionId !== 'string' || sessionId === '') return;
|
|
838
|
+
|
|
839
|
+
busy = true;
|
|
840
|
+
try {
|
|
841
|
+
const value = await post('pending', { sessionId });
|
|
842
|
+
const file = value && value.pending ? value.pending.file : undefined;
|
|
843
|
+
if (typeof file !== 'string' || file === '') return;
|
|
844
|
+
await openCanvasTab(sidebarRight, file);
|
|
845
|
+
if (stopped) return;
|
|
846
|
+
await post('pending/clear', { sessionId, file }).catch(() => {});
|
|
847
|
+
} catch {
|
|
848
|
+
// The sidebar store may not be minted yet; the next tick retries.
|
|
849
|
+
} finally {
|
|
850
|
+
busy = false;
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
const timer = setInterval(() => { void tick(); }, 1500);
|
|
855
|
+
void tick();
|
|
856
|
+
return () => {
|
|
857
|
+
stopped = true;
|
|
858
|
+
clearInterval(timer);
|
|
859
|
+
};
|
|
860
|
+
}, 'justskill-design: canvas request watcher');
|
|
861
|
+
},
|
|
862
|
+
};
|
|
863
|
+
},
|
|
864
|
+
});
|