@rooode/dsh-plugin-preview 0.1.19 → 0.1.21

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 +148 -22
  2. package/lib/index.js +52 -14
  3. package/package.json +2 -2
package/lib/client.js CHANGED
@@ -27,6 +27,7 @@
27
27
  const STORAGE_KEY_TREE_OPEN = 'dsh:preview:tree:open:v1';
28
28
  const STORAGE_KEY_TREE_WIDTH = 'dsh:preview:tree:width:v1';
29
29
  const STORAGE_KEY_MANUAL_WS = 'dsh:preview:manual_workspace:v1';
30
+ const STORAGE_KEY_SHOW_HIDDEN = 'dsh:preview:tree:show_hidden:v1';
30
31
  const DEFAULT_PANEL_WIDTH = 700;
31
32
  const MIN_PANEL_WIDTH = 380;
32
33
  const DEFAULT_TREE_WIDTH = 220;
@@ -55,6 +56,7 @@
55
56
  let globalIsPanelOpen = false;
56
57
  let globalIsTreeOpen = true;
57
58
  let globalIsMaximized = false;
59
+ let globalShowHidden = true;
58
60
  let globalManualWorkspace = null;
59
61
  let globalPanelWidth = DEFAULT_PANEL_WIDTH;
60
62
  let globalTreeWidth = DEFAULT_TREE_WIDTH;
@@ -73,6 +75,13 @@
73
75
  }
74
76
  } catch (e) {}
75
77
 
78
+ try {
79
+ const savedShowHidden = localStorage.getItem(STORAGE_KEY_SHOW_HIDDEN);
80
+ if (savedShowHidden !== null) {
81
+ globalShowHidden = savedShowHidden === 'true';
82
+ }
83
+ } catch (e) {}
84
+
76
85
  try {
77
86
  const savedWidth = localStorage.getItem(STORAGE_KEY_WIDTH);
78
87
  if (savedWidth) {
@@ -100,6 +109,14 @@
100
109
  }
101
110
  } catch (e) {}
102
111
 
