@dsh-xhl/dsh-file-explorer 0.1.8 → 0.2.0

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
@@ -5,12 +5,14 @@
5
5
  ## 功能
6
6
 
7
7
  - **聊天页标题栏图标**(`conversation.session.header.utilities`):一个内置编辑器图标,点击打开文件预览 / 编辑弹窗(聊天页保持与现状一致,无下拉)
8
- - **Hero / 新建会话页 ButtonGroup**:同一浮层位置由插件定位到会话列右上角(hero 阶段显示,有记录后由标题栏图标接管,无重复);hero 上为**左主按钮 + 右下拉**的组合:
8
+ - **Hero / 新建会话页 ButtonGroup**:通过 `@dsh-xhl/dsh-hero-flex` 插件声明的
9
+ `hero.flex` 槽位渲染(**未安装该插件则不显示**);hero 上为**左主按钮 +
10
+ 右下拉**的组合:
9
11
  - **编辑器**(默认):点击打开文件预览 / 编辑弹窗(Ctrl+P 亦可)
10
12
  - **文件夹**:点击直接打开会话工作区所在的**系统文件夹**(Windows `explorer` / macOS `open` / Linux `xdg-open`)
11
13
  - **VSCode**:点击用 `code` 命令行在 **VS Code** 中打开会话工作区(自动探测安装位置 / PATH;宿主明确报告未安装时该项自动隐藏,探测请求本身失败则保持可见并退避重试)
12
14
  - 选择记忆在 localStorage,点击后底部提示确认打开的路径;下拉选中某项会先切换默认打开方式,随后立即执行对应的打开动作
13
- - 纯插件实现,不改动框架代码(详见 `docs/hero-filex-button.md`)
15
+ - 纯插件实现,不改动框架代码;hero 按钮组渲染依赖独立插件 `@dsh-xhl/dsh-hero-flex`(详见 `docs/hero-filex-button.md`)
14
16
  - **对话内点击文件 → 编辑器预览**:聊天里的文件路径链接(工具行、产物行、正文提及)默认用系统程序打开;插件拦截 `remote.session.openWorkspacePath` 与 `workspaces.openPath` 两条通道,改为在编辑器弹窗中预览(目录或 cwd 外的路径仍落回系统打开)
15
17
  - **文件列表**:左侧目录树(跳过 `node_modules`/`.git`/`dist` 等)
16
18
  - **按文件名过滤**:输入即过滤
@@ -28,7 +30,7 @@ src/
28
30
  + /filex/file 媒体路由(图片/PDF 字节),loopback 信任围栏
29
31
  trust-fence.ts Host-header loopback 校验(DNS-rebinding 防护)
30
32
  client/
31
- index.tsx 入口:聊天页 header 图标(内置编辑器)+ hero 模式 ButtonGroup + overlay 弹窗 + Ctrl+P 快捷键
33
+ index.tsx 入口:聊天页 header 图标(内置编辑器)+ hero 模式 ButtonGroup(hero.flex 槽位)+ overlay 弹窗 + Ctrl+P 快捷键
32
34
  Explorer.tsx 弹窗:文件树 / 文件名过滤 / 全文搜索 / 查看器分派(primitives 重排)
33
35
  TextEditor.tsx CodeMirror 6 编辑器 + 搜索跳转高亮
34
36
  lang.ts 扩展名 → CodeMirror 语言映射
@@ -41,9 +43,27 @@ src/
41
43
 
42
44
  ```bash
43
45
  pnpm install
44
- pnpm build # lib/index.js(host)+ lib/client.js(client bundle,CodeMirror 内联)
46
+ pnpm build # lib/index.js(host)+ lib/client.js(client bundle,CodeMirror 内联)+ 图标校验
45
47
  ```
46
48
 
