@rooode/dsh-plugin-preview 0.1.1 → 0.1.3
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 +931 -45
- package/lib/index.js +172 -1
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -23,8 +23,13 @@
|
|
|
23
23
|
// =========================================================================
|
|
24
24
|
const STORAGE_KEY_TABS = 'dsh:preview:tabs:v1';
|
|
25
25
|
const STORAGE_KEY_WIDTH = 'dsh:preview:panel_width:v1';
|
|
26
|
-
const
|
|
26
|
+
const STORAGE_KEY_TREE_OPEN = 'dsh:preview:tree:open:v1';
|
|
27
|
+
const STORAGE_KEY_TREE_WIDTH = 'dsh:preview:tree:width:v1';
|
|
28
|
+
const DEFAULT_PANEL_WIDTH = 760;
|
|
27
29
|
const MIN_PANEL_WIDTH = 380;
|
|
30
|
+
const DEFAULT_TREE_WIDTH = 240;
|
|
31
|
+
const MIN_TREE_WIDTH = 180;
|
|
32
|
+
const MAX_TREE_WIDTH = 450;
|
|
28
33
|
const SUPPORTED_EXTENSIONS = new Set([
|
|
29
34
|
'.md', '.markdown', '.mdown', '.mkdn', '.mdwn',
|
|
30
35
|
'.txt', '.log', '.json', '.yaml', '.yml',
|
|
@@ -39,7 +44,10 @@
|
|
|
39
44
|
let globalTabs = [];
|
|
40
45
|
let globalActiveTabId = null;
|
|
41
46
|
let globalIsPanelOpen = false;
|
|
47
|
+
let globalIsTreeOpen = true;
|
|
42
48
|
let globalPanelWidth = DEFAULT_PANEL_WIDTH;
|
|
49
|
+
let globalTreeWidth = DEFAULT_TREE_WIDTH;
|
|
50
|
+
let globalLastOpenTime = 0;
|
|
43
51
|
const stateListeners = new Set();
|
|
44
52
|
|
|
45
53
|
try {
|
|
@@ -52,6 +60,23 @@
|
|
|
52
60
|
}
|
|
53
61
|
} catch (e) {}
|
|
54
62
|
|
|
63
|
+
try {
|
|
64
|
+
const savedTreeOpen = localStorage.getItem(STORAGE_KEY_TREE_OPEN);
|
|
65
|
+
if (savedTreeOpen !== null) {
|
|
66
|
+
globalIsTreeOpen = savedTreeOpen === 'true';
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const savedTreeWidth = localStorage.getItem(STORAGE_KEY_TREE_WIDTH);
|
|
72
|
+
if (savedTreeWidth) {
|
|
73
|
+
const parsed = parseInt(savedTreeWidth, 10);
|
|
74
|
+
if (!isNaN(parsed) && parsed >= MIN_TREE_WIDTH) {
|
|
75
|
+
globalTreeWidth = Math.min(parsed, MAX_TREE_WIDTH);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (e) {}
|
|
79
|
+
|
|
55
80
|
try {
|
|
56
81
|
const savedTabs = localStorage.getItem(STORAGE_KEY_TABS);
|
|
57
82
|
if (savedTabs) {
|
|
@@ -83,8 +108,27 @@
|
|
|
83
108
|
notifyStateChange();
|
|
84
109
|
}
|
|
85
110
|
|
|
111
|
+
function setIsTreeOpen(open) {
|
|
112
|
+
globalIsTreeOpen = open;
|
|
113
|
+
try {
|
|
114
|
+
localStorage.setItem(STORAGE_KEY_TREE_OPEN, String(open));
|
|
115
|
+
} catch (e) {}
|
|
116
|
+
notifyStateChange();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function setTreeWidth(width) {
|
|
120
|
+
globalTreeWidth = Math.max(MIN_TREE_WIDTH, Math.min(width, MAX_TREE_WIDTH));
|
|
121
|
+
try {
|
|
122
|
+
localStorage.setItem(STORAGE_KEY_TREE_WIDTH, String(globalTreeWidth));
|
|
123
|
+
} catch (e) {}
|
|
124
|
+
notifyStateChange();
|
|
125
|
+
}
|
|
126
|
+
|
|
86
127
|
function setPanelOpen(open) {
|
|
87
128
|
globalIsPanelOpen = open;
|
|
129
|
+
if (open) {
|
|
130
|
+
globalLastOpenTime = Date.now();
|
|
131
|
+
}
|
|
88
132
|
notifyStateChange();
|
|
89
133
|
}
|
|
90
134
|
|
|
@@ -108,6 +152,61 @@
|
|
|
108
152
|
return SUPPORTED_EXTENSIONS.has(ext);
|
|
109
153
|
}
|
|
110
154
|
|
|
155
|
+
function formatBytes(bytes) {
|
|
156
|
+
if (!bytes || bytes === 0) return '0 B';
|
|
157
|
+
const k = 1024;
|
|
158
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
159
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
160
|
+
return (bytes / Math.pow(k, i)).toFixed(i === 0 ? 0 : 1) + ' ' + sizes[i];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getFileIcon(node) {
|
|
164
|
+
if (node.isDir) {
|
|
165
|
+
return { icon: '📁', color: '#eab308' };
|
|
166
|
+
}
|
|
167
|
+
const ext = (node.extension || getFileExtension(node.name || node.path || '')).toLowerCase();
|
|
168
|
+
if (ext === '.md' || ext === '.markdown') return { icon: '📝', color: '#0284c7' };
|
|
169
|
+
if (ext === '.json') return { icon: '{ }', color: '#10b981' };
|
|
170
|
+
if (ext === '.ts' || ext === '.tsx') return { icon: 'TS', color: '#3b82f6' };
|
|
171
|
+
if (ext === '.js' || ext === '.jsx' || ext === '.mjs') return { icon: 'JS', color: '#eab308' };
|
|
172
|
+
if (ext === '.yaml' || ext === '.yml' || ext === '.toml' || ext === '.ini') return { icon: '⚙️', color: '#f97316' };
|
|
173
|
+
if (ext === '.html' || ext === '.htm') return { icon: '🌐', color: '#ef4444' };
|
|
174
|
+
if (ext === '.css' || ext === '.scss' || ext === '.less') return { icon: '#', color: '#06b6d4' };
|
|
175
|
+
if (ext === '.py') return { icon: '🐍', color: '#38bdf8' };
|
|
176
|
+
if (ext === '.sh' || ext === '.bash' || ext === '.ps1') return { icon: '⌨️', color: '#8b5cf6' };
|
|
177
|
+
if (['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp'].includes(ext)) return { icon: '🖼️', color: '#ec4899' };
|
|
178
|
+
if (ext === '.log' || ext === '.txt') return { icon: '📄', color: '#64748b' };
|
|
179
|
+
return { icon: '📄', color: '#94a3b8' };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getCurrentWorkspaceRoot() {
|
|
183
|
+
if (clientCtx && clientCtx.workspaces) {
|
|
184
|
+
try {
|
|
185
|
+
if (clientCtx.workspaces.current?.directory) {
|
|
186
|
+
return clientCtx.workspaces.current.directory;
|
|
187
|
+
}
|
|
188
|
+
const snapshot = clientCtx.workspaces.list?.getSnapshot();
|
|
189
|
+
if (snapshot?.items?.length > 0) {
|
|
190
|
+
return snapshot.items[0].directory || snapshot.items[0].path || '';
|
|
191
|
+
}
|
|
192
|
+
} catch (e) {}
|
|
193
|
+
}
|
|
194
|
+
if (clientCtx && clientCtx.sessions) {
|
|
195
|
+
try {
|
|
196
|
+
const snapshot = clientCtx.sessions.list?.getSnapshot();
|
|
197
|
+
const currentSessionId = snapshot?.current;
|
|
198
|
+
if (currentSessionId && snapshot?.byId?.[currentSessionId]?.cwd) {
|
|
199
|
+
return snapshot.byId[currentSessionId].cwd;
|
|
200
|
+
}
|
|
201
|
+
} catch (e) {}
|
|
202
|
+
}
|
|
203
|
+
if (typeof location !== 'undefined' && location.hash) {
|
|
204
|
+
const m = location.hash.match(/workspace=([^&]+)/);
|
|
205
|
+
if (m) return decodeURIComponent(m[1]);
|
|
206
|
+
}
|
|
207
|
+
return '';
|
|
208
|
+
}
|
|
209
|
+
|
|
111
210
|
function resolveAbsoluteFilePath(targetPath) {
|
|
112
211
|
if (!targetPath) return '';
|
|
113
212
|
let cleanPath = targetPath.trim();
|
|
@@ -122,24 +221,7 @@
|
|
|
122
221
|
if (isWinAbs || isPosixAbs) {
|
|
123
222
|
return cleanPath;
|
|
124
223
|
}
|
|
125
|
-
|
|
126
|
-
if (clientCtx && clientCtx.sessions) {
|
|
127
|
-
try {
|
|
128
|
-
const snapshot = clientCtx.sessions.list.getSnapshot();
|
|
129
|
-
const currentSessionId = snapshot.current;
|
|
130
|
-
if (currentSessionId && snapshot.byId[currentSessionId]) {
|
|
131
|
-
cwd = snapshot.byId[currentSessionId].cwd || '';
|
|
132
|
-
}
|
|
133
|
-
} catch (e) {}
|
|
134
|
-
}
|
|
135
|
-
if (!cwd && clientCtx && clientCtx.workspaces) {
|
|
136
|
-
try {
|
|
137
|
-
const wsList = clientCtx.workspaces.list.getSnapshot();
|
|
138
|
-
if (wsList.items && wsList.items.length > 0) {
|
|
139
|
-
cwd = wsList.items[0].directory || '';
|
|
140
|
-
}
|
|
141
|
-
} catch (e) {}
|
|
142
|
-
}
|
|
224
|
+
const cwd = getCurrentWorkspaceRoot();
|
|
143
225
|
if (cwd) {
|
|
144
226
|
const isWinCwd = /^[a-zA-Z]:[\\/]/.test(cwd);
|
|
145
227
|
const sep = isWinCwd ? '\\' : '/';
|
|
@@ -179,6 +261,7 @@
|
|
|
179
261
|
globalTabs = [newTab, ...globalTabs];
|
|
180
262
|
globalActiveTabId = tabId;
|
|
181
263
|
}
|
|
264
|
+
globalLastOpenTime = Date.now();
|
|
182
265
|
globalIsPanelOpen = true;
|
|
183
266
|
saveTabsToStorage();
|
|
184
267
|
notifyStateChange();
|
|
@@ -196,7 +279,6 @@
|
|
|
196
279
|
globalActiveTabId = nextTabs[nextIdx].id;
|
|
197
280
|
} else {
|
|
198
281
|
globalActiveTabId = null;
|
|
199
|
-
globalIsPanelOpen = false;
|
|
200
282
|
}
|
|
201
283
|
}
|
|
202
284
|
saveTabsToStorage();
|
|
@@ -206,7 +288,6 @@
|
|
|
206
288
|
function closeAllTabs() {
|
|
207
289
|
globalTabs = [];
|
|
208
290
|
globalActiveTabId = null;
|
|
209
|
-
globalIsPanelOpen = false;
|
|
210
291
|
saveTabsToStorage();
|
|
211
292
|
notifyStateChange();
|
|
212
293
|
}
|
|
@@ -681,10 +762,300 @@
|
|
|
681
762
|
};
|
|
682
763
|
}
|
|
683
764
|
|
|
765
|
+
// =========================================================================
|
|
766
|
+
// React Components: Workspace File Tree Explorer
|
|
767
|
+
// =========================================================================
|
|
768
|
+
function FileTreeNodeItem({ node, depth = 0, expandedPaths, onToggleExpand, activeFilePath, onOpenFile, searchQuery }) {
|
|
769
|
+
const isDir = node.isDir;
|
|
770
|
+
const isExpanded = expandedPaths.has(node.path);
|
|
771
|
+
const isActive = !isDir && activeFilePath && activeFilePath.toLowerCase() === node.path.toLowerCase();
|
|
772
|
+
const iconInfo = getFileIcon(node);
|
|
773
|
+
|
|
774
|
+
const indentStyle = { paddingLeft: `${depth * 14 + 6}px` };
|
|
775
|
+
|
|
776
|
+
const handleClick = (e) => {
|
|
777
|
+
e.stopPropagation();
|
|
778
|
+
if (isDir) {
|
|
779
|
+
onToggleExpand(node.path);
|
|
780
|
+
} else {
|
|
781
|
+
onOpenFile(node.path);
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
return h(Fragment, null,
|
|
786
|
+
h('div', {
|
|
787
|
+
className: `dsh-tree-node-row ${isDir ? 'is-dir' : 'is-file'} ${isActive ? 'active' : ''}`,
|
|
788
|
+
style: indentStyle,
|
|
789
|
+
onClick: handleClick,
|
|
790
|
+
title: `${node.name}\n${node.path}${node.size ? ' (' + formatBytes(node.size) + ')' : ''}`,
|
|
791
|
+
},
|
|
792
|
+
// Chevron indicator for directory
|
|
793
|
+
isDir ? h('span', {
|
|
794
|
+
className: `dsh-tree-chevron ${isExpanded ? 'expanded' : ''}`,
|
|
795
|
+
}, isExpanded ? '▼' : '▶') : h('span', { className: 'dsh-tree-chevron-placeholder' }),
|
|
796
|
+
|
|
797
|
+
// File / Directory Icon
|
|
798
|
+
h('span', {
|
|
799
|
+
className: 'dsh-tree-icon',
|
|
800
|
+
style: { color: isDir ? (isExpanded ? '#ca8a04' : '#eab308') : iconInfo.color },
|
|
801
|
+
}, isDir ? (isExpanded ? '📂' : '📁') : iconInfo.icon),
|
|
802
|
+
|
|
803
|
+
// File / Directory Name
|
|
804
|
+
h('span', { className: 'dsh-tree-name' }, node.name),
|
|
805
|
+
|
|
806
|
+
// Size / Count badge
|
|
807
|
+
isDir && node.childCount !== undefined ? h('span', { className: 'dsh-tree-badge' }, node.childCount) : (
|
|
808
|
+
!isDir && node.size ? h('span', { className: 'dsh-tree-size-badge' }, formatBytes(node.size)) : null
|
|
809
|
+
)
|
|
810
|
+
),
|
|
811
|
+
|
|
812
|
+
// Children if expanded
|
|
813
|
+
isDir && isExpanded && node.children && node.children.length > 0 ? (
|
|
814
|
+
node.children.map(child => h(FileTreeNodeItem, {
|
|
815
|
+
key: child.path,
|
|
816
|
+
node: child,
|
|
817
|
+
depth: depth + 1,
|
|
818
|
+
expandedPaths,
|
|
819
|
+
onToggleExpand,
|
|
820
|
+
activeFilePath,
|
|
821
|
+
onOpenFile,
|
|
822
|
+
searchQuery,
|
|
823
|
+
}))
|
|
824
|
+
) : null
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function filterTreeNode(node, query) {
|
|
829
|
+
if (!query) return true;
|
|
830
|
+
const q = query.toLowerCase();
|
|
831
|
+
if (node.name.toLowerCase().includes(q)) return true;
|
|
832
|
+
if (node.isDir && node.children) {
|
|
833
|
+
return node.children.some(child => filterTreeNode(child, query));
|
|
834
|
+
}
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function filterTree(tree, query) {
|
|
839
|
+
if (!query) return tree;
|
|
840
|
+
return tree.filter(node => filterTreeNode(node, query)).map(node => {
|
|
841
|
+
if (!node.isDir) return node;
|
|
842
|
+
return {
|
|
843
|
+
...node,
|
|
844
|
+
children: filterTree(node.children || [], query),
|
|
845
|
+
};
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function collectAllDirPaths(tree) {
|
|
850
|
+
const paths = [];
|
|
851
|
+
const traverse = (nodes) => {
|
|
852
|
+
for (const n of nodes) {
|
|
853
|
+
if (n.isDir) {
|
|
854
|
+
paths.push(n.path);
|
|
855
|
+
if (n.children) traverse(n.children);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
traverse(tree);
|
|
860
|
+
return paths;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function WorkspaceTreeSidebar({
|
|
864
|
+
width,
|
|
865
|
+
activeFilePath,
|
|
866
|
+
onOpenFile,
|
|
867
|
+
onCloseSidebar,
|
|
868
|
+
}) {
|
|
869
|
+
const [treeData, setTreeData] = useState(null);
|
|
870
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
871
|
+
const [error, setError] = useState(null);
|
|
872
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
873
|
+
const [expandedPaths, setExpandedPaths] = useState(new Set());
|
|
874
|
+
const currentWs = getCurrentWorkspaceRoot();
|
|
875
|
+
|
|
876
|
+
const loadTree = useCallback(async (force = false) => {
|
|
877
|
+
setIsLoading(true);
|
|
878
|
+
setError(null);
|
|
879
|
+
try {
|
|
880
|
+
const rootPath = getCurrentWorkspaceRoot();
|
|
881
|
+
const query = encodeURIComponent(rootPath);
|
|
882
|
+
const res = await fetch(`/api/preview/workspace-tree?path=${query}`);
|
|
883
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
884
|
+
const json = await res.json();
|
|
885
|
+
if (!json.ok) throw new Error(json?.error?.message || '读取工作区目录失败');
|
|
886
|
+
setTreeData(json);
|
|
887
|
+
// By default expand top 1 level directories
|
|
888
|
+
if (json.tree) {
|
|
889
|
+
const topDirs = json.tree.filter(n => n.isDir).map(n => n.path);
|
|
890
|
+
setExpandedPaths(new Set(topDirs));
|
|
891
|
+
}
|
|
892
|
+
} catch (err) {
|
|
893
|
+
setError(err.message || '加载工作空间文件树失败');
|
|
894
|
+
} finally {
|
|
895
|
+
setIsLoading(false);
|
|
896
|
+
}
|
|
897
|
+
}, []);
|
|
898
|
+
|
|
899
|
+
useEffect(() => {
|
|
900
|
+
loadTree();
|
|
901
|
+
}, [loadTree]);
|
|
902
|
+
|
|
903
|
+
const handleToggleExpand = (dirPath) => {
|
|
904
|
+
setExpandedPaths(prev => {
|
|
905
|
+
const next = new Set(prev);
|
|
906
|
+
if (next.has(dirPath)) next.delete(dirPath);
|
|
907
|
+
else next.add(dirPath);
|
|
908
|
+
return next;
|
|
909
|
+
});
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
const handleExpandAll = () => {
|
|
913
|
+
if (treeData?.tree) {
|
|
914
|
+
setExpandedPaths(new Set(collectAllDirPaths(treeData.tree)));
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
const handleCollapseAll = () => {
|
|
919
|
+
setExpandedPaths(new Set());
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
const displayTree = useMemo(() => {
|
|
923
|
+
if (!treeData?.tree) return [];
|
|
924
|
+
return filterTree(treeData.tree, searchQuery.trim());
|
|
925
|
+
}, [treeData, searchQuery]);
|
|
926
|
+
|
|
927
|
+
// If searching, auto expand matching folders
|
|
928
|
+
useEffect(() => {
|
|
929
|
+
if (searchQuery.trim() && treeData?.tree) {
|
|
930
|
+
const allMatchingDirs = collectAllDirPaths(filterTree(treeData.tree, searchQuery.trim()));
|
|
931
|
+
setExpandedPaths(new Set(allMatchingDirs));
|
|
932
|
+
}
|
|
933
|
+
}, [searchQuery, treeData]);
|
|
934
|
+
|
|
935
|
+
return h('div', {
|
|
936
|
+
className: 'dsh-tree-sidebar',
|
|
937
|
+
style: { width: `${width}px` },
|
|
938
|
+
},
|
|
939
|
+
// Sidebar Header
|
|
940
|
+
h('div', { className: 'dsh-tree-sidebar-header' },
|
|
941
|
+
h('div', { className: 'dsh-tree-sidebar-title', title: treeData?.workspaceRoot || currentWs },
|
|
942
|
+
h('span', { className: 'dsh-tree-header-icon' }, '📁'),
|
|
943
|
+
h('span', { className: 'dsh-tree-header-text' }, treeData?.workspaceName || getFileName(currentWs) || '工作区')
|
|
944
|
+
),
|
|
945
|
+
h('div', { className: 'dsh-tree-header-actions' },
|
|
946
|
+
h('button', {
|
|
947
|
+
type: 'button',
|
|
948
|
+
className: 'dsh-tree-action-btn',
|
|
949
|
+
onClick: () => loadTree(true),
|
|
950
|
+
title: '刷新文件树',
|
|
951
|
+
}, '🔄'),
|
|
952
|
+
h('button', {
|
|
953
|
+
type: 'button',
|
|
954
|
+
className: 'dsh-tree-action-btn',
|
|
955
|
+
onClick: handleExpandAll,
|
|
956
|
+
title: '全部展开',
|
|
957
|
+
}, '📂'),
|
|
958
|
+
h('button', {
|
|
959
|
+
type: 'button',
|
|
960
|
+
className: 'dsh-tree-action-btn',
|
|
961
|
+
onClick: handleCollapseAll,
|
|
962
|
+
title: '全部折叠',
|
|
963
|
+
}, '📁'),
|
|
964
|
+
onCloseSidebar ? h('button', {
|
|
965
|
+
type: 'button',
|
|
966
|
+
className: 'dsh-tree-action-btn',
|
|
967
|
+
onClick: onCloseSidebar,
|
|
968
|
+
title: '收起目录树 (快捷键 Ctrl/Cmd+B)',
|
|
969
|
+
}, '✕') : null
|
|
970
|
+
)
|
|
971
|
+
),
|
|
972
|
+
|
|
973
|
+
// Search Input
|
|
974
|
+
h('div', { className: 'dsh-tree-sidebar-search' },
|
|
975
|
+
h('div', { className: 'dsh-tree-search-wrapper' },
|
|
976
|
+
h('input', {
|
|
977
|
+
type: 'text',
|
|
978
|
+
className: 'dsh-tree-search-input',
|
|
979
|
+
placeholder: '🔍 过滤文件 (如 .md, ts)...',
|
|
980
|
+
value: searchQuery,
|
|
981
|
+
onChange: (e) => setSearchQuery(e.target.value),
|
|
982
|
+
}),
|
|
983
|
+
searchQuery ? h('button', {
|
|
984
|
+
type: 'button',
|
|
985
|
+
className: 'dsh-tree-search-clear',
|
|
986
|
+
onClick: () => setSearchQuery(''),
|
|
987
|
+
title: '清空过滤条件',
|
|
988
|
+
}, '✕') : null
|
|
989
|
+
)
|
|
990
|
+
),
|
|
991
|
+
|
|
992
|
+
// Tree Content
|
|
993
|
+
h('div', { className: 'dsh-tree-node-list' },
|
|
994
|
+
isLoading ? (
|
|
995
|
+
h('div', { className: 'dsh-tree-loading' }, '正在扫描工作区文件...')
|
|
996
|
+
) : error ? (
|
|
997
|
+
h('div', { className: 'dsh-tree-error' },
|
|
998
|
+
h('p', null, `⚠️ ${error}`),
|
|
999
|
+
h('button', {
|
|
1000
|
+
type: 'button',
|
|
1001
|
+
className: 'dsh-tree-retry-btn',
|
|
1002
|
+
onClick: () => loadTree(true),
|
|
1003
|
+
}, '重试')
|
|
1004
|
+
)
|
|
1005
|
+
) : displayTree.length === 0 ? (
|
|
1006
|
+
h('div', { className: 'dsh-tree-empty' }, searchQuery ? '未匹配到相关文件' : '工作区暂无文件')
|
|
1007
|
+
) : (
|
|
1008
|
+
displayTree.map(node => h(FileTreeNodeItem, {
|
|
1009
|
+
key: node.path,
|
|
1010
|
+
node,
|
|
1011
|
+
depth: 0,
|
|
1012
|
+
expandedPaths,
|
|
1013
|
+
onToggleExpand: handleToggleExpand,
|
|
1014
|
+
activeFilePath,
|
|
1015
|
+
onOpenFile,
|
|
1016
|
+
searchQuery,
|
|
1017
|
+
}))
|
|
1018
|
+
)
|
|
1019
|
+
),
|
|
1020
|
+
|
|
1021
|
+
// Sidebar Footer with file count stats
|
|
1022
|
+
treeData ? h('div', { className: 'dsh-tree-sidebar-footer' },
|
|
1023
|
+
h('span', null, `${treeData.totalDirs || 0} 目录`),
|
|
1024
|
+
h('span', { className: 'dsh-tree-footer-dot' }, '•'),
|
|
1025
|
+
h('span', null, `${treeData.totalFiles || 0} 文件`)
|
|
1026
|
+
) : null
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function WorkspaceWelcomeView({ workspaceRoot, workspaceName, onOpenFile, onOpenTree }) {
|
|
1031
|
+
return h('div', { className: 'dsh-preview-welcome-container' },
|
|
1032
|
+
h('div', { className: 'dsh-preview-welcome-card' },
|
|
1033
|
+
h('div', { className: 'dsh-welcome-icon' }, '🗂️'),
|
|
1034
|
+
h('h3', { className: 'dsh-welcome-title' }, `工作空间: ${workspaceName || '当前项目'}`),
|
|
1035
|
+
h('p', { className: 'dsh-welcome-path' }, workspaceRoot || '本地项目目录'),
|
|
1036
|
+
h('p', { className: 'dsh-welcome-desc' },
|
|
1037
|
+
'在左侧目录树中选择并点击任意文件(Markdown 文档、代码文件、配置文件等),即可在此多标签预览、阅读与对照。'
|
|
1038
|
+
),
|
|
1039
|
+
h('div', { className: 'dsh-welcome-actions' },
|
|
1040
|
+
h('button', {
|
|
1041
|
+
type: 'button',
|
|
1042
|
+
className: 'dsh-welcome-btn primary',
|
|
1043
|
+
onClick: onOpenTree,
|
|
1044
|
+
}, '📁 浏览工作区文件树'),
|
|
1045
|
+
h('button', {
|
|
1046
|
+
type: 'button',
|
|
1047
|
+
className: 'dsh-welcome-btn',
|
|
1048
|
+
onClick: () => triggerRevealInExplorer(workspaceRoot || '.'),
|
|
1049
|
+
}, '🖥️ 在资源管理器中查看')
|
|
1050
|
+
)
|
|
1051
|
+
)
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
684
1055
|
// =========================================================================
|
|
685
1056
|
// React Components: MarkdownPreviewView
|
|
686
1057
|
// =========================================================================
|
|
687
|
-
function MarkdownPreviewView({ tab, fileInfo, isLoading, error, onRefresh, onOpenNative, onReveal }) {
|
|
1058
|
+
function MarkdownPreviewView({ tab, fileInfo, isLoading, error, isTreeOpen, onToggleTree, onRefresh, onOpenNative, onReveal }) {
|
|
688
1059
|
const [viewMode, setViewMode] = useState(tab.viewMode || 'preview');
|
|
689
1060
|
const [isTocOpen, setIsTocOpen] = useState(false);
|
|
690
1061
|
const [searchQuery, setSearchQuery] = useState('');
|
|
@@ -735,6 +1106,13 @@
|
|
|
735
1106
|
h('div', { className: 'dsh-preview-toolbar' },
|
|
736
1107
|
// Left: View mode toggles
|
|
737
1108
|
h('div', { className: 'dsh-preview-toolbar-left' },
|
|
1109
|
+
h('button', {
|
|
1110
|
+
type: 'button',
|
|
1111
|
+
className: `dsh-preview-tool-btn ${isTreeOpen ? 'active' : ''}`,
|
|
1112
|
+
onClick: onToggleTree,
|
|
1113
|
+
title: isTreeOpen ? '隐藏工作区目录树 (Ctrl/Cmd+B)' : '展开工作区目录树 (Ctrl/Cmd+B)',
|
|
1114
|
+
}, '🗂️ 目录树'),
|
|
1115
|
+
|
|
738
1116
|
h('div', { className: 'dsh-preview-mode-group' },
|
|
739
1117
|
h('button', {
|
|
740
1118
|
type: 'button',
|
|
@@ -928,6 +1306,11 @@
|
|
|
928
1306
|
const [activeTabId, setActiveTabId] = useState(globalActiveTabId);
|
|
929
1307
|
const [isOpen, setIsOpen] = useState(globalIsPanelOpen);
|
|
930
1308
|
const [width, setWidth] = useState(globalPanelWidth);
|
|
1309
|
+
const [isTreeOpen, setLocalIsTreeOpen] = useState(globalIsTreeOpen);
|
|
1310
|
+
const [treeWidth, setLocalTreeWidth] = useState(globalTreeWidth);
|
|
1311
|
+
const [isTreeDragging, setIsTreeDragging] = useState(false);
|
|
1312
|
+
const treeDragStartX = useRef(0);
|
|
1313
|
+
const treeDragStartWidth = useRef(0);
|
|
931
1314
|
const [fileData, setFileData] = useState({});
|
|
932
1315
|
const [loadingMap, setLoadingMap] = useState({});
|
|
933
1316
|
const [errorMap, setErrorMap] = useState({});
|
|
@@ -942,6 +1325,8 @@
|
|
|
942
1325
|
setActiveTabId(globalActiveTabId);
|
|
943
1326
|
setIsOpen(globalIsPanelOpen);
|
|
944
1327
|
setWidth(globalPanelWidth);
|
|
1328
|
+
setLocalIsTreeOpen(globalIsTreeOpen);
|
|
1329
|
+
setLocalTreeWidth(globalTreeWidth);
|
|
945
1330
|
};
|
|
946
1331
|
stateListeners.add(update);
|
|
947
1332
|
return () => stateListeners.delete(update);
|
|
@@ -977,7 +1362,7 @@
|
|
|
977
1362
|
}
|
|
978
1363
|
}, [activeTab, loadTabFile]);
|
|
979
1364
|
|
|
980
|
-
// Drag Resize Handlers
|
|
1365
|
+
// Drag Resize Handlers (Panel Width)
|
|
981
1366
|
const onResizePointerDown = useCallback((e) => {
|
|
982
1367
|
e.preventDefault();
|
|
983
1368
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
@@ -1000,39 +1385,116 @@
|
|
|
1000
1385
|
}
|
|
1001
1386
|
}, [isDragging]);
|
|
1002
1387
|
|
|
1003
|
-
//
|
|
1388
|
+
// Drag Resize Handlers (Tree Width)
|
|
1389
|
+
const onTreeResizePointerDown = useCallback((e) => {
|
|
1390
|
+
e.preventDefault();
|
|
1391
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
1392
|
+
treeDragStartX.current = e.clientX;
|
|
1393
|
+
treeDragStartWidth.current = globalTreeWidth;
|
|
1394
|
+
setIsTreeDragging(true);
|
|
1395
|
+
}, []);
|
|
1396
|
+
|
|
1397
|
+
const onTreeResizePointerMove = useCallback((e) => {
|
|
1398
|
+
if (!isTreeDragging) return;
|
|
1399
|
+
const dx = e.clientX - treeDragStartX.current;
|
|
1400
|
+
const newWidth = treeDragStartWidth.current + dx;
|
|
1401
|
+
setTreeWidth(newWidth);
|
|
1402
|
+
}, [isTreeDragging]);
|
|
1403
|
+
|
|
1404
|
+
const onTreeResizePointerUp = useCallback((e) => {
|
|
1405
|
+
if (isTreeDragging) {
|
|
1406
|
+
setIsTreeDragging(false);
|
|
1407
|
+
try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (err) {}
|
|
1408
|
+
}
|
|
1409
|
+
}, [isTreeDragging]);
|
|
1410
|
+
|
|
1411
|
+
const drawerRef = useRef(null);
|
|
1412
|
+
|
|
1413
|
+
// Outside Click Listener: Click on the chat conversation / dialog box outside the drawer to auto close/hide it
|
|
1414
|
+
useEffect(() => {
|
|
1415
|
+
if (!isOpen) return;
|
|
1416
|
+
|
|
1417
|
+
const handleOutsidePointerDown = (e) => {
|
|
1418
|
+
// 1. Ignore if just opened within 250ms
|
|
1419
|
+
if (Date.now() - globalLastOpenTime < 250) return;
|
|
1420
|
+
// 2. Ignore during dragging resize handle
|
|
1421
|
+
if (isDragging || isTreeDragging) return;
|
|
1422
|
+
|
|
1423
|
+
const target = e.target;
|
|
1424
|
+
if (!target) return;
|
|
1425
|
+
|
|
1426
|
+
// 3. If clicking inside the preview drawer, do not close
|
|
1427
|
+
const drawerEl = drawerRef.current || document.querySelector('.dsh-preview-drawer-container');
|
|
1428
|
+
if (drawerEl && (drawerEl === target || drawerEl.contains(target))) {
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// 4. If clicking on the floating pill, do not close
|
|
1433
|
+
const floatPill = document.querySelector('.dsh-preview-float-pill');
|
|
1434
|
+
if (floatPill && (floatPill === target || floatPill.contains(target))) {
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// 5. If clicking on a file link or button that opens a file preview, do not close (let interceptor open it)
|
|
1439
|
+
if (target.closest) {
|
|
1440
|
+
const btn = target.closest('button');
|
|
1441
|
+
if (btn) {
|
|
1442
|
+
const candidate = btn.getAttribute('title') || btn.getAttribute('aria-label') || btn.textContent || '';
|
|
1443
|
+
if (isPreviewableFile(candidate.trim())) return;
|
|
1444
|
+
}
|
|
1445
|
+
const anchor = target.closest('a');
|
|
1446
|
+
if (anchor) {
|
|
1447
|
+
const href = anchor.getAttribute('href') || '';
|
|
1448
|
+
if (isPreviewableFile(href)) return;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// 6. Outside click in dialog/conversation/background: Auto hide the preview drawer!
|
|
1453
|
+
setPanelOpen(false);
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
window.addEventListener('pointerdown', handleOutsidePointerDown, true);
|
|
1457
|
+
return () => {
|
|
1458
|
+
window.removeEventListener('pointerdown', handleOutsidePointerDown, true);
|
|
1459
|
+
};
|
|
1460
|
+
}, [isOpen, isDragging, isTreeDragging]);
|
|
1461
|
+
|
|
1462
|
+
// Hotkey: Esc closes preview, Ctrl/Cmd+B toggles file tree
|
|
1004
1463
|
useEffect(() => {
|
|
1005
1464
|
const onKeyDown = (e) => {
|
|
1006
1465
|
if (e.key === 'Escape' && globalIsPanelOpen) {
|
|
1007
1466
|
setPanelOpen(false);
|
|
1008
1467
|
}
|
|
1468
|
+
if ((e.ctrlKey || e.metaKey) && (e.key === 'b' || e.key === 'B')) {
|
|
1469
|
+
e.preventDefault();
|
|
1470
|
+
setIsTreeOpen(!globalIsTreeOpen);
|
|
1471
|
+
}
|
|
1009
1472
|
};
|
|
1010
1473
|
window.addEventListener('keydown', onKeyDown);
|
|
1011
1474
|
return () => window.removeEventListener('keydown', onKeyDown);
|
|
1012
1475
|
}, []);
|
|
1013
1476
|
|
|
1014
|
-
if (!isOpen && tabs.length === 0) return null;
|
|
1015
|
-
|
|
1016
1477
|
const activeKey = activeTab?.filePath?.toLowerCase();
|
|
1017
1478
|
const currentFileInfo = activeKey ? fileData[activeKey] : null;
|
|
1018
1479
|
const currentLoading = activeKey ? !!loadingMap[activeKey] : false;
|
|
1019
1480
|
const currentError = activeKey ? errorMap[activeKey] : null;
|
|
1020
1481
|
|
|
1021
1482
|
return h(Fragment, null,
|
|
1022
|
-
// Floating Mini Button to Reopen (when closed
|
|
1023
|
-
!isOpen
|
|
1483
|
+
// Floating Mini Button to Reopen (when closed)
|
|
1484
|
+
!isOpen ? h('div', {
|
|
1024
1485
|
className: 'dsh-preview-float-pill',
|
|
1025
1486
|
onClick: () => setPanelOpen(true),
|
|
1026
|
-
title:
|
|
1487
|
+
title: tabs.length > 0 ? `展开文档与工作区文件浏览 (${tabs.length} 个标签)` : '浏览工作空间文件树',
|
|
1027
1488
|
},
|
|
1028
|
-
h('span', { className: 'dsh-float-pill-icon' }, '
|
|
1029
|
-
h('span', { className: 'dsh-float-pill-text' }, `预览: ${activeTab?.title || '文档'}`),
|
|
1030
|
-
h('span', { className: 'dsh-float-pill-badge' }, tabs.length)
|
|
1489
|
+
h('span', { className: 'dsh-float-pill-icon' }, '📁'),
|
|
1490
|
+
h('span', { className: 'dsh-float-pill-text' }, tabs.length > 0 ? `预览: ${activeTab?.title || '文档'}` : '工作区文件'),
|
|
1491
|
+
tabs.length > 0 ? h('span', { className: 'dsh-float-pill-badge' }, tabs.length) : null
|
|
1031
1492
|
) : null,
|
|
1032
1493
|
|
|
1033
1494
|
// Right-Side Slide Drawer
|
|
1034
1495
|
isOpen ? h('div', {
|
|
1035
|
-
|
|
1496
|
+
ref: drawerRef,
|
|
1497
|
+
className: `dsh-preview-drawer-container ${isDragging || isTreeDragging ? 'dragging' : ''}`,
|
|
1036
1498
|
style: { width: `${width}px` },
|
|
1037
1499
|
},
|
|
1038
1500
|
// Drag Handle on Left Border
|
|
@@ -1048,7 +1510,7 @@
|
|
|
1048
1510
|
h('div', { className: 'dsh-preview-drawer-header' },
|
|
1049
1511
|
// Tabs Bar
|
|
1050
1512
|
h('div', { className: 'dsh-preview-tabs-row' },
|
|
1051
|
-
tabs.map(tab => {
|
|
1513
|
+
tabs.length > 0 ? tabs.map(tab => {
|
|
1052
1514
|
const isActive = tab.id === activeTabId;
|
|
1053
1515
|
const ext = (tab.extension || '').toLowerCase();
|
|
1054
1516
|
const icon = ext === '.json' ? '{ }' : (ext === '.ts' || ext === '.js' ? 'TS' : '📝');
|
|
@@ -1067,11 +1529,20 @@
|
|
|
1067
1529
|
title: '关闭此标签',
|
|
1068
1530
|
}, '✕')
|
|
1069
1531
|
);
|
|
1070
|
-
})
|
|
1532
|
+
}) : h('div', { className: 'dsh-preview-tab-item active' },
|
|
1533
|
+
h('span', { className: 'dsh-tab-icon' }, '📁'),
|
|
1534
|
+
h('span', { className: 'dsh-tab-title' }, '工作空间文件树')
|
|
1535
|
+
)
|
|
1071
1536
|
),
|
|
1072
1537
|
|
|
1073
1538
|
// Header Right Window Controls
|
|
1074
1539
|
h('div', { className: 'dsh-preview-window-controls' },
|
|
1540
|
+
h('button', {
|
|
1541
|
+
type: 'button',
|
|
1542
|
+
className: `dsh-win-ctrl-btn ${isTreeOpen ? 'active' : ''}`,
|
|
1543
|
+
onClick: () => setIsTreeOpen(!globalIsTreeOpen),
|
|
1544
|
+
title: isTreeOpen ? '收起目录树 (Ctrl/Cmd+B)' : '展开目录树 (Ctrl/Cmd+B)',
|
|
1545
|
+
}, '🗂️ 目录树'),
|
|
1075
1546
|
tabs.length > 1 ? h('button', {
|
|
1076
1547
|
type: 'button',
|
|
1077
1548
|
className: 'dsh-win-ctrl-btn',
|
|
@@ -1087,16 +1558,43 @@
|
|
|
1087
1558
|
)
|
|
1088
1559
|
),
|
|
1089
1560
|
|
|
1090
|
-
// Drawer Body
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1561
|
+
// Drawer Body (Split View between Workspace Tree & Preview View)
|
|
1562
|
+
h('div', { className: 'dsh-preview-drawer-body-split' },
|
|
1563
|
+
// Left: Workspace File Tree
|
|
1564
|
+
isTreeOpen ? h(WorkspaceTreeSidebar, {
|
|
1565
|
+
width: treeWidth,
|
|
1566
|
+
activeFilePath: activeTab?.filePath,
|
|
1567
|
+
onOpenFile: openPreviewFile,
|
|
1568
|
+
onCloseSidebar: () => setIsTreeOpen(false),
|
|
1569
|
+
}) : null,
|
|
1570
|
+
|
|
1571
|
+
// Resizer between Tree and Content
|
|
1572
|
+
isTreeOpen ? h('div', {
|
|
1573
|
+
className: `dsh-tree-splitter ${isTreeDragging ? 'dragging' : ''}`,
|
|
1574
|
+
onPointerDown: onTreeResizePointerDown,
|
|
1575
|
+
onPointerMove: onTreeResizePointerMove,
|
|
1576
|
+
onPointerUp: onTreeResizePointerUp,
|
|
1577
|
+
title: '拖拽调整目录树宽度',
|
|
1578
|
+
}) : null,
|
|
1579
|
+
|
|
1580
|
+
// Right: Content View or Welcome View
|
|
1581
|
+
activeTab ? h(MarkdownPreviewView, {
|
|
1582
|
+
tab: activeTab,
|
|
1583
|
+
fileInfo: currentFileInfo,
|
|
1584
|
+
isLoading: currentLoading,
|
|
1585
|
+
error: currentError,
|
|
1586
|
+
isTreeOpen: isTreeOpen,
|
|
1587
|
+
onToggleTree: () => setIsTreeOpen(!globalIsTreeOpen),
|
|
1588
|
+
onRefresh: () => loadTabFile(activeTab, true),
|
|
1589
|
+
onOpenNative: triggerOpenNative,
|
|
1590
|
+
onReveal: triggerRevealInExplorer,
|
|
1591
|
+
}) : h(WorkspaceWelcomeView, {
|
|
1592
|
+
workspaceRoot: getCurrentWorkspaceRoot(),
|
|
1593
|
+
workspaceName: getFileName(getCurrentWorkspaceRoot()),
|
|
1594
|
+
onOpenFile: openPreviewFile,
|
|
1595
|
+
onOpenTree: () => setIsTreeOpen(true),
|
|
1596
|
+
})
|
|
1597
|
+
)
|
|
1100
1598
|
) : null
|
|
1101
1599
|
);
|
|
1102
1600
|
}
|
|
@@ -2162,6 +2660,394 @@
|
|
|
2162
2660
|
.dsh-preview-toast-success .dsh-preview-toast-icon { color: #22c55e; font-weight: bold; }
|
|
2163
2661
|
.dsh-preview-toast-error { border-color: #ef4444; }
|
|
2164
2662
|
.dsh-preview-toast-error .dsh-preview-toast-icon { color: #ef4444; font-weight: bold; }
|
|
2663
|
+
|
|
2664
|
+
/* Workspace Tree Sidebar & Split Layout */
|
|
2665
|
+
.dsh-preview-drawer-body-split {
|
|
2666
|
+
display: flex;
|
|
2667
|
+
flex-direction: row;
|
|
2668
|
+
flex: 1;
|
|
2669
|
+
min-height: 0;
|
|
2670
|
+
overflow: hidden;
|
|
2671
|
+
width: 100%;
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
.dsh-tree-sidebar {
|
|
2675
|
+
display: flex;
|
|
2676
|
+
flex-direction: column;
|
|
2677
|
+
overflow: hidden;
|
|
2678
|
+
flex-shrink: 0;
|
|
2679
|
+
background: var(--dsw-alias-bg-layer-2, #f8fafc);
|
|
2680
|
+
border-right: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
|
|
2681
|
+
height: 100%;
|
|
2682
|
+
user-select: none;
|
|
2683
|
+
box-sizing: border-box;
|
|
2684
|
+
}
|
|
2685
|
+
body[data-ds-dark-theme] .dsh-tree-sidebar {
|
|
2686
|
+
background: var(--dsw-alias-bg-layer-2, #151821);
|
|
2687
|
+
border-right-color: var(--dsw-alias-border-l3, #2a3140);
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
.dsh-tree-sidebar-header {
|
|
2691
|
+
height: 36px;
|
|
2692
|
+
min-height: 36px;
|
|
2693
|
+
display: flex;
|
|
2694
|
+
align-items: center;
|
|
2695
|
+
justify-content: space-between;
|
|
2696
|
+
padding: 0 8px;
|
|
2697
|
+
border-bottom: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
|
|
2698
|
+
font-weight: 600;
|
|
2699
|
+
font-size: 12px;
|
|
2700
|
+
color: var(--dsw-alias-label-primary, #0f172a);
|
|
2701
|
+
background: rgba(0, 0, 0, 0.02);
|
|
2702
|
+
box-sizing: border-box;
|
|
2703
|
+
}
|
|
2704
|
+
body[data-ds-dark-theme] .dsh-tree-sidebar-header {
|
|
2705
|
+
border-bottom-color: var(--dsw-alias-border-l3, #2a3140);
|
|
2706
|
+
background: rgba(255, 255, 255, 0.02);
|
|
2707
|
+
color: #e2e8f0;
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
.dsh-tree-sidebar-title {
|
|
2711
|
+
display: flex;
|
|
2712
|
+
align-items: center;
|
|
2713
|
+
gap: 5px;
|
|
2714
|
+
overflow: hidden;
|
|
2715
|
+
text-overflow: ellipsis;
|
|
2716
|
+
white-space: nowrap;
|
|
2717
|
+
font-size: 12px;
|
|
2718
|
+
}
|
|
2719
|
+
.dsh-tree-header-icon { font-size: 13px; }
|
|
2720
|
+
.dsh-tree-header-text {
|
|
2721
|
+
overflow: hidden;
|
|
2722
|
+
text-overflow: ellipsis;
|
|
2723
|
+
white-space: nowrap;
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
.dsh-tree-header-actions {
|
|
2727
|
+
display: flex;
|
|
2728
|
+
align-items: center;
|
|
2729
|
+
gap: 2px;
|
|
2730
|
+
flex-shrink: 0;
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
.dsh-tree-action-btn {
|
|
2734
|
+
width: 22px;
|
|
2735
|
+
height: 22px;
|
|
2736
|
+
border-radius: 4px;
|
|
2737
|
+
border: none;
|
|
2738
|
+
background: transparent;
|
|
2739
|
+
color: var(--dsw-alias-label-secondary, #64748b);
|
|
2740
|
+
cursor: pointer;
|
|
2741
|
+
display: inline-flex;
|
|
2742
|
+
align-items: center;
|
|
2743
|
+
justify-content: center;
|
|
2744
|
+
font-size: 11px;
|
|
2745
|
+
padding: 0;
|
|
2746
|
+
transition: all 0.1s;
|
|
2747
|
+
}
|
|
2748
|
+
.dsh-tree-action-btn:hover {
|
|
2749
|
+
background: rgba(0, 0, 0, 0.06);
|
|
2750
|
+
color: var(--dsw-alias-label-primary, #0f172a);
|
|
2751
|
+
}
|
|
2752
|
+
body[data-ds-dark-theme] .dsh-tree-action-btn:hover {
|
|
2753
|
+
background: rgba(255, 255, 255, 0.08);
|
|
2754
|
+
color: #ffffff;
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
.dsh-tree-sidebar-search {
|
|
2758
|
+
padding: 6px 8px;
|
|
2759
|
+
border-bottom: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
|
|
2760
|
+
background: var(--dsw-alias-bg-layer-1, #ffffff);
|
|
2761
|
+
box-sizing: border-box;
|
|
2762
|
+
}
|
|
2763
|
+
body[data-ds-dark-theme] .dsh-tree-sidebar-search {
|
|
2764
|
+
border-bottom-color: var(--dsw-alias-border-l3, #2a3140);
|
|
2765
|
+
background: var(--dsw-alias-bg-layer-1, #161922);
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
.dsh-tree-search-wrapper {
|
|
2769
|
+
position: relative;
|
|
2770
|
+
display: flex;
|
|
2771
|
+
align-items: center;
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
.dsh-tree-search-input {
|
|
2775
|
+
width: 100%;
|
|
2776
|
+
height: 24px;
|
|
2777
|
+
border-radius: 4px;
|
|
2778
|
+
border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
|
|
2779
|
+
padding: 0 20px 0 6px;
|
|
2780
|
+
font-size: 11px;
|
|
2781
|
+
background: var(--dsw-alias-bg-base, #f8fafc);
|
|
2782
|
+
color: inherit;
|
|
2783
|
+
box-sizing: border-box;
|
|
2784
|
+
outline: none;
|
|
2785
|
+
transition: border-color 0.15s;
|
|
2786
|
+
}
|
|
2787
|
+
.dsh-tree-search-input:focus {
|
|
2788
|
+
border-color: var(--dsw-static-deepseek-500, #4176e6);
|
|
2789
|
+
background: #ffffff;
|
|
2790
|
+
}
|
|
2791
|
+
body[data-ds-dark-theme] .dsh-tree-search-input {
|
|
2792
|
+
background: #1e2330;
|
|
2793
|
+
border-color: #334155;
|
|
2794
|
+
color: #f1f5f9;
|
|
2795
|
+
}
|
|
2796
|
+
body[data-ds-dark-theme] .dsh-tree-search-input:focus {
|
|
2797
|
+
background: #181b24;
|
|
2798
|
+
border-color: #60a5fa;
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
.dsh-tree-search-clear {
|
|
2802
|
+
position: absolute;
|
|
2803
|
+
right: 4px;
|
|
2804
|
+
width: 16px;
|
|
2805
|
+
height: 16px;
|
|
2806
|
+
border-radius: 50%;
|
|
2807
|
+
border: none;
|
|
2808
|
+
background: rgba(0, 0, 0, 0.1);
|
|
2809
|
+
color: inherit;
|
|
2810
|
+
font-size: 9px;
|
|
2811
|
+
display: flex;
|
|
2812
|
+
align-items: center;
|
|
2813
|
+
justify-content: center;
|
|
2814
|
+
cursor: pointer;
|
|
2815
|
+
padding: 0;
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
.dsh-tree-node-list {
|
|
2819
|
+
flex: 1;
|
|
2820
|
+
min-height: 0;
|
|
2821
|
+
overflow-y: auto;
|
|
2822
|
+
overflow-x: hidden;
|
|
2823
|
+
padding: 4px 2px;
|
|
2824
|
+
box-sizing: border-box;
|
|
2825
|
+
}
|
|
2826
|
+
.dsh-tree-node-list::-webkit-scrollbar {
|
|
2827
|
+
width: 4px;
|
|
2828
|
+
}
|
|
2829
|
+
.dsh-tree-node-list::-webkit-scrollbar-thumb {
|
|
2830
|
+
background: rgba(0, 0, 0, 0.15);
|
|
2831
|
+
border-radius: 2px;
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
.dsh-tree-node-row {
|
|
2835
|
+
display: flex;
|
|
2836
|
+
align-items: center;
|
|
2837
|
+
height: 24px;
|
|
2838
|
+
min-height: 24px;
|
|
2839
|
+
border-radius: 4px;
|
|
2840
|
+
margin: 1px 4px;
|
|
2841
|
+
cursor: pointer;
|
|
2842
|
+
font-size: 12px;
|
|
2843
|
+
color: var(--dsw-alias-label-secondary, #334155);
|
|
2844
|
+
gap: 3px;
|
|
2845
|
+
transition: background 0.1s ease, color 0.1s ease;
|
|
2846
|
+
overflow: hidden;
|
|
2847
|
+
box-sizing: border-box;
|
|
2848
|
+
}
|
|
2849
|
+
body[data-ds-dark-theme] .dsh-tree-node-row {
|
|
2850
|
+
color: #94a3b8;
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
.dsh-tree-node-row:hover {
|
|
2854
|
+
background: var(--dsw-alias-bg-hover, rgba(0, 0, 0, 0.05));
|
|
2855
|
+
color: var(--dsw-alias-label-primary, #0f172a);
|
|
2856
|
+
}
|
|
2857
|
+
body[data-ds-dark-theme] .dsh-tree-node-row:hover {
|
|
2858
|
+
background: rgba(255, 255, 255, 0.06);
|
|
2859
|
+
color: #f1f5f9;
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
.dsh-tree-node-row.active {
|
|
2863
|
+
background: rgba(14, 165, 233, 0.14);
|
|
2864
|
+
color: #0284c7;
|
|
2865
|
+
font-weight: 600;
|
|
2866
|
+
}
|
|
2867
|
+
body[data-ds-dark-theme] .dsh-tree-node-row.active {
|
|
2868
|
+
background: rgba(56, 189, 248, 0.16);
|
|
2869
|
+
color: #38bdf8;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
.dsh-tree-chevron {
|
|
2873
|
+
width: 14px;
|
|
2874
|
+
height: 14px;
|
|
2875
|
+
display: inline-flex;
|
|
2876
|
+
align-items: center;
|
|
2877
|
+
justify-content: center;
|
|
2878
|
+
font-size: 8px;
|
|
2879
|
+
color: #94a3b8;
|
|
2880
|
+
flex-shrink: 0;
|
|
2881
|
+
user-select: none;
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
.dsh-tree-chevron-placeholder {
|
|
2885
|
+
width: 14px;
|
|
2886
|
+
height: 14px;
|
|
2887
|
+
display: inline-flex;
|
|
2888
|
+
flex-shrink: 0;
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
.dsh-tree-icon {
|
|
2892
|
+
width: 16px;
|
|
2893
|
+
height: 16px;
|
|
2894
|
+
display: inline-flex;
|
|
2895
|
+
align-items: center;
|
|
2896
|
+
justify-content: center;
|
|
2897
|
+
font-size: 12px;
|
|
2898
|
+
flex-shrink: 0;
|
|
2899
|
+
font-family: ui-monospace, monospace;
|
|
2900
|
+
font-weight: 700;
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
.dsh-tree-name {
|
|
2904
|
+
flex: 1;
|
|
2905
|
+
overflow: hidden;
|
|
2906
|
+
text-overflow: ellipsis;
|
|
2907
|
+
white-space: nowrap;
|
|
2908
|
+
font-size: 12px;
|
|
2909
|
+
text-align: left;
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
.dsh-tree-badge {
|
|
2913
|
+
font-size: 10px;
|
|
2914
|
+
color: #94a3b8;
|
|
2915
|
+
background: rgba(0, 0, 0, 0.05);
|
|
2916
|
+
padding: 0 4px;
|
|
2917
|
+
border-radius: 8px;
|
|
2918
|
+
margin-right: 4px;
|
|
2919
|
+
flex-shrink: 0;
|
|
2920
|
+
}
|
|
2921
|
+
body[data-ds-dark-theme] .dsh-tree-badge {
|
|
2922
|
+
background: rgba(255, 255, 255, 0.08);
|
|
2923
|
+
color: #64748b;
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
.dsh-tree-size-badge {
|
|
2927
|
+
font-size: 10px;
|
|
2928
|
+
color: #94a3b8;
|
|
2929
|
+
margin-right: 4px;
|
|
2930
|
+
flex-shrink: 0;
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
.dsh-tree-loading, .dsh-tree-error, .dsh-tree-empty {
|
|
2934
|
+
padding: 24px 12px;
|
|
2935
|
+
text-align: center;
|
|
2936
|
+
font-size: 12px;
|
|
2937
|
+
color: #94a3b8;
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
.dsh-tree-retry-btn {
|
|
2941
|
+
margin-top: 8px;
|
|
2942
|
+
padding: 2px 10px;
|
|
2943
|
+
border-radius: 4px;
|
|
2944
|
+
border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
|
|
2945
|
+
background: transparent;
|
|
2946
|
+
color: inherit;
|
|
2947
|
+
cursor: pointer;
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
.dsh-tree-sidebar-footer {
|
|
2951
|
+
height: 24px;
|
|
2952
|
+
min-height: 24px;
|
|
2953
|
+
border-top: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
|
|
2954
|
+
display: flex;
|
|
2955
|
+
align-items: center;
|
|
2956
|
+
justify-content: center;
|
|
2957
|
+
gap: 6px;
|
|
2958
|
+
font-size: 11px;
|
|
2959
|
+
color: #94a3b8;
|
|
2960
|
+
background: rgba(0, 0, 0, 0.015);
|
|
2961
|
+
}
|
|
2962
|
+
body[data-ds-dark-theme] .dsh-tree-sidebar-footer {
|
|
2963
|
+
border-top-color: var(--dsw-alias-border-l3, #2a3140);
|
|
2964
|
+
}
|
|
2965
|
+
.dsh-tree-footer-dot { font-size: 8px; }
|
|
2966
|
+
|
|
2967
|
+
/* Splitter between Tree and Preview */
|
|
2968
|
+
.dsh-tree-splitter {
|
|
2969
|
+
width: 4px;
|
|
2970
|
+
cursor: col-resize;
|
|
2971
|
+
background: transparent;
|
|
2972
|
+
transition: background 0.15s;
|
|
2973
|
+
flex-shrink: 0;
|
|
2974
|
+
z-index: 10;
|
|
2975
|
+
}
|
|
2976
|
+
.dsh-tree-splitter:hover, .dsh-tree-splitter.dragging {
|
|
2977
|
+
background: var(--dsw-static-deepseek-500, #4176e6);
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
/* Welcome Empty View */
|
|
2981
|
+
.dsh-preview-welcome-container {
|
|
2982
|
+
flex: 1;
|
|
2983
|
+
display: flex;
|
|
2984
|
+
align-items: center;
|
|
2985
|
+
justify-content: center;
|
|
2986
|
+
padding: 32px;
|
|
2987
|
+
background: var(--dsw-alias-bg-layer-1, #ffffff);
|
|
2988
|
+
color: var(--dsw-alias-label-primary, #0f172a);
|
|
2989
|
+
overflow: auto;
|
|
2990
|
+
}
|
|
2991
|
+
body[data-ds-dark-theme] .dsh-preview-welcome-container {
|
|
2992
|
+
background: var(--dsw-alias-bg-layer-1, #161922);
|
|
2993
|
+
color: #e2e8f0;
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
.dsh-preview-welcome-card {
|
|
2997
|
+
max-width: 420px;
|
|
2998
|
+
text-align: center;
|
|
2999
|
+
display: flex;
|
|
3000
|
+
flex-direction: column;
|
|
3001
|
+
align-items: center;
|
|
3002
|
+
gap: 12px;
|
|
3003
|
+
}
|
|
3004
|
+
.dsh-welcome-icon { font-size: 40px; }
|
|
3005
|
+
.dsh-welcome-title { margin: 0; font-size: 16px; font-weight: 600; }
|
|
3006
|
+
.dsh-welcome-path {
|
|
3007
|
+
margin: 0;
|
|
3008
|
+
font-size: 11px;
|
|
3009
|
+
color: #64748b;
|
|
3010
|
+
background: rgba(0, 0, 0, 0.04);
|
|
3011
|
+
padding: 4px 8px;
|
|
3012
|
+
border-radius: 4px;
|
|
3013
|
+
word-break: break-all;
|
|
3014
|
+
}
|
|
3015
|
+
body[data-ds-dark-theme] .dsh-welcome-path {
|
|
3016
|
+
background: rgba(255, 255, 255, 0.06);
|
|
3017
|
+
color: #94a3b8;
|
|
3018
|
+
}
|
|
3019
|
+
.dsh-welcome-desc {
|
|
3020
|
+
margin: 0;
|
|
3021
|
+
font-size: 12px;
|
|
3022
|
+
color: var(--dsw-alias-label-secondary, #64748b);
|
|
3023
|
+
line-height: 1.6;
|
|
3024
|
+
}
|
|
3025
|
+
.dsh-welcome-actions {
|
|
3026
|
+
display: flex;
|
|
3027
|
+
gap: 8px;
|
|
3028
|
+
margin-top: 8px;
|
|
3029
|
+
}
|
|
3030
|
+
.dsh-welcome-btn {
|
|
3031
|
+
padding: 6px 14px;
|
|
3032
|
+
border-radius: 6px;
|
|
3033
|
+
font-size: 12px;
|
|
3034
|
+
border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
|
|
3035
|
+
background: transparent;
|
|
3036
|
+
color: inherit;
|
|
3037
|
+
cursor: pointer;
|
|
3038
|
+
transition: all 0.15s;
|
|
3039
|
+
}
|
|
3040
|
+
.dsh-welcome-btn:hover {
|
|
3041
|
+
background: rgba(0, 0, 0, 0.05);
|
|
3042
|
+
}
|
|
3043
|
+
.dsh-welcome-btn.primary {
|
|
3044
|
+
background: var(--dsw-static-deepseek-500, #4176e6);
|
|
3045
|
+
color: #ffffff;
|
|
3046
|
+
border-color: var(--dsw-static-deepseek-500, #4176e6);
|
|
3047
|
+
}
|
|
3048
|
+
.dsh-welcome-btn.primary:hover {
|
|
3049
|
+
opacity: 0.9;
|
|
3050
|
+
}
|
|
2165
3051
|
`;
|
|
2166
3052
|
document.head.appendChild(style);
|
|
2167
3053
|
}
|
package/lib/index.js
CHANGED
|
@@ -130,6 +130,146 @@ export class PreviewService extends Service {
|
|
|
130
130
|
});
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
readWorkspaceTree(targetDir, maxDepth = 4, includeAll = false) {
|
|
134
|
+
try {
|
|
135
|
+
const normalizedRoot = path.resolve(targetDir);
|
|
136
|
+
if (!fs.existsSync(normalizedRoot)) {
|
|
137
|
+
return { ok: false, error: { code: 'DIR_NOT_FOUND', message: '工作区目录不存在: ' + normalizedRoot } };
|
|
138
|
+
}
|
|
139
|
+
const stat = fs.statSync(normalizedRoot);
|
|
140
|
+
if (!stat.isDirectory()) {
|
|
141
|
+
return { ok: false, error: { code: 'NOT_A_DIRECTORY', message: '目标路径不是目录: ' + normalizedRoot } };
|
|
142
|
+
}
|
|
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'
|
|
147
|
+
]);
|
|
148
|
+
let totalFiles = 0;
|
|
149
|
+
let totalDirs = 0;
|
|
150
|
+
|
|
151
|
+
const buildTree = (dirPath, currentDepth) => {
|
|
152
|
+
if (currentDepth > maxDepth) return [];
|
|
153
|
+
let entries = [];
|
|
154
|
+
try {
|
|
155
|
+
entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
156
|
+
} catch (e) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
const nodes = [];
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
const entryName = entry.name;
|
|
162
|
+
const lowerName = entryName.toLowerCase();
|
|
163
|
+
if (!includeAll) {
|
|
164
|
+
if (ignoreDirs.has(lowerName)) continue;
|
|
165
|
+
if (entryName.startsWith('.') && entryName !== '.env' && entryName !== '.gitignore') continue;
|
|
166
|
+
}
|
|
167
|
+
const fullPath = path.join(dirPath, entryName);
|
|
168
|
+
const relPath = path.relative(normalizedRoot, fullPath).replace(/\\/g, '/');
|
|
169
|
+
try {
|
|
170
|
+
if (entry.isDirectory()) {
|
|
171
|
+
totalDirs++;
|
|
172
|
+
const children = buildTree(fullPath, currentDepth + 1);
|
|
173
|
+
nodes.push({
|
|
174
|
+
name: entryName,
|
|
175
|
+
path: fullPath,
|
|
176
|
+
relativePath: relPath,
|
|
177
|
+
isDir: true,
|
|
178
|
+
children,
|
|
179
|
+
childCount: children.length,
|
|
180
|
+
});
|
|
181
|
+
} else if (entry.isFile()) {
|
|
182
|
+
totalFiles++;
|
|
183
|
+
const ext = path.extname(entryName).toLowerCase();
|
|
184
|
+
let size = 0;
|
|
185
|
+
let mtime = 0;
|
|
186
|
+
try {
|
|
187
|
+
const s = fs.statSync(fullPath);
|
|
188
|
+
size = s.size;
|
|
189
|
+
mtime = s.mtimeMs;
|
|
190
|
+
} catch (e) {}
|
|
191
|
+
nodes.push({
|
|
192
|
+
name: entryName,
|
|
193
|
+
path: fullPath,
|
|
194
|
+
relativePath: relPath,
|
|
195
|
+
isDir: false,
|
|
196
|
+
extension: ext,
|
|
197
|
+
size,
|
|
198
|
+
mtime,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
} catch (e) {}
|
|
202
|
+
}
|
|
203
|
+
nodes.sort((a, b) => {
|
|
204
|
+
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
205
|
+
return a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' });
|
|
206
|
+
});
|
|
207
|
+
return nodes;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const tree = buildTree(normalizedRoot, 1);
|
|
211
|
+
const workspaceName = path.basename(normalizedRoot) || normalizedRoot;
|
|
212
|
+
return {
|
|
213
|
+
ok: true,
|
|
214
|
+
workspaceRoot: normalizedRoot,
|
|
215
|
+
workspaceName,
|
|
216
|
+
tree,
|
|
217
|
+
totalFiles,
|
|
218
|
+
totalDirs,
|
|
219
|
+
};
|
|
220
|
+
} catch (err) {
|
|
221
|
+
return { ok: false, error: { code: 'SCAN_ERROR', message: err.message || '扫描工作区文件树失败' } };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
listDirectoryEntries(targetDir, includeAll = false) {
|
|
226
|
+
try {
|
|
227
|
+
const normalized = path.resolve(targetDir);
|
|
228
|
+
if (!fs.existsSync(normalized) || !fs.statSync(normalized).isDirectory()) {
|
|
229
|
+
return { ok: false, error: { code: 'DIR_NOT_FOUND', message: '目录不存在' } };
|
|
230
|
+
}
|
|
231
|
+
const ignoreDirs = new Set(['node_modules', '.git', '.dsh', '.gemini', '.tempmediastorage', '.idea', '.vscode']);
|
|
232
|
+
const rawEntries = fs.readdirSync(normalized, { withFileTypes: true });
|
|
233
|
+
const nodes = [];
|
|
234
|
+
for (const entry of rawEntries) {
|
|
235
|
+
const name = entry.name;
|
|
236
|
+
if (!includeAll) {
|
|
237
|
+
if (ignoreDirs.has(name.toLowerCase())) continue;
|
|
238
|
+
if (name.startsWith('.') && name !== '.env' && name !== '.gitignore') continue;
|
|
239
|
+
}
|
|
240
|
+
const fullPath = path.join(normalized, name);
|
|
241
|
+
const isDir = entry.isDirectory();
|
|
242
|
+
let size = 0;
|
|
243
|
+
let mtime = 0;
|
|
244
|
+
let ext = '';
|
|
245
|
+
if (!isDir) {
|
|
246
|
+
ext = path.extname(name).toLowerCase();
|
|
247
|
+
try {
|
|
248
|
+
const s = fs.statSync(fullPath);
|
|
249
|
+
size = s.size;
|
|
250
|
+
mtime = s.mtimeMs;
|
|
251
|
+
} catch (e) {}
|
|
252
|
+
}
|
|
253
|
+
nodes.push({
|
|
254
|
+
name,
|
|
255
|
+
path: fullPath,
|
|
256
|
+
relativePath: name,
|
|
257
|
+
isDir,
|
|
258
|
+
extension: ext,
|
|
259
|
+
size,
|
|
260
|
+
mtime,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
nodes.sort((a, b) => {
|
|
264
|
+
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
265
|
+
return a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' });
|
|
266
|
+
});
|
|
267
|
+
return { ok: true, entries: nodes };
|
|
268
|
+
} catch (err) {
|
|
269
|
+
return { ok: false, error: { code: 'LIST_ERROR', message: err.message || '读取目录失败' } };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
133
273
|
async handleHttpRequest(req, res) {
|
|
134
274
|
this.setCorsHeaders(res);
|
|
135
275
|
if (req.method === 'OPTIONS') {
|
|
@@ -144,7 +284,7 @@ export class PreviewService extends Service {
|
|
|
144
284
|
try {
|
|
145
285
|
if (pathname === '/api/preview/status') {
|
|
146
286
|
res.statusCode = 200;
|
|
147
|
-
res.end(JSON.stringify({ ok: true, version: '0.1.
|
|
287
|
+
res.end(JSON.stringify({ ok: true, version: '0.1.3', service: 'preview' }));
|
|
148
288
|
return;
|
|
149
289
|
}
|
|
150
290
|
if (pathname === '/api/preview/read' && req.method === 'GET') {
|
|
@@ -159,6 +299,37 @@ export class PreviewService extends Service {
|
|
|
159
299
|
res.end(JSON.stringify(result));
|
|
160
300
|
return;
|
|
161
301
|
}
|
|
302
|
+
if (pathname === '/api/preview/workspace-tree' && req.method === 'GET') {
|
|
303
|
+
const targetPath = url.searchParams.get('path') || process.cwd();
|
|
304
|
+
const depth = parseInt(url.searchParams.get('depth') || '4', 10);
|
|
305
|
+
const includeAll = url.searchParams.get('includeAll') === 'true';
|
|
306
|
+
const result = this.readWorkspaceTree(targetPath, isNaN(depth) ? 4 : depth, includeAll);
|
|
307
|
+
res.statusCode = result.ok ? 200 : 404;
|
|
308
|
+
res.end(JSON.stringify(result));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (pathname === '/api/preview/list-dir' && req.method === 'GET') {
|
|
312
|
+
const targetPath = url.searchParams.get('path') || process.cwd();
|
|
313
|
+
const includeAll = url.searchParams.get('includeAll') === 'true';
|
|
314
|
+
const result = this.listDirectoryEntries(targetPath, includeAll);
|
|
315
|
+
res.statusCode = result.ok ? 200 : 404;
|
|
316
|
+
res.end(JSON.stringify(result));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (pathname === '/api/preview/save' && req.method === 'POST') {
|
|
320
|
+
const body = await this.parseRequestBody(req);
|
|
321
|
+
if (!body.path || typeof body.content !== 'string') {
|
|
322
|
+
res.statusCode = 400;
|
|
323
|
+
res.end(JSON.stringify({ ok: false, error: { code: 'INVALID_PARAMS', message: '参数格式错误' } }));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const normalized = path.resolve(body.path);
|
|
327
|
+
fs.writeFileSync(normalized, body.content, 'utf-8');
|
|
328
|
+
const stat = fs.statSync(normalized);
|
|
329
|
+
res.statusCode = 200;
|
|
330
|
+
res.end(JSON.stringify({ ok: true, size: stat.size, mtime: stat.mtimeMs }));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
162
333
|
if (pathname === '/api/preview/reveal' && req.method === 'POST') {
|
|
163
334
|
const body = await this.parseRequestBody(req);
|
|
164
335
|
if (!body.path) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rooode/dsh-plugin-preview",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "DeepSeek Harness Markdown
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "DeepSeek Harness Markdown 文档与工作空间文件浏览器右侧预览插件 (参考 WorkBuddy FileTabs 实现,支持工作区文件树、多标签、GFM 渲染、TOC 大纲、源码分栏)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|