@dsh-xhl/dsh-file-explorer 0.1.3 → 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 CHANGED
@@ -4,29 +4,27 @@
4
4
 
5
5
  ## 功能
6
6
 
7
- - **标题栏 ButtonGroup**(`conversation.session.header.utilities`):左侧按钮 + 右侧下拉(编辑器 / 文件夹 / VSCode)
8
- - **Hero / 新建会话同样显示**:同一 ButtonGroup 通过框架通用浮层 `shell.overlay` 由插件自身定位到会话列右上角(hero 阶段显示,有记录后由标题栏图标接管,无重复);纯插件实现,不改动框架代码(详见 `docs/hero-filex-button.md`)
7
+ - **标题栏图标**(`conversation.session.header.utilities`):一个内置编辑器图标,点击打开文件预览 / 编辑弹窗
8
+ - **Hero / 新建会话同样显示**:同一图标通过框架通用浮层 `shell.overlay` 由插件自身定位到会话列右上角(hero 阶段显示,有记录后由标题栏图标接管,无重复);纯插件实现,不改动框架代码(详见 `docs/hero-filex-button.md`)
9
9
  - **对话内点击文件 → 编辑器预览**:聊天里的文件路径链接(工具行、产物行、正文提及)默认用系统程序打开;插件拦截 `remote.session.openWorkspacePath` 与 `workspaces.openPath` 两条通道,改为在编辑器弹窗中预览(目录或 cwd 外的路径仍落回系统打开)
10
- - **编辑器**(默认):点击打开全屏文件预览 / 编辑弹窗(Ctrl+P 亦可)
11
- - **文件夹**:点击直接打开会话工作区所在的**系统文件夹**(Windows `explorer` / macOS `open` / Linux `xdg-open`)
12
- - **VSCode**:点击用 `code` 命令行在 **VS Code** 中打开会话工作区(自动探测安装位置 / PATH)
13
- - 选择记忆在 localStorage,点击后底部提示确认打开的路径;下拉选中某项会先切换默认打开方式,随后立即执行对应的打开动作
10
+ - 打开系统文件夹 / 在外部编辑器(如 VS Code)打开工作区**由 DSH 新版壳自带**,本插件不再提供模式下拉与外部启动器,只保留内置编辑器
14
11
  - **文件列表**:左侧目录树(跳过 `node_modules`/`.git`/`dist` 等)
15
12
  - **按文件名过滤**:输入即过滤
16
13
  - **全文搜索**:正则 / 大小写 / 全词匹配、包含/排除过滤,结果按文件分组、命中高亮,点击跳转并高亮匹配
17
14
  - **编辑**:CodeMirror 6(20+ 语言语法高亮含 Vue SFC、行号、撤销/重做、Ctrl+S 保存、只读切换、Tab 缩进)
18
15
  - **预览**:图片直接显示、PDF 内嵌查看(Markdown / HTML 预览已移除——大文档渲染会卡死界面)
16
+ - **全屏**:弹窗标题栏的全屏按钮,一键把预览 / 编辑弹窗放大到整个窗口(图标转为「退出全屏」,再点一次还原)
19
17
  - **界面**:复用 Web 壳设计系统(`@deepseek-ai/dsh-client-ui-primitives`:Button / Menu / Modal / Input / Toast / Tooltip + `--dsw-*` token),随主题明暗自适应
20
18
 
21
19
  ## 架构
22
20
 
