@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 +8 -8
- package/lib/client.js +163 -15
- package/lib/index.js +15 -7
- package/lib/types/context-types.d.ts +11 -0
- package/package.json +1 -1
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
|
}
|
|
@@ -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
|
|
38324
|
-
*
|
|
38325
|
-
* 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.
|
|
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
|
|
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
|
-
/**
|
|
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