@dsh-xhl/dsh-file-explorer 0.1.6 → 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/README.md +6 -1
- package/cordis.patch.yml +8 -8
- package/lib/client.js +196 -38
- package/lib/index.js +15 -7
- package/lib/types/context-types.d.ts +11 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
- **Hero / 新建会话页 ButtonGroup**:同一浮层位置由插件定位到会话列右上角(hero 阶段显示,有记录后由标题栏图标接管,无重复);hero 上为**左主按钮 + 右下拉**的组合:
|
|
9
9
|
- **编辑器**(默认):点击打开文件预览 / 编辑弹窗(Ctrl+P 亦可)
|
|
10
10
|
- **文件夹**:点击直接打开会话工作区所在的**系统文件夹**(Windows `explorer` / macOS `open` / Linux `xdg-open`)
|
|
11
|
-
- **VSCode**:点击用 `code` 命令行在 **VS Code** 中打开会话工作区(自动探测安装位置 / PATH
|
|
11
|
+
- **VSCode**:点击用 `code` 命令行在 **VS Code** 中打开会话工作区(自动探测安装位置 / PATH;宿主明确报告未安装时该项自动隐藏,探测请求本身失败则保持可见并退避重试)
|
|
12
12
|
- 选择记忆在 localStorage,点击后底部提示确认打开的路径;下拉选中某项会先切换默认打开方式,随后立即执行对应的打开动作
|
|
13
13
|
- 纯插件实现,不改动框架代码(详见 `docs/hero-filex-button.md`)
|
|
14
14
|
- **对话内点击文件 → 编辑器预览**:聊天里的文件路径链接(工具行、产物行、正文提及)默认用系统程序打开;插件拦截 `remote.session.openWorkspacePath` 与 `workspaces.openPath` 两条通道,改为在编辑器弹窗中预览(目录或 cwd 外的路径仍落回系统打开)
|
|
@@ -44,6 +44,11 @@ pnpm install
|
|
|
44
44
|
pnpm build # lib/index.js(host)+ lib/client.js(client bundle,CodeMirror 内联)
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
> 构建采用**原地覆盖**(不再先删除 `lib/`)。宿主半在进程启动时载入内存,而 profile
|
|
48
|
+
> 若 link 到本工作区则会直读磁盘:构建中途清空目录会让 HMR 读到空 bundle,导致
|
|
49
|
+
> `/filex` 路由与客户端 import 瞬时失败(表现为 `unknown filex API method ...`)。
|
|
50
|
+
> 改动客户端后无需重启,宿主半的改动需重启 DSH 进程。
|
|
51
|
+
|
|
47
52
|
## 安装 / 挂载(desktop profile 示例)
|
|
48
53
|
|
|
49
54
|
```bash
|
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
|
|
37018
|
-
the primitive sm button is already 28px
|
|
37019
|
-
the width / padding / radius need pinning
|
|
37020
|
-
.
|
|
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
|
|
37026
|
-
a 28px-tall button
|
|
37027
|
-
|
|
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:
|
|
37031
|
-
.filex-group .filex-group-trigger{margin-left:-1px;border-radius:0
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -38242,31 +38274,41 @@ window.__ModuleLoader__.load({
|
|
|
38242
38274
|
} catch {}
|
|
38243
38275
|
}
|
|
38244
38276
|
/**
|
|
38245
|
-
* Probe the host
|
|
38246
|
-
*
|
|
38247
|
-
*
|
|
38248
|
-
|
|
38249
|
-
|
|
38277
|
+
* Probe the host for VS Code availability and hide the VSCode option when the
|
|
38278
|
+
* host positively reports it missing (a `{ vscode: false }` answer), so the
|
|
38279
|
+
* entry never shows as broken when the CLI is genuinely absent.
|
|
38280
|
+
*
|
|
38281
|
+
* A FAILED probe is not an answer: the host may simply be mid-reload (its
|
|
38282
|
+
* routes vanish for a moment while the bundle is rewritten), and latching
|
|
38283
|
+
* "unavailable" then would hide VSCode for the rest of the page's life over a
|
|
38284
|
+
* transient hiccup. Failures therefore keep the option visible and retry a few
|
|
38285
|
+
* times; picking it without a CLI still surfaces the host's own
|
|
38286
|
+
* 「未找到 VS Code 命令行工具」 error, which is the honest report.
|
|
38287
|
+
*/
|
|
38288
|
+
function probeVscode(attempt = 0) {
|
|
38250
38289
|
api.fsCapabilities().then((result) => {
|
|
38251
|
-
|
|
38252
|
-
|
|
38253
|
-
|
|
38254
|
-
|
|
38255
|
-
|
|
38256
|
-
|
|
38257
|
-
|
|
38258
|
-
|
|
38259
|
-
|
|
38290
|
+
if (result.vscode === false) {
|
|
38291
|
+
store = {
|
|
38292
|
+
...store,
|
|
38293
|
+
vscode: false
|
|
38294
|
+
};
|
|
38295
|
+
if (store.mode === "vscode") store = {
|
|
38296
|
+
...store,
|
|
38297
|
+
mode: "editor"
|
|
38298
|
+
};
|
|
38299
|
+
emit();
|
|
38300
|
+
return;
|
|
38301
|
+
}
|
|
38302
|
+
if (!store.vscode) {
|
|
38303
|
+
store = {
|
|
38304
|
+
...store,
|
|
38305
|
+
vscode: true
|
|
38306
|
+
};
|
|
38307
|
+
emit();
|
|
38308
|
+
}
|
|
38260
38309
|
}).catch(() => {
|
|
38261
|
-
|
|
38262
|
-
|
|
38263
|
-
vscode: false
|
|
38264
|
-
};
|
|
38265
|
-
if (store.mode === "vscode") store = {
|
|
38266
|
-
...store,
|
|
38267
|
-
mode: "editor"
|
|
38268
|
-
};
|
|
38269
|
-
emit();
|
|
38310
|
+
if (attempt >= 4) return;
|
|
38311
|
+
window.setTimeout(() => probeVscode(attempt + 1), 1e3 * (attempt + 1));
|
|
38270
38312
|
});
|
|
38271
38313
|
}
|
|
38272
38314
|
const MODE_ITEMS = [
|
|
@@ -38310,15 +38352,21 @@ window.__ModuleLoader__.load({
|
|
|
38310
38352
|
}
|
|
38311
38353
|
/**
|
|
38312
38354
|
* The hero ButtonGroup: the left button carries the remembered mode's icon and
|
|
38313
|
-
* runs its action, the right chevron opens the mode dropdown. Rendered
|
|
38314
|
-
*
|
|
38315
|
-
* built-in-editor
|
|
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.
|
|
38316
38363
|
*/
|
|
38317
38364
|
function HeroGroup(props) {
|
|
38318
38365
|
const state = (0, react.useSyncExternalStore)(subscribe, getSnapshot);
|
|
38319
38366
|
const [menuOpen, setMenuOpen] = (0, react.useState)(false);
|
|
38320
38367
|
const useSessions = props.useSessions ?? (() => void 0);
|
|
38321
|
-
const
|
|
38368
|
+
const current = useSessions((s) => s.current);
|
|
38369
|
+
const sessionId = props.sessionId ?? current;
|
|
38322
38370
|
const sessionCwd = useSessions((s) => sessionId !== void 0 ? s.byId?.[sessionId]?.cwd : void 0);
|
|
38323
38371
|
const mode = state.mode;
|
|
38324
38372
|
const items = state.vscode ? MODE_ITEMS : MODE_ITEMS.filter((item) => item.id !== "vscode");
|
|
@@ -38379,6 +38427,103 @@ window.__ModuleLoader__.load({
|
|
|
38379
38427
|
});
|
|
38380
38428
|
}
|
|
38381
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
|
+
/**
|
|
38382
38527
|
* Hero / new-session floating utility: the hero ButtonGroup, rendered by the
|
|
38383
38528
|
* plugin itself through the generic `shell.overlay` floating layer (no shell
|
|
38384
38529
|
* change needed) and pinned to the conversation column's top-right corner —
|
|
@@ -38390,11 +38535,19 @@ window.__ModuleLoader__.load({
|
|
|
38390
38535
|
* tracked automatically. Clicking behaves exactly like the in-chat icon: in a
|
|
38391
38536
|
* blank-session hero the actions bind to that session, and with no session
|
|
38392
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.
|
|
38393
38542
|
*/
|
|
38394
38543
|
function HeroFilexButton(props) {
|
|
38395
38544
|
const [pos, setPos] = (0, react.useState)(null);
|
|
38396
38545
|
(0, react.useEffect)(() => {
|
|
38397
38546
|
const update = () => {
|
|
38547
|
+
if (document.querySelector(".filex-corner") !== null) {
|
|
38548
|
+
setPos(null);
|
|
38549
|
+
return;
|
|
38550
|
+
}
|
|
38398
38551
|
const column = document.querySelector("[data-phase]");
|
|
38399
38552
|
if (column === null || column.getAttribute("data-phase") !== "hero") {
|
|
38400
38553
|
setPos(null);
|
|
@@ -38523,6 +38676,11 @@ window.__ModuleLoader__.load({
|
|
|
38523
38676
|
heroFab();
|
|
38524
38677
|
};
|
|
38525
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 })));
|
|
38526
38684
|
probeVscode();
|
|
38527
38685
|
ctx.effect(() => {
|
|
38528
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
|
-
/**
|
|
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
|
-
|
|
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 (
|
|
338
|
-
* without workspace metadata fall back to the
|
|
339
|
-
*
|
|
340
|
-
* the
|
|
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
|
-
|
|
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.
|
|
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",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"README.md"
|
|
42
42
|
],
|
|
43
43
|
"scripts": {
|
|
44
|
-
"build": "
|
|
44
|
+
"build": "tsc -p tsconfig.build.json && tsdown",
|
|
45
45
|
"typecheck": "tsc --noEmit",
|
|
46
46
|
"bundle": "tsdown",
|
|
47
47
|
"prepare": "tsdown"
|