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

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/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
@@ -37014,21 +37014,37 @@ window.__ModuleLoader__.load({
37014
37014
  */
37015
37015
  const CSS = `
37016
37016
  /* ── header icon (utilities slot) ────────────────────────────────────── */
37017
- /* Same 28×28 round button as dsh-git-gui's header entry (.gg-header-btn):
37018
- the primitive sm button is already 28px tall with a 16px icon, so only
37019
- the width / padding / radius need pinning down. */
37020
- .filex-header-btn{width:28px;height:28px;padding:0;border-radius:14px}
37017
+ /* Same 28×28 header entry as dsh-git-gui's (.gg-header-btn) but with a
37018
+ modest rounded-rectangle corner: the primitive sm button is already 28px
37019
+ tall with a 16px icon, so only the width / padding / radius need pinning
37020
+ down. An 8px radius keeps it a rounded square rather than a full circle. */
37021
+ .filex-header-btn{width:28px;height:28px;padding:0;border-radius:8px}
37021
37022
 
37022
37023
  /* ── hero ButtonGroup (shell.overlay entry) ──────────────────────────── */
37023
37024
  /* The mode group shown only on the hero / new-session screen: the left
37024
37025
  button carries the remembered mode's icon, the right chevron the mode
37025
- dropdown. Outer corners reuse the in-chat header-button radius (14px on
37026
- a 28px-tall button = a fully round cap), so the group reads as one pill
37027
- with the same curve as the session header icon. */
37026
+ dropdown. Outer corners use the same rounded-rectangle radius as the
37027
+ header buttons (8px on a 28px-tall button), so the group reads as one
37028
+ pill consistent with the session header icons. */
37028
37029
  .filex-group{display:inline-flex;align-items:center;position:relative;height:28px}
37029
37030
  .filex-group .filex-group-main,.filex-group .filex-group-trigger{height:28px}
37030
- .filex-group .filex-group-main{border-radius:14px 0 0 14px;padding:0 9px}
37031
- .filex-group .filex-group-trigger{margin-left:-1px;border-radius:0 14px 14px 0;border-left:0;padding:0 7px}
37031
+ .filex-group .filex-group-main{border-radius:8px 0 0 8px;padding:0 9px}
37032
+ .filex-group .filex-group-trigger{margin-left:-1px;border-radius:0 8px 8px 0;border-left:0;padding:0 7px}
37033
+
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)}
37032
37048
 
37033
37049
  /* ── hero floating utility (shell.overlay entry) ─────────────────────── */
37034
37050
  /* The ButtonGroup, pinned by the plugin to the conversation column's
@@ -38196,6 +38212,20 @@ window.__ModuleLoader__.load({
38196
38212
  };
38197
38213
  emit();
38198
38214
  }
38215
+ /**
38216
+ * Resolve the session's authoritative working directory from the host
38217
+ * (`session.cwd` reads the session header), falling back to the framework
38218
+ * session-list hint only when the host round-trip fails. This keeps
38219
+ * reveal / vscode pointing at the session's own workspace even when the
38220
+ * session-list snapshot carries no cwd for the bound session.
38221
+ */
38222
+ async function resolveCwdHint(sessionId, fallback) {
38223
+ try {
38224
+ const result = await api.sessionCwd({ sessionId });
38225
+ if (result.cwd !== "") return result.cwd;
38226
+ } catch {}
38227
+ return fallback;
38228
+ }
38199
38229
  /** Ask the host to reveal the session's working folder in the OS file manager. */
38200
38230
  async function openSystemFolder(sessionId, cwd) {
38201
38231
  const resolved = sessionId !== void 0 && sessionId !== "" ? sessionId : activeSessionId;
@@ -38204,7 +38234,8 @@ window.__ModuleLoader__.load({
38204
38234
  return;
38205
38235
  }
38206
38236
  try {
38207
- showNotice(`已打开文件夹:${(await api.fsReveal({ sessionId: resolved }, cwd)).cwd}`);
38237
+ const hint = await resolveCwdHint(resolved, cwd);
38238
+ showNotice(`已打开文件夹:${(await api.fsReveal({ sessionId: resolved }, hint)).cwd}`);
38208
38239
  } catch (error) {
38209
38240
  showNotice(`打开系统文件夹失败:${error instanceof Error ? error.message : String(error)}`);
38210
38241
  }
@@ -38217,7 +38248,8 @@ window.__ModuleLoader__.load({
38217
38248
  return;
38218
38249
  }
38219
38250
  try {
38220
- showNotice(`已在 VS Code 打开:${(await api.fsVscode({ sessionId: resolved }, cwd)).cwd}`);
38251
+ const hint = await resolveCwdHint(resolved, cwd);
38252
+ showNotice(`已在 VS Code 打开:${(await api.fsVscode({ sessionId: resolved }, hint)).cwd}`);
38221
38253
  } catch (error) {
38222
38254
  showNotice(`用 VS Code 打开失败:${error instanceof Error ? error.message : String(error)}`);
38223
38255
  }
@@ -38320,15 +38352,21 @@ window.__ModuleLoader__.load({
38320
38352
  }
38321
38353
  /**
38322
38354
  * The hero ButtonGroup: the left button carries the remembered mode's icon and
38323
- * runs its action, the right chevron opens the mode dropdown. Rendered only on
38324
- * the hero / new-session screen — the in-chat header keeps the single
38325
- * built-in-editor `HeaderIcon`.
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
38358
+ * `HeaderIcon`.
38359
+ *
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.
38326
38363
  */
38327
38364
  function HeroGroup(props) {
38328
38365
  const state = (0, react.useSyncExternalStore)(subscribe, getSnapshot);
38329
38366
  const [menuOpen, setMenuOpen] = (0, react.useState)(false);
38330
38367
  const useSessions = props.useSessions ?? (() => void 0);
38331
- const sessionId = useSessions((s) => s.current);
38368
+ const current = useSessions((s) => s.current);
38369
+ const sessionId = props.sessionId ?? current;
38332
38370
  const sessionCwd = useSessions((s) => sessionId !== void 0 ? s.byId?.[sessionId]?.cwd : void 0);
38333
38371
  const mode = state.mode;
38334
38372
  const items = state.vscode ? MODE_ITEMS : MODE_ITEMS.filter((item) => item.id !== "vscode");
@@ -38389,6 +38427,103 @@ window.__ModuleLoader__.load({
38389
38427
  });
38390
38428
  }
38391
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
+ /**
38392
38527
  * Hero / new-session floating utility: the hero ButtonGroup, rendered by the
38393
38528
  * plugin itself through the generic `shell.overlay` floating layer (no shell
38394
38529
  * change needed) and pinned to the conversation column's top-right corner —
@@ -38400,11 +38535,19 @@ window.__ModuleLoader__.load({
38400
38535
  * tracked automatically. Clicking behaves exactly like the in-chat icon: in a
38401
38536
  * blank-session hero the actions bind to that session, and with no session
38402
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.
38403
38542
  */
38404
38543
  function HeroFilexButton(props) {
38405
38544
  const [pos, setPos] = (0, react.useState)(null);
38406
38545
  (0, react.useEffect)(() => {
38407
38546
  const update = () => {
38547
+ if (document.querySelector(".filex-corner") !== null) {
38548
+ setPos(null);
38549
+ return;
38550
+ }
38408
38551
  const column = document.querySelector("[data-phase]");
38409
38552
  if (column === null || column.getAttribute("data-phase") !== "hero") {
38410
38553
  setPos(null);
@@ -38533,6 +38676,11 @@ window.__ModuleLoader__.load({
38533
38676
  heroFab();
38534
38677
  };
38535
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 })));
38536
38684
  probeVscode();
38537
38685
  ctx.effect(() => {
38538
38686
  const deps = {
package/lib/index.js CHANGED
@@ -134,11 +134,16 @@ function isWithin(parent, child) {
134
134
  if (c === p) return true;
135
135
  return c.startsWith(p + "\\") || c.startsWith(p + "/") || c.startsWith(p + "\\") || c.startsWith(p + "/");
136
136
  }
137
- /** Resolve a session's authoritative working directory (header cwd first, process cwd last). */
137
+ /**
138
+ * Resolve a session's authoritative working directory (header cwd first).
139
+ * A session with no workspace has no meaningful directory — falling back to
140
+ * the host process cwd (the DSH installation's home) would silently point
141
+ * file operations at the wrong tree, so this fails loudly instead.
142
+ */
138
143
  function sessionCwdOf(ctx, sessionId) {
139
144
  const headerCwd = ctx.sessions.get(sessionId)?.header.cwd;
140
145
  if (headerCwd !== void 0 && headerCwd !== "") return resolve(headerCwd);
141
- return process.cwd();
146
+ throw new FilexError("no-workspace", "该会话没有关联的工作区目录。请先为该会话选择一个工作区。", 400);
142
147
  }
143
148
  async function readJsonBody(req) {
144
149
  let raw = "";
@@ -334,15 +339,18 @@ function openInSystemFileManager(target) {
334
339
  }
335
340
  /**
336
341
  * Resolve the directory a reveal/open action should point at. Authoritative
337
- * source: the session's own header cwd. When it is absent (sessions created
338
- * without workspace metadata fall back to the host process cwd — usually
339
- * meaningless), accept the loopback-only client's session-list cwd hint so
340
- * the folder still opens where the user works.
342
+ * source: the session's own header cwd. When it is absent (a session created
343
+ * without workspace metadata), fall back to the loopback-only client's
344
+ * session-list cwd hint — and only then fail loudly, never to the host
345
+ * process cwd (which is the DSH installation's home directory and would make
346
+ * "open folder" appear to open the user's home).
341
347
  */
342
348
  function revealCwdOf(ctx, sessionId, rawHint) {
343
349
  const headerCwd = ctx.sessions.get(sessionId)?.header.cwd;
344
350
  const hint = typeof rawHint === "string" && rawHint !== "" ? rawHint : void 0;
345
- return headerCwd !== void 0 && headerCwd !== "" ? resolve(headerCwd) : hint !== void 0 && isAbsolute(hint) ? resolve(hint) : process.cwd();
351
+ if (headerCwd !== void 0 && headerCwd !== "") return resolve(headerCwd);
352
+ if (hint !== void 0 && isAbsolute(hint)) return resolve(hint);
353
+ throw new FilexError("no-workspace", "该会话没有关联的工作区目录,无法打开系统文件夹。请先为该会话选择一个工作区。", 400);
346
354
  }
347
355
  /**
348
356
  * Locate the VS Code CLI (`code`). Checks the standard install locations
@@ -111,11 +111,22 @@ export interface FilexSlotsService {
111
111
  id?: string;
112
112
  key?: string;
113
113
  order?: number;
114
+ priority?: number;
114
115
  label?: string | (() => string);
115
116
  inject?: (...args: unknown[]) => Record<string, unknown>;
116
117
  }, component: unknown): () => void;
117
118
  inject(key: string, callback: () => () => void): () => void;
118
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
+ }
119
130
  declare module 'cordis' {
120
131
  interface Context {
121
132
  webServer: FilexWebServer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-xhl/dsh-file-explorer",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "DSH 外部编译型插件:工作区文件预览 / 编辑(CodeMirror 6 编辑器 + 文件树 + 全文搜索 + Markdown/HTML/图片/PDF 预览)。移植自同目录下的动态 Cordis 插件版本。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",