49
+ > **图标命名陷阱(务必先读)**:宿主 `@deepseek-ai/dsh-client-ui-primitives`
50
+ > 的图标导出已从**尺寸后缀**(`Icon*16` / `Icon*14`)改为**字重后缀**
51
+ > (`Icon*Regular` / `Icon*Medium`),旧名在宿主里**一个都不存在**。
52
+ >
53
+ > 这个失败模式极其隐蔽:客户端 bundle 通过冻结的模块表解析原语,缺失的名字
54
+ > 拿到的是 `undefined`,React 在渲染时抛 "Element type is invalid",而槽位的
55
+ > 错误边界会**静默退位(abdicate)**——插件看起来已安装、已注册,却什么都不渲染。
56
+ >
57
+ > 曾经的坑在于开发依赖停留在 `0.1.0-rc.6`(仍是旧命名),于是 `tsc` 放行、
58
+ > 构建成功,只有运行时才炸。现在 `pnpm build` 末尾的
59
+ > [`scripts/check-icons.mjs`](scripts/check-icons.mjs) 会把产物引用的图标与
60
+ > **宿主真实导出**逐一比对,对不上直接构建失败。
61
+ >
62
+ > 若 `pnpm typecheck` 报某图标不存在,多半是本地 `node_modules` 里的原语包
63
+ > 落后于宿主:把 `node_modules/@deepseek-ai/dsh-client-ui-primitives` 指向
64
+ > 宿主实际安装的那份(`$env:LOCALAPPDATA\Volta\tools\image\packages\@deepseek-ai\dsh\node_modules\@deepseek-ai\dsh\node_modules\@deepseek-ai\dsh-client-ui-primitives`)
65
+ > 再重试。
66
+
47
67
  > 构建采用**原地覆盖**(不再先删除 `lib/`)。宿主半在进程启动时载入内存,而 profile
48
68
  > 若 link 到本工作区则会直读磁盘:构建中途清空目录会让 HMR 读到空 bundle,导致
49
69
  > `/filex` 路由与客户端 import 瞬时失败(表现为 `unknown filex API method ...`)。
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
@@ -36902,7 +36902,7 @@ window.__ModuleLoader__.load({
36902
36902
  children: ctxMenu.empty ? "请先在编辑器中选中文本" : reference(ctxMenu)
36903
36903
  })]
36904
36904
  }),
36905
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRightUpOutline16, {}),
36905
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRightUpOutlineRegular, {}),
36906
36906
  disabled: ctxMenu.empty
36907
36907
  }, {
36908
36908
  id: "insertFile",
@@ -36913,7 +36913,7 @@ window.__ModuleLoader__.load({
36913
36913
  children: reference()
36914
36914
  })]
36915
36915
  }),
36916
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRightUpOutline16, {})
36916
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRightUpOutlineRegular, {})
36917
36917
  }] : [];
36918
36918
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
36919
36919
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -36993,7 +36993,7 @@ window.__ModuleLoader__.load({
36993
36993
  }),
36994
36994
  insertError !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
36995
36995
  text: `插入聊天框失败:${insertError}`,
36996
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, {}),
36996
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, {}),
36997
36997
  onDone: () => setInsertError("")
36998
36998
  }, toastKey)
36999
36999
  ] });
