dsh-edge 0.10.0 → 0.11.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.i18n.yaml +2 -2
- package/README.md +7 -1
- package/README.zh.md +7 -1
- package/THIRD_PARTY_NOTICES.md +3 -1
- package/dist/index.html +1 -1
- package/dist/plugins/@deepseek-ai/dsh-client-ui-directory-picker-browse/client.js +1045 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-plan/client.js +140 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-reference/client.js +252 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-settings-plugin-inventory/client.js +658 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-settings-plugins/client.js +1827 -0
- package/dist/plugins/dsh-edge-client-ui/client.js +57 -218
- package/package.json +10 -1
- package/worker/direct/index.js +1353 -956
- package/worker/isolated/index.js +983 -586
|
@@ -0,0 +1,1045 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@deepseek-ai/dsh-client-ui-directory-picker-browse",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
10
|
+
//#region ../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
11
|
+
function r(e) {
|
|
12
|
+
var t, f, n = "";
|
|
13
|
+
if ("string" == typeof e || "number" == typeof e) n += e;
|
|
14
|
+
else if ("object" == typeof e) if (Array.isArray(e)) {
|
|
15
|
+
var o = e.length;
|
|
16
|
+
for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
|
|
17
|
+
} else for (f in e) e[f] && (n && (n += " "), n += f);
|
|
18
|
+
return n;
|
|
19
|
+
}
|
|
20
|
+
function clsx() {
|
|
21
|
+
for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
|
|
22
|
+
return n;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-directory-picker-browse/src/client/DirectoryBrowser.module.css.mjs
|
|
26
|
+
const css = ".ZuhsRW_dialog.ZuhsRW_dialog{--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);gap:0;width:min(680px,100%);height:min(500px,100dvh - 32px);padding:0}.ZuhsRW_editorScope{display:contents}.ZuhsRW_header{border-bottom:.5px solid var(--dsw-alias-border-l3);flex-direction:column;flex:none;gap:8px;padding:16px 14px 8px 24px;display:flex}.ZuhsRW_title{min-height:28px;color:var(--dsw-alias-label-primary);align-items:flex-end;margin:0;font-size:16px;font-weight:510;line-height:24px;display:flex}.ZuhsRW_crumbBar{box-sizing:border-box;border:1px solid #0000;border-radius:8px;align-items:center;gap:4px;min-height:24px;margin-left:-9px;padding:0 8px;display:flex}.ZuhsRW_crumbBar:has(.ZuhsRW_crumbEditZone:enabled:hover),.ZuhsRW_crumbBar:has(.ZuhsRW_crumbEditZone:focus-visible),.ZuhsRW_crumbBar:has(.ZuhsRW_pathInput){border-color:var(--dsw-alias-border-l2)}.ZuhsRW_millerRow{scrollbar-width:none;flex:1 1 0;align-items:stretch;gap:12px;min-height:0;display:flex;overflow-x:auto}.ZuhsRW_crumbTrail{scrollbar-width:none;flex:0 auto;align-items:center;gap:4px;min-width:0;display:flex;overflow-x:auto}.ZuhsRW_crumbSeat{flex:none;align-items:center;gap:4px;min-width:0;display:inline-flex}.ZuhsRW_crumb{text-overflow:ellipsis;white-space:nowrap;max-width:160px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;padding:0;font-size:13px;font-weight:500;line-height:20px;overflow:hidden}.ZuhsRW_crumb:hover{color:var(--dsw-alias-label-primary)}.ZuhsRW_crumbChevron{color:var(--dsw-alias-label-tertiary);flex:none}.ZuhsRW_crumbEditZone{cursor:text;background:0 0;border:none;outline:none;flex:1 0 34px;justify-content:flex-end;align-items:center;min-width:34px;height:22px;padding:0;display:flex}.ZuhsRW_crumbEditGlyph{color:var(--dsw-alias-label-tertiary);flex:none}.ZuhsRW_crumbEditZone:enabled:hover .ZuhsRW_crumbEditGlyph,.ZuhsRW_crumbEditZone:focus-visible .ZuhsRW_crumbEditGlyph{color:var(--dsw-alias-label-primary)}.ZuhsRW_crumbEditZone:disabled{cursor:default}.ZuhsRW_crumbEditZone:disabled .ZuhsRW_crumbEditGlyph{color:var(--dsw-alias-label-caption)}.ZuhsRW_pathInput{box-sizing:border-box;min-width:0;height:22px;color:var(--dsw-alias-label-primary);background:0 0;border:none;outline:none;flex:1 1 0;padding:0;font-size:13px;line-height:20px}.ZuhsRW_content{flex-direction:column;flex:1 1 0;min-height:0;padding:16px 16px 16px 24px;display:flex;position:relative}.ZuhsRW_column{flex-direction:column;flex:1 1 0;gap:2px;min-width:256px;padding-right:8px;display:flex;overflow-y:auto}.ZuhsRW_divider{background:var(--dsw-alias-border-l3);flex:none;width:.5px}.ZuhsRW_rowSeat{flex:none;display:flex}.ZuhsRW_row{text-align:left;cursor:pointer;background:0 0;border:none;border-radius:6px;flex:none;align-items:center;gap:4px;width:100%;height:28px;padding:4px;display:flex}.ZuhsRW_row:hover{background:var(--dsw-alias-interactive-bg-hover)}.ZuhsRW_rowSelected,.ZuhsRW_rowSelected:hover{background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover))}.ZuhsRW_rowIcon{color:var(--dsw-alias-label-secondary);flex:none}.ZuhsRW_rowIconSelected{color:var(--dsw-alias-button-info-fill);flex:none}.ZuhsRW_rowName{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary);flex:1 1 0;font-size:13px;font-weight:500;line-height:20px;overflow:hidden}.ZuhsRW_rowChevron{color:var(--dsw-alias-label-tertiary);flex:none}.ZuhsRW_status,.ZuhsRW_error{padding:4px 120px 4px 4px;font-size:12px;line-height:18px}.ZuhsRW_status{color:var(--dsw-alias-label-secondary)}.ZuhsRW_error{color:var(--dsw-alias-state-error-primary)}.ZuhsRW_loadingFloat{background:var(--dsw-alias-bg-layer-2);padding:2px 8px;position:absolute;bottom:8px;right:16px}.ZuhsRW_footerBar{border-top:.5px solid var(--dsw-alias-border-l3);flex-wrap:wrap;flex:none;align-items:center;gap:8px;padding:16px 24px;display:flex}.ZuhsRW_showHiddenToggle{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;align-items:center;gap:4px;padding:0;font-size:13px;font-weight:500;line-height:20px;display:inline-flex}.ZuhsRW_showHiddenToggle:hover{color:var(--dsw-alias-label-primary)}.ZuhsRW_showHiddenToggle:disabled{color:var(--dsw-alias-label-caption);cursor:default}.ZuhsRW_showHiddenToggleActive{color:var(--dsw-alias-label-primary)}.ZuhsRW_footerGap{flex:1 1 0}.ZuhsRW_footerAction{min-width:72px}.ZuhsRW_createDialog.ZuhsRW_createDialog{gap:0;width:min(380px,100%);padding:0}.ZuhsRW_createBody{flex-direction:column;gap:12px;padding:22px 24px 20px;display:flex}.ZuhsRW_createTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:510;line-height:24px}.ZuhsRW_createIn{color:var(--dsw-alias-label-primary);margin:0;font-size:14px;line-height:22px}.ZuhsRW_createInput{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l4);width:100%;height:44px;color:var(--dsw-alias-label-primary);background:0 0;border-radius:22px;outline:none;padding:7px 14px;font-size:14px;line-height:22px}.ZuhsRW_createInput::placeholder{color:var(--dsw-alias-label-caption)}.ZuhsRW_createActions{justify-content:flex-end;align-items:center;gap:8px;margin-top:8px;display:flex}";
|
|
27
|
+
const tagId = "@deepseek-ai/dsh-client-ui-directory-picker-browse/DirectoryBrowser.module.css";
|
|
28
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
29
|
+
const tag = document.createElement("style");
|
|
30
|
+
tag.dataset.plugin = "@deepseek-ai/dsh-client-ui-directory-picker-browse";
|
|
31
|
+
tag.dataset.pluginCss = tagId;
|
|
32
|
+
tag.textContent = css;
|
|
33
|
+
document.head.appendChild(tag);
|
|
34
|
+
}
|
|
35
|
+
var DirectoryBrowser_module_css_default = {
|
|
36
|
+
"column": "ZuhsRW_column",
|
|
37
|
+
"content": "ZuhsRW_content",
|
|
38
|
+
"createActions": "ZuhsRW_createActions",
|
|
39
|
+
"createBody": "ZuhsRW_createBody",
|
|
40
|
+
"createDialog": "ZuhsRW_createDialog",
|
|
41
|
+
"createIn": "ZuhsRW_createIn",
|
|
42
|
+
"createInput": "ZuhsRW_createInput",
|
|
43
|
+
"createTitle": "ZuhsRW_createTitle",
|
|
44
|
+
"crumb": "ZuhsRW_crumb",
|
|
45
|
+
"crumbBar": "ZuhsRW_crumbBar",
|
|
46
|
+
"crumbChevron": "ZuhsRW_crumbChevron",
|
|
47
|
+
"crumbEditGlyph": "ZuhsRW_crumbEditGlyph",
|
|
48
|
+
"crumbEditZone": "ZuhsRW_crumbEditZone",
|
|
49
|
+
"crumbSeat": "ZuhsRW_crumbSeat",
|
|
50
|
+
"crumbTrail": "ZuhsRW_crumbTrail",
|
|
51
|
+
"dialog": "ZuhsRW_dialog",
|
|
52
|
+
"divider": "ZuhsRW_divider",
|
|
53
|
+
"editorScope": "ZuhsRW_editorScope",
|
|
54
|
+
"error": "ZuhsRW_error",
|
|
55
|
+
"footerAction": "ZuhsRW_footerAction",
|
|
56
|
+
"footerBar": "ZuhsRW_footerBar",
|
|
57
|
+
"footerGap": "ZuhsRW_footerGap",
|
|
58
|
+
"header": "ZuhsRW_header",
|
|
59
|
+
"loadingFloat": "ZuhsRW_loadingFloat",
|
|
60
|
+
"millerRow": "ZuhsRW_millerRow",
|
|
61
|
+
"pathInput": "ZuhsRW_pathInput",
|
|
62
|
+
"row": "ZuhsRW_row",
|
|
63
|
+
"rowChevron": "ZuhsRW_rowChevron",
|
|
64
|
+
"rowIcon": "ZuhsRW_rowIcon",
|
|
65
|
+
"rowIconSelected": "ZuhsRW_rowIconSelected",
|
|
66
|
+
"rowName": "ZuhsRW_rowName",
|
|
67
|
+
"rowSeat": "ZuhsRW_rowSeat",
|
|
68
|
+
"rowSelected": "ZuhsRW_rowSelected",
|
|
69
|
+
"showHiddenToggle": "ZuhsRW_showHiddenToggle",
|
|
70
|
+
"showHiddenToggleActive": "ZuhsRW_showHiddenToggleActive",
|
|
71
|
+
"status": "ZuhsRW_status",
|
|
72
|
+
"title": "ZuhsRW_title"
|
|
73
|
+
};
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region lib/types/client/DirectoryBrowser.js
|
|
76
|
+
/**
|
|
77
|
+
* The in-app workspace-directory browser (figma Harness 813-23126 family): a
|
|
78
|
+
* 680×500 dialog (clamped to short/narrow viewports — the Miller row scrolls
|
|
79
|
+
* sideways, the columns scroll down) whose header carries the title, the selection-path
|
|
80
|
+
* breadcrumb, and a click-to-edit path zone; below it a Miller view — one
|
|
81
|
+
* full-width level until a row is selected, then two columns splitting the
|
|
82
|
+
* row evenly (256px floor; level | selected folder's children) around a
|
|
83
|
+
* hairline divider. Navigations land selection-anchored and quiet: the
|
|
84
|
+
* previous view keeps rendering while a crumb jump or a submitted path is
|
|
85
|
+
* scanned, then target and parent legs land as one two-pane frame (a slow
|
|
86
|
+
* parent leg falls back to landing the target alone and upgrading in
|
|
87
|
+
* place), so stepping back keeps two panes away from the display root and
|
|
88
|
+
* navigation never flashes an intermediate frame. Selecting in the
|
|
89
|
+
* right column shifts the view one level deeper. "New folder" opens a nested
|
|
90
|
+
* create dialog targeting the selected folder (or the level itself) and
|
|
91
|
+
* selects the created folder. Open adopts the selected folder, falling back
|
|
92
|
+
* to the listed level. Pure consumer of the injected browse calls — the
|
|
93
|
+
* owning flow decides what "Open" means and owns the workspace-creation
|
|
94
|
+
* error surface. Hidden entries are host-flagged and hidden by default; the
|
|
95
|
+
* footer's fixed-label "Show hidden files" toggle (aria-pressed, check when
|
|
96
|
+
* on) reveals them (client-side only). The path editor announces itself with
|
|
97
|
+
* a pencil glyph and a bar-wide hover-lit outline, opens seeded with a
|
|
98
|
+
* trailing separator, and keeps the panes under the draft: the final segment
|
|
99
|
+
* prefix-filters the LAST pane while that pane's level is the one the draft's
|
|
100
|
+
* directory part names (a dot-led prefix also reveals the hidden entries it
|
|
101
|
+
* names, and a prefix nobody matches releases the filter), while any other
|
|
102
|
+
* directory part is scanned after a short debounce and lands like any other
|
|
103
|
+
* navigation — selection-anchored and two-pane away from the display root,
|
|
104
|
+
* both legs waited out so one keystroke moves the view once. The pane arity
|
|
105
|
+
* holds throughout: the last pane is the level the path names and the one
|
|
106
|
+
* beside it is its parent, so typing deeper descends and erasing segments
|
|
107
|
+
* walks back up, moving the Miller view without leaving the editor. Panes the
|
|
108
|
+
* draft walked to stay put when the editor closes (cancellation included):
|
|
109
|
+
* the crumbs name where the walk ended, and Open's fallback target follows
|
|
110
|
+
* them.
|
|
111
|
+
*/
|
|
112
|
+
/** Failure text from the injected directory operation. */
|
|
113
|
+
function failureText(error) {
|
|
114
|
+
if (error !== null && typeof error === "object" && "rpcError" in error) {
|
|
115
|
+
const rpcError = error.rpcError;
|
|
116
|
+
if (rpcError !== null && typeof rpcError === "object" && "message" in rpcError && typeof rpcError.message === "string") return rpcError.message;
|
|
117
|
+
}
|
|
118
|
+
return error instanceof Error ? error.message : String(error);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* How long a scan may stay visually silent before the floating "Loading…"
|
|
122
|
+
* pill appears. The stale view keeps rendering while a scan is in flight, so
|
|
123
|
+
* a listing that settles inside this window swaps the panes with no
|
|
124
|
+
* intermediate frame at all; only a genuinely slow host (a network mount, a
|
|
125
|
+
* cold disk) surfaces the indicator.
|
|
126
|
+
*/
|
|
127
|
+
const SLOW_SCAN_DELAY_MS = 300;
|
|
128
|
+
/**
|
|
129
|
+
* How long a navigation landing waits for its parent leg before committing
|
|
130
|
+
* the target alone. Inside the window both legs land as ONE two-pane frame —
|
|
131
|
+
* no single-pane flash between them; past it the target commits single-pane
|
|
132
|
+
* at once (an Enter-submitted navigation is never held hostage by a stalled
|
|
133
|
+
* parent) and the late parent leg upgrades the landing in place.
|
|
134
|
+
*/
|
|
135
|
+
const PARENT_LEG_WAIT_MS = 200;
|
|
136
|
+
/**
|
|
137
|
+
* How long a typed draft rests before the panes follow it to a directory no
|
|
138
|
+
* pane lists. The window absorbs the keystrokes that walk through
|
|
139
|
+
* intermediate directory parts (every character of `/usr/lo` past the
|
|
140
|
+
* separator would otherwise be its own scan) while staying short enough that
|
|
141
|
+
* a pause reads as "the list moved with me".
|
|
142
|
+
*/
|
|
143
|
+
const DRAFT_PREVIEW_DEBOUNCE_MS = 250;
|
|
144
|
+
/**
|
|
145
|
+
* Breadcrumb rows for display: inside the home subtree the chain starts at a
|
|
146
|
+
* localized Home crumb; outside it the full ancestry shows, the root labeled
|
|
147
|
+
* by its own path.
|
|
148
|
+
*/
|
|
149
|
+
function displayCrumbs(listing, homeLabel) {
|
|
150
|
+
const homeIndex = listing.crumbs.findIndex((crumb) => crumb.path === listing.home);
|
|
151
|
+
if (homeIndex === -1) return listing.crumbs;
|
|
152
|
+
const tail = listing.crumbs.slice(homeIndex + 1);
|
|
153
|
+
return [{
|
|
154
|
+
name: homeLabel,
|
|
155
|
+
path: listing.home,
|
|
156
|
+
hidden: false
|
|
157
|
+
}, ...tail];
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The listing's platform separator, inferred from the home path the host
|
|
161
|
+
* stamped — never from typed text or entry paths, where a backslash is a
|
|
162
|
+
* legal POSIX name character. Still a heuristic at the last step: a POSIX
|
|
163
|
+
* home directory whose own name contains a backslash would misread.
|
|
164
|
+
* TODO: replace with a host-stamped `separator` field on the wire
|
|
165
|
+
* DirectoryListing so the platform fact travels verbatim (the trade-off is
|
|
166
|
+
* recorded in the directory-picker capability seam Agent Note).
|
|
167
|
+
*/
|
|
168
|
+
function separatorOf(listing) {
|
|
169
|
+
return listing.home.includes("\\") ? "\\" : "/";
|
|
170
|
+
}
|
|
171
|
+
/** The listed level as a directory part: its own path, separator-terminated (the root already is). */
|
|
172
|
+
function levelDirectory(listing) {
|
|
173
|
+
const sep = separatorOf(listing);
|
|
174
|
+
return listing.path.endsWith(sep) ? listing.path : `${listing.path}${sep}`;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The draft's directory part — everything through its last separator — or
|
|
178
|
+
* null while no separator has been typed at all (nothing addresses a
|
|
179
|
+
* directory yet). The platform comes from `listing`: on Windows a forward
|
|
180
|
+
* slash separates too (the host's `resolve` accepts either), while on POSIX a
|
|
181
|
+
* backslash is a legal name character and never separates.
|
|
182
|
+
*/
|
|
183
|
+
function draftDirectory(listing, draft) {
|
|
184
|
+
const cut = separatorOf(listing) === "\\" ? Math.max(draft.lastIndexOf("\\"), draft.lastIndexOf("/")) : draft.lastIndexOf("/");
|
|
185
|
+
return cut === -1 ? null : draft.slice(0, cut + 1);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* How the draft reads against one level: the directory part it names, and —
|
|
189
|
+
* when `listing` is the level that directory part addresses — the final
|
|
190
|
+
* segment that prefix-filters it while the user types (case-insensitively,
|
|
191
|
+
* downstream). A level answers a directory part when its own path is that
|
|
192
|
+
* part, or when it is the level that very text just produced (`scanned`): the
|
|
193
|
+
* host resolves what it is given, so `..` segments and Windows forward
|
|
194
|
+
* slashes reach a level whose path spells the request differently.
|
|
195
|
+
* @param listing - the level to read the draft against.
|
|
196
|
+
* @param draft - the current path draft.
|
|
197
|
+
* @param scanned - the last draft-following scan's directory and landing.
|
|
198
|
+
* @returns the draft's directory part (null with no separator typed) and its
|
|
199
|
+
* filtering tail (null when this level does not answer that directory).
|
|
200
|
+
*/
|
|
201
|
+
function readDraft(listing, draft, scanned) {
|
|
202
|
+
const directory = draftDirectory(listing, draft);
|
|
203
|
+
if (directory === null) return {
|
|
204
|
+
directory: null,
|
|
205
|
+
tail: null
|
|
206
|
+
};
|
|
207
|
+
return {
|
|
208
|
+
directory,
|
|
209
|
+
tail: directory === levelDirectory(listing) || scanned !== null && scanned.directory === directory && scanned.landed === listing.path ? draft.slice(directory.length) : null
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The rows one column renders. The selection is exempt from every filter: it
|
|
214
|
+
* anchors the two-pane view (crumbs and the child pane point at it), so
|
|
215
|
+
* neither the hidden filter after a dot-reveal pick nor a prefix miss may
|
|
216
|
+
* orphan it. A prefix narrows the level only while some row it would actually
|
|
217
|
+
* show matches — a tail nobody matches is a name being spelled, not a demand
|
|
218
|
+
* for an empty pane, so the level shows whole and its hidden rows return to
|
|
219
|
+
* obeying the toggle. Counting only displayable rows keeps that true because
|
|
220
|
+
* every hidden name is dot-prefixed, and a matching prefix therefore reveals
|
|
221
|
+
* it; otherwise the level could narrow to nothing.
|
|
222
|
+
*/
|
|
223
|
+
function visibleEntries(entries, selectedPath, showHidden, filterPrefix) {
|
|
224
|
+
const needle = filterPrefix === null ? "" : filterPrefix.toLowerCase();
|
|
225
|
+
const displayable = (entry) => showHidden || !entry.hidden || needle.startsWith(".");
|
|
226
|
+
const matches = (entry) => displayable(entry) && entry.name.toLowerCase().startsWith(needle);
|
|
227
|
+
const narrowing = needle !== "" && entries.some(matches);
|
|
228
|
+
return entries.filter((entry) => {
|
|
229
|
+
if (entry.path === selectedPath) return true;
|
|
230
|
+
if (narrowing) return matches(entry);
|
|
231
|
+
return showHidden || !entry.hidden;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
/** One column of folder rows (the Miller view renders one or two of these). */
|
|
235
|
+
function LevelColumn({ entries, selectedPath, busy, onPick, showHidden, filterPrefix, pathEditing }) {
|
|
236
|
+
const visible = visibleEntries(entries, selectedPath, showHidden, filterPrefix);
|
|
237
|
+
return (0, react_jsx_runtime.jsx)("div", {
|
|
238
|
+
className: DirectoryBrowser_module_css_default.column,
|
|
239
|
+
role: "list",
|
|
240
|
+
children: visible.map((entry) => {
|
|
241
|
+
const selected = entry.path === selectedPath;
|
|
242
|
+
return (0, react_jsx_runtime.jsx)("span", {
|
|
243
|
+
role: "listitem",
|
|
244
|
+
className: DirectoryBrowser_module_css_default.rowSeat,
|
|
245
|
+
children: (0, react_jsx_runtime.jsxs)("button", {
|
|
246
|
+
type: "button",
|
|
247
|
+
"aria-current": selected || void 0,
|
|
248
|
+
className: clsx(DirectoryBrowser_module_css_default.row, selected && DirectoryBrowser_module_css_default.rowSelected),
|
|
249
|
+
disabled: busy,
|
|
250
|
+
onMouseDown: pathEditing ? (event) => {
|
|
251
|
+
event.preventDefault();
|
|
252
|
+
} : void 0,
|
|
253
|
+
onClick: () => {
|
|
254
|
+
onPick(entry);
|
|
255
|
+
},
|
|
256
|
+
children: [
|
|
257
|
+
selected ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpen16, {
|
|
258
|
+
size: 16,
|
|
259
|
+
className: DirectoryBrowser_module_css_default.rowIconSelected
|
|
260
|
+
}) : (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderClose16, {
|
|
261
|
+
size: 16,
|
|
262
|
+
className: DirectoryBrowser_module_css_default.rowIcon
|
|
263
|
+
}),
|
|
264
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
265
|
+
className: DirectoryBrowser_module_css_default.rowName,
|
|
266
|
+
children: entry.name
|
|
267
|
+
}),
|
|
268
|
+
(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, {
|
|
269
|
+
size: 12,
|
|
270
|
+
className: DirectoryBrowser_module_css_default.rowChevron
|
|
271
|
+
})
|
|
272
|
+
]
|
|
273
|
+
})
|
|
274
|
+
}, entry.path);
|
|
275
|
+
})
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Render the directory-browser dialog.
|
|
280
|
+
* @param props - owner-controlled browser props.
|
|
281
|
+
* @returns the dialog element (null while closed, via Modal).
|
|
282
|
+
*/
|
|
283
|
+
function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen, onClose, busy, t }) {
|
|
284
|
+
const [parent, setParent] = (0, react.useState)(null);
|
|
285
|
+
const [selected, setSelected] = (0, react.useState)(null);
|
|
286
|
+
const [child, setChild] = (0, react.useState)(null);
|
|
287
|
+
const [loading, setLoading] = (0, react.useState)(false);
|
|
288
|
+
const [slowScan, setSlowScan] = (0, react.useState)(false);
|
|
289
|
+
const [scanWindow, setScanWindow] = (0, react.useState)(0);
|
|
290
|
+
const [error, setError] = (0, react.useState)(null);
|
|
291
|
+
const [pathDraft, setPathDraft] = (0, react.useState)(null);
|
|
292
|
+
const [showHidden, setShowHidden] = (0, react.useState)(false);
|
|
293
|
+
const [folderDraft, setFolderDraft] = (0, react.useState)(null);
|
|
294
|
+
const [creatingFolder, setCreatingFolder] = (0, react.useState)(false);
|
|
295
|
+
const [createError, setCreateError] = (0, react.useState)(null);
|
|
296
|
+
const requestSeq = (0, react.useRef)(0);
|
|
297
|
+
const scanController = (0, react.useRef)(null);
|
|
298
|
+
const openGeneration = (0, react.useRef)(0);
|
|
299
|
+
const crumbTrailRef = (0, react.useRef)(null);
|
|
300
|
+
const composingRef = (0, react.useRef)(false);
|
|
301
|
+
(0, react.useEffect)(() => () => {
|
|
302
|
+
requestSeq.current += 1;
|
|
303
|
+
openGeneration.current += 1;
|
|
304
|
+
scanController.current?.abort();
|
|
305
|
+
}, []);
|
|
306
|
+
const compositionGuard = {
|
|
307
|
+
onCompositionStart: () => {
|
|
308
|
+
composingRef.current = true;
|
|
309
|
+
},
|
|
310
|
+
onCompositionEnd: () => {
|
|
311
|
+
composingRef.current = false;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
/** Newer intent wins: invalidate the pending listing's settlement AND abort its wire request. */
|
|
315
|
+
const supersede = (0, react.useCallback)(() => {
|
|
316
|
+
scanController.current?.abort();
|
|
317
|
+
scanController.current = null;
|
|
318
|
+
return ++requestSeq.current;
|
|
319
|
+
}, []);
|
|
320
|
+
/** Hide any prior indicator and start a fresh silence window for one listing call. */
|
|
321
|
+
const restartSlowScanWindow = (0, react.useCallback)(() => {
|
|
322
|
+
setSlowScan(false);
|
|
323
|
+
setScanWindow((value) => value + 1);
|
|
324
|
+
}, []);
|
|
325
|
+
/** Launch one listing under a fresh controller so a later supersession can abort it. */
|
|
326
|
+
const launchListing = (0, react.useCallback)((path) => {
|
|
327
|
+
const seq = supersede();
|
|
328
|
+
const controller = new AbortController();
|
|
329
|
+
scanController.current = controller;
|
|
330
|
+
restartSlowScanWindow();
|
|
331
|
+
return {
|
|
332
|
+
seq,
|
|
333
|
+
scan: listDirectory(path, controller.signal)
|
|
334
|
+
};
|
|
335
|
+
}, [
|
|
336
|
+
supersede,
|
|
337
|
+
restartSlowScanWindow,
|
|
338
|
+
listDirectory
|
|
339
|
+
]);
|
|
340
|
+
/**
|
|
341
|
+
* Launch a follow-up listing under the CURRENT supersession seq: a newer
|
|
342
|
+
* intent aborts it like the leg it continues, and it supersedes nothing.
|
|
343
|
+
*/
|
|
344
|
+
const continueScan = (0, react.useCallback)((path) => {
|
|
345
|
+
const controller = new AbortController();
|
|
346
|
+
scanController.current = controller;
|
|
347
|
+
restartSlowScanWindow();
|
|
348
|
+
return listDirectory(path, controller.signal);
|
|
349
|
+
}, [restartSlowScanWindow, listDirectory]);
|
|
350
|
+
/**
|
|
351
|
+
* Enter owns the view from submission until its navigation lands, so the
|
|
352
|
+
* debounce timer the same keystrokes armed must not supersede it. Cleared
|
|
353
|
+
* by the next edit (and by opening the editor); a failed submission leaves
|
|
354
|
+
* it set until the operator edits again, so the rejected path is not
|
|
355
|
+
* immediately re-scanned as a preview.
|
|
356
|
+
*/
|
|
357
|
+
const previewSuspended = (0, react.useRef)(false);
|
|
358
|
+
const viewRef = (0, react.useRef)({
|
|
359
|
+
parent: null,
|
|
360
|
+
child: null
|
|
361
|
+
});
|
|
362
|
+
(0, react.useEffect)(() => {
|
|
363
|
+
viewRef.current = {
|
|
364
|
+
parent,
|
|
365
|
+
child
|
|
366
|
+
};
|
|
367
|
+
}, [parent, child]);
|
|
368
|
+
const scanned = (0, react.useRef)(null);
|
|
369
|
+
/**
|
|
370
|
+
* A landed preview replaced the pane a keyboard operator may have Tabbed
|
|
371
|
+
* onto, so the focus it drops is re-parked on the still-open editor (the
|
|
372
|
+
* Modal has no focus trap). Consumed by the refocus effect below.
|
|
373
|
+
*/
|
|
374
|
+
const refocusPathInput = (0, react.useRef)(false);
|
|
375
|
+
/**
|
|
376
|
+
* Replace the whole view with a freshly scanned level. Away from the
|
|
377
|
+
* display root — the same collapse the crumb header renders, so crumbs and
|
|
378
|
+
* pane shape never disagree — the landing is two-pane: the target's ACTUAL
|
|
379
|
+
* parent-level entry re-selected (left pane = parent, right pane = the
|
|
380
|
+
* target), so a crumb jump reads as stepping back one pane. Both legs land
|
|
381
|
+
* as one frame when the parent leg settles within
|
|
382
|
+
* {@link PARENT_LEG_WAIT_MS}; past that bound (or at the display root) the
|
|
383
|
+
* target commits alone — single wide level, loading ends — and a late
|
|
384
|
+
* parent leg still upgrades the landing in place. A failed parent leg, or a
|
|
385
|
+
* truncated parent window that lacks the target, leaves the single-pane
|
|
386
|
+
* landing — the upgrade must never orphan the selection it exists to
|
|
387
|
+
* anchor. Until whichever commit comes first, the previous view keeps
|
|
388
|
+
* rendering: a landing swaps the panes, it never blanks them.
|
|
389
|
+
*
|
|
390
|
+
* Two callers, one landing shape. A submitted path (Enter, a crumb) closes
|
|
391
|
+
* the editor on arrival, announces its failure, and takes the wait bound —
|
|
392
|
+
* it is answering a gesture, so it may not hang on a stalled parent. The
|
|
393
|
+
* editor's own draft-following scan keeps all three to itself: it is
|
|
394
|
+
* speculative, nothing waits on it, and the stale view keeps rendering, so
|
|
395
|
+
* it waits for BOTH legs rather than flashing a single pane it would then
|
|
396
|
+
* upgrade — one keystroke must move the view once. A failure leaves the
|
|
397
|
+
* last readable panes standing and says nothing, while an arrival clears
|
|
398
|
+
* the stale message and re-parks focus the swap dropped.
|
|
399
|
+
* @param path - the level to list; absent lists the Host home directory.
|
|
400
|
+
* @param options - `closeEditor` retires the path draft on arrival and
|
|
401
|
+
* bounds the wait for the parent leg; `announce` surfaces a failure as the
|
|
402
|
+
* dialog's alert.
|
|
403
|
+
*/
|
|
404
|
+
const land = (0, react.useCallback)((path, options) => {
|
|
405
|
+
const { seq, scan } = launchListing(path);
|
|
406
|
+
setLoading(true);
|
|
407
|
+
if (options.announce) setError(null);
|
|
408
|
+
const settle = () => {
|
|
409
|
+
setLoading(false);
|
|
410
|
+
if (options.closeEditor) {
|
|
411
|
+
setPathDraft(null);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
setError(null);
|
|
415
|
+
refocusPathInput.current = true;
|
|
416
|
+
};
|
|
417
|
+
scan.then((target) => {
|
|
418
|
+
if (seq !== requestSeq.current) return;
|
|
419
|
+
if (!options.closeEditor && path !== void 0) scanned.current = {
|
|
420
|
+
directory: path,
|
|
421
|
+
landed: target.path
|
|
422
|
+
};
|
|
423
|
+
let landed = false;
|
|
424
|
+
const landSingle = () => {
|
|
425
|
+
if (landed || seq !== requestSeq.current) return;
|
|
426
|
+
landed = true;
|
|
427
|
+
setParent(target);
|
|
428
|
+
setSelected(null);
|
|
429
|
+
setChild(null);
|
|
430
|
+
settle();
|
|
431
|
+
};
|
|
432
|
+
if (displayCrumbs(target, "").length < 2) {
|
|
433
|
+
landSingle();
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const parentCrumb = target.crumbs.at(-2);
|
|
437
|
+
/* v8 ignore next -- narrowing: a two-deep display chain implies a parent crumb (root-to-target inclusive). */
|
|
438
|
+
if (parentCrumb === void 0) {
|
|
439
|
+
landSingle();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
continueScan(parentCrumb.path).then((parentLevel) => {
|
|
443
|
+
if (seq !== requestSeq.current) return;
|
|
444
|
+
const sep = separatorOf(parentLevel);
|
|
445
|
+
const fold = (value) => sep === "\\" ? value.toLowerCase() : value;
|
|
446
|
+
const match = parentLevel.entries.find((entry) => fold(entry.path) === fold(target.path));
|
|
447
|
+
if (match === void 0) {
|
|
448
|
+
landSingle();
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
landed = true;
|
|
452
|
+
setParent(parentLevel);
|
|
453
|
+
setSelected(match);
|
|
454
|
+
setChild(target);
|
|
455
|
+
settle();
|
|
456
|
+
}, () => {
|
|
457
|
+
landSingle();
|
|
458
|
+
});
|
|
459
|
+
if (options.closeEditor) window.setTimeout(landSingle, PARENT_LEG_WAIT_MS);
|
|
460
|
+
}, (reason) => {
|
|
461
|
+
if (seq !== requestSeq.current) return;
|
|
462
|
+
setLoading(false);
|
|
463
|
+
if (options.announce) setError(failureText(reason));
|
|
464
|
+
});
|
|
465
|
+
}, [launchListing, continueScan]);
|
|
466
|
+
/** Commit a submitted path (Enter, a crumb, the initial home listing): the editor closes, failures surface. */
|
|
467
|
+
const navigate = (0, react.useCallback)((path) => {
|
|
468
|
+
land(path, {
|
|
469
|
+
closeEditor: true,
|
|
470
|
+
announce: true
|
|
471
|
+
});
|
|
472
|
+
}, [land]);
|
|
473
|
+
const refocusPick = (0, react.useRef)(false);
|
|
474
|
+
const refocusEditZone = (0, react.useRef)(false);
|
|
475
|
+
const pathInputRef = (0, react.useRef)(null);
|
|
476
|
+
const editZoneRef = (0, react.useRef)(null);
|
|
477
|
+
/**
|
|
478
|
+
* Select a row of the listed level and preview its children on the right.
|
|
479
|
+
* Deliberately NOT one-frame like navigate(): a pick's first duty is the
|
|
480
|
+
* immediate selected state on the clicked row, and the pane split IS that
|
|
481
|
+
* feedback (aria-current pill, crumbs following the selection) — holding
|
|
482
|
+
* it back for the child listing would make clicks feel dropped. The quiet
|
|
483
|
+
* rule governs whole-view replacement, where nothing acknowledges the
|
|
484
|
+
* click but the swap itself.
|
|
485
|
+
*/
|
|
486
|
+
const select = (0, react.useCallback)((entry) => {
|
|
487
|
+
const { seq, scan } = launchListing(entry.path);
|
|
488
|
+
if (pathDraft !== null) refocusPick.current = true;
|
|
489
|
+
setPathDraft(null);
|
|
490
|
+
setSelected(entry);
|
|
491
|
+
setChild(null);
|
|
492
|
+
setLoading(true);
|
|
493
|
+
setError(null);
|
|
494
|
+
scan.then((next) => {
|
|
495
|
+
if (seq !== requestSeq.current) return;
|
|
496
|
+
setChild(next);
|
|
497
|
+
setLoading(false);
|
|
498
|
+
}, (reason) => {
|
|
499
|
+
if (seq !== requestSeq.current) return;
|
|
500
|
+
setLoading(false);
|
|
501
|
+
setError(failureText(reason));
|
|
502
|
+
setSelected(null);
|
|
503
|
+
refocusEditZone.current = true;
|
|
504
|
+
});
|
|
505
|
+
}, [launchListing, pathDraft]);
|
|
506
|
+
/**
|
|
507
|
+
* Walk the panes to the directory the draft addresses, WITHOUT closing the
|
|
508
|
+
* editor. The landing is an ordinary one — selection-anchored and two-pane
|
|
509
|
+
* away from the display root — so typing a path moves the Miller view
|
|
510
|
+
* exactly as a crumb jump does, and the draft's final segment
|
|
511
|
+
* prefix-filters the arrival from the next render on.
|
|
512
|
+
*/
|
|
513
|
+
const previewDraftLevel = (0, react.useCallback)((directory) => {
|
|
514
|
+
land(directory, {
|
|
515
|
+
closeEditor: false,
|
|
516
|
+
announce: false
|
|
517
|
+
});
|
|
518
|
+
}, [land]);
|
|
519
|
+
/** Abandon path editing (Escape or clicking away) and restore the crumb view. */
|
|
520
|
+
const cancelPathEdit = (0, react.useCallback)(() => {
|
|
521
|
+
supersede();
|
|
522
|
+
setLoading(false);
|
|
523
|
+
setPathDraft(null);
|
|
524
|
+
setError(null);
|
|
525
|
+
if (child === null) setSelected(null);
|
|
526
|
+
if (parent === null) navigate();
|
|
527
|
+
}, [
|
|
528
|
+
supersede,
|
|
529
|
+
child,
|
|
530
|
+
parent,
|
|
531
|
+
navigate
|
|
532
|
+
]);
|
|
533
|
+
/** A right-column pick advances the view one level: child becomes the level. */
|
|
534
|
+
const advance = (0, react.useCallback)((entry) => {
|
|
535
|
+
/* v8 ignore next -- narrowing guard: the right column only renders with a child listing. */
|
|
536
|
+
if (child === null) return;
|
|
537
|
+
setParent(child);
|
|
538
|
+
select(entry);
|
|
539
|
+
}, [child, select]);
|
|
540
|
+
(0, react.useEffect)(() => {
|
|
541
|
+
openGeneration.current += 1;
|
|
542
|
+
if (open) {
|
|
543
|
+
setParent(null);
|
|
544
|
+
setSelected(null);
|
|
545
|
+
setChild(null);
|
|
546
|
+
setCreatingFolder(false);
|
|
547
|
+
setShowHidden(false);
|
|
548
|
+
navigate();
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
supersede();
|
|
552
|
+
setLoading(false);
|
|
553
|
+
setError(null);
|
|
554
|
+
setPathDraft(null);
|
|
555
|
+
setFolderDraft(null);
|
|
556
|
+
setCreateError(null);
|
|
557
|
+
refocusPick.current = false;
|
|
558
|
+
refocusEditZone.current = false;
|
|
559
|
+
}, [
|
|
560
|
+
open,
|
|
561
|
+
navigate,
|
|
562
|
+
supersede
|
|
563
|
+
]);
|
|
564
|
+
/** The folder a create or Open acts on: the selection, else the listed level. */
|
|
565
|
+
const targetPath = selected?.path ?? parent?.path ?? null;
|
|
566
|
+
const targetName = selected?.name ?? (parent === null ? "" : displayCrumbs(parent, t("browser.home")).at(-1)?.name ?? parent.path);
|
|
567
|
+
const confirmCreate = () => {
|
|
568
|
+
/* v8 ignore next -- reentry fence: the nested dialog only renders with a target and disables while creating. */
|
|
569
|
+
if (targetPath === null || folderDraft === null || creatingFolder) return;
|
|
570
|
+
const name = folderDraft;
|
|
571
|
+
if (name.trim() === "") return;
|
|
572
|
+
setCreatingFolder(true);
|
|
573
|
+
setCreateError(null);
|
|
574
|
+
const generation = openGeneration.current;
|
|
575
|
+
createDirectory(targetPath, name).then((createdPath) => {
|
|
576
|
+
if (generation !== openGeneration.current) return;
|
|
577
|
+
setCreatingFolder(false);
|
|
578
|
+
setFolderDraft(null);
|
|
579
|
+
const { seq, scan } = launchListing(targetPath);
|
|
580
|
+
setLoading(true);
|
|
581
|
+
setError(null);
|
|
582
|
+
scan.then((level) => {
|
|
583
|
+
/* v8 ignore next -- same fence as navigate/select; the modal blocks superseding input */
|
|
584
|
+
if (seq !== requestSeq.current) return;
|
|
585
|
+
setParent(level);
|
|
586
|
+
setLoading(false);
|
|
587
|
+
select({
|
|
588
|
+
name,
|
|
589
|
+
path: createdPath,
|
|
590
|
+
hidden: false
|
|
591
|
+
});
|
|
592
|
+
}, (reason) => {
|
|
593
|
+
/* v8 ignore next -- same fence as navigate/select; the modal blocks superseding input */
|
|
594
|
+
if (seq !== requestSeq.current) return;
|
|
595
|
+
setLoading(false);
|
|
596
|
+
setError(failureText(reason));
|
|
597
|
+
});
|
|
598
|
+
}, (reason) => {
|
|
599
|
+
if (generation !== openGeneration.current) return;
|
|
600
|
+
setCreatingFolder(false);
|
|
601
|
+
setCreateError(failureText(reason));
|
|
602
|
+
});
|
|
603
|
+
};
|
|
604
|
+
(0, react.useEffect)(() => {
|
|
605
|
+
if (!loading) {
|
|
606
|
+
setSlowScan(false);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const timer = window.setTimeout(() => {
|
|
610
|
+
setSlowScan(true);
|
|
611
|
+
}, SLOW_SCAN_DELAY_MS);
|
|
612
|
+
return () => {
|
|
613
|
+
window.clearTimeout(timer);
|
|
614
|
+
};
|
|
615
|
+
}, [loading, scanWindow]);
|
|
616
|
+
(0, react.useEffect)(() => {
|
|
617
|
+
if (pathDraft === null) return;
|
|
618
|
+
const timer = window.setTimeout(() => {
|
|
619
|
+
if (previewSuspended.current) return;
|
|
620
|
+
const current = viewRef.current.child ?? viewRef.current.parent;
|
|
621
|
+
if (current === null) return;
|
|
622
|
+
const { directory, tail } = readDraft(current, pathDraft, scanned.current);
|
|
623
|
+
if (directory === null || tail !== null) return;
|
|
624
|
+
previewDraftLevel(directory);
|
|
625
|
+
}, DRAFT_PREVIEW_DEBOUNCE_MS);
|
|
626
|
+
return () => {
|
|
627
|
+
window.clearTimeout(timer);
|
|
628
|
+
};
|
|
629
|
+
}, [pathDraft, previewDraftLevel]);
|
|
630
|
+
const crumbSource = child ?? parent;
|
|
631
|
+
const typedPrefix = crumbSource === null || pathDraft === null ? null : readDraft(crumbSource, pathDraft, scanned.current).tail;
|
|
632
|
+
const crumbs = crumbSource === null ? [] : displayCrumbs(crumbSource, t("browser.home"));
|
|
633
|
+
const crumbTail = crumbs.at(-1)?.path;
|
|
634
|
+
(0, react.useEffect)(() => {
|
|
635
|
+
const trail = crumbTrailRef.current;
|
|
636
|
+
if (trail !== null) trail.scrollLeft = trail.scrollWidth;
|
|
637
|
+
}, [crumbTail]);
|
|
638
|
+
const millerRowRef = (0, react.useRef)(null);
|
|
639
|
+
const childPath = child?.path;
|
|
640
|
+
(0, react.useEffect)(() => {
|
|
641
|
+
const row = millerRowRef.current;
|
|
642
|
+
if (row !== null && childPath !== void 0) row.scrollLeft = row.scrollWidth;
|
|
643
|
+
}, [childPath]);
|
|
644
|
+
(0, react.useEffect)(() => {
|
|
645
|
+
if (refocusPathInput.current) {
|
|
646
|
+
refocusPathInput.current = false;
|
|
647
|
+
if (document.activeElement === document.body) pathInputRef.current?.focus();
|
|
648
|
+
}
|
|
649
|
+
if (pathDraft !== null) return;
|
|
650
|
+
if (refocusPick.current) {
|
|
651
|
+
refocusPick.current = false;
|
|
652
|
+
refocusEditZone.current = false;
|
|
653
|
+
const rowHost = millerRowRef.current;
|
|
654
|
+
/* v8 ignore next -- narrowing guard: the miller row is mounted whenever a pick just committed. */
|
|
655
|
+
if (rowHost === null) return;
|
|
656
|
+
const row = rowHost.querySelector("button[aria-current=\"true\"]");
|
|
657
|
+
/* v8 ignore next -- narrowing guard: the pick that set the flag just rendered its aria-current row. */
|
|
658
|
+
if (row === null) return;
|
|
659
|
+
row.focus();
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (refocusEditZone.current) {
|
|
663
|
+
refocusEditZone.current = false;
|
|
664
|
+
if (document.activeElement !== document.body) return;
|
|
665
|
+
const zone = editZoneRef.current;
|
|
666
|
+
/* v8 ignore next -- narrowing guard: crumb mode renders the edit zone whenever the editor just closed. */
|
|
667
|
+
if (zone === null) return;
|
|
668
|
+
zone.focus();
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
if (!open) return null;
|
|
672
|
+
const twoPane = selected !== null;
|
|
673
|
+
const parentInert = busy || folderDraft !== null;
|
|
674
|
+
const draftPending = pathDraft !== null;
|
|
675
|
+
return (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
676
|
+
open,
|
|
677
|
+
onClose: () => {
|
|
678
|
+
if (folderDraft === null && !busy) onClose();
|
|
679
|
+
},
|
|
680
|
+
title: t("browser.title"),
|
|
681
|
+
className: clsx(DirectoryBrowser_module_css_default.dialog),
|
|
682
|
+
headless: true,
|
|
683
|
+
children: [(0, react_jsx_runtime.jsxs)("div", {
|
|
684
|
+
className: DirectoryBrowser_module_css_default.editorScope,
|
|
685
|
+
onKeyDown: (event) => {
|
|
686
|
+
if (event.key !== "Escape" || pathDraft === null) return;
|
|
687
|
+
event.stopPropagation();
|
|
688
|
+
refocusEditZone.current = document.activeElement === pathInputRef.current;
|
|
689
|
+
cancelPathEdit();
|
|
690
|
+
},
|
|
691
|
+
onBlur: (event) => {
|
|
692
|
+
if (pathDraft === null) return;
|
|
693
|
+
if (!document.hasFocus()) return;
|
|
694
|
+
const card = event.currentTarget.closest("[role=\"dialog\"]");
|
|
695
|
+
/* v8 ignore next -- narrowing guard: this scope always renders inside the Modal card. */
|
|
696
|
+
if (card === null) return;
|
|
697
|
+
if (event.relatedTarget instanceof Node && card.contains(event.relatedTarget)) return;
|
|
698
|
+
refocusEditZone.current = false;
|
|
699
|
+
cancelPathEdit();
|
|
700
|
+
},
|
|
701
|
+
children: [
|
|
702
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
703
|
+
className: DirectoryBrowser_module_css_default.header,
|
|
704
|
+
children: [(0, react_jsx_runtime.jsx)("h2", {
|
|
705
|
+
className: DirectoryBrowser_module_css_default.title,
|
|
706
|
+
children: t("browser.title")
|
|
707
|
+
}), (0, react_jsx_runtime.jsx)("div", {
|
|
708
|
+
className: DirectoryBrowser_module_css_default.crumbBar,
|
|
709
|
+
children: pathDraft === null ? (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("span", {
|
|
710
|
+
className: DirectoryBrowser_module_css_default.crumbTrail,
|
|
711
|
+
role: "navigation",
|
|
712
|
+
ref: crumbTrailRef,
|
|
713
|
+
children: crumbs.map((crumb, index) => (0, react_jsx_runtime.jsxs)("span", {
|
|
714
|
+
className: DirectoryBrowser_module_css_default.crumbSeat,
|
|
715
|
+
children: [index > 0 && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, {
|
|
716
|
+
size: 12,
|
|
717
|
+
className: DirectoryBrowser_module_css_default.crumbChevron
|
|
718
|
+
}), (0, react_jsx_runtime.jsx)("button", {
|
|
719
|
+
type: "button",
|
|
720
|
+
className: DirectoryBrowser_module_css_default.crumb,
|
|
721
|
+
disabled: parentInert,
|
|
722
|
+
onClick: () => {
|
|
723
|
+
navigate(crumb.path);
|
|
724
|
+
},
|
|
725
|
+
children: crumb.name
|
|
726
|
+
})]
|
|
727
|
+
}, crumb.path))
|
|
728
|
+
}), (0, react_jsx_runtime.jsx)("button", {
|
|
729
|
+
type: "button",
|
|
730
|
+
className: DirectoryBrowser_module_css_default.crumbEditZone,
|
|
731
|
+
"aria-label": t("browser.editPath"),
|
|
732
|
+
title: t("browser.editPath"),
|
|
733
|
+
disabled: parentInert,
|
|
734
|
+
ref: editZoneRef,
|
|
735
|
+
onClick: () => {
|
|
736
|
+
supersede();
|
|
737
|
+
setLoading(false);
|
|
738
|
+
previewSuspended.current = false;
|
|
739
|
+
if (parent === null) {
|
|
740
|
+
setPathDraft("");
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
const base = selected?.path ?? parent.path;
|
|
744
|
+
const sep = separatorOf(parent);
|
|
745
|
+
setPathDraft(base.endsWith(sep) ? base : `${base}${sep}`);
|
|
746
|
+
},
|
|
747
|
+
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, {
|
|
748
|
+
size: 14,
|
|
749
|
+
className: DirectoryBrowser_module_css_default.crumbEditGlyph
|
|
750
|
+
})
|
|
751
|
+
})] }) : (0, react_jsx_runtime.jsx)("input", {
|
|
752
|
+
className: DirectoryBrowser_module_css_default.pathInput,
|
|
753
|
+
value: pathDraft,
|
|
754
|
+
"aria-label": t("browser.editPath"),
|
|
755
|
+
autoFocus: true,
|
|
756
|
+
ref: pathInputRef,
|
|
757
|
+
disabled: parentInert,
|
|
758
|
+
onChange: (event) => {
|
|
759
|
+
supersede();
|
|
760
|
+
setLoading(false);
|
|
761
|
+
previewSuspended.current = false;
|
|
762
|
+
setPathDraft(event.target.value);
|
|
763
|
+
},
|
|
764
|
+
...compositionGuard,
|
|
765
|
+
onKeyDown: (event) => {
|
|
766
|
+
if (event.key === "Enter" && !composingRef.current) {
|
|
767
|
+
event.preventDefault();
|
|
768
|
+
if (pathDraft.trim() !== "") {
|
|
769
|
+
refocusEditZone.current = true;
|
|
770
|
+
previewSuspended.current = true;
|
|
771
|
+
navigate(pathDraft);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
})
|
|
776
|
+
})]
|
|
777
|
+
}),
|
|
778
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
779
|
+
className: DirectoryBrowser_module_css_default.content,
|
|
780
|
+
children: [
|
|
781
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
782
|
+
className: DirectoryBrowser_module_css_default.millerRow,
|
|
783
|
+
ref: millerRowRef,
|
|
784
|
+
children: [
|
|
785
|
+
parent !== null && (0, react_jsx_runtime.jsx)(LevelColumn, {
|
|
786
|
+
entries: parent.entries,
|
|
787
|
+
selectedPath: selected?.path ?? null,
|
|
788
|
+
busy: parentInert,
|
|
789
|
+
onPick: select,
|
|
790
|
+
showHidden,
|
|
791
|
+
filterPrefix: child === null ? typedPrefix : null,
|
|
792
|
+
pathEditing: draftPending
|
|
793
|
+
}),
|
|
794
|
+
twoPane && (0, react_jsx_runtime.jsx)("span", { className: DirectoryBrowser_module_css_default.divider }),
|
|
795
|
+
twoPane && child !== null && (0, react_jsx_runtime.jsx)(LevelColumn, {
|
|
796
|
+
entries: child.entries,
|
|
797
|
+
selectedPath: null,
|
|
798
|
+
busy: parentInert,
|
|
799
|
+
onPick: advance,
|
|
800
|
+
showHidden,
|
|
801
|
+
filterPrefix: typedPrefix,
|
|
802
|
+
pathEditing: draftPending
|
|
803
|
+
})
|
|
804
|
+
]
|
|
805
|
+
}),
|
|
806
|
+
loading && slowScan && (0, react_jsx_runtime.jsx)("div", {
|
|
807
|
+
className: clsx(DirectoryBrowser_module_css_default.status, DirectoryBrowser_module_css_default.loadingFloat),
|
|
808
|
+
role: "status",
|
|
809
|
+
children: t("browser.loading")
|
|
810
|
+
}),
|
|
811
|
+
(parent?.truncated === true || child?.truncated === true) && (0, react_jsx_runtime.jsx)("div", {
|
|
812
|
+
className: DirectoryBrowser_module_css_default.status,
|
|
813
|
+
role: "status",
|
|
814
|
+
children: t("browser.truncated")
|
|
815
|
+
}),
|
|
816
|
+
error !== null && (0, react_jsx_runtime.jsx)("div", {
|
|
817
|
+
className: DirectoryBrowser_module_css_default.error,
|
|
818
|
+
role: "alert",
|
|
819
|
+
children: error
|
|
820
|
+
})
|
|
821
|
+
]
|
|
822
|
+
}),
|
|
823
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
824
|
+
className: DirectoryBrowser_module_css_default.footerBar,
|
|
825
|
+
children: [
|
|
826
|
+
(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
827
|
+
variant: "outline",
|
|
828
|
+
icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, { size: 14 }),
|
|
829
|
+
disabled: parent === null || loading || parentInert || draftPending,
|
|
830
|
+
onClick: () => {
|
|
831
|
+
setFolderDraft("");
|
|
832
|
+
setCreateError(null);
|
|
833
|
+
},
|
|
834
|
+
children: t("browser.newFolder")
|
|
835
|
+
}),
|
|
836
|
+
(0, react_jsx_runtime.jsxs)("button", {
|
|
837
|
+
type: "button",
|
|
838
|
+
className: clsx(DirectoryBrowser_module_css_default.showHiddenToggle, showHidden && DirectoryBrowser_module_css_default.showHiddenToggleActive),
|
|
839
|
+
"aria-pressed": showHidden,
|
|
840
|
+
disabled: parentInert,
|
|
841
|
+
onMouseDown: draftPending ? (event) => {
|
|
842
|
+
event.preventDefault();
|
|
843
|
+
} : void 0,
|
|
844
|
+
onClick: () => {
|
|
845
|
+
setShowHidden((prev) => !prev);
|
|
846
|
+
},
|
|
847
|
+
children: [t("browser.showHidden"), showHidden && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, { size: 14 })]
|
|
848
|
+
}),
|
|
849
|
+
(0, react_jsx_runtime.jsx)("span", { className: DirectoryBrowser_module_css_default.footerGap }),
|
|
850
|
+
(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
851
|
+
variant: "outline",
|
|
852
|
+
className: clsx(DirectoryBrowser_module_css_default.footerAction),
|
|
853
|
+
disabled: parentInert,
|
|
854
|
+
onClick: onClose,
|
|
855
|
+
children: t("browser.cancel")
|
|
856
|
+
}),
|
|
857
|
+
(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
858
|
+
variant: "primary",
|
|
859
|
+
className: clsx(DirectoryBrowser_module_css_default.footerAction),
|
|
860
|
+
disabled: targetPath === null || loading || parentInert || draftPending,
|
|
861
|
+
/* v8 ignore next -- narrowing guard: Open disables while no target exists. */
|
|
862
|
+
onClick: () => {
|
|
863
|
+
if (targetPath !== null) onOpen(targetPath);
|
|
864
|
+
},
|
|
865
|
+
children: t("browser.open")
|
|
866
|
+
})
|
|
867
|
+
]
|
|
868
|
+
})
|
|
869
|
+
]
|
|
870
|
+
}), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
871
|
+
open: folderDraft !== null,
|
|
872
|
+
onClose: () => {
|
|
873
|
+
if (!creatingFolder) setFolderDraft(null);
|
|
874
|
+
},
|
|
875
|
+
title: t("browser.newFolder"),
|
|
876
|
+
className: clsx(DirectoryBrowser_module_css_default.createDialog),
|
|
877
|
+
headless: true,
|
|
878
|
+
children: (0, react_jsx_runtime.jsxs)("div", {
|
|
879
|
+
className: DirectoryBrowser_module_css_default.createBody,
|
|
880
|
+
children: [
|
|
881
|
+
(0, react_jsx_runtime.jsx)("h3", {
|
|
882
|
+
className: DirectoryBrowser_module_css_default.createTitle,
|
|
883
|
+
children: t("browser.newFolder")
|
|
884
|
+
}),
|
|
885
|
+
(0, react_jsx_runtime.jsx)("p", {
|
|
886
|
+
className: DirectoryBrowser_module_css_default.createIn,
|
|
887
|
+
children: t("browser.createIn", { name: targetName })
|
|
888
|
+
}),
|
|
889
|
+
(0, react_jsx_runtime.jsx)("input", {
|
|
890
|
+
className: DirectoryBrowser_module_css_default.createInput,
|
|
891
|
+
value: folderDraft ?? "",
|
|
892
|
+
"aria-label": t("browser.folderName"),
|
|
893
|
+
placeholder: t("browser.untitledFolder"),
|
|
894
|
+
autoFocus: true,
|
|
895
|
+
disabled: creatingFolder,
|
|
896
|
+
onChange: (event) => {
|
|
897
|
+
setFolderDraft(event.target.value);
|
|
898
|
+
},
|
|
899
|
+
...compositionGuard,
|
|
900
|
+
onKeyDown: (event) => {
|
|
901
|
+
if (event.key === "Enter" && !composingRef.current) {
|
|
902
|
+
event.preventDefault();
|
|
903
|
+
confirmCreate();
|
|
904
|
+
}
|
|
905
|
+
if (event.key === "Escape") {
|
|
906
|
+
event.stopPropagation();
|
|
907
|
+
if (!creatingFolder) setFolderDraft(null);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}),
|
|
911
|
+
createError !== null && (0, react_jsx_runtime.jsx)("div", {
|
|
912
|
+
className: DirectoryBrowser_module_css_default.error,
|
|
913
|
+
role: "alert",
|
|
914
|
+
children: createError
|
|
915
|
+
}),
|
|
916
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
917
|
+
className: DirectoryBrowser_module_css_default.createActions,
|
|
918
|
+
children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
919
|
+
variant: "outline",
|
|
920
|
+
disabled: creatingFolder,
|
|
921
|
+
onClick: () => {
|
|
922
|
+
setFolderDraft(null);
|
|
923
|
+
},
|
|
924
|
+
children: t("browser.cancel")
|
|
925
|
+
}), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
926
|
+
variant: "primary",
|
|
927
|
+
disabled: creatingFolder || folderDraft === null || folderDraft.trim() === "",
|
|
928
|
+
onClick: confirmCreate,
|
|
929
|
+
children: t("browser.create")
|
|
930
|
+
})]
|
|
931
|
+
})
|
|
932
|
+
]
|
|
933
|
+
})
|
|
934
|
+
})]
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
//#endregion
|
|
938
|
+
//#region lib/types/client/flow.js
|
|
939
|
+
/**
|
|
940
|
+
* The browse picking occupant (package-internal; the `./client` surface
|
|
941
|
+
* exposes only the Loader exports). Same-package tests exercise it directly
|
|
942
|
+
* through this module.
|
|
943
|
+
*/
|
|
944
|
+
/**
|
|
945
|
+
* Flow occupant: adapts the hole's owner conversation onto the browser
|
|
946
|
+
* dialog — a confirmed directory is the picked path, dismissal is the
|
|
947
|
+
* cancellation. Browse failures (unreadable targets, create conflicts) stay
|
|
948
|
+
* inside the dialog's own alert surfaces, so the owner's `onError` arm is
|
|
949
|
+
* never driven by this occupant.
|
|
950
|
+
* @param props - owner conversation plus the injected browse face.
|
|
951
|
+
* @returns the dialog element (renders nothing while closed).
|
|
952
|
+
*/
|
|
953
|
+
function BrowseDirectoryFlow(props) {
|
|
954
|
+
return (0, react.createElement)(DirectoryBrowser, {
|
|
955
|
+
open: props.open,
|
|
956
|
+
busy: props.busy,
|
|
957
|
+
listDirectory: props.listDirectory,
|
|
958
|
+
createDirectory: props.createDirectory,
|
|
959
|
+
t: props.t,
|
|
960
|
+
onOpen: props.onPicked,
|
|
961
|
+
onClose: props.onCancel
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
//#endregion
|
|
965
|
+
//#region lib/types/client/index.js
|
|
966
|
+
/** Locale namespace owning the browser dialog's copy. */
|
|
967
|
+
const LOCALE_NS = "directory-browser";
|
|
968
|
+
/** Required services (cordis fiber inject): the slot registry, workspace UI service, and locale. */
|
|
969
|
+
const inject = [
|
|
970
|
+
"slots",
|
|
971
|
+
"uiWorkspace",
|
|
972
|
+
"locale"
|
|
973
|
+
];
|
|
974
|
+
/**
|
|
975
|
+
* Client plugin body: register the dialog's dictionaries and the browse flow
|
|
976
|
+
* into both directory-flow holes through `slots.inject()` because the
|
|
977
|
+
* ui-workspace entries may activate later or replace their declarations.
|
|
978
|
+
* @param ctx - client root context.
|
|
979
|
+
*/
|
|
980
|
+
function apply(ctx) {
|
|
981
|
+
ctx.effect(() => {
|
|
982
|
+
const disposers = [];
|
|
983
|
+
const dictionaries = [["zh", {
|
|
984
|
+
"browser.title": "选择工作区目录",
|
|
985
|
+
"browser.home": "主目录",
|
|
986
|
+
"browser.newFolder": "新建文件夹",
|
|
987
|
+
"browser.folderName": "文件夹名称",
|
|
988
|
+
"browser.createIn": "在\"{name}\"中新建文件夹",
|
|
989
|
+
"browser.untitledFolder": "未命名文件夹",
|
|
990
|
+
"browser.create": "创建",
|
|
991
|
+
"browser.cancel": "取消",
|
|
992
|
+
"browser.open": "打开",
|
|
993
|
+
"browser.editPath": "编辑路径",
|
|
994
|
+
"browser.loading": "加载中…",
|
|
995
|
+
"browser.truncated": "文件夹过多,仅显示开头部分。",
|
|
996
|
+
"browser.showHidden": "显示隐藏文件"
|
|
997
|
+
}], ["en", {
|
|
998
|
+
"browser.title": "Select Workspace Directory",
|
|
999
|
+
"browser.home": "Home",
|
|
1000
|
+
"browser.newFolder": "New folder",
|
|
1001
|
+
"browser.folderName": "Folder name",
|
|
1002
|
+
"browser.createIn": "New folder in \"{name}\"",
|
|
1003
|
+
"browser.untitledFolder": "Untitled folder",
|
|
1004
|
+
"browser.create": "Create",
|
|
1005
|
+
"browser.cancel": "Cancel",
|
|
1006
|
+
"browser.open": "Open",
|
|
1007
|
+
"browser.editPath": "Edit path",
|
|
1008
|
+
"browser.loading": "Loading…",
|
|
1009
|
+
"browser.truncated": "Too many folders to list; only the beginning is shown.",
|
|
1010
|
+
"browser.showHidden": "Show hidden files"
|
|
1011
|
+
}]];
|
|
1012
|
+
try {
|
|
1013
|
+
for (const [locale, dict] of dictionaries) disposers.push(ctx.locale.register(LOCALE_NS, locale, dict));
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
for (const dispose of disposers.reverse()) dispose();
|
|
1016
|
+
throw error;
|
|
1017
|
+
}
|
|
1018
|
+
return () => {
|
|
1019
|
+
for (const dispose of disposers) dispose();
|
|
1020
|
+
};
|
|
1021
|
+
}, "directory-picker-browse: dialog dictionaries");
|
|
1022
|
+
const injected = () => ({
|
|
1023
|
+
listDirectory: (path, signal) => ctx.uiWorkspace.listDirectory(path, signal),
|
|
1024
|
+
createDirectory: (path, name) => ctx.uiWorkspace.createDirectory(path, name),
|
|
1025
|
+
t: ctx.locale.bind(LOCALE_NS)
|
|
1026
|
+
});
|
|
1027
|
+
ctx.slots.inject("conversation.hero.workspace.directoryFlow", () => ctx.slots.inject("sidebar.workspaces.directoryFlow", function* () {
|
|
1028
|
+
yield ctx.slots.register({
|
|
1029
|
+
name: "conversation.hero.workspace.directoryFlow",
|
|
1030
|
+
inject: injected
|
|
1031
|
+
}, BrowseDirectoryFlow);
|
|
1032
|
+
yield ctx.slots.register({
|
|
1033
|
+
name: "sidebar.workspaces.directoryFlow",
|
|
1034
|
+
inject: injected
|
|
1035
|
+
}, BrowseDirectoryFlow);
|
|
1036
|
+
}));
|
|
1037
|
+
}
|
|
1038
|
+
//#endregion
|
|
1039
|
+
exports.apply = apply;
|
|
1040
|
+
exports.inject = inject;
|
|
1041
|
+
return module.exports;
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
//# sourceMappingURL=client.js.map
|