23
21
  ```
24
22
  src/
25
- index.ts Host:/filex/api JSON 路由(session.cwd / fs.list / fs.read / fs.write / fs.search / fs.reveal
23
+ index.ts Host:/filex/api JSON 路由(session.cwd / fs.list / fs.read / fs.write / fs.search)
26
24
  + /filex/file 媒体路由(图片/PDF 字节),loopback 信任围栏
27
25
  trust-fence.ts Host-header loopback 校验(DNS-rebinding 防护)
28
26
  client/
29
- index.tsx 入口:header ButtonGroup(编辑器/文件夹模式)+ overlay 弹窗 + Ctrl+P 快捷键
27
+ index.tsx 入口:header 图标(内置编辑器)+ overlay 弹窗 + Ctrl+P 快捷键
30
28
  Explorer.tsx 弹窗:文件树 / 文件名过滤 / 全文搜索 / 查看器分派(primitives 重排)
31
29
  TextEditor.tsx CodeMirror 6 编辑器 + 搜索跳转高亮
32
30
  lang.ts 扩展名 → CodeMirror 语言映射
package/cordis.patch.yml CHANGED
@@ -1,8 +1,8 @@
1
- # dsh-file-explorer bundle patch
2
- #
3
- # 通过 bundle 通道挂载:
4
- # dsh plugin --profile <name> add @dsh-xhl/dsh-file-explorer@<version>
5
- # 或手动把 '@dsh-xhl/dsh-file-explorer' 加进 profile 的 dsh.profile.bundles 并 link 依赖。
6
- - insert:
7
- - id: dsh-file-explorer
8
- name: '@dsh-xhl/dsh-file-explorer'
1
+ # dsh-file-explorer bundle patch
2
+ #
3
+ # 通过 bundle 通道挂载:
4
+ # dsh plugin --profile <name> add @dsh-xhl/dsh-file-explorer@<version>
5
+ # 或手动把 '@dsh-xhl/dsh-file-explorer' 加进 profile 的 dsh.profile.bundles 并 link 依赖。
6
+ - insert:
7
+ - id: dsh-file-explorer
8
+ name: '@dsh-xhl/dsh-file-explorer'
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,16 +37001,17 @@ 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 ButtonGroup, the modal surface).
37004
+ * native-looking pieces (the header icon, the modal surface).
37014
37005
  */
37015
37006
  const CSS = `
37016
- /* ── header ButtonGroup (utilities slot) ─────────────────────────────── */
37017
- .filex-group{display:inline-flex;align-items:center;position:relative}
37018
- .filex-group .filex-group-main{border-radius:8px 0 0 8px;padding:0 9px}
37019
- .filex-group .filex-group-trigger{margin-left:-1px;border-radius:0 8px 8px 0;border-left:0;padding:0 7px}
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}
37020
37012
 
37021
37013
  /* ── hero floating utility (shell.overlay entry) ─────────────────────── */
37022
- /* The same ButtonGroup, pinned by the plugin to the conversation column's
37014
+ /* The same icon, pinned by the plugin to the conversation column's
37023
37015
  top-right while the column is in its hero phase. The shell.overlay layer
37024
37016
  is click-through by design, so the entry opts back into pointer events;
37025
37017
  no explicit z-index keeps it below sibling overlay entries (modal,
@@ -37033,6 +37025,12 @@ window.__ModuleLoader__.load({
37033
37025
 
37034
37026
  /* ── modal surface (headless primitives Modal) ───────────────────────── */
37035
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)}
37036
37034
 
37037
37035
  /* ── explorer frame ──────────────────────────────────────────────────── */
37038
37036
  .filex-explorer{display:flex;flex-direction:column;height:100%;min-height:0}
@@ -37357,6 +37355,8 @@ window.__ModuleLoader__.load({
37357
37355
  const [jumpRanges, setJumpRanges] = (0, react.useState)(void 0);
37358
37356
  const [collapsed, setCollapsed] = (0, react.useState)(/* @__PURE__ */ new Set());
37359
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);
37360
37360
  const [sideMenu, setSideMenu] = (0, react.useState)(null);
37361
37361
  const [sideToast, setSideToast] = (0, react.useState)("");
37362
37362
  const [toastKey, setToastKey] = (0, react.useState)(0);
@@ -37714,7 +37714,7 @@ window.__ModuleLoader__.load({
37714
37714
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
37715
37715
  open: true,
37716
37716
  headless: true,
37717
- className: "filex-modal",
37717
+ className: `filex-modal${fullscreen ? " filex-modal-full" : ""}`,
37718
37718
  title: openPath ?? "文件预览 / 编辑",
37719
37719
  closeLabel: "关闭",
37720
37720
  onClose: handleClose,
@@ -37732,18 +37732,30 @@ window.__ModuleLoader__.load({
37732
37732
  })
37733
37733
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
37734
37734
  className: "filex-header-actions",
37735
- children: [savedFlash !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
37736
- className: `filex-badge${savedFlash.startsWith("保存失败") ? " filex-badge-warn" : ""}`,
37737
- children: savedFlash
37738
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
37739
- type: "button",
37740
- size: "sm",
37741
- variant: "ghost",
37742
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, {}),
37743
- title: "关闭(Esc)",
37744
- "aria-label": "关闭",
37745
- onClick: handleClose
37746
- })]
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
+ ]
37747
37759
  })]
