@rooode/dsh-plugin-preview 0.1.9 → 0.1.10
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/lib/client.js +528 -77
- package/lib/index.js +36 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
const STORAGE_KEY_WIDTH = 'dsh:preview:panel_width:v1';
|
|
26
26
|
const STORAGE_KEY_TREE_OPEN = 'dsh:preview:tree:open:v1';
|
|
27
27
|
const STORAGE_KEY_TREE_WIDTH = 'dsh:preview:tree:width:v1';
|
|
28
|
-
const
|
|
28
|
+
const STORAGE_KEY_MANUAL_WS = 'dsh:preview:manual_workspace:v1';
|
|
29
|
+
const DEFAULT_PANEL_WIDTH = 620;
|
|
29
30
|
const MIN_PANEL_WIDTH = 380;
|
|
30
31
|
const DEFAULT_TREE_WIDTH = 240;
|
|
31
32
|
const MIN_TREE_WIDTH = 180;
|
|
@@ -50,11 +51,20 @@
|
|
|
50
51
|
let globalActiveTabId = null;
|
|
51
52
|
let globalIsPanelOpen = false;
|
|
52
53
|
let globalIsTreeOpen = true;
|
|
54
|
+
let globalIsMaximized = false;
|
|
55
|
+
let globalManualWorkspace = null;
|
|
53
56
|
let globalPanelWidth = DEFAULT_PANEL_WIDTH;
|
|
54
57
|
let globalTreeWidth = DEFAULT_TREE_WIDTH;
|
|
55
58
|
let globalLastOpenTime = 0;
|
|
56
59
|
const stateListeners = new Set();
|
|
57
60
|
|
|
61
|
+
try {
|
|
62
|
+
const savedManualWs = localStorage.getItem(STORAGE_KEY_MANUAL_WS);
|
|
63
|
+
if (savedManualWs && savedManualWs.trim()) {
|
|
64
|
+
globalManualWorkspace = savedManualWs.trim();
|
|
65
|
+
}
|
|
66
|
+
} catch (e) {}
|
|
67
|
+
|
|
58
68
|
try {
|
|
59
69
|
const savedWidth = localStorage.getItem(STORAGE_KEY_WIDTH);
|
|
60
70
|
if (savedWidth) {
|
|
@@ -121,6 +131,26 @@
|
|
|
121
131
|
notifyStateChange();
|
|
122
132
|
}
|
|
123
133
|
|
|
134
|
+
function setIsMaximized(maximized) {
|
|
135
|
+
globalIsMaximized = maximized;
|
|
136
|
+
notifyStateChange();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function setManualWorkspace(dirPath) {
|
|
140
|
+
if (dirPath && typeof dirPath === 'string' && dirPath.trim()) {
|
|
141
|
+
globalManualWorkspace = dirPath.trim();
|
|
142
|
+
try {
|
|
143
|
+
localStorage.setItem(STORAGE_KEY_MANUAL_WS, globalManualWorkspace);
|
|
144
|
+
} catch (e) {}
|
|
145
|
+
} else {
|
|
146
|
+
globalManualWorkspace = null;
|
|
147
|
+
try {
|
|
148
|
+
localStorage.removeItem(STORAGE_KEY_MANUAL_WS);
|
|
149
|
+
} catch (e) {}
|
|
150
|
+
}
|
|
151
|
+
notifyStateChange();
|
|
152
|
+
}
|
|
153
|
+
|
|
124
154
|
function setTreeWidth(width) {
|
|
125
155
|
globalTreeWidth = Math.max(MIN_TREE_WIDTH, Math.min(width, MAX_TREE_WIDTH));
|
|
126
156
|
try {
|
|
@@ -220,31 +250,143 @@
|
|
|
220
250
|
return 'code';
|
|
221
251
|
}
|
|
222
252
|
|
|
223
|
-
function
|
|
224
|
-
|
|
253
|
+
function getAllAvailableWorkspaces() {
|
|
254
|
+
const map = new Map();
|
|
255
|
+
const currentRoot = getCurrentWorkspaceRoot().replace(/[\\/]+$/, '').toLowerCase();
|
|
256
|
+
|
|
257
|
+
// 1. Collect from active & historical sessions in clientCtx.sessions
|
|
258
|
+
if (clientCtx && clientCtx.sessions) {
|
|
225
259
|
try {
|
|
226
|
-
|
|
227
|
-
|
|
260
|
+
const sSnap = clientCtx.sessions.list?.getSnapshot();
|
|
261
|
+
if (sSnap && sSnap.byId) {
|
|
262
|
+
for (const sid of Object.keys(sSnap.byId)) {
|
|
263
|
+
const s = sSnap.byId[sid];
|
|
264
|
+
if (s && s.cwd && typeof s.cwd === 'string' && s.cwd.trim()) {
|
|
265
|
+
const norm = s.cwd.trim().replace(/[\\/]+$/, '');
|
|
266
|
+
const key = norm.toLowerCase();
|
|
267
|
+
if (!map.has(key)) {
|
|
268
|
+
map.set(key, {
|
|
269
|
+
id: s.workspaceId || norm,
|
|
270
|
+
path: norm,
|
|
271
|
+
name: s.workspaceTitle || getFileName(norm) || norm,
|
|
272
|
+
isCurrent: key === currentRoot,
|
|
273
|
+
source: 'session',
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
228
278
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
279
|
+
} catch (e) {}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 2. Collect from workspaces store in clientCtx.workspaces
|
|
283
|
+
if (clientCtx && clientCtx.workspaces) {
|
|
284
|
+
try {
|
|
285
|
+
const wSnap = clientCtx.workspaces.list?.getSnapshot();
|
|
286
|
+
if (wSnap) {
|
|
287
|
+
const rawList = wSnap.workspaces || wSnap.items || wSnap.byId || [];
|
|
288
|
+
const list = Array.isArray(rawList) ? rawList : Object.values(rawList);
|
|
289
|
+
for (const w of list) {
|
|
290
|
+
const dir = (w && (w.directory || w.path || w.cwd)) || '';
|
|
291
|
+
if (dir && typeof dir === 'string' && dir.trim()) {
|
|
292
|
+
const norm = dir.trim().replace(/[\\/]+$/, '');
|
|
293
|
+
const key = norm.toLowerCase();
|
|
294
|
+
if (!map.has(key)) {
|
|
295
|
+
map.set(key, {
|
|
296
|
+
id: w.id || norm,
|
|
297
|
+
path: norm,
|
|
298
|
+
name: w.title || w.name || getFileName(norm) || norm,
|
|
299
|
+
isCurrent: key === currentRoot,
|
|
300
|
+
source: 'workspace',
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
232
305
|
}
|
|
233
306
|
} catch (e) {}
|
|
234
307
|
}
|
|
308
|
+
|
|
309
|
+
// 3. Collect from globalManualWorkspace if set
|
|
310
|
+
if (globalManualWorkspace && typeof globalManualWorkspace === 'string' && globalManualWorkspace.trim()) {
|
|
311
|
+
const norm = globalManualWorkspace.trim().replace(/[\\/]+$/, '');
|
|
312
|
+
const key = norm.toLowerCase();
|
|
313
|
+
if (!map.has(key)) {
|
|
314
|
+
map.set(key, {
|
|
315
|
+
id: 'manual',
|
|
316
|
+
path: norm,
|
|
317
|
+
name: getFileName(norm) || norm,
|
|
318
|
+
isCurrent: true,
|
|
319
|
+
source: 'manual',
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const result = Array.from(map.values());
|
|
325
|
+
// Update isCurrent flags
|
|
326
|
+
result.forEach(ws => {
|
|
327
|
+
ws.isCurrent = ws.path.replace(/[\\/]+$/, '').toLowerCase() === currentRoot;
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function getCurrentWorkspaceRoot() {
|
|
334
|
+
// Priority 1: User manual override selection
|
|
335
|
+
if (globalManualWorkspace && typeof globalManualWorkspace === 'string' && globalManualWorkspace.trim()) {
|
|
336
|
+
return globalManualWorkspace.trim();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Priority 2: Active session working directory (cwd)
|
|
235
340
|
if (clientCtx && clientCtx.sessions) {
|
|
236
341
|
try {
|
|
237
|
-
const
|
|
238
|
-
const currentSessionId =
|
|
239
|
-
if (currentSessionId &&
|
|
240
|
-
|
|
342
|
+
const sSnap = clientCtx.sessions.list?.getSnapshot();
|
|
343
|
+
const currentSessionId = sSnap?.current;
|
|
344
|
+
if (currentSessionId && sSnap?.byId?.[currentSessionId]?.cwd) {
|
|
345
|
+
const cwd = sSnap.byId[currentSessionId].cwd;
|
|
346
|
+
if (cwd && typeof cwd === 'string' && cwd.trim()) {
|
|
347
|
+
return cwd.trim();
|
|
348
|
+
}
|
|
241
349
|
}
|
|
242
350
|
} catch (e) {}
|
|
243
351
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
352
|
+
|
|
353
|
+
// Priority 3: Current workspace in workspaces store
|
|
354
|
+
if (clientCtx && clientCtx.workspaces) {
|
|
355
|
+
try {
|
|
356
|
+
const wSnap = clientCtx.workspaces.list?.getSnapshot();
|
|
357
|
+
if (wSnap) {
|
|
358
|
+
const curId = wSnap.current;
|
|
359
|
+
if (curId && wSnap.workspaces && wSnap.workspaces[curId]) {
|
|
360
|
+
const w = wSnap.workspaces[curId];
|
|
361
|
+
const dir = w.directory || w.path || w.cwd;
|
|
362
|
+
if (dir) return dir.trim();
|
|
363
|
+
}
|
|
364
|
+
if (wSnap.byId && curId && wSnap.byId[curId]) {
|
|
365
|
+
const w = wSnap.byId[curId];
|
|
366
|
+
const dir = w.directory || w.path || w.cwd;
|
|
367
|
+
if (dir) return dir.trim();
|
|
368
|
+
}
|
|
369
|
+
if (Array.isArray(wSnap.items) && wSnap.items.length > 0) {
|
|
370
|
+
const first = wSnap.items[0];
|
|
371
|
+
const dir = first.directory || first.path || first.cwd;
|
|
372
|
+
if (dir) return dir.trim();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
} catch (e) {}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Priority 4: URL hash or search parameter
|
|
379
|
+
if (typeof location !== 'undefined') {
|
|
380
|
+
if (location.hash) {
|
|
381
|
+
const m = location.hash.match(/workspace=([^&]+)/);
|
|
382
|
+
if (m) return decodeURIComponent(m[1]);
|
|
383
|
+
}
|
|
384
|
+
if (location.search) {
|
|
385
|
+
const m = location.search.match(/workspace=([^&]+)/);
|
|
386
|
+
if (m) return decodeURIComponent(m[1]);
|
|
387
|
+
}
|
|
247
388
|
}
|
|
389
|
+
|
|
248
390
|
return '';
|
|
249
391
|
}
|
|
250
392
|
|
|
@@ -915,6 +1057,9 @@
|
|
|
915
1057
|
const [error, setError] = useState(null);
|
|
916
1058
|
const [searchQuery, setSearchQuery] = useState('');
|
|
917
1059
|
const [expandedPaths, setExpandedPaths] = useState(new Set());
|
|
1060
|
+
const [isSelectorOpen, setIsSelectorOpen] = useState(false);
|
|
1061
|
+
const [customPathInput, setCustomPathInput] = useState('');
|
|
1062
|
+
const [validating, setValidating] = useState(false);
|
|
918
1063
|
const currentWs = getCurrentWorkspaceRoot();
|
|
919
1064
|
|
|
920
1065
|
const loadTree = useCallback(async (force = false) => {
|
|
@@ -942,7 +1087,7 @@
|
|
|
942
1087
|
|
|
943
1088
|
useEffect(() => {
|
|
944
1089
|
loadTree();
|
|
945
|
-
}, [loadTree]);
|
|
1090
|
+
}, [loadTree, globalManualWorkspace]);
|
|
946
1091
|
|
|
947
1092
|
const handleToggleExpand = (dirPath) => {
|
|
948
1093
|
setExpandedPaths(prev => {
|
|
@@ -976,15 +1121,63 @@
|
|
|
976
1121
|
}
|
|
977
1122
|
}, [searchQuery, treeData]);
|
|
978
1123
|
|
|
1124
|
+
const availableWorkspaces = useMemo(() => {
|
|
1125
|
+
return getAllAvailableWorkspaces();
|
|
1126
|
+
}, [isSelectorOpen, globalManualWorkspace]);
|
|
1127
|
+
|
|
1128
|
+
const handleSelectWs = (wsPath) => {
|
|
1129
|
+
setManualWorkspace(wsPath);
|
|
1130
|
+
setIsSelectorOpen(false);
|
|
1131
|
+
showToast(`已切换工作区: ${getFileName(wsPath)}`, 'success');
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
const handleCustomSubmit = async (e) => {
|
|
1135
|
+
if (e) e.preventDefault();
|
|
1136
|
+
const target = customPathInput.trim();
|
|
1137
|
+
if (!target) return;
|
|
1138
|
+
setValidating(true);
|
|
1139
|
+
try {
|
|
1140
|
+
const res = await fetch('/api/preview/validate-dir', {
|
|
1141
|
+
method: 'POST',
|
|
1142
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1143
|
+
body: JSON.stringify({ path: target }),
|
|
1144
|
+
});
|
|
1145
|
+
const json = await res.json();
|
|
1146
|
+
if (json.ok && json.path) {
|
|
1147
|
+
setManualWorkspace(json.path);
|
|
1148
|
+
setCustomPathInput('');
|
|
1149
|
+
setIsSelectorOpen(false);
|
|
1150
|
+
showToast(`已切换工作空间: ${json.name}`, 'success');
|
|
1151
|
+
} else {
|
|
1152
|
+
showToast(json?.error?.message || '指定目录不存在或不是文件夹', 'error');
|
|
1153
|
+
}
|
|
1154
|
+
} catch (err) {
|
|
1155
|
+
showToast('验证目录路径失败', 'error');
|
|
1156
|
+
} finally {
|
|
1157
|
+
setValidating(false);
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
const handleResetAuto = () => {
|
|
1162
|
+
setManualWorkspace(null);
|
|
1163
|
+
setIsSelectorOpen(false);
|
|
1164
|
+
showToast('已恢复根据当前会话自动识别工作空间', 'info');
|
|
1165
|
+
};
|
|
1166
|
+
|
|
979
1167
|
return h('div', {
|
|
980
1168
|
className: 'dsh-tree-sidebar',
|
|
981
1169
|
style: { width: `${width}px` },
|
|
982
1170
|
},
|
|
983
1171
|
// Sidebar Header
|
|
984
1172
|
h('div', { className: 'dsh-tree-sidebar-header' },
|
|
985
|
-
h('div', {
|
|
1173
|
+
h('div', {
|
|
1174
|
+
className: 'dsh-tree-sidebar-title dsh-tree-title-clickable',
|
|
1175
|
+
onClick: () => setIsSelectorOpen(!isSelectorOpen),
|
|
1176
|
+
title: `当前工作空间: ${treeData?.workspaceRoot || currentWs || '未指定'}\n点击切换工作空间`,
|
|
1177
|
+
},
|
|
986
1178
|
h('span', { className: 'dsh-tree-header-icon' }, '📁'),
|
|
987
|
-
h('span', { className: 'dsh-tree-header-text' }, treeData?.workspaceName || getFileName(currentWs) || '工作区')
|
|
1179
|
+
h('span', { className: 'dsh-tree-header-text' }, treeData?.workspaceName || getFileName(currentWs) || '工作区'),
|
|
1180
|
+
h('span', { className: 'dsh-tree-header-chevron' }, isSelectorOpen ? '▲' : '▼')
|
|
988
1181
|
),
|
|
989
1182
|
h('div', { className: 'dsh-tree-header-actions' },
|
|
990
1183
|
h('button', {
|
|
@@ -1014,6 +1207,62 @@
|
|
|
1014
1207
|
)
|
|
1015
1208
|
),
|
|
1016
1209
|
|
|
1210
|
+
// Workspace Selector Dropdown Popover
|
|
1211
|
+
isSelectorOpen ? h('div', { className: 'dsh-ws-selector-popover' },
|
|
1212
|
+
h('div', { className: 'dsh-ws-popover-header' },
|
|
1213
|
+
h('span', { style: { fontWeight: 600, fontSize: 12 } }, '选择工作空间'),
|
|
1214
|
+
h('button', {
|
|
1215
|
+
type: 'button',
|
|
1216
|
+
className: 'dsh-ws-popover-close',
|
|
1217
|
+
onClick: () => setIsSelectorOpen(false),
|
|
1218
|
+
}, '✕')
|
|
1219
|
+
),
|
|
1220
|
+
|
|
1221
|
+
// List of detected workspaces
|
|
1222
|
+
h('div', { className: 'dsh-ws-list' },
|
|
1223
|
+
availableWorkspaces.length > 0 ? availableWorkspaces.map(ws => (
|
|
1224
|
+
h('div', {
|
|
1225
|
+
key: ws.path,
|
|
1226
|
+
className: `dsh-ws-item ${ws.isCurrent ? 'active' : ''}`,
|
|
1227
|
+
onClick: () => handleSelectWs(ws.path),
|
|
1228
|
+
title: ws.path,
|
|
1229
|
+
},
|
|
1230
|
+
h('div', { className: 'dsh-ws-item-info' },
|
|
1231
|
+
h('span', { className: 'dsh-ws-item-icon' }, '📁'),
|
|
1232
|
+
h('span', { className: 'dsh-ws-item-name' }, ws.name),
|
|
1233
|
+
h('span', { className: 'dsh-ws-item-path' }, ws.path)
|
|
1234
|
+
),
|
|
1235
|
+
ws.isCurrent ? h('span', { className: 'dsh-ws-item-check' }, '✓') : null
|
|
1236
|
+
)
|
|
1237
|
+
)) : h('div', { className: 'dsh-ws-empty' }, '暂无其他检测到的工作空间')
|
|
1238
|
+
),
|
|
1239
|
+
|
|
1240
|
+
// Custom Path Input
|
|
1241
|
+
h('form', { className: 'dsh-ws-custom-form', onSubmit: handleCustomSubmit },
|
|
1242
|
+
h('div', { className: 'dsh-ws-input-row' },
|
|
1243
|
+
h('input', {
|
|
1244
|
+
type: 'text',
|
|
1245
|
+
className: 'dsh-ws-input',
|
|
1246
|
+
placeholder: '输入或粘贴本地目录路径...',
|
|
1247
|
+
value: customPathInput,
|
|
1248
|
+
onChange: (e) => setCustomPathInput(e.target.value),
|
|
1249
|
+
}),
|
|
1250
|
+
h('button', {
|
|
1251
|
+
type: 'submit',
|
|
1252
|
+
className: 'dsh-ws-btn',
|
|
1253
|
+
disabled: validating || !customPathInput.trim(),
|
|
1254
|
+
}, validating ? '验证...' : '切换')
|
|
1255
|
+
)
|
|
1256
|
+
),
|
|
1257
|
+
|
|
1258
|
+
// Auto-detect reset link
|
|
1259
|
+
globalManualWorkspace ? h('button', {
|
|
1260
|
+
type: 'button',
|
|
1261
|
+
className: 'dsh-ws-reset-btn',
|
|
1262
|
+
onClick: handleResetAuto,
|
|
1263
|
+
}, '🔄 恢复自动识别当前会话工作区') : null
|
|
1264
|
+
) : null,
|
|
1265
|
+
|
|
1017
1266
|
// Search Input
|
|
1018
1267
|
h('div', { className: 'dsh-tree-sidebar-search' },
|
|
1019
1268
|
h('div', { className: 'dsh-tree-search-wrapper' },
|
|
@@ -2463,6 +2712,7 @@
|
|
|
2463
2712
|
const [activeTabId, setActiveTabId] = useState(globalActiveTabId);
|
|
2464
2713
|
const [isOpen, setIsOpen] = useState(globalIsPanelOpen);
|
|
2465
2714
|
const [width, setWidth] = useState(globalPanelWidth);
|
|
2715
|
+
const [isMaximized, setLocalIsMaximized] = useState(globalIsMaximized);
|
|
2466
2716
|
const [isTreeOpen, setLocalIsTreeOpen] = useState(globalIsTreeOpen);
|
|
2467
2717
|
const [treeWidth, setLocalTreeWidth] = useState(globalTreeWidth);
|
|
2468
2718
|
const [isTreeDragging, setIsTreeDragging] = useState(false);
|
|
@@ -2482,6 +2732,7 @@
|
|
|
2482
2732
|
setActiveTabId(globalActiveTabId);
|
|
2483
2733
|
setIsOpen(globalIsPanelOpen);
|
|
2484
2734
|
setWidth(globalPanelWidth);
|
|
2735
|
+
setLocalIsMaximized(globalIsMaximized);
|
|
2485
2736
|
setLocalIsTreeOpen(globalIsTreeOpen);
|
|
2486
2737
|
setLocalTreeWidth(globalTreeWidth);
|
|
2487
2738
|
};
|
|
@@ -2489,6 +2740,30 @@
|
|
|
2489
2740
|
return () => stateListeners.delete(update);
|
|
2490
2741
|
}, []);
|
|
2491
2742
|
|
|
2743
|
+
// Synchronize side-by-side layout CSS variables with document.body
|
|
2744
|
+
useEffect(() => {
|
|
2745
|
+
if (typeof document === 'undefined') return;
|
|
2746
|
+
if (isOpen) {
|
|
2747
|
+
document.body.classList.add('dsh-preview-sidebar-active');
|
|
2748
|
+
if (isMaximized) {
|
|
2749
|
+
document.body.classList.add('dsh-preview-maximized');
|
|
2750
|
+
document.body.style.setProperty('--dsh-preview-width', '100vw');
|
|
2751
|
+
} else {
|
|
2752
|
+
document.body.classList.remove('dsh-preview-maximized');
|
|
2753
|
+
document.body.style.setProperty('--dsh-preview-width', `${width}px`);
|
|
2754
|
+
}
|
|
2755
|
+
} else {
|
|
2756
|
+
document.body.classList.remove('dsh-preview-sidebar-active');
|
|
2757
|
+
document.body.classList.remove('dsh-preview-maximized');
|
|
2758
|
+
document.body.style.removeProperty('--dsh-preview-width');
|
|
2759
|
+
}
|
|
2760
|
+
return () => {
|
|
2761
|
+
document.body.classList.remove('dsh-preview-sidebar-active');
|
|
2762
|
+
document.body.classList.remove('dsh-preview-maximized');
|
|
2763
|
+
document.body.style.removeProperty('--dsh-preview-width');
|
|
2764
|
+
};
|
|
2765
|
+
}, [isOpen, width, isMaximized]);
|
|
2766
|
+
|
|
2492
2767
|
const activeTab = useMemo(() => {
|
|
2493
2768
|
return tabs.find(t => t.id === activeTabId) || tabs[0] || null;
|
|
2494
2769
|
}, [tabs, activeTabId]);
|
|
@@ -2567,54 +2842,6 @@
|
|
|
2567
2842
|
|
|
2568
2843
|
const drawerRef = useRef(null);
|
|
2569
2844
|
|
|
2570
|
-
// Outside Click Listener: Click on the chat conversation / dialog box outside the drawer to auto close/hide it
|
|
2571
|
-
useEffect(() => {
|
|
2572
|
-
if (!isOpen) return;
|
|
2573
|
-
|
|
2574
|
-
const handleOutsidePointerDown = (e) => {
|
|
2575
|
-
// 1. Ignore if just opened within 250ms
|
|
2576
|
-
if (Date.now() - globalLastOpenTime < 250) return;
|
|
2577
|
-
// 2. Ignore during dragging resize handle
|
|
2578
|
-
if (isDragging || isTreeDragging) return;
|
|
2579
|
-
|
|
2580
|
-
const target = e.target;
|
|
2581
|
-
if (!target) return;
|
|
2582
|
-
|
|
2583
|
-
// 3. If clicking inside the preview drawer, do not close
|
|
2584
|
-
const drawerEl = drawerRef.current || document.querySelector('.dsh-preview-drawer-container');
|
|
2585
|
-
if (drawerEl && (drawerEl === target || drawerEl.contains(target))) {
|
|
2586
|
-
return;
|
|
2587
|
-
}
|
|
2588
|
-
|
|
2589
|
-
// 4. If clicking on the right activity rail, preview drawer or git drawer, do not close
|
|
2590
|
-
if (target.closest && target.closest('.dsh-right-activity-rail, .dsh-rail-tab-item, .dsh-preview-drawer-container, .dsh-git-drawer-container')) {
|
|
2591
|
-
return;
|
|
2592
|
-
}
|
|
2593
|
-
|
|
2594
|
-
// 5. If clicking on a file link or button that opens a file preview, do not close (let interceptor open it)
|
|
2595
|
-
if (target.closest) {
|
|
2596
|
-
const btn = target.closest('button');
|
|
2597
|
-
if (btn) {
|
|
2598
|
-
const candidate = btn.getAttribute('title') || btn.getAttribute('aria-label') || btn.textContent || '';
|
|
2599
|
-
if (isPreviewableFile(candidate.trim())) return;
|
|
2600
|
-
}
|
|
2601
|
-
const anchor = target.closest('a');
|
|
2602
|
-
if (anchor) {
|
|
2603
|
-
const href = anchor.getAttribute('href') || '';
|
|
2604
|
-
if (isPreviewableFile(href)) return;
|
|
2605
|
-
}
|
|
2606
|
-
}
|
|
2607
|
-
|
|
2608
|
-
// 6. Outside click in dialog/conversation/background: Auto hide the preview drawer!
|
|
2609
|
-
setPanelOpen(false);
|
|
2610
|
-
};
|
|
2611
|
-
|
|
2612
|
-
window.addEventListener('pointerdown', handleOutsidePointerDown, true);
|
|
2613
|
-
return () => {
|
|
2614
|
-
window.removeEventListener('pointerdown', handleOutsidePointerDown, true);
|
|
2615
|
-
};
|
|
2616
|
-
}, [isOpen, isDragging, isTreeDragging]);
|
|
2617
|
-
|
|
2618
2845
|
// Mutual exclusion with other tool windows (e.g. Git modal)
|
|
2619
2846
|
useEffect(() => {
|
|
2620
2847
|
const handleToolWindowOpen = (e) => {
|
|
@@ -2679,17 +2906,17 @@
|
|
|
2679
2906
|
null,
|
|
2680
2907
|
isOpen ? h('div', {
|
|
2681
2908
|
ref: drawerRef,
|
|
2682
|
-
className: `dsh-preview-drawer-container ${isDragging || isTreeDragging ? 'dragging' : ''}`,
|
|
2683
|
-
style: { width: `${width}px` },
|
|
2909
|
+
className: `dsh-preview-drawer-container ${isDragging || isTreeDragging ? 'dragging' : ''} ${isMaximized ? 'maximized' : ''}`,
|
|
2910
|
+
style: { width: isMaximized ? '100vw' : `${width}px` },
|
|
2684
2911
|
},
|
|
2685
|
-
// Drag Handle on Left Border
|
|
2686
|
-
h('div', {
|
|
2912
|
+
// Drag Handle on Left Border (only when not maximized)
|
|
2913
|
+
!isMaximized ? h('div', {
|
|
2687
2914
|
className: 'dsh-preview-drag-handle',
|
|
2688
2915
|
onPointerDown: onResizePointerDown,
|
|
2689
2916
|
onPointerMove: onResizePointerMove,
|
|
2690
2917
|
onPointerUp: onResizePointerUp,
|
|
2691
2918
|
title: '左右拖拽调整预览面板宽度',
|
|
2692
|
-
}),
|
|
2919
|
+
}) : null,
|
|
2693
2920
|
|
|
2694
2921
|
// Drawer Header with FileTabs
|
|
2695
2922
|
h('div', { className: 'dsh-preview-drawer-header' },
|
|
@@ -2734,11 +2961,17 @@
|
|
|
2734
2961
|
onClick: closeAllTabs,
|
|
2735
2962
|
title: '关闭所有标签页',
|
|
2736
2963
|
}, '✕ 全部') : null,
|
|
2964
|
+
h('button', {
|
|
2965
|
+
type: 'button',
|
|
2966
|
+
className: 'dsh-win-ctrl-btn',
|
|
2967
|
+
onClick: () => setIsMaximized(!globalIsMaximized),
|
|
2968
|
+
title: isMaximized ? '还原为分栏并排模式' : '最大化窗口',
|
|
2969
|
+
}, isMaximized ? '🗗 还原' : '🗖 最大化'),
|
|
2737
2970
|
h('button', {
|
|
2738
2971
|
type: 'button',
|
|
2739
2972
|
className: 'dsh-win-ctrl-btn dsh-win-ctrl-close',
|
|
2740
2973
|
onClick: () => setPanelOpen(false),
|
|
2741
|
-
title: '
|
|
2974
|
+
title: '收起预览侧边栏 (Esc)',
|
|
2742
2975
|
}, '✕')
|
|
2743
2976
|
)
|
|
2744
2977
|
),
|
|
@@ -3041,33 +3274,251 @@
|
|
|
3041
3274
|
color: #2563eb;
|
|
3042
3275
|
}
|
|
3043
3276
|
|
|
3044
|
-
/*
|
|
3277
|
+
/* Side-by-Side Right Sidebar Layout Integration */
|
|
3278
|
+
body.dsh-preview-sidebar-active div[class*="centerCol"] {
|
|
3279
|
+
margin-right: var(--dsh-preview-width, 620px) !important;
|
|
3280
|
+
max-width: calc(100% - var(--dsh-preview-width, 620px)) !important;
|
|
3281
|
+
box-sizing: border-box !important;
|
|
3282
|
+
transition: margin-right 0.18s cubic-bezier(0.16, 1, 0.3, 1), max-width 0.18s cubic-bezier(0.16, 1, 0.3, 1) !important;
|
|
3283
|
+
}
|
|
3284
|
+
body.dsh-preview-sidebar-active.dsh-preview-maximized div[class*="centerCol"] {
|
|
3285
|
+
display: none !important;
|
|
3286
|
+
}
|
|
3287
|
+
body.dsh-preview-sidebar-active.dsh-preview-maximized .dsh-preview-drawer-container {
|
|
3288
|
+
width: 100vw !important;
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
/* Workspace Switcher Header & Popover */
|
|
3292
|
+
.dsh-tree-sidebar-title.dsh-tree-title-clickable {
|
|
3293
|
+
cursor: pointer;
|
|
3294
|
+
padding: 2px 6px;
|
|
3295
|
+
border-radius: 4px;
|
|
3296
|
+
transition: background 0.15s;
|
|
3297
|
+
gap: 4px;
|
|
3298
|
+
}
|
|
3299
|
+
.dsh-tree-sidebar-title.dsh-tree-title-clickable:hover {
|
|
3300
|
+
background: rgba(0, 0, 0, 0.06);
|
|
3301
|
+
}
|
|
3302
|
+
body[data-ds-dark-theme] .dsh-tree-sidebar-title.dsh-tree-title-clickable:hover {
|
|
3303
|
+
background: rgba(255, 255, 255, 0.08);
|
|
3304
|
+
}
|
|
3305
|
+
.dsh-tree-header-chevron {
|
|
3306
|
+
font-size: 8px;
|
|
3307
|
+
opacity: 0.6;
|
|
3308
|
+
margin-left: 2px;
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
.dsh-ws-selector-popover {
|
|
3312
|
+
position: absolute;
|
|
3313
|
+
top: 38px;
|
|
3314
|
+
left: 6px;
|
|
3315
|
+
right: 6px;
|
|
3316
|
+
max-width: 380px;
|
|
3317
|
+
z-index: 1000;
|
|
3318
|
+
background: var(--dsw-alias-bg-layer-1, #ffffff);
|
|
3319
|
+
border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
|
|
3320
|
+
border-radius: 8px;
|
|
3321
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
|
|
3322
|
+
padding: 10px;
|
|
3323
|
+
display: flex;
|
|
3324
|
+
flex-direction: column;
|
|
3325
|
+
gap: 8px;
|
|
3326
|
+
box-sizing: border-box;
|
|
3327
|
+
animation: dshPopoverFadeIn 0.15s ease;
|
|
3328
|
+
}
|
|
3329
|
+
body[data-ds-dark-theme] .dsh-ws-selector-popover {
|
|
3330
|
+
background: var(--dsw-alias-bg-layer-2, #1b202c);
|
|
3331
|
+
border-color: var(--dsw-alias-border-l2, #334155);
|
|
3332
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
|
|
3333
|
+
}
|
|
3334
|
+
@keyframes dshPopoverFadeIn {
|
|
3335
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
3336
|
+
to { opacity: 1; transform: translateY(0); }
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
.dsh-ws-popover-header {
|
|
3340
|
+
display: flex;
|
|
3341
|
+
align-items: center;
|
|
3342
|
+
justify-content: space-between;
|
|
3343
|
+
padding-bottom: 6px;
|
|
3344
|
+
border-bottom: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
|
|
3345
|
+
color: var(--dsw-alias-label-primary, #0f172a);
|
|
3346
|
+
}
|
|
3347
|
+
body[data-ds-dark-theme] .dsh-ws-popover-header {
|
|
3348
|
+
border-bottom-color: var(--dsw-alias-border-l3, #2d3748);
|
|
3349
|
+
color: #f1f5f9;
|
|
3350
|
+
}
|
|
3351
|
+
.dsh-ws-popover-close {
|
|
3352
|
+
background: none;
|
|
3353
|
+
border: none;
|
|
3354
|
+
color: var(--dsw-alias-label-secondary, #64748b);
|
|
3355
|
+
cursor: pointer;
|
|
3356
|
+
font-size: 11px;
|
|
3357
|
+
padding: 2px 4px;
|
|
3358
|
+
border-radius: 3px;
|
|
3359
|
+
}
|
|
3360
|
+
.dsh-ws-popover-close:hover {
|
|
3361
|
+
background: rgba(0, 0, 0, 0.06);
|
|
3362
|
+
color: var(--dsw-alias-label-primary, #0f172a);
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
.dsh-ws-list {
|
|
3366
|
+
display: flex;
|
|
3367
|
+
flex-direction: column;
|
|
3368
|
+
gap: 3px;
|
|
3369
|
+
max-height: 180px;
|
|
3370
|
+
overflow-y: auto;
|
|
3371
|
+
padding: 2px 0;
|
|
3372
|
+
}
|
|
3373
|
+
.dsh-ws-item {
|
|
3374
|
+
display: flex;
|
|
3375
|
+
align-items: center;
|
|
3376
|
+
justify-content: space-between;
|
|
3377
|
+
padding: 6px 8px;
|
|
3378
|
+
border-radius: 6px;
|
|
3379
|
+
cursor: pointer;
|
|
3380
|
+
transition: background 0.12s;
|
|
3381
|
+
font-size: 12px;
|
|
3382
|
+
}
|
|
3383
|
+
.dsh-ws-item:hover {
|
|
3384
|
+
background: var(--dsw-alias-bg-hover, rgba(0, 0, 0, 0.05));
|
|
3385
|
+
}
|
|
3386
|
+
body[data-ds-dark-theme] .dsh-ws-item:hover {
|
|
3387
|
+
background: rgba(255, 255, 255, 0.06);
|
|
3388
|
+
}
|
|
3389
|
+
.dsh-ws-item.active {
|
|
3390
|
+
background: rgba(14, 165, 233, 0.12);
|
|
3391
|
+
color: #0284c7;
|
|
3392
|
+
font-weight: 600;
|
|
3393
|
+
}
|
|
3394
|
+
body[data-ds-dark-theme] .dsh-ws-item.active {
|
|
3395
|
+
background: rgba(56, 189, 248, 0.16);
|
|
3396
|
+
color: #38bdf8;
|
|
3397
|
+
}
|
|
3398
|
+
.dsh-ws-item-info {
|
|
3399
|
+
display: flex;
|
|
3400
|
+
flex-direction: column;
|
|
3401
|
+
gap: 1px;
|
|
3402
|
+
overflow: hidden;
|
|
3403
|
+
flex: 1;
|
|
3404
|
+
}
|
|
3405
|
+
.dsh-ws-item-name {
|
|
3406
|
+
font-size: 12px;
|
|
3407
|
+
font-weight: 600;
|
|
3408
|
+
overflow: hidden;
|
|
3409
|
+
text-overflow: ellipsis;
|
|
3410
|
+
white-space: nowrap;
|
|
3411
|
+
}
|
|
3412
|
+
.dsh-ws-item-path {
|
|
3413
|
+
font-size: 10px;
|
|
3414
|
+
color: var(--dsw-alias-label-tertiary, #94a3b8);
|
|
3415
|
+
overflow: hidden;
|
|
3416
|
+
text-overflow: ellipsis;
|
|
3417
|
+
white-space: nowrap;
|
|
3418
|
+
}
|
|
3419
|
+
.dsh-ws-item-check {
|
|
3420
|
+
color: #0284c7;
|
|
3421
|
+
font-weight: bold;
|
|
3422
|
+
margin-left: 6px;
|
|
3423
|
+
}
|
|
3424
|
+
.dsh-ws-empty {
|
|
3425
|
+
padding: 12px;
|
|
3426
|
+
text-align: center;
|
|
3427
|
+
font-size: 11px;
|
|
3428
|
+
color: var(--dsw-alias-label-secondary, #64748b);
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
.dsh-ws-custom-form {
|
|
3432
|
+
margin-top: 4px;
|
|
3433
|
+
padding-top: 6px;
|
|
3434
|
+
border-top: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
|
|
3435
|
+
}
|
|
3436
|
+
body[data-ds-dark-theme] .dsh-ws-custom-form {
|
|
3437
|
+
border-top-color: var(--dsw-alias-border-l3, #2d3748);
|
|
3438
|
+
}
|
|
3439
|
+
.dsh-ws-input-row {
|
|
3440
|
+
display: flex;
|
|
3441
|
+
gap: 6px;
|
|
3442
|
+
align-items: center;
|
|
3443
|
+
}
|
|
3444
|
+
.dsh-ws-input {
|
|
3445
|
+
flex: 1;
|
|
3446
|
+
height: 26px;
|
|
3447
|
+
border-radius: 4px;
|
|
3448
|
+
border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
|
|
3449
|
+
padding: 0 8px;
|
|
3450
|
+
font-size: 11px;
|
|
3451
|
+
background: var(--dsw-alias-bg-base, #ffffff);
|
|
3452
|
+
color: inherit;
|
|
3453
|
+
outline: none;
|
|
3454
|
+
}
|
|
3455
|
+
body[data-ds-dark-theme] .dsh-ws-input {
|
|
3456
|
+
background: #141720;
|
|
3457
|
+
border-color: #334155;
|
|
3458
|
+
color: #f1f5f9;
|
|
3459
|
+
}
|
|
3460
|
+
.dsh-ws-btn {
|
|
3461
|
+
height: 26px;
|
|
3462
|
+
padding: 0 10px;
|
|
3463
|
+
border-radius: 4px;
|
|
3464
|
+
border: none;
|
|
3465
|
+
background: var(--dsw-static-deepseek-500, #4176e6);
|
|
3466
|
+
color: #ffffff;
|
|
3467
|
+
font-size: 11px;
|
|
3468
|
+
font-weight: 500;
|
|
3469
|
+
cursor: pointer;
|
|
3470
|
+
white-space: nowrap;
|
|
3471
|
+
}
|
|
3472
|
+
.dsh-ws-btn:hover {
|
|
3473
|
+
opacity: 0.9;
|
|
3474
|
+
}
|
|
3475
|
+
.dsh-ws-btn:disabled {
|
|
3476
|
+
opacity: 0.5;
|
|
3477
|
+
cursor: not-allowed;
|
|
3478
|
+
}
|
|
3479
|
+
.dsh-ws-reset-btn {
|
|
3480
|
+
font-size: 11px;
|
|
3481
|
+
color: var(--dsw-static-deepseek-500, #4176e6);
|
|
3482
|
+
background: none;
|
|
3483
|
+
border: none;
|
|
3484
|
+
cursor: pointer;
|
|
3485
|
+
padding: 4px 0 0;
|
|
3486
|
+
text-align: left;
|
|
3487
|
+
text-decoration: underline;
|
|
3488
|
+
}
|
|
3489
|
+
body[data-ds-dark-theme] .dsh-ws-reset-btn {
|
|
3490
|
+
color: #60a5fa;
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
/* Right-Side Persistent / Split Sidebar Container */
|
|
3045
3494
|
.dsh-preview-drawer-container {
|
|
3046
3495
|
position: fixed;
|
|
3047
3496
|
top: 0;
|
|
3048
3497
|
right: 0;
|
|
3049
3498
|
bottom: 0;
|
|
3499
|
+
width: var(--dsh-preview-width, 620px);
|
|
3050
3500
|
height: 100vh;
|
|
3051
|
-
z-index:
|
|
3501
|
+
z-index: 990;
|
|
3052
3502
|
background: var(--dsw-alias-bg-layer-1, #ffffff);
|
|
3053
3503
|
color: var(--dsw-alias-label-primary, #0f172a);
|
|
3054
3504
|
border-left: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
|
|
3055
|
-
box-shadow: -
|
|
3505
|
+
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.08);
|
|
3056
3506
|
display: flex;
|
|
3057
3507
|
flex-direction: column;
|
|
3058
3508
|
box-sizing: border-box;
|
|
3059
3509
|
overflow: hidden;
|
|
3510
|
+
transition: width 0.18s cubic-bezier(0.16, 1, 0.3, 1);
|
|
3060
3511
|
animation: dshPreviewSlideIn 0.24s cubic-bezier(0.16, 1, 0.3, 1);
|
|
3061
3512
|
}
|
|
3062
3513
|
body[data-ds-dark-theme] .dsh-preview-drawer-container {
|
|
3063
3514
|
background: var(--dsw-alias-bg-layer-1, #161922);
|
|
3064
3515
|
color: var(--dsw-alias-label-primary, #e2e8f0);
|
|
3065
3516
|
border-left-color: var(--dsw-alias-border-l3, #2d3748);
|
|
3066
|
-
box-shadow: -
|
|
3517
|
+
box-shadow: -6px 0 20px rgba(0, 0, 0, 0.35);
|
|
3067
3518
|
}
|
|
3068
3519
|
.dsh-preview-drawer-container.dragging {
|
|
3069
3520
|
user-select: none;
|
|
3070
|
-
transition: none;
|
|
3521
|
+
transition: none !important;
|
|
3071
3522
|
}
|
|
3072
3523
|
@keyframes dshPreviewSlideIn {
|
|
3073
3524
|
from { transform: translateX(100%); }
|
package/lib/index.js
CHANGED
|
@@ -354,6 +354,42 @@ export class PreviewService extends Service {
|
|
|
354
354
|
res.end(JSON.stringify({ ok: true }));
|
|
355
355
|
return;
|
|
356
356
|
}
|
|
357
|
+
if (pathname === '/api/preview/workspaces' && req.method === 'GET') {
|
|
358
|
+
const currentCwd = process.cwd();
|
|
359
|
+
const workspaces = [
|
|
360
|
+
{
|
|
361
|
+
id: 'cwd',
|
|
362
|
+
path: currentCwd,
|
|
363
|
+
name: path.basename(currentCwd) || currentCwd,
|
|
364
|
+
isCurrent: true,
|
|
365
|
+
}
|
|
366
|
+
];
|
|
367
|
+
res.statusCode = 200;
|
|
368
|
+
res.end(JSON.stringify({ ok: true, workspaces }));
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (pathname === '/api/preview/validate-dir' && req.method === 'POST') {
|
|
372
|
+
const body = await this.parseRequestBody(req);
|
|
373
|
+
const targetPath = body.path;
|
|
374
|
+
if (!targetPath) {
|
|
375
|
+
res.statusCode = 400;
|
|
376
|
+
res.end(JSON.stringify({ ok: false, error: { code: 'MISSING_PARAM', message: '缺少 path 参数' } }));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const normalized = path.resolve(targetPath);
|
|
380
|
+
if (fs.existsSync(normalized) && fs.statSync(normalized).isDirectory()) {
|
|
381
|
+
res.statusCode = 200;
|
|
382
|
+
res.end(JSON.stringify({
|
|
383
|
+
ok: true,
|
|
384
|
+
path: normalized,
|
|
385
|
+
name: path.basename(normalized) || normalized,
|
|
386
|
+
}));
|
|
387
|
+
} else {
|
|
388
|
+
res.statusCode = 404;
|
|
389
|
+
res.end(JSON.stringify({ ok: false, error: { code: 'NOT_FOUND', message: '指定目录不存在或不是文件夹' } }));
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
357
393
|
res.statusCode = 404;
|
|
358
394
|
res.end(JSON.stringify({ ok: false, error: { code: 'NOT_FOUND', message: '未找到路由: ' + pathname } }));
|
|
359
395
|
} catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rooode/dsh-plugin-preview",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "DeepSeek Harness Markdown 文档与工作空间文件浏览器右侧预览插件 (支持工作区文件树、JSON 交互结构树/格式化、Java/C++/Python/JS/TS/YAML/Go/Rust 多语言语法高亮与符号大纲、自动换行与多标签 FileTabs)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|