@doubleelec/dsh-workspace-explorer 0.7.1-fork.4

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/index.js ADDED
@@ -0,0 +1,385 @@
1
+ import { open, readFile, readdir, stat, writeFile } from "node:fs/promises";
2
+ import { basename, join } from "node:path";
3
+ /** @internal 运行期配置(供单元测试调整),不构成公开 API。 */
4
+ const cfg = {
5
+ ignore: [...[
6
+ ".git",
7
+ "node_modules",
8
+ "__pycache__",
9
+ ".venv",
10
+ "venv",
11
+ ".pytest_cache",
12
+ ".ruff_cache",
13
+ ".mypy_cache",
14
+ "dist",
15
+ "build",
16
+ ".next",
17
+ ".nuxt",
18
+ "coverage",
19
+ ".idea",
20
+ "target"
21
+ ]],
22
+ max: 400,
23
+ peekMaxLines: 60
24
+ };
25
+ const WHOLE_MAX_BYTES = 32768;
26
+ const SMALL_FILE_MAX = 4194304;
27
+ const PAGE_SCAN_CHUNK = 262144;
28
+ const TREE_MAX_DEPTH = 10;
29
+ const TREE_MAX_ENTRIES = 5e3;
30
+ const MAX_CACHED_LINES = 2e6;
31
+ const LINE_CACHE_MAX_FILES = 64;
32
+ /** @internal 大文件行起始字节缓存(key=绝对路径),供单元测试断言,不构成公开 API。 */
33
+ const lineIndexCache = /* @__PURE__ */ new Map();
34
+ async function readJsonBody(req) {
35
+ let raw = "";
36
+ for await (const chunk of req) raw += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf-8");
37
+ try {
38
+ const parsed = JSON.parse(raw);
39
+ return parsed !== null && typeof parsed === "object" ? parsed : {};
40
+ } catch {
41
+ return {};
42
+ }
43
+ }
44
+ function writeJson(res, value, status = 200) {
45
+ res.writeHead(status, {
46
+ "content-type": "application/json; charset=utf-8",
47
+ "cache-control": "no-cache"
48
+ });
49
+ res.end(JSON.stringify(value));
50
+ }
51
+ /** @internal 把「工作区根目录 + 相对路径」解析为绝对路径,并校验 rel 不含危险段。 */
52
+ function resolveRel(root, rel) {
53
+ if (root === "") return { error: "missing-root" };
54
+ if (rel !== "") {
55
+ if (rel.split("/").some((s) => s === "" || s === "." || s === "..")) return { error: "bad-rel" };
56
+ }
57
+ return { abs: rel === "" ? root : root.replace(/\/+$/, "") + "/" + rel };
58
+ }
59
+ /** @internal 列一个目录层级(目录优先、按名排序、噪声目录过滤、400 上限)。 */
60
+ async function listDir(abs, baseRel) {
61
+ const dirents = await readdir(abs, { withFileTypes: true });
62
+ const out = [];
63
+ for (const d of dirents) {
64
+ if (d.name === ".DS_Store") continue;
65
+ if (d.isDirectory() && cfg.ignore.includes(d.name)) continue;
66
+ const target = join(abs, d.name);
67
+ let size = null;
68
+ if (d.isFile()) try {
69
+ size = (await stat(target)).size;
70
+ } catch {}
71
+ out.push({
72
+ name: d.name,
73
+ type: d.isDirectory() ? "directory" : "file",
74
+ path: target,
75
+ rel: baseRel === "" ? d.name : baseRel + "/" + d.name,
76
+ size
77
+ });
78
+ }
79
+ out.sort((a, b) => a.type !== b.type ? a.type === "directory" ? -1 : 1 : a.name.localeCompare(b.name));
80
+ const truncated = out.length > cfg.max;
81
+ return {
82
+ entries: truncated ? out.slice(0, cfg.max) : out,
83
+ truncated
84
+ };
85
+ }
86
+ /**
87
+ * 按行读取 [offset, offset+limit) 一页内容。
88
+ * 小文件(≤4MB)整读、行数精确;大文件块扫描定位行区间,行数未知(null)。
89
+ */
90
+ /** @internal 按行读取一页内容。 */
91
+ async function readLinesPage(abs, offset, limit) {
92
+ const size = (await stat(abs)).size;
93
+ if (size <= SMALL_FILE_MAX) {
94
+ const lines = (await readFile(abs)).toString("utf-8").split("\n");
95
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
96
+ const page = lines.slice(offset, offset + limit);
97
+ return {
98
+ content: page.join("\n"),
99
+ startLine: offset,
100
+ lineCount: lines.length,
101
+ hasMore: offset + page.length < lines.length
102
+ };
103
+ }
104
+ return pageScanLarge(abs, size, offset, limit);
105
+ }
106
+ /** @internal 大文件分页:增量缓存每行起始字节,翻页复用已扫描结果,避免每页从头重扫 O(n)。 */
107
+ async function pageScanLarge(abs, size, offset, limit) {
108
+ const target = offset + limit;
109
+ const fh = await open(abs, "r");
110
+ try {
111
+ if (target > MAX_CACHED_LINES) {
112
+ let pos = 0;
113
+ let newlines = 0;
114
+ let startByte = offset === 0 ? 0 : -1;
115
+ let endByte = -1;
116
+ while (pos < size && endByte === -1) {
117
+ const want = Math.min(PAGE_SCAN_CHUNK, size - pos);
118
+ const chunk = Buffer.allocUnsafe(want);
119
+ await fh.read(chunk, 0, want, pos);
120
+ let idx = chunk.indexOf(10);
121
+ while (idx !== -1) {
122
+ newlines++;
123
+ if (newlines === offset) startByte = pos + idx + 1;
124
+ if (newlines === target) {
125
+ endByte = pos + idx + 1;
126
+ break;
127
+ }
128
+ idx = chunk.indexOf(10, idx + 1);
129
+ }
130
+ pos += want;
131
+ }
132
+ if (startByte === -1) startByte = size;
133
+ if (endByte === -1) endByte = size;
134
+ const len = endByte - startByte;
135
+ const buf = len > 0 ? Buffer.allocUnsafe(len) : Buffer.alloc(0);
136
+ if (len > 0) await fh.read(buf, 0, len, startByte);
137
+ return {
138
+ content: buf.toString("utf-8").replace(/\n$/, ""),
139
+ startLine: offset,
140
+ lineCount: null,
141
+ hasMore: endByte < size
142
+ };
143
+ }
144
+ let cached = lineIndexCache.get(abs);
145
+ if (!cached || cached.size !== size) {
146
+ if (lineIndexCache.size >= LINE_CACHE_MAX_FILES) lineIndexCache.clear();
147
+ cached = {
148
+ offsets: [0],
149
+ scannedBytes: 0,
150
+ size
151
+ };
152
+ lineIndexCache.set(abs, cached);
153
+ }
154
+ while (cached.offsets.length <= target && cached.scannedBytes < size) {
155
+ const pos = cached.scannedBytes;
156
+ const want = Math.min(PAGE_SCAN_CHUNK, size - pos);
157
+ const chunk = Buffer.allocUnsafe(want);
158
+ await fh.read(chunk, 0, want, pos);
159
+ let idx = chunk.indexOf(10);
160
+ while (idx !== -1) {
161
+ cached.offsets.push(pos + idx + 1);
162
+ idx = chunk.indexOf(10, idx + 1);
163
+ }
164
+ cached.scannedBytes = pos + want;
165
+ }
166
+ const startByte = offset < cached.offsets.length ? cached.offsets[offset] : size;
167
+ const endByte = target < cached.offsets.length ? cached.offsets[target] : size;
168
+ const len = endByte - startByte;
169
+ const buf = len > 0 ? Buffer.allocUnsafe(len) : Buffer.alloc(0);
170
+ if (len > 0) await fh.read(buf, 0, len, startByte);
171
+ const fullyScanned = cached.scannedBytes >= size;
172
+ const lastOffset = cached.offsets[cached.offsets.length - 1];
173
+ const lineCount = fullyScanned ? cached.offsets.length - (lastOffset === size ? 1 : 0) : null;
174
+ return {
175
+ content: buf.toString("utf-8").replace(/\n$/, ""),
176
+ startLine: offset,
177
+ lineCount,
178
+ hasMore: endByte < size
179
+ };
180
+ } finally {
181
+ await fh.close();
182
+ }
183
+ }
184
+ /** @internal 嗅探是否二进制(NUL 字节),只读前 8KB。 */
185
+ async function sniffBinary(abs, size) {
186
+ const probe = Buffer.alloc(Math.min(8192, size));
187
+ if (probe.length === 0) return false;
188
+ const fh = await open(abs, "r");
189
+ try {
190
+ await fh.read(probe, 0, probe.length, 0);
191
+ } finally {
192
+ await fh.close();
193
+ }
194
+ return probe.includes(0);
195
+ }
196
+ /** @internal 递归收集目录树节点(树根相对 rel 从 '' 开始;受深度/条目预算限制)。 */
197
+ async function buildTreeNodes(abs, rel, depth, budget, out) {
198
+ if (depth < 0 || budget.remaining <= 0) return;
199
+ const { entries } = await listDir(abs, rel);
200
+ for (const e of entries) {
201
+ if (budget.remaining <= 0) break;
202
+ out.push({
203
+ name: e.name,
204
+ type: e.type,
205
+ rel: e.rel
206
+ });
207
+ budget.remaining--;
208
+ if (e.type === "directory") await buildTreeNodes(e.path, e.rel, depth - 1, budget, out);
209
+ }
210
+ }
211
+ var src_default = {
212
+ inject: ["webServer"],
213
+ apply(ctx) {
214
+ const routes = [
215
+ {
216
+ kind: "exact",
217
+ path: "/dsh-we/api/config",
218
+ handler: async (req, res) => {
219
+ const body = await readJsonBody(req);
220
+ if (Array.isArray(body.ignore)) cfg.ignore = body.ignore.map((s) => String(s)).filter((s) => s !== "");
221
+ if (typeof body.max === "number" && body.max >= 1 && body.max <= 2e3) cfg.max = Math.floor(body.max);
222
+ if (typeof body.peekMaxLines === "number" && body.peekMaxLines >= 10 && body.peekMaxLines <= 500) cfg.peekMaxLines = Math.floor(body.peekMaxLines);
223
+ return writeJson(res, {
224
+ ok: true,
225
+ ignore: cfg.ignore,
226
+ max: cfg.max,
227
+ peekMaxLines: cfg.peekMaxLines
228
+ });
229
+ }
230
+ },
231
+ {
232
+ kind: "exact",
233
+ path: "/dsh-we/api/list",
234
+ handler: async (req, res) => {
235
+ const body = await readJsonBody(req);
236
+ const rel = String(body.rel ?? "");
237
+ const resolved = resolveRel(String(body.root ?? body.path ?? ""), rel);
238
+ if ("error" in resolved) return writeJson(res, {
239
+ ok: false,
240
+ error: resolved.error
241
+ });
242
+ const abs = resolved.abs;
243
+ try {
244
+ const { entries, truncated } = await listDir(abs, rel);
245
+ return writeJson(res, {
246
+ ok: true,
247
+ path: abs,
248
+ rel,
249
+ entries,
250
+ truncated
251
+ });
252
+ } catch (err) {
253
+ return writeJson(res, {
254
+ ok: false,
255
+ error: err instanceof Error ? err.message : String(err)
256
+ });
257
+ }
258
+ }
259
+ },
260
+ {
261
+ kind: "exact",
262
+ path: "/dsh-we/api/peek",
263
+ handler: async (req, res) => {
264
+ const body = await readJsonBody(req);
265
+ const resolved = resolveRel(String(body.root ?? ""), String(body.rel ?? ""));
266
+ if ("error" in resolved) return writeJson(res, {
267
+ ok: false,
268
+ error: resolved.error
269
+ });
270
+ const path = resolved.abs;
271
+ try {
272
+ const size = (await stat(path)).size;
273
+ if (await sniffBinary(path, size)) return writeJson(res, {
274
+ ok: true,
275
+ binary: true,
276
+ size,
277
+ lineCount: null,
278
+ startLine: 0,
279
+ content: "",
280
+ hasMore: false
281
+ });
282
+ if (body.whole === true && size <= WHOLE_MAX_BYTES) {
283
+ const lines = (await readFile(path)).toString("utf-8").split("\n");
284
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
285
+ return writeJson(res, {
286
+ ok: true,
287
+ binary: false,
288
+ size,
289
+ lineCount: lines.length,
290
+ startLine: 0,
291
+ content: lines.join("\n"),
292
+ hasMore: false
293
+ });
294
+ }
295
+ return writeJson(res, {
296
+ ok: true,
297
+ binary: false,
298
+ size,
299
+ ...await readLinesPage(path, Math.max(0, Math.floor(Number(body.offset) || 0)), Math.min(2e3, Math.max(1, Math.floor(Number(body.limit) || cfg.peekMaxLines))))
300
+ });
301
+ } catch (err) {
302
+ return writeJson(res, {
303
+ ok: false,
304
+ error: err instanceof Error ? err.message : String(err)
305
+ });
306
+ }
307
+ }
308
+ },
309
+ {
310
+ kind: "exact",
311
+ path: "/dsh-we/api/tree",
312
+ handler: async (req, res) => {
313
+ const body = await readJsonBody(req);
314
+ const resolved = resolveRel(String(body.root ?? ""), String(body.rel ?? ""));
315
+ if ("error" in resolved) return writeJson(res, {
316
+ ok: false,
317
+ error: resolved.error
318
+ });
319
+ const path = resolved.abs;
320
+ const depth = Math.min(TREE_MAX_DEPTH, Math.max(1, Math.floor(Number(body.depth) || 3)));
321
+ const maxEntries = Math.min(TREE_MAX_ENTRIES, Math.max(1, Math.floor(Number(body.maxEntries) || 200)));
322
+ try {
323
+ const name = basename(path.replace(/\/+$/, "")) || basename(path);
324
+ const entries = [];
325
+ const budget = { remaining: maxEntries };
326
+ await buildTreeNodes(path, "", depth, budget, entries);
327
+ return writeJson(res, {
328
+ ok: true,
329
+ name,
330
+ entries,
331
+ entryCount: entries.length,
332
+ truncated: budget.remaining <= 0
333
+ });
334
+ } catch (err) {
335
+ return writeJson(res, {
336
+ ok: false,
337
+ error: err instanceof Error ? err.message : String(err)
338
+ });
339
+ }
340
+ }
341
+ },
342
+ {
343
+ kind: "exact",
344
+ path: "/dsh-we/api/write",
345
+ handler: async (req, res) => {
346
+ const body = await readJsonBody(req);
347
+ const resolved = resolveRel(String(body.root ?? ""), String(body.rel ?? ""));
348
+ if ("error" in resolved) return writeJson(res, {
349
+ ok: false,
350
+ error: resolved.error
351
+ });
352
+ const abs = resolved.abs;
353
+ const content = typeof body.content === "string" ? body.content : null;
354
+ if (content === null) return writeJson(res, {
355
+ ok: false,
356
+ error: "missing-content"
357
+ });
358
+ try {
359
+ if (typeof body.expectedSize === "number") try {
360
+ const info = await stat(abs);
361
+ if (info.size !== body.expectedSize) return writeJson(res, {
362
+ ok: false,
363
+ error: "file-changed",
364
+ currentSize: info.size
365
+ });
366
+ } catch {}
367
+ await writeFile(abs, content, "utf-8");
368
+ return writeJson(res, {
369
+ ok: true,
370
+ size: (await stat(abs)).size
371
+ });
372
+ } catch (err) {
373
+ return writeJson(res, {
374
+ ok: false,
375
+ error: err instanceof Error ? err.message : String(err)
376
+ });
377
+ }
378
+ }
379
+ }
380
+ ];
381
+ for (const route of routes) ctx.webServer.register(route);
382
+ }
383
+ };
384
+ //#endregion
385
+ export { buildTreeNodes, cfg, src_default as default, lineIndexCache, listDir, pageScanLarge, readLinesPage, resolveRel, sniffBinary };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 纯格式化工具(浏览器端,无 DOM / 无副作用),供面板与单元测试复用。
3
+ */
4
+ /** 人类可读文件大小(空值返回空串)。 */
5
+ export declare const fmtSize: (n: number | null | undefined) => string;
6
+ /** 浏览器安全的 basename(兼容正斜杠结尾;DSH 内不依赖 node:path)。 */
7
+ export declare const basename: (p: string) => string;
8
+ /** 取小写扩展名;点开头(隐藏文件)或无扩展名返回空串。 */
9
+ export declare const extOf: (name: string) => string;
10
+ /** 把 /dsh-we/api/tree 的平铺条目渲染成带缩进与树形连线的文本块(目录拖拽 / 多选批量插入共用)。 */
11
+ export declare function formatTreeBlock(name: string, entries: Array<{
12
+ rel: string;
13
+ type: string;
14
+ name: string;
15
+ }>, truncated: boolean): string;
@@ -0,0 +1,8 @@
1
+ interface CtxLike {
2
+ get(name: string): unknown;
3
+ effect(fn: () => () => void): void;
4
+ inject(deps: string[], callback: (scope: unknown) => unknown): void;
5
+ }
6
+ export declare const inject: string[];
7
+ export declare function apply(ctx: CtxLike): void;
8
+ export {};
@@ -0,0 +1,66 @@
1
+ /**
2
+ * 轻量 Markdown 渲染器(浏览器端,无 DOM / 无副作用)。
3
+ *
4
+ * 只覆盖预览场景的常用子集:标题 / 加粗 / 斜体 / 删除线 / 行内码 /
5
+ * 代码块(围栏```/缩进4空格) / 引用 / 有序+无序列表(含任务列表) /
6
+ * 分隔线 / 链接(纯文本展示,不生成 <a> 以免 file:// 外跳) / 表格(简单行)。
7
+ * 输出 React 元素描述(不直接依赖 react,调用方用 h 函数还原),
8
+ * 因此天然免疫 XSS——不做任何 innerHTML 拼接。
9
+ */
10
+ export type MdNode = {
11
+ t: 'h';
12
+ level: 1 | 2 | 3 | 4 | 5 | 6;
13
+ inline: MdInline[];
14
+ } | {
15
+ t: 'p';
16
+ inline: MdInline[];
17
+ } | {
18
+ t: 'code';
19
+ lang: string;
20
+ text: string;
21
+ } | {
22
+ t: 'quote';
23
+ children: MdNode[];
24
+ } | {
25
+ t: 'ul';
26
+ items: MdInline[][];
27
+ } | {
28
+ t: 'ol';
29
+ start: number;
30
+ items: MdInline[][];
31
+ } | {
32
+ t: 'task';
33
+ checked: boolean[];
34
+ items: MdInline[][];
35
+ } | {
36
+ t: 'hr';
37
+ } | {
38
+ t: 'table';
39
+ head: MdInline[][];
40
+ rows: MdInline[][][];
41
+ };
42
+ export type MdInline = {
43
+ t: 'text';
44
+ text: string;
45
+ } | {
46
+ t: 'b';
47
+ children: MdInline[];
48
+ } | {
49
+ t: 'i';
50
+ children: MdInline[];
51
+ } | {
52
+ t: 's';
53
+ children: MdInline[];
54
+ } | {
55
+ t: 'code';
56
+ text: string;
57
+ } | {
58
+ t: 'link';
59
+ text: string;
60
+ };
61
+ /** 解析行内格式,返回 inline 节点数组。 */
62
+ export declare function parseInline(src: string): MdInline[];
63
+ /** 解析整篇 Markdown 为块节点数组。 */
64
+ export declare function parseMarkdown(src: string): MdNode[];
65
+ /** 是否 Markdown 文件(按扩展名)。 */
66
+ export declare function isMarkdownFile(name: string): boolean;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * 弹窗高度纯函数(无 DOM / 无副作用,可单元测试)。
3
+ *
4
+ * 背景:弹窗高度 = 输入框顶部 − 会话头部底部,输入框多行变高、手机键盘弹起、
5
+ * 会话切换后旧监听没更新都会让它忽高忽矮;旧下限 200 被头部+搜索框吃完就只剩几行。
6
+ * 对策:自动测量保底 320 + 底部拖拽条手动覆盖(存 localStorage,双击恢复自动)。
7
+ */
8
+ /** 自动/手动高度下限:保底能看清 ~10 行文件。 */
9
+ export declare const POPUP_MIN_H = 320;
10
+ /** 手动高度 localStorage key。 */
11
+ export declare const POPUP_MANUAL_KEY = "dshwe.popupH.v1";
12
+ /** 手动宽度下限:再窄目录树没法看。 */
13
+ export declare const POPUP_MIN_W = 280;
14
+ /** 手动宽度 localStorage key。 */
15
+ export declare const POPUP_MANUAL_W_KEY = "dshwe.popupW.v1";
16
+ /**
17
+ * 自动高度数学(measurePopup 的 DOM-free 部分)。
18
+ * @param top - 弹窗顶部(会话 header 底部 + 8)。
19
+ * @param bottomLimit - 底部上限(输入框顶部 − 8,无输入框时 vh − 48)。
20
+ * @param vh - 视口高度(移动端取 min(innerHeight, visualViewport.height))。
21
+ * @returns 钳制到 [minH, vh − top − 16] 的高度;空间不足保底 minH(允许轻微盖住输入框)。
22
+ */
23
+ export declare function autoPopupHeight(top: number, bottomLimit: number, vh: number, minH?: number): number;
24
+ /**
25
+ * 手动高度钳制(拖拽中 / 渲染时用,顺手取整)。
26
+ */
27
+ export declare function clampPopupHeight(h: number, top: number, vh: number, minH?: number): number;
28
+ /**
29
+ * 读手动高度:无存储 / 非法值 / 无 window(node 单测)一律返回 null(回落自动高度)。
30
+ */
31
+ export declare function loadManualHeight(key?: string): number | null;
32
+ /**
33
+ * 写手动高度:null 清除(恢复自动);存储不可用时静默忽略,自动高度兜底。
34
+ */
35
+ export declare function saveManualHeight(h: number | null, key?: string): void;
36
+ /**
37
+ * 手动宽度钳制(拖拽中 / 渲染时用,顺手取整)。
38
+ * @param w - 拖拽目标宽度;左边缘左拉变宽(增量为负),右推变窄。
39
+ * @param vw - 视口宽度;最大留 16px 边距,手机上自动收窄不挤出屏幕。
40
+ */
41
+ export declare function clampPopupWidth(w: number, vw: number, minW?: number): number;
42
+ /**
43
+ * 读手动宽度:无存储 / 非法值 / 无 window(node 单测)一律返回 null(回落设置页宽度)。
44
+ */
45
+ export declare function loadManualWidth(key?: string): number | null;
46
+ /**
47
+ * 写手动宽度:null 清除(恢复设置页宽度);存储不可用时静默忽略。
48
+ */
49
+ export declare function saveManualWidth(w: number | null, key?: string): void;
@@ -0,0 +1,39 @@
1
+ import type { Context } from 'cordis';
2
+ /** 请求面(结构子集:URL/method/headers + 异步 body 迭代)。 */
3
+ export interface WsHttpRequest {
4
+ url?: string;
5
+ method?: string;
6
+ headers: Record<string, string | string[] | undefined>;
7
+ [Symbol.asyncIterator](): AsyncIterator<string | Uint8Array>;
8
+ }
9
+ /** 响应面(结构子集:status/header/body 写)。 */
10
+ export interface WsHttpResponse {
11
+ statusCode: number;
12
+ writeHead(status: number, headers?: Record<string, string>): void;
13
+ end(body?: string | Uint8Array): void;
14
+ }
15
+ /** 一行目录条目。 */
16
+ export interface WsEntry {
17
+ name: string;
18
+ type: 'directory' | 'file';
19
+ path: string;
20
+ rel: string;
21
+ size: number | null;
22
+ }
23
+ /** cordis Context 增强:webServer 路由注册面(镜像 @deepseek-ai/dsh-host-webserver 的 WebRoute)。 */
24
+ declare module 'cordis' {
25
+ interface Context {
26
+ webServer: {
27
+ register(route: {
28
+ kind: 'exact' | 'prefix';
29
+ path: string;
30
+ handler: (req: WsHttpRequest, res: WsHttpResponse) => void | Promise<void>;
31
+ }): () => void;
32
+ };
33
+ }
34
+ }
35
+ declare const _default: {
36
+ inject: string[];
37
+ apply(ctx: Context): void;
38
+ };
39
+ export default _default;
package/manifest.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "id": "elec-workspace-explorer",
3
+ "name": "工作区文件面板 (Workspace Explorer)",
4
+ "version": "0.7.1-fork.4",
5
+ "description": "右侧弹出面板展示当前工作区目录文件树,顶部 Tab 切换文件/设置,点击或拖拽文件引用到聊天输入框发送给大模型",
6
+ "kind": "dynamic-cordis-plugin",
7
+ "platforms": {
8
+ "host": "src/index.ts",
9
+ "client": "src/client/index.tsx"
10
+ },
11
+ "host": {
12
+ "handlers": [
13
+ "ws-tree.list",
14
+ "ws-tree.peek",
15
+ "ws-tree.tree"
16
+ ],
17
+ "services": [
18
+ "fs"
19
+ ]
20
+ },
21
+ "client": {
22
+ "slots": [
23
+ "conversation.session.header.utilities",
24
+ "sidebar.footer.action",
25
+ "shell.overlay",
26
+ "conversation.input.dock",
27
+ "settings.section"
28
+ ],
29
+ "services": [
30
+ "slots",
31
+ "layout",
32
+ "workspaces"
33
+ ]
34
+ },
35
+ "license": "MIT"
36
+ }