@rooode/dsh-plugin-preview 0.1.2 → 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 +876 -47
- 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,9 @@
|
|
|
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;
|
|
43
50
|
let globalLastOpenTime = 0;
|
|
44
51
|
const stateListeners = new Set();
|
|
45
52
|
|
|
@@ -53,6 +60,23 @@
|
|
|
53
60
|
}
|
|
54
61
|
} catch (e) {}
|
|
55
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
|
+
|
|
56
80
|
try {
|
|
57
81
|
const savedTabs = localStorage.getItem(STORAGE_KEY_TABS);
|
|
58
82
|
if (savedTabs) {
|
|
@@ -84,6 +108,22 @@
|
|
|
84
108
|
notifyStateChange();
|
|
85
109
|
}
|
|
86
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
|
+
|
|
87
127
|
function setPanelOpen(open) {
|
|
88
128
|
globalIsPanelOpen = open;
|
|
89
129
|
if (open) {
|
|
@@ -112,6 +152,61 @@
|
|
|
112
152
|
return SUPPORTED_EXTENSIONS.has(ext);
|
|
113
153
|
}
|
|
114
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
|
+
|
|
115
210
|
function resolveAbsoluteFilePath(targetPath) {
|
|
116
211
|
if (!targetPath) return '';
|
|
117
212
|
let cleanPath = targetPath.trim();
|
|
@@ -126,24 +221,7 @@
|
|
|
126
221
|
if (isWinAbs || isPosixAbs) {
|
|
127
222
|
return cleanPath;
|
|
128
223
|
}
|
|
129
|
-
|
|
130
|
-
if (clientCtx && clientCtx.sessions) {
|
|
131
|
-
try {
|
|
132
|
-
const snapshot = clientCtx.sessions.list.getSnapshot();
|
|
133
|
-
const currentSessionId = snapshot.current;
|
|
134
|
-
if (currentSessionId && snapshot.byId[currentSessionId]) {
|
|
135
|
-
cwd = snapshot.byId[currentSessionId].cwd || '';
|
|
136
|
-
}
|
|
137
|
-
} catch (e) {}
|
|
138
|
-
}
|
|
139
|
-
if (!cwd && clientCtx && clientCtx.workspaces) {
|
|
140
|
-
try {
|
|
141
|
-
const wsList = clientCtx.workspaces.list.getSnapshot();
|
|
142
|
-
if (wsList.items && wsList.items.length > 0) {
|
|
143
|
-
cwd = wsList.items[0].directory || '';
|
|
144
|
-
}
|
|
145
|
-
} catch (e) {}
|
|
146
|
-
}
|
|
224
|
+
const cwd = getCurrentWorkspaceRoot();
|
|
147
225
|
if (cwd) {
|
|
148
226
|
const isWinCwd = /^[a-zA-Z]:[\\/]/.test(cwd);
|
|
149
227
|
const sep = isWinCwd ? '\\' : '/';
|
|
@@ -201,7 +279,6 @@
|
|
|
201
279
|
globalActiveTabId = nextTabs[nextIdx].id;
|
|
202
280
|
} else {
|
|
203
281
|
globalActiveTabId = null;
|
|
204
|
-
globalIsPanelOpen = false;
|
|
205
282
|
}
|
|
206
283
|
}
|
|
207
284
|
saveTabsToStorage();
|
|
@@ -211,7 +288,6 @@
|
|
|
211
288
|
function closeAllTabs() {
|
|
212
289
|
globalTabs = [];
|
|
213
290
|
globalActiveTabId = null;
|
|
214
|
-
globalIsPanelOpen = false;
|
|
215
291
|
saveTabsToStorage();
|
|
216
292
|
notifyStateChange();
|
|
217
293
|
}
|
|
@@ -686,10 +762,300 @@
|
|
|
686
762
|
};
|
|
687
763
|
}
|
|
688
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
|
+
|
|
689
1055
|
// =========================================================================
|
|
690
1056
|
// React Components: MarkdownPreviewView
|
|
691
1057
|
// =========================================================================
|
|
692
|
-
function MarkdownPreviewView({ tab, fileInfo, isLoading, error, onRefresh, onOpenNative, onReveal }) {
|
|
1058
|
+
function MarkdownPreviewView({ tab, fileInfo, isLoading, error, isTreeOpen, onToggleTree, onRefresh, onOpenNative, onReveal }) {
|
|
693
1059
|
const [viewMode, setViewMode] = useState(tab.viewMode || 'preview');
|
|
694
1060
|
const [isTocOpen, setIsTocOpen] = useState(false);
|
|
695
1061
|
const [searchQuery, setSearchQuery] = useState('');
|
|
@@ -740,6 +1106,13 @@
|
|
|
740
1106
|
h('div', { className: 'dsh-preview-toolbar' },
|
|
741
1107
|
// Left: View mode toggles
|
|
742
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
|
+
|
|
743
1116
|
h('div', { className: 'dsh-preview-mode-group' },
|
|
744
1117
|
h('button', {
|
|
745
1118
|
type: 'button',
|
|
@@ -933,6 +1306,11 @@
|
|
|
933
1306
|
const [activeTabId, setActiveTabId] = useState(globalActiveTabId);
|
|
934
1307
|
const [isOpen, setIsOpen] = useState(globalIsPanelOpen);
|
|
935
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);
|
|
936
1314
|
const [fileData, setFileData] = useState({});
|
|
937
1315
|
const [loadingMap, setLoadingMap] = useState({});
|
|
938
1316
|
const [errorMap, setErrorMap] = useState({});
|
|
@@ -947,6 +1325,8 @@
|
|
|
947
1325
|
setActiveTabId(globalActiveTabId);
|
|
948
1326
|
setIsOpen(globalIsPanelOpen);
|
|
949
1327
|
setWidth(globalPanelWidth);
|
|
1328
|
+
setLocalIsTreeOpen(globalIsTreeOpen);
|
|
1329
|
+
setLocalTreeWidth(globalTreeWidth);
|
|
950
1330
|
};
|
|
951
1331
|
stateListeners.add(update);
|
|
952
1332
|
return () => stateListeners.delete(update);
|
|
@@ -982,7 +1362,7 @@
|
|
|
982
1362
|
}
|
|
983
1363
|
}, [activeTab, loadTabFile]);
|
|
984
1364
|
|
|
985
|
-
// Drag Resize Handlers
|
|
1365
|
+
// Drag Resize Handlers (Panel Width)
|
|
986
1366
|
const onResizePointerDown = useCallback((e) => {
|
|
987
1367
|
e.preventDefault();
|
|
988
1368
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
@@ -1005,6 +1385,29 @@
|
|
|
1005
1385
|
}
|
|
1006
1386
|
}, [isDragging]);
|
|
1007
1387
|
|
|
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
|
+
|
|
1008
1411
|
const drawerRef = useRef(null);
|
|
1009
1412
|
|
|
1010
1413
|
// Outside Click Listener: Click on the chat conversation / dialog box outside the drawer to auto close/hide it
|
|
@@ -1015,7 +1418,7 @@
|
|
|
1015
1418
|
// 1. Ignore if just opened within 250ms
|
|
1016
1419
|
if (Date.now() - globalLastOpenTime < 250) return;
|
|
1017
1420
|
// 2. Ignore during dragging resize handle
|
|
1018
|
-
if (isDragging) return;
|
|
1421
|
+
if (isDragging || isTreeDragging) return;
|
|
1019
1422
|
|
|
1020
1423
|
const target = e.target;
|
|
1021
1424
|
if (!target) return;
|
|
@@ -1054,42 +1457,44 @@
|
|
|
1054
1457
|
return () => {
|
|
1055
1458
|
window.removeEventListener('pointerdown', handleOutsidePointerDown, true);
|
|
1056
1459
|
};
|
|
1057
|
-
}, [isOpen, isDragging]);
|
|
1460
|
+
}, [isOpen, isDragging, isTreeDragging]);
|
|
1058
1461
|
|
|
1059
|
-
// Hotkey: Esc closes preview
|
|
1462
|
+
// Hotkey: Esc closes preview, Ctrl/Cmd+B toggles file tree
|
|
1060
1463
|
useEffect(() => {
|
|
1061
1464
|
const onKeyDown = (e) => {
|
|
1062
1465
|
if (e.key === 'Escape' && globalIsPanelOpen) {
|
|
1063
1466
|
setPanelOpen(false);
|
|
1064
1467
|
}
|
|
1468
|
+
if ((e.ctrlKey || e.metaKey) && (e.key === 'b' || e.key === 'B')) {
|
|
1469
|
+
e.preventDefault();
|
|
1470
|
+
setIsTreeOpen(!globalIsTreeOpen);
|
|
1471
|
+
}
|
|
1065
1472
|
};
|
|
1066
1473
|
window.addEventListener('keydown', onKeyDown);
|
|
1067
1474
|
return () => window.removeEventListener('keydown', onKeyDown);
|
|
1068
1475
|
}, []);
|
|
1069
1476
|
|
|
1070
|
-
if (!isOpen && tabs.length === 0) return null;
|
|
1071
|
-
|
|
1072
1477
|
const activeKey = activeTab?.filePath?.toLowerCase();
|
|
1073
1478
|
const currentFileInfo = activeKey ? fileData[activeKey] : null;
|
|
1074
1479
|
const currentLoading = activeKey ? !!loadingMap[activeKey] : false;
|
|
1075
1480
|
const currentError = activeKey ? errorMap[activeKey] : null;
|
|
1076
1481
|
|
|
1077
1482
|
return h(Fragment, null,
|
|
1078
|
-
// Floating Mini Button to Reopen (when closed
|
|
1079
|
-
!isOpen
|
|
1483
|
+
// Floating Mini Button to Reopen (when closed)
|
|
1484
|
+
!isOpen ? h('div', {
|
|
1080
1485
|
className: 'dsh-preview-float-pill',
|
|
1081
1486
|
onClick: () => setPanelOpen(true),
|
|
1082
|
-
title:
|
|
1487
|
+
title: tabs.length > 0 ? `展开文档与工作区文件浏览 (${tabs.length} 个标签)` : '浏览工作空间文件树',
|
|
1083
1488
|
},
|
|
1084
|
-
h('span', { className: 'dsh-float-pill-icon' }, '
|
|
1085
|
-
h('span', { className: 'dsh-float-pill-text' }, `预览: ${activeTab?.title || '文档'}`),
|
|
1086
|
-
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
|
|
1087
1492
|
) : null,
|
|
1088
1493
|
|
|
1089
1494
|
// Right-Side Slide Drawer
|
|
1090
1495
|
isOpen ? h('div', {
|
|
1091
1496
|
ref: drawerRef,
|
|
1092
|
-
className: `dsh-preview-drawer-container ${isDragging ? 'dragging' : ''}`,
|
|
1497
|
+
className: `dsh-preview-drawer-container ${isDragging || isTreeDragging ? 'dragging' : ''}`,
|
|
1093
1498
|
style: { width: `${width}px` },
|
|
1094
1499
|
},
|
|
1095
1500
|
// Drag Handle on Left Border
|
|
@@ -1105,7 +1510,7 @@
|
|
|
1105
1510
|
h('div', { className: 'dsh-preview-drawer-header' },
|
|
1106
1511
|
// Tabs Bar
|
|
1107
1512
|
h('div', { className: 'dsh-preview-tabs-row' },
|
|
1108
|
-
tabs.map(tab => {
|
|
1513
|
+
tabs.length > 0 ? tabs.map(tab => {
|
|
1109
1514
|
const isActive = tab.id === activeTabId;
|
|
1110
1515
|
const ext = (tab.extension || '').toLowerCase();
|
|
1111
1516
|
const icon = ext === '.json' ? '{ }' : (ext === '.ts' || ext === '.js' ? 'TS' : '📝');
|
|
@@ -1124,11 +1529,20 @@
|
|
|
1124
1529
|
title: '关闭此标签',
|
|
1125
1530
|
}, '✕')
|
|
1126
1531
|
);
|
|
1127
|
-
})
|
|
1532
|
+
}) : h('div', { className: 'dsh-preview-tab-item active' },
|
|
1533
|
+
h('span', { className: 'dsh-tab-icon' }, '📁'),
|
|
1534
|
+
h('span', { className: 'dsh-tab-title' }, '工作空间文件树')
|
|
1535
|
+
)
|
|
1128
1536
|
),
|
|
1129
1537
|
|
|
1130
1538
|
// Header Right Window Controls
|
|
1131
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
|
+
}, '🗂️ 目录树'),
|
|
1132
1546
|
tabs.length > 1 ? h('button', {
|
|
1133
1547
|
type: 'button',
|
|
1134
1548
|
className: 'dsh-win-ctrl-btn',
|
|
@@ -1144,16 +1558,43 @@
|
|
|
1144
1558
|
)
|
|
1145
1559
|
),
|
|
1146
1560
|
|
|
1147
|
-
// Drawer Body
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
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
|
+
)
|
|
1157
1598
|
) : null
|
|
1158
1599
|
);
|
|
1159
1600
|
}
|
|
@@ -2219,6 +2660,394 @@
|
|
|
2219
2660
|
.dsh-preview-toast-success .dsh-preview-toast-icon { color: #22c55e; font-weight: bold; }
|
|
2220
2661
|
.dsh-preview-toast-error { border-color: #ef4444; }
|
|
2221
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
|
+
}
|
|
2222
3051
|
`;
|
|
2223
3052
|
document.head.appendChild(style);
|
|
2224
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",
|