@rooode/dsh-plugin-preview 0.1.14 → 0.1.16

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
@@ -22,6 +22,7 @@
22
22
  // Storage & State Constants
23
23
  // =========================================================================
24
24
  const STORAGE_KEY_TABS = 'dsh:preview:tabs:v1';
25
+ const STORAGE_KEY_WS_TABS = 'dsh:preview:ws_tabs:v2';
25
26
  const STORAGE_KEY_WIDTH = 'dsh:preview:panel_width:v1';
26
27
  const STORAGE_KEY_TREE_OPEN = 'dsh:preview:tree:open:v1';
27
28
  const STORAGE_KEY_TREE_WIDTH = 'dsh:preview:tree:width:v1';
@@ -44,9 +45,11 @@
44
45
  ]);
45
46
 
46
47
  // =========================================================================
47
- // Global Event Bus & State Store
48
+ // Global Event Bus & State Store (Workspace-Scoped Tabs)
48
49
  // =========================================================================
49
50
  let clientCtx = null;
51
+ let workspaceTabsMap = {}; // { [normalizedWsKey: string]: { tabs: Tab[], activeTabId: string | null } }
52
+ let currentWorkspaceKey = '';
50
53
  let globalTabs = [];
51
54
  let globalActiveTabId = null;
52
55
  let globalIsPanelOpen = false;
@@ -58,6 +61,11 @@
58
61
  let globalLastOpenTime = 0;
59
62
  const stateListeners = new Set();
60
63
 
64
+ function normalizeWorkspaceKey(rawPath) {
65
+ if (!rawPath || typeof rawPath !== 'string' || !rawPath.trim()) return '__default__';
66
+ return rawPath.trim().replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
67
+ }
68
+
61
69
  try {
62
70
  const savedManualWs = localStorage.getItem(STORAGE_KEY_MANUAL_WS);
63
71
  if (savedManualWs && savedManualWs.trim()) {
@@ -92,28 +100,86 @@
92
100
  }
93
101
  } catch (e) {}
94
102
 