@@ -37020,40 +37020,19 @@ window.__ModuleLoader__.load({
37020
37020
  down. An 8px radius keeps it a rounded square rather than a full circle. */
37021
37021
  .filex-header-btn{width:28px;height:28px;padding:0;border-radius:8px}
37022
37022
 
37023
- /* ── hero ButtonGroup (shell.overlay entry) ──────────────────────────── */
37024
- /* The mode group shown only on the hero / new-session screen: the left
37023
+ /* ── hero ButtonGroup (hero.flex slot entry) ─────────────────────────── */
37024
+ /* The mode group shown on the hero / blank-session screen: the left
37025
37025
  button carries the remembered mode's icon, the right chevron the mode
37026
37026
  dropdown. Outer corners use the same rounded-rectangle radius as the
37027
37027
  header buttons (8px on a 28px-tall button), so the group reads as one
37028
- pill consistent with the session header icons. */
37028
+ pill consistent with the session header icons. The host is the
37029
+ dsh-hero-flex plugin's hf-row (its own stylesheet); this sheet only
37030
+ styles the group itself. */
37029
37031
  .filex-group{display:inline-flex;align-items:center;position:relative;height:28px}
37030
37032
  .filex-group .filex-group-main,.filex-group .filex-group-trigger{height:28px}
37031
37033
  .filex-group .filex-group-main{border-radius:8px 0 0 8px;padding:0 9px}
37032
37034
  .filex-group .filex-group-trigger{margin-left:-1px;border-radius:0 8px 8px 0;border-left:0;padding:0 7px}
37033
37035
 
37034
- /* ── header corner seat (new runtime) ────────────────────────────────── */
37035
- /* The corner occupant: the hero ButtonGroup directly left of the right-
37036
- sidebar expand button in one flex row — the same corner seat the shell's
37037
- expand button occupies alone once the header chrome is shown, now shared
37038
- with the plugin's open-actions group. The seat's own margin and the
37039
- header's padding position the row at the far right of the blank header. */
37040
- .filex-corner{display:flex;align-items:center;gap:8px}
37041
- /* Re-painted expand button, same footprint as the shell's ExpandButton
37042
- (28×28, 15px mirrored panel icon) but with a rounded-rectangle corner to
37043
- match the editor button beside it; visible only while the panel is
37044
- collapsed. */
37045
- .filex-corner-expand{width:28px;height:28px;color:var(--dsw-alias-label-secondary,#9ca3af);cursor:pointer;background:transparent;border:none;border-radius:8px;flex:none;justify-content:center;align-items:center;padding:6px;display:inline-flex}
37046
- .filex-corner-expand:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.15))}
37047
- .filex-corner-expand-icon{transform:scaleX(-1)}
37048
-
37049
- /* ── hero floating utility (shell.overlay entry) ─────────────────────── */
37050
- /* The ButtonGroup, pinned by the plugin to the conversation column's
37051
- top-right while the column is in its hero phase. The shell.overlay layer
37052
- is click-through by design, so the entry opts back into pointer events;
37053
- no explicit z-index keeps it below sibling overlay entries (modal,
37054
- notices) and any body-portaled popup. */
37055
- .filex-hero-fab{position:fixed;pointer-events:auto}
37056
-
37057
37036
  /* ── transient notice strip ──────────────────────────────────────────── */
37058
37037
  .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}
37059
37038
  .filex-notice-btn{border:0;background:transparent;color:inherit;cursor:pointer;font-size:14px;line-height:1;padding:2px 4px;opacity:.7;flex-shrink:0}
@@ -37311,7 +37290,7 @@ window.__ModuleLoader__.load({
37311
37290
  onContextMenu(node.relPath, e.clientX, e.clientY);
37312
37291
  },
37313
37292
  children: [
37314
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, { className: "filex-chev" }),
37293
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "filex-chev" }),
37315
37294
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FileIcon, {
37316
37295
  kind: "dir",
37317
37296
  name: node.name,
@@ -37574,7 +37553,7 @@ window.__ModuleLoader__.load({
37574
37553
  children: `@file:${sideMenu.relPath} lines:${sideMenu.line}`
37575
37554
  })]
37576
37555
  }),
37577
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRightUpOutline16, {})
37556
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRightUpOutlineRegular, {})
37578
37557
  }] : [], {
37579
37558
  id: "insertFile",
37580
37559
  label: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -37584,7 +37563,7 @@ window.__ModuleLoader__.load({
37584
37563
  children: `@file:${sideMenu.relPath}`
37585
37564
  })]
37586
37565
  }),
37587
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRightUpOutline16, {})
37566
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRightUpOutlineRegular, {})
37588
37567
  }] : [];
37589
37568
  let sideContent;
