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.
@@ -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