dsh-mobile 0.6.17 → 0.6.18

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/lib/client.js CHANGED
@@ -8145,7 +8145,8 @@ window.__ModuleLoader__.load({
8145
8145
  draft: "",
8146
8146
  error: null,
8147
8147
  hostBusy: false,
8148
- committed: false
8148
+ committed: false,
8149
+ creating: false
8149
8150
  });
8150
8151
  /** Read navigation stops on close; committing is final until the owner withdraws this flow. */
8151
8152
  var MobileDirectoryBrowser = class {
@@ -8199,6 +8200,14 @@ window.__ModuleLoader__.load({
8199
8200
  hostBusy
8200
8201
  });
8201
8202
  }
8203
+ /** Block navigation/adoption while this flow's child-folder editor owns interaction. */
8204
+ setCreating(creating) {
8205
+ if (this.disposed || this.snapshot.creating === creating) return;
8206
+ this.publish({
8207
+ ...this.snapshot,
8208
+ creating
8209
+ });
8210
+ }
8202
8211
  /** Read a folder or the initial home; superseded results cannot publish into the new request. */
8203
8212
  navigate(path) {
8204
8213
  if (!this.canNavigate()) return false;
@@ -8281,7 +8290,7 @@ window.__ModuleLoader__.load({
8281
8290
  }
8282
8291
  /** Cancel reading immediately; a committed/Host-busy adoption cannot be reported as cancelled. */
8283
8292
  cancel() {
8284
- if (!this.canNavigate()) return false;
8293
+ if (!this.canClose()) return false;
8285
8294
  this.withdrawRead();
8286
8295
  this.publish(CLOSED);
8287
8296
  return true;
@@ -8295,9 +8304,12 @@ window.__ModuleLoader__.load({
8295
8304
  await Promise.allSettled([...this.tasks]);
8296
8305
  if (this.taskFailures.length > 0) throw new AggregateError(this.taskFailures, "Mobile directory observers failed during a listing");
8297
8306
  }
8298
- canNavigate() {
8307
+ canClose() {
8299
8308
  return !this.disposed && this.snapshot.open && !this.snapshot.hostBusy && !this.snapshot.committed;
8300
8309
  }
8310
+ canNavigate() {
8311
+ return this.canClose() && !this.snapshot.creating;
8312
+ }
8301
8313
  current(generation) {
8302
8314
  return !this.disposed && this.snapshot.open && generation === this.generation;
8303
8315
  }
@@ -8311,6 +8323,171 @@ window.__ModuleLoader__.load({
8311
8323
  for (const listener of this.listeners) listener();
8312
8324
  }
8313
8325
  };
8326
+ /** A creation failure without raw response bodies or filesystem details. */
8327
+ var MobileDirectoryCreateError = class extends Error {
8328
+ code;
8329
+ constructor(code) {
8330
+ super(`Mobile directory creation failed: ${code}`);
8331
+ this.code = code;
8332
+ this.name = "MobileDirectoryCreateError";
8333
+ }
8334
+ };
8335
+ /** Portable single-segment validation; the Host additionally enforces its filesystem rules. */
8336
+ function isMobileDirectoryName(name) {
8337
+ return name.trim() !== "" && name.length <= 255 && name !== "." && name !== ".." && !/[\\/\0]/u.test(name);
8338
+ }
8339
+ const CREATE_CLOSED = Object.freeze({
8340
+ open: false,
8341
+ parent: null,
8342
+ name: "",
8343
+ pending: false,
8344
+ error: null
8345
+ });
8346
+ /** Owns one non-replayed write, its deadline, cancellation and physical teardown. */
8347
+ var MobileDirectoryCreator = class {
8348
+ create;
8349
+ onCreated;
8350
+ deadlineMs;
8351
+ snapshot = CREATE_CLOSED;
8352
+ listeners = /* @__PURE__ */ new Set();
8353
+ tasks = /* @__PURE__ */ new Set();
8354
+ failures = [];
8355
+ active;
8356
+ generation = 0;
8357
+ disposed = false;
8358
+ constructor(create, onCreated, deadlineMs = 3e4) {
8359
+ this.create = create;
8360
+ this.onCreated = onCreated;
8361
+ this.deadlineMs = deadlineMs;
8362
+ }
8363
+ getSnapshot = () => this.snapshot;
8364
+ /** Subscribe to this editor; the returned disposer withdraws the observer. */
8365
+ subscribe(listener) {
8366
+ this.listeners.add(listener);
8367
+ return () => {
8368
+ this.listeners.delete(listener);
8369
+ };
8370
+ }
8371
+ /** Open against one already confirmed parent; cancelled physical work retains admission. */
8372
+ open(parent) {
8373
+ if (this.disposed || this.snapshot.open || this.tasks.size > 0 || !isMobileDirectoryPath(parent)) return false;
8374
+ this.publish({
8375
+ ...CREATE_CLOSED,
8376
+ open: true,
8377
+ parent
8378
+ });
8379
+ return true;
8380
+ }
8381
+ /** Edit only while no write is in flight; typing never starts a filesystem operation. */
8382
+ setName(name) {
8383
+ if (this.disposed || !this.snapshot.open || this.snapshot.pending) return;
8384
+ this.publish({
8385
+ ...this.snapshot,
8386
+ name,
8387
+ error: null
8388
+ });
8389
+ }
8390
+ /** Admit one explicit creation. Repeated clicks/Enter cannot replay an admitted request. */
8391
+ submit() {
8392
+ if (this.disposed || !this.snapshot.open || this.snapshot.pending || this.tasks.size > 0 || this.snapshot.parent === null) return false;
8393
+ const name = this.snapshot.name.trim();
8394
+ if (!isMobileDirectoryName(name)) {
8395
+ this.publish({
8396
+ ...this.snapshot,
8397
+ error: "invalid-name"
8398
+ });
8399
+ return false;
8400
+ }
8401
+ const parent = this.snapshot.parent;
8402
+ const generation = this.generation;
8403
+ const controller = new AbortController();
8404
+ this.active = controller;
8405
+ this.publish({
8406
+ ...this.snapshot,
8407
+ pending: true,
8408
+ error: null
8409
+ });
8410
+ if (!this.current(generation) || controller.signal.aborted) {
8411
+ if (this.active === controller) this.active = void 0;
8412
+ if (!this.disposed) this.publish({
8413
+ ...this.snapshot,
8414
+ pending: false
8415
+ });
8416
+ return false;
8417
+ }
8418
+ const timer = setTimeout(() => {
8419
+ controller.abort(new MobileDirectoryCreateError("timeout"));
8420
+ if (this.current(generation)) this.publish({
8421
+ ...this.snapshot,
8422
+ error: "timeout"
8423
+ });
8424
+ }, this.deadlineMs);
8425
+ const task = (async () => {
8426
+ let created;
8427
+ try {
8428
+ created = await this.create(parent, name, controller.signal);
8429
+ controller.signal.throwIfAborted();
8430
+ if (!isMobileDirectoryPath(created)) throw new MobileDirectoryCreateError("invalid-response");
8431
+ } catch (error) {
8432
+ created = void 0;
8433
+ if (this.current(generation)) {
8434
+ const failure = controller.signal.aborted ? controller.signal.reason : error;
8435
+ this.publish({
8436
+ ...this.snapshot,
8437
+ error: failure instanceof MobileDirectoryCreateError ? failure.code : "request-failed"
8438
+ });
8439
+ }
8440
+ } finally {
8441
+ clearTimeout(timer);
8442
+ if (this.active === controller) this.active = void 0;
8443
+ if (!this.disposed) this.publish({
8444
+ ...this.snapshot,
8445
+ pending: false
8446
+ });
8447
+ }
8448
+ if (created !== void 0 && this.current(generation)) {
8449
+ this.publish(CREATE_CLOSED);
8450
+ this.onCreated(created);
8451
+ }
8452
+ })();
8453
+ this.tasks.add(task);
8454
+ task.then(() => {
8455
+ this.tasks.delete(task);
8456
+ }, (error) => {
8457
+ this.tasks.delete(task);
8458
+ this.failures.push(error);
8459
+ });
8460
+ return true;
8461
+ }
8462
+ /** Withdraw the editor and late results; an already completed mkdir is not rolled back. */
8463
+ close() {
8464
+ if (this.disposed || !this.snapshot.open) return false;
8465
+ this.generation++;
8466
+ this.active?.abort();
8467
+ this.publish({
8468
+ ...CREATE_CLOSED,
8469
+ pending: this.active !== void 0
8470
+ });
8471
+ return true;
8472
+ }
8473
+ /** Stop admission synchronously and await every accepted write before completion. */
8474
+ async dispose() {
8475
+ this.disposed = true;
8476
+ this.generation++;
8477
+ this.active?.abort();
8478
+ this.snapshot = CREATE_CLOSED;
8479
+ this.listeners.clear();
8480
+ await Promise.allSettled([...this.tasks]);
8481
+ if (this.failures.length > 0) throw new AggregateError(this.failures, "Mobile directory creation observers failed");
8482
+ }
8483
+ current(generation) {
8484
+ return !this.disposed && this.snapshot.open && generation === this.generation;
8485
+ }
8486
+ publish(snapshot) {
8487
+ this.snapshot = Object.freeze(snapshot);
8488
+ for (const listener of this.listeners) listener();
8489
+ }
8490
+ };
8314
8491
  //#endregion
8315
8492
  //#region src/mobile-directory-picker.ts
8316
8493
  /** Dedicated Mobile directory-flow occupant; stock DSH retains its own chooser. */
@@ -8335,7 +8512,17 @@ window.__ModuleLoader__.load({
8335
8512
  "invalid-response": "目录响应不可用,请重试或重新连接电脑",
8336
8513
  "request-failed": "读取目录失败,请检查连接后重试",
8337
8514
  timeout: "读取目录超时,请重试或取消",
8338
- failedOpen: "无法打开目录选择器,请重新连接后重试"
8515
+ failedOpen: "无法打开目录选择器,请重新连接后重试",
8516
+ create: "新建文件夹",
8517
+ createTitle: "新建文件夹名称",
8518
+ createAction: "创建",
8519
+ createInvalid: "请输入单个文件夹名称,不能包含路径分隔符。",
8520
+ createExists: "同名文件夹已存在。",
8521
+ createFailed: "无法创建文件夹,请检查该目录的写入权限后重试。",
8522
+ createParent: "创建位置",
8523
+ creating: "正在创建文件夹…",
8524
+ createStopping: "正在结束创建请求…",
8525
+ createUnknown: "无法确认创建结果,文件夹可能已创建。请返回并重新读取目录,确认后再决定是否重试。"
8339
8526
  },
8340
8527
  en: {
8341
8528
  title: "Select workspace directory",
@@ -8357,7 +8544,17 @@ window.__ModuleLoader__.load({
8357
8544
  "invalid-response": "The directory response is unavailable. Retry or reconnect to the computer.",
8358
8545
  "request-failed": "Could not load this directory. Check the connection and retry.",
8359
8546
  timeout: "Loading timed out. Retry or cancel.",
8360
- failedOpen: "Could not open the directory picker. Reconnect and retry."
8547
+ failedOpen: "Could not open the directory picker. Reconnect and retry.",
8548
+ create: "New folder",
8549
+ createTitle: "New folder name",
8550
+ createAction: "Create",
8551
+ createInvalid: "Enter a single folder name without path separators.",
8552
+ createExists: "A folder with that name already exists.",
8553
+ createFailed: "Could not create the folder. Check write access to this directory and retry.",
8554
+ createParent: "Create in",
8555
+ creating: "Creating folder…",
8556
+ createStopping: "Finishing the creation request…",
8557
+ createUnknown: "The creation result is unknown; the folder may already exist. Go back and reload the directory before deciding whether to retry."
8361
8558
  },
8362
8559
  it: {
8363
8560
  title: "Seleziona la cartella del workspace",
@@ -8378,11 +8575,21 @@ window.__ModuleLoader__.load({
8378
8575
  unauthorized: "Autorizzazione non disponibile. Chiudi questa finestra e riconnettiti al computer.",
8379
8576
  "invalid-response": "Risposta della cartella non disponibile. Riprova o riconnettiti al computer.",
8380
8577
  "request-failed": "Impossibile caricare la cartella. Controlla la connessione e riprova.",
8578
+ create: "Nuova cartella",
8579
+ createTitle: "Nome della nuova cartella",
8580
+ createAction: "Crea",
8581
+ createInvalid: "Inserisci un singolo nome di cartella senza separatori di percorso.",
8582
+ createExists: "Esiste già una cartella con questo nome.",
8583
+ createFailed: "Impossibile creare la cartella. Controlla i permessi di scrittura e riprova.",
8381
8584
  timeout: "Caricamento scaduto. Riprova o annulla.",
8382
- failedOpen: "Impossibile aprire la selezione della cartella. Riconnettiti e riprova."
8585
+ failedOpen: "Impossibile aprire la selezione della cartella. Riconnettiti e riprova.",
8586
+ createParent: "Crea in",
8587
+ creating: "Creazione della cartella…",
8588
+ createStopping: "Chiusura della richiesta di creazione…",
8589
+ createUnknown: "Esito della creazione sconosciuto: la cartella potrebbe già esistere. Torna indietro e ricarica la directory prima di riprovare."
8383
8590
  }
8384
8591
  };
8385
- /** Authenticated read-only Gateway route; no native picker or Host service is replaced. */
8592
+ /** Authenticated directory browsing/creation route; no native picker or Host service is replaced. */
8386
8593
  const WORKSPACE_DIRECTORIES_PATH = "/mobile-access/workspace-directories";
8387
8594
  /** Ask only this plugin's live directory dialog to handle a routed native Back. */
8388
8595
  function dismissMobileDirectoryPicker(dialog) {
@@ -8399,6 +8606,32 @@ window.__ModuleLoader__.load({
8399
8606
  }
8400
8607
  return "en";
8401
8608
  }
8609
+ /** Create one child directory through the same authenticated route that lists them. */
8610
+ async function createDirectory(path, name, signal) {
8611
+ const response = await fetch(WORKSPACE_DIRECTORIES_PATH, {
8612
+ method: "POST",
8613
+ credentials: "same-origin",
8614
+ cache: "no-store",
8615
+ headers: { "content-type": "application/json" },
8616
+ body: JSON.stringify({
8617
+ path,
8618
+ name
8619
+ }),
8620
+ signal
8621
+ });
8622
+ if (!response.ok) {
8623
+ const code = response.status === 409 ? "directory-exists" : response.status === 400 ? "directory-create-failed" : response.status === 401 || response.status === 403 ? "unauthorized" : "directory-create-failed";
8624
+ try {
8625
+ await response.body?.cancel();
8626
+ } catch (error) {
8627
+ throw new MobileDirectoryCreateError(code);
8628
+ }
8629
+ throw new MobileDirectoryCreateError(code);
8630
+ }
8631
+ const payload = await response.json();
8632
+ if (typeof payload?.path !== "string" || !isMobileDirectoryPath(payload.path)) throw new MobileDirectoryCreateError("invalid-response");
8633
+ return payload.path;
8634
+ }
8402
8635
  async function readDirectories(path, signal) {
8403
8636
  const query = path === null ? "" : `?path=${encodeURIComponent(path)}`;
8404
8637
  const response = await fetch(`${WORKSPACE_DIRECTORIES_PATH}${query}`, {
@@ -8424,25 +8657,51 @@ window.__ModuleLoader__.load({
8424
8657
  const STYLES = `
8425
8658
  dialog.dshm-picker{position:fixed;inset:0;box-sizing:border-box;width:100%;max-width:none;height:100%;max-height:none;margin:0;padding:max(16px,env(safe-area-inset-top)) max(16px,env(safe-area-inset-right)) max(16px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left));border:0;background:transparent;color:var(--dsw-alias-label-primary,#16181d);font:400 14px/1.5 system-ui;overflow:hidden}
8426
8659
  dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::backdrop{background:rgb(0 0 0 / 42%)}
8427
- .dshm-picker__panel{display:flex;flex-direction:column;box-sizing:border-box;width:min(680px,100%);height:min(500px,100%);min-height:0;overflow:hidden;border:1px solid var(--dsw-alias-border-l2,#dde0e4);border-radius:16px;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 16px 56px rgb(0 0 0 / 18%)}
8428
- .dshm-picker__header{display:flex;align-items:center;flex:none;gap:12px;padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l2,#dde0e4)}
8429
- .dshm-picker__title{flex:1;min-width:0;margin:0;font-size:16px;font-weight:500;overflow-wrap:anywhere}
8430
- .dshm-picker__toolbar{display:flex;flex-wrap:wrap;gap:8px;flex:none;padding:12px 16px 8px}.dshm-picker__path-label{flex:1;min-width:min(180px,100%);display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--dsw-alias-label-secondary,#606873)}
8431
- .dshm-picker__path{box-sizing:border-box;width:100%;min-width:0;min-height:48px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#f7f8fa);color:var(--dsw-alias-label-primary,#16181d);font:400 16px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace}
8660
+ .dshm-picker__panel{display:flex;flex-direction:column;box-sizing:border-box;width:min(680px,100%);height:min(720px,100%);min-height:0;overflow:hidden;border:0;border-radius:20px;box-shadow:0 0 0 .5px rgba(0 0 0 / 16%),0 3px 8px rgba(0 0 0 / 4%);background:var(--dsw-alias-bg-layer-1,#fff)}
8661
+ .dshm-picker__header{display:flex;align-items:center;flex:none;gap:8px;padding:4px 8px 4px 12px;border-bottom:1px solid var(--dsw-alias-border-l2,#dde0e4)}
8662
+ .dshm-picker__title{flex:1;min-width:0;margin:0;font-size:16px;font-weight:510;line-height:24px;overflow-wrap:anywhere}
8663
+ .dshm-picker__toolbar{display:flex;align-items:center;flex-wrap:nowrap;gap:8px;flex:none;padding:8px 10px}.dshm-picker__path-label{flex:1;min-width:0;display:flex;color:var(--dsw-alias-label-secondary,#606873)}
8664
+ .dshm-picker__path-caption{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
8665
+ .dshm-picker__path{box-sizing:border-box;width:100%;min-width:0;min-height:44px;padding:6px 8px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:8px;background:var(--dsw-alias-bg-layer-2,#f7f8fa);color:var(--dsw-alias-label-primary,#16181d);font:400 16px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace}
8432
8666
  .dshm-picker__path[aria-invalid=true]{border-color:var(--dsw-alias-state-error-primary,#b42318)}.dshm-picker__go{align-self:flex-end}
8433
- .dshm-picker__navigation{display:flex;flex:none;gap:8px;padding:0 16px 8px}.dshm-picker__navigation .dshm-picker__button{flex:1;white-space:normal}
8434
- .dshm-picker__list{flex:1;min-height:0;margin:0;padding:4px 12px;overflow:auto;overscroll-behavior:contain;list-style:none;scroll-padding:4px}
8435
- .dshm-picker__row{display:flex;align-items:center;gap:12px;box-sizing:border-box;width:100%;min-height:48px;padding:10px 12px;border:0;border-radius:8px;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer;touch-action:manipulation}
8667
+ .dshm-picker__navigation{display:flex;flex:none;gap:8px;padding:0}
8668
+ .dshm-picker__list{flex:1;min-height:0;margin:0;padding:0 10px 4px;overflow:auto;overscroll-behavior:contain;list-style:none;scroll-padding:4px}
8669
+ .dshm-picker__row{display:flex;align-items:center;gap:8px;box-sizing:border-box;width:100%;min-height:44px;padding:6px 8px;border:0;border-radius:8px;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer;touch-action:manipulation}
8436
8670
  .dshm-picker__folder{flex:none;width:20px;height:20px;color:var(--dsw-alias-label-secondary,#606873)}
8437
8671
  .dshm-picker__name{flex:1;min-width:0;overflow-wrap:anywhere}.dshm-picker__row:hover:not(:disabled),.dshm-picker__button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#eef1f5)}
8438
8672
  .dshm-picker__row:active:not(:disabled),.dshm-picker__button:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active,#e4e8ee)}
8439
- .dshm-picker__button{flex:none;box-sizing:border-box;min-width:48px;min-height:48px;padding:10px 14px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:10px;background:transparent;color:inherit;font:500 14px/1.4 system-ui;cursor:pointer;touch-action:manipulation}
8673
+ .dshm-picker__button{flex:none;box-sizing:border-box;min-width:44px;min-height:44px;padding:6px 10px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:8px;background:transparent;color:inherit;font:500 14px/1.4 system-ui;cursor:pointer;touch-action:manipulation}
8674
+ .dshm-picker__icon{display:inline-flex;align-items:center;justify-content:center;width:44px;padding:0;border-color:transparent}.dshm-picker__icon>svg{display:block;width:18px;height:18px}
8440
8675
  .dshm-picker :is(button,input):focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#2563eb);outline-offset:2px}.dshm-picker button:disabled{opacity:.5;cursor:default}
8441
- .dshm-picker__status{flex:none;margin:0;padding:8px 16px;font-size:13px;color:var(--dsw-alias-label-secondary,#606873);overflow-wrap:anywhere}.dshm-picker__error{color:var(--dsw-alias-state-error-primary,#b42318)}
8442
- .dshm-picker__status[hidden],.dshm-picker__button[hidden]{display:none}.dshm-picker__footer{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;flex:none;gap:8px;padding:12px 16px;border-top:1px solid var(--dsw-alias-border-l2,#dde0e4)}
8443
- .dshm-picker__confirm{border-color:transparent;background:var(--dsw-alias-button-primary-fill,#2563eb);color:var(--dsw-alias-label-primary-foreground,#fff)}.dshm-picker__confirm:hover:not(:disabled),.dshm-picker__confirm:active:not(:disabled){background:var(--dsw-alias-button-primary-hover,#1d4ed8)}
8444
- @media(max-width:420px){dialog.dshm-picker{padding:max(8px,env(safe-area-inset-top)) max(8px,env(safe-area-inset-right)) max(8px,env(safe-area-inset-bottom)) max(8px,env(safe-area-inset-left))}.dshm-picker__header,.dshm-picker__toolbar,.dshm-picker__footer{padding-left:12px;padding-right:12px}.dshm-picker__footer .dshm-picker__button{flex:1}.dshm-picker__path-label{min-width:0}}
8676
+ .dshm-picker [hidden]{display:none!important}.dshm-picker__create{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:8px;padding:12px 16px}
8677
+ .dshm-picker__create-title{margin:0;font-size:13px;color:var(--dsw-alias-label-secondary,#606873)}
8678
+ .dshm-picker__create-input{box-sizing:border-box;width:100%;min-width:0;min-height:48px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#f7f8fa);color:var(--dsw-alias-label-primary,#0f1115);font:inherit;font-size:16px;line-height:1.4}
8679
+ .dshm-picker__create-actions{display:flex;justify-content:flex-end;gap:8px}.dshm-picker__create-error{color:var(--dsw-alias-state-error-primary,#b42318)}
8680
+ .dshm-picker__create-trigger{margin-right:auto;border-color:transparent;padding-left:8px;padding-right:8px}
8681
+ .dshm-picker__status{flex:none;margin:0;padding:4px 10px;font-size:13px;color:var(--dsw-alias-label-secondary,#606873);overflow-wrap:anywhere}.dshm-picker__error{color:var(--dsw-alias-state-error-primary,#b42318)}
8682
+ .dshm-picker__status[hidden],.dshm-picker__button[hidden]{display:none}.dshm-picker__footer{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;flex:none;gap:8px;padding:6px 10px;border-top:1px solid var(--dsw-alias-border-l2,#dde0e4)}
8683
+ .dshm-picker__confirm{min-width:72px;border-color:transparent;background:var(--dsw-alias-button-primary-fill,#2563eb);color:var(--dsw-alias-label-primary-foreground,#fff)}.dshm-picker__confirm:hover:not(:disabled),.dshm-picker__confirm:active:not(:disabled){background:var(--dsw-alias-button-primary-hover,#1d4ed8)}
8684
+ @media(max-width:420px){dialog.dshm-picker{padding:max(8px,env(safe-area-inset-top)) max(8px,env(safe-area-inset-right)) max(8px,env(safe-area-inset-bottom)) max(8px,env(safe-area-inset-left))}}
8445
8685
  `;
8686
+ /**
8687
+ * Whether DSH's own browse directory picker is already mounted on this page.
8688
+ *
8689
+ * DSH's adaptive `directory-picker` resolves to
8690
+ * `@deepseek-ai/dsh-host-directory-picker-auto`, which mounts the Host backend
8691
+ * and the Client face together and picks `browse` whenever the host is remotely
8692
+ * reachable. Its client face fills the same directory-flow slots this plugin
8693
+ * would use, so a plugin-owned picker would shadow the upstream one and take
8694
+ * away its folder creation. The desktop keeps the native OS chooser; a remote
8695
+ * browser can only reach the web implementation.
8696
+ *
8697
+ * @returns true when the upstream browse surface owns those slots on this page.
8698
+ */
8699
+ function hasUpstreamBrowsePicker(view) {
8700
+ if (!("__DSH_BOOT__" in view)) return false;
8701
+ const graph = view.__DSH_BOOT__;
8702
+ if (typeof graph !== "object" || graph === null || !("entries" in graph) || !Array.isArray(graph.entries)) return false;
8703
+ return graph.entries.some((entry) => typeof entry === "object" && entry !== null && "id" in entry && entry.id === "@deepseek-ai/dsh-client-ui-directory-picker-browse");
8704
+ }
8446
8705
  function openDirectoryDialog(doc, getProps, trackDisposal) {
8447
8706
  const view = doc.defaultView;
8448
8707
  if (view === null) throw new Error("Directory picker requires a browser window");
@@ -8468,18 +8727,40 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
8468
8727
  if (name !== void 0) value.setAttribute(`data-dshm-picker-${name}`, "");
8469
8728
  return value;
8470
8729
  };
8730
+ const iconButton = (label, outline, name) => {
8731
+ const value = button("", name);
8732
+ value.classList.add("dshm-picker__icon");
8733
+ value.setAttribute("aria-label", label);
8734
+ value.title = label;
8735
+ const icon = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
8736
+ icon.setAttribute("viewBox", "0 0 24 24");
8737
+ icon.setAttribute("fill", "none");
8738
+ icon.setAttribute("stroke", "currentColor");
8739
+ icon.setAttribute("stroke-width", "1.75");
8740
+ icon.setAttribute("stroke-linecap", "round");
8741
+ icon.setAttribute("stroke-linejoin", "round");
8742
+ icon.setAttribute("aria-hidden", "true");
8743
+ const shape = doc.createElementNS("http://www.w3.org/2000/svg", "path");
8744
+ shape.setAttribute("d", outline);
8745
+ icon.append(shape);
8746
+ value.append(icon);
8747
+ return value;
8748
+ };
8471
8749
  const header = doc.createElement("header");
8472
8750
  header.className = "dshm-picker__header";
8473
8751
  const title = doc.createElement("h2");
8474
8752
  title.className = "dshm-picker__title";
8475
8753
  title.textContent = copy.title;
8476
- const close = button(copy.close);
8754
+ const close = iconButton(copy.close, "M6 6l12 12M6 18L18 6");
8477
8755
  header.append(title, close);
8478
8756
  const toolbar = doc.createElement("form");
8479
8757
  toolbar.className = "dshm-picker__toolbar";
8480
8758
  const pathLabel = doc.createElement("label");
8481
8759
  pathLabel.className = "dshm-picker__path-label";
8482
- pathLabel.textContent = copy.path;
8760
+ const pathCaption = doc.createElement("span");
8761
+ pathCaption.className = "dshm-picker__path-caption";
8762
+ pathCaption.textContent = copy.path;
8763
+ pathLabel.append(pathCaption);
8483
8764
  const path = doc.createElement("input");
8484
8765
  path.type = "text";
8485
8766
  path.className = "dshm-picker__path";
@@ -8488,16 +8769,17 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
8488
8769
  path.autocomplete = "off";
8489
8770
  path.maxLength = 8192;
8490
8771
  pathLabel.append(path);
8491
- const go = button(copy.go, "load");
8772
+ const go = iconButton(copy.go, "M5 12h14m-6-6 6 6-6 6", "load");
8492
8773
  go.type = "submit";
8493
8774
  go.classList.add("dshm-picker__go");
8494
8775
  toolbar.append(pathLabel, go);
8495
8776
  const navigation = doc.createElement("nav");
8496
8777
  navigation.className = "dshm-picker__navigation";
8497
8778
  navigation.setAttribute("aria-label", copy.title);
8498
- const home = button(copy.home, "home");
8499
- const up = button(copy.up, "up");
8779
+ const home = iconButton(copy.home, "m3 10 9-7 9 7v10a1 1 0 0 1-1 1h-5v-7H9v7H4a1 1 0 0 1-1-1Z", "home");
8780
+ const up = iconButton(copy.up, "M12 20V4m-7 7 7-7 7 7", "up");
8500
8781
  navigation.append(home, up);
8782
+ toolbar.prepend(navigation);
8501
8783
  const status = doc.createElement("p");
8502
8784
  status.className = "dshm-picker__status";
8503
8785
  status.setAttribute("role", "status");
@@ -8513,13 +8795,82 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
8513
8795
  footer.className = "dshm-picker__footer";
8514
8796
  const retry = button(copy.retry, "retry");
8515
8797
  retry.hidden = true;
8798
+ const create = button(copy.create, "create");
8799
+ create.classList.add("dshm-picker__create-trigger");
8516
8800
  const cancel = button(copy.cancel, "cancel");
8517
8801
  const confirm = button(copy.open, "confirm");
8518
8802
  confirm.classList.add("dshm-picker__confirm");
8519
- footer.append(retry, cancel, confirm);
8520
- panel.append(header, toolbar, navigation, status, error, list, footer);
8803
+ const createPanel = doc.createElement("div");
8804
+ createPanel.className = "dshm-picker__create";
8805
+ createPanel.hidden = true;
8806
+ const createTitle = doc.createElement("label");
8807
+ createTitle.className = "dshm-picker__create-title";
8808
+ createTitle.textContent = copy.createTitle;
8809
+ const createInput = doc.createElement("input");
8810
+ createInput.type = "text";
8811
+ createInput.className = "dshm-picker__create-input";
8812
+ createInput.setAttribute("data-dshm-picker-create-name", "");
8813
+ createInput.spellcheck = false;
8814
+ createInput.autocomplete = "off";
8815
+ createInput.maxLength = 255;
8816
+ createTitle.append(createInput);
8817
+ const createParent = doc.createElement("p");
8818
+ createParent.className = "dshm-picker__status";
8819
+ const createStatus = doc.createElement("p");
8820
+ createStatus.className = "dshm-picker__status";
8821
+ createStatus.setAttribute("role", "status");
8822
+ createStatus.setAttribute("aria-live", "polite");
8823
+ const createError = doc.createElement("p");
8824
+ createError.className = "dshm-picker__status dshm-picker__create-error";
8825
+ createError.setAttribute("role", "alert");
8826
+ createError.setAttribute("data-dshm-picker-create-error", "");
8827
+ createError.hidden = true;
8828
+ const createActions = doc.createElement("div");
8829
+ createActions.className = "dshm-picker__create-actions";
8830
+ const createCancel = button(copy.cancel, "create-cancel");
8831
+ const createConfirm = button(copy.createAction, "create-confirm");
8832
+ createActions.append(createCancel, createConfirm);
8833
+ createPanel.append(createParent, createTitle, createError, createStatus, createActions);
8834
+ footer.append(create, retry, cancel, confirm);
8835
+ panel.append(header, toolbar, status, error, list, createPanel, footer);
8521
8836
  dialog.append(panel);
8522
8837
  const browser = new MobileDirectoryBrowser(readDirectories);
8838
+ const creator = new MobileDirectoryCreator(createDirectory, (created) => {
8839
+ browser.navigate(created);
8840
+ });
8841
+ let creationNotice = false;
8842
+ let creationNoticeListing = null;
8843
+ const closeCreate = () => {
8844
+ const child = creator.getSnapshot();
8845
+ if (child.pending || child.error === "timeout" || child.error === "request-failed" || child.error === "invalid-response") {
8846
+ creationNotice = true;
8847
+ creationNoticeListing = browser.getSnapshot().listing;
8848
+ }
8849
+ if (creator.close()) {
8850
+ if (create.disabled) close.focus();
8851
+ else create.focus();
8852
+ }
8853
+ };
8854
+ create.addEventListener("click", () => {
8855
+ const parent = browser.getSnapshot().listing?.path;
8856
+ if (!browser.canConfirm() || parent === void 0 || !creator.open(parent)) return;
8857
+ creationNotice = false;
8858
+ createInput.focus();
8859
+ createInput.select();
8860
+ });
8861
+ createCancel.addEventListener("click", closeCreate);
8862
+ createConfirm.addEventListener("click", () => {
8863
+ creator.submit();
8864
+ });
8865
+ createInput.addEventListener("input", () => {
8866
+ creator.setName(createInput.value);
8867
+ });
8868
+ createInput.addEventListener("keydown", (event) => {
8869
+ if (event.key === "Enter" && !event.isComposing) {
8870
+ event.preventDefault();
8871
+ creator.submit();
8872
+ }
8873
+ });
8523
8874
  let disposed = false;
8524
8875
  let closedByOwner = false;
8525
8876
  let renderedListing;
@@ -8527,22 +8878,41 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
8527
8878
  const render = () => {
8528
8879
  if (disposed) return;
8529
8880
  const state = browser.getSnapshot();
8881
+ if (creationNotice && state.listing !== creationNoticeListing) creationNotice = false;
8882
+ const child = creator.getSnapshot();
8530
8883
  const adopting = state.committed || state.hostBusy;
8531
- path.disabled = adopting;
8884
+ const blocked = adopting || state.creating;
8885
+ path.disabled = blocked;
8532
8886
  if (path.value !== state.draft) path.value = state.draft;
8533
8887
  path.setAttribute("aria-invalid", String(state.error === "bad-path"));
8534
- go.disabled = adopting || state.phase === "loading" || !isMobileDirectoryPath(state.draft);
8535
- home.disabled = adopting;
8536
- up.disabled = adopting || state.listing?.parent === void 0;
8888
+ go.disabled = blocked || state.phase === "loading" || !isMobileDirectoryPath(state.draft);
8889
+ home.disabled = blocked;
8890
+ up.disabled = blocked || state.listing?.parent === void 0;
8537
8891
  confirm.disabled = !browser.canConfirm();
8538
8892
  cancel.disabled = adopting;
8539
8893
  close.disabled = adopting;
8540
8894
  retry.hidden = state.phase !== "error" || state.error === "bad-path";
8541
- retry.disabled = adopting;
8542
- status.textContent = adopting ? copy.adopting : state.phase === "loading" ? copy.loading : state.listing?.truncated === true ? copy.truncated : state.listing?.entries.length === 0 ? copy.empty : "";
8543
- status.hidden = status.textContent === "";
8895
+ retry.disabled = blocked;
8896
+ create.disabled = !browser.canConfirm();
8897
+ title.textContent = child.open ? copy.create : copy.title;
8898
+ toolbar.hidden = child.open;
8899
+ navigation.hidden = child.open;
8900
+ list.hidden = child.open;
8901
+ footer.hidden = child.open;
8902
+ createPanel.hidden = !child.open;
8903
+ if (createInput.value !== child.name) createInput.value = child.name;
8904
+ createInput.disabled = child.pending;
8905
+ createInput.setAttribute("aria-invalid", String(child.error === "invalid-name"));
8906
+ createConfirm.disabled = child.pending || !isMobileDirectoryName(child.name.trim());
8907
+ createParent.textContent = `${copy.createParent}: ${child.parent ?? ""}`;
8908
+ createError.textContent = child.error === null ? "" : child.error === "invalid-name" ? copy.createInvalid : child.error === "directory-exists" ? copy.createExists : child.error === "directory-create-failed" ? copy.createFailed : child.error === "unauthorized" ? copy.unauthorized : copy.createUnknown;
8909
+ createError.hidden = child.error === null;
8910
+ createStatus.textContent = child.pending && child.error === null ? copy.creating : "";
8911
+ createStatus.hidden = createStatus.textContent === "";
8912
+ status.textContent = child.pending ? copy.createStopping : creationNotice ? copy.createUnknown : adopting ? copy.adopting : state.phase === "loading" ? copy.loading : state.listing?.truncated === true ? copy.truncated : state.listing?.entries.length === 0 ? copy.empty : "";
8913
+ status.hidden = child.open || status.textContent === "";
8544
8914
  error.textContent = state.error === null ? "" : copy[state.error];
8545
- error.hidden = state.error === null;
8915
+ error.hidden = child.open || state.error === null;
8546
8916
  list.setAttribute("aria-busy", String(state.phase === "loading"));
8547
8917
  if (renderedListing !== state.listing) {
8548
8918
  renderedListing = state.listing;
@@ -8572,11 +8942,21 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
8572
8942
  list.append(item);
8573
8943
  }
8574
8944
  }
8575
- for (const row of list.querySelectorAll("button")) row.disabled = adopting;
8945
+ for (const row of list.querySelectorAll("button")) row.disabled = blocked;
8576
8946
  };
8577
8947
  const unsubscribe = browser.subscribe(render);
8948
+ const unsubscribeCreate = creator.subscribe(() => {
8949
+ const child = creator.getSnapshot();
8950
+ browser.setCreating(child.open || child.pending);
8951
+ render();
8952
+ });
8578
8953
  const requestClose = () => {
8954
+ if (creator.getSnapshot().open) {
8955
+ closeCreate();
8956
+ return;
8957
+ }
8579
8958
  if (disposed || !browser.cancel()) return;
8959
+ creator.close();
8580
8960
  closedByOwner = true;
8581
8961
  if (dialog.open) dialog.close();
8582
8962
  getProps().onCancel();
@@ -8597,6 +8977,8 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
8597
8977
  if (disposal !== void 0) return disposal;
8598
8978
  disposed = true;
8599
8979
  unsubscribe();
8980
+ unsubscribeCreate();
8981
+ const creationStopped = creator.dispose();
8600
8982
  const stopped = browser.dispose();
8601
8983
  const failures = [];
8602
8984
  const clean = (action) => {
@@ -8623,11 +9005,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
8623
9005
  if (trigger?.isConnected) trigger.focus({ preventScroll: true });
8624
9006
  });
8625
9007
  disposal = (async () => {
8626
- try {
8627
- await stopped;
8628
- } catch (error) {
8629
- failures.push(error);
8630
- }
9008
+ for (const result of await Promise.allSettled([stopped, creationStopped])) if (result.status === "rejected") failures.push(result.reason);
8631
9009
  if (failures.length > 0) throw new AggregateError(failures, "Mobile directory dialog disposal failed");
8632
9010
  })();
8633
9011
  return disposal;
@@ -14227,7 +14605,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
14227
14605
  getLayout: () => ctx.get("layout"),
14228
14606
  getSidebarRight: () => ctx.get("sidebarRight")
14229
14607
  }));
14230
- const removeDirectoryPicker = window.__DSH_MOBILE_FRONTEND__ === "dedicated" ? installMobileDirectoryPicker(ctx, document) : () => Promise.resolve();
14608
+ const removeDirectoryPicker = window.__DSH_MOBILE_FRONTEND__ === "dedicated" && !hasUpstreamBrowsePicker(window) ? installMobileDirectoryPicker(ctx, document) : () => Promise.resolve();
14231
14609
  return async () => {
14232
14610
  const removal = removeDirectoryPicker();
14233
14611
  try {