37590
37569
  if (searchMode === "name") {
@@ -37777,7 +37756,7 @@ window.__ModuleLoader__.load({
37777
37756
  type: "button",
37778
37757
  size: "sm",
37779
37758
  variant: "ghost",
37780
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFullscreenOutline16, { className: fullscreen ? "filex-fs-on" : void 0 }),
37759
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFullscreenOutlineRegular, { className: fullscreen ? "filex-fs-on" : void 0 }),
37781
37760
  title: fullscreen ? "退出全屏" : "全屏",
37782
37761
  "aria-label": fullscreen ? "退出全屏" : "全屏",
37783
37762
  onClick: () => setFullscreen((v) => !v)
@@ -37786,7 +37765,7 @@ window.__ModuleLoader__.load({
37786
37765
  type: "button",
37787
37766
  size: "sm",
37788
37767
  variant: "ghost",
37789
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, {}),
37768
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutlineRegular, {}),
37790
37769
  title: "关闭(Esc)",
37791
37770
  "aria-label": "关闭",
37792
37771
  onClick: handleClose
@@ -37824,7 +37803,7 @@ window.__ModuleLoader__.load({
37824
37803
  type: "button",
37825
37804
  size: "sm",
37826
37805
  variant: "outline",
37827
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline14, {}),
37806
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutlineRegular, {}),
37828
37807
  disabled: loading,
37829
37808
  onClick: () => void refresh(),
37830
37809
  title: "刷新文件列表",
@@ -37985,7 +37964,7 @@ window.__ModuleLoader__.load({
37985
37964
  }),
37986
37965
  sideToast !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
37987
37966
  text: `插入聊天框失败:${sideToast}`,
37988
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, {}),
37967
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, {}),
37989
37968
  onDone: () => setSideToast("")
37990
37969
  }, toastKey)
37991
37970
  ] });