37748
37760
  }),
37749
37761
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -38023,26 +38035,24 @@ window.__ModuleLoader__.load({
38023
38035
  //#endregion
38024
38036
  //#region src/client/index.tsx
38025
38037
  /**
38026
- * Client half of dsh-file-explorer: a header ButtonGroup that opens either
38027
- * the file-explorer modal (editor mode), the session's working folder in the
38028
- * OS file manager (folder mode), or the workspace in VS Code (vscode mode),
38029
- * 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.
38030
38041
  *
38031
- * The ButtonGroup registers into `conversation.session.header.utilities`:
38032
- * the left button carries the active mode's icon and triggers its action,
38033
- * the right button opens a dropdown to switch between 编辑器 (editor),
38034
- * 文件夹 (folder), and VSCode. Selecting an entry both persists it as the
38035
- * new default mode and immediately runs that mode's open action. The choice
38036
- * is persisted in localStorage so it survives reloads.
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.
38037
38047
  *
38038
- * The same ButtonGroup is also shown on the new-session screen through the
38048
+ * The same icon is also shown on the new-session screen through the
38039
38049
  * generic `shell.overlay` floating layer (no shell change involved): a
38040
38050
  * plugin-owned entry pins it to the conversation column's top-right while
38041
38051
  * the column is in its hero phase — the no-session hero and the
38042
38052
  * blank-session (new chat) hero, where the session header and its utilities
38043
38053
  * seat are absent or deliberately hidden. The floating entry resolves its
38044
- * session itself: the actions fall back to the live selection
38045
- * (`useSessions` → `state.current`) and show a notice when none exists.
38054
+ * session itself: the action falls back to the live selection
38055
+ * (`useSessions` → `state.current`) and shows a notice when none exists.
38046
38056
  *
38047
38057
  * Session binding: every /filex request is conversation-scoped, so the modal
38048
38058
  * must know WHICH session it belongs to — the host resolves the workspace
@@ -38058,51 +38068,11 @@ window.__ModuleLoader__.load({
38058
38068
  "remote",
38059
38069
  "remote.session"
38060
38070
  ];
38061
- const MODE_STORAGE_KEY = "dsh-file-explorer.header-mode";
38062
- /** Read the persisted mode; anything unknown defaults to the editor. */
38063
- function readStoredMode() {
38064
- try {
38065
- const stored = window.localStorage.getItem(MODE_STORAGE_KEY);
38066
- if (stored === "folder" || stored === "vscode") return stored;
38067
- return "editor";
38068
- } catch {
38069
- return "editor";
38070
- }
38071
- }
38072
- /** The official VS Code logo (brand colors), sized like the kit icons. */
38073
- function VscodeIcon(props) {
38074
- const { size = 16, className } = props;
38075
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
38076
- width: size,
38077
- height: size,
38078
- viewBox: "0 0 32 32",
38079
- className,
38080
- "aria-hidden": true,
38081
- focusable: "false",
38082
- xmlns: "http://www.w3.org/2000/svg",
38083
- children: [
38084
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
38085
- 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",
38086
- fill: "#0065a9"
38087
- }),
38088
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
38089
- 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",
38090
- fill: "#007acc"
38091
- }),
38092
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
38093
- 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",
38094
- fill: "#1f9cf0"
38095
- })
38096
- ]
38097
- });
38098
- }
38099
38071
  let ctxRef;
38100
38072
  let store = {
38101
38073
  open: false,
38102
38074
  sessionId: "",
38103
38075
  notice: null,
38104
- mode: readStoredMode(),
38105
- vscode: true,
38106
38076
  pendingPath: null,
38107
38077
  pendingSeq: 0
38108
38078
  };
