@dsh-xhl/dsh-file-explorer 0.1.2 → 0.1.5
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 +7 -7
- package/lib/client.js +229 -237
- package/lib/index.js +0 -135
- package/lib/types/context-types.d.ts +29 -0
- package/package.json +113 -113
package/README.md
CHANGED
|
@@ -4,27 +4,27 @@
|
|
|
4
4
|
|
|
5
5
|
## 功能
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
- 选择记忆在 localStorage,点击后底部提示确认打开的路径;下拉选中某项会先切换默认打开方式,随后立即执行对应的打开动作
|
|
7
|
+
- **标题栏图标**(`conversation.session.header.utilities`):一个内置编辑器图标,点击打开文件预览 / 编辑弹窗
|
|
8
|
+
- **Hero / 新建会话同样显示**:同一图标通过框架通用浮层 `shell.overlay` 由插件自身定位到会话列右上角(hero 阶段显示,有记录后由标题栏图标接管,无重复);纯插件实现,不改动框架代码(详见 `docs/hero-filex-button.md`)
|
|
9
|
+
- **对话内点击文件 → 编辑器预览**:聊天里的文件路径链接(工具行、产物行、正文提及)默认用系统程序打开;插件拦截 `remote.session.openWorkspacePath` 与 `workspaces.openPath` 两条通道,改为在编辑器弹窗中预览(目录或 cwd 外的路径仍落回系统打开)
|
|
10
|
+
- 打开系统文件夹 / 在外部编辑器(如 VS Code)打开工作区**由 DSH 新版壳自带**,本插件不再提供模式下拉与外部启动器,只保留内置编辑器
|
|
12
11
|
- **文件列表**:左侧目录树(跳过 `node_modules`/`.git`/`dist` 等)
|
|
13
12
|
- **按文件名过滤**:输入即过滤
|
|
14
13
|
- **全文搜索**:正则 / 大小写 / 全词匹配、包含/排除过滤,结果按文件分组、命中高亮,点击跳转并高亮匹配
|
|
15
14
|
- **编辑**:CodeMirror 6(20+ 语言语法高亮含 Vue SFC、行号、撤销/重做、Ctrl+S 保存、只读切换、Tab 缩进)
|
|
16
15
|
- **预览**:图片直接显示、PDF 内嵌查看(Markdown / HTML 预览已移除——大文档渲染会卡死界面)
|
|
16
|
+
- **全屏**:弹窗标题栏的全屏按钮,一键把预览 / 编辑弹窗放大到整个窗口(图标转为「退出全屏」,再点一次还原)
|
|
17
17
|
- **界面**:复用 Web 壳设计系统(`@deepseek-ai/dsh-client-ui-primitives`:Button / Menu / Modal / Input / Toast / Tooltip + `--dsw-*` token),随主题明暗自适应
|
|
18
18
|
|
|
19
19
|
## 架构
|
|
20
20
|
|
|
21
21
|
```
|
|
22
22
|
src/
|
|
23
|
-
index.ts Host:/filex/api JSON 路由(session.cwd / fs.list / fs.read / fs.write / fs.search
|
|
23
|
+
index.ts Host:/filex/api JSON 路由(session.cwd / fs.list / fs.read / fs.write / fs.search)
|
|
24
24
|
+ /filex/file 媒体路由(图片/PDF 字节),loopback 信任围栏
|
|
25
25
|
trust-fence.ts Host-header loopback 校验(DNS-rebinding 防护)
|
|
26
26
|
client/
|
|
27
|
-
index.tsx 入口:header
|
|
27
|
+
index.tsx 入口:header 图标(内置编辑器)+ overlay 弹窗 + Ctrl+P 快捷键
|
|
28
28
|
Explorer.tsx 弹窗:文件树 / 文件名过滤 / 全文搜索 / 查看器分派(primitives 重排)
|
|
29
29
|
TextEditor.tsx CodeMirror 6 编辑器 + 搜索跳转高亮
|
|
30
30
|
lang.ts 扩展名 → CodeMirror 语言映射
|
package/lib/client.js
CHANGED
|
@@ -49,15 +49,6 @@ window.__ModuleLoader__.load({
|
|
|
49
49
|
path,
|
|
50
50
|
content
|
|
51
51
|
}),
|
|
52
|
-
fsReveal: (scope, cwd, signal) => call("fs.reveal", {
|
|
53
|
-
sessionId: scope.sessionId,
|
|
54
|
-
...cwd !== void 0 && cwd !== "" ? { cwd } : {}
|
|
55
|
-
}, signal),
|
|
56
|
-
fsVscode: (scope, cwd, signal) => call("fs.vscode", {
|
|
57
|
-
sessionId: scope.sessionId,
|
|
58
|
-
...cwd !== void 0 && cwd !== "" ? { cwd } : {}
|
|
59
|
-
}, signal),
|
|
60
|
-
fsCapabilities: (signal) => call("fs.capabilities", {}, signal),
|
|
61
52
|
fsSearch: (scope, pattern, options, include, exclude, signal) => call("fs.search", {
|
|
62
53
|
sessionId: scope.sessionId,
|
|
63
54
|
pattern,
|
|
@@ -37010,13 +37001,22 @@ window.__ModuleLoader__.load({
|
|
|
37010
37001
|
* inputs, modals, toasts, tooltips) is left to them; this sheet only covers
|
|
37011
37002
|
* the explorer's bespoke layout — the split body, tree rows, search rows,
|
|
37012
37003
|
* editor chrome — and the small adjustments that compose primitives into
|
|
37013
|
-
* native-looking pieces (the header
|
|
37004
|
+
* native-looking pieces (the header icon, the modal surface).
|
|
37014
37005
|
*/
|
|
37015
37006
|
const CSS = `
|
|
37016
|
-
/* ── header
|
|
37017
|
-
.
|
|
37018
|
-
|
|
37019
|
-
|
|
37007
|
+
/* ── header icon (utilities slot) ────────────────────────────────────── */
|
|
37008
|
+
/* Same 28×28 round button as dsh-git-gui's header entry (.gg-header-btn):
|
|
37009
|
+
the primitive sm button is already 28px tall with a 16px icon, so only
|
|
37010
|
+
the width / padding / radius need pinning down. */
|
|
37011
|
+
.filex-header-btn{width:28px;height:28px;padding:0;border-radius:14px}
|
|
37012
|
+
|
|
37013
|
+
/* ── hero floating utility (shell.overlay entry) ─────────────────────── */
|
|
37014
|
+
/* The same icon, pinned by the plugin to the conversation column's
|
|
37015
|
+
top-right while the column is in its hero phase. The shell.overlay layer
|
|
37016
|
+
is click-through by design, so the entry opts back into pointer events;
|
|
37017
|
+
no explicit z-index keeps it below sibling overlay entries (modal,
|
|
37018
|
+
notices) and any body-portaled popup. */
|
|
37019
|
+
.filex-hero-fab{position:fixed;pointer-events:auto}
|
|
37020
37020
|
|
|
37021
37021
|
/* ── transient notice strip ──────────────────────────────────────────── */
|
|
37022
37022
|
.filex-notice{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);z-index:2147483003;display:flex;align-items:center;gap:10px;max-width:70vw;padding:9px 14px;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--dsw-alias-state-warn-primary,#f59e0b);background:var(--dsw-alias-bg-layer-2,#1b1b22);border:1px solid rgba(245,158,11,.5);border-radius:10px;box-shadow:0 8px 28px rgba(0,0,0,.45);pointer-events:auto}
|
|
@@ -37025,6 +37025,12 @@ window.__ModuleLoader__.load({
|
|
|
37025
37025
|
|
|
37026
37026
|
/* ── modal surface (headless primitives Modal) ───────────────────────── */
|
|
37027
37027
|
.filex-modal{width:min(1200px,92vw);height:min(780px,88vh);gap:0;padding:0;border-radius:16px}
|
|
37028
|
+
/* Full-viewport mode (header toggle). The shell's dialog sits in a fixed
|
|
37029
|
+
root with 24px padding, so a fixed inset:0 card fills the whole window
|
|
37030
|
+
without being inset by that padding. */
|
|
37031
|
+
.filex-modal-full{position:fixed;inset:0;width:auto;height:auto;border-radius:0}
|
|
37032
|
+
/* The expand-corners glyph rotated 180° reads as "exit fullscreen". */
|
|
37033
|
+
.filex-fs-on{transform:rotate(180deg)}
|
|
37028
37034
|
|
|
37029
37035
|
/* ── explorer frame ──────────────────────────────────────────────────── */
|
|
37030
37036
|
.filex-explorer{display:flex;flex-direction:column;height:100%;min-height:0}
|
|
@@ -37349,6 +37355,8 @@ window.__ModuleLoader__.load({
|
|
|
37349
37355
|
const [jumpRanges, setJumpRanges] = (0, react.useState)(void 0);
|
|
37350
37356
|
const [collapsed, setCollapsed] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
37351
37357
|
const [savedFlash, setSavedFlash] = (0, react.useState)("");
|
|
37358
|
+
/** Full-viewport mode: the modal card fills the whole window (toggle in the header). */
|
|
37359
|
+
const [fullscreen, setFullscreen] = (0, react.useState)(false);
|
|
37352
37360
|
const [sideMenu, setSideMenu] = (0, react.useState)(null);
|
|
37353
37361
|
const [sideToast, setSideToast] = (0, react.useState)("");
|
|
37354
37362
|
const [toastKey, setToastKey] = (0, react.useState)(0);
|
|
@@ -37706,7 +37714,7 @@ window.__ModuleLoader__.load({
|
|
|
37706
37714
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
37707
37715
|
open: true,
|
|
37708
37716
|
headless: true,
|
|
37709
|
-
className: "filex-modal"
|
|
37717
|
+
className: `filex-modal${fullscreen ? " filex-modal-full" : ""}`,
|
|
37710
37718
|
title: openPath ?? "文件预览 / 编辑",
|
|
37711
37719
|
closeLabel: "关闭",
|
|
37712
37720
|
onClose: handleClose,
|
|
@@ -37724,18 +37732,30 @@ window.__ModuleLoader__.load({
|
|
|
37724
37732
|
})
|
|
37725
37733
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
37726
37734
|
className: "filex-header-actions",
|
|
37727
|
-
children: [
|
|
37728
|
-
|
|
37729
|
-
|
|
37730
|
-
|
|
37731
|
-
|
|
37732
|
-
|
|
37733
|
-
|
|
37734
|
-
|
|
37735
|
-
|
|
37736
|
-
|
|
37737
|
-
|
|
37738
|
-
|
|
37735
|
+
children: [
|
|
37736
|
+
savedFlash !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
37737
|
+
className: `filex-badge${savedFlash.startsWith("保存失败") ? " filex-badge-warn" : ""}`,
|
|
37738
|
+
children: savedFlash
|
|
37739
|
+
}),
|
|
37740
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
37741
|
+
type: "button",
|
|
37742
|
+
size: "sm",
|
|
37743
|
+
variant: "ghost",
|
|
37744
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFullscreenOutline16, { className: fullscreen ? "filex-fs-on" : void 0 }),
|
|
37745
|
+
title: fullscreen ? "退出全屏" : "全屏",
|
|
37746
|
+
"aria-label": fullscreen ? "退出全屏" : "全屏",
|
|
37747
|
+
onClick: () => setFullscreen((v) => !v)
|
|
37748
|
+
}),
|
|
37749
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
37750
|
+
type: "button",
|
|
37751
|
+
size: "sm",
|
|
37752
|
+
variant: "ghost",
|
|
37753
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, {}),
|
|
37754
|
+
title: "关闭(Esc)",
|
|
37755
|
+
"aria-label": "关闭",
|
|
37756
|
+
onClick: handleClose
|
|
37757
|
+
})
|
|
37758
|
+
]
|
|
37739
37759
|
})]
|
|
37740
37760
|
}),
|
|
37741
37761
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -37946,32 +37966,93 @@ window.__ModuleLoader__.load({
|
|
|
37946
37966
|
* @returns the disposer restoring the original method (HMR-safe).
|
|
37947
37967
|
*/
|
|
37948
37968
|
function wrapOpenPath(workspaces, deps) {
|
|
37969
|
+
const descriptor = Object.getOwnPropertyDescriptor(workspaces, "openPath");
|
|
37949
37970
|
const original = workspaces.openPath;
|
|
37950
|
-
|
|
37971
|
+
const wrapped = async (path) => {
|
|
37951
37972
|
const sessionId = deps.currentSessionId();
|
|
37952
37973
|
if (sessionId !== void 0) {
|
|
37953
37974
|
if (await deps.openInEditor(path, sessionId)) return;
|
|
37954
37975
|
}
|
|
37955
37976
|
return original.call(workspaces, path);
|
|
37956
37977
|
};
|
|
37978
|
+
Object.defineProperty(workspaces, "openPath", {
|
|
37979
|
+
configurable: true,
|
|
37980
|
+
enumerable: descriptor?.enumerable ?? true,
|
|
37981
|
+
writable: false,
|
|
37982
|
+
value: wrapped
|
|
37983
|
+
});
|
|
37984
|
+
return () => {
|
|
37985
|
+
if (descriptor !== void 0) Object.defineProperty(workspaces, "openPath", descriptor);
|
|
37986
|
+
else {
|
|
37987
|
+
delete workspaces.openPath;
|
|
37988
|
+
workspaces.openPath = original;
|
|
37989
|
+
}
|
|
37990
|
+
};
|
|
37991
|
+
}
|
|
37992
|
+
/**
|
|
37993
|
+
* Wrap `remote.session.openWorkspacePath` — the chat file-open funnel in
|
|
37994
|
+
* current runtimes (ui-chat's `ChatView` calls it for tool-row path links,
|
|
37995
|
+
* the produced-files row, and prose file mentions alike). Intercepted opens
|
|
37996
|
+
* go to the explorer modal and resolve as a successful open (the caller only
|
|
37997
|
+
* checks `ok`); anything that declines falls through to the original RPC,
|
|
37998
|
+
* which opens the path with the Host OS default application.
|
|
37999
|
+
*
|
|
38000
|
+
* The generated remote face exposes `openWorkspacePath` as an own accessor
|
|
38001
|
+
* (getter without setter), so a plain assignment silently fails — the
|
|
38002
|
+
* property is redefined instead, and the original descriptor is restored on
|
|
38003
|
+
* disposal (HMR-safe).
|
|
38004
|
+
* @param service - the `remote.session` face to wrap.
|
|
38005
|
+
* @param deps - per-call takeover decisions (same as {@link wrapOpenPath}).
|
|
38006
|
+
* @returns the disposer restoring the original method (HMR-safe).
|
|
38007
|
+
*/
|
|
38008
|
+
function wrapOpenWorkspacePath(service, deps) {
|
|
38009
|
+
const descriptor = Object.getOwnPropertyDescriptor(service, "openWorkspacePath");
|
|
38010
|
+
const original = service.openWorkspacePath;
|
|
38011
|
+
const wrapped = async (input) => {
|
|
38012
|
+
const sessionId = deps.currentSessionId();
|
|
38013
|
+
if (sessionId !== void 0) {
|
|
38014
|
+
if (await deps.openInEditor(input.path, sessionId)) return {
|
|
38015
|
+
ok: true,
|
|
38016
|
+
value: { opened: true }
|
|
38017
|
+
};
|
|
38018
|
+
}
|
|
38019
|
+
return original.call(service, input);
|
|
38020
|
+
};
|
|
38021
|
+
Object.defineProperty(service, "openWorkspacePath", {
|
|
38022
|
+
configurable: true,
|
|
38023
|
+
enumerable: descriptor?.enumerable ?? true,
|
|
38024
|
+
writable: false,
|
|
38025
|
+
value: wrapped
|
|
38026
|
+
});
|
|
37957
38027
|
return () => {
|
|
37958
|
-
|
|
38028
|
+
if (descriptor !== void 0) Object.defineProperty(service, "openWorkspacePath", descriptor);
|
|
38029
|
+
else {
|
|
38030
|
+
delete service.openWorkspacePath;
|
|
38031
|
+
service.openWorkspacePath = original;
|
|
38032
|
+
}
|
|
37959
38033
|
};
|
|
37960
38034
|
}
|
|
37961
38035
|
//#endregion
|
|
37962
38036
|
//#region src/client/index.tsx
|
|
37963
38037
|
/**
|
|
37964
|
-
* Client half of dsh-file-explorer: a header
|
|
37965
|
-
*
|
|
37966
|
-
*
|
|
37967
|
-
* plus the overlay modal, the Ctrl+P shortcut, and transient notices.
|
|
38038
|
+
* Client half of dsh-file-explorer: a single header icon that opens the
|
|
38039
|
+
* built-in file-explorer modal (preview / edit), plus the overlay modal, the
|
|
38040
|
+
* Ctrl+P shortcut, and transient notices.
|
|
37968
38041
|
*
|
|
37969
|
-
* The
|
|
37970
|
-
* the
|
|
37971
|
-
*
|
|
37972
|
-
*
|
|
37973
|
-
*
|
|
37974
|
-
*
|
|
38042
|
+
* The icon registers into `conversation.session.header.utilities`. Opening
|
|
38043
|
+
* the session's working folder in the OS file manager and opening it in an
|
|
38044
|
+
* external editor are features of the DSH shell itself in current versions,
|
|
38045
|
+
* so this plugin deliberately carries only its own built-in editor and no
|
|
38046
|
+
* mode dropdown / no external launchers.
|
|
38047
|
+
*
|
|
38048
|
+
* The same icon is also shown on the new-session screen through the
|
|
38049
|
+
* generic `shell.overlay` floating layer (no shell change involved): a
|
|
38050
|
+
* plugin-owned entry pins it to the conversation column's top-right while
|
|
38051
|
+
* the column is in its hero phase — the no-session hero and the
|
|
38052
|
+
* blank-session (new chat) hero, where the session header and its utilities
|
|
38053
|
+
* seat are absent or deliberately hidden. The floating entry resolves its
|
|
38054
|
+
* session itself: the action falls back to the live selection
|
|
38055
|
+
* (`useSessions` → `state.current`) and shows a notice when none exists.
|
|
37975
38056
|
*
|
|
37976
38057
|
* Session binding: every /filex request is conversation-scoped, so the modal
|
|
37977
38058
|
* must know WHICH session it belongs to — the host resolves the workspace
|
|
@@ -37983,53 +38064,15 @@ window.__ModuleLoader__.load({
|
|
|
37983
38064
|
const inject = [
|
|
37984
38065
|
"slots",
|
|
37985
38066
|
"sessions",
|
|
37986
|
-
"workspaces"
|
|
38067
|
+
"workspaces",
|
|
38068
|
+
"remote",
|
|
38069
|
+
"remote.session"
|
|
37987
38070
|
];
|
|
37988
|
-
const MODE_STORAGE_KEY = "dsh-file-explorer.header-mode";
|
|
37989
|
-
/** Read the persisted mode; anything unknown defaults to the editor. */
|
|
37990
|
-
function readStoredMode() {
|
|
37991
|
-
try {
|
|
37992
|
-
const stored = window.localStorage.getItem(MODE_STORAGE_KEY);
|
|
37993
|
-
if (stored === "folder" || stored === "vscode") return stored;
|
|
37994
|
-
return "editor";
|
|
37995
|
-
} catch {
|
|
37996
|
-
return "editor";
|
|
37997
|
-
}
|
|
37998
|
-
}
|
|
37999
|
-
/** The official VS Code logo (brand colors), sized like the kit icons. */
|
|
38000
|
-
function VscodeIcon(props) {
|
|
38001
|
-
const { size = 16, className } = props;
|
|
38002
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
38003
|
-
width: size,
|
|
38004
|
-
height: size,
|
|
38005
|
-
viewBox: "0 0 32 32",
|
|
38006
|
-
className,
|
|
38007
|
-
"aria-hidden": true,
|
|
38008
|
-
focusable: "false",
|
|
38009
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
38010
|
-
children: [
|
|
38011
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
38012
|
-
d: "M29.01,5.03,23.244,2.254a1.742,1.742,0,0,0-1.989.338L2.38,19.8A1.166,1.166,0,0,0,2.3,21.447c.025.027.05.053.077.077l1.541,1.4a1.165,1.165,0,0,0,1.489.066L28.142,5.75A1.158,1.158,0,0,1,30,6.672V6.605A1.748,1.748,0,0,0,29.01,5.03Z",
|
|
38013
|
-
fill: "#0065a9"
|
|
38014
|
-
}),
|
|
38015
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
38016
|
-
d: "M29.01,26.97l-5.766,2.777a1.745,1.745,0,0,1-1.989-.338L2.38,12.2A1.166,1.166,0,0,1,2.3,10.553c.025-.027.05-.053.077-.077l1.541-1.4A1.165,1.165,0,0,1,5.41,9.01L28.142,26.25A1.158,1.158,0,0,0,30,25.328V25.4A1.749,1.749,0,0,1,29.01,26.97Z",
|
|
38017
|
-
fill: "#007acc"
|
|
38018
|
-
}),
|
|
38019
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
38020
|
-
d: "M23.244,29.747a1.745,1.745,0,0,1-1.989-.338A1.025,1.025,0,0,0,23,28.684V3.316a1.024,1.024,0,0,0-1.749-.724,1.744,1.744,0,0,1,1.989-.339l5.765,2.772A1.748,1.748,0,0,1,30,6.6V25.4a1.748,1.748,0,0,1-.991,1.576Z",
|
|
38021
|
-
fill: "#1f9cf0"
|
|
38022
|
-
})
|
|
38023
|
-
]
|
|
38024
|
-
});
|
|
38025
|
-
}
|
|
38026
38071
|
let ctxRef;
|
|
38027
38072
|
let store = {
|
|
38028
38073
|
open: false,
|
|
38029
38074
|
sessionId: "",
|
|
38030
38075
|
notice: null,
|
|
38031
|
-
mode: readStoredMode(),
|
|
38032
|
-
vscode: true,
|
|
38033
38076
|
pendingPath: null,
|
|
38034
38077
|
pendingSeq: 0
|
|
38035
38078
|
};
|
|
@@ -38079,7 +38122,7 @@ window.__ModuleLoader__.load({
|
|
|
38079
38122
|
function openExplorer(sessionId, path) {
|
|
38080
38123
|
const resolved = sessionId !== void 0 && sessionId !== "" ? sessionId : activeSessionId;
|
|
38081
38124
|
if (resolved === void 0) {
|
|
38082
|
-
showNotice("
|
|
38125
|
+
showNotice("没有可用的会话:无法确定文件工作区。请先新建/选择一个会话。");
|
|
38083
38126
|
return;
|
|
38084
38127
|
}
|
|
38085
38128
|
store = {
|
|
@@ -38094,32 +38137,7 @@ window.__ModuleLoader__.load({
|
|
|
38094
38137
|
};
|
|
38095
38138
|
emit();
|
|
38096
38139
|
}
|
|
38097
|
-
/**
|
|
38098
|
-
async function openSystemFolder(sessionId, cwd) {
|
|
38099
|
-
const resolved = sessionId !== void 0 && sessionId !== "" ? sessionId : activeSessionId;
|
|
38100
|
-
if (resolved === void 0) {
|
|
38101
|
-
showNotice("没有可用的会话:无法确定文件工作区。请先新建/选择一个会话。");
|
|
38102
|
-
return;
|
|
38103
|
-
}
|
|
38104
|
-
try {
|
|
38105
|
-
showNotice(`已打开文件夹:${(await api.fsReveal({ sessionId: resolved }, cwd)).cwd}`);
|
|
38106
|
-
} catch (error) {
|
|
38107
|
-
showNotice(`打开系统文件夹失败:${error instanceof Error ? error.message : String(error)}`);
|
|
38108
|
-
}
|
|
38109
|
-
}
|
|
38110
|
-
/** Ask the host to open the session's working folder in VS Code. */
|
|
38111
|
-
async function openInVscode(sessionId, cwd) {
|
|
38112
|
-
const resolved = sessionId !== void 0 && sessionId !== "" ? sessionId : activeSessionId;
|
|
38113
|
-
if (resolved === void 0) {
|
|
38114
|
-
showNotice("没有可用的会话:无法确定文件工作区。请先新建/选择一个会话。");
|
|
38115
|
-
return;
|
|
38116
|
-
}
|
|
38117
|
-
try {
|
|
38118
|
-
showNotice(`已在 VS Code 打开:${(await api.fsVscode({ sessionId: resolved }, cwd)).cwd}`);
|
|
38119
|
-
} catch (error) {
|
|
38120
|
-
showNotice(`用 VS Code 打开失败:${error instanceof Error ? error.message : String(error)}`);
|
|
38121
|
-
}
|
|
38122
|
-
}
|
|
38140
|
+
/** Close the modal (session binding stays for the next open). */
|
|
38123
38141
|
function closeExplorer() {
|
|
38124
38142
|
store = {
|
|
38125
38143
|
...store,
|
|
@@ -38127,125 +38145,80 @@ window.__ModuleLoader__.load({
|
|
|
38127
38145
|
};
|
|
38128
38146
|
emit();
|
|
38129
38147
|
}
|
|
38130
|
-
|
|
38131
|
-
if (store.mode === mode) return;
|
|
38132
|
-
store = {
|
|
38133
|
-
...store,
|
|
38134
|
-
mode
|
|
38135
|
-
};
|
|
38136
|
-
emit();
|
|
38137
|
-
try {
|
|
38138
|
-
window.localStorage.setItem(MODE_STORAGE_KEY, mode);
|
|
38139
|
-
} catch {}
|
|
38140
|
-
}
|
|
38141
|
-
/**
|
|
38142
|
-
* Probe the host once per activation for VS Code availability and hide the
|
|
38143
|
-
* VSCode option (falling back to the editor) when it is missing — the user
|
|
38144
|
-
* explicitly wants the entry hidden rather than shown-but-broken.
|
|
38145
|
-
*/
|
|
38146
|
-
function probeVscode() {
|
|
38147
|
-
api.fsCapabilities().then((result) => {
|
|
38148
|
-
store = {
|
|
38149
|
-
...store,
|
|
38150
|
-
vscode: result.vscode
|
|
38151
|
-
};
|
|
38152
|
-
if (!result.vscode && store.mode === "vscode") store = {
|
|
38153
|
-
...store,
|
|
38154
|
-
mode: "editor"
|
|
38155
|
-
};
|
|
38156
|
-
emit();
|
|
38157
|
-
}).catch(() => {
|
|
38158
|
-
store = {
|
|
38159
|
-
...store,
|
|
38160
|
-
vscode: false
|
|
38161
|
-
};
|
|
38162
|
-
if (store.mode === "vscode") store = {
|
|
38163
|
-
...store,
|
|
38164
|
-
mode: "editor"
|
|
38165
|
-
};
|
|
38166
|
-
emit();
|
|
38167
|
-
});
|
|
38168
|
-
}
|
|
38169
|
-
const MODE_ITEMS = [
|
|
38170
|
-
{
|
|
38171
|
-
id: "editor",
|
|
38172
|
-
label: "编辑器",
|
|
38173
|
-
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, {})
|
|
38174
|
-
},
|
|
38175
|
-
{
|
|
38176
|
-
id: "folder",
|
|
38177
|
-
label: "文件夹",
|
|
38178
|
-
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpen16, {})
|
|
38179
|
-
},
|
|
38180
|
-
{
|
|
38181
|
-
id: "vscode",
|
|
38182
|
-
label: "VSCode",
|
|
38183
|
-
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VscodeIcon, {})
|
|
38184
|
-
}
|
|
38185
|
-
];
|
|
38186
|
-
/** The header ButtonGroup (session header utilities slot; receives sessionId). */
|
|
38148
|
+
/** The header icon: the built-in file explorer (preview / edit modal). */
|
|
38187
38149
|
function HeaderGroup(props) {
|
|
38188
|
-
const
|
|
38189
|
-
const [menuOpen, setMenuOpen] = (0, react.useState)(false);
|
|
38190
|
-
const sessionCwd = (props.useSessions ?? (() => void 0))((s) => props.sessionId !== void 0 ? s.byId?.[props.sessionId]?.cwd : void 0);
|
|
38191
|
-
const mode = state.mode;
|
|
38192
|
-
const items = state.vscode ? MODE_ITEMS : MODE_ITEMS.filter((item) => item.id !== "vscode");
|
|
38193
|
-
const mainIcon = mode === "folder" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpen16, {}) : mode === "vscode" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VscodeIcon, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, {});
|
|
38194
|
-
const mainTitle = mode === "folder" ? "打开系统文件夹" : mode === "vscode" ? "用 VSCode 打开工作区" : "文件预览 / 编辑(Ctrl+P)";
|
|
38195
|
-
const onMainClick = (e) => {
|
|
38196
|
-
e.stopPropagation();
|
|
38197
|
-
if (mode === "folder") openSystemFolder(props.sessionId, sessionCwd);
|
|
38198
|
-
else if (mode === "vscode") openInVscode(props.sessionId, sessionCwd);
|
|
38199
|
-
else openExplorer(props.sessionId);
|
|
38200
|
-
};
|
|
38150
|
+
const title = "文件预览 / 编辑(Ctrl+P)";
|
|
38201
38151
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
38202
|
-
label:
|
|
38152
|
+
label: title,
|
|
38203
38153
|
side: "bottom",
|
|
38204
38154
|
delayMs: 400,
|
|
38205
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.
|
|
38206
|
-
|
|
38207
|
-
|
|
38208
|
-
|
|
38209
|
-
|
|
38210
|
-
|
|
38211
|
-
|
|
38212
|
-
|
|
38213
|
-
|
|
38214
|
-
|
|
38215
|
-
|
|
38216
|
-
|
|
38217
|
-
else if (next === "vscode") openInVscode(props.sessionId, sessionCwd);
|
|
38218
|
-
else openExplorer(props.sessionId);
|
|
38219
|
-
},
|
|
38220
|
-
onClose: () => setMenuOpen(false),
|
|
38221
|
-
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
38222
|
-
className: "filex-group",
|
|
38223
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
38224
|
-
type: "button",
|
|
38225
|
-
className: "filex-group-main",
|
|
38226
|
-
size: "sm",
|
|
38227
|
-
variant: "outline",
|
|
38228
|
-
icon: mainIcon,
|
|
38229
|
-
title: mainTitle,
|
|
38230
|
-
"aria-label": mainTitle,
|
|
38231
|
-
onClick: onMainClick
|
|
38232
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
38233
|
-
type: "button",
|
|
38234
|
-
className: "filex-group-trigger",
|
|
38235
|
-
size: "sm",
|
|
38236
|
-
variant: "outline",
|
|
38237
|
-
title: "选择打开方式:编辑器 / 文件夹 / VSCode",
|
|
38238
|
-
"aria-label": "选择打开方式",
|
|
38239
|
-
onClick: (e) => {
|
|
38240
|
-
e.stopPropagation();
|
|
38241
|
-
setMenuOpen((v) => !v);
|
|
38242
|
-
},
|
|
38243
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
|
|
38244
|
-
})]
|
|
38245
|
-
})
|
|
38155
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
38156
|
+
type: "button",
|
|
38157
|
+
className: "filex-header-btn",
|
|
38158
|
+
size: "sm",
|
|
38159
|
+
variant: "outline",
|
|
38160
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, {}),
|
|
38161
|
+
title,
|
|
38162
|
+
"aria-label": "文件预览 / 编辑",
|
|
38163
|
+
onClick: (e) => {
|
|
38164
|
+
e.stopPropagation();
|
|
38165
|
+
openExplorer(props.sessionId);
|
|
38166
|
+
}
|
|
38246
38167
|
})
|
|
38247
38168
|
});
|
|
38248
38169
|
}
|
|
38170
|
+
/**
|
|
38171
|
+
* Hero / new-session floating utility: the same HeaderGroup, rendered by the
|
|
38172
|
+
* plugin itself through the generic `shell.overlay` floating layer (no shell
|
|
38173
|
+
* change needed) and pinned to the conversation column's top-right corner —
|
|
38174
|
+
* the spot where the session-header utilities sit once a conversation has
|
|
38175
|
+
* records. Shown only while the conversation column is in its `hero` phase
|
|
38176
|
+
* (no session at all, or a blank session whose header is deliberately
|
|
38177
|
+
* hidden); the position is measured from the rendered column root
|
|
38178
|
+
* (`[data-phase]`), so sidebar collapse and details-panel toggles are
|
|
38179
|
+
* tracked automatically. Clicking behaves exactly like the header icon: in a
|
|
38180
|
+
* blank-session hero the action binds to that session, and with no session
|
|
38181
|
+
* at all it surfaces the no-session notice.
|
|
38182
|
+
*/
|
|
38183
|
+
function HeroFilexButton() {
|
|
38184
|
+
const [pos, setPos] = (0, react.useState)(null);
|
|
38185
|
+
(0, react.useEffect)(() => {
|
|
38186
|
+
const update = () => {
|
|
38187
|
+
const column = document.querySelector("[data-phase]");
|
|
38188
|
+
if (column === null || column.getAttribute("data-phase") !== "hero") {
|
|
38189
|
+
setPos(null);
|
|
38190
|
+
return;
|
|
38191
|
+
}
|
|
38192
|
+
const rect = column.getBoundingClientRect();
|
|
38193
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
38194
|
+
setPos(null);
|
|
38195
|
+
return;
|
|
38196
|
+
}
|
|
38197
|
+
const top = rect.top + 14;
|
|
38198
|
+
const right = window.innerWidth - rect.right + 28;
|
|
38199
|
+
setPos((current) => current !== null && current.top === top && current.right === right ? current : {
|
|
38200
|
+
top,
|
|
38201
|
+
right
|
|
38202
|
+
});
|
|
38203
|
+
};
|
|
38204
|
+
const timer = window.setInterval(update, 400);
|
|
38205
|
+
window.addEventListener("resize", update);
|
|
38206
|
+
update();
|
|
38207
|
+
return () => {
|
|
38208
|
+
window.clearInterval(timer);
|
|
38209
|
+
window.removeEventListener("resize", update);
|
|
38210
|
+
};
|
|
38211
|
+
}, []);
|
|
38212
|
+
if (pos === null) return null;
|
|
38213
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
38214
|
+
className: "filex-hero-fab",
|
|
38215
|
+
style: {
|
|
38216
|
+
top: pos.top,
|
|
38217
|
+
right: pos.right
|
|
38218
|
+
},
|
|
38219
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HeaderGroup, {})
|
|
38220
|
+
});
|
|
38221
|
+
}
|
|
38249
38222
|
/** The overlay entry: keyboard shortcut + modal + transient notice. */
|
|
38250
38223
|
function ExplorerOverlay(props) {
|
|
38251
38224
|
const state = (0, react.useSyncExternalStore)(subscribe, getSnapshot);
|
|
@@ -38321,25 +38294,44 @@ window.__ModuleLoader__.load({
|
|
|
38321
38294
|
order: 10,
|
|
38322
38295
|
label: "文件预览 / 编辑"
|
|
38323
38296
|
}, HeaderGroup));
|
|
38324
|
-
ctx.slots.inject("shell.overlay", () =>
|
|
38325
|
-
|
|
38326
|
-
|
|
38327
|
-
|
|
38328
|
-
|
|
38329
|
-
|
|
38330
|
-
|
|
38331
|
-
|
|
38332
|
-
|
|
38333
|
-
|
|
38334
|
-
|
|
38335
|
-
|
|
38336
|
-
|
|
38337
|
-
|
|
38297
|
+
ctx.slots.inject("shell.overlay", () => {
|
|
38298
|
+
const overlay = ctx.slots.register({
|
|
38299
|
+
name: "shell.overlay",
|
|
38300
|
+
id: "file-explorer-overlay",
|
|
38301
|
+
order: 100,
|
|
38302
|
+
label: "文件预览"
|
|
38303
|
+
}, ExplorerOverlay);
|
|
38304
|
+
const heroFab = ctx.slots.register({
|
|
38305
|
+
name: "shell.overlay",
|
|
38306
|
+
id: "file-explorer-hero-fab",
|
|
38307
|
+
order: 90,
|
|
38308
|
+
label: "文件预览 / 编辑(hero)"
|
|
38309
|
+
}, HeroFilexButton);
|
|
38310
|
+
return () => {
|
|
38311
|
+
overlay();
|
|
38312
|
+
heroFab();
|
|
38313
|
+
};
|
|
38314
|
+
});
|
|
38315
|
+
ctx.effect(() => {
|
|
38316
|
+
const deps = {
|
|
38317
|
+
currentSessionId: () => activeSessionId,
|
|
38318
|
+
openInEditor: async (path, sessionId) => {
|
|
38319
|
+
try {
|
|
38320
|
+
await api.fsRead({ sessionId }, path);
|
|
38321
|
+
} catch {
|
|
38322
|
+
return false;
|
|
38323
|
+
}
|
|
38324
|
+
openExplorer(sessionId, path);
|
|
38325
|
+
return true;
|
|
38338
38326
|
}
|
|
38339
|
-
|
|
38340
|
-
|
|
38341
|
-
}
|
|
38342
|
-
|
|
38327
|
+
};
|
|
38328
|
+
const restoreOpenPath = wrapOpenPath(ctx.workspaces, deps);
|
|
38329
|
+
const restoreWorkspacePath = ctx.remote?.session?.openWorkspacePath !== void 0 ? wrapOpenWorkspacePath(ctx.remote.session, deps) : () => {};
|
|
38330
|
+
return () => {
|
|
38331
|
+
restoreOpenPath();
|
|
38332
|
+
restoreWorkspacePath();
|
|
38333
|
+
};
|
|
38334
|
+
}, "dsh-file-explorer: file-open interception");
|
|
38343
38335
|
} catch (error) {
|
|
38344
38336
|
fail("load", error);
|
|
38345
38337
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
2
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
3
|
//#region src/trust-fence.ts
|
|
5
4
|
function header(headers, name) {
|
|
@@ -297,110 +296,6 @@ function parseGlobList(raw) {
|
|
|
297
296
|
}
|
|
298
297
|
return out;
|
|
299
298
|
}
|
|
300
|
-
/**
|
|
301
|
-
* Launch the OS file manager pointing at `target` (a directory). Detached and
|
|
302
|
-
* stdio-ignored so the spawn never blocks the host or leaks pipes; failures
|
|
303
|
-
* are swallowed — the API reports unsupported platforms instead.
|
|
304
|
-
*
|
|
305
|
-
* NOTE: never pass `windowsHide: true` on Windows — explorer.exe honors the
|
|
306
|
-
* startup show-window flag and would open its folder window hidden, making
|
|
307
|
-
* the click look dead. The plain GUI-app spawn shows the window normally.
|
|
308
|
-
*/
|
|
309
|
-
function openInSystemFileManager(target) {
|
|
310
|
-
let command;
|
|
311
|
-
let args;
|
|
312
|
-
if (process.platform === "win32") {
|
|
313
|
-
command = "explorer";
|
|
314
|
-
args = [target];
|
|
315
|
-
} else if (process.platform === "darwin") {
|
|
316
|
-
command = "open";
|
|
317
|
-
args = [target];
|
|
318
|
-
} else if (process.platform === "linux") {
|
|
319
|
-
command = "xdg-open";
|
|
320
|
-
args = [target];
|
|
321
|
-
} else return false;
|
|
322
|
-
try {
|
|
323
|
-
const child = spawn(command, args, {
|
|
324
|
-
detached: true,
|
|
325
|
-
stdio: "ignore"
|
|
326
|
-
});
|
|
327
|
-
child.on("error", () => {});
|
|
328
|
-
child.unref();
|
|
329
|
-
return true;
|
|
330
|
-
} catch {
|
|
331
|
-
return false;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Resolve the directory a reveal/open action should point at. Authoritative
|
|
336
|
-
* source: the session's own header cwd. When it is absent (sessions created
|
|
337
|
-
* without workspace metadata fall back to the host process cwd — usually
|
|
338
|
-
* meaningless), accept the loopback-only client's session-list cwd hint so
|
|
339
|
-
* the folder still opens where the user works.
|
|
340
|
-
*/
|
|
341
|
-
function revealCwdOf(ctx, sessionId, rawHint) {
|
|
342
|
-
const headerCwd = ctx.sessions.get(sessionId)?.header.cwd;
|
|
343
|
-
const hint = typeof rawHint === "string" && rawHint !== "" ? rawHint : void 0;
|
|
344
|
-
return headerCwd !== void 0 && headerCwd !== "" ? resolve(headerCwd) : hint !== void 0 && isAbsolute(hint) ? resolve(hint) : process.cwd();
|
|
345
|
-
}
|
|
346
|
-
/**
|
|
347
|
-
* Locate the VS Code CLI (`code`). Checks the standard install locations
|
|
348
|
-
* first, then falls back to a PATH scan. Windows `code` is a .cmd shim, so
|
|
349
|
-
* the caller must route it through `cmd /c` (spawn alone cannot execute it).
|
|
350
|
-
*/
|
|
351
|
-
async function resolveVscodeCli() {
|
|
352
|
-
const candidates = [];
|
|
353
|
-
if (process.platform === "win32") {
|
|
354
|
-
const local = process.env.LOCALAPPDATA;
|
|
355
|
-
if (local !== void 0) candidates.push(join(local, "Programs", "Microsoft VS Code", "bin", "code.cmd"));
|
|
356
|
-
const pf = process.env.ProgramFiles;
|
|
357
|
-
if (pf !== void 0) candidates.push(join(pf, "Microsoft VS Code", "bin", "code.cmd"));
|
|
358
|
-
const pf86 = process.env["ProgramFiles(x86)"];
|
|
359
|
-
if (pf86 !== void 0) candidates.push(join(pf86, "Microsoft VS Code", "bin", "code.cmd"));
|
|
360
|
-
} else if (process.platform === "darwin") candidates.push("/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code");
|
|
361
|
-
else if (process.platform === "linux") candidates.push("/usr/bin/code", "/usr/local/bin/code", "/snap/bin/code");
|
|
362
|
-
for (const candidate of candidates) try {
|
|
363
|
-
await stat(candidate);
|
|
364
|
-
return candidate;
|
|
365
|
-
} catch {}
|
|
366
|
-
const name = process.platform === "win32" ? "code.cmd" : "code";
|
|
367
|
-
const paths = (process.env.PATH ?? "").split(process.platform === "win32" ? ";" : ":");
|
|
368
|
-
for (const dir of paths) {
|
|
369
|
-
if (dir === "") continue;
|
|
370
|
-
const probe = join(dir, name);
|
|
371
|
-
try {
|
|
372
|
-
await stat(probe);
|
|
373
|
-
return probe;
|
|
374
|
-
} catch {}
|
|
375
|
-
}
|
|
376
|
-
return null;
|
|
377
|
-
}
|
|
378
|
-
/**
|
|
379
|
-
* Launch VS Code pointed at `target` (a directory). On Windows the CLI is a
|
|
380
|
-
* .cmd shim, so it is routed through `cmd /c` with the console hidden (the
|
|
381
|
-
* hidden console belongs to cmd only — the Code GUI window is unaffected).
|
|
382
|
-
*/
|
|
383
|
-
function openInVscode(cli, target) {
|
|
384
|
-
try {
|
|
385
|
-
const child = process.platform === "win32" ? spawn("cmd", [
|
|
386
|
-
"/c",
|
|
387
|
-
cli,
|
|
388
|
-
target
|
|
389
|
-
], {
|
|
390
|
-
detached: true,
|
|
391
|
-
stdio: "ignore",
|
|
392
|
-
windowsHide: true
|
|
393
|
-
}) : spawn(cli, [target], {
|
|
394
|
-
detached: true,
|
|
395
|
-
stdio: "ignore"
|
|
396
|
-
});
|
|
397
|
-
child.on("error", () => {});
|
|
398
|
-
child.unref();
|
|
399
|
-
return true;
|
|
400
|
-
} catch {
|
|
401
|
-
return false;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
299
|
function buildApi(ctx) {
|
|
405
300
|
const cwdOf = (payload) => {
|
|
406
301
|
const sessionId = requireString(payload, "sessionId");
|
|
@@ -456,36 +351,6 @@ function buildApi(ctx) {
|
|
|
456
351
|
}
|
|
457
352
|
return { ok: true };
|
|
458
353
|
},
|
|
459
|
-
"fs.reveal": async (payload) => {
|
|
460
|
-
const { sessionId } = cwdOf(payload);
|
|
461
|
-
const cwd = revealCwdOf(ctx, sessionId, payload.cwd);
|
|
462
|
-
if (!(await stat(cwd).catch(() => {
|
|
463
|
-
throw new FilexError("fs-error", `cannot open "${cwd}": not found`, 404);
|
|
464
|
-
})).isDirectory()) throw new FilexError("fs-error", `"${cwd}" is not a directory`);
|
|
465
|
-
if (!openInSystemFileManager(cwd)) throw new FilexError("unsupported", "current platform has no file-manager launcher", 501);
|
|
466
|
-
return {
|
|
467
|
-
ok: true,
|
|
468
|
-
cwd
|
|
469
|
-
};
|
|
470
|
-
},
|
|
471
|
-
"fs.vscode": async (payload) => {
|
|
472
|
-
const { sessionId } = cwdOf(payload);
|
|
473
|
-
const cwd = revealCwdOf(ctx, sessionId, payload.cwd);
|
|
474
|
-
if (!(await stat(cwd).catch(() => {
|
|
475
|
-
throw new FilexError("fs-error", `cannot open "${cwd}": not found`, 404);
|
|
476
|
-
})).isDirectory()) throw new FilexError("fs-error", `"${cwd}" is not a directory`);
|
|
477
|
-
const cli = await resolveVscodeCli();
|
|
478
|
-
if (cli === null) throw new FilexError("vscode-not-found", "未找到 VS Code 命令行工具(code)。请确认 VS Code 已安装且勾选了“添加到 PATH”", 404);
|
|
479
|
-
if (!openInVscode(cli, cwd)) throw new FilexError("unsupported", "failed to launch the VS Code CLI", 501);
|
|
480
|
-
return {
|
|
481
|
-
ok: true,
|
|
482
|
-
cwd,
|
|
483
|
-
cli
|
|
484
|
-
};
|
|
485
|
-
},
|
|
486
|
-
"fs.capabilities": async () => {
|
|
487
|
-
return { vscode: await resolveVscodeCli() !== null };
|
|
488
|
-
},
|
|
489
354
|
"fs.search": async (payload) => {
|
|
490
355
|
const { cwd } = cwdOf(payload);
|
|
491
356
|
const pattern = requireString(payload, "pattern").trim();
|
|
@@ -64,6 +64,31 @@ export interface FilexWorkspacesService {
|
|
|
64
64
|
/** Open a filesystem path through the Host (OS default application). */
|
|
65
65
|
openPath(path: string): Promise<void>;
|
|
66
66
|
}
|
|
67
|
+
/** Result of the chat's workspace-path open RPC (dsh-api-remotes face). */
|
|
68
|
+
export interface FilexWorkspaceOpenResult {
|
|
69
|
+
ok: boolean;
|
|
70
|
+
value?: {
|
|
71
|
+
opened: boolean;
|
|
72
|
+
};
|
|
73
|
+
error?: {
|
|
74
|
+
message: string;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The `ctx.remote.session` face (dsh-api-remotes): the remote API the
|
|
79
|
+
* conversation view uses for chat-side file opens in current runtimes —
|
|
80
|
+
* `session/openWorkspacePath` opens with the Host OS default application
|
|
81
|
+
* unless a plugin takes it over.
|
|
82
|
+
*/
|
|
83
|
+
export interface FilexRemoteSession {
|
|
84
|
+
openWorkspacePath(input: {
|
|
85
|
+
path: string;
|
|
86
|
+
}): Promise<FilexWorkspaceOpenResult>;
|
|
87
|
+
}
|
|
88
|
+
/** The `ctx.remote` service face (structural subset used by this plugin). */
|
|
89
|
+
export interface FilexRemoteService {
|
|
90
|
+
session: FilexRemoteSession;
|
|
91
|
+
}
|
|
67
92
|
/** The composer input face (client conversation service). */
|
|
68
93
|
export interface FilexConversationInput {
|
|
69
94
|
state: {
|
|
@@ -98,6 +123,10 @@ declare module 'cordis' {
|
|
|
98
123
|
slots: FilexSlotsService;
|
|
99
124
|
/** Client-only: the chat's file-open funnel the explorer intercepts. */
|
|
100
125
|
workspaces: FilexWorkspacesService;
|
|
126
|
+
/** Client-only: the remote API face (dsh-api-remotes); chat-side file
|
|
127
|
+
* opens route through `remote.session.openWorkspacePath` in current
|
|
128
|
+
* runtimes, which the explorer also intercepts. */
|
|
129
|
+
remote?: FilexRemoteService;
|
|
101
130
|
/** Subscribe to session events (cordis event API). */
|
|
102
131
|
on(event: string, listener: (session: unknown, event: unknown) => void): () => void;
|
|
103
132
|
/** DSH-vendored cordis lifecycle helper. */
|
package/package.json
CHANGED
|
@@ -1,113 +1,113 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@dsh-xhl/dsh-file-explorer",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "DSH 外部编译型插件:工作区文件预览 / 编辑(CodeMirror 6 编辑器 + 文件树 + 全文搜索 + Markdown/HTML/图片/PDF 预览)。移植自同目录下的动态 Cordis 插件版本。",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "lib/index.js",
|
|
7
|
-
"types": "lib/types/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./lib/types/index.d.ts",
|
|
11
|
-
"default": "./lib/index.js"
|
|
12
|
-
},
|
|
13
|
-
"./client": "./lib/client.js",
|
|
14
|
-
"./package.json": "./package.json"
|
|
15
|
-
},
|
|
16
|
-
"engines": {
|
|
17
|
-
"node": ">=20"
|
|
18
|
-
},
|
|
19
|
-
"publishConfig": {
|
|
20
|
-
"access": "public",
|
|
21
|
-
"registry": "https://registry.npmjs.org"
|
|
22
|
-
},
|
|
23
|
-
"dsh": {
|
|
24
|
-
"bundle": {
|
|
25
|
-
"patch": "./cordis.patch.yml"
|
|
26
|
-
},
|
|
27
|
-
"client": {
|
|
28
|
-
"inject": [
|
|
29
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
30
|
-
"@deepseek-ai/dsh-client-ui-slots",
|
|
31
|
-
"@deepseek-ai/dsh-client-web-react"
|
|
32
|
-
],
|
|
33
|
-
"platform": "web"
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
"files": [
|
|
37
|
-
"lib/index.js",
|
|
38
|
-
"lib/client.js",
|
|
39
|
-
"lib/types/**/*.d.ts",
|
|
40
|
-
"cordis.patch.yml",
|
|
41
|
-
"README.md"
|
|
42
|
-
],
|
|
43
|
-
"scripts": {
|
|
44
|
-
"build": "node -e \"fs.rmSync('lib',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && tsdown",
|
|
45
|
-
"typecheck": "tsc --noEmit",
|
|
46
|
-
"bundle": "tsdown",
|
|
47
|
-
"prepare": "tsdown"
|
|
48
|
-
},
|
|
49
|
-
"license": "MIT",
|
|
50
|
-
"peerDependencies": {
|
|
51
|
-
"@deepseek-ai/cordis": "^4.0.1",
|
|
52
|
-
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
53
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
|
|
54
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
55
|
-
"@deepseek-ai/dsh-client-web-react": "^0.1.0-rc.6",
|
|
56
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
57
|
-
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
|
58
|
-
"cordis": "^4.0.0-rc.7",
|
|
59
|
-
"react": "^18.2.0",
|
|
60
|
-
"react-dom": "^18.2.0"
|
|
61
|
-
},
|
|
62
|
-
"peerDependenciesMeta": {
|
|
63
|
-
"cordis": {
|
|
64
|
-
"optional": true
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
"dependencies": {
|
|
68
|
-
"@codemirror/commands": "^6.10.4",
|
|
69
|
-
"@codemirror/lang-cpp": "^6.0.3",
|
|
70
|
-
"@codemirror/lang-css": "^6.3.1",
|
|
71
|
-
"@codemirror/lang-go": "^6.0.1",
|
|
72
|
-
"@codemirror/lang-html": "^6.4.12",
|
|
73
|
-
"@codemirror/lang-java": "^6.0.2",
|
|
74
|
-
"@codemirror/lang-javascript": "^6.2.5",
|
|
75
|
-
"@codemirror/lang-json": "^6.0.2",
|
|
76
|
-
"@codemirror/lang-markdown": "^6.5.2",
|
|
77
|
-
"@codemirror/lang-php": "^6.0.2",
|
|
78
|
-
"@codemirror/lang-python": "^6.2.1",
|
|
79
|
-
"@codemirror/lang-rust": "^6.0.2",
|
|
80
|
-
"@codemirror/lang-sql": "^6.10.0",
|
|
81
|
-
"@codemirror/lang-vue": "^0.1.3",
|
|
82
|
-
"@codemirror/lang-xml": "^6.1.0",
|
|
83
|
-
"@codemirror/lang-yaml": "^6.1.3",
|
|
84
|
-
"@codemirror/language": "^6.12.4",
|
|
85
|
-
"@codemirror/legacy-modes": "^6.5.3",
|
|
86
|
-
"@codemirror/search": "6.7.1",
|
|
87
|
-
"@codemirror/state": "^6.7.1",
|
|
88
|
-
"@codemirror/view": "^6.43.8",
|
|
89
|
-
"@lezer/highlight": "^1.2.3",
|
|
90
|
-
"vscode-icons-js": "^11.6.1"
|
|
91
|
-
},
|
|
92
|
-
"devDependencies": {
|
|
93
|
-
"@deepseek-ai/cordis": "^4.0.1",
|
|
94
|
-
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
|
|
95
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
|
|
96
|
-
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
97
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",
|
|
98
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
|
|
99
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
100
|
-
"@deepseek-ai/dsh-client-web-react": "^0.1.0-rc.6",
|
|
101
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
102
|
-
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
|
103
|
-
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
104
|
-
"@types/node": "^22.0.0",
|
|
105
|
-
"@types/react": "~18.3.1",
|
|
106
|
-
"@types/react-dom": "~18.3.1",
|
|
107
|
-
"cordis": "^4.0.0-rc.7",
|
|
108
|
-
"react": "^18.2.0",
|
|
109
|
-
"react-dom": "^18.2.0",
|
|
110
|
-
"tsdown": "^0.22.2",
|
|
111
|
-
"typescript": "^5.6.0"
|
|
112
|
-
}
|
|
113
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@dsh-xhl/dsh-file-explorer",
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "DSH 外部编译型插件:工作区文件预览 / 编辑(CodeMirror 6 编辑器 + 文件树 + 全文搜索 + Markdown/HTML/图片/PDF 预览)。移植自同目录下的动态 Cordis 插件版本。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/types/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": "./lib/client.js",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public",
|
|
21
|
+
"registry": "https://registry.npmjs.org"
|
|
22
|
+
},
|
|
23
|
+
"dsh": {
|
|
24
|
+
"bundle": {
|
|
25
|
+
"patch": "./cordis.patch.yml"
|
|
26
|
+
},
|
|
27
|
+
"client": {
|
|
28
|
+
"inject": [
|
|
29
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
30
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
31
|
+
"@deepseek-ai/dsh-client-web-react"
|
|
32
|
+
],
|
|
33
|
+
"platform": "web"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"lib/index.js",
|
|
38
|
+
"lib/client.js",
|
|
39
|
+
"lib/types/**/*.d.ts",
|
|
40
|
+
"cordis.patch.yml",
|
|
41
|
+
"README.md"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "node -e \"fs.rmSync('lib',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && tsdown",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"bundle": "tsdown",
|
|
47
|
+
"prepare": "tsdown"
|
|
48
|
+
},
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
52
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
53
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
55
|
+
"@deepseek-ai/dsh-client-web-react": "^0.1.0-rc.6",
|
|
56
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
57
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
|
58
|
+
"cordis": "^4.0.0-rc.7",
|
|
59
|
+
"react": "^18.2.0",
|
|
60
|
+
"react-dom": "^18.2.0"
|
|
61
|
+
},
|
|
62
|
+
"peerDependenciesMeta": {
|
|
63
|
+
"cordis": {
|
|
64
|
+
"optional": true
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"@codemirror/commands": "^6.10.4",
|
|
69
|
+
"@codemirror/lang-cpp": "^6.0.3",
|
|
70
|
+
"@codemirror/lang-css": "^6.3.1",
|
|
71
|
+
"@codemirror/lang-go": "^6.0.1",
|
|
72
|
+
"@codemirror/lang-html": "^6.4.12",
|
|
73
|
+
"@codemirror/lang-java": "^6.0.2",
|
|
74
|
+
"@codemirror/lang-javascript": "^6.2.5",
|
|
75
|
+
"@codemirror/lang-json": "^6.0.2",
|
|
76
|
+
"@codemirror/lang-markdown": "^6.5.2",
|
|
77
|
+
"@codemirror/lang-php": "^6.0.2",
|
|
78
|
+
"@codemirror/lang-python": "^6.2.1",
|
|
79
|
+
"@codemirror/lang-rust": "^6.0.2",
|
|
80
|
+
"@codemirror/lang-sql": "^6.10.0",
|
|
81
|
+
"@codemirror/lang-vue": "^0.1.3",
|
|
82
|
+
"@codemirror/lang-xml": "^6.1.0",
|
|
83
|
+
"@codemirror/lang-yaml": "^6.1.3",
|
|
84
|
+
"@codemirror/language": "^6.12.4",
|
|
85
|
+
"@codemirror/legacy-modes": "^6.5.3",
|
|
86
|
+
"@codemirror/search": "6.7.1",
|
|
87
|
+
"@codemirror/state": "^6.7.1",
|
|
88
|
+
"@codemirror/view": "^6.43.8",
|
|
89
|
+
"@lezer/highlight": "^1.2.3",
|
|
90
|
+
"vscode-icons-js": "^11.6.1"
|
|
91
|
+
},
|
|
92
|
+
"devDependencies": {
|
|
93
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
94
|
+
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
|
|
95
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
|
|
96
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
97
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",
|
|
98
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
|
|
99
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
100
|
+
"@deepseek-ai/dsh-client-web-react": "^0.1.0-rc.6",
|
|
101
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
102
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
|
103
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
104
|
+
"@types/node": "^22.0.0",
|
|
105
|
+
"@types/react": "~18.3.1",
|
|
106
|
+
"@types/react-dom": "~18.3.1",
|
|
107
|
+
"cordis": "^4.0.0-rc.7",
|
|
108
|
+
"react": "^18.2.0",
|
|
109
|
+
"react-dom": "^18.2.0",
|
|
110
|
+
"tsdown": "^0.22.2",
|
|
111
|
+
"typescript": "^5.6.0"
|
|
112
|
+
}
|
|
113
|
+
}
|