@@ -38075,19 +38054,17 @@ window.__ModuleLoader__.load({
38075
38054
  *
38076
38055
  * The in-chat session header (`conversation.session.header.utilities`) keeps
38077
38056
  * a single built-in-editor icon: clicking it opens the file-explorer modal
38078
- * (preview / edit). The hero / new-session floating utility rendered by the
38079
- * plugin itself through the generic `shell.overlay` layer carries the full
38080
- * ButtonGroup — the left button performs the remembered open action
38081
- * (内置编辑器 / 系统文件夹 / VS Code) and the right chevron opens a dropdown
38082
- * to switch between them. Selecting an entry both persists it as the new
38083
- * default and immediately runs that action.
38057
+ * (preview / edit).
38084
38058
  *
38085
- * The floating entry pins itself to the conversation column's top-right
38086
- * while the column is in its hero phase — the no-session hero and the
38087
- * blank-session (new chat) hero, where the session header and its utilities
38088
- * seat are absent or deliberately hidden. It resolves its session itself:
38089
- * the actions fall back to the live selection (`useSessions` →
38090
- * `state.current`) and show a notice when none exists.
38059
+ * The hero / blank-session open-actions ButtonGroup (编辑器 / 系统文件夹 /
38060
+ * VS Code) renders ONLY through the `hero.flex` slot declared by the
38061
+ * dsh-hero-flex plugin, which owns the header's corner seat at priority -1
38062
+ * and hosts the additive flex row beside the right-sidebar expand button.
38063
+ * Without dsh-hero-flex installed the ButtonGroup does not render at all —
38064
+ * this plugin no longer occupies the corner seat nor floats a fallback
38065
+ * overlay. The left button performs the remembered open action and the right
38066
+ * chevron opens a dropdown to switch between them; selecting an entry both
38067
+ * persists it as the new default and immediately runs that action.
38091
38068
  *
38092
38069
  * Session binding: every /filex request is conversation-scoped, so the modal
38093
38070
  * must know WHICH session it belongs to — the host resolves the workspace
@@ -38095,6 +38072,25 @@ window.__ModuleLoader__.load({
38095
38072
  * the framework-resolved sessionId; Ctrl+P falls back to the currently
38096
38073
  * selected session (`useSessions` → `state.current`).
38097
38074
  */
38075
+ /**
38076
+ * Icon primitives are resolved through the host's frozen module table, so a
38077
+ * name the host no longer exports arrives as `undefined` and React throws
38078
+ * "Element type is invalid" at render time. Inside a slot that throw is
38079
+ * swallowed by the slot error boundary, which silently abdicates the seat —
38080
+ * the plugin looks installed and registered while rendering nothing. Fail
38081
+ * loudly at load instead, where the message is still visible.
38082
+ */
38083
+ function assertPrimitives() {
38084
+ const missing = Object.entries({
38085
+ Button: _deepseek_ai_dsh_client_ui_primitives.Button,
38086
+ Menu: _deepseek_ai_dsh_client_ui_primitives.Menu,
38087
+ Tooltip: _deepseek_ai_dsh_client_ui_primitives.Tooltip,
38088
+ IconChevronDownOutlineRegular: _deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular,
38089
+ IconEditOutlineRegular: _deepseek_ai_dsh_client_ui_primitives.IconEditOutlineRegular,
38090
+ IconFolderOpenRegular: _deepseek_ai_dsh_client_ui_primitives.IconFolderOpenRegular
38091
+ }).filter(([, v]) => v === void 0).map(([k]) => k);
38092
+ if (missing.length > 0) throw new Error(`[dsh-file-explorer] 宿主 @deepseek-ai/dsh-client-ui-primitives 缺少: ${missing.join(", ")}。图标导出命名已从尺寸后缀(16/14)改为字重后缀(Regular/Medium),请同步更新源码引用。`);
38093
+ }
38098
38094
  /** Services required before mounting. */
38099
38095
  const inject = [
38100
38096
  "slots",
@@ -38315,12 +38311,12 @@ window.__ModuleLoader__.load({
38315
38311
  {
38316
38312
  id: "editor",
38317
38313
  label: "编辑器",
38318
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, {})
38314
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutlineRegular, {})
38319
38315
  },
38320
38316
  {
38321
38317
  id: "folder",
38322
38318
  label: "文件夹",
38323
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpen16, {})
38319
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenRegular, {})
38324
38320
  },
38325
38321
  {
38326
38322
  id: "vscode",
@@ -38340,7 +38336,7 @@ window.__ModuleLoader__.load({
38340
38336
  className: "filex-header-btn",
38341
38337
  size: "sm",
38342
38338
  variant: "outline",
38343
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, {}),
38339
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutlineRegular, {}),
38344
38340
  title,
38345
38341
  "aria-label": "文件预览 / 编辑",
38346
38342
  onClick: (e) => {
@@ -38352,25 +38348,24 @@ window.__ModuleLoader__.load({
38352
38348
  }
38353
38349
  /**
38354
38350
  * The hero ButtonGroup: the left button carries the remembered mode's icon and
38355
- * runs its action, the right chevron opens the mode dropdown. Rendered on the
38356
- * hero / new-session screen (both the corner seat on the new runtime and the
38357
- * floating fallback) — the in-chat header keeps the single built-in-editor
38351
+ * runs its action, the right chevron opens the mode dropdown. Rendered only
38352
+ * through the `hero.flex` slot declared by the dsh-hero-flex plugin (hero /
38353
+ * blank-session header) — the in-chat header keeps the single built-in-editor
38358
38354
  * `HeaderIcon`.
38359
38355
  *
38360
- * `sessionId` is optional: the corner seat supplies it from the session
38361
- * standard kit; the floating fallback (root-scoped overlay) resolves it from
38362
- * `useSessions` → `state.current` instead.
38356
+ * `sessionId` comes from the slot's session standard kit; without it the
38357
+ * actions fall back to the live selection (`useSessions` → `state.current`)
38358
+ * and show a notice when none exists.
38363
38359
  */
38364
38360
  function HeroGroup(props) {
38365
38361
  const state = (0, react.useSyncExternalStore)(subscribe, getSnapshot);
38366
38362
  const [menuOpen, setMenuOpen] = (0, react.useState)(false);
38367
38363
  const useSessions = props.useSessions ?? (() => void 0);
38368
- const current = useSessions((s) => s.current);
38369
- const sessionId = props.sessionId ?? current;
38364
+ const sessionId = props.sessionId;
38370
38365
  const sessionCwd = useSessions((s) => sessionId !== void 0 ? s.byId?.[sessionId]?.cwd : void 0);
38371
38366
  const mode = state.mode;
38372
38367
  const items = state.vscode ? MODE_ITEMS : MODE_ITEMS.filter((item) => item.id !== "vscode");
38373
- 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, {});
38368
+ const mainIcon = mode === "folder" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenRegular, {}) : mode === "vscode" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VscodeIcon, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutlineRegular, {});
38374
38369
  const mainTitle = mode === "folder" ? "打开系统文件夹" : mode === "vscode" ? "用 VSCode 打开工作区" : "文件预览 / 编辑(Ctrl+P)";
38375
38370
  const onMainClick = (e) => {
38376
38371
  e.stopPropagation();
@@ -38420,169 +38415,12 @@ window.__ModuleLoader__.load({
38420
38415
  e.stopPropagation();
38421
38416
  setMenuOpen((v) => !v);
38422
38417
  },
38423
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
38418
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, {})
38424
38419
  })]
38425
38420
  })
