@rooode/dsh-plugin-preview 0.1.20 → 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 +135 -11
  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';
@@ -1278,6 +1327,41 @@
1278
1327
  );
1279
1328
  }
1280
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
+
1281
1365
  function IconChevronRight() {
1282
1366
  return h('svg', {
1283
1367
  width: 9,
@@ -1453,7 +1537,7 @@
1453
1537
  );
1454
1538
  }
1455
1539
 
1456
- function TreeContextMenu({ x, y, node, isRoot, clipboard, onAction, onClose }) {
1540
+ function TreeContextMenu({ x, y, node, isRoot, showHidden, clipboard, onAction, onClose }) {
1457
1541
  const menuRef = useRef(null);
1458
1542
  useEffect(() => {
1459
1543
  const handleDown = (e) => {
@@ -1535,6 +1619,12 @@
1535
1619
  )
1536
1620
  ] : null,
1537
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
+
1538
1628
  isRoot ? [
1539
1629
  h('div', { key: 'refresh', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('refresh'); } },
1540
1630
  h('span', { className: 'dsh-menu-icon' }, '🔄'),
@@ -1636,7 +1726,7 @@
1636
1726
 
1637
1727
  return h(Fragment, null,
1638
1728
  h('div', {
1639
- 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' : ''}`,
1640
1730
  style: indentStyle,
1641
1731
  onClick: handleClick,
1642
1732
  onContextMenu: handleContextMenu,
@@ -1646,7 +1736,7 @@
1646
1736
  onDragOver: handleDragOver,
1647
1737
  onDragLeave: handleDragLeave,
1648
1738
  onDrop: handleDrop,
1649
- 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) + ')' : ''}`,
1650
1740
  },
1651
1741
  // Chevron indicator for directory
1652
1742
  isDir ? h('span', {
@@ -1773,6 +1863,7 @@
1773
1863
  const [clipboard, setClipboard] = useState(null); // { action: 'cut', path: string, name: string, isDir: boolean } | null
1774
1864
 
1775
1865
  const [currentWs, setCurrentWs] = useState(getCurrentWorkspaceRoot());
1866
+ const [showHidden, setShowHidden] = useState(globalShowHidden);
1776
1867
 
1777
1868
  useEffect(() => {
1778
1869
  const handleStateUpdate = () => {
@@ -1780,10 +1871,13 @@
1780
1871
  if (newWs !== currentWs) {
1781
1872
  setCurrentWs(newWs);
1782
1873
  }
1874
+ if (globalShowHidden !== showHidden) {
1875
+ setShowHidden(globalShowHidden);
1876
+ }
1783
1877
  };
1784
1878
  stateListeners.add(handleStateUpdate);
1785
1879
  return () => stateListeners.delete(handleStateUpdate);
1786
- }, [currentWs]);
1880
+ }, [currentWs, showHidden]);
1787
1881
 
1788
1882
  const loadTree = useCallback(async (force = false, targetWs) => {
1789
1883
  setIsLoading(true);
@@ -1795,26 +1889,29 @@
1795
1889
  return;
1796
1890
  }
1797
1891
  const query = encodeURIComponent(rootPath);
1798
- const res = await fetch(`/api/preview/workspace-tree?path=${query}`);
1892
+ const res = await fetch(`/api/preview/workspace-tree?path=${query}&showHidden=${showHidden}`);
1799
1893
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
1800
1894
  const json = await res.json();
1801
1895
  if (!json.ok) throw new Error(json?.error?.message || '读取工作区目录失败');
1802
1896
  setTreeData(json);
1803
- // By default expand top 1 level directories
1897
+ // By default expand top 1 level directories if not already expanded
1804
1898
  if (json.tree) {
1805
1899
  const topDirs = json.tree.filter(n => n.isDir).map(n => n.path);
1806
- setExpandedPaths(new Set(topDirs));
1900
+ setExpandedPaths(prev => {
1901
+ if (prev.size === 0) return new Set(topDirs);
1902
+ return prev;
1903
+ });
1807
1904
  }
1808
1905
  } catch (err) {
1809
1906
  setError(err.message || '加载工作空间文件树失败');
1810
1907
  } finally {
1811
1908
  setIsLoading(false);
1812
1909
  }
1813
- }, [currentWs]);
1910
+ }, [currentWs, showHidden]);
1814
1911
 
1815
1912
  useEffect(() => {
1816
1913
  loadTree(false, currentWs);
1817
- }, [loadTree, currentWs, globalManualWorkspace]);
1914
+ }, [loadTree, currentWs, showHidden, globalManualWorkspace]);
1818
1915
 
1819
1916
  const handleToggleExpand = (dirPath) => {
1820
1917
  setExpandedPaths(prev => {
@@ -2061,11 +2158,20 @@
2061
2158
  if (node) triggerOpenNative(node.path);
2062
2159
  } else if (action === 'delete') {
2063
2160
  if (node) handleDelete(node);
2161
+ } else if (action === 'toggle-hidden') {
2162
+ handleToggleShowHidden();
2064
2163
  } else if (action === 'refresh') {
2065
2164
  loadTree(true);
2066
2165
  }
2067
2166
  };
2068
2167
 
2168
+ const handleToggleShowHidden = () => {
2169
+ const next = !showHidden;
2170
+ setShowHidden(next);
2171
+ setGlobalShowHidden(next);
2172
+ showToast(next ? '已开启显示隐藏文件与点文件' : '已隐藏点文件与隐藏项', 'info');
2173
+ };
2174
+
2069
2175
  const rootPath = treeData?.workspaceRoot || currentWs;
2070
2176
  const isCreatingAtRoot = creatingItem && creatingItem.parentPath && rootPath && creatingItem.parentPath.replace(/\\/g, '/').toLowerCase() === rootPath.replace(/\\/g, '/').toLowerCase();
2071
2177
  const isRootDropTarget = dragOverNode && rootPath && dragOverNode.path && dragOverNode.path.replace(/\\/g, '/').toLowerCase() === rootPath.replace(/\\/g, '/').toLowerCase();
@@ -2098,6 +2204,12 @@
2098
2204
  onClick: () => handleStartCreate(rootPath, 'dir'),
2099
2205
  title: '新建文件夹 (在根目录)',
2100
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)),
2101
2213
  h('button', {
2102
2214
  type: 'button',
2103
2215
  className: 'dsh-tree-action-btn',
@@ -2289,6 +2401,7 @@
2289
2401
  y: contextMenu.y,
2290
2402
  node: contextMenu.node,
2291
2403
  isRoot: contextMenu.isRoot,
2404
+ showHidden,
2292
2405
  clipboard,
2293
2406
  onAction: handleContextMenuAction,
2294
2407
  onClose: () => setContextMenu(null),
@@ -5609,6 +5722,10 @@
5609
5722
  background: rgba(255, 255, 255, 0.1);
5610
5723
  color: #ffffff;
5611
5724
  }
5725
+ .dsh-tree-action-btn.active {
5726
+ color: var(--dsw-static-deepseek-500, #3b82f6);
5727
+ background: rgba(59, 130, 246, 0.12);
5728
+ }
5612
5729
 
5613
5730
  .dsh-tree-sidebar-search {
5614
5731
  padding: 6px 8px;
@@ -5724,6 +5841,13 @@
5724
5841
  background: rgba(56, 189, 248, 0.16);
5725
5842
  color: #38bdf8;
5726
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
+ }
5727
5851
 
5728
5852
  .dsh-tree-chevron {
5729
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.20)');
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.20",
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",