@@ -38167,32 +38137,7 @@ window.__ModuleLoader__.load({
38167
38137
  };
38168
38138
  emit();
38169
38139
  }
38170
- /** Ask the host to reveal the session's working folder in the OS file manager. */
38171
- async function openSystemFolder(sessionId, cwd) {
38172
- const resolved = sessionId !== void 0 && sessionId !== "" ? sessionId : activeSessionId;
38173
- if (resolved === void 0) {
38174
- showNotice("没有可用的会话:无法确定文件工作区。请先新建/选择一个会话。");
38175
- return;
38176
- }
38177
- try {
38178
- showNotice(`已打开文件夹:${(await api.fsReveal({ sessionId: resolved }, cwd)).cwd}`);
38179
- } catch (error) {
38180
- showNotice(`打开系统文件夹失败:${error instanceof Error ? error.message : String(error)}`);
38181
- }
38182
- }
38183
- /** Ask the host to open the session's working folder in VS Code. */
38184
- async function openInVscode(sessionId, cwd) {
38185
- const resolved = sessionId !== void 0 && sessionId !== "" ? sessionId : activeSessionId;
38186
- if (resolved === void 0) {
38187
- showNotice("没有可用的会话:无法确定文件工作区。请先新建/选择一个会话。");
38188
- return;
38189
- }
38190
- try {
38191
- showNotice(`已在 VS Code 打开:${(await api.fsVscode({ sessionId: resolved }, cwd)).cwd}`);
38192
- } catch (error) {
38193
- showNotice(`用 VS Code 打开失败:${error instanceof Error ? error.message : String(error)}`);
38194
- }
38195
- }
38140
+ /** Close the modal (session binding stays for the next open). */
38196
38141
  function closeExplorer() {
38197
38142
  store = {
38198
38143
  ...store,
@@ -38200,122 +38145,25 @@ window.__ModuleLoader__.load({
38200
38145
  };
38201
38146
  emit();
38202
38147
  }
38203
- function setMode(mode) {
38204
- if (store.mode === mode) return;
38205
- store = {
38206
- ...store,
38207
- mode
38208
- };
38209
- emit();
38210
- try {
38211
- window.localStorage.setItem(MODE_STORAGE_KEY, mode);
38212
- } catch {}
38213
- }
38214
- /**
38215
- * Probe the host once per activation for VS Code availability and hide the
38216
- * VSCode option (falling back to the editor) when it is missing — the user
38217
- * explicitly wants the entry hidden rather than shown-but-broken.
38218
- */
38219
- function probeVscode() {
38220
- api.fsCapabilities().then((result) => {
38221
- store = {
38222
- ...store,
38223
- vscode: result.vscode
38224
- };
38225
- if (!result.vscode && store.mode === "vscode") store = {
38226
- ...store,
38227
- mode: "editor"
38228
- };
38229
- emit();
38230
- }).catch(() => {
38231
- store = {
38232
- ...store,
38233
- vscode: false
38234
- };
38235
- if (store.mode === "vscode") store = {
38236
- ...store,
38237
- mode: "editor"
38238
- };
38239
- emit();
38240
- });
38241
- }
38242
- const MODE_ITEMS = [
38243
- {
38244
- id: "editor",
38245
- label: "编辑器",
38246
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, {})
38247
- },
38248
- {
38249
- id: "folder",
38250
- label: "文件夹",
38251
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpen16, {})
38252
- },
38253
- {
38254
- id: "vscode",
38255
- label: "VSCode",
38256
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VscodeIcon, {})
38257
- }
38258
- ];
38259
- /** The header ButtonGroup (session header utilities slot; receives sessionId). */
38148
+ /** The header icon: the built-in file explorer (preview / edit modal). */
38260
38149
  function HeaderGroup(props) {
38261
- const state = (0, react.useSyncExternalStore)(subscribe, getSnapshot);
38262
- const [menuOpen, setMenuOpen] = (0, react.useState)(false);
38263
- const sessionCwd = (props.useSessions ?? (() => void 0))((s) => props.sessionId !== void 0 ? s.byId?.[props.sessionId]?.cwd : void 0);
38264
- const mode = state.mode;
38265
- const items = state.vscode ? MODE_ITEMS : MODE_ITEMS.filter((item) => item.id !== "vscode");
38266
- 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, {});
38267
- const mainTitle = mode === "folder" ? "打开系统文件夹" : mode === "vscode" ? "用 VSCode 打开工作区" : "文件预览 / 编辑(Ctrl+P)";
38268
- const onMainClick = (e) => {
38269
- e.stopPropagation();
38270
- if (mode === "folder") openSystemFolder(props.sessionId, sessionCwd);
38271
- else if (mode === "vscode") openInVscode(props.sessionId, sessionCwd);
38272
- else openExplorer(props.sessionId);
38273
- };
38150
+ const title = "文件预览 / 编辑(Ctrl+P)";
38274
38151
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
38275
- label: mainTitle,
38152
+ label: title,
38276
38153
  side: "bottom",