38426
38421
  })
38427
38422
  });
38428
38423
  }
38429
- /**
38430
- * Corner expand button re-painted beside the explorer ButtonGroup while this
38431
- * plugin occupies the header's corner seat. Matches the shell's `ExpandButton`
38432
- * (28×28, mirrored panel icon, tooltip); the toggle goes through the
38433
- * cross-plugin `sidebarRight` face. Hides itself while the panel is open,
38434
- * exactly like the shipped button — the DOM probe (`data-sidebar-right-open`)
38435
- * is the same attribute the shell's panel root carries, so it tracks even
38436
- * opens that did not come from this button.
38437
- */
38438
- function CornerExpandButton(props) {
38439
- const [expanded, setExpanded] = (0, react.useState)(false);
38440
- (0, react.useEffect)(() => {
38441
- const probe = () => {
38442
- setExpanded(document.querySelector("[data-sidebar-right-open]") !== null);
38443
- };
38444
- probe();
38445
- const observer = new MutationObserver(probe);
38446
- observer.observe(document.body, {
38447
- childList: true,
38448
- subtree: true,
38449
- attributes: true,
38450
- attributeFilter: ["data-sidebar-right-open"]
38451
- });
38452
- window.addEventListener("resize", probe);
38453
- return () => {
38454
- observer.disconnect();
38455
- window.removeEventListener("resize", probe);
38456
- };
38457
- }, []);
38458
- if (expanded) return null;
38459
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
38460
- label: "展开右侧栏",
38461
- side: "bottom",
38462
- delayMs: 500,
38463
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
38464
- type: "button",
38465
- className: "filex-corner-expand",
38466
- "aria-label": "展开右侧栏",
38467
- "data-sidebar-right-expand": "",
38468
- onClick: () => {
38469
- props.sidebar.toggleExpanded();
38470
- },
38471
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPanelLeftOutline16, { className: "filex-corner-expand-icon" })
38472
- })
38473
- });
38474
- }
38475
- /**
38476
- * The header's corner seat while the new-session header chrome is blank: the
38477
- * explorer ButtonGroup sits directly left of the expand button — the same
38478
- * corner seat, one flex row, no floating layer, no measuring. In a session
38479
- * with records the `utilities` seat renders (where the editor icon lives), so
38480
- * only the expand button is re-painted here; the DOM probe keeps the pair
38481
- * from ever duplicating.
38482
- */
38483
- function HeaderCornerGroup(props) {
38484
- const [utilitiesPresent, setUtilitiesPresent] = (0, react.useState)(false);
38485
- const [sidebar, setSidebar] = (0, react.useState)(void 0);
38486
- (0, react.useEffect)(() => {
38487
- let alive = true;
38488
- let tries = 0;
38489
- const read = () => {
38490
- if (!alive) return;
38491
- let value;
38492
- try {
38493
- value = ctxRef?.get?.("sidebarRight", false) ?? ctxRef?.sidebarRight;
38494
- } catch {
38495
- value = void 0;
38496
- }
38497
- if (value !== void 0) {
38498
- setSidebar(value);
38499
- return;
38500
- }
38501
- tries += 1;
38502
- if (tries < 40) setTimeout(read, 250);
38503
- };
38504
- read();
38505
- return () => {
38506
- alive = false;
38507
- };
38508
- }, []);
38509
- (0, react.useEffect)(() => {
38510
- const probe = () => {
38511
- setUtilitiesPresent(document.querySelector("[data-slot=\"conversation.session.header.utilities\"]") !== null);
38512
- };
38513
- probe();
38514
- const observer = new MutationObserver(probe);
38515
- observer.observe(document.body, {
38516
- childList: true,
38517
- subtree: true
38518
- });
38519
- return () => observer.disconnect();
38520
- }, []);
38521
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
38522
- className: "filex-corner",
38523
- children: [!utilitiesPresent && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HeroGroup, { sessionId: props.sessionId }), sidebar !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CornerExpandButton, { sidebar })]
38524
- });
38525
- }
38526
- /**
38527
- * Hero / new-session floating utility: the hero ButtonGroup, rendered by the
38528
- * plugin itself through the generic `shell.overlay` floating layer (no shell
38529
- * change needed) and pinned to the conversation column's top-right corner —
38530
- * the spot where the session-header utilities sit once a conversation has
38531
- * records. Shown only while the conversation column is in its `hero` phase
38532
- * (no session at all, or a blank session whose header is deliberately
38533
- * hidden); the position is measured from the rendered column root
38534
- * (`[data-phase]`), so sidebar collapse and details-panel toggles are
38535
- * tracked automatically. Clicking behaves exactly like the in-chat icon: in a
38536
- * blank-session hero the actions bind to that session, and with no session
38537
- * at all they surface the no-session notice.
38538
- *
38539
- * On the new runtime the corner seat carries the same ButtonGroup, so this
38540
- * fallback hides itself the moment `.filex-corner` is rendered — the two
38541
- * paths never overlap.
38542
- */
38543
- function HeroFilexButton(props) {
38544
- const [pos, setPos] = (0, react.useState)(null);
38545
- (0, react.useEffect)(() => {
38546
- const update = () => {
38547
- if (document.querySelector(".filex-corner") !== null) {
38548
- setPos(null);
38549
- return;
38550
- }
38551
- const column = document.querySelector("[data-phase]");
38552
- if (column === null || column.getAttribute("data-phase") !== "hero") {
38553
- setPos(null);
38554
- return;
38555
- }
38556
- const rect = column.getBoundingClientRect();
38557
- if (rect.width === 0 || rect.height === 0) {
38558
- setPos(null);
38559
- return;
38560
- }
38561
- const top = rect.top + 14;
38562
- const right = window.innerWidth - rect.right + 28;
38563
- setPos((current) => current !== null && current.top === top && current.right === right ? current : {
38564
- top,
38565
- right
38566
- });
38567
- };
38568
- const timer = window.setInterval(update, 400);
38569
- window.addEventListener("resize", update);
38570
- update();
38571
- return () => {
38572
- window.clearInterval(timer);
38573
- window.removeEventListener("resize", update);
38574
- };
38575
- }, []);
38576
- if (pos === null) return null;
38577
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
38578
- className: "filex-hero-fab",
38579
- style: {
38580
- top: pos.top,
38581
- right: pos.right
38582
- },
38583
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HeroGroup, { useSessions: props.useSessions })
38584
- });
38585
- }
38586
38424
  /** The overlay entry: keyboard shortcut + modal + transient notice. */
