dsh-mobile 0.6.16 → 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
@@ -6630,18 +6630,67 @@ window.__ModuleLoader__.load({
6630
6630
  const omitted = !entry.required && excluded.has(entry.id);
6631
6631
  return matchesQuery(entry.id, query) && (filter === "all" || (filter === "excluded" ? omitted : !omitted));
6632
6632
  }
6633
+ /** Format a module's measured transfer size for the settings panel.
6634
+ * @param entry - Catalog row with optional measured sizes.
6635
+ * @returns A short size label, or `undefined` when nothing was measured.
6636
+ */
6637
+ function formatClientModuleSize(entry) {
6638
+ const value = entry.gzipBytes ?? entry.bytes;
6639
+ if (value === void 0 || !Number.isFinite(value) || value < 0) return void 0;
6640
+ if (value === 0) return "0 KB";
6641
+ const kilobytes = value / 1024;
6642
+ return kilobytes >= 1024 ? `${(kilobytes / 1024).toFixed(1)} MB` : `${String(Math.max(1, Math.round(kilobytes)))} KB`;
6643
+ }
6644
+ /** Sort measured modules largest first, keeping unknown sizes last and ties stable by ID. */
6645
+ function sortClientModulesBySize(entries) {
6646
+ return [...entries].sort((left, right) => {
6647
+ const leftBytes = left.gzipBytes ?? left.bytes;
6648
+ const rightBytes = right.gzipBytes ?? right.bytes;
6649
+ if (leftBytes === void 0 && rightBytes !== void 0) return 1;
6650
+ if (rightBytes === void 0 && leftBytes !== void 0) return -1;
6651
+ return (rightBytes ?? 0) - (leftBytes ?? 0) || (left.id < right.id ? -1 : left.id > right.id ? 1 : 0);
6652
+ });
6653
+ }
6654
+ /**
6655
+ * Choose the smallest loadable set: required entries plus everything they depend on.
6656
+ *
6657
+ * Excluding every optional entry naively would keep a required module pointing at
6658
+ * an excluded dependency, which the Gateway rejects on save. The closure keeps
6659
+ * those dependencies loaded instead.
6660
+ * @param entries - Full catalog with declared dependencies.
6661
+ * @returns IDs to exclude, in catalog order.
6662
+ */
6663
+ function clientModuleMinimalExclusion(entries) {
6664
+ const byId = new Map(entries.map((entry) => [entry.id, entry]));
6665
+ const keep = new Set(entries.filter((entry) => entry.required).map((entry) => entry.id));
6666
+ const pending = [...keep];
6667
+ while (pending.length > 0) {
6668
+ const entry = byId.get(pending.pop());
6669
+ if (entry === void 0) continue;
6670
+ for (const dependency of entry.dependencies) {
6671
+ if (keep.has(dependency)) continue;
6672
+ keep.add(dependency);
6673
+ pending.push(dependency);
6674
+ }
6675
+ }
6676
+ return entries.filter((entry) => !keep.has(entry.id)).map((entry) => entry.id);
6677
+ }
6633
6678
  /** Editable selections retain unknown server exclusions and never cascade dependencies. */
6634
6679
  var ClientModuleDraft = class {
6635
- entries;
6636
6680
  initial;
6637
6681
  excluded;
6638
6682
  entriesById;
6683
+ catalogEntries;
6639
6684
  constructor(entries, excludedIds) {
6640
- this.entries = entries;
6685
+ this.catalogEntries = entries;
6641
6686
  this.initial = new Set(excludedIds);
6642
6687
  this.excluded = new Set(excludedIds);
6643
6688
  this.entriesById = new Map(entries.map((entry) => [entry.id, entry]));
6644
6689
  }
6690
+ /** @returns The original catalog with its most recently measured metadata. */
6691
+ get entries() {
6692
+ return this.catalogEntries;
6693
+ }
6645
6694
  /** @returns Number of changed loading choices, including hidden filtered rows. */
6646
6695
  get changeCount() {
6647
6696
  let count = 0;
@@ -6689,13 +6738,60 @@ window.__ModuleLoader__.load({
6689
6738
  if (loaded) this.excluded.delete(id);
6690
6739
  else this.excluded.add(id);
6691
6740
  }
6741
+ /** @returns Bytes the current selection still downloads, when sizes were measured. */
6742
+ loadedTransferBytes() {
6743
+ let total = 0;
6744
+ for (const entry of this.entries) {
6745
+ if (!this.isLoaded(entry.id)) continue;
6746
+ total += entry.gzipBytes ?? entry.bytes ?? 0;
6747
+ }
6748
+ return total;
6749
+ }
6750
+ /** @returns Number of selected modules whose transfer size is still unknown. */
6751
+ loadedUnknownSizeCount() {
6752
+ return this.entries.filter((entry) => this.isLoaded(entry.id) && entry.gzipBytes === void 0 && entry.bytes === void 0).length;
6753
+ }
6754
+ /** Refresh metadata without replacing saved or unsaved loading choices. */
6755
+ updateMeasurements(entries) {
6756
+ const measured = new Map(entries.map((entry) => [entry.id, entry]));
6757
+ for (const entry of this.entries) {
6758
+ const next = measured.get(entry.id);
6759
+ if (next === void 0) continue;
6760
+ const refreshed = { ...entry };
6761
+ delete refreshed.bytes;
6762
+ delete refreshed.gzipBytes;
6763
+ delete refreshed.description;
6764
+ this.entriesById.set(entry.id, {
6765
+ ...refreshed,
6766
+ ...next.bytes === void 0 ? {} : { bytes: next.bytes },
6767
+ ...next.gzipBytes === void 0 ? {} : { gzipBytes: next.gzipBytes },
6768
+ ...next.description === void 0 ? {} : { description: next.description }
6769
+ });
6770
+ }
6771
+ this.catalogEntries = this.entries.map((entry) => this.entriesById.get(entry.id));
6772
+ }
6773
+ /** Exclude every optional entry except dependencies a required entry still needs.
6774
+ * @returns Whether this draft changed; saving remains a separate action.
6775
+ */
6776
+ applyMinimalSelection() {
6777
+ const target = new Set(clientModuleMinimalExclusion(this.entries));
6778
+ let changed = false;
6779
+ for (const entry of this.entries) {
6780
+ if (entry.required) continue;
6781
+ const shouldExclude = target.has(entry.id);
6782
+ if (shouldExclude !== this.excluded.has(entry.id)) changed = true;
6783
+ if (shouldExclude) this.excluded.add(entry.id);
6784
+ else this.excluded.delete(entry.id);
6785
+ }
6786
+ return changed;
6787
+ }
6692
6788
  /** Filter a draft without changing hidden choices.
6693
6789
  * @param query - Search input.
6694
6790
  * @param filter - Requested loading selection.
6695
6791
  * @returns Catalog entries in their original stable order.
6696
6792
  */
6697
6793
  matchingEntries(query, filter) {
6698
- return this.entries.filter((entry) => clientModuleMatches(entry, this.excluded, query, filter));
6794
+ return this.entries.map((entry) => this.entriesById.get(entry.id)).filter((entry) => clientModuleMatches(entry, this.excluded, query, filter));
6699
6795
  }
6700
6796
  /** Find invalid old exclusions by name or ID, independently from the protected catalog row.
6701
6797
  * @param query - Search input.
@@ -6711,6 +6807,12 @@ window.__ModuleLoader__.load({
6711
6807
  /** Device-local module selection and the computer's defaults share a read-only catalog. */
6712
6808
  const CLIENT_MODULE_COPY = {
6713
6809
  zh: {
6810
+ measure: "测量体积",
6811
+ measuring: "正在测量…",
6812
+ cancelMeasure: "取消测量",
6813
+ measured: "测量结束,未测到的项目保持未知。你的模块选择未变。",
6814
+ measureFailed: "测量未完成,已保留模块选择和已有体积。",
6815
+ totalPartial: "已测合计约 {size};另有 {count} 项未知",
6714
6816
  title: "移动页面模块",
6715
6817
  computer: "设置此电脑的移动访问默认项;电脑插件不变。",
6716
6818
  device: "仅本设备,下次打开生效;电脑插件不变。",
@@ -6754,6 +6856,11 @@ window.__ModuleLoader__.load({
6754
6856
  changes: "未保存:{count} 项改动",
6755
6857
  unchanged: "没有未保存的改动",
6756
6858
  results: "匹配 {visible} / {total} 项",
6859
+ sizeHint: "按传输体积从大到小排列;数字为 gzip 后的字节。说明取自插件包自身的描述字段,不代表上游认证。",
6860
+ total: "勾选合计约 {size}",
6861
+ totalUnknown: "勾选合计:部分模块未测到体积",
6862
+ minimal: "仅必需(最小加载)",
6863
+ minimalConfirm: "把除了启动必需项及其依赖之外全部设为不加载?保存后下次打开生效。",
6757
6864
  retry: "重新读取",
6758
6865
  saving: "正在保存…",
6759
6866
  readTimeout: "读取超时。当前页面和已保存设置未变,请重试。",
@@ -6766,6 +6873,12 @@ window.__ModuleLoader__.load({
6766
6873
  clearInvalid: "移除旧排除"
6767
6874
  },
6768
6875
  en: {
6876
+ measure: "Measure sizes",
6877
+ measuring: "Measuring…",
6878
+ cancelMeasure: "Cancel measurement",
6879
+ measured: "Measurement ended; unavailable sizes stay unknown. Your module choices are unchanged.",
6880
+ measureFailed: "Measurement did not complete. Your choices and known sizes are kept.",
6881
+ totalPartial: "Measured ≈ {size}; {count} sizes unknown",
6769
6882
  title: "Mobile page modules",
6770
6883
  computer: "Mobile defaults for this computer. Desktop plugins are unchanged.",
6771
6884
  device: "For this device’s next open only. Computer plugins are unchanged.",
@@ -6809,6 +6922,11 @@ window.__ModuleLoader__.load({
6809
6922
  changes: "Unsaved: {count} changes",
6810
6923
  unchanged: "No unsaved changes",
6811
6924
  results: "Matching {visible} / {total}",
6925
+ sizeHint: "Sorted by transfer size; numbers are gzipped bytes. Descriptions come from each plugin package; this is not an upstream endorsement.",
6926
+ total: "Selection ≈ {size}",
6927
+ totalUnknown: "Selection total: some sizes are unknown",
6928
+ minimal: "Required only (minimal load)",
6929
+ minimalConfirm: "Mark everything except required modules and their dependencies as not loaded? Applies on the next open after saving.",
6812
6930
  retry: "Reread settings",
6813
6931
  saving: "Saving…",
6814
6932
  readTimeout: "Reading timed out. This page and saved settings are unchanged; try again.",
@@ -6821,6 +6939,12 @@ window.__ModuleLoader__.load({
6821
6939
  clearInvalid: "Remove old exclusion"
6822
6940
  },
6823
6941
  it: {
6942
+ measure: "Misura dimensioni",
6943
+ measuring: "Misurazione…",
6944
+ cancelMeasure: "Annulla misurazione",
6945
+ measured: "Misurazione terminata; le dimensioni non disponibili restano sconosciute. Le scelte non cambiano.",
6946
+ measureFailed: "Misurazione incompleta. Scelte e dimensioni note conservate.",
6947
+ totalPartial: "Misurati ≈ {size}; {count} dimensioni sconosciute",
6824
6948
  title: "Moduli della pagina mobile",
6825
6949
  computer: "Predefiniti mobile del computer; i plugin non cambiano.",
6826
6950
  device: "Solo alla prossima apertura su questo dispositivo; i plugin del computer non cambiano.",
@@ -6864,6 +6988,11 @@ window.__ModuleLoader__.load({
6864
6988
  changes: "Non salvate: {count} modifiche",
6865
6989
  unchanged: "Nessuna modifica non salvata",
6866
6990
  results: "Corrispondenti {visible} / {total}",
6991
+ sizeHint: "Ordinati per dimensione trasferita; i numeri sono byte gzip. Le descrizioni provengono dal pacchetto del plugin; non sono un’approvazione upstream.",
6992
+ total: "Selezione ≈ {size}",
6993
+ totalUnknown: "Totale selezione: alcune dimensioni sono sconosciute",
6994
+ minimal: "Solo obbligatori (caricamento minimo)",
6995
+ minimalConfirm: "Impostare come non caricati tutti i moduli tranne gli obbligatori e le loro dipendenze? Vale alla prossima apertura dopo il salvataggio.",
6867
6996
  retry: "Rileggi impostazioni",
6868
6997
  saving: "Salvataggio…",
6869
6998
  readTimeout: "Lettura scaduta. Pagina e impostazioni salvate non cambiano; riprova.",
@@ -6895,6 +7024,18 @@ window.__ModuleLoader__.load({
6895
7024
  read: 15e3,
6896
7025
  write: 2e4
6897
7026
  };
7027
+ /** Describe measured bytes without presenting unmeasured modules as a complete total. */
7028
+ function clientModuleTotalLabel(draft, locale) {
7029
+ const copy = CLIENT_MODULE_COPY[locale];
7030
+ const unknownSizes = draft?.loadedUnknownSizeCount() ?? 0;
7031
+ const size = formatClientModuleSize({
7032
+ id: "",
7033
+ required: false,
7034
+ dependencies: [],
7035
+ gzipBytes: draft?.loadedTransferBytes() ?? 0
7036
+ }) ?? "0 KB";
7037
+ return unknownSizes > 0 ? copy.totalPartial.replace("{size}", size).replace("{count}", String(unknownSizes)) : copy.total.replace("{size}", size);
7038
+ }
6898
7039
  /** Validate catalog response fields before creating interactive controls.
6899
7040
  * @param value - Gateway or computer-admin JSON response.
6900
7041
  * @returns The fields used by the module-selection dialog.
@@ -6908,10 +7049,23 @@ window.__ModuleLoader__.load({
6908
7049
  return dependency;
6909
7050
  });
6910
7051
  if (entry.id.trim() === "") throw new Error("invalid_module_catalog");
7052
+ const sizes = {};
7053
+ for (const field of ["bytes", "gzipBytes"]) {
7054
+ if (!(field in entry)) continue;
7055
+ const measured = entry[field];
7056
+ if (typeof measured !== "number" || !Number.isSafeInteger(measured) || measured < 0) throw new Error("invalid_module_catalog");
7057
+ sizes[field] = measured;
7058
+ }
7059
+ if ("description" in entry) {
7060
+ const description = entry.description;
7061
+ if (typeof description !== "string" || description.trim() === "" || description.length > 240) throw new Error("invalid_module_catalog");
7062
+ sizes.description = description;
7063
+ }
6911
7064
  return {
6912
7065
  id: entry.id,
6913
7066
  required: entry.required,
6914
- dependencies
7067
+ dependencies,
7068
+ ...sizes
6915
7069
  };
6916
7070
  });
6917
7071
  if (new Set(entries.map((entry) => entry.id)).size !== entries.length) throw new Error("invalid_module_catalog");
@@ -6942,10 +7096,11 @@ window.__ModuleLoader__.load({
6942
7096
  rejectCancellation(new DOMException("Dialog closed", "AbortError"));
6943
7097
  };
6944
7098
  signal.addEventListener("abort", abort, { once: true });
7099
+ const mutation = body !== void 0 && body.measure !== true;
6945
7100
  const timer = setTimeout(() => {
6946
- rejectCancellation(new ClientModuleRequestTimeout(body !== void 0));
7101
+ rejectCancellation(new ClientModuleRequestTimeout(mutation));
6947
7102
  controller.abort();
6948
- }, body === void 0 ? CLIENT_MODULE_REQUEST_TIMEOUT_MS.read : CLIENT_MODULE_REQUEST_TIMEOUT_MS.write);
7103
+ }, mutation ? CLIENT_MODULE_REQUEST_TIMEOUT_MS.write : CLIENT_MODULE_REQUEST_TIMEOUT_MS.read);
6949
7104
  const operation = async () => {
6950
7105
  const init = {
6951
7106
  method: body === void 0 ? "GET" : "POST",
@@ -6989,6 +7144,8 @@ window.__ModuleLoader__.load({
6989
7144
  .dsh-module-dialog[open]{display:flex;flex-direction:column}.dsh-module-dialog::backdrop{background:rgb(15 23 42 / 40%)}
6990
7145
  .dsh-module-header{display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l2);flex:none}
6991
7146
  .dsh-module-header h2{flex:1;min-width:0;font-size:20px;line-height:1.4;margin:0;overflow-wrap:anywhere}.dsh-module-header .dsh-module-button{flex:none}
7147
+ .dsh-module-packageDescription{margin:2px 0 0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
7148
+ .dsh-module-description-full{margin:4px 0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.45}
6992
7149
  .dsh-module-body{min-height:0;overflow:auto;overscroll-behavior:contain;padding:16px;scroll-padding:12px}.dsh-module-dialog p{margin:0 0 12px;overflow-wrap:anywhere}
6993
7150
  .dsh-module-hint,.dsh-module-source,.dsh-module-choice small,.dsh-module-groupHint,.dsh-module-results{font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
6994
7151
  .dsh-module-tools{display:grid;grid-template-columns:minmax(0,1fr) minmax(100px,.45fr);gap:12px;margin:16px 0 8px}.dsh-module-field{display:flex;flex-direction:column;gap:4px;min-width:0;font-size:13px}
@@ -7006,7 +7163,7 @@ window.__ModuleLoader__.load({
7006
7163
  .dsh-module-footer p{font-size:13px;line-height:20px;margin:0 0 8px}.dsh-module-actions{display:flex;flex-wrap:wrap;gap:8px}.dsh-module-actions .dsh-module-button{flex:1;min-width:0;white-space:normal;line-height:20px;padding:8px 12px}
7007
7164
  .dsh-module-button:disabled{opacity:.5;cursor:default}.dsh-module-save{background:var(--dsw-alias-bg-module-platform);font-weight:600}
7008
7165
  .dsh-module-notice:empty{display:none}.dsh-module-notice{max-height:min(96px,24dvh);overflow:auto;font-size:14px;line-height:22px;overscroll-behavior:contain}.dsh-module-empty{padding:16px 0}
7009
- @media(max-width:420px){.dsh-module-tools{grid-template-columns:minmax(0,1fr)}.dsh-module-body{padding:12px}.dsh-module-header,.dsh-module-footer{padding-left:12px;padding-right:12px}}
7166
+ @media(max-width:420px){.dsh-module-tools{grid-template-columns:minmax(0,1fr)}.dsh-module-body{padding:12px}.dsh-module-header,.dsh-module-footer{padding-left:12px;padding-right:12px}.dsh-module-header{flex-wrap:wrap;gap:8px}.dsh-module-header h2{flex:1 1 100%}.dsh-module-header .dsh-module-button{flex:1 1 0;min-width:0;white-space:normal;overflow-wrap:anywhere;line-height:1.4;padding:8px 12px}}
7010
7167
  .dsh-module-error{color:var(--dsw-alias-state-error-primary,#b42318)}
7011
7168
  `;
7012
7169
  const openDialogs = /* @__PURE__ */ new Set();
@@ -7053,7 +7210,9 @@ window.__ModuleLoader__.load({
7053
7210
  const title = document.createElement("h2");
7054
7211
  title.textContent = copy.title;
7055
7212
  const close = button(copy.close);
7056
- header.append(title, close);
7213
+ const measure = button(copy.measure);
7214
+ measure.dataset.clientModuleMeasure = "true";
7215
+ header.append(title, measure, close);
7057
7216
  const body = document.createElement("div");
7058
7217
  body.className = "dsh-module-body";
7059
7218
  const intro = paragraph(copy[scope]);
@@ -7063,6 +7222,7 @@ window.__ModuleLoader__.load({
7063
7222
  const helpTitle = document.createElement("summary");
7064
7223
  helpTitle.textContent = copy.help;
7065
7224
  help.append(helpTitle, hint);
7225
+ help.append(paragraph(copy.sizeHint, "dsh-module-hint"));
7066
7226
  const source = paragraph("", "dsh-module-source");
7067
7227
  const status = paragraph("", "dsh-module-notice");
7068
7228
  status.setAttribute("role", "status");
@@ -7109,14 +7269,16 @@ window.__ModuleLoader__.load({
7109
7269
  actions.className = "dsh-module-actions";
7110
7270
  const save = button(copy.save);
7111
7271
  save.classList.add("dsh-module-save");
7272
+ const minimal = button(copy.minimal);
7112
7273
  const reset = button(copy.reset);
7113
7274
  const reread = button(copy.retry);
7114
7275
  actions.append(save, reset, reread);
7115
7276
  footer.append(changes, status, actions);
7116
- body.append(intro, source, tools, results, list, help);
7277
+ body.append(intro, source, tools, results, minimal, list, help);
7117
7278
  dialog.append(header, body, footer);
7118
7279
  let disposed = false;
7119
7280
  let busy;
7281
+ let measurementController;
7120
7282
  let generation = 0;
7121
7283
  let draft;
7122
7284
  let unconfirmedWrite = false;
@@ -7132,11 +7294,15 @@ window.__ModuleLoader__.load({
7132
7294
  const changed = draft?.changeCount ?? 0;
7133
7295
  save.disabled = busy !== void 0 || changed === 0;
7134
7296
  reset.disabled = busy !== void 0 || draft === void 0;
7297
+ minimal.disabled = busy !== void 0 || draft === void 0;
7298
+ measure.disabled = busy !== void 0 && busy !== "measure" || draft === void 0;
7299
+ measure.textContent = busy === "measure" ? copy.cancelMeasure : copy.measure;
7135
7300
  reread.disabled = busy !== void 0;
7136
7301
  search.disabled = busy !== void 0 || draft === void 0;
7137
7302
  filter.disabled = search.disabled;
7138
7303
  save.textContent = busy === "write" ? copy.saving : copy.save;
7139
- changes.textContent = `${changed === 0 ? copy.unchanged : copy.changes.replace("{count}", String(changed))} · ${copy.nextOpen}`;
7304
+ const total = clientModuleTotalLabel(draft, locale);
7305
+ changes.textContent = `${changed === 0 ? copy.unchanged : copy.changes.replace("{count}", String(changed))} · ${total} · ${copy.nextOpen}`;
7140
7306
  list.setAttribute("aria-busy", String(busy !== void 0));
7141
7307
  for (const input of choices.values()) input.disabled = busy !== void 0 || input.dataset.required === "true";
7142
7308
  for (const action of invalidActions.values()) action.disabled = busy !== void 0;
@@ -7200,7 +7366,7 @@ window.__ModuleLoader__.load({
7200
7366
  "advanced",
7201
7367
  "required"
7202
7368
  ]) {
7203
- const entries = visible.filter((entry) => clientModuleGroup(entry) === group);
7369
+ const entries = sortClientModulesBySize(visible.filter((entry) => clientModuleGroup(entry) === group));
7204
7370
  if (entries.length === 0) continue;
7205
7371
  const section = document.createElement(group === "extensions" ? "section" : "details");
7206
7372
  section.className = "dsh-module-group";
@@ -7238,9 +7404,19 @@ window.__ModuleLoader__.load({
7238
7404
  shortName.textContent = clientModuleDisplayName(entry.id);
7239
7405
  const selected = document.createElement("small");
7240
7406
  selected.textContent = entry.required ? copy.required : input.checked ? copy.loaded : copy.excluded;
7241
- name.append(shortName, selected);
7407
+ const size = formatClientModuleSize(entry);
7408
+ const sizeLabel = document.createElement("small");
7409
+ sizeLabel.className = "dsh-module-size";
7410
+ sizeLabel.textContent = size ?? "";
7411
+ if (size !== void 0) sizeLabel.dataset.clientModuleSize = "true";
7412
+ name.append(shortName, selected, sizeLabel);
7242
7413
  label.append(input, name);
7243
7414
  row.append(label);
7415
+ if (entry.description !== void 0) {
7416
+ const summary = paragraph(entry.description, "dsh-module-packageDescription");
7417
+ summary.dataset.clientModuleDescription = "true";
7418
+ row.append(summary);
7419
+ }
7244
7420
  const detail = document.createElement("details");
7245
7421
  detail.className = "dsh-module-entryDetails";
7246
7422
  const detailSummary = document.createElement("summary");
@@ -7249,7 +7425,9 @@ window.__ModuleLoader__.load({
7249
7425
  const code = document.createElement("code");
7250
7426
  code.textContent = entry.id;
7251
7427
  id.append(code);
7252
- detail.append(detailSummary, id, paragraph(entry.dependencies.length === 0 ? copy.noDependencies : `${copy.dependencies}${entry.dependencies.join(", ")}`));
7428
+ detail.append(detailSummary, id);
7429
+ if (entry.description !== void 0) detail.append(paragraph(entry.description, "dsh-module-description-full"));
7430
+ detail.append(paragraph(entry.dependencies.length === 0 ? copy.noDependencies : `${copy.dependencies}${entry.dependencies.join(", ")}`));
7253
7431
  row.append(detail);
7254
7432
  input.addEventListener("change", () => {
7255
7433
  if (busy !== void 0 || draft === void 0) return;
@@ -7284,23 +7462,36 @@ window.__ModuleLoader__.load({
7284
7462
  };
7285
7463
  const runRequest = (payload) => {
7286
7464
  if (disposed || busy !== void 0 || payload !== void 0 && draft === void 0) return;
7465
+ const measuring = payload?.measure === true;
7466
+ const mutation = payload !== void 0 && !measuring;
7287
7467
  const ownGeneration = ++generation;
7288
- busy = payload === void 0 ? "read" : "write";
7289
- if (payload !== void 0) unconfirmedWrite = true;
7468
+ busy = measuring ? "measure" : mutation ? "write" : "read";
7469
+ if (mutation) unconfirmedWrite = true;
7470
+ if (measuring) measurementController = new AbortController();
7290
7471
  status.classList.remove("dsh-module-error");
7291
- status.textContent = payload === void 0 ? copy.loading : copy.saving;
7472
+ status.textContent = measuring ? copy.measuring : mutation ? copy.saving : copy.loading;
7292
7473
  updateControls();
7293
- requestClientModuleView(scope, controller.signal, payload).then((view) => {
7474
+ requestClientModuleView(scope, measuring ? measurementController.signal : controller.signal, payload).then((view) => {
7294
7475
  if (disposed || ownGeneration !== generation) return;
7295
- unconfirmedWrite = false;
7296
- render(view);
7297
- status.textContent = payload === void 0 ? "" : copy.saved;
7476
+ if (measuring && draft !== void 0) {
7477
+ draft.updateMeasurements(view.entries);
7478
+ renderList();
7479
+ } else {
7480
+ unconfirmedWrite = false;
7481
+ render(view);
7482
+ }
7483
+ status.textContent = measuring ? copy.measured : mutation ? copy.saved : "";
7298
7484
  }, (error) => {
7299
7485
  if (disposed || ownGeneration !== generation) return;
7486
+ if (measuring) {
7487
+ status.textContent = copy.measureFailed;
7488
+ return;
7489
+ }
7300
7490
  if (error instanceof ClientModuleUiConflict) unconfirmedWrite = false;
7301
- failed(error, payload !== void 0);
7491
+ failed(error, mutation);
7302
7492
  }).finally(() => {
7303
7493
  if (disposed || ownGeneration !== generation) return;
7494
+ measurementController = void 0;
7304
7495
  busy = void 0;
7305
7496
  updateControls();
7306
7497
  });
@@ -7313,6 +7504,18 @@ window.__ModuleLoader__.load({
7313
7504
  reset.addEventListener("click", () => {
7314
7505
  if (busy === void 0 && draft !== void 0 && window.confirm(copy.resetConfirm)) runRequest({ reset: true });
7315
7506
  });
7507
+ minimal.addEventListener("click", () => {
7508
+ if (busy !== void 0 || draft === void 0 || !window.confirm(copy.minimalConfirm)) return;
7509
+ if (draft.applyMinimalSelection()) {
7510
+ renderList();
7511
+ search.focus({ preventScroll: true });
7512
+ }
7513
+ updateControls();
7514
+ });
7515
+ measure.addEventListener("click", () => {
7516
+ if (busy === "measure") measurementController?.abort();
7517
+ else if (busy === void 0 && draft !== void 0) runRequest({ measure: true });
7518
+ });
7316
7519
  reread.addEventListener("click", () => {
7317
7520
  if (busy === void 0 && ((draft?.changeCount ?? 0) === 0 || window.confirm(copy.reloadConfirm))) runRequest();
7318
7521
  });
@@ -7321,6 +7524,7 @@ window.__ModuleLoader__.load({
7321
7524
  disposed = true;
7322
7525
  ++generation;
7323
7526
  controller.abort();
7527
+ measurementController?.abort();
7324
7528
  openDialogs.delete(openDialog);
7325
7529
  window.removeEventListener("dsh-mobile:native-back", onBack, true);
7326
7530
  window.removeEventListener("keydown", onEscape, true);
@@ -7890,6 +8094,1074 @@ window.__ModuleLoader__.load({
7890
8094
  };
7891
8095
  }
7892
8096
  //#endregion
8097
+ //#region src/mobile-directory-browser.ts
8098
+ /** Expected Gateway or response-validation failure for one read-only listing. */
8099
+ var MobileDirectoryReadError = class extends Error {
8100
+ code;
8101
+ constructor(code, options) {
8102
+ super(`Mobile directory listing failed: ${code}`, options);
8103
+ this.code = code;
8104
+ this.name = "MobileDirectoryReadError";
8105
+ }
8106
+ };
8107
+ /** Accept portable absolute spellings; the Host remains authoritative for its platform. */
8108
+ function isMobileDirectoryPath(path) {
8109
+ return path.length > 0 && path.length <= 8192 && !path.includes("\0") && (path.startsWith("/") || /^[a-z]:[\\/]/iu.test(path) || /^\\\\[^\\/]+[\\/][^\\/]+/u.test(path));
8110
+ }
8111
+ function object(value) {
8112
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
8113
+ }
8114
+ /** Validate a wire response before publishing names or adopting a Host path. */
8115
+ function parseMobileDirectoryListing(value) {
8116
+ const record = object(value);
8117
+ const fail = () => {
8118
+ throw new MobileDirectoryReadError("invalid-response");
8119
+ };
8120
+ if (record === void 0 || typeof record.path !== "string" || !isMobileDirectoryPath(record.path) || typeof record.truncated !== "boolean" || !Array.isArray(record.entries) || record.entries.length > 500) return fail();
8121
+ const parent = record.parent;
8122
+ if (parent !== void 0 && (typeof parent !== "string" || !isMobileDirectoryPath(parent))) return fail();
8123
+ const paths = /* @__PURE__ */ new Set();
8124
+ const entries = [];
8125
+ for (const candidate of record.entries) {
8126
+ const entry = object(candidate);
8127
+ if (entry === void 0 || typeof entry.name !== "string" || entry.name.length === 0 || entry.name.includes("\0") || typeof entry.path !== "string" || !isMobileDirectoryPath(entry.path) || paths.has(entry.path)) return fail();
8128
+ paths.add(entry.path);
8129
+ entries.push(Object.freeze({
8130
+ name: entry.name,
8131
+ path: entry.path
8132
+ }));
8133
+ }
8134
+ return Object.freeze({
8135
+ path: record.path,
8136
+ ...parent === void 0 ? {} : { parent },
8137
+ entries: Object.freeze(entries),
8138
+ truncated: record.truncated
8139
+ });
8140
+ }
8141
+ const CLOSED = Object.freeze({
8142
+ open: false,
8143
+ phase: "idle",
8144
+ listing: null,
8145
+ draft: "",
8146
+ error: null,
8147
+ hostBusy: false,
8148
+ committed: false,
8149
+ creating: false
8150
+ });
8151
+ /** Read navigation stops on close; committing is final until the owner withdraws this flow. */
8152
+ var MobileDirectoryBrowser = class {
8153
+ read;
8154
+ deadlineMs;
8155
+ snapshot = CLOSED;
8156
+ listeners = /* @__PURE__ */ new Set();
8157
+ tasks = /* @__PURE__ */ new Set();
8158
+ taskFailures = [];
8159
+ activeRead;
8160
+ generation = 0;
8161
+ disposed = false;
8162
+ lastTarget = null;
8163
+ constructor(read, deadlineMs = 3e4) {
8164
+ this.read = read;
8165
+ this.deadlineMs = deadlineMs;
8166
+ }
8167
+ getSnapshot = () => this.snapshot;
8168
+ /** Subscribe to this occurrence; the returned disposer removes the observer. */
8169
+ subscribe(listener) {
8170
+ this.listeners.add(listener);
8171
+ return () => {
8172
+ this.listeners.delete(listener);
8173
+ };
8174
+ }
8175
+ /** Start a new open edge at the Host home; returns false after permanent disposal. */
8176
+ start() {
8177
+ if (this.disposed || this.snapshot.open) return false;
8178
+ this.publish({
8179
+ ...CLOSED,
8180
+ open: true
8181
+ });
8182
+ return this.navigate(null);
8183
+ }
8184
+ /** Editing invalidates any pending scan and cannot commit the previously listed path. */
8185
+ setDraft(draft) {
8186
+ if (!this.canNavigate() || draft === this.snapshot.draft) return;
8187
+ this.withdrawRead();
8188
+ this.publish({
8189
+ ...this.snapshot,
8190
+ draft,
8191
+ phase: "idle",
8192
+ error: isMobileDirectoryPath(draft) ? null : "bad-path"
8193
+ });
8194
+ }
8195
+ /** Project the owner's adoption state without undoing this flow's synchronous commit claim. */
8196
+ setHostBusy(hostBusy) {
8197
+ if (this.disposed || this.snapshot.hostBusy === hostBusy) return;
8198
+ this.publish({
8199
+ ...this.snapshot,
8200
+ hostBusy
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
+ }
8211
+ /** Read a folder or the initial home; superseded results cannot publish into the new request. */
8212
+ navigate(path) {
8213
+ if (!this.canNavigate()) return false;
8214
+ this.withdrawRead();
8215
+ if (path !== null && !isMobileDirectoryPath(path)) {
8216
+ this.publish({
8217
+ ...this.snapshot,
8218
+ draft: path,
8219
+ phase: "error",
8220
+ error: "bad-path"
8221
+ });
8222
+ return false;
8223
+ }
8224
+ this.lastTarget = path;
8225
+ const controller = new AbortController();
8226
+ this.activeRead = controller;
8227
+ const generation = this.generation;
8228
+ this.publish({
8229
+ ...this.snapshot,
8230
+ phase: "loading",
8231
+ error: null,
8232
+ ...path === null ? {} : { draft: path }
8233
+ });
8234
+ const task = (async () => {
8235
+ const timeout = setTimeout(() => {
8236
+ controller.abort(new MobileDirectoryReadError("timeout"));
8237
+ }, this.deadlineMs);
8238
+ try {
8239
+ const value = await this.read(path, controller.signal);
8240
+ controller.signal.throwIfAborted();
8241
+ const listing = parseMobileDirectoryListing(value);
8242
+ if (!this.current(generation)) return;
8243
+ this.publish({
8244
+ ...this.snapshot,
8245
+ phase: "idle",
8246
+ listing,
8247
+ draft: listing.path,
8248
+ error: null
8249
+ });
8250
+ } catch (error) {
8251
+ if (!this.current(generation)) return;
8252
+ const reason = controller.signal.aborted ? controller.signal.reason : error;
8253
+ this.publish({
8254
+ ...this.snapshot,
8255
+ phase: "error",
8256
+ error: reason instanceof MobileDirectoryReadError ? reason.code : "request-failed"
8257
+ });
8258
+ } finally {
8259
+ clearTimeout(timeout);
8260
+ if (this.activeRead === controller) this.activeRead = void 0;
8261
+ }
8262
+ })();
8263
+ this.tasks.add(task);
8264
+ task.then(() => {
8265
+ this.tasks.delete(task);
8266
+ }, (error) => {
8267
+ this.tasks.delete(task);
8268
+ this.taskFailures.push(error);
8269
+ });
8270
+ return true;
8271
+ }
8272
+ /** Retry only the explicitly failed read, without adopting or replaying a write. */
8273
+ retry() {
8274
+ return this.navigate(this.lastTarget);
8275
+ }
8276
+ /** Whether the confirmed listing still matches the editable path and can be adopted. */
8277
+ canConfirm() {
8278
+ const value = this.snapshot;
8279
+ return this.canNavigate() && value.phase === "idle" && value.error === null && value.listing !== null && value.draft === value.listing.path;
8280
+ }
8281
+ /** Claim one adoption synchronously; returns no path for stale input, busy state or duplicate taps. */
8282
+ confirm() {
8283
+ if (!this.canConfirm()) return void 0;
8284
+ const path = this.snapshot.listing.path;
8285
+ this.publish({
8286
+ ...this.snapshot,
8287
+ committed: true
8288
+ });
8289
+ return path;
8290
+ }
8291
+ /** Cancel reading immediately; a committed/Host-busy adoption cannot be reported as cancelled. */
8292
+ cancel() {
8293
+ if (!this.canClose()) return false;
8294
+ this.withdrawRead();
8295
+ this.publish(CLOSED);
8296
+ return true;
8297
+ }
8298
+ /** Withdraw callbacks immediately and await every admitted read before disposal completes. */
8299
+ async dispose() {
8300
+ this.disposed = true;
8301
+ this.withdrawRead();
8302
+ this.snapshot = CLOSED;
8303
+ this.listeners.clear();
8304
+ await Promise.allSettled([...this.tasks]);
8305
+ if (this.taskFailures.length > 0) throw new AggregateError(this.taskFailures, "Mobile directory observers failed during a listing");
8306
+ }
8307
+ canClose() {
8308
+ return !this.disposed && this.snapshot.open && !this.snapshot.hostBusy && !this.snapshot.committed;
8309
+ }
8310
+ canNavigate() {
8311
+ return this.canClose() && !this.snapshot.creating;
8312
+ }
8313
+ current(generation) {
8314
+ return !this.disposed && this.snapshot.open && generation === this.generation;
8315
+ }
8316
+ withdrawRead() {
8317
+ this.generation++;
8318
+ this.activeRead?.abort();
8319
+ this.activeRead = void 0;
8320
+ }
8321
+ publish(snapshot) {
8322
+ this.snapshot = Object.freeze(snapshot);
8323
+ for (const listener of this.listeners) listener();
8324
+ }
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
+ };
8491
+ //#endregion
8492
+ //#region src/mobile-directory-picker.ts
8493
+ /** Dedicated Mobile directory-flow occupant; stock DSH retains its own chooser. */
8494
+ const MESSAGES = {
8495
+ zh: {
8496
+ title: "选择工作区目录",
8497
+ path: "目录路径",
8498
+ go: "前往",
8499
+ home: "主目录",
8500
+ up: "上一级",
8501
+ cancel: "取消",
8502
+ close: "关闭",
8503
+ open: "打开",
8504
+ retry: "重试",
8505
+ loading: "正在读取文件夹…",
8506
+ adopting: "正在打开工作区…",
8507
+ empty: "此目录中没有子文件夹",
8508
+ truncated: "文件夹过多,仅显示开头部分;可输入完整路径前往其他目录",
8509
+ "bad-path": "请输入电脑上的绝对目录路径,例如 C:\\projects 或 /home/user/projects",
8510
+ unavailable: "无法读取此目录,请检查路径和访问权限后重试",
8511
+ unauthorized: "连接授权不可用,请关闭窗口并重新连接电脑",
8512
+ "invalid-response": "目录响应不可用,请重试或重新连接电脑",
8513
+ "request-failed": "读取目录失败,请检查连接后重试",
8514
+ timeout: "读取目录超时,请重试或取消",
8515
+ failedOpen: "无法打开目录选择器,请重新连接后重试",
8516
+ create: "新建文件夹",
8517
+ createTitle: "新建文件夹名称",
8518
+ createAction: "创建",
8519
+ createInvalid: "请输入单个文件夹名称,不能包含路径分隔符。",
8520
+ createExists: "同名文件夹已存在。",
8521
+ createFailed: "无法创建文件夹,请检查该目录的写入权限后重试。",
8522
+ createParent: "创建位置",
8523
+ creating: "正在创建文件夹…",
8524
+ createStopping: "正在结束创建请求…",
8525
+ createUnknown: "无法确认创建结果,文件夹可能已创建。请返回并重新读取目录,确认后再决定是否重试。"
8526
+ },
8527
+ en: {
8528
+ title: "Select workspace directory",
8529
+ path: "Directory path",
8530
+ go: "Go",
8531
+ home: "Home",
8532
+ up: "Up one level",
8533
+ cancel: "Cancel",
8534
+ close: "Close",
8535
+ open: "Open",
8536
+ retry: "Retry",
8537
+ loading: "Loading folders…",
8538
+ adopting: "Opening workspace…",
8539
+ empty: "No subfolders in this directory",
8540
+ truncated: "Only the first folders are shown; enter a full path to open another directory",
8541
+ "bad-path": "Enter an absolute computer path, such as C:\\projects or /home/user/projects",
8542
+ unavailable: "Could not read this directory. Check its path and access permissions, then retry.",
8543
+ unauthorized: "Connection authorization is unavailable. Close this dialog and reconnect to the computer.",
8544
+ "invalid-response": "The directory response is unavailable. Retry or reconnect to the computer.",
8545
+ "request-failed": "Could not load this directory. Check the connection and retry.",
8546
+ timeout: "Loading timed out. Retry or cancel.",
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."
8558
+ },
8559
+ it: {
8560
+ title: "Seleziona la cartella del workspace",
8561
+ path: "Percorso della cartella",
8562
+ go: "Vai",
8563
+ home: "Home",
8564
+ up: "Cartella superiore",
8565
+ cancel: "Annulla",
8566
+ close: "Chiudi",
8567
+ open: "Apri",
8568
+ retry: "Riprova",
8569
+ loading: "Caricamento delle cartelle…",
8570
+ adopting: "Apertura del workspace…",
8571
+ empty: "Nessuna sottocartella in questa cartella",
8572
+ truncated: "Sono mostrate solo le prime cartelle; inserisci un percorso completo per aprirne un’altra",
8573
+ "bad-path": "Inserisci un percorso assoluto del computer, come C:\\projects o /home/user/projects",
8574
+ unavailable: "Impossibile leggere questa cartella. Controlla il percorso e i permessi, poi riprova.",
8575
+ unauthorized: "Autorizzazione non disponibile. Chiudi questa finestra e riconnettiti al computer.",
8576
+ "invalid-response": "Risposta della cartella non disponibile. Riprova o riconnettiti al computer.",
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.",
8584
+ timeout: "Caricamento scaduto. Riprova o annulla.",
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."
8590
+ }
8591
+ };
8592
+ /** Authenticated directory browsing/creation route; no native picker or Host service is replaced. */
8593
+ const WORKSPACE_DIRECTORIES_PATH = "/mobile-access/workspace-directories";
8594
+ /** Ask only this plugin's live directory dialog to handle a routed native Back. */
8595
+ function dismissMobileDirectoryPicker(dialog) {
8596
+ if (dialog.tagName !== "DIALOG" || dialog.getAttribute("data-dshm-picker-dialog") !== "true" || dialog.getAttribute("open") === null) return false;
8597
+ const dismiss = new Event("dsh-mobile:directory-picker-dismiss", { cancelable: true });
8598
+ dialog.dispatchEvent(dismiss);
8599
+ return dismiss.defaultPrevented;
8600
+ }
8601
+ function localeOf(doc) {
8602
+ const languages = [doc.documentElement.lang, ...doc.defaultView?.navigator.languages ?? []];
8603
+ for (const language of languages) {
8604
+ const code = language.toLowerCase().split("-")[0];
8605
+ if (code === "zh" || code === "en" || code === "it") return code;
8606
+ }
8607
+ return "en";
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
+ }
8635
+ async function readDirectories(path, signal) {
8636
+ const query = path === null ? "" : `?path=${encodeURIComponent(path)}`;
8637
+ const response = await fetch(`${WORKSPACE_DIRECTORIES_PATH}${query}`, {
8638
+ credentials: "same-origin",
8639
+ cache: "no-store",
8640
+ signal
8641
+ });
8642
+ if (!response.ok) {
8643
+ const code = response.status === 400 ? "bad-path" : response.status === 401 || response.status === 403 ? "unauthorized" : "unavailable";
8644
+ try {
8645
+ await response.body?.cancel();
8646
+ } catch (error) {
8647
+ throw new MobileDirectoryReadError(code, { cause: error });
8648
+ }
8649
+ throw new MobileDirectoryReadError(code);
8650
+ }
8651
+ try {
8652
+ return await response.json();
8653
+ } catch (error) {
8654
+ throw new MobileDirectoryReadError("invalid-response", { cause: error });
8655
+ }
8656
+ }
8657
+ const STYLES = `
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}
8659
+ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::backdrop{background:rgb(0 0 0 / 42%)}
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}
8666
+ .dshm-picker__path[aria-invalid=true]{border-color:var(--dsw-alias-state-error-primary,#b42318)}.dshm-picker__go{align-self:flex-end}
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}
8670
+ .dshm-picker__folder{flex:none;width:20px;height:20px;color:var(--dsw-alias-label-secondary,#606873)}
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)}
8672
+ .dshm-picker__row:active:not(:disabled),.dshm-picker__button:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active,#e4e8ee)}
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}
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}
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))}}
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
+ }
8705
+ function openDirectoryDialog(doc, getProps, trackDisposal) {
8706
+ const view = doc.defaultView;
8707
+ if (view === null) throw new Error("Directory picker requires a browser window");
8708
+ const copy = MESSAGES[localeOf(doc)];
8709
+ const active = doc.activeElement;
8710
+ const activeText = active instanceof HTMLElement && (active.isContentEditable || active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement);
8711
+ const trigger = active instanceof HTMLElement && !activeText ? active : void 0;
8712
+ if (activeText) active.blur();
8713
+ const dialog = doc.createElement("dialog");
8714
+ dialog.className = "dshm-picker";
8715
+ dialog.lang = localeOf(doc);
8716
+ dialog.setAttribute("role", "dialog");
8717
+ dialog.setAttribute("aria-modal", "true");
8718
+ dialog.setAttribute("aria-label", copy.title);
8719
+ dialog.setAttribute("data-dshm-picker-dialog", "true");
8720
+ const panel = doc.createElement("section");
8721
+ panel.className = "dshm-picker__panel";
8722
+ const button = (text, name) => {
8723
+ const value = doc.createElement("button");
8724
+ value.type = "button";
8725
+ value.textContent = text;
8726
+ value.className = "dshm-picker__button";
8727
+ if (name !== void 0) value.setAttribute(`data-dshm-picker-${name}`, "");
8728
+ return value;
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
+ };
8749
+ const header = doc.createElement("header");
8750
+ header.className = "dshm-picker__header";
8751
+ const title = doc.createElement("h2");
8752
+ title.className = "dshm-picker__title";
8753
+ title.textContent = copy.title;
8754
+ const close = iconButton(copy.close, "M6 6l12 12M6 18L18 6");
8755
+ header.append(title, close);
8756
+ const toolbar = doc.createElement("form");
8757
+ toolbar.className = "dshm-picker__toolbar";
8758
+ const pathLabel = doc.createElement("label");
8759
+ pathLabel.className = "dshm-picker__path-label";
8760
+ const pathCaption = doc.createElement("span");
8761
+ pathCaption.className = "dshm-picker__path-caption";
8762
+ pathCaption.textContent = copy.path;
8763
+ pathLabel.append(pathCaption);
8764
+ const path = doc.createElement("input");
8765
+ path.type = "text";
8766
+ path.className = "dshm-picker__path";
8767
+ path.setAttribute("data-dshm-picker-path", "");
8768
+ path.spellcheck = false;
8769
+ path.autocomplete = "off";
8770
+ path.maxLength = 8192;
8771
+ pathLabel.append(path);
8772
+ const go = iconButton(copy.go, "M5 12h14m-6-6 6 6-6 6", "load");
8773
+ go.type = "submit";
8774
+ go.classList.add("dshm-picker__go");
8775
+ toolbar.append(pathLabel, go);
8776
+ const navigation = doc.createElement("nav");
8777
+ navigation.className = "dshm-picker__navigation";
8778
+ navigation.setAttribute("aria-label", copy.title);
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");
8781
+ navigation.append(home, up);
8782
+ toolbar.prepend(navigation);
8783
+ const status = doc.createElement("p");
8784
+ status.className = "dshm-picker__status";
8785
+ status.setAttribute("role", "status");
8786
+ status.setAttribute("aria-live", "polite");
8787
+ const error = doc.createElement("p");
8788
+ error.className = "dshm-picker__status dshm-picker__error";
8789
+ error.setAttribute("role", "alert");
8790
+ error.hidden = true;
8791
+ const list = doc.createElement("ul");
8792
+ list.className = "dshm-picker__list";
8793
+ list.setAttribute("aria-label", copy.title);
8794
+ const footer = doc.createElement("footer");
8795
+ footer.className = "dshm-picker__footer";
8796
+ const retry = button(copy.retry, "retry");
8797
+ retry.hidden = true;
8798
+ const create = button(copy.create, "create");
8799
+ create.classList.add("dshm-picker__create-trigger");
8800
+ const cancel = button(copy.cancel, "cancel");
8801
+ const confirm = button(copy.open, "confirm");
8802
+ confirm.classList.add("dshm-picker__confirm");
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);
8836
+ dialog.append(panel);
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
+ });
8874
+ let disposed = false;
8875
+ let closedByOwner = false;
8876
+ let renderedListing;
8877
+ let disposal;
8878
+ const render = () => {
8879
+ if (disposed) return;
8880
+ const state = browser.getSnapshot();
8881
+ if (creationNotice && state.listing !== creationNoticeListing) creationNotice = false;
8882
+ const child = creator.getSnapshot();
8883
+ const adopting = state.committed || state.hostBusy;
8884
+ const blocked = adopting || state.creating;
8885
+ path.disabled = blocked;
8886
+ if (path.value !== state.draft) path.value = state.draft;
8887
+ path.setAttribute("aria-invalid", String(state.error === "bad-path"));
8888
+ go.disabled = blocked || state.phase === "loading" || !isMobileDirectoryPath(state.draft);
8889
+ home.disabled = blocked;
8890
+ up.disabled = blocked || state.listing?.parent === void 0;
8891
+ confirm.disabled = !browser.canConfirm();
8892
+ cancel.disabled = adopting;
8893
+ close.disabled = adopting;
8894
+ retry.hidden = state.phase !== "error" || state.error === "bad-path";
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 === "";
8914
+ error.textContent = state.error === null ? "" : copy[state.error];
8915
+ error.hidden = child.open || state.error === null;
8916
+ list.setAttribute("aria-busy", String(state.phase === "loading"));
8917
+ if (renderedListing !== state.listing) {
8918
+ renderedListing = state.listing;
8919
+ list.replaceChildren();
8920
+ for (const entry of state.listing?.entries ?? []) {
8921
+ const item = doc.createElement("li");
8922
+ const row = button("");
8923
+ row.className = "dshm-picker__row";
8924
+ const icon = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
8925
+ icon.setAttribute("class", "dshm-picker__folder");
8926
+ icon.setAttribute("aria-hidden", "true");
8927
+ icon.setAttribute("viewBox", "0 0 24 24");
8928
+ icon.setAttribute("fill", "none");
8929
+ icon.setAttribute("stroke", "currentColor");
8930
+ icon.setAttribute("stroke-width", "1.5");
8931
+ const outline = doc.createElementNS("http://www.w3.org/2000/svg", "path");
8932
+ outline.setAttribute("d", "M3 7V6a2 2 0 0 1 2-2h5l2 3h7a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z");
8933
+ icon.append(outline);
8934
+ const name = doc.createElement("span");
8935
+ name.className = "dshm-picker__name";
8936
+ name.textContent = entry.name;
8937
+ row.append(icon, name);
8938
+ row.addEventListener("click", () => {
8939
+ browser.navigate(entry.path);
8940
+ });
8941
+ item.append(row);
8942
+ list.append(item);
8943
+ }
8944
+ }
8945
+ for (const row of list.querySelectorAll("button")) row.disabled = blocked;
8946
+ };
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
+ });
8953
+ const requestClose = () => {
8954
+ if (creator.getSnapshot().open) {
8955
+ closeCreate();
8956
+ return;
8957
+ }
8958
+ if (disposed || !browser.cancel()) return;
8959
+ creator.close();
8960
+ closedByOwner = true;
8961
+ if (dialog.open) dialog.close();
8962
+ getProps().onCancel();
8963
+ };
8964
+ const onBack = (event) => {
8965
+ if (!event.cancelable || event.defaultPrevented || !dialog.open) return;
8966
+ event.preventDefault();
8967
+ event.stopImmediatePropagation();
8968
+ requestClose();
8969
+ };
8970
+ const onEscape = (event) => {
8971
+ if (event.key !== "Escape" || event.isComposing || event.ctrlKey || event.altKey || event.metaKey || event.defaultPrevented || !dialog.open) return;
8972
+ event.preventDefault();
8973
+ event.stopImmediatePropagation();
8974
+ requestClose();
8975
+ };
8976
+ const dispose = () => {
8977
+ if (disposal !== void 0) return disposal;
8978
+ disposed = true;
8979
+ unsubscribe();
8980
+ unsubscribeCreate();
8981
+ const creationStopped = creator.dispose();
8982
+ const stopped = browser.dispose();
8983
+ const failures = [];
8984
+ const clean = (action) => {
8985
+ try {
8986
+ action();
8987
+ } catch (error) {
8988
+ failures.push(error);
8989
+ }
8990
+ };
8991
+ clean(() => {
8992
+ view.removeEventListener("dsh-mobile:native-back", onBack, true);
8993
+ });
8994
+ clean(() => {
8995
+ view.removeEventListener("keydown", onEscape, true);
8996
+ });
8997
+ closedByOwner = true;
8998
+ clean(() => {
8999
+ if (dialog.open) dialog.close();
9000
+ });
9001
+ clean(() => {
9002
+ dialog.remove();
9003
+ });
9004
+ clean(() => {
9005
+ if (trigger?.isConnected) trigger.focus({ preventScroll: true });
9006
+ });
9007
+ disposal = (async () => {
9008
+ for (const result of await Promise.allSettled([stopped, creationStopped])) if (result.status === "rejected") failures.push(result.reason);
9009
+ if (failures.length > 0) throw new AggregateError(failures, "Mobile directory dialog disposal failed");
9010
+ })();
9011
+ return disposal;
9012
+ };
9013
+ toolbar.addEventListener("submit", (event) => {
9014
+ event.preventDefault();
9015
+ browser.navigate(path.value);
9016
+ });
9017
+ path.addEventListener("input", () => {
9018
+ browser.setDraft(path.value);
9019
+ });
9020
+ home.addEventListener("click", () => {
9021
+ browser.navigate(null);
9022
+ });
9023
+ up.addEventListener("click", () => {
9024
+ const parent = browser.getSnapshot().listing?.parent;
9025
+ if (parent !== void 0) browser.navigate(parent);
9026
+ });
9027
+ retry.addEventListener("click", () => {
9028
+ browser.retry();
9029
+ });
9030
+ cancel.addEventListener("click", requestClose);
9031
+ close.addEventListener("click", requestClose);
9032
+ confirm.addEventListener("click", () => {
9033
+ const picked = browser.confirm();
9034
+ if (picked !== void 0) getProps().onPicked(picked);
9035
+ });
9036
+ dialog.addEventListener("cancel", (event) => {
9037
+ event.preventDefault();
9038
+ requestClose();
9039
+ });
9040
+ dialog.addEventListener("dsh-mobile:directory-picker-dismiss", (event) => {
9041
+ if (!dialog.open || disposed || !event.cancelable) return;
9042
+ event.preventDefault();
9043
+ requestClose();
9044
+ });
9045
+ dialog.addEventListener("click", (event) => {
9046
+ if (event.target === dialog) requestClose();
9047
+ });
9048
+ dialog.addEventListener("close", () => {
9049
+ if (!disposed && !closedByOwner && browser.cancel()) getProps().onCancel();
9050
+ });
9051
+ view.addEventListener("dsh-mobile:native-back", onBack, true);
9052
+ view.addEventListener("keydown", onEscape, true);
9053
+ try {
9054
+ doc.body.append(dialog);
9055
+ dialog.showModal();
9056
+ close.focus({ preventScroll: true });
9057
+ browser.start();
9058
+ browser.setHostBusy(getProps().busy === true);
9059
+ } catch (error) {
9060
+ trackDisposal(dispose());
9061
+ throw error;
9062
+ }
9063
+ return {
9064
+ setHostBusy: (value) => {
9065
+ browser.setHostBusy(value);
9066
+ },
9067
+ dispose
9068
+ };
9069
+ }
9070
+ /** Register two public directory flows and drain their read work before removing styles. */
9071
+ function installMobileDirectoryPicker(ctx, doc) {
9072
+ const style = doc.createElement("style");
9073
+ style.dataset.plugin = "dsh-mobile-directory-picker";
9074
+ style.textContent = STYLES;
9075
+ doc.head.append(style);
9076
+ const owners = /* @__PURE__ */ new Set();
9077
+ const pending = /* @__PURE__ */ new Set();
9078
+ const cleanupFailures = [];
9079
+ let disposed = false;
9080
+ const trackDisposal = (task) => {
9081
+ pending.add(task);
9082
+ task.then(() => {
9083
+ pending.delete(task);
9084
+ }, (error) => {
9085
+ pending.delete(task);
9086
+ cleanupFailures.push(error);
9087
+ console.error("DSH Mobile directory picker cleanup failed");
9088
+ });
9089
+ };
9090
+ const retire = (owner) => {
9091
+ if (owners.delete(owner)) trackDisposal(owner.dispose());
9092
+ };
9093
+ function MobileDirectoryFlow(props) {
9094
+ const current = (0, react.useRef)(props);
9095
+ current.current = props;
9096
+ const owner = (0, react.useRef)(void 0);
9097
+ (0, react.useEffect)(() => {
9098
+ if (!props.open || disposed) return;
9099
+ try {
9100
+ const dialog = openDirectoryDialog(doc, () => current.current, trackDisposal);
9101
+ owners.add(dialog);
9102
+ owner.current = dialog;
9103
+ return () => {
9104
+ owner.current = void 0;
9105
+ retire(dialog);
9106
+ };
9107
+ } catch (error) {
9108
+ console.error("DSH Mobile directory picker could not open");
9109
+ current.current.onError(MESSAGES[localeOf(doc)].failedOpen);
9110
+ }
9111
+ }, [props.open]);
9112
+ (0, react.useEffect)(() => {
9113
+ owner.current?.setHostBusy(props.busy === true);
9114
+ }, [props.busy]);
9115
+ return null;
9116
+ }
9117
+ let remove;
9118
+ try {
9119
+ remove = ctx.slots.inject("sidebar.workspaces.directoryFlow", () => ctx.slots.inject("conversation.hero.workspace.directoryFlow", () => {
9120
+ const removeSidebar = ctx.slots.register({
9121
+ name: "sidebar.workspaces.directoryFlow",
9122
+ id: "dsh-mobile-directory-picker",
9123
+ priority: -1
9124
+ }, MobileDirectoryFlow);
9125
+ let removeHero;
9126
+ try {
9127
+ removeHero = ctx.slots.register({
9128
+ name: "conversation.hero.workspace.directoryFlow",
9129
+ id: "dsh-mobile-directory-picker",
9130
+ priority: -1
9131
+ }, MobileDirectoryFlow);
9132
+ } catch (error) {
9133
+ removeSidebar();
9134
+ throw error;
9135
+ }
9136
+ return () => {
9137
+ removeHero();
9138
+ removeSidebar();
9139
+ };
9140
+ }));
9141
+ } catch (error) {
9142
+ style.remove();
9143
+ throw error;
9144
+ }
9145
+ return async () => {
9146
+ disposed = true;
9147
+ const failures = [];
9148
+ try {
9149
+ remove();
9150
+ } catch (error) {
9151
+ failures.push(error);
9152
+ }
9153
+ for (const owner of [...owners]) try {
9154
+ retire(owner);
9155
+ } catch (error) {
9156
+ failures.push(error);
9157
+ }
9158
+ await Promise.allSettled([...pending]);
9159
+ style.remove();
9160
+ failures.push(...cleanupFailures);
9161
+ if (failures.length > 0) throw new AggregateError(failures, "Mobile directory picker disposal failed");
9162
+ };
9163
+ }
9164
+ //#endregion
7893
9165
  //#region src/client.ts
7894
9166
  /** Map a host-reported provider onto the UI union, defaulting to the built-in one. */
7895
9167
  function parseMobileRemoteProvider(value) {
@@ -7944,6 +9216,7 @@ window.__ModuleLoader__.load({
7944
9216
  const top = layers.at(-1);
7945
9217
  if (top === void 0) return;
7946
9218
  claim();
9219
+ if (dismissMobileDirectoryPicker(top)) return;
7947
9220
  const modelEntry = [...modelPopups].find(([popup]) => popup.contains(top));
7948
9221
  if (modelEntry !== void 0) {
7949
9222
  const [modelPopup, trigger] = modelEntry;
@@ -13332,16 +14605,22 @@ window.__ModuleLoader__.load({
13332
14605
  getLayout: () => ctx.get("layout"),
13333
14606
  getSidebarRight: () => ctx.get("sidebarRight")
13334
14607
  }));
13335
- return () => {
13336
- disposeClientModuleDialogs();
13337
- removeModuleSettings();
13338
- removeSettingsAction();
13339
- removeCustom();
13340
- removeSurface();
13341
- removeVoiceSession();
13342
- removeDraftTransfer();
13343
- localFont.dispose();
13344
- style.remove();
14608
+ const removeDirectoryPicker = window.__DSH_MOBILE_FRONTEND__ === "dedicated" && !hasUpstreamBrowsePicker(window) ? installMobileDirectoryPicker(ctx, document) : () => Promise.resolve();
14609
+ return async () => {
14610
+ const removal = removeDirectoryPicker();
14611
+ try {
14612
+ disposeClientModuleDialogs();
14613
+ removeModuleSettings();
14614
+ removeSettingsAction();
14615
+ removeCustom();
14616
+ removeSurface();
14617
+ removeVoiceSession();
14618
+ removeDraftTransfer();
14619
+ localFont.dispose();
14620
+ style.remove();
14621
+ } finally {
14622
+ await removal;
14623
+ }
13345
14624
  };
13346
14625
  }
13347
14626
  const removeControl = installDshLanguageBoundSurface(() => {