38277
38154
  delayMs: 400,
38278
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
38279
- open: menuOpen,
38280
- align: "end",
38281
- portal: true,
38282
- compact: true,
38283
- selectedId: mode,
38284
- items,
38285
- onSelect: (id) => {
38286
- const next = id === "folder" || id === "vscode" ? id : "editor";
38287
- setMode(next);
38288
- setMenuOpen(false);
38289
- if (next === "folder") openSystemFolder(props.sessionId, sessionCwd);
38290
- else if (next === "vscode") openInVscode(props.sessionId, sessionCwd);
38291
- else openExplorer(props.sessionId);
38292
- },
38293
- onClose: () => setMenuOpen(false),
38294
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
38295
- className: "filex-group",
38296
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
38297
- type: "button",
38298
- className: "filex-group-main",
38299
- size: "sm",
38300
- variant: "outline",
38301
- icon: mainIcon,
38302
- title: mainTitle,
38303
- "aria-label": mainTitle,
38304
- onClick: onMainClick
38305
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
38306
- type: "button",
38307
- className: "filex-group-trigger",
38308
- size: "sm",
38309
- variant: "outline",
38310
- title: "选择打开方式:编辑器 / 文件夹 / VSCode",
38311
- "aria-label": "选择打开方式",
38312
- onClick: (e) => {
38313
- e.stopPropagation();
38314
- setMenuOpen((v) => !v);
38315
- },
38316
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
38317
- })]
38318
- })
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
+ }
38319
38167
  })
38320
38168
  });
38321
38169
  }
@@ -38329,10 +38177,10 @@ window.__ModuleLoader__.load({
38329
38177
  * hidden); the position is measured from the rendered column root
38330
38178
  * (`[data-phase]`), so sidebar collapse and details-panel toggles are
38331
38179
  * tracked automatically. Clicking behaves exactly like the header icon: in a
38332
- * blank-session hero the actions bind to that session, and with no session
38333
- * at all they surface the no-session notice.
38180
+ * blank-session hero the action binds to that session, and with no session
38181
+ * at all it surfaces the no-session notice.
38334
38182
  */
38335
- function HeroFilexButton(props) {
38183
+ function HeroFilexButton() {
38336
38184
  const [pos, setPos] = (0, react.useState)(null);
38337
38185
  (0, react.useEffect)(() => {
38338
38186
  const update = () => {
@@ -38368,7 +38216,7 @@ window.__ModuleLoader__.load({
38368
38216
  top: pos.top,
38369
38217
  right: pos.right
38370
38218
  },
38371
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HeaderGroup, { useSessions: props.useSessions })
38219
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HeaderGroup, {})
38372
38220
  });
38373
38221
  }
38374
38222
  /** The overlay entry: keyboard shortcut + modal + transient notice. */
@@ -38464,7 +38312,6 @@ window.__ModuleLoader__.load({
38464
38312
  heroFab();
38465
38313
  };
38466
38314
  });
38467
- probeVscode();
38468
38315
  ctx.effect(() => {
38469
38316
  const deps = {
38470
38317
  currentSessionId: () => activeSessionId,
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-xhl/dsh-file-explorer",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "DSH 外部编译型插件:工作区文件预览 / 编辑(CodeMirror 6 编辑器 + 文件树 + 全文搜索 + Markdown/HTML/图片/PDF 预览)。移植自同目录下的动态 Cordis 插件版本。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",