38587
38425
  function ExplorerOverlay(props) {
38588
38426
  const state = (0, react.useSyncExternalStore)(subscribe, getSnapshot);
@@ -38637,6 +38475,7 @@ window.__ModuleLoader__.load({
38637
38475
  } catch {}
38638
38476
  };
38639
38477
  try {
38478
+ assertPrimitives();
38640
38479
  const tagId = "dsh-file-explorer/styles";
38641
38480
  ctx.effect(() => {
38642
38481
  const dark = detectDark();
@@ -38658,29 +38497,21 @@ window.__ModuleLoader__.load({
38658
38497
  order: 10,
38659
38498
  label: "文件预览 / 编辑"
38660
38499
  }, HeaderIcon));
38661
- ctx.slots.inject("shell.overlay", () => {
38662
- const overlay = ctx.slots.register({
38663
- name: "shell.overlay",
38664
- id: "file-explorer-overlay",
38665
- order: 100,
38666
- label: "文件预览"
38667
- }, ExplorerOverlay);
38668
- const heroFab = ctx.slots.register({
38669
- name: "shell.overlay",
38670
- id: "file-explorer-hero-fab",
38671
- order: 90,
38672
- label: "文件预览 / 编辑(hero)"
38673
- }, HeroFilexButton);
38674
- return () => {
38675
- overlay();
38676
- heroFab();
38677
- };
38678
- });
38679
- ctx.slots.inject("conversation.session.header.corner", () => ctx.slots.register({
38680
- name: "conversation.session.header.corner",
38681
- priority: -1,
38682
- label: "文件预览 / 编辑(角落)"
38683
- }, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HeaderCornerGroup, { sessionId: props.sessionId })));
38500
+ ctx.slots.inject("shell.overlay", () => ctx.slots.register({
38501
+ name: "shell.overlay",
38502
+ id: "file-explorer-overlay",
38503
+ order: 100,
38504
+ label: "文件预览"
38505
+ }, ExplorerOverlay));
38506
+ ctx.slots.inject("hero.flex", () => ctx.slots.register({
38507
+ name: "hero.flex",
38508
+ id: "file-explorer",
38509
+ order: 10,
38510
+ label: "文件预览 / 编辑(hero)"
38511
+ }, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HeroGroup, {
38512
+ sessionId: props.sessionId,
38513
+ useSessions: props.useSessions
38514
+ })));
38684
38515
  probeVscode();
38685
38516
  ctx.effect(() => {
38686
38517
  const deps = {
@@ -117,16 +117,6 @@ export interface FilexSlotsService {
117
117
  }, component: unknown): () => void;
118
118
  inject(key: string, callback: () => () => void): () => void;
119
119
  }
120
- /**
121
- * The cross-plugin right-sidebar face (`dsh-client-ui-sidebar-right` provides
122
- * it as `ctx.sidebarRight`). Only the expand-toggle and expanded-state read
123
- * are used here, to re-paint the header's corner expand button beside the
124
- * explorer ButtonGroup while the plugin occupies the corner seat.
125
- */
126
- export interface FilexSidebarRight {
127
- isExpanded(): boolean;
128
- toggleExpanded(): void;
129
- }
130
120
  declare module 'cordis' {
131
121
  interface Context {
132
122
  webServer: FilexWebServer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-xhl/dsh-file-explorer",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "DSH 外部编译型插件:工作区文件预览 / 编辑(CodeMirror 6 编辑器 + 文件树 + 全文搜索 + Markdown/HTML/图片/PDF 预览)。移植自同目录下的动态 Cordis 插件版本。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -41,9 +41,10 @@
41
41
  "README.md"
42
42
  ],
43
43
  "scripts": {
44
- "build": "tsc -p tsconfig.build.json && tsdown",
44
+ "build": "tsc -p tsconfig.build.json && tsdown && node scripts/check-icons.mjs",
45
45
  "typecheck": "tsc --noEmit",
46
46
  "bundle": "tsdown",
47
+ "check:icons": "node scripts/check-icons.mjs",
47
48
  "prepare": "tsdown"
48
49
  },
49
50
  "license": "MIT",