112
+ function setGlobalShowHidden(show) {
113
+ globalShowHidden = !!show;
114
+ try {
115
+ localStorage.setItem(STORAGE_KEY_SHOW_HIDDEN, String(globalShowHidden));
116
+ } catch (e) {}
117
+ notifyStateChange();
118
+ }
119
+
103
120
  function loadAllWorkspaceTabsFromStorage() {
104
121
  try {
105
122
  const raw = localStorage.getItem(STORAGE_KEY_WS_TABS);
@@ -247,10 +264,23 @@
247
264
  return parts[parts.length - 1] || filePath;
248
265
  }
249
266
 
267
+ const KNOWN_TEXT_DOTFILES = new Set([
268
+ '.gitignore', '.gitattributes', '.gitmodules', '.gitconfig',
269
+ '.dockerignore', 'dockerfile', '.editorconfig',
270
+ '.prettierrc', '.eslintrc', '.eslintignore', '.prettierignore',
271
+ '.npmrc', '.nvmrc', '.yarnrc', '.babelrc', '.swcrc',
272
+ '.cursorrules', '.commitlintrc', '.env',
273
+ 'license', 'makefile', 'gemfile', 'procfile', 'caddyfile'
274
+ ]);
275
+
250
276
  function isPreviewableFile(filePath) {
251
277
  if (!filePath) return false;
278
+ const cleanName = getFileName(filePath).toLowerCase();
279
+ if (KNOWN_TEXT_DOTFILES.has(cleanName) || cleanName.startsWith('.env')) return true;
252
280
  const ext = getFileExtension(filePath);
253
- return SUPPORTED_EXTENSIONS.has(ext);
281
+ if (SUPPORTED_EXTENSIONS.has(ext)) return true;
282
+ if (cleanName.startsWith('.') && !['.png', '.jpg', '.jpeg', '.gif', '.zip', '.tar', '.gz', '.exe', '.dll', '.bin', '.pdf'].includes(ext)) return true;
283
+ return false;
254
284
  }
255
285
 
256
286
  function formatBytes(bytes) {
@@ -263,8 +293,20 @@
263
293
 
264
294
  function getFileIcon(node) {
265
295
  if (node.isDir) {
296
+ const dName = (node.name || '').toLowerCase();
297
+ if (dName === '.git') return { icon: '🌿', color: '#f05032' };
298
+ if (dName === '.github') return { icon: '🐙', color: '#8b5cf6' };
299
+ if (dName === '.vscode' || dName === '.idea' || dName === '.cursor') return { icon: '⚙️', color: '#38bdf8' };
266
300
  return { icon: '📁', color: '#eab308' };
267
301
  }
302
+ const rawName = (node.name || '').toLowerCase();
303
+ if (rawName.startsWith('.env')) return { icon: '🔒', color: '#10b981' };
304
+ if (rawName.startsWith('.git')) return { icon: '🌿', color: '#f05032' };
305
+ if (rawName === '.dockerignore' || rawName === 'dockerfile') return { icon: '🐳', color: '#2496ed' };
306
+ if (rawName.startsWith('.prettier') || rawName.startsWith('.eslint')) return { icon: '✨', color: '#f59e0b' };
307
+ if (rawName === '.npmrc' || rawName === '.nvmrc' || rawName === '.yarnrc') return { icon: '📦', color: '#cb3837' };
308
+ if (rawName === '.editorconfig' || rawName === '.cursorrules' || rawName === '.commitlintrc') return { icon: '⚙️', color: '#0284c7' };
309
+
268
310
  const ext = (node.extension || getFileExtension(node.name || node.path || '')).toLowerCase();
269
311
  if (ext === '.md' || ext === '.markdown') return { icon: '📝', color: '#0284c7' };
270
312
  if (ext === '.json') return { icon: '{ }', color: '#10b981' };
@@ -287,9 +329,16 @@
287
329
  return { icon: '📄', color: '#94a3b8' };
288
330
  }
289
331
 
290
-
291
332
  function getFileTypeCategory(filePath) {
292
333
  if (!filePath) return 'text';
334
+ const cleanName = getFileName(filePath).toLowerCase();
335
+ if (cleanName.startsWith('.env') || cleanName === '.gitignore' || cleanName === '.gitattributes' || cleanName === '.dockerignore' || cleanName === '.editorconfig' || cleanName === '.npmrc' || cleanName === '.nvmrc' || cleanName === '.cursorrules') return 'config';
336
+ if (cleanName.startsWith('.prettier') || cleanName.startsWith('.eslint')) {
337
+ if (cleanName.endsWith('.json')) return 'json';
338
+ if (cleanName.endsWith('.js') || cleanName.endsWith('.cjs')) return 'javascript';
339
+ if (cleanName.endsWith('.yml') || cleanName.endsWith('.yaml')) return 'yaml';
340
+ return 'json';
341
+ }
293
342
  const ext = getFileExtension(filePath).toLowerCase();
294
343
  if (['.md', '.markdown', '.mdown', '.mkdn', '.mdwn'].includes(ext)) return 'markdown';
295
344
  if (['.json', '.jsonc', '.json5', '.geojson', '.lock'].includes(ext)) return 'json';
@@ -958,7 +1007,7 @@
958
1007
  return t.split('|').map(c => c.trim());
959
1008
  };
960
1009
 
961
- const tableRegex = /^(\|?.+\|.*)\n(\|?[\s\-:|]+\|.*)\n((?:\|?.*\|.*(?:\n|$))*)/gm;
1010
+ const tableRegex = /^([ \t]*\|?.+\|.*)\n([ \t]*\|?[ \t]*:?-+:?[ \t]*(?:\|[ \t]*:?-+:?[ \t]*)+\|?[ \t]*)(?:\n((?:[ \t]*\|.*\|.*(?:\n|$))*))?/gm;
962
1011
  text = text.replace(tableRegex, (match, headerLine, alignLine, bodyLines) => {
963
1012
  const headerCells = parseTableRow(headerLine);
964
1013
  const alignCells = parseTableRow(alignLine);
@@ -975,16 +1024,18 @@
975
1024
  });
976
1025
  tableHtml += '</tr></thead><tbody>';
977
1026
 
978
- const rows = bodyLines.trim().split('\n');
979
- for (const row of rows) {
980
- if (!row.trim()) continue;
981
- const cells = parseTableRow(row);
982
- tableHtml += '<tr>';
983
- cells.forEach((cell, i) => {
984
- const align = aligns[i] || 'left';
985
- tableHtml += `<td style="text-align:${align}">${formatInlineStyles(cell)}</td>`;
986
- });
987
- tableHtml += '</tr>';
1027
+ if (bodyLines) {
1028
+ const rows = bodyLines.trim().split('\n');
1029
+ for (const row of rows) {
1030
+ if (!row.trim()) continue;
1031
+ const cells = parseTableRow(row);
1032
+ tableHtml += '<tr>';
1033
+ cells.forEach((cell, i) => {
1034
+ const align = aligns[i] || 'left';
1035
+ tableHtml += `<td style="text-align:${align}">${formatInlineStyles(cell)}</td>`;
1036
+ });
1037
+ tableHtml += '</tr>';
1038
+ }
988
1039
  }
989
1040
  tableHtml += '</tbody></table></div>\n\n';
990
1041
  return tableHtml;
@@ -1276,6 +1327,41 @@
1276
1327
  );
1277
1328
  }
1278
1329
 
1330
+ function IconEye() {
1331
+ return h('svg', {
1332
+ width: 13,
1333
+ height: 13,
1334
+ viewBox: '0 0 16 16',
1335
+ fill: 'none',
1336
+ stroke: 'currentColor',
1337
+ strokeWidth: 1.4,
1338
+ strokeLinecap: 'round',
1339
+ strokeLinejoin: 'round',
1340
+ style: { display: 'block', flexShrink: 0 }
1341
+ },
1342
+ h('path', { d: 'M1.5 8s2.5-4.5 6.5-4.5 6.5 4.5 6.5 4.5-2.5 4.5-6.5 4.5S1.5 8 1.5 8z' }),
1343
+ h('circle', { cx: 8, cy: 8, r: 2.2 })
1344
+ );
1345
+ }
1346
+
1347
+ function IconEyeOff() {
1348
+ return h('svg', {
1349
+ width: 13,
1350
+ height: 13,
1351
+ viewBox: '0 0 16 16',
1352
+ fill: 'none',
1353
+ stroke: 'currentColor',
1354
+ strokeWidth: 1.4,
1355
+ strokeLinecap: 'round',
1356
+ strokeLinejoin: 'round',
1357
+ style: { display: 'block', flexShrink: 0 }
1358
+ },
1359
+ h('path', { d: 'M2 8s2.2-3.8 6-3.8c1.5 0 2.8.6 3.9 1.5M14 8s-2.2 3.8-6 3.8c-1.5 0-2.8-.6-3.9-1.5' }),
1360
+ h('circle', { cx: 8, cy: 8, r: 2.2 }),
1361
+ h('line', { x1: 2, y1: 2, x2: 14, y2: 14 })
1362
+ );
1363
+ }
1364
+
1279
1365
  function IconChevronRight() {
1280
1366
  return h('svg', {
1281
1367
  width: 9,
@@ -1451,7 +1537,7 @@
1451
1537
  );
1452
1538
  }
1453
1539
 
1454
- function TreeContextMenu({ x, y, node, isRoot, clipboard, onAction, onClose }) {
1540
+ function TreeContextMenu({ x, y, node, isRoot, showHidden, clipboard, onAction, onClose }) {
1455
1541
  const menuRef = useRef(null);
1456
1542
  useEffect(() => {
1457
1543
  const handleDown = (e) => {
@@ -1533,6 +1619,12 @@
1533
1619
  )
1534
1620
  ] : null,
1535
1621
 
1622
+ h('div', { key: 'div-hidden', className: 'dsh-menu-divider' }),
1623
+ h('div', { key: 'toggle-hidden', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('toggle-hidden'); } },
1624
+ h('span', { className: 'dsh-menu-icon' }, showHidden ? '👁️' : '🙈'),
1625
+ h('span', null, showHidden ? '隐藏点文件与隐藏项' : '显示点文件与隐藏项')
1626
+ ),
1627
+
1536
1628
  isRoot ? [
1537
1629
  h('div', { key: 'refresh', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('refresh'); } },
1538
1630
  h('span', { className: 'dsh-menu-icon' }, '🔄'),
@@ -1634,7 +1726,7 @@
1634
1726
 
1635
1727
  return h(Fragment, null,
1636
1728
  h('div', {
1637
- className: `dsh-tree-node-row ${isDir ? 'is-dir' : 'is-file'} ${isActive ? 'active' : ''} ${isDropTarget ? 'drop-target' : ''} ${isDragged ? 'is-dragged' : ''} ${isCut ? 'is-cut' : ''}`,
1729
+ className: `dsh-tree-node-row ${isDir ? 'is-dir' : 'is-file'} ${isActive ? 'active' : ''} ${isDropTarget ? 'drop-target' : ''} ${isDragged ? 'is-dragged' : ''} ${isCut ? 'is-cut' : ''} ${node.isHidden ? 'is-hidden' : ''}`,
1638
1730
  style: indentStyle,
1639
1731
  onClick: handleClick,
1640
1732
  onContextMenu: handleContextMenu,
@@ -1644,7 +1736,7 @@
1644
1736
  onDragOver: handleDragOver,
1645
1737
  onDragLeave: handleDragLeave,
1646
1738
  onDrop: handleDrop,
1647
- title: `${node.name}\n${node.path}${node.size ? ' (' + formatBytes(node.size) + ')' : ''}`,
1739
+ title: `${node.name}${node.isHidden ? ' (隐藏项)' : ''}\n${node.path}${node.size ? ' (' + formatBytes(node.size) + ')' : ''}`,
1648
1740
  },
1649
1741
  // Chevron indicator for directory
1650
1742
  isDir ? h('span', {
@@ -1771,6 +1863,7 @@
1771
1863
  const [clipboard, setClipboard] = useState(null); // { action: 'cut', path: string, name: string, isDir: boolean } | null
1772
1864
 
1773
1865
  const [currentWs, setCurrentWs] = useState(getCurrentWorkspaceRoot());
1866
+ const [showHidden, setShowHidden] = useState(globalShowHidden);
1774
1867
 
1775
1868
  useEffect(() => {
1776
1869
  const handleStateUpdate = () => {
@@ -1778,10 +1871,13 @@
1778
1871
  if (newWs !== currentWs) {
1779
1872
  setCurrentWs(newWs);
1780
1873
  }
1874
+ if (globalShowHidden !== showHidden) {
1875
+ setShowHidden(globalShowHidden);
1876
+ }
1781
1877
  };
1782
1878
  stateListeners.add(handleStateUpdate);
1783
1879
  return () => stateListeners.delete(handleStateUpdate);
1784
- }, [currentWs]);
1880
+ }, [currentWs, showHidden]);
1785
1881
 
1786
1882
  const loadTree = useCallback(async (force = false, targetWs) => {
1787
1883
  setIsLoading(true);
@@ -1793,26 +1889,29 @@
1793
1889
  return;
1794
1890
  }
1795
1891
  const query = encodeURIComponent(rootPath);
1796
- const res = await fetch(`/api/preview/workspace-tree?path=${query}`);
1892
+ const res = await fetch(`/api/preview/workspace-tree?path=${query}&showHidden=${showHidden}`);
1797
1893
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
1798
1894
  const json = await res.json();
1799
1895
  if (!json.ok) throw new Error(json?.error?.message || '读取工作区目录失败');
1800
1896
  setTreeData(json);
1801
- // By default expand top 1 level directories
1897
+ // By default expand top 1 level directories if not already expanded
1802
1898
  if (json.tree) {
1803
1899
  const topDirs = json.tree.filter(n => n.isDir).map(n => n.path);
1804
- setExpandedPaths(new Set(topDirs));
1900
+ setExpandedPaths(prev => {
1901
+ if (prev.size === 0) return new Set(topDirs);
1902
+ return prev;
1903
+ });
1805
1904
  }
1806
1905
  } catch (err) {
1807
1906
  setError(err.message || '加载工作空间文件树失败');
1808
1907
  } finally {
1809
1908
  setIsLoading(false);
1810
1909
  }
1811
- }, [currentWs]);
1910
+ }, [currentWs, showHidden]);
1812
1911
 
1813
1912
  useEffect(() => {
1814
1913
  loadTree(false, currentWs);
1815
- }, [loadTree, currentWs, globalManualWorkspace]);
1914
+ }, [loadTree, currentWs, showHidden, globalManualWorkspace]);
1816
1915
 
1817
1916
  const handleToggleExpand = (dirPath) => {
1818
1917
  setExpandedPaths(prev => {
@@ -2059,11 +2158,20 @@
2059
2158
  if (node) triggerOpenNative(node.path);
2060
2159
  } else if (action === 'delete') {
2061
2160
  if (node) handleDelete(node);
2161
+ } else if (action === 'toggle-hidden') {
2162
+ handleToggleShowHidden();
2062
2163
  } else if (action === 'refresh') {
2063
2164
  loadTree(true);
2064
2165
  }
2065
2166
  };
2066
2167
 
2168
+ const handleToggleShowHidden = () => {
2169
+ const next = !showHidden;
2170
+ setShowHidden(next);
2171
+ setGlobalShowHidden(next);
2172
+ showToast(next ? '已开启显示隐藏文件与点文件' : '已隐藏点文件与隐藏项', 'info');
2173
+ };
2174
+
2067
2175
  const rootPath = treeData?.workspaceRoot || currentWs;
2068
2176
  const isCreatingAtRoot = creatingItem && creatingItem.parentPath && rootPath && creatingItem.parentPath.replace(/\\/g, '/').toLowerCase() === rootPath.replace(/\\/g, '/').toLowerCase();
2069
2177
  const isRootDropTarget = dragOverNode && rootPath && dragOverNode.path && dragOverNode.path.replace(/\\/g, '/').toLowerCase() === rootPath.replace(/\\/g, '/').toLowerCase();
@@ -2096,6 +2204,12 @@
2096
2204
  onClick: () => handleStartCreate(rootPath, 'dir'),
2097
2205
  title: '新建文件夹 (在根目录)',
2098
2206
  }, h(IconNewFolder)),
