@rooode/dsh-plugin-preview 0.1.9 → 0.1.11

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 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
+ }
349
+ }
350
+ } catch (e) {}
351
+ }
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
+ }
241
374
  }
242
375
  } catch (e) {}
243
376
  }
244
- if (typeof location !== 'undefined' && location.hash) {
245
- const m = location.hash.match(/workspace=([^&]+)/);
246
- if (m) return decodeURIComponent(m[1]);
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
 
@@ -352,6 +494,97 @@
352
494
  }
353
495
  }
354
496
 
497
+ function renameOpenTabPaths(oldPath, newPath, isDir = false) {
498
+ if (!oldPath || !newPath) return;
499
+ let changed = false;
500
+ const oldNorm = oldPath.replace(/\\/g, '/').toLowerCase();
501
+ const newNorm = newPath.replace(/\\/g, '/');
502
+
503
+ globalTabs = globalTabs.map(tab => {
504
+ const tabNorm = tab.filePath.replace(/\\/g, '/').toLowerCase();
505
+ if (!isDir) {
506
+ if (tabNorm === oldNorm) {
507
+ changed = true;
508
+ const newFileName = getFileName(newPath);
509
+ const newExt = getFileExtension(newPath);
510
+ const newCat = getFileTypeCategory(newPath);
511
+ return {
512
+ ...tab,
513
+ id: 'tab:' + newPath.toLowerCase(),
514
+ filePath: newPath,
515
+ title: newFileName,
516
+ extension: newExt,
517
+ category: newCat,
518
+ };
519
+ }
520
+ } else {
521
+ if (tabNorm === oldNorm || tabNorm.startsWith(oldNorm + '/')) {
522
+ changed = true;
523
+ const subPath = tab.filePath.replace(/\\/g, '/').slice(oldNorm.length);
524
+ const updatedFilePath = newNorm + subPath;
525
+ const newFileName = getFileName(updatedFilePath);
526
+ const newExt = getFileExtension(updatedFilePath);
527
+ const newCat = getFileTypeCategory(updatedFilePath);
528
+ return {
529
+ ...tab,
530
+ id: 'tab:' + updatedFilePath.toLowerCase(),
531
+ filePath: updatedFilePath,
532
+ title: newFileName,
533
+ extension: newExt,
534
+ category: newCat,
535
+ };
536
+ }
537
+ }
538
+ return tab;
539
+ });
540
+
541
+ if (changed) {
542
+ if (globalActiveTabId) {
543
+ const activeNorm = globalActiveTabId.toLowerCase();
544
+ const targetPrefix = 'tab:' + oldNorm;
545
+ if (activeNorm === targetPrefix) {
546
+ globalActiveTabId = 'tab:' + newPath.toLowerCase();
547
+ } else if (isDir && activeNorm.startsWith(targetPrefix + '/')) {
548
+ const sub = activeNorm.slice(targetPrefix.length);
549
+ globalActiveTabId = 'tab:' + newNorm.toLowerCase() + sub;
550
+ }
551
+ }
552
+ saveTabsToStorage();
553
+ notifyStateChange();
554
+ }
555
+ }
556
+
557
+ function removeOpenTabsForPath(targetPath, isDir = false) {
558
+ if (!targetPath) return;
559
+ let changed = false;
560
+ const delNorm = targetPath.replace(/\\/g, '/').toLowerCase();
561
+ const nextTabs = globalTabs.filter(tab => {
562
+ const tabNorm = tab.filePath.replace(/\\/g, '/').toLowerCase();
563
+ if (!isDir) {
564
+ if (tabNorm === delNorm) {
565
+ changed = true;
566
+ return false;
567
+ }
568
+ } else {
569
+ if (tabNorm === delNorm || tabNorm.startsWith(delNorm + '/')) {
570
+ changed = true;
571
+ return false;
572
+ }
573
+ }
574
+ return true;
575
+ });
576
+
577
+ if (changed) {
578
+ globalTabs = nextTabs;
579
+ if (!globalTabs.some(t => t.id === globalActiveTabId)) {
580
+ globalActiveTabId = globalTabs.length > 0 ? globalTabs[0].id : null;
581
+ }
582
+ saveTabsToStorage();
583
+ notifyStateChange();
584
+ }
585
+ }
586
+
587
+
355
588
  // =========================================================================
356
589
  // Toast Notification Utility
357
590
  // =========================================================================
@@ -807,12 +1040,262 @@
807
1040
  }
808
1041
 
809
1042
  // =========================================================================
810
- // React Components: Workspace File Tree Explorer
1043
+ // React Components: Workspace File Tree Explorer (with DnD, Create & Move)
811
1044
  // =========================================================================
812
- function FileTreeNodeItem({ node, depth = 0, expandedPaths, onToggleExpand, activeFilePath, onOpenFile, searchQuery }) {
1045
+
1046
+ function isValidDropTarget(draggedItem, targetNode) {
1047
+ if (!draggedItem || !targetNode) return false;
1048
+ if (!targetNode.isDir) return false;
1049
+ const src = draggedItem.path.replace(/\\/g, '/').toLowerCase();
1050
+ const dest = targetNode.path.replace(/\\/g, '/').toLowerCase();
1051
+ // Cannot drop on itself
1052
+ if (src === dest) return false;
1053
+ // Cannot drop into current parent directory (already there)
1054
+ const lastSlash = src.lastIndexOf('/');
1055
+ const srcParent = lastSlash > 0 ? src.substring(0, lastSlash) : '';
1056
+ if (srcParent && srcParent === dest) return false;
1057
+ // If dragged is a dir, cannot drop into its own children or itself
1058
+ if (draggedItem.isDir) {
1059
+ if (dest.startsWith(src + '/')) return false;
1060
+ }
1061
+ return true;
1062
+ }
1063
+
1064
+ function InlineCreateInputNode({ depth = 0, type, onConfirm, onCancel }) {
1065
+ const [val, setVal] = useState('');
1066
+ const inputRef = useRef(null);
1067
+
1068
+ useEffect(() => {
1069
+ if (inputRef.current) {
1070
+ inputRef.current.focus();
1071
+ inputRef.current.select();
1072
+ }
1073
+ }, []);
1074
+
1075
+ const handleKeyDown = (e) => {
1076
+ if (e.key === 'Enter') {
1077
+ e.preventDefault();
1078
+ e.stopPropagation();
1079
+ if (val.trim()) onConfirm(val.trim());
1080
+ else onCancel();
1081
+ } else if (e.key === 'Escape') {
1082
+ e.preventDefault();
1083
+ e.stopPropagation();
1084
+ onCancel();
1085
+ }
1086
+ };
1087
+
1088
+ const handleBlur = () => {
1089
+ if (!val.trim()) onCancel();
1090
+ };
1091
+
1092
+ return h('div', {
1093
+ className: 'dsh-tree-inline-input-row',
1094
+ style: { paddingLeft: `${depth * 14 + 6}px` },
1095
+ onClick: (e) => e.stopPropagation(),
1096
+ },
1097
+ h('span', { className: 'dsh-tree-chevron-placeholder' }),
1098
+ h('span', {
1099
+ className: 'dsh-tree-icon',
1100
+ style: { color: type === 'dir' ? '#eab308' : '#38bdf8' },
1101
+ }, type === 'dir' ? '📁' : '📄'),
1102
+ h('input', {
1103
+ ref: inputRef,
1104
+ type: 'text',
1105
+ className: 'dsh-tree-inline-input',
1106
+ value: val,
1107
+ placeholder: type === 'dir' ? '新文件夹名称...' : '新文件名 (如 doc.md)...',
1108
+ onChange: (e) => setVal(e.target.value),
1109
+ onKeyDown: handleKeyDown,
1110
+ onBlur: handleBlur,
1111
+ })
1112
+ );
1113
+ }
1114
+
1115
+ function InlineRenameInputNode({ node, depth = 0, onConfirm, onCancel }) {
1116
+ const [val, setVal] = useState(node.name);
1117
+ const inputRef = useRef(null);
1118
+
1119
+ useEffect(() => {
1120
+ if (inputRef.current) {
1121
+ inputRef.current.focus();
1122
+ if (!node.isDir && node.name.includes('.')) {
1123
+ const lastDot = node.name.lastIndexOf('.');
1124
+ if (lastDot > 0) {
1125
+ inputRef.current.setSelectionRange(0, lastDot);
1126
+ return;
1127
+ }
1128
+ }
1129
+ inputRef.current.select();
1130
+ }
1131
+ }, [node]);
1132
+
1133
+ const handleKeyDown = (e) => {
1134
+ if (e.key === 'Enter') {
1135
+ e.preventDefault();
1136
+ e.stopPropagation();
1137
+ if (val.trim() && val.trim() !== node.name) {
1138
+ onConfirm(val.trim());
1139
+ } else {
1140
+ onCancel();
1141
+ }
1142
+ } else if (e.key === 'Escape') {
1143
+ e.preventDefault();
1144
+ e.stopPropagation();
1145
+ onCancel();
1146
+ }
1147
+ };
1148
+
1149
+ const handleBlur = () => {
1150
+ onCancel();
1151
+ };
1152
+
1153
+ const iconInfo = getFileIcon(node);
1154
+
1155
+ return h('div', {
1156
+ className: 'dsh-tree-inline-input-row',
1157
+ style: { paddingLeft: `${depth * 14 + 6}px` },
1158
+ onClick: (e) => e.stopPropagation(),
1159
+ },
1160
+ h('span', { className: 'dsh-tree-chevron-placeholder' }),
1161
+ h('span', {
1162
+ className: 'dsh-tree-icon',
1163
+ style: { color: node.isDir ? '#eab308' : iconInfo.color },
1164
+ }, node.isDir ? '📁' : iconInfo.icon),
1165
+ h('input', {
1166
+ ref: inputRef,
1167
+ type: 'text',
1168
+ className: 'dsh-tree-inline-input',
1169
+ value: val,
1170
+ onChange: (e) => setVal(e.target.value),
1171
+ onKeyDown: handleKeyDown,
1172
+ onBlur: handleBlur,
1173
+ })
1174
+ );
1175
+ }
1176
+
1177
+ function TreeContextMenu({ x, y, node, isRoot, clipboard, onAction, onClose }) {
1178
+ const menuRef = useRef(null);
1179
+ useEffect(() => {
1180
+ const handleDown = (e) => {
1181
+ if (menuRef.current && !menuRef.current.contains(e.target)) {
1182
+ onClose();
1183
+ }
1184
+ };
1185
+ window.addEventListener('mousedown', handleDown, true);
1186
+ window.addEventListener('scroll', onClose, true);
1187
+ return () => {
1188
+ window.removeEventListener('mousedown', handleDown, true);
1189
+ window.removeEventListener('scroll', onClose, true);
1190
+ };
1191
+ }, [onClose]);
1192
+
1193
+ const style = {
1194
+ position: 'fixed',
1195
+ top: `${Math.min(Math.max(10, y), window.innerHeight - 300)}px`,
1196
+ left: `${Math.min(Math.max(10, x), window.innerWidth - 210)}px`,
1197
+ zIndex: 99999,
1198
+ };
1199
+
1200
+ const isDir = isRoot || (node && node.isDir);
1201
+
1202
+ return h('div', { ref: menuRef, className: 'dsh-tree-context-menu', style },
1203
+ isDir ? [
1204
+ h('div', { key: 'new-file', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('new-file', node); } },
1205
+ h('span', { className: 'dsh-menu-icon' }, '📄'),
1206
+ h('span', null, '新建文件')
1207
+ ),
1208
+ h('div', { key: 'new-dir', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('new-dir', node); } },
1209
+ h('span', { className: 'dsh-menu-icon' }, '📁'),
1210
+ h('span', null, '新建文件夹')
1211
+ ),
1212
+ h('div', { key: 'div-1', className: 'dsh-menu-divider' }),
1213
+ ] : null,
1214
+
1215
+ !isRoot && !isDir ? h('div', { key: 'preview', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('preview', node); } },
1216
+ h('span', { className: 'dsh-menu-icon' }, '📖'),
1217
+ h('span', null, '打开预览')
1218
+ ) : null,
1219
+
1220
+ !isRoot ? h('div', { key: 'cut', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('cut', node); } },
1221
+ h('span', { className: 'dsh-menu-icon' }, '✂️'),
1222
+ h('span', null, '剪切')
1223
+ ) : null,
1224
+
1225
+ isDir && clipboard && clipboard.action === 'cut' ? h('div', {
1226
+ key: 'paste',
1227
+ className: 'dsh-menu-item',
1228
+ onClick: () => { onClose(); onAction('paste', node); }
1229
+ },
1230
+ h('span', { className: 'dsh-menu-icon' }, '📋'),
1231
+ h('span', null, `粘贴 "${clipboard.name}" 到此处`)
1232
+ ) : null,
1233
+
1234
+ !isRoot ? h('div', { key: 'rename', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('rename', node); } },
1235
+ h('span', { className: 'dsh-menu-icon' }, '🏷️'),
1236
+ h('span', null, '重命名')
1237
+ ) : null,
1238
+
1239
+ !isRoot ? h('div', { key: 'div-2', className: 'dsh-menu-divider' }) : null,
1240
+
1241
+ !isRoot ? h('div', { key: 'reveal', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('reveal', node); } },
1242
+ h('span', { className: 'dsh-menu-icon' }, '📂'),
1243
+ h('span', null, '在文件管理器中定位')
1244
+ ) : null,
1245
+
1246
+ !isRoot && !isDir ? h('div', { key: 'native', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('native', node); } },
1247
+ h('span', { className: 'dsh-menu-icon' }, '🌐'),
1248
+ h('span', null, '系统默认应用打开')
1249
+ ) : null,
1250
+
1251
+ !isRoot ? [
1252
+ h('div', { key: 'div-3', className: 'dsh-menu-divider' }),
1253
+ h('div', { key: 'delete', className: 'dsh-menu-item dsh-menu-item-danger', onClick: () => { onClose(); onAction('delete', node); } },
1254
+ h('span', { className: 'dsh-menu-icon' }, '🗑️'),
1255
+ h('span', null, '删除')
1256
+ )
1257
+ ] : null,
1258
+
1259
+ isRoot ? [
1260
+ h('div', { key: 'refresh', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('refresh'); } },
1261
+ h('span', { className: 'dsh-menu-icon' }, '🔄'),
1262
+ h('span', null, '刷新目录树')
1263
+ )
1264
+ ] : null
1265
+ );
1266
+ }
1267
+
1268
+ function FileTreeNodeItem({
1269
+ node,
1270
+ depth = 0,
1271
+ expandedPaths,
1272
+ onToggleExpand,
1273
+ activeFilePath,
1274
+ onOpenFile,
1275
+ searchQuery,
1276
+ creatingItem,
1277
+ onConfirmCreate,
1278
+ onCancelCreate,
1279
+ renamingPath,
1280
+ onConfirmRename,
1281
+ onCancelRename,
1282
+ onContextMenu,
1283
+ draggedNode,
1284
+ dragOverNode,
1285
+ onDragStart,
1286
+ onDragEnd,
1287
+ onDragOver,
1288
+ onDragLeave,
1289
+ onDrop,
1290
+ clipboard,
1291
+ }) {
813
1292
  const isDir = node.isDir;
814
1293
  const isExpanded = expandedPaths.has(node.path);
815
1294
  const isActive = !isDir && activeFilePath && activeFilePath.toLowerCase() === node.path.toLowerCase();
1295
+ const isRenaming = renamingPath && renamingPath.toLowerCase() === node.path.toLowerCase();
1296
+ const isDragged = draggedNode && draggedNode.path && draggedNode.path.toLowerCase() === node.path.toLowerCase();
1297
+ const isDropTarget = isDir && dragOverNode && dragOverNode.path && dragOverNode.path.toLowerCase() === node.path.toLowerCase();
1298
+ const isCut = clipboard && clipboard.action === 'cut' && clipboard.path && clipboard.path.toLowerCase() === node.path.toLowerCase();
816
1299
  const iconInfo = getFileIcon(node);
817
1300
 
818
1301
  const indentStyle = { paddingLeft: `${depth * 14 + 6}px` };
@@ -826,16 +1309,70 @@
826
1309
  }
