@rooode/dsh-plugin-preview 0.1.8 → 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.
Files changed (3) hide show
  1. package/lib/client.js +528 -77
  2. package/lib/index.js +36 -0
  3. package/package.json +3 -2
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 DEFAULT_PANEL_WIDTH = 760;
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 getCurrentWorkspaceRoot() {
224
- if (clientCtx && clientCtx.workspaces) {
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
- if (clientCtx.workspaces.current?.directory) {
227
- return clientCtx.workspaces.current.directory;
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
- const snapshot = clientCtx.workspaces.list?.getSnapshot();
230
- if (snapshot?.items?.length > 0) {
231
- return snapshot.items[0].directory || snapshot.items[0].path || '';
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 snapshot = clientCtx.sessions.list?.getSnapshot();
238
- const currentSessionId = snapshot?.current;
239
- if (currentSessionId && snapshot?.byId?.[currentSessionId]?.cwd) {
240
- return snapshot.byId[currentSessionId].cwd;
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
- if (typeof location !== 'undefined' && location.hash) {
245
- const m = location.hash.match(/workspace=([^&]+)/);
246
- if (m) return decodeURIComponent(m[1]);
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', { className: 'dsh-tree-sidebar-title', title: treeData?.workspaceRoot || currentWs },
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: '收起预览面板 (Esc)',
2974
+ title: '收起预览侧边栏 (Esc)',
2742
2975
  }, '✕')
2743
2976
  )
2744
2977
  ),
@@ -3041,33 +3274,251 @@
3041
3274
  color: #2563eb;
3042
3275
  }
3043
3276
 
3044
- /* Right-Side Drawer Container */
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: 9995;
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: -8px 0 32px rgba(0, 0, 0, 0.14);
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: -12px 0 40px rgba(0, 0, 0, 0.45);
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.8",
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",
@@ -18,11 +18,12 @@
18
18
  "dev": "node scripts/build.js --watch",
19
19
  "typecheck": "tsc --noEmit",
20
20
  "package": "node scripts/build.js && npm pack",
21
- "prepublishOnly": "npm run build"
21
+ "prepublishOnly": "node scripts/build.js"
22
22
  },
23
23
  "keywords": [
24
24
  "deepseek-harness",
25
25
  "dsh",
26
+ "dsh-plugin",
26
27
  "cordis-plugin",
27
28
  "preview",
28
29
  "markdown-preview",