2207
+ h('button', {
2208
+ type: 'button',
2209
+ className: `dsh-tree-action-btn ${showHidden ? 'active' : ''}`,
2210
+ onClick: handleToggleShowHidden,
2211
+ title: showHidden ? '隐藏点文件与隐藏项 (当前已显示)' : '显示点文件与隐藏项 (当前已隐藏)',
2212
+ }, showHidden ? h(IconEye) : h(IconEyeOff)),
2099
2213
  h('button', {
2100
2214
  type: 'button',
2101
2215
  className: 'dsh-tree-action-btn',
@@ -2287,6 +2401,7 @@
2287
2401
  y: contextMenu.y,
2288
2402
  node: contextMenu.node,
2289
2403
  isRoot: contextMenu.isRoot,
2404
+ showHidden,
2290
2405
  clipboard,
2291
2406
  onAction: handleContextMenuAction,
2292
2407
  onClose: () => setContextMenu(null),
@@ -5607,6 +5722,10 @@
5607
5722
  background: rgba(255, 255, 255, 0.1);
5608
5723
  color: #ffffff;
5609
5724
  }
5725
+ .dsh-tree-action-btn.active {
5726
+ color: var(--dsw-static-deepseek-500, #3b82f6);
5727
+ background: rgba(59, 130, 246, 0.12);
5728
+ }
5610
5729
 
5611
5730
  .dsh-tree-sidebar-search {
5612
5731
  padding: 6px 8px;
@@ -5722,6 +5841,13 @@
5722
5841
  background: rgba(56, 189, 248, 0.16);
5723
5842
  color: #38bdf8;
5724
5843
  }
5844
+ .dsh-tree-node-row.is-hidden {
5845
+ opacity: 0.72;
5846
+ }
5847
+ .dsh-tree-node-row.is-hidden:hover,
5848
+ .dsh-tree-node-row.is-hidden.active {
5849
+ opacity: 1;
5850
+ }
5725
5851
 
5726
5852
  .dsh-tree-chevron {
5727
5853
  width: 14px;
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.19)');
32
+ console.log('[PreviewService] Registered /api/preview route on webServer (v0.1.21)');
33
33
  } else {
34
34
  console.warn('[PreviewService] webServer not available yet in ctx');
35
35
  }
@@ -130,7 +130,19 @@ export class PreviewService extends Service {
130
130
  });