827
1310
  };
828
1311
 
1312
+ const handleContextMenu = (e) => {
1313
+ e.preventDefault();
1314
+ e.stopPropagation();
1315
+ onContextMenu(node, e);
1316
+ };
1317
+
1318
+ const handleDragStart = (e) => {
1319
+ e.stopPropagation();
1320
+ e.dataTransfer.setData('text/plain', node.path);
1321
+ e.dataTransfer.effectAllowed = 'move';
1322
+ onDragStart(node);
1323
+ };
1324
+
1325
+ const handleDragOver = (e) => {
1326
+ if (isDir && isValidDropTarget(draggedNode, node)) {
1327
+ e.preventDefault();
1328
+ e.stopPropagation();
1329
+ e.dataTransfer.dropEffect = 'move';
1330
+ onDragOver(node);
1331
+ }
1332
+ };
1333
+
1334
+ const handleDragLeave = (e) => {
1335
+ if (isDir) {
1336
+ e.stopPropagation();
1337
+ onDragLeave(node);
1338
+ }
1339
+ };
1340
+
1341
+ const handleDrop = (e) => {
1342
+ if (isDir && isValidDropTarget(draggedNode, node)) {
1343
+ e.preventDefault();
1344
+ e.stopPropagation();
1345
+ onDrop(draggedNode, node);
1346
+ }
1347
+ };
1348
+
1349
+ if (isRenaming) {
1350
+ return h(InlineRenameInputNode, {
1351
+ node,
1352
+ depth,
1353
+ onConfirm: (newName) => onConfirmRename(node, newName),
1354
+ onCancel: onCancelRename,
1355
+ });
1356
+ }
1357
+
829
1358
  return h(Fragment, null,
830
1359
  h('div', {
831
- className: `dsh-tree-node-row ${isDir ? 'is-dir' : 'is-file'} ${isActive ? 'active' : ''}`,
1360
+ className: `dsh-tree-node-row ${isDir ? 'is-dir' : 'is-file'} ${isActive ? 'active' : ''} ${isDropTarget ? 'drop-target' : ''} ${isDragged ? 'is-dragged' : ''} ${isCut ? 'is-cut' : ''}`,
832
1361
  style: indentStyle,
833
1362
  onClick: handleClick,
1363
+ onContextMenu: handleContextMenu,
1364
+ draggable: true,
1365
+ onDragStart: handleDragStart,
1366
+ onDragEnd: onDragEnd,
1367
+ onDragOver: handleDragOver,
1368
+ onDragLeave: handleDragLeave,
1369
+ onDrop: handleDrop,
834
1370
  title: `${node.name}\n${node.path}${node.size ? ' (' + formatBytes(node.size) + ')' : ''}`,
835
1371
  },
836
1372
  // Chevron indicator for directory
837
1373
  isDir ? h('span', {
838
1374
  className: `dsh-tree-chevron ${isExpanded ? 'expanded' : ''}`,
1375
+ onClick: (e) => { e.stopPropagation(); onToggleExpand(node.path); },
839
1376
  }, isExpanded ? '▼' : '▶') : h('span', { className: 'dsh-tree-chevron-placeholder' }),
840
1377
 
841
1378
  // File / Directory Icon
@@ -853,18 +1390,47 @@
853
1390
  )
854
1391
  ),
855
1392
 
856
- // Children if expanded
857
- isDir && isExpanded && node.children && node.children.length > 0 ? (
858
- node.children.map(child => h(FileTreeNodeItem, {
859
- key: child.path,
860
- node: child,
861
- depth: depth + 1,
862
- expandedPaths,
863
- onToggleExpand,
864
- activeFilePath,
865
- onOpenFile,
866
- searchQuery,
867
- }))
1393
+ // Children or Inline creation node if expanded
1394
+ isDir && isExpanded ? (
1395
+ h(Fragment, null,
1396
+ // If creating inside this directory, render inline creation node first
1397
+ creatingItem && creatingItem.parentPath && creatingItem.parentPath.replace(/\\/g, '/').toLowerCase() === node.path.replace(/\\/g, '/').toLowerCase() ? (
1398
+ h(InlineCreateInputNode, {
1399
+ depth: depth + 1,
1400
+ type: creatingItem.type,
1401
+ onConfirm: onConfirmCreate,
1402
+ onCancel: onCancelCreate,
1403
+ })
1404
+ ) : null,
1405
+
1406
+ node.children && node.children.length > 0 ? (
1407
+ node.children.map(child => h(FileTreeNodeItem, {
1408
+ key: child.path,
1409
+ node: child,
1410
+ depth: depth + 1,
1411
+ expandedPaths,
1412
+ onToggleExpand,
1413
+ activeFilePath,
1414
+ onOpenFile,
1415
+ searchQuery,
1416
+ creatingItem,
1417
+ onConfirmCreate,
1418
+ onCancelCreate,
1419
+ renamingPath,
1420
+ onConfirmRename,
1421
+ onCancelRename,
1422
+ onContextMenu,
1423
+ draggedNode,
1424
+ dragOverNode,
1425
+ onDragStart,
1426
+ onDragEnd,
1427
+ onDragOver,
1428
+ onDragLeave,
1429
+ onDrop,
1430
+ clipboard,
1431
+ }))
1432
+ ) : null
1433
+ )
868
1434
  ) : null
869
1435
  );
