@civilization/dsh-workspace-files 0.3.0 → 0.3.2
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/README.md +6 -1
- package/lib/client.js +211 -77
- package/lib/client.js.map +1 -1
- package/lib/index.js +151 -47
- package/package.json +1 -1
- package/src/client/icons.tsx +19 -0
- package/src/client/index.tsx +149 -27
- package/src/files.ts +74 -33
- package/src/index.ts +98 -22
package/lib/index.js
CHANGED
|
@@ -1,83 +1,119 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
1
3
|
import Schema from "@deepseek-ai/schemastery";
|
|
2
4
|
import { promises } from "node:fs";
|
|
3
|
-
import { join, relative, resolve } from "node:path";
|
|
4
5
|
//#region src/files.ts
|
|
5
6
|
const DEFAULT_IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
6
7
|
".git",
|
|
7
8
|
".svn",
|
|
8
9
|
".hg",
|
|
9
10
|
"node_modules",
|
|
11
|
+
".pnpm-store",
|
|
10
12
|
".turbo",
|
|
11
13
|
".next",
|
|
14
|
+
".nuxt",
|
|
15
|
+
".output",
|
|
12
16
|
"dist",
|
|
13
17
|
"build",
|
|
14
|
-
"
|
|
18
|
+
"out",
|
|
19
|
+
".cache",
|
|
20
|
+
".idea",
|
|
21
|
+
".vscode",
|
|
22
|
+
".husky",
|
|
23
|
+
"venv",
|
|
24
|
+
".venv",
|
|
25
|
+
"env",
|
|
26
|
+
"__pycache__",
|
|
27
|
+
"target",
|
|
28
|
+
".gradle",
|
|
29
|
+
".cargo",
|
|
30
|
+
"vendor",
|
|
31
|
+
"coverage",
|
|
32
|
+
"tmp",
|
|
33
|
+
"temp",
|
|
34
|
+
"logs",
|
|
35
|
+
".dsh"
|
|
15
36
|
]);
|
|
16
37
|
function isWithin(parent, child) {
|
|
17
38
|
const rel = relative(parent, child);
|
|
18
39
|
return !rel.startsWith("..") && !rel.includes(":");
|
|
19
40
|
}
|
|
41
|
+
async function loadGitIgnoreNames(rootDir) {
|
|
42
|
+
const names = /* @__PURE__ */ new Set();
|
|
43
|
+
try {
|
|
44
|
+
const gitignorePath = join(rootDir, ".gitignore");
|
|
45
|
+
const content = await promises.readFile(gitignorePath, "utf8");
|
|
46
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
47
|
+
const line = rawLine.trim();
|
|
48
|
+
if (!line || line.startsWith("#") || line.startsWith("!")) continue;
|
|
49
|
+
const clean = line.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
50
|
+
if (clean && !clean.includes("*") && !clean.includes("?") && !clean.includes("/")) names.add(clean);
|
|
51
|
+
}
|
|
52
|
+
} catch {}
|
|
53
|
+
return names;
|
|
54
|
+
}
|
|
20
55
|
async function scanWorkspaceTree(cwd, options = {}) {
|
|
21
56
|
const root = resolve(cwd);
|
|
22
57
|
const maxDepth = options.maxDepth ?? 16;
|
|
23
58
|
const showHidden = options.showHidden ?? false;
|
|
24
59
|
const showIgnored = options.showIgnored ?? false;
|
|
60
|
+
const gitIgnoredNames = showIgnored ? /* @__PURE__ */ new Set() : await loadGitIgnoreNames(root);
|
|
25
61
|
let totalFiles = 0;
|
|
26
62
|
let totalDirs = 0;
|
|
63
|
+
let totalNodes = 0;
|
|
64
|
+
const MAX_NODES_SAFETY_LIMIT = 3e4;
|
|
27
65
|
async function walk(dirPath, currentDepth) {
|
|
28
|
-
if (currentDepth > maxDepth) return [];
|
|
66
|
+
if (currentDepth > maxDepth || totalNodes >= MAX_NODES_SAFETY_LIMIT) return [];
|
|
29
67
|
let dirents;
|
|
30
68
|
try {
|
|
31
69
|
dirents = await promises.readdir(dirPath, { withFileTypes: true });
|
|
32
70
|
} catch {
|
|
33
71
|
return [];
|
|
34
72
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
38
|
-
return a.name.localeCompare(b.name, void 0, {
|
|
39
|
-
numeric: true,
|
|
40
|
-
sensitivity: "base"
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
const items = [];
|
|
73
|
+
const dirTasks = [];
|
|
74
|
+
const fileItems = [];
|
|
44
75
|
for (const ent of dirents) {
|
|
76
|
+
if (totalNodes >= MAX_NODES_SAFETY_LIMIT) break;
|
|
45
77
|
const name = ent.name;
|
|
46
78
|
if (!showHidden && name.startsWith(".") && name !== ".gitignore" && name !== ".env") {
|
|
47
79
|
if (!showIgnored) continue;
|
|
48
80
|
}
|
|
49
|
-
if (!showIgnored
|
|
81
|
+
if (!showIgnored) {
|
|
82
|
+
if (ent.isDirectory() && DEFAULT_IGNORED_DIRS.has(name)) continue;
|
|
83
|
+
if (gitIgnoredNames.has(name)) continue;
|
|
84
|
+
}
|
|
50
85
|
const fullPath = join(dirPath, name);
|
|
51
86
|
if (!isWithin(root, fullPath)) continue;
|
|
52
87
|
const relPath = relative(root, fullPath).replace(/\\/g, "/");
|
|
53
88
|
if (ent.isDirectory()) {
|
|
54
89
|
totalDirs += 1;
|
|
55
|
-
|
|
56
|
-
|
|
90
|
+
totalNodes += 1;
|
|
91
|
+
dirTasks.push(walk(fullPath, currentDepth + 1).then((children) => ({
|
|
57
92
|
name,
|
|
58
93
|
path: relPath,
|
|
59
94
|
isDirectory: true,
|
|
60
95
|
children
|
|
61
|
-
});
|
|
96
|
+
})));
|
|
62
97
|
} else {
|
|
63
98
|
totalFiles += 1;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
const st = await promises.stat(fullPath);
|
|
68
|
-
size = st.size;
|
|
69
|
-
mtime = st.mtimeMs;
|
|
70
|
-
} catch {}
|
|
71
|
-
items.push({
|
|
99
|
+
totalNodes += 1;
|
|
100
|
+
fileItems.push({
|
|
72
101
|
name,
|
|
73
102
|
path: relPath,
|
|
74
|
-
isDirectory: false
|
|
75
|
-
size,
|
|
76
|
-
mtime
|
|
103
|
+
isDirectory: false
|
|
77
104
|
});
|
|
78
105
|
}
|
|
79
106
|
}
|
|
80
|
-
|
|
107
|
+
const validDirs = (await Promise.all(dirTasks)).filter((d) => d !== null);
|
|
108
|
+
validDirs.sort((a, b) => a.name.localeCompare(b.name, void 0, {
|
|
109
|
+
numeric: true,
|
|
110
|
+
sensitivity: "base"
|
|
111
|
+
}));
|
|
112
|
+
fileItems.sort((a, b) => a.name.localeCompare(b.name, void 0, {
|
|
113
|
+
numeric: true,
|
|
114
|
+
sensitivity: "base"
|
|
115
|
+
}));
|
|
116
|
+
return [...validDirs, ...fileItems];
|
|
81
117
|
}
|
|
82
118
|
return {
|
|
83
119
|
root,
|
|
@@ -145,6 +181,30 @@ function writeJson(res, status, body) {
|
|
|
145
181
|
});
|
|
146
182
|
res.end(data);
|
|
147
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* 拒绝工作区之外的目标:files.open 的 path 来自请求体,必须是工作区内的相对/绝对路径,
|
|
186
|
+
* 否则模型或页面脚本可以借宿主进程去打开任意本地路径。
|
|
187
|
+
*/
|
|
188
|
+
function assertInsideWorkspace(cwd, fullPath) {
|
|
189
|
+
const root = resolve(cwd);
|
|
190
|
+
const target = resolve(fullPath);
|
|
191
|
+
const rel = relative(root, target);
|
|
192
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new Error("Path must stay inside the workspace.");
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* 启动系统文件管理器/默认应用。spawn 在命令不存在时会 emit 'error',
|
|
196
|
+
* 没有监听器就会变成未捕获异常拖垮宿主进程,因此这里必须兜住。
|
|
197
|
+
*/
|
|
198
|
+
function launch(command, args) {
|
|
199
|
+
const child = spawn(command, args, {
|
|
200
|
+
detached: true,
|
|
201
|
+
stdio: "ignore"
|
|
202
|
+
});
|
|
203
|
+
child.on("error", (err) => {
|
|
204
|
+
console.warn(`[dsh-workspace-files] failed to launch ${command}:`, err?.message || String(err));
|
|
205
|
+
});
|
|
206
|
+
child.unref();
|
|
207
|
+
}
|
|
148
208
|
function resolveCwd(ctx, body) {
|
|
149
209
|
if (body.cwd?.trim()) return body.cwd.trim();
|
|
150
210
|
if (body.root?.trim()) return body.root.trim();
|
|
@@ -155,25 +215,13 @@ function resolveCwd(ctx, body) {
|
|
|
155
215
|
return process.cwd();
|
|
156
216
|
}
|
|
157
217
|
const SETTINGS_NS = "workspace-files";
|
|
218
|
+
const scanCache = /* @__PURE__ */ new Map();
|
|
219
|
+
const CACHE_TTL_MS = 2e4;
|
|
158
220
|
function apply(ctx, config = {}) {
|
|
159
221
|
const liveConfig = {
|
|
160
222
|
maxDepth: typeof config?.maxDepth === "number" && config.maxDepth > 0 ? config.maxDepth : 16,
|
|
161
223
|
showHidden: Boolean(config?.showHidden)
|
|
162
224
|
};
|
|
163
|
-
if (ctx.inject) ctx.inject(["settings"], (scopedCtx) => {
|
|
164
|
-
try {
|
|
165
|
-
const scope = scopedCtx.settings?.register(SETTINGS_NS, Config, { base: liveConfig });
|
|
166
|
-
const applyChange = () => {
|
|
167
|
-
const current = scope?.get?.();
|
|
168
|
-
if (typeof current?.maxDepth === "number") liveConfig.maxDepth = current.maxDepth;
|
|
169
|
-
if (typeof current?.showHidden === "boolean") liveConfig.showHidden = current.showHidden;
|
|
170
|
-
};
|
|
171
|
-
applyChange();
|
|
172
|
-
scope?.watch?.(applyChange);
|
|
173
|
-
} catch (err) {
|
|
174
|
-
console.warn("[dsh-workspace-files] failed to register settings scope:", err);
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
225
|
const webServer = ctx.webServer;
|
|
178
226
|
if (!webServer) return;
|
|
179
227
|
const handleRpc = async (req, res, prefix) => {
|
|
@@ -210,6 +258,39 @@ function apply(ctx, config = {}) {
|
|
|
210
258
|
}
|
|
211
259
|
return;
|
|
212
260
|
}
|
|
261
|
+
if (method === "files.open") {
|
|
262
|
+
try {
|
|
263
|
+
const body = await readJsonBody(req);
|
|
264
|
+
const cwd = resolveCwd(ctx, body);
|
|
265
|
+
const rawPath = String(body.path || "");
|
|
266
|
+
if (!rawPath) {
|
|
267
|
+
writeJson(res, 400, {
|
|
268
|
+
ok: false,
|
|
269
|
+
error: { message: "Missing path parameter" }
|
|
270
|
+
});
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const fullPath = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
|
|
274
|
+
assertInsideWorkspace(cwd, fullPath);
|
|
275
|
+
const action = body.action || "reveal";
|
|
276
|
+
const isDirectory = Boolean(body.isDirectory);
|
|
277
|
+
if (process.platform === "win32") {
|
|
278
|
+
const winPath = fullPath.replace(/\//g, "\\");
|
|
279
|
+
launch("explorer.exe", action === "reveal" && !isDirectory ? [`/select,${winPath}`] : [winPath]);
|
|
280
|
+
} else if (process.platform === "darwin") {
|
|
281
|
+
if (action === "reveal") launch("open", [isDirectory ? fullPath : "-R", fullPath]);
|
|
282
|
+
else launch("open", [fullPath]);
|
|
283
|
+
} else if (action === "reveal") launch("xdg-open", [isDirectory ? fullPath : resolve(fullPath, "..")]);
|
|
284
|
+
else launch("xdg-open", [fullPath]);
|
|
285
|
+
writeJson(res, 200, { ok: true });
|
|
286
|
+
} catch (err) {
|
|
287
|
+
writeJson(res, 500, {
|
|
288
|
+
ok: false,
|
|
289
|
+
error: { message: err?.message || String(err) }
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
213
294
|
if (method !== "files.tree") {
|
|
214
295
|
writeJson(res, 404, {
|
|
215
296
|
ok: false,
|
|
@@ -219,13 +300,36 @@ function apply(ctx, config = {}) {
|
|
|
219
300
|
}
|
|
220
301
|
try {
|
|
221
302
|
const body = await readJsonBody(req);
|
|
303
|
+
const cwd = resolveCwd(ctx, body);
|
|
304
|
+
const showHidden = typeof body.showHidden === "boolean" ? body.showHidden : liveConfig.showHidden;
|
|
305
|
+
const showIgnored = Boolean(body.showIgnored);
|
|
306
|
+
const maxDepth = typeof body.maxDepth === "number" && body.maxDepth > 0 ? body.maxDepth : liveConfig.maxDepth;
|
|
307
|
+
const forceRefresh = Boolean(body.forceRefresh);
|
|
308
|
+
const cacheKey = `${cwd}::${showHidden}::${showIgnored}::${maxDepth}`;
|
|
309
|
+
const now = Date.now();
|
|
310
|
+
if (!forceRefresh) {
|
|
311
|
+
const cached = scanCache.get(cacheKey);
|
|
312
|
+
if (cached && now - cached.timestamp < CACHE_TTL_MS) {
|
|
313
|
+
writeJson(res, 200, {
|
|
314
|
+
ok: true,
|
|
315
|
+
value: cached.result,
|
|
316
|
+
cached: true
|
|
317
|
+
});
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const result = await scanWorkspaceTree(cwd, {
|
|
322
|
+
showHidden,
|
|
323
|
+
showIgnored,
|
|
324
|
+
maxDepth
|
|
325
|
+
});
|
|
326
|
+
scanCache.set(cacheKey, {
|
|
327
|
+
timestamp: now,
|
|
328
|
+
result
|
|
329
|
+
});
|
|
222
330
|
writeJson(res, 200, {
|
|
223
331
|
ok: true,
|
|
224
|
-
value:
|
|
225
|
-
showHidden: typeof body.showHidden === "boolean" ? body.showHidden : liveConfig.showHidden,
|
|
226
|
-
showIgnored: Boolean(body.showIgnored),
|
|
227
|
-
maxDepth: typeof body.maxDepth === "number" && body.maxDepth > 0 ? body.maxDepth : liveConfig.maxDepth
|
|
228
|
-
})
|
|
332
|
+
value: result
|
|
229
333
|
});
|
|
230
334
|
} catch (err) {
|
|
231
335
|
writeJson(res, 500, {
|
package/package.json
CHANGED
package/src/client/icons.tsx
CHANGED
|
@@ -167,3 +167,22 @@ export function IconChevronDown({ size = 14 }: { size?: number }): JSX.Element {
|
|
|
167
167
|
)
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
export function IconExplorer({ size = 12 }: { size?: number }): JSX.Element {
|
|
171
|
+
return (
|
|
172
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="currentColor" style={{ display: 'inline-block', verticalAlign: 'middle', flexShrink: 0 }}>
|
|
173
|
+
<path d="M1.75 2A1.75 1.75 0 000 3.75v8.5C0 13.216.784 14 1.75 14h12.5A1.75 1.75 0 0016 12.25v-6.5A1.75 1.75 0 0014.25 4H7.5a.25.25 0 01-.2-.1l-.9-1.2C6.07 2.26 5.55 2 5 2H1.75zM1.5 5.5h13v6.75a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25V5.5z" />
|
|
174
|
+
</svg>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function IconExternalApp({ size = 12 }: { size?: number }): JSX.Element {
|
|
179
|
+
return (
|
|
180
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.35" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'inline-block', verticalAlign: 'middle', flexShrink: 0 }}>
|
|
181
|
+
<path d="M12.5 9.5v3.25a1.25 1.25 0 01-1.25 1.25H3.25A1.25 1.25 0 012 12.75V4.75A1.25 1.25 0 013.25 3.5H6.5" />
|
|
182
|
+
<polyline points="10 2 14 2 14 6" />
|
|
183
|
+
<line x1="6.5" y1="9.5" x2="13.5" y2="2.5" />
|
|
184
|
+
</svg>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
package/src/client/index.tsx
CHANGED
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
IconCollapseAll,
|
|
14
14
|
IconCopy,
|
|
15
15
|
IconExpandAll,
|
|
16
|
+
IconExplorer,
|
|
17
|
+
IconExternalApp,
|
|
16
18
|
IconFolder,
|
|
17
19
|
IconList,
|
|
18
20
|
IconRefresh,
|
|
@@ -22,6 +24,9 @@ import {
|
|
|
22
24
|
|
|
23
25
|
export const inject = ['slots', 'sidebarRightTabs']
|
|
24
26
|
|
|
27
|
+
/** 构建时由 tsdown define 注入,避免卡片上的版本号与 package.json 漂移。 */
|
|
28
|
+
declare const __PLUGIN_VERSION__: string
|
|
29
|
+
|
|
25
30
|
let globalCtx: Context | null = null
|
|
26
31
|
|
|
27
32
|
const OFFICIAL_FILES_TAB_ID = '@civilization/dsh-workspace-files'
|
|
@@ -113,28 +118,16 @@ export function apply(ctx: Context): void {
|
|
|
113
118
|
}, WorkspaceFilesTabTitle)), 'dsh-workspace-files: official tab title')
|
|
114
119
|
}
|
|
115
120
|
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
targetCtx.effect?.(() => targetCtx.slots.inject('settings.plugin.item', function* () {
|
|
120
|
-
yield targetCtx.slots.register({
|
|
121
|
-
name: 'settings.plugin.item',
|
|
122
|
-
key: 'workspace-files',
|
|
123
|
-
}, WorkspaceFilesSettingsCard)
|
|
124
|
-
yield targetCtx.slots.register({
|
|
125
|
-
name: 'settings.plugin.item',
|
|
126
|
-
key: '@civilization/dsh-workspace-files',
|
|
127
|
-
}, WorkspaceFilesSettingsCard)
|
|
128
|
-
}), 'dsh-workspace-files: settings card')
|
|
129
|
-
}
|
|
130
|
-
|
|
121
|
+
// 设置中心“插件”页的自定义卡片:DSH 0.2.x 用 settings.plugins.tab(list 槽)承载
|
|
122
|
+
// 各插件自己的配置视图,注册需要 id/order/label;旧版的 settings.plugin.item 槽与
|
|
123
|
+
// settingsScope 服务在 0.2.0 已不存在。
|
|
131
124
|
if (slots) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
})
|
|
125
|
+
ctx.effect(() => slots.inject('settings.plugins.tab', () => slots.register({
|
|
126
|
+
name: 'settings.plugins.tab',
|
|
127
|
+
id: 'workspace-files',
|
|
128
|
+
order: 20,
|
|
129
|
+
label: () => '工作区文件',
|
|
130
|
+
}, WorkspaceFilesSettingsCard)), 'dsh-workspace-files: settings card')
|
|
138
131
|
}
|
|
139
132
|
}
|
|
140
133
|
|
|
@@ -451,7 +444,7 @@ export function WorkspaceFilesSettingsCard(): JSX.Element {
|
|
|
451
444
|
<div className="dsh-wf-head-text">
|
|
452
445
|
<div className="dsh-wf-name">
|
|
453
446
|
<span>工作区文件</span>
|
|
454
|
-
<span className="dsh-wf-version">
|
|
447
|
+
<span className="dsh-wf-version">v{__PLUGIN_VERSION__}</span>
|
|
455
448
|
</div>
|
|
456
449
|
<div className="dsh-wf-desc">
|
|
457
450
|
浏览与检索工作区文件树,支持递归目录扫描与一键引用。
|
|
@@ -582,7 +575,7 @@ function WorkspaceFilesView({
|
|
|
582
575
|
return () => window.removeEventListener('dsh-workspace-files:config-change', handleCfgChange)
|
|
583
576
|
}, [])
|
|
584
577
|
|
|
585
|
-
const loadTree = useCallback(async (quiet = false) => {
|
|
578
|
+
const loadTree = useCallback(async (quiet = false, forceRefresh = false) => {
|
|
586
579
|
if (!quiet) setLoading(true)
|
|
587
580
|
setError(null)
|
|
588
581
|
try {
|
|
@@ -591,6 +584,7 @@ function WorkspaceFilesView({
|
|
|
591
584
|
cwd,
|
|
592
585
|
showHidden,
|
|
593
586
|
maxDepth: activeConfig.maxDepth,
|
|
587
|
+
forceRefresh,
|
|
594
588
|
})
|
|
595
589
|
setData(res)
|
|
596
590
|
|
|
@@ -725,6 +719,60 @@ function WorkspaceFilesView({
|
|
|
725
719
|
})
|
|
726
720
|
}
|
|
727
721
|
|
|
722
|
+
// 在文件资源管理器中定位/打开
|
|
723
|
+
const handleRevealInExplorer = (e: React.MouseEvent, item: FileItem) => {
|
|
724
|
+
e.stopPropagation()
|
|
725
|
+
fetch('/dsh-workspace-files/api/files.open', {
|
|
726
|
+
method: 'POST',
|
|
727
|
+
headers: { 'content-type': 'application/json' },
|
|
728
|
+
body: JSON.stringify({
|
|
729
|
+
action: 'reveal',
|
|
730
|
+
path: item.path,
|
|
731
|
+
isDirectory: Boolean(item.isDirectory),
|
|
732
|
+
sessionId,
|
|
733
|
+
cwd,
|
|
734
|
+
}),
|
|
735
|
+
})
|
|
736
|
+
.then(res => res.json())
|
|
737
|
+
.then(data => {
|
|
738
|
+
if (data.ok) {
|
|
739
|
+
showToastMsg(`已在资源管理器中显示: ${item.name}`)
|
|
740
|
+
} else {
|
|
741
|
+
showToastMsg(`打开失败: ${data.error?.message || '未知错误'}`)
|
|
742
|
+
}
|
|
743
|
+
})
|
|
744
|
+
.catch(err => {
|
|
745
|
+
showToastMsg(`请求失败: ${err?.message || String(err)}`)
|
|
746
|
+
})
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// 用系统默认应用打开
|
|
750
|
+
const handleOpenDefaultApp = (e: React.MouseEvent, item: FileItem) => {
|
|
751
|
+
e.stopPropagation()
|
|
752
|
+
fetch('/dsh-workspace-files/api/files.open', {
|
|
753
|
+
method: 'POST',
|
|
754
|
+
headers: { 'content-type': 'application/json' },
|
|
755
|
+
body: JSON.stringify({
|
|
756
|
+
action: 'openDefault',
|
|
757
|
+
path: item.path,
|
|
758
|
+
isDirectory: Boolean(item.isDirectory),
|
|
759
|
+
sessionId,
|
|
760
|
+
cwd,
|
|
761
|
+
}),
|
|
762
|
+
})
|
|
763
|
+
.then(res => res.json())
|
|
764
|
+
.then(data => {
|
|
765
|
+
if (data.ok) {
|
|
766
|
+
showToastMsg(`已启动系统应用打开: ${item.name}`)
|
|
767
|
+
} else {
|
|
768
|
+
showToastMsg(`打开失败: ${data.error?.message || '未知错误'}`)
|
|
769
|
+
}
|
|
770
|
+
})
|
|
771
|
+
.catch(err => {
|
|
772
|
+
showToastMsg(`请求失败: ${err?.message || String(err)}`)
|
|
773
|
+
})
|
|
774
|
+
}
|
|
775
|
+
|
|
728
776
|
const [searchViewMode, setSearchViewMode] = useState<'flat' | 'tree'>('flat')
|
|
729
777
|
const [searchExpandedPaths, setSearchExpandedPaths] = useState<Set<string>>(() => new Set())
|
|
730
778
|
|
|
@@ -885,7 +933,7 @@ function WorkspaceFilesView({
|
|
|
885
933
|
<button
|
|
886
934
|
type="button"
|
|
887
935
|
className="dsh-files-tool-btn"
|
|
888
|
-
onClick={() => void loadTree(true)}
|
|
936
|
+
onClick={() => void loadTree(false, true)}
|
|
889
937
|
disabled={loading}
|
|
890
938
|
title="刷新文件树"
|
|
891
939
|
>
|
|
@@ -927,7 +975,10 @@ function WorkspaceFilesView({
|
|
|
927
975
|
/* ── 扁平搜索结果列表 ── */
|
|
928
976
|
<div className="dsh-files-search-container">
|
|
929
977
|
<div className="dsh-files-search-header">
|
|
930
|
-
<span className="dsh-files-search-count"
|
|
978
|
+
<span className="dsh-files-search-count">
|
|
979
|
+
找到 {flatSearchResults.length} 个匹配项
|
|
980
|
+
{flatSearchResults.length > 150 ? '(展示前 150 项)' : ''}
|
|
981
|
+
</span>
|
|
931
982
|
<button
|
|
932
983
|
type="button"
|
|
933
984
|
className="dsh-files-view-toggle-btn"
|
|
@@ -939,7 +990,7 @@ function WorkspaceFilesView({
|
|
|
939
990
|
</button>
|
|
940
991
|
</div>
|
|
941
992
|
<div className="dsh-files-flat-list">
|
|
942
|
-
{flatSearchResults.map(({ item, parentDir }) => (
|
|
993
|
+
{flatSearchResults.slice(0, 150).map(({ item, parentDir }) => (
|
|
943
994
|
<div
|
|
944
995
|
key={item.path}
|
|
945
996
|
className={`dsh-files-flat-row ${item.isDirectory ? 'is-directory' : ''}`}
|
|
@@ -984,6 +1035,22 @@ function WorkspaceFilesView({
|
|
|
984
1035
|
<IconAt size={13} />
|
|
985
1036
|
<span className="dsh-files-action-at-text">@引用</span>
|
|
986
1037
|
</button>
|
|
1038
|
+
<button
|
|
1039
|
+
type="button"
|
|
1040
|
+
className="dsh-files-action-btn"
|
|
1041
|
+
onClick={e => handleRevealInExplorer(e, item)}
|
|
1042
|
+
title={`在文件资源管理器中显示此${item.isDirectory ? '目录' : '文件'}`}
|
|
1043
|
+
>
|
|
1044
|
+
<IconExplorer size={12} />
|
|
1045
|
+
</button>
|
|
1046
|
+
<button
|
|
1047
|
+
type="button"
|
|
1048
|
+
className="dsh-files-action-btn"
|
|
1049
|
+
onClick={e => handleOpenDefaultApp(e, item)}
|
|
1050
|
+
title={`用系统默认应用打开此${item.isDirectory ? '目录' : '文件'}`}
|
|
1051
|
+
>
|
|
1052
|
+
<IconExternalApp size={12} />
|
|
1053
|
+
</button>
|
|
987
1054
|
<button
|
|
988
1055
|
type="button"
|
|
989
1056
|
className="dsh-files-action-btn"
|
|
@@ -995,6 +1062,11 @@ function WorkspaceFilesView({
|
|
|
995
1062
|
</div>
|
|
996
1063
|
</div>
|
|
997
1064
|
))}
|
|
1065
|
+
{flatSearchResults.length > 150 && (
|
|
1066
|
+
<div style={{ padding: '10px 14px', fontSize: 11, textAlign: 'center', color: 'var(--dsw-alias-label-tertiary, #8b93a1)', borderTop: '1px dashed var(--dsw-alias-border-l2, #e5e7eb)' }}>
|
|
1067
|
+
匹配项较多,已限制展示前 150 项以保证流畅度,请输入更详细关键字
|
|
1068
|
+
</div>
|
|
1069
|
+
)}
|
|
998
1070
|
</div>
|
|
999
1071
|
</div>
|
|
1000
1072
|
) : (
|
|
@@ -1037,6 +1109,8 @@ function WorkspaceFilesView({
|
|
|
1037
1109
|
onOpenFile={onOpenFile}
|
|
1038
1110
|
onMention={handleMention}
|
|
1039
1111
|
onCopyPath={handleCopyPath}
|
|
1112
|
+
onRevealInExplorer={handleRevealInExplorer}
|
|
1113
|
+
onOpenDefaultApp={handleOpenDefaultApp}
|
|
1040
1114
|
/>
|
|
1041
1115
|
))}
|
|
1042
1116
|
</div>
|
|
@@ -1065,8 +1139,22 @@ function FileTreeNode(props: {
|
|
|
1065
1139
|
onOpenFile: (path: string) => void
|
|
1066
1140
|
onMention: (e: React.MouseEvent, item: FileItem) => void
|
|
1067
1141
|
onCopyPath: (e: React.MouseEvent, path: string) => void
|
|
1142
|
+
onRevealInExplorer: (e: React.MouseEvent, item: FileItem) => void
|
|
1143
|
+
onOpenDefaultApp: (e: React.MouseEvent, item: FileItem) => void
|
|
1068
1144
|
}): JSX.Element {
|
|
1069
|
-
const {
|
|
1145
|
+
const {
|
|
1146
|
+
item,
|
|
1147
|
+
depth,
|
|
1148
|
+
query,
|
|
1149
|
+
isSearchMode,
|
|
1150
|
+
expandedPaths,
|
|
1151
|
+
onToggleExpand,
|
|
1152
|
+
onOpenFile,
|
|
1153
|
+
onMention,
|
|
1154
|
+
onCopyPath,
|
|
1155
|
+
onRevealInExplorer,
|
|
1156
|
+
onOpenDefaultApp,
|
|
1157
|
+
} = props
|
|
1070
1158
|
const isExpanded = expandedPaths.has(item.path)
|
|
1071
1159
|
|
|
1072
1160
|
if (item.isDirectory) {
|
|
@@ -1098,6 +1186,22 @@ function FileTreeNode(props: {
|
|
|
1098
1186
|
<IconAt size={13} />
|
|
1099
1187
|
<span className="dsh-files-action-at-text">@引用</span>
|
|
1100
1188
|
</button>
|
|
1189
|
+
<button
|
|
1190
|
+
type="button"
|
|
1191
|
+
className="dsh-files-action-btn"
|
|
1192
|
+
onClick={e => onRevealInExplorer(e, item)}
|
|
1193
|
+
title="在文件资源管理器中打开此目录"
|
|
1194
|
+
>
|
|
1195
|
+
<IconExplorer size={12} />
|
|
1196
|
+
</button>
|
|
1197
|
+
<button
|
|
1198
|
+
type="button"
|
|
1199
|
+
className="dsh-files-action-btn"
|
|
1200
|
+
onClick={e => onOpenDefaultApp(e, item)}
|
|
1201
|
+
title="用系统默认应用打开此目录"
|
|
1202
|
+
>
|
|
1203
|
+
<IconExternalApp size={12} />
|
|
1204
|
+
</button>
|
|
1101
1205
|
<button
|
|
1102
1206
|
type="button"
|
|
1103
1207
|
className="dsh-files-action-btn"
|
|
@@ -1122,6 +1226,8 @@ function FileTreeNode(props: {
|
|
|
1122
1226
|
onOpenFile={onOpenFile}
|
|
1123
1227
|
onMention={onMention}
|
|
1124
1228
|
onCopyPath={onCopyPath}
|
|
1229
|
+
onRevealInExplorer={onRevealInExplorer}
|
|
1230
|
+
onOpenDefaultApp={onOpenDefaultApp}
|
|
1125
1231
|
/>
|
|
1126
1232
|
))}
|
|
1127
1233
|
</div>
|
|
@@ -1161,6 +1267,22 @@ function FileTreeNode(props: {
|
|
|
1161
1267
|
<IconAt size={13} />
|
|
1162
1268
|
<span className="dsh-files-action-at-text">@引用</span>
|
|
1163
1269
|
</button>
|
|
1270
|
+
<button
|
|
1271
|
+
type="button"
|
|
1272
|
+
className="dsh-files-action-btn"
|
|
1273
|
+
onClick={e => onRevealInExplorer(e, item)}
|
|
1274
|
+
title="在文件资源管理器中显示此文件"
|
|
1275
|
+
>
|
|
1276
|
+
<IconExplorer size={12} />
|
|
1277
|
+
</button>
|
|
1278
|
+
<button
|
|
1279
|
+
type="button"
|
|
1280
|
+
className="dsh-files-action-btn"
|
|
1281
|
+
onClick={e => onOpenDefaultApp(e, item)}
|
|
1282
|
+
title="用系统默认应用打开此文件"
|
|
1283
|
+
>
|
|
1284
|
+
<IconExternalApp size={12} />
|
|
1285
|
+
</button>
|
|
1164
1286
|
<button
|
|
1165
1287
|
type="button"
|
|
1166
1288
|
className="dsh-files-action-btn"
|