131
131
  }
132
132
 
133
- readWorkspaceTree(targetDir, maxDepth = 4, includeAll = false) {
133
+ isAllowedDotfile(name) {
134
+ const lower = name.toLowerCase();
135
+ if (lower.startsWith('.env')) return true;
136
+ if (lower.startsWith('.gitignore') || lower.startsWith('.dockerignore') || lower.startsWith('.npmignore')) return true;
137
+ if (lower.startsWith('.prettier') || lower.startsWith('.eslint') || lower.startsWith('.stylelint') || lower.startsWith('.editorconfig')) return true;
138
+ if (lower.startsWith('.babel') || lower.startsWith('.swcrc') || lower.startsWith('.postcss') || lower.startsWith('.tailwind')) return true;
139
+ if (lower.startsWith('.vscode') || lower.startsWith('.github') || lower.startsWith('.husky')) return true;
140
+ if (lower.startsWith('.cursor') || lower.startsWith('.windsurf')) return true;
141
+ if (lower.startsWith('.npmrc') || lower.startsWith('.yarnrc') || lower.startsWith('.nvmrc') || lower.startsWith('.node-version')) return true;
142
+ return false;
143
+ }
144
+
145
+ readWorkspaceTree(targetDir, maxDepth = 4, includeAll = false, showHidden = false) {
134
146
  try {
135
147
  const normalizedRoot = path.resolve(targetDir);
136
148
  if (!fs.existsSync(normalizedRoot)) {
@@ -140,10 +152,10 @@ export class PreviewService extends Service {
140
152
  if (!stat.isDirectory()) {
141
153
  return { ok: false, error: { code: 'NOT_A_DIRECTORY', message: '目标路径不是目录: ' + normalizedRoot } };
142
154
  }
143
- const ignoreDirs = new Set([
144
- 'node_modules', '.git', '.dsh', '.gemini', '.tempmediastorage', '.idea', '.vscode',
145
- 'dist', 'build', 'coverage', '.next', '.nuxt', '.output', '.turbo', '.cache',
146
- '__pycache__', '.pytest_cache', 'target', 'vendor'
155
+ const defaultIgnoreDirs = new Set([
156
+ 'node_modules', 'dist', 'build', 'coverage', '.next', '.nuxt', '.output', '.turbo', '.cache',
157
+ '__pycache__', '.pytest_cache', 'target', 'vendor', 'tmp', 'temp',
158
+ '.dsh', '.gemini', '.idea', '.tempmediastorage', '.git'
147
159
  ]);
148
160
  let totalFiles = 0;
149
161
  let totalDirs = 0;
@@ -160,10 +172,18 @@ export class PreviewService extends Service {
160
172
  for (const entry of entries) {
161
173
  const entryName = entry.name;
162
174
  const lowerName = entryName.toLowerCase();
175
+ const isHidden = entryName.startsWith('.') || entryName.startsWith('~$');
176
+
163
177
  if (!includeAll) {
164
- if (ignoreDirs.has(lowerName)) continue;
165
- if (entryName.startsWith('.') && entryName !== '.env' && entryName !== '.gitignore') continue;
178
+ if (!showHidden) {
179
+ if (defaultIgnoreDirs.has(lowerName)) continue;
180
+ if (lowerName === '.git' || lowerName === '.dsh' || lowerName === '.gemini' || lowerName === '.idea') continue;
181
+ if (isHidden && !this.isAllowedDotfile(entryName)) continue;
182
+ } else {
183
+ if (lowerName === 'node_modules' || lowerName === '__pycache__' || lowerName === '.pytest_cache') continue;
184
+ }
166
185
  }
186
+
167
187
  const fullPath = path.join(dirPath, entryName);
168
188
  const relPath = path.relative(normalizedRoot, fullPath).replace(/\\/g, '/');
169
189
  try {
@@ -175,6 +195,7 @@ export class PreviewService extends Service {
175
195
  path: fullPath,
176
196
  relativePath: relPath,
177
197
  isDir: true,
198
+ isHidden,
178
199
  children,
179
200
  childCount: children.length,
180
201
  });
@@ -193,6 +214,7 @@ export class PreviewService extends Service {
193
214
  path: fullPath,
194
215
  relativePath: relPath,
195
216
  isDir: false,
217
+ isHidden,
196
218
  extension: ext,
197
219
  size,
198
220
  mtime,
@@ -222,21 +244,34 @@ export class PreviewService extends Service {
222
244
  }
223
245
  }
224
246
 
225
- listDirectoryEntries(targetDir, includeAll = false) {
247
+ listDirectoryEntries(targetDir, includeAll = false, showHidden = false) {
226
248
  try {
227
249
  const normalized = path.resolve(targetDir);
228
250
  if (!fs.existsSync(normalized) || !fs.statSync(normalized).isDirectory()) {
229
251
  return { ok: false, error: { code: 'DIR_NOT_FOUND', message: '目录不存在' } };
230
252
  }
231
- const ignoreDirs = new Set(['node_modules', '.git', '.dsh', '.gemini', '.tempmediastorage', '.idea', '.vscode']);
253
+ const defaultIgnoreDirs = new Set([
254
+ 'node_modules', 'dist', 'build', 'coverage', '.next', '.nuxt', '.output', '.turbo', '.cache',
255
+ '__pycache__', '.pytest_cache', 'target', 'vendor', 'tmp', 'temp',
256
+ '.dsh', '.gemini', '.idea', '.tempmediastorage', '.git'
257
+ ]);
232
258
  const rawEntries = fs.readdirSync(normalized, { withFileTypes: true });
233
259
  const nodes = [];
234
260
  for (const entry of rawEntries) {
235
261
  const name = entry.name;
262
+ const lowerName = name.toLowerCase();
263
+ const isHidden = name.startsWith('.') || name.startsWith('~$');
264
+
236
265
  if (!includeAll) {
237
- if (ignoreDirs.has(name.toLowerCase())) continue;
238
- if (name.startsWith('.') && name !== '.env' && name !== '.gitignore') continue;
266
+ if (!showHidden) {
267
+ if (defaultIgnoreDirs.has(lowerName)) continue;
268
+ if (lowerName === '.git' || lowerName === '.dsh' || lowerName === '.gemini' || lowerName === '.idea') continue;
269
+ if (isHidden && !this.isAllowedDotfile(name)) continue;
270
+ } else {
271
+ if (lowerName === 'node_modules' || lowerName === '__pycache__' || lowerName === '.pytest_cache') continue;
272
+ }
239
273
  }
274
+
240
275
  const fullPath = path.join(normalized, name);
241
276
  const isDir = entry.isDirectory();
242
277
  let size = 0;
@@ -255,6 +290,7 @@ export class PreviewService extends Service {
255
290
  path: fullPath,
256
291
  relativePath: name,
257
292
  isDir,
293
+ isHidden,
258
294
  extension: ext,
259
295
  size,
260
296
  mtime,
@@ -425,7 +461,8 @@ export class PreviewService extends Service {
425
461
  const targetPath = url.searchParams.get('path') || process.cwd();
426
462
  const depth = parseInt(url.searchParams.get('depth') || '4', 10);
427
463
  const includeAll = url.searchParams.get('includeAll') === 'true';
428
- const result = this.readWorkspaceTree(targetPath, isNaN(depth) ? 4 : depth, includeAll);
464
+ const showHidden = url.searchParams.get('showHidden') === 'true';
465
+ const result = this.readWorkspaceTree(targetPath, isNaN(depth) ? 4 : depth, includeAll, showHidden);
429
466
  res.statusCode = result.ok ? 200 : 404;
430
467
  res.end(JSON.stringify(result));
431
468
  return;
@@ -433,7 +470,8 @@ export class PreviewService extends Service {
433
470
  if (pathname === '/api/preview/list-dir' && req.method === 'GET') {
434
471
  const targetPath = url.searchParams.get('path') || process.cwd();
435
472
  const includeAll = url.searchParams.get('includeAll') === 'true';
436
- const result = this.listDirectoryEntries(targetPath, includeAll);
473
+ const showHidden = url.searchParams.get('showHidden') === 'true';
474
+ const result = this.listDirectoryEntries(targetPath, includeAll, showHidden);
437
475
  res.statusCode = result.ok ? 200 : 404;
438
476
  res.end(JSON.stringify(result));
439
477
  return;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rooode/dsh-plugin-preview",
3
- "version": "0.1.19",
4
- "description": "DeepSeek Harness Markdown 文档与工作空间文件浏览器右侧预览插件 (支持工作区文件树、文件夹创建与文件移动、JSON 交互结构树/格式化、Java/C++/Python/JS/TS/YAML/Go/Rust 多语言语法高亮与符号大纲、自动换行与多标签 FileTabs)",
3
+ "version": "0.1.21",
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",
7
7
  "types": "lib/index.d.ts",