870
1436
  }
@@ -915,6 +1481,18 @@
915
1481
  const [error, setError] = useState(null);
916
1482
  const [searchQuery, setSearchQuery] = useState('');
917
1483
  const [expandedPaths, setExpandedPaths] = useState(new Set());
1484
+ const [isSelectorOpen, setIsSelectorOpen] = useState(false);
1485
+ const [customPathInput, setCustomPathInput] = useState('');
1486
+ const [validating, setValidating] = useState(false);
1487
+
1488
+ // File operations state
1489
+ const [creatingItem, setCreatingItem] = useState(null); // { parentPath: string, type: 'dir' | 'file' } | null
1490
+ const [renamingPath, setRenamingPath] = useState(null); // string | null
1491
+ const [contextMenu, setContextMenu] = useState(null); // { x: number, y: number, node: any, isRoot: boolean } | null
1492
+ const [draggedNode, setDraggedNode] = useState(null);
1493
+ const [dragOverNode, setDragOverNode] = useState(null);
1494
+ const [clipboard, setClipboard] = useState(null); // { action: 'cut', path: string, name: string, isDir: boolean } | null
1495
+
918
1496
  const currentWs = getCurrentWorkspaceRoot();
919
1497
 
920
1498
  const loadTree = useCallback(async (force = false) => {
@@ -942,7 +1520,7 @@
942
1520
 
943
1521
  useEffect(() => {
944
1522
  loadTree();
945
- }, [loadTree]);
1523
+ }, [loadTree, globalManualWorkspace]);
946
1524
 
947
1525
  const handleToggleExpand = (dirPath) => {
948
1526
  setExpandedPaths(prev => {
@@ -976,15 +1554,225 @@
976
1554
  }
977
1555
  }, [searchQuery, treeData]);
978
1556
 