95
- try {
96
- const savedTabs = localStorage.getItem(STORAGE_KEY_TABS);
97
- if (savedTabs) {
98
- globalTabs = JSON.parse(savedTabs);
99
- if (globalTabs.length > 0) {
100
- globalActiveTabId = globalTabs[0].id;
103
+ function loadAllWorkspaceTabsFromStorage() {
104
+ try {
105
+ const raw = localStorage.getItem(STORAGE_KEY_WS_TABS);
106
+ if (raw) {
107
+ workspaceTabsMap = JSON.parse(raw) || {};
108
+ } else {
109
+ // Backward-compatible fallback for legacy STORAGE_KEY_TABS
110
+ const oldRaw = localStorage.getItem(STORAGE_KEY_TABS);
111
+ if (oldRaw) {
112
+ const oldTabs = JSON.parse(oldRaw);
113
+ if (Array.isArray(oldTabs) && oldTabs.length > 0) {
114
+ const wsKey = normalizeWorkspaceKey(getCurrentWorkspaceRoot());
115
+ workspaceTabsMap[wsKey] = {
116
+ tabs: oldTabs,
117
+ activeTabId: oldTabs[0] ? oldTabs[0].id : null,
118
+ };
119
+ }
120
+ }
101
121
  }
102
- }
103
- } catch (e) {}
104
-
105
- function notifyStateChange() {
106
- for (const fn of stateListeners) {
107
- try { fn(); } catch (err) { console.error('[Preview] State notify error:', err); }
122
+ } catch (e) {
123
+ workspaceTabsMap = {};
108
124
  }
109
125
  }
110
126
 
111
127
  function saveTabsToStorage() {
112
128
  try {
113
- localStorage.setItem(STORAGE_KEY_TABS, JSON.stringify(globalTabs.slice(0, 15)));
129
+ const wsKey = currentWorkspaceKey || normalizeWorkspaceKey(getCurrentWorkspaceRoot());
130
+ if (!workspaceTabsMap || typeof workspaceTabsMap !== 'object') {
131
+ workspaceTabsMap = {};
132
+ }
133
+ workspaceTabsMap[wsKey] = {
134
+ tabs: (globalTabs || []).slice(0, 20),
135
+ activeTabId: globalActiveTabId || null,
136
+ };
137
+ localStorage.setItem(STORAGE_KEY_WS_TABS, JSON.stringify(workspaceTabsMap));
114
138
  } catch (e) {}
115
139
  }
116
140
 
141
+ function syncCurrentWorkspaceTabs(targetWsRoot) {
142
+ const newWsRoot = targetWsRoot || getCurrentWorkspaceRoot();
143
+ const newKey = normalizeWorkspaceKey(newWsRoot);
144
+
145
+ // Save previous workspace's tabs
146
+ if (currentWorkspaceKey && currentWorkspaceKey !== newKey) {
147
+ if (!workspaceTabsMap || typeof workspaceTabsMap !== 'object') {
148
+ workspaceTabsMap = {};
149
+ }
150
+ workspaceTabsMap[currentWorkspaceKey] = {
151
+ tabs: (globalTabs || []).slice(0, 20),
152
+ activeTabId: globalActiveTabId || null,
153
+ };
154
+ }
155
+
156
+ currentWorkspaceKey = newKey;
157
+
158
+ // Load target workspace's tabs
159
+ const stored = workspaceTabsMap && workspaceTabsMap[newKey];
160
+ if (stored && Array.isArray(stored.tabs)) {
161
+ globalTabs = [...stored.tabs];
162
+ globalActiveTabId = stored.activeTabId && globalTabs.some(t => t.id === stored.activeTabId)
163
+ ? stored.activeTabId
164
+ : (globalTabs[0] ? globalTabs[0].id : null);
165
+ } else {
166
+ globalTabs = [];
167
+ globalActiveTabId = null;
168
+ }
169
+
170
+ saveTabsToStorage();
171
+ notifyStateChange();
172
+ }
173
+
174
+ // Initial load
175
+ loadAllWorkspaceTabsFromStorage();
176
+
177
+ function notifyStateChange() {
178
+ for (const fn of stateListeners) {
179
+ try { fn(); } catch (err) { console.error('[Preview] State notify error:', err); }
180
+ }
181
+ }
182
+
117
183
  function setPanelWidth(width) {
118
184
  const maxW = Math.floor(window.innerWidth * 0.92);
119
185
  globalPanelWidth = Math.max(MIN_PANEL_WIDTH, Math.min(width, maxW));
@@ -435,6 +501,13 @@
435
501
  const ext = getFileExtension(fullPath);
436
502
  const tabId = 'tab:' + fullPath.toLowerCase();
437
503
 
504
+ // Ensure current workspace tabs are loaded
505
+ const currentWs = getCurrentWorkspaceRoot();
506
+ const currentKey = normalizeWorkspaceKey(currentWs);
507
+ if (currentWorkspaceKey !== currentKey) {
508
+ syncCurrentWorkspaceTabs(currentWs);
509
+ }
510
+
438
511
  const existingIndex = globalTabs.findIndex(t => t.id === tabId || t.filePath.toLowerCase() === fullPath.toLowerCase());
439
512
  if (existingIndex >= 0) {
440
513
  globalActiveTabId = globalTabs[existingIndex].id;
@@ -509,43 +582,56 @@
509
582
  const oldNorm = oldPath.replace(/\\/g, '/').toLowerCase();
510
583
  const newNorm = newPath.replace(/\\/g, '/');
511
584
 
512
- globalTabs = globalTabs.map(tab => {
513
- const tabNorm = tab.filePath.replace(/\\/g, '/').toLowerCase();
514
- if (!isDir) {
515
- if (tabNorm === oldNorm) {
516
- changed = true;
517
- const newFileName = getFileName(newPath);
518
- const newExt = getFileExtension(newPath);
519
- const newCat = getFileTypeCategory(newPath);
520
- return {
521
- ...tab,
522
- id: 'tab:' + newPath.toLowerCase(),
523
- filePath: newPath,
524
- title: newFileName,
525
- extension: newExt,
526
- category: newCat,
527
- };
585
+ const updateList = (list) => {
586
+ if (!Array.isArray(list)) return [];
587
+ return list.map(tab => {
588
+ const tabNorm = tab.filePath.replace(/\\/g, '/').toLowerCase();
589
+ if (!isDir) {
590
+ if (tabNorm === oldNorm) {
591
+ changed = true;
592
+ const newFileName = getFileName(newPath);
593
+ const newExt = getFileExtension(newPath);
594
+ const newCat = getFileTypeCategory(newPath);
595
+ return {
596
+ ...tab,
597
+ id: 'tab:' + newPath.toLowerCase(),
598
+ filePath: newPath,
599
+ title: newFileName,
600
+ extension: newExt,
601
+ category: newCat,
602
+ };
603
+ }
604
+ } else {
605
+ if (tabNorm === oldNorm || tabNorm.startsWith(oldNorm + '/')) {
606
+ changed = true;
607
+ const subPath = tab.filePath.replace(/\\/g, '/').slice(oldNorm.length);
608
+ const updatedFilePath = newNorm + subPath;
609
+ const newFileName = getFileName(updatedFilePath);
610
+ const newExt = getFileExtension(updatedFilePath);
611
+ const newCat = getFileTypeCategory(updatedFilePath);
612
+ return {
613
+ ...tab,
614
+ id: 'tab:' + updatedFilePath.toLowerCase(),
615
+ filePath: updatedFilePath,
616
+ title: newFileName,
617
+ extension: newExt,
618
+ category: newCat,
619
+ };
620
+ }
528
621
  }
529
- } else {
530
- if (tabNorm === oldNorm || tabNorm.startsWith(oldNorm + '/')) {
531
- changed = true;
532
- const subPath = tab.filePath.replace(/\\/g, '/').slice(oldNorm.length);
533
- const updatedFilePath = newNorm + subPath;
534
- const newFileName = getFileName(updatedFilePath);
535
- const newExt = getFileExtension(updatedFilePath);
536
- const newCat = getFileTypeCategory(updatedFilePath);
537
- return {
538
- ...tab,
539
- id: 'tab:' + updatedFilePath.toLowerCase(),
540
- filePath: updatedFilePath,
541
- title: newFileName,
542
- extension: newExt,
543
- category: newCat,
544
- };
622
+ return tab;
623
+ });
624
+ };
625
+
626
+ globalTabs = updateList(globalTabs);
627
+
628
+ if (workspaceTabsMap && typeof workspaceTabsMap === 'object') {
629
+ for (const k of Object.keys(workspaceTabsMap)) {
630
+ if (workspaceTabsMap[k] && Array.isArray(workspaceTabsMap[k].tabs)) {
631
+ workspaceTabsMap[k].tabs = updateList(workspaceTabsMap[k].tabs);
545
632
  }
546
633
  }
547
- return tab;
548
- });
634
+ }
549
635
 
550
636
  if (changed) {
551
637
  if (globalActiveTabId) {
@@ -567,24 +653,37 @@
567
653
  if (!targetPath) return;
568
654
  let changed = false;
569
655
  const delNorm = targetPath.replace(/\\/g, '/').toLowerCase();
570
- const nextTabs = globalTabs.filter(tab => {
571
- const tabNorm = tab.filePath.replace(/\\/g, '/').toLowerCase();
572
- if (!isDir) {
573
- if (tabNorm === delNorm) {
574
- changed = true;
575
- return false;
656
+
657
+ const filterList = (list) => {
658
+ if (!Array.isArray(list)) return [];
659
+ return list.filter(tab => {
660
+ const tabNorm = tab.filePath.replace(/\\/g, '/').toLowerCase();
661
+ if (!isDir) {
662
+ if (tabNorm === delNorm) {
663
+ changed = true;
664
+ return false;
665
+ }
666
+ } else {
667
+ if (tabNorm === delNorm || tabNorm.startsWith(delNorm + '/')) {
668
+ changed = true;
669
+ return false;
670
+ }
576
671
  }
577
- } else {
578
- if (tabNorm === delNorm || tabNorm.startsWith(delNorm + '/')) {
579
- changed = true;
580
- return false;
672
+ return true;
673
+ });
674
+ };
675
+
676
+ globalTabs = filterList(globalTabs);
677
+
678
+ if (workspaceTabsMap && typeof workspaceTabsMap === 'object') {
679
+ for (const k of Object.keys(workspaceTabsMap)) {
680
+ if (workspaceTabsMap[k] && Array.isArray(workspaceTabsMap[k].tabs)) {
681
+ workspaceTabsMap[k].tabs = filterList(workspaceTabsMap[k].tabs);
581
682
  }
582
683
  }
583
- return true;
584
- });
684
+ }
585
685
 
586
686
  if (changed) {
587
- globalTabs = nextTabs;
588
687
  if (!globalTabs.some(t => t.id === globalActiveTabId)) {
589
688
  globalActiveTabId = globalTabs.length > 0 ? globalTabs[0].id : null;
590
689
  }
@@ -3158,10 +3257,12 @@
3158
3257
  }, [normalizedContent, category, isHighlighted, searchQuery]);
3159
3258
 
3160
3259
  const lineCount = highlightedLines.length;
3161
- const digitCount = Math.max(2, String(lineCount).length);
3162
- const gutterWidth = Math.max(24, digitCount * 7.5 + 12);
3260
+ const digitCount = Math.max(1, String(lineCount).length);
3163
3261
 
3164
- return h('div', { className: `dsh-code-editor-view ${isWordWrap ? 'word-wrap' : ''}` },
3262
+ return h('div', {
3263
+ className: `dsh-code-editor-view ${isWordWrap ? 'word-wrap' : ''}`,
3264
+ style: { '--dsh-gutter-digits': digitCount }
3265
+ },
3165
3266
  highlightedLines.map((lineHtml, i) => {
3166
3267
  const lineNum = i + 1;
3167
3268
  return h('div', {
@@ -3172,7 +3273,6 @@
3172
3273
  h('span', {
3173
3274
  id: `line-gutter-${lineNum}`,
3174
3275
  className: 'dsh-line-gutter',
3175
- style: { minWidth: `${gutterWidth}px` },
3176
3276
  onClick: () => onLineClick && onLineClick(lineNum),
3177
3277
  title: `第 ${lineNum} 行 (点击定位)`,
3178
3278
  }, lineNum),
@@ -3986,7 +4086,7 @@
3986
4086
  if (current && current !== lastKnownWorkspace) {
3987
4087
  console.log('[Preview] Active workspace auto-switched:', lastKnownWorkspace, '->', current);
3988
4088
  lastKnownWorkspace = current;
3989
- notifyStateChange();
4089
+ syncCurrentWorkspaceTabs(current);
3990
4090
  }
3991
4091
  };
3992
4092
 
@@ -5961,10 +6061,13 @@
5961
6061
  .dsh-line-gutter {
5962
6062
  display: inline-block;
5963
6063
  box-sizing: border-box;
5964
- padding: 0 8px 0 6px;
6064
+ width: calc(var(--dsh-gutter-digits, 2) * 1ch + 8px);
6065
+ min-width: calc(var(--dsh-gutter-digits, 2) * 1ch + 8px);
6066
+ padding: 0 4px 0 2px;
5965
6067
  text-align: right;
5966
6068
  user-select: none;
5967
6069
  color: var(--dsw-alias-label-tertiary, #94a3b8);
6070
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Cascadia Code", "Fira Code", monospace;
5968
6071
  font-size: 11px;
5969
6072
  line-height: 20px;
5970
6073
  flex-shrink: 0;
@@ -5987,7 +6090,7 @@
5987
6090
  .dsh-line-code {
5988
6091
  flex: 1;
5989
6092
  min-width: 0;
5990
- padding: 0 12px 0 10px;
6093
+ padding: 0 8px 0 6px;
5991
6094
  white-space: pre;
5992
6095
  font-family: inherit;
5993
6096
  font-size: inherit;
@@ -6290,6 +6393,9 @@
6290
6393
  window.__dsh_open_preview = openPreviewFile;
6291
6394
  }
6292
6395
 
6396
+ loadAllWorkspaceTabsFromStorage();
6397
+ syncCurrentWorkspaceTabs(getCurrentWorkspaceRoot());
6398
+
6293
6399
  injectStyles();
6294
6400
  setupFileInterceptors(ctx);
6295
6401
 
package/lib/index.js CHANGED
@@ -29,7 +29,7 @@ export class PreviewService extends Service {
29
29
  handler: this.handleHttpRequest.bind(this),
30
30
  });
31
31
  });
32
- console.log('[PreviewService] Registered /api/preview route on webServer (v0.1.14)');
32
+ console.log('[PreviewService] Registered /api/preview route on webServer (v0.1.16)');
33
33
  } else {
34
34
  console.warn('[PreviewService] webServer not available yet in ctx');
35
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rooode/dsh-plugin-preview",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
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",