1557
+ const availableWorkspaces = useMemo(() => {
1558
+ return getAllAvailableWorkspaces();
1559
+ }, [isSelectorOpen, globalManualWorkspace]);
1560
+
1561
+ const handleSelectWs = (wsPath) => {
1562
+ setManualWorkspace(wsPath);
1563
+ setIsSelectorOpen(false);
1564
+ showToast(`已切换工作区: ${getFileName(wsPath)}`, 'success');
1565
+ };
1566
+
1567
+ const handleCustomSubmit = async (e) => {
1568
+ if (e) e.preventDefault();
1569
+ const target = customPathInput.trim();
1570
+ if (!target) return;
1571
+ setValidating(true);
1572
+ try {
1573
+ const res = await fetch('/api/preview/validate-dir', {
1574
+ method: 'POST',
1575
+ headers: { 'Content-Type': 'application/json' },
1576
+ body: JSON.stringify({ path: target }),
1577
+ });
1578
+ const json = await res.json();
1579
+ if (json.ok && json.path) {
1580
+ setManualWorkspace(json.path);
1581
+ setCustomPathInput('');
1582
+ setIsSelectorOpen(false);
1583
+ showToast(`已切换工作空间: ${json.name}`, 'success');
1584
+ } else {
1585
+ showToast(json?.error?.message || '指定目录不存在或不是文件夹', 'error');
1586
+ }
1587
+ } catch (err) {
1588
+ showToast('验证目录路径失败', 'error');
1589
+ } finally {
1590
+ setValidating(false);
1591
+ }
1592
+ };
1593
+
1594
+ const handleResetAuto = () => {
1595
+ setManualWorkspace(null);
1596
+ setIsSelectorOpen(false);
1597
+ showToast('已恢复根据当前会话自动识别工作空间', 'info');
1598
+ };
1599
+
1600
+ // =========================================================================
1601
+ // File Operations Handlers
1602
+ // =========================================================================
1603
+ const handleStartCreate = (parentPath, type) => {
1604
+ const root = treeData?.workspaceRoot || currentWs;
1605
+ const target = parentPath || root;
1606
+ setCreatingItem({ parentPath: target, type });
1607
+ if (target) {
1608
+ setExpandedPaths(prev => new Set(prev).add(target));
1609
+ }
1610
+ };
1611
+
1612
+ const handleConfirmCreate = async (name) => {
1613
+ if (!creatingItem || !name) return;
1614
+ const isDir = creatingItem.type === 'dir';
1615
+ const endpoint = isDir ? '/api/preview/create-dir' : '/api/preview/create-file';
1616
+ try {
1617
+ const res = await fetch(endpoint, {
1618
+ method: 'POST',
1619
+ headers: { 'Content-Type': 'application/json' },
1620
+ body: JSON.stringify({
1621
+ parentPath: creatingItem.parentPath,
1622
+ name: name.trim(),
1623
+ }),
1624
+ });
1625
+ const json = await res.json();
1626
+ if (json.ok) {
1627
+ setCreatingItem(null);
1628
+ setExpandedPaths(prev => new Set(prev).add(creatingItem.parentPath));
1629
+ await loadTree(true);
1630
+ showToast(`已创建${isDir ? '文件夹' : '文件'}: ${json.name}`, 'success');
1631
+ if (!isDir && json.path) {
1632
+ openPreviewFile(json.path);
1633
+ }
1634
+ } else {
1635
+ showToast(json?.error?.message || `创建${isDir ? '文件夹' : '文件'}失败`, 'error');
1636
+ }
1637
+ } catch (err) {
1638
+ showToast(`创建失败: ${err.message}`, 'error');
1639
+ }
1640
+ };
1641
+
1642
+ const handleConfirmRename = async (node, newName) => {
1643
+ if (!node || !newName || newName.trim() === node.name) {
1644
+ setRenamingPath(null);
1645
+ return;
1646
+ }
1647
+ try {
1648
+ const res = await fetch('/api/preview/rename', {
1649
+ method: 'POST',
1650
+ headers: { 'Content-Type': 'application/json' },
1651
+ body: JSON.stringify({
1652
+ path: node.path,
1653
+ newName: newName.trim(),
1654
+ }),
1655
+ });
1656
+ const json = await res.json();
1657
+ if (json.ok) {
1658
+ setRenamingPath(null);
1659
+ renameOpenTabPaths(json.sourcePath, json.targetPath, node.isDir);
1660
+ await loadTree(true);
1661
+ showToast(`已重命名为: ${json.name}`, 'success');
1662
+ } else {
1663
+ showToast(json?.error?.message || '重命名失败', 'error');
1664
+ }
1665
+ } catch (err) {
1666
+ showToast(`重命名失败: ${err.message}`, 'error');
1667
+ }
1668
+ };
1669
+
1670
+ const handleMove = async (sourcePath, targetDirPath) => {
1671
+ if (!sourcePath || !targetDirPath) return;
1672
+ try {
1673
+ const res = await fetch('/api/preview/move', {
1674
+ method: 'POST',
1675
+ headers: { 'Content-Type': 'application/json' },
1676
+ body: JSON.stringify({
1677
+ sourcePath,
1678
+ targetPath: targetDirPath,
1679
+ }),
1680
+ });
1681
+ const json = await res.json();
1682
+ if (json.ok) {
1683
+ if (json.noop) {
1684
+ showToast('目标路径与源路径相同', 'info');
1685
+ return;
1686
+ }
1687
+ renameOpenTabPaths(json.sourcePath, json.targetPath, json.isDir);
1688
+ setExpandedPaths(prev => new Set(prev).add(targetDirPath));
1689
+ await loadTree(true);
1690
+ showToast(`已移动 ${json.name} 到 ${getFileName(targetDirPath)}`, 'success');
1691
+ } else {
1692
+ showToast(json?.error?.message || '移动文件失败', 'error');
1693
+ }
1694
+ } catch (err) {
1695
+ showToast(`移动失败: ${err.message}`, 'error');
1696
+ }
1697
+ };
1698
+
1699
+ const handleDelete = async (node) => {
1700
+ if (!node) return;
1701
+ const isDir = node.isDir;
1702
+ const msg = `确定要永久删除${isDir ? '文件夹' : '文件'} "${node.name}" 吗?此操作不可恢复。`;
1703
+ if (typeof window !== 'undefined' && !window.confirm(msg)) return;
1704
+
1705
+ try {
1706
+ const res = await fetch('/api/preview/delete', {
1707
+ method: 'POST',
1708
+ headers: { 'Content-Type': 'application/json' },
1709
+ body: JSON.stringify({ path: node.path }),
1710
+ });
1711
+ const json = await res.json();
1712
+ if (json.ok) {
1713
+ removeOpenTabsForPath(node.path, isDir);
1714
+ await loadTree(true);
1715
+ showToast(`已删除${isDir ? '文件夹' : '文件'}: ${node.name}`, 'info');
1716
+ } else {
1717
+ showToast(json?.error?.message || '删除失败', 'error');
1718
+ }
1719
+ } catch (err) {
1720
+ showToast(`删除失败: ${err.message}`, 'error');
1721
+ }
1722
+ };
1723
+
1724
+ const handleContextMenuAction = (action, node) => {
1725
+ const rootPath = treeData?.workspaceRoot || currentWs;
1726
+ if (action === 'new-file') {
1727
+ const target = node ? (node.isDir ? node.path : (node.path.substring(0, Math.max(node.path.lastIndexOf('/'), node.path.lastIndexOf('\\'))))) : rootPath;
1728
+ handleStartCreate(target, 'file');
1729
+ } else if (action === 'new-dir') {
1730
+ const target = node ? (node.isDir ? node.path : (node.path.substring(0, Math.max(node.path.lastIndexOf('/'), node.path.lastIndexOf('\\'))))) : rootPath;
1731
+ handleStartCreate(target, 'dir');
1732
+ } else if (action === 'preview') {
1733
+ if (node && !node.isDir) onOpenFile(node.path);
1734
+ } else if (action === 'cut') {
1735
+ if (node) {
1736
+ setClipboard({ action: 'cut', path: node.path, name: node.name, isDir: node.isDir });
1737
+ showToast(`已剪切: ${node.name}`, 'info');
1738
+ }
1739
+ } else if (action === 'paste') {
1740
+ if (clipboard && clipboard.action === 'cut') {
1741
+ const target = node ? (node.isDir ? node.path : (node.path.substring(0, Math.max(node.path.lastIndexOf('/'), node.path.lastIndexOf('\\'))))) : rootPath;
1742
+ handleMove(clipboard.path, target);
1743
+ setClipboard(null);
1744
+ }
1745
+ } else if (action === 'rename') {
1746
+ if (node) setRenamingPath(node.path);
1747
+ } else if (action === 'reveal') {
1748
+ if (node) triggerRevealInExplorer(node.path);
1749
+ } else if (action === 'native') {
1750
+ if (node) triggerOpenNative(node.path);
1751
+ } else if (action === 'delete') {
1752
+ if (node) handleDelete(node);
1753
+ } else if (action === 'refresh') {
1754
+ loadTree(true);
1755
+ }
1756
+ };
1757
+
1758
+ const rootPath = treeData?.workspaceRoot || currentWs;
1759
+ const isCreatingAtRoot = creatingItem && creatingItem.parentPath && rootPath && creatingItem.parentPath.replace(/\\/g, '/').toLowerCase() === rootPath.replace(/\\/g, '/').toLowerCase();
1760
+ const isRootDropTarget = dragOverNode && rootPath && dragOverNode.path && dragOverNode.path.replace(/\\/g, '/').toLowerCase() === rootPath.replace(/\\/g, '/').toLowerCase();
1761
+
979
1762
  return h('div', {
980
1763
  className: 'dsh-tree-sidebar',
981
1764
  style: { width: `${width}px` },
982
1765
  },
983
1766
  // Sidebar Header
984
1767
  h('div', { className: 'dsh-tree-sidebar-header' },
985
- h('div', { className: 'dsh-tree-sidebar-title', title: treeData?.workspaceRoot || currentWs },
1768
+ h('div', {
1769
+ className: 'dsh-tree-sidebar-title dsh-tree-title-clickable',
1770
+ onClick: () => setIsSelectorOpen(!isSelectorOpen),
1771
+ title: `当前工作空间: ${treeData?.workspaceRoot || currentWs || '未指定'}\n点击切换工作空间`,
1772
+ },
986
1773
  h('span', { className: 'dsh-tree-header-icon' }, '📁'),
987
- h('span', { className: 'dsh-tree-header-text' }, treeData?.workspaceName || getFileName(currentWs) || '工作区')
1774
+ h('span', { className: 'dsh-tree-header-text' }, treeData?.workspaceName || getFileName(currentWs) || '工作区'),
1775
+ h('span', { className: 'dsh-tree-header-chevron' }, isSelectorOpen ? '▲' : '▼')
988
1776
  ),
989
1777
  h('div', { className: 'dsh-tree-header-actions' },
990
1778
  h('button', {
@@ -996,8 +1784,20 @@
996
1784
  h('button', {
997
1785
  type: 'button',
998
1786
  className: 'dsh-tree-action-btn',
999
- onClick: handleExpandAll,
1000
- title: '全部展开',
1787
+ onClick: () => handleStartCreate(rootPath, 'file'),
1788
+ title: '在根目录新建文件',
1789
+ }, '📄➕'),
1790
+ h('button', {
1791
+ type: 'button',
1792
+ className: 'dsh-tree-action-btn',
1793
+ onClick: () => handleStartCreate(rootPath, 'dir'),
1794
+ title: '在根目录新建文件夹',
1795
+ }, '📁➕'),
1796
+ h('button', {
1797
+ type: 'button',
1798
+ className: 'dsh-tree-action-btn',
1799
+ onClick: handleExpandAll,
1800
+ title: '全部展开',
1001
1801
  }, '📂'),
1002
1802
  h('button', {
1003
1803
  type: 'button',
@@ -1014,6 +1814,62 @@
1014
1814
  )
1015
1815
  ),
1016
1816
 
1817
+ // Workspace Selector Dropdown Popover
1818
+ isSelectorOpen ? h('div', { className: 'dsh-ws-selector-popover' },
1819
+ h('div', { className: 'dsh-ws-popover-header' },
1820
+ h('span', { style: { fontWeight: 600, fontSize: 12 } }, '选择工作空间'),
1821
+ h('button', {
1822
+ type: 'button',
1823
+ className: 'dsh-ws-popover-close',
1824
+ onClick: () => setIsSelectorOpen(false),
1825
+ }, '✕')
1826
+ ),
1827
+
1828
+ // List of detected workspaces
1829
+ h('div', { className: 'dsh-ws-list' },
1830
+ availableWorkspaces.length > 0 ? availableWorkspaces.map(ws => (
1831
+ h('div', {
1832
+ key: ws.path,
1833
+ className: `dsh-ws-item ${ws.isCurrent ? 'active' : ''}`,
1834
+ onClick: () => handleSelectWs(ws.path),
1835
+ title: ws.path,
1836
+ },
1837
+ h('div', { className: 'dsh-ws-item-info' },
1838
+ h('span', { className: 'dsh-ws-item-icon' }, '📁'),
1839
+ h('span', { className: 'dsh-ws-item-name' }, ws.name),
1840
+ h('span', { className: 'dsh-ws-item-path' }, ws.path)
1841
+ ),
1842
+ ws.isCurrent ? h('span', { className: 'dsh-ws-item-check' }, '✓') : null
1843
+ )
1844
+ )) : h('div', { className: 'dsh-ws-empty' }, '暂无其他检测到的工作空间')
1845
+ ),
1846
+
1847
+ // Custom Path Input
1848
+ h('form', { className: 'dsh-ws-custom-form', onSubmit: handleCustomSubmit },
1849
+ h('div', { className: 'dsh-ws-input-row' },
1850
+ h('input', {
1851
+ type: 'text',
1852
+ className: 'dsh-ws-input',
1853
+ placeholder: '输入或粘贴本地目录路径...',
1854
+ value: customPathInput,
1855
+ onChange: (e) => setCustomPathInput(e.target.value),
1856
+ }),
1857
+ h('button', {
1858
+ type: 'submit',
1859
+ className: 'dsh-ws-btn',
1860
+ disabled: validating || !customPathInput.trim(),
1861
+ }, validating ? '验证...' : '切换')
1862
+ )
1863
+ ),
1864
+
1865
+ // Auto-detect reset link
1866
+ globalManualWorkspace ? h('button', {
1867
+ type: 'button',
1868
+ className: 'dsh-ws-reset-btn',
1869
+ onClick: handleResetAuto,
1870
+ }, '🔄 恢复自动识别当前会话工作区') : null
1871
+ ) : null,
1872
+
1017
1873
  // Search Input
1018
1874
  h('div', { className: 'dsh-tree-sidebar-search' },
1019
1875
  h('div', { className: 'dsh-tree-search-wrapper' },
@@ -1034,7 +1890,40 @@
1034
1890
  ),
1035
1891
 
1036
1892
  // Tree Content
1037
- h('div', { className: 'dsh-tree-node-list' },
1893
+ h('div', {
1894
+ className: `dsh-tree-node-list ${isRootDropTarget ? 'drop-target-root' : ''}`,
1895
+ onContextMenu: (e) => {
1896
+ e.preventDefault();
1897
+ setContextMenu({ x: e.clientX, y: e.clientY, node: null, isRoot: true });
1898
+ },
1899
+ onDragOver: (e) => {
1900
+ if (draggedNode && isValidDropTarget(draggedNode, { path: rootPath, isDir: true })) {
1901
+ e.preventDefault();
1902
+ e.dataTransfer.dropEffect = 'move';
1903
+ setDragOverNode({ path: rootPath, isDir: true });
1904
+ }
1905
+ },
1906
+ onDragLeave: () => {
1907
+ if (dragOverNode && dragOverNode.path === rootPath) {
1908
+ setDragOverNode(null);
1909
+ }
1910
+ },
1911
+ onDrop: (e) => {
1912
+ e.preventDefault();
1913
+ setDragOverNode(null);
1914
+ if (draggedNode && isValidDropTarget(draggedNode, { path: rootPath, isDir: true })) {
1915
+ handleMove(draggedNode.path, rootPath);
1916
+ }
1917
+ },
1918
+ },
1919
+ // If creating at root level, render inline create input at top
1920
+ isCreatingAtRoot ? h(InlineCreateInputNode, {
1921
+ depth: 0,
1922
+ type: creatingItem.type,
1923
+ onConfirm: handleConfirmCreate,
1924
+ onCancel: () => setCreatingItem(null),
1925
+ }) : null,
1926
+
1038
1927
  isLoading ? (
1039
1928
  h('div', { className: 'dsh-tree-loading' }, '正在扫描工作区文件...')
1040
1929
  ) : error ? (
@@ -1046,7 +1935,7 @@
1046
1935
  onClick: () => loadTree(true),
1047
1936
  }, '重试')
1048
1937
  )
1049
- ) : displayTree.length === 0 ? (
1938
+ ) : displayTree.length === 0 && !isCreatingAtRoot ? (
1050
1939
  h('div', { className: 'dsh-tree-empty' }, searchQuery ? '未匹配到相关文件' : '工作区暂无文件')
1051
1940
  ) : (
1052
1941
  displayTree.map(node => h(FileTreeNodeItem, {
@@ -1058,10 +1947,42 @@
1058
1947
  activeFilePath,
1059
1948
  onOpenFile,
1060
1949
  searchQuery,
1950
+ creatingItem,
1951
+ onConfirmCreate: handleConfirmCreate,
1952
+ onCancelCreate: () => setCreatingItem(null),
1953
+ renamingPath,
1954
+ onConfirmRename: handleConfirmRename,
1955
+ onCancelRename: () => setRenamingPath(null),
1956
+ onContextMenu: (targetNode, e) => {
1957
+ setContextMenu({ x: e.clientX, y: e.clientY, node: targetNode, isRoot: false });
1958
+ },
1959
+ draggedNode,
1960
+ dragOverNode,
1961
+ onDragStart: (targetNode) => setDraggedNode(targetNode),
1962
+ onDragEnd: () => { setDraggedNode(null); setDragOverNode(null); },
1963
+ onDragOver: (targetNode) => setDragOverNode(targetNode),
1964
+ onDragLeave: () => setDragOverNode(null),
1965
+ onDrop: (source, target) => {
1966
+ setDraggedNode(null);
1967
+ setDragOverNode(null);
1968
+ handleMove(source.path, target.path);
1969
+ },
1970
+ clipboard,
1061
1971
  }))
1062
1972
  )
1063
1973
  ),
1064
1974
 
1975
+ // Context Menu Overlay
1976
+ contextMenu ? h(TreeContextMenu, {
1977
+ x: contextMenu.x,
1978
+ y: contextMenu.y,
1979
+ node: contextMenu.node,
1980
+ isRoot: contextMenu.isRoot,
1981
+ clipboard,
1982
+ onAction: handleContextMenuAction,
1983
+ onClose: () => setContextMenu(null),
1984
+ }) : null,
1985
+
1065
1986
  // Sidebar Footer with file count stats
1066
1987
  treeData ? h('div', { className: 'dsh-tree-sidebar-footer' },
1067
1988
  h('span', null, `${treeData.totalDirs || 0} 目录`),
@@ -1993,22 +2914,62 @@
1993
2914
  }
1994
2915
 
1995
2916
  // =========================================================================
1996
- // React Component: CodeEditorView
2917
+ // React Component: CodeEditorView (Unified line-row architecture)
1997
2918
  // =========================================================================
1998
- function CodeEditorView({ content, category, isHighlighted = true, isWordWrap = true, searchQuery = '', onLineClick }) {
1999
- const lines = useMemo(() => (content ? content.split('\n') : []), [content]);
2919
+ function splitHighlightedHtmlIntoLines(html) {
2920
+ if (!html) return [];
2921
+ const rawLines = html.split('\n');
2922
+ const lines = [];
2923
+ const openTags = [];
2924
+
2925
+ for (let i = 0; i < rawLines.length; i++) {
2926
+ let line = rawLines[i];
2927
+ let prefix = '';
2928
+ if (openTags.length > 0) {
2929
+ prefix = openTags.map(t => '<' + t.name + (t.attrs ? ' ' + t.attrs : '') + '>').join('');
2930
+ }
2931
+
2932
+ const tagRegex = /<\/?([a-zA-Z0-9\-]+)([^>]*)>/g;
2933
+ let match;
2934
+ while ((match = tagRegex.exec(line)) !== null) {
2935
+ const isClosing = match[0].startsWith('</');
2936
+ const tagName = match[1];
2937
+ const attrs = match[2];
2938
+ if (isClosing) {
2939
+ const lastIdx = openTags.map(t => t.name).lastIndexOf(tagName);
2940
+ if (lastIdx !== -1) {
2941
+ openTags.splice(lastIdx, 1);
2942
+ }
2943
+ } else if (!match[0].endsWith('/>')) {
2944
+ openTags.push({ name: tagName, attrs: attrs.trim() });
2945
+ }
2946
+ }
2000
2947
 
2001
- const highlightedHtml = useMemo(() => {
2948
+ let suffix = '';
2949
+ if (openTags.length > 0) {
2950
+ suffix = openTags.slice().reverse().map(t => '</' + t.name + '>').join('');
2951
+ }
2952
+
2953
+ lines.push(prefix + line + suffix);
2954
+ }
2955
+
2956
+ return lines;
2957
+ }
2958
+
2959
+ function CodeEditorView({ content, category, isHighlighted = true, isWordWrap = true, searchQuery = '', onLineClick }) {
2960
+ const normalizedContent = useMemo(() => {
2002
2961
  if (!content) return '';
2962
+ return content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
2963
+ }, [content]);
2964
+
2965
+ const highlightedLines = useMemo(() => {
2966
+ if (!normalizedContent) return [];
2967
+ let html = '';
2003
2968
  if (!isHighlighted) {
2004
- let escaped = escapeHtml(content);
2005
- if (searchQuery.trim()) {
2006
- const q = escapeHtml(searchQuery.trim());
2007
- escaped = escaped.replace(new RegExp(`(${q})`, 'gi'), '<mark class="dsh-search-highlight">$1</mark>');
2008
- }
2009
- return escaped;
2969
+ html = escapeHtml(normalizedContent);
2970
+ } else {
2971
+ html = highlightCode(normalizedContent, category);
2010
2972
  }
2011
- let html = highlightCode(content, category);
2012
2973
  if (searchQuery.trim()) {
2013
2974
  try {
2014
2975
  const q = escapeHtml(searchQuery.trim());
@@ -2016,27 +2977,34 @@
2016
2977
  html = html.replace(regex, '<mark class="dsh-search-highlight">$1</mark>');
2017
2978
  } catch (e) {}
2018
2979
  }
2019
- return html;
2020
- }, [content, category, isHighlighted, searchQuery]);
2980
+ return splitHighlightedHtmlIntoLines(html);
2981
+ }, [normalizedContent, category, isHighlighted, searchQuery]);
2982
+
2983
+ const lineCount = highlightedLines.length;
2984
+ const digitCount = Math.max(2, String(lineCount).length);
2985
+ const gutterWidth = Math.max(24, digitCount * 7.5 + 12);
2021
2986
 
2022
2987
  return h('div', { className: `dsh-code-editor-view ${isWordWrap ? 'word-wrap' : ''}` },
2023
- h('div', { className: 'dsh-code-gutter' },
2024
- lines.map((_, i) => h('div', {
2025
- key: i,
2026
- id: `line-gutter-${i + 1}`,
2027
- className: 'dsh-gutter-line',
2028
- onClick: () => onLineClick && onLineClick(i + 1),
2029
- title: `第 ${i + 1} 行 (点击定位)`
2030
- }, i + 1))
2031
- ),
2032
- h('div', { className: 'dsh-code-content' },
2033
- h('pre', { className: 'dsh-code-pre' },
2034
- h('code', {
2035
- className: `dsh-code-lang-${category}`,
2036
- dangerouslySetInnerHTML: { __html: highlightedHtml }
2988
+ highlightedLines.map((lineHtml, i) => {
2989
+ const lineNum = i + 1;
2990
+ return h('div', {
2991
+ key: lineNum,
2992
+ id: `line-row-${lineNum}`,
2993
+ className: 'dsh-code-line',
2994
+ },
2995
+ h('span', {
2996
+ id: `line-gutter-${lineNum}`,
2997
+ className: 'dsh-line-gutter',
2998
+ style: { minWidth: `${gutterWidth}px` },
2999
+ onClick: () => onLineClick && onLineClick(lineNum),
3000
+ title: `第 ${lineNum} 行 (点击定位)`,
3001
+ }, lineNum),
3002
+ h('span', {
3003
+ className: 'dsh-line-code',
3004
+ dangerouslySetInnerHTML: { __html: lineHtml ? lineHtml : ' ' },
2037
3005
  })
2038
- )
2039
- )
3006
+ );
3007
+ })
2040
3008
  );
2041
3009
  }
2042
3010
 
@@ -2138,7 +3106,9 @@
2138
3106
  const el = contentRef.current.querySelector(`#${item.id}`);
2139
3107
  if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
2140
3108
  } else {
2141
- const el = document.getElementById(`line-gutter-${item.line}`);
3109
+ const rowEl = document.getElementById(`line-row-${item.line}`);
3110
+ const gutterEl = document.getElementById(`line-gutter-${item.line}`);
3111
+ const el = rowEl || gutterEl;
2142
3112
  if (el) {
2143
3113
  el.scrollIntoView({ behavior: 'smooth', block: 'center' });
2144
3114
  el.classList.add('dsh-line-highlight-flash');
@@ -2463,6 +3433,7 @@
2463
3433
  const [activeTabId, setActiveTabId] = useState(globalActiveTabId);
2464
3434
  const [isOpen, setIsOpen] = useState(globalIsPanelOpen);
2465
3435
  const [width, setWidth] = useState(globalPanelWidth);
3436
+ const [isMaximized, setLocalIsMaximized] = useState(globalIsMaximized);
2466
3437
  const [isTreeOpen, setLocalIsTreeOpen] = useState(globalIsTreeOpen);
2467
3438
  const [treeWidth, setLocalTreeWidth] = useState(globalTreeWidth);
2468
3439
  const [isTreeDragging, setIsTreeDragging] = useState(false);
@@ -2482,6 +3453,7 @@
2482
3453
  setActiveTabId(globalActiveTabId);
2483
3454
  setIsOpen(globalIsPanelOpen);
2484
3455
  setWidth(globalPanelWidth);
3456
+ setLocalIsMaximized(globalIsMaximized);
2485
3457
  setLocalIsTreeOpen(globalIsTreeOpen);
2486
3458
  setLocalTreeWidth(globalTreeWidth);
2487
3459
  };
@@ -2489,6 +3461,30 @@
2489
3461
  return () => stateListeners.delete(update);
2490
3462
  }, []);
2491
3463
 
3464
+ // Synchronize side-by-side layout CSS variables with document.body
3465
+ useEffect(() => {
3466
+ if (typeof document === 'undefined') return;
3467
+ if (isOpen) {
3468
+ document.body.classList.add('dsh-preview-sidebar-active');
3469
+ if (isMaximized) {
3470
+ document.body.classList.add('dsh-preview-maximized');
3471
+ document.body.style.setProperty('--dsh-preview-width', '100vw');
3472
+ } else {
3473
+ document.body.classList.remove('dsh-preview-maximized');
3474
+ document.body.style.setProperty('--dsh-preview-width', `${width}px`);
3475
+ }
3476
+ } else {
3477
+ document.body.classList.remove('dsh-preview-sidebar-active');
3478
+ document.body.classList.remove('dsh-preview-maximized');
3479
+ document.body.style.removeProperty('--dsh-preview-width');
3480
+ }
3481
+ return () => {
3482
+ document.body.classList.remove('dsh-preview-sidebar-active');
3483
+ document.body.classList.remove('dsh-preview-maximized');
3484
+ document.body.style.removeProperty('--dsh-preview-width');
3485
+ };
3486
+ }, [isOpen, width, isMaximized]);
3487
+
2492
3488
  const activeTab = useMemo(() => {
2493
3489
  return tabs.find(t => t.id === activeTabId) || tabs[0] || null;
2494
3490
  }, [tabs, activeTabId]);
@@ -2567,54 +3563,6 @@
2567
3563
 
2568
3564
  const drawerRef = useRef(null);
2569
3565
 
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
3566
  // Mutual exclusion with other tool windows (e.g. Git modal)
2619
3567
  useEffect(() => {
2620
3568
  const handleToolWindowOpen = (e) => {
@@ -2679,17 +3627,17 @@
2679
3627
  null,
2680
3628
  isOpen ? h('div', {
2681
3629
  ref: drawerRef,
2682
- className: `dsh-preview-drawer-container ${isDragging || isTreeDragging ? 'dragging' : ''}`,
2683
- style: { width: `${width}px` },
3630
+ className: `dsh-preview-drawer-container ${isDragging || isTreeDragging ? 'dragging' : ''} ${isMaximized ? 'maximized' : ''}`,
3631
+ style: { width: isMaximized ? '100vw' : `${width}px` },
2684
3632
  },
2685
- // Drag Handle on Left Border
2686
- h('div', {
3633
+ // Drag Handle on Left Border (only when not maximized)
3634
+ !isMaximized ? h('div', {
2687
3635
  className: 'dsh-preview-drag-handle',
2688
3636
  onPointerDown: onResizePointerDown,
2689
3637
  onPointerMove: onResizePointerMove,
2690
3638
  onPointerUp: onResizePointerUp,
2691
3639
  title: '左右拖拽调整预览面板宽度',
2692
- }),
3640
+ }) : null,
2693
3641
 
2694
3642
  // Drawer Header with FileTabs
2695
3643
  h('div', { className: 'dsh-preview-drawer-header' },
@@ -2734,11 +3682,17 @@
2734
3682
  onClick: closeAllTabs,
2735
3683
  title: '关闭所有标签页',
2736
3684
  }, '✕ 全部') : null,
3685
+ h('button', {
3686
+ type: 'button',
3687
+ className: 'dsh-win-ctrl-btn',
3688
+ onClick: () => setIsMaximized(!globalIsMaximized),
3689
+ title: isMaximized ? '还原为分栏并排模式' : '最大化窗口',
3690
+ }, isMaximized ? '🗗 还原' : '🗖 最大化'),
2737
3691
  h('button', {
2738
3692
  type: 'button',
2739
3693
  className: 'dsh-win-ctrl-btn dsh-win-ctrl-close',
2740
3694
  onClick: () => setPanelOpen(false),
2741
- title: '收起预览面板 (Esc)',
3695
+ title: '收起预览侧边栏 (Esc)',
2742
3696
  }, '✕')
2743
3697
  )
2744
3698
  ),
@@ -2923,151 +3877,369 @@
2923
3877
  transform: translateX(-4px);
2924
3878
  box-shadow: -5px 5px 18px rgba(59, 130, 246, 0.22);
2925
3879
  }
2926
- body[data-ds-dark-theme] .dsh-rail-tab-item:hover,
2927
- body[data-ds-dark-theme] .dsh-preview-right-rail-tab:hover,
2928
- body[data-ds-dark-theme] .dsh-preview-float-pill:hover,
2929
- body[data-ds-dark-theme] .dsh-git-right-rail-tab:hover {
2930
- background: var(--dsw-alias-bg-layer-1, #252a36);
2931
- color: #60a5fa;
2932
- border-color: #3b82f6;
3880
+ body[data-ds-dark-theme] .dsh-rail-tab-item:hover,
3881
+ body[data-ds-dark-theme] .dsh-preview-right-rail-tab:hover,
3882
+ body[data-ds-dark-theme] .dsh-preview-float-pill:hover,
3883
+ body[data-ds-dark-theme] .dsh-git-right-rail-tab:hover {
3884
+ background: var(--dsw-alias-bg-layer-1, #252a36);
3885
+ color: #60a5fa;
3886
+ border-color: #3b82f6;
3887
+ }
3888
+ .dsh-rail-tab-item.active,
3889
+ .dsh-preview-right-rail-tab.active,
3890
+ .dsh-git-right-rail-tab.active {
3891
+ background: #2563eb;
3892
+ color: #ffffff;
3893
+ border-color: #1d4ed8;
3894
+ transform: translateX(-3px);
3895
+ box-shadow: -5px 5px 18px rgba(37, 99, 235, 0.35);
3896
+ }
3897
+
3898
+ .dsh-rail-tab-icon {
3899
+ display: flex;
3900
+ align-items: center;
3901
+ justify-content: center;
3902
+ width: 16px;
3903
+ height: 16px;
3904
+ min-width: 16px;
3905
+ min-height: 16px;
3906
+ max-width: 16px;
3907
+ max-height: 16px;
3908
+ line-height: 1;
3909
+ flex-shrink: 0;
3910
+ transition: none !important;
3911
+ animation: none !important;
3912
+ transform: none !important;
3913
+ }
3914
+ .dsh-rail-tab-icon svg {
3915
+ width: 14px !important;
3916
+ height: 14px !important;
3917
+ min-width: 14px !important;
3918
+ min-height: 14px !important;
3919
+ max-width: 14px !important;
3920
+ max-height: 14px !important;
3921
+ flex-shrink: 0 !important;
3922
+ display: block !important;
3923
+ transition: none !important;
3924
+ animation: none !important;
3925
+ transform: none !important;
3926
+ }
3927
+
3928
+ .dsh-rail-tab-title, .dsh-preview-rail-title, .dsh-git-rail-title {
3929
+ writing-mode: vertical-rl;
3930
+ text-orientation: mixed;
3931
+ font-size: 11.5px;
3932
+ font-weight: 600;
3933
+ letter-spacing: 2px;
3934
+ line-height: 1;
3935
+ flex-shrink: 0;
3936
+ user-select: none;
3937
+ transition: none !important;
3938
+ animation: none !important;
3939
+ transform: none !important;
3940
+ }
3941
+
3942
+ .dsh-rail-tab-badge, .dsh-float-pill-badge, .dsh-preview-rail-badge, .dsh-git-rail-badge {
3943
+ writing-mode: horizontal-tb;
3944
+ display: inline-flex;
3945
+ align-items: center;
3946
+ justify-content: center;
3947
+ min-width: 17px;
3948
+ height: 17px;
3949
+ min-height: 17px;
3950
+ max-height: 17px;
3951
+ padding: 0 4px;
3952
+ border-radius: 9px;
3953
+ background: #2563eb;
3954
+ color: #ffffff;
3955
+ font-size: 10px;
3956
+ font-weight: 700;
3957
+ line-height: 1;
3958
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
3959
+ flex-shrink: 0;
3960
+ box-sizing: border-box;
3961
+ transition: none !important;
3962
+ animation: none !important;
3963
+ transform: none !important;
3964
+ }
3965
+ body[data-ds-dark-theme] .dsh-rail-tab-badge,
3966
+ body[data-ds-dark-theme] .dsh-float-pill-badge,
3967
+ body[data-ds-dark-theme] .dsh-preview-rail-badge,
3968
+ body[data-ds-dark-theme] .dsh-git-rail-badge {
3969
+ background: #3b82f6;
3970
+ }
3971
+ .dsh-rail-tab-item.active .dsh-rail-tab-badge,
3972
+ .dsh-preview-right-rail-tab.active .dsh-preview-rail-badge,
3973
+ .dsh-git-right-rail-tab.active .dsh-git-rail-badge {
3974
+ background: #ffffff;
3975
+ color: #2563eb;
3976
+ }
3977
+
3978
+ .dsh-rail-tab-badge-placeholder {
3979
+ width: 17px;
3980
+ height: 17px;
3981
+ min-height: 17px;
3982
+ max-height: 17px;
3983
+ flex-shrink: 0;
3984
+ visibility: hidden;
3985
+ }
3986
+ body[data-ds-dark-theme] .dsh-rail-tab-badge,
3987
+ body[data-ds-dark-theme] .dsh-float-pill-badge,
3988
+ body[data-ds-dark-theme] .dsh-preview-rail-badge {
3989
+ background: #3b82f6;
3990
+ }
3991
+ .dsh-rail-tab-item.active .dsh-rail-tab-badge,
3992
+ .dsh-preview-right-rail-tab.active .dsh-float-pill-badge,
3993
+ .dsh-preview-right-rail-tab.active .dsh-preview-rail-badge {
3994
+ background: #ffffff;
3995
+ color: #2563eb;
3996
+ }
3997
+
3998
+ /* Side-by-Side Right Sidebar Layout Integration */
3999
+ body.dsh-preview-sidebar-active div[class*="centerCol"] {
4000
+ margin-right: var(--dsh-preview-width, 620px) !important;
4001
+ max-width: calc(100% - var(--dsh-preview-width, 620px)) !important;
4002
+ box-sizing: border-box !important;
4003
+ 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;
4004
+ }
4005
+ body.dsh-preview-sidebar-active.dsh-preview-maximized div[class*="centerCol"] {
4006
+ display: none !important;
4007
+ }
4008
+ body.dsh-preview-sidebar-active.dsh-preview-maximized .dsh-preview-drawer-container {
4009
+ width: 100vw !important;
4010
+ }
4011
+
4012
+ /* Workspace Switcher Header & Popover */
4013
+ .dsh-tree-sidebar-title.dsh-tree-title-clickable {
4014
+ cursor: pointer;
4015
+ padding: 2px 6px;
4016
+ border-radius: 4px;
4017
+ transition: background 0.15s;
4018
+ gap: 4px;
4019
+ }
4020
+ .dsh-tree-sidebar-title.dsh-tree-title-clickable:hover {
4021
+ background: rgba(0, 0, 0, 0.06);
4022
+ }
4023
+ body[data-ds-dark-theme] .dsh-tree-sidebar-title.dsh-tree-title-clickable:hover {
4024
+ background: rgba(255, 255, 255, 0.08);
4025
+ }
4026
+ .dsh-tree-header-chevron {
4027
+ font-size: 8px;
4028
+ opacity: 0.6;
4029
+ margin-left: 2px;
4030
+ }
4031
+
4032
+ .dsh-ws-selector-popover {
4033
+ position: absolute;
4034
+ top: 38px;
4035
+ left: 6px;
4036
+ right: 6px;
4037
+ max-width: 380px;
4038
+ z-index: 1000;
4039
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
4040
+ border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
4041
+ border-radius: 8px;
4042
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
4043
+ padding: 10px;
4044
+ display: flex;
4045
+ flex-direction: column;
4046
+ gap: 8px;
4047
+ box-sizing: border-box;
4048
+ animation: dshPopoverFadeIn 0.15s ease;
4049
+ }
4050
+ body[data-ds-dark-theme] .dsh-ws-selector-popover {
4051
+ background: var(--dsw-alias-bg-layer-2, #1b202c);
4052
+ border-color: var(--dsw-alias-border-l2, #334155);
4053
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
4054
+ }
4055
+ @keyframes dshPopoverFadeIn {
4056
+ from { opacity: 0; transform: translateY(-4px); }
4057
+ to { opacity: 1; transform: translateY(0); }
4058
+ }
4059
+
4060
+ .dsh-ws-popover-header {
4061
+ display: flex;
4062
+ align-items: center;
4063
+ justify-content: space-between;
4064
+ padding-bottom: 6px;
4065
+ border-bottom: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
4066
+ color: var(--dsw-alias-label-primary, #0f172a);
4067
+ }
4068
+ body[data-ds-dark-theme] .dsh-ws-popover-header {
4069
+ border-bottom-color: var(--dsw-alias-border-l3, #2d3748);
4070
+ color: #f1f5f9;
4071
+ }
4072
+ .dsh-ws-popover-close {
4073
+ background: none;
4074
+ border: none;
4075
+ color: var(--dsw-alias-label-secondary, #64748b);
4076
+ cursor: pointer;
4077
+ font-size: 11px;
4078
+ padding: 2px 4px;
4079
+ border-radius: 3px;
4080
+ }
4081
+ .dsh-ws-popover-close:hover {
4082
+ background: rgba(0, 0, 0, 0.06);
4083
+ color: var(--dsw-alias-label-primary, #0f172a);
4084
+ }
4085
+
4086
+ .dsh-ws-list {
4087
+ display: flex;
4088
+ flex-direction: column;
4089
+ gap: 3px;
4090
+ max-height: 180px;
4091
+ overflow-y: auto;
4092
+ padding: 2px 0;
4093
+ }
4094
+ .dsh-ws-item {
4095
+ display: flex;
4096
+ align-items: center;
4097
+ justify-content: space-between;
4098
+ padding: 6px 8px;
4099
+ border-radius: 6px;
4100
+ cursor: pointer;
4101
+ transition: background 0.12s;
4102
+ font-size: 12px;
4103
+ }
4104
+ .dsh-ws-item:hover {
4105
+ background: var(--dsw-alias-bg-hover, rgba(0, 0, 0, 0.05));
4106
+ }
4107
+ body[data-ds-dark-theme] .dsh-ws-item:hover {
4108
+ background: rgba(255, 255, 255, 0.06);
4109
+ }
4110
+ .dsh-ws-item.active {
4111
+ background: rgba(14, 165, 233, 0.12);
4112
+ color: #0284c7;
4113
+ font-weight: 600;
4114
+ }
4115
+ body[data-ds-dark-theme] .dsh-ws-item.active {
4116
+ background: rgba(56, 189, 248, 0.16);
4117
+ color: #38bdf8;
4118
+ }
4119
+ .dsh-ws-item-info {
4120
+ display: flex;
4121
+ flex-direction: column;
4122
+ gap: 1px;
4123
+ overflow: hidden;
4124
+ flex: 1;
4125
+ }
4126
+ .dsh-ws-item-name {
4127
+ font-size: 12px;
4128
+ font-weight: 600;
4129
+ overflow: hidden;
4130
+ text-overflow: ellipsis;
4131
+ white-space: nowrap;
4132
+ }
4133
+ .dsh-ws-item-path {
4134
+ font-size: 10px;
4135
+ color: var(--dsw-alias-label-tertiary, #94a3b8);
4136
+ overflow: hidden;
4137
+ text-overflow: ellipsis;
4138
+ white-space: nowrap;
4139
+ }
4140
+ .dsh-ws-item-check {
4141
+ color: #0284c7;
4142
+ font-weight: bold;
4143
+ margin-left: 6px;
2933
4144
  }
2934
- .dsh-rail-tab-item.active,
2935
- .dsh-preview-right-rail-tab.active,
2936
- .dsh-git-right-rail-tab.active {
2937
- background: #2563eb;
2938
- color: #ffffff;
2939
- border-color: #1d4ed8;
2940
- transform: translateX(-3px);
2941
- box-shadow: -5px 5px 18px rgba(37, 99, 235, 0.35);
4145
+ .dsh-ws-empty {
4146
+ padding: 12px;
4147
+ text-align: center;
4148
+ font-size: 11px;
4149
+ color: var(--dsw-alias-label-secondary, #64748b);
2942
4150
  }
2943
4151
 
2944
- .dsh-rail-tab-icon {
4152
+ .dsh-ws-custom-form {
4153
+ margin-top: 4px;
4154
+ padding-top: 6px;
4155
+ border-top: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
4156
+ }
4157
+ body[data-ds-dark-theme] .dsh-ws-custom-form {
4158
+ border-top-color: var(--dsw-alias-border-l3, #2d3748);
4159
+ }
4160
+ .dsh-ws-input-row {
2945
4161
  display: flex;
4162
+ gap: 6px;
2946
4163
  align-items: center;
2947
- justify-content: center;
2948
- width: 16px;
2949
- height: 16px;
2950
- min-width: 16px;
2951
- min-height: 16px;
2952
- max-width: 16px;
2953
- max-height: 16px;
2954
- line-height: 1;
2955
- flex-shrink: 0;
2956
- transition: none !important;
2957
- animation: none !important;
2958
- transform: none !important;
2959
4164
  }
2960
- .dsh-rail-tab-icon svg {
2961
- width: 14px !important;
2962
- height: 14px !important;
2963
- min-width: 14px !important;
2964
- min-height: 14px !important;
2965
- max-width: 14px !important;
2966
- max-height: 14px !important;
2967
- flex-shrink: 0 !important;
2968
- display: block !important;
2969
- transition: none !important;
2970
- animation: none !important;
2971
- transform: none !important;
4165
+ .dsh-ws-input {
4166
+ flex: 1;
4167
+ height: 26px;
4168
+ border-radius: 4px;
4169
+ border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
4170
+ padding: 0 8px;
4171
+ font-size: 11px;
4172
+ background: var(--dsw-alias-bg-base, #ffffff);
4173
+ color: inherit;
4174
+ outline: none;
2972
4175
  }
2973
-
2974
- .dsh-rail-tab-title, .dsh-preview-rail-title, .dsh-git-rail-title {
2975
- writing-mode: vertical-rl;
2976
- text-orientation: mixed;
2977
- font-size: 11.5px;
2978
- font-weight: 600;
2979
- letter-spacing: 2px;
2980
- line-height: 1;
2981
- flex-shrink: 0;
2982
- user-select: none;
2983
- transition: none !important;
2984
- animation: none !important;
2985
- transform: none !important;
4176
+ body[data-ds-dark-theme] .dsh-ws-input {
4177
+ background: #141720;
4178
+ border-color: #334155;
4179
+ color: #f1f5f9;
2986
4180
  }
2987
-
2988
- .dsh-rail-tab-badge, .dsh-float-pill-badge, .dsh-preview-rail-badge, .dsh-git-rail-badge {
2989
- writing-mode: horizontal-tb;
2990
- display: inline-flex;
2991
- align-items: center;
2992
- justify-content: center;
2993
- min-width: 17px;
2994
- height: 17px;
2995
- min-height: 17px;
2996
- max-height: 17px;
2997
- padding: 0 4px;
2998
- border-radius: 9px;
2999
- background: #2563eb;
4181
+ .dsh-ws-btn {
4182
+ height: 26px;
4183
+ padding: 0 10px;
4184
+ border-radius: 4px;
4185
+ border: none;
4186
+ background: var(--dsw-static-deepseek-500, #4176e6);
3000
4187
  color: #ffffff;
3001
- font-size: 10px;
3002
- font-weight: 700;
3003
- line-height: 1;
3004
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
3005
- flex-shrink: 0;
3006
- box-sizing: border-box;
3007
- transition: none !important;
3008
- animation: none !important;
3009
- transform: none !important;
3010
- }
3011
- body[data-ds-dark-theme] .dsh-rail-tab-badge,
3012
- body[data-ds-dark-theme] .dsh-float-pill-badge,
3013
- body[data-ds-dark-theme] .dsh-preview-rail-badge,
3014
- body[data-ds-dark-theme] .dsh-git-rail-badge {
3015
- background: #3b82f6;
4188
+ font-size: 11px;
4189
+ font-weight: 500;
4190
+ cursor: pointer;
4191
+ white-space: nowrap;
3016
4192
  }
3017
- .dsh-rail-tab-item.active .dsh-rail-tab-badge,
3018
- .dsh-preview-right-rail-tab.active .dsh-preview-rail-badge,
3019
- .dsh-git-right-rail-tab.active .dsh-git-rail-badge {
3020
- background: #ffffff;
3021
- color: #2563eb;
4193
+ .dsh-ws-btn:hover {
4194
+ opacity: 0.9;
3022
4195
  }
3023
-
3024
- .dsh-rail-tab-badge-placeholder {
3025
- width: 17px;
3026
- height: 17px;
3027
- min-height: 17px;
3028
- max-height: 17px;
3029
- flex-shrink: 0;
3030
- visibility: hidden;
4196
+ .dsh-ws-btn:disabled {
4197
+ opacity: 0.5;
4198
+ cursor: not-allowed;
3031
4199
  }
3032
- body[data-ds-dark-theme] .dsh-rail-tab-badge,
3033
- body[data-ds-dark-theme] .dsh-float-pill-badge,
3034
- body[data-ds-dark-theme] .dsh-preview-rail-badge {
3035
- background: #3b82f6;
4200
+ .dsh-ws-reset-btn {
4201
+ font-size: 11px;
4202
+ color: var(--dsw-static-deepseek-500, #4176e6);
4203
+ background: none;
4204
+ border: none;
4205
+ cursor: pointer;
4206
+ padding: 4px 0 0;
4207
+ text-align: left;
4208
+ text-decoration: underline;
3036
4209
  }
3037
- .dsh-rail-tab-item.active .dsh-rail-tab-badge,
3038
- .dsh-preview-right-rail-tab.active .dsh-float-pill-badge,
3039
- .dsh-preview-right-rail-tab.active .dsh-preview-rail-badge {
3040
- background: #ffffff;
3041
- color: #2563eb;
4210
+ body[data-ds-dark-theme] .dsh-ws-reset-btn {
4211
+ color: #60a5fa;
3042
4212
  }
3043
4213
 
3044
- /* Right-Side Drawer Container */
4214
+ /* Right-Side Persistent / Split Sidebar Container */
3045
4215
  .dsh-preview-drawer-container {
3046
4216
  position: fixed;
3047
4217
  top: 0;
3048
4218
  right: 0;
3049
4219
  bottom: 0;
4220
+ width: var(--dsh-preview-width, 620px);
3050
4221
  height: 100vh;
3051
- z-index: 9995;
4222
+ z-index: 990;
3052
4223
  background: var(--dsw-alias-bg-layer-1, #ffffff);
3053
4224
  color: var(--dsw-alias-label-primary, #0f172a);
3054
4225
  border-left: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
3055
- box-shadow: -8px 0 32px rgba(0, 0, 0, 0.14);
4226
+ box-shadow: -4px 0 16px rgba(0, 0, 0, 0.08);
3056
4227
  display: flex;
3057
4228
  flex-direction: column;
3058
4229
  box-sizing: border-box;
3059
4230
  overflow: hidden;
4231
+ transition: width 0.18s cubic-bezier(0.16, 1, 0.3, 1);
3060
4232
  animation: dshPreviewSlideIn 0.24s cubic-bezier(0.16, 1, 0.3, 1);
3061
4233
  }
3062
4234
  body[data-ds-dark-theme] .dsh-preview-drawer-container {
3063
4235
  background: var(--dsw-alias-bg-layer-1, #161922);
3064
4236
  color: var(--dsw-alias-label-primary, #e2e8f0);
3065
4237
  border-left-color: var(--dsw-alias-border-l3, #2d3748);
3066
- box-shadow: -12px 0 40px rgba(0, 0, 0, 0.45);
4238
+ box-shadow: -6px 0 20px rgba(0, 0, 0, 0.35);
3067
4239
  }
3068
4240
  .dsh-preview-drawer-container.dragging {
3069
4241
  user-select: none;
3070
- transition: none;
4242
+ transition: none !important;
3071
4243
  }
3072
4244
  @keyframes dshPreviewSlideIn {
3073
4245
  from { transform: translateX(100%); }
@@ -4290,6 +5462,124 @@
4290
5462
  }
4291
5463
  .dsh-tree-footer-dot { font-size: 8px; }
4292
5464
 
5465
+ /* Drag & Drop & Cut Styles */
5466
+ .dsh-tree-node-row.drop-target {
5467
+ background: rgba(14, 165, 233, 0.22) !important;
5468
+ outline: 1px dashed #0284c7 !important;
5469
+ color: #0284c7 !important;
5470
+ }
5471
+ body[data-ds-dark-theme] .dsh-tree-node-row.drop-target {
5472
+ background: rgba(56, 189, 248, 0.25) !important;
5473
+ outline-color: #38bdf8 !important;
5474
+ color: #38bdf8 !important;
5475
+ }
5476
+ .dsh-tree-node-list.drop-target-root {
5477
+ background: rgba(14, 165, 233, 0.08) !important;
5478
+ outline: 2px dashed #0284c7 !important;
5479
+ outline-offset: -4px;
5480
+ }
5481
+ body[data-ds-dark-theme] .dsh-tree-node-list.drop-target-root {
5482
+ background: rgba(56, 189, 248, 0.1) !important;
5483
+ outline-color: #38bdf8 !important;
5484
+ }
5485
+ .dsh-tree-node-row.is-dragged {
5486
+ opacity: 0.45;
5487
+ filter: grayscale(0.6);
5488
+ }
5489
+ .dsh-tree-node-row.is-cut {
5490
+ opacity: 0.5;
5491
+ filter: grayscale(0.5);
5492
+ border: 1px dashed #94a3b8;
5493
+ }
5494
+
5495
+ /* Inline Creation & Rename Input */
5496
+ .dsh-tree-inline-input-row {
5497
+ display: flex;
5498
+ align-items: center;
5499
+ height: 24px;
5500
+ margin: 1px 4px;
5501
+ gap: 3px;
5502
+ box-sizing: border-box;
5503
+ }
5504
+ .dsh-tree-inline-input {
5505
+ flex: 1;
5506
+ height: 20px;
5507
+ border-radius: 3px;
5508
+ border: 1px solid #38bdf8;
5509
+ background: var(--dsw-alias-bg-base, #ffffff);
5510
+ color: inherit;
5511
+ font-size: 11px;
5512
+ padding: 0 4px;
5513
+ outline: none;
5514
+ box-sizing: border-box;
5515
+ box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.4);
5516
+ }
5517
+ body[data-ds-dark-theme] .dsh-tree-inline-input {
5518
+ background: #1e2330;
5519
+ color: #f1f5f9;
5520
+ border-color: #38bdf8;
5521
+ }
5522
+
5523
+ /* Tree Context Menu */
5524
+ .dsh-tree-context-menu {
5525
+ position: fixed;
5526
+ min-width: 170px;
5527
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
5528
+ border: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
5529
+ border-radius: 6px;
5530
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2), 0 8px 10px -6px rgba(0, 0, 0, 0.15);
5531
+ padding: 4px;
5532
+ user-select: none;
5533
+ font-family: inherit;
5534
+ }
5535
+ body[data-ds-dark-theme] .dsh-tree-context-menu {
5536
+ background: #1a1e29;
5537
+ border-color: #2e384d;
5538
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5);
5539
+ }
5540
+ .dsh-menu-item {
5541
+ display: flex;
5542
+ align-items: center;
5543
+ gap: 8px;
5544
+ padding: 5px 8px;
5545
+ border-radius: 4px;
5546
+ font-size: 12px;
5547
+ cursor: pointer;
5548
+ color: var(--dsw-alias-label-primary, #1e293b);
5549
+ transition: background 0.1s;
5550
+ }
5551
+ body[data-ds-dark-theme] .dsh-menu-item {
5552
+ color: #e2e8f0;
5553
+ }
5554
+ .dsh-menu-item:hover {
5555
+ background: var(--dsw-alias-bg-hover, rgba(0, 0, 0, 0.06));
5556
+ }
5557
+ body[data-ds-dark-theme] .dsh-menu-item:hover {
5558
+ background: rgba(255, 255, 255, 0.09);
5559
+ }
5560
+ .dsh-menu-item-danger {
5561
+ color: #ef4444 !important;
5562
+ }
5563
+ .dsh-menu-item-danger:hover {
5564
+ background: rgba(239, 68, 68, 0.12) !important;
5565
+ }
5566
+ .dsh-menu-icon {
5567
+ font-size: 13px;
5568
+ width: 16px;
5569
+ display: inline-flex;
5570
+ align-items: center;
5571
+ justify-content: center;
5572
+ flex-shrink: 0;
5573
+ }
5574
+ .dsh-menu-divider {
5575
+ height: 1px;
5576
+ background: var(--dsw-alias-border-l3, #e2e8f0);
5577
+ margin: 3px 4px;
5578
+ }
5579
+ body[data-ds-dark-theme] .dsh-menu-divider {
5580
+ background: #2a3140;
5581
+ }
5582
+
4293
5583
  /* Splitter between Tree and Preview */
4294
5584
  .dsh-tree-splitter {
4295
5585
  width: 4px;
@@ -4409,85 +5699,89 @@
4409
5699
  body[data-ds-dark-theme] .tok-prop { color: #9cdcfe; }
4410
5700
  body[data-ds-dark-theme] .tok-selector { color: #d7ba7d; }
4411
5701
 
4412
- /* Code Editor Viewer */
5702
+ /* Code Editor Viewer (with unified line row architecture for 100% perfect alignment) */
4413
5703
  .dsh-code-editor-view {
4414
5704
  display: flex;
5705
+ flex-direction: column;
5706
+ width: 100%;
4415
5707
  min-height: 100%;
4416
- font-family: "Cascadia Code", "Fira Code", Consolas, Menlo, monospace;
4417
- font-size: 13px;
5708
+ overflow: auto;
5709
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Cascadia Code", "Fira Code", monospace;
5710
+ font-size: 12px;
4418
5711
  line-height: 20px;
4419
5712
  background: var(--dsw-alias-bg-layer-1, #ffffff);
4420
5713
  color: var(--dsw-alias-label-primary, #1e293b);
5714
+ padding: 8px 0;
5715
+ box-sizing: border-box;
5716
+ user-select: text;
4421
5717
  }
4422
5718
  body[data-ds-dark-theme] .dsh-code-editor-view {
4423
- background: #181b24;
5719
+ background: #161922;
4424
5720
  color: #d4d4d4;
4425
5721
  }
4426
5722
 
4427
- .dsh-code-gutter {
4428
- width: 44px;
4429
- min-width: 44px;
4430
- padding: 12px 6px 12px 0;
5723
+ .dsh-code-line {
5724
+ display: flex;
5725
+ flex-direction: row;
5726
+ align-items: flex-start;
5727
+ min-height: 20px;
5728
+ line-height: 20px;
5729
+ width: 100%;
5730
+ box-sizing: border-box;
5731
+ transition: background 0.1s;
5732
+ }
5733
+ .dsh-code-line:hover {
5734
+ background: rgba(0, 0, 0, 0.035);
5735
+ }
5736
+ body[data-ds-dark-theme] .dsh-code-line:hover {
5737
+ background: rgba(255, 255, 255, 0.035);
5738
+ }
5739
+ .dsh-code-line.dsh-line-highlight-flash {
5740
+ background: rgba(14, 165, 233, 0.22) !important;
5741
+ }
5742
+
5743
+ .dsh-line-gutter {
5744
+ display: inline-block;
5745
+ box-sizing: border-box;
5746
+ padding: 0 8px 0 6px;
4431
5747
  text-align: right;
4432
5748
  user-select: none;
4433
- background: var(--dsw-alias-bg-layer-2, #f8fafc);
4434
- border-right: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
4435
5749
  color: var(--dsw-alias-label-tertiary, #94a3b8);
4436
- font-size: 12px;
5750
+ font-size: 11px;
4437
5751
  line-height: 20px;
4438
5752
  flex-shrink: 0;
5753
+ border-right: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
5754
+ cursor: pointer;
5755
+ transition: color 0.1s;
4439
5756
  }
4440
- body[data-ds-dark-theme] .dsh-code-gutter {
4441
- background: #141720;
5757
+ body[data-ds-dark-theme] .dsh-line-gutter {
4442
5758
  border-right-color: #2a3140;
4443
- color: #64748b;
4444
- }
4445
-
4446
- .dsh-gutter-line {
4447
- height: 20px;
4448
- cursor: pointer;
4449
- padding-right: 8px;
5759
+ color: #55637a;
4450
5760
  }
4451
- .dsh-gutter-line:hover {
5761
+ .dsh-line-gutter:hover {
4452
5762
  color: var(--dsw-brand-primary, #0284c7);
4453
- font-weight: bold;
5763
+ font-weight: 600;
4454
5764
  }
4455
- .dsh-gutter-line.dsh-line-highlight-flash {
4456
- background: rgba(14, 165, 233, 0.25);
4457
- color: #0284c7;
5765
+ body[data-ds-dark-theme] .dsh-line-gutter:hover {
5766
+ color: #38bdf8;
4458
5767
  }
4459
5768
 
4460
- .dsh-code-content {
5769
+ .dsh-line-code {
4461
5770
  flex: 1;
4462
5771
  min-width: 0;
4463
- padding: 12px 16px;
4464
- overflow: auto;
4465
- }
4466
- .dsh-code-pre {
4467
- margin: 0;
5772
+ padding: 0 12px 0 10px;
5773
+ white-space: pre;
4468
5774
  font-family: inherit;
4469
5775
  font-size: inherit;
4470
5776
  line-height: 20px;
4471
5777
  tab-size: 2;
4472
- white-space: pre;
5778
+ box-sizing: border-box;
4473
5779
  }
4474
- .dsh-code-editor-view.word-wrap .dsh-code-pre {
5780
+ .dsh-code-editor-view.word-wrap .dsh-line-code {
4475
5781
  white-space: pre-wrap;
4476
5782
  word-break: normal;
4477
5783
  overflow-wrap: break-word;
4478
5784
  }
4479
- .dsh-code-editor-view {
4480
- display: flex;
4481
- min-height: 100%;
4482
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
4483
- font-size: 12.5px;
4484
- line-height: 20px;
4485
- background: var(--dsw-alias-bg-layer-1, #ffffff);
4486
- color: var(--dsw-alias-label-primary, #1e293b);
4487
- }
4488
- .dsh-code-pre code {
4489
- font-family: inherit;
4490
- }
4491
5785
 
4492
5786
  /* Interactive JSON Inspector */
4493
5787
  .dsh-json-inspector {