dsh-edge 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/THIRD_PARTY_NOTICES.md +1318 -1075
  2. package/dist/assets/index-Df-65__b.js +110 -0
  3. package/dist/assets/index-b24khbeK.css +1 -0
  4. package/dist/assets/langs/{php-DDkx9sim.js → php-DJl5gRdb.js} +2 -2
  5. package/dist/assets/langs/{ruby-DCxyoPen.js → ruby-5eNB0pDK.js} +2 -2
  6. package/dist/assets/vendor-BNsW4eBh.css +1 -0
  7. package/dist/assets/vendor-CCJJTK99.js +411 -0
  8. package/dist/index.html +8 -8
  9. package/dist/plugins/@deepseek-ai/dsh-api-gateway/client.js +1513 -77
  10. package/dist/plugins/@deepseek-ai/dsh-api-remotes/client.js +3608 -757
  11. package/dist/plugins/@deepseek-ai/dsh-api-session-controller/client.js +2777 -0
  12. package/dist/plugins/@deepseek-ai/dsh-api-workspace-controller/client.js +415 -0
  13. package/dist/plugins/@deepseek-ai/dsh-client-connection/client.js +1975 -7466
  14. package/dist/plugins/@deepseek-ai/dsh-client-locale/client.js +225 -76
  15. package/dist/plugins/@deepseek-ai/dsh-client-modules/client.js +67 -15
  16. package/dist/plugins/@deepseek-ai/dsh-client-ui-agent-preset/client.js +270 -460
  17. package/dist/plugins/@deepseek-ai/dsh-client-ui-approval/client.js +292 -0
  18. package/dist/plugins/@deepseek-ai/dsh-client-ui-attachment/client.js +42 -17
  19. package/dist/plugins/@deepseek-ai/dsh-client-ui-brand-official/client.js +7 -9
  20. package/dist/plugins/@deepseek-ai/dsh-client-ui-chat/client.js +8177 -0
  21. package/dist/plugins/@deepseek-ai/dsh-client-ui-commands/client.js +22 -10
  22. package/dist/plugins/@deepseek-ai/dsh-client-ui-conversation/client.js +14950 -8910
  23. package/dist/plugins/@deepseek-ai/dsh-client-ui-deliverables/client.js +161 -137
  24. package/dist/plugins/@deepseek-ai/dsh-client-ui-goal/client.js +6 -7
  25. package/dist/plugins/@deepseek-ai/dsh-client-ui-input-trigger/client.js +268 -40
  26. package/dist/plugins/@deepseek-ai/dsh-client-ui-jobs/client.js +1 -1
  27. package/dist/plugins/@deepseek-ai/dsh-client-ui-layout/client.js +45 -11
  28. package/dist/plugins/@deepseek-ai/dsh-client-ui-message-feedback/client.js +30 -58
  29. package/dist/plugins/@deepseek-ai/dsh-client-ui-model-selection/client.js +188 -89
  30. package/dist/plugins/@deepseek-ai/dsh-client-ui-permission-presets/client.js +105 -75
  31. package/dist/plugins/@deepseek-ai/dsh-client-ui-renderer/client.js +650 -188
  32. package/dist/plugins/@deepseek-ai/dsh-client-ui-schedule/client.js +307 -0
  33. package/dist/plugins/@deepseek-ai/dsh-client-ui-session/client.js +340 -0
  34. package/dist/plugins/@deepseek-ai/dsh-client-ui-settings/client.js +57 -50
  35. package/dist/plugins/@deepseek-ai/dsh-client-ui-settings-general/client.js +134 -69
  36. package/dist/plugins/@deepseek-ai/dsh-client-ui-settings-models/client.js +575 -442
  37. package/dist/plugins/@deepseek-ai/dsh-client-ui-sidebar/client.js +24 -11
  38. package/dist/plugins/@deepseek-ai/dsh-client-ui-skill/client.js +13 -9
  39. package/dist/plugins/@deepseek-ai/dsh-client-ui-subagent/client.js +44 -20
  40. package/dist/plugins/@deepseek-ai/dsh-client-ui-theme/client.js +265 -94
  41. package/dist/plugins/@deepseek-ai/dsh-client-ui-tool/client.js +899 -433
  42. package/dist/plugins/@deepseek-ai/dsh-client-ui-trajectory/client.js +1472 -613
  43. package/dist/plugins/@deepseek-ai/dsh-client-ui-user-questions/client.js +249 -95
  44. package/dist/plugins/@deepseek-ai/dsh-client-ui-workflow-run/client.js +5 -5
  45. package/dist/plugins/@deepseek-ai/dsh-client-ui-workspace/client.js +425 -146
  46. package/dist/plugins/@deepseek-ai/dsh-typert-registry/client.js +29 -14
  47. package/dist/plugins/dsh-edge-client-ui/client.js +28 -26
  48. package/package.json +62 -58
  49. package/scripts/verify-packed.mjs +1 -1
  50. package/worker/direct/index.js +2866 -1466
  51. package/worker/isolated/index.js +2438 -1038
  52. package/dist/assets/index-C6eRlFa6.css +0 -1
  53. package/dist/assets/index-ClqxG24t.js +0 -96
  54. package/dist/assets/vendor-CjyC-hUb.css +0 -1
  55. package/dist/assets/vendor-D22_Mp1f.js +0 -411
  56. package/dist/plugins/@deepseek-ai/dsh-client-runtime/client.js +0 -10574
@@ -7,7 +7,7 @@ window.__ModuleLoader__.load({
7
7
  let react_jsx_runtime = require("react/jsx-runtime");
8
8
  let react = require("react");
9
9
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
- let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
10
+ let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
11
11
  //#region ../../../vendor/cosmokit/src/misc.ts
12
12
  /** Return true when a value is `null` or `undefined`. */
13
13
  function isNullable(value) {
@@ -806,9 +806,11 @@ window.__ModuleLoader__.load({
806
806
  const LOCALE_SETTINGS_NAMESPACE = "locale";
807
807
  /** Field carrying an explicit locale selection; absence delegates to the browser. */
808
808
  const LOCALE_PREFERENCE_FIELD = "preference";
809
+ /** Accepted BCP 47-style language ids. */
810
+ const LOCALE_ID_PATTERN = /^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/u;
809
811
  /** Locale identifiers shipped by the browser client. */
810
812
  const LOCALE_IDS = ["zh", "en"];
811
- Schema.object({ [LOCALE_PREFERENCE_FIELD]: Schema.union([...LOCALE_IDS]).required(false) });
813
+ Schema.object({ [LOCALE_PREFERENCE_FIELD]: Schema.string().pattern(LOCALE_ID_PATTERN).required(false) });
812
814
  //#endregion
813
815
  //#region lib/types/locales/zh.js
814
816
  /** zh base dictionary for the common namespace: cross-feature standard words. */
@@ -818,6 +820,13 @@ window.__ModuleLoader__.load({
818
820
  "close": "关闭",
819
821
  "copy": "复制",
820
822
  "copied": "复制成功",
823
+ "copy.failed": "复制失败",
824
+ "copy.value": "复制值",
825
+ "copy.json": "复制 JSON",
826
+ "copy.path": "复制属性路径",
827
+ "copy.prettyJson": "复制格式化 JSON",
828
+ "copy.compactJson": "复制紧凑 JSON",
829
+ "copy.optionsHint": "{action};右键点击可选择复制方式",
821
830
  "retry": "重试",
822
831
  "loading": "加载中…",
823
832
  "load.failed": "加载失败",
@@ -834,9 +843,17 @@ window.__ModuleLoader__.load({
834
843
  "collapse": "收起",
835
844
  "expand": "展开",
836
845
  "back": "返回",
846
+ "brand.localBuild": "DSH 本地构建",
837
847
  "unknown": "未知",
838
848
  "none": "无",
839
- "truncated": "已截断"
849
+ "truncated": "已截断",
850
+ "json.collapseNode": "收起 JSON 节点",
851
+ "json.expandNode": "展开 JSON 节点",
852
+ "json.label": "JSON",
853
+ "markdown.footnotes": "脚注",
854
+ "markdown.truncatedCharacters": "… 已截断,共 {total} 字符",
855
+ "number.thousand": "{value}K",
856
+ "number.million": "{value}M"
840
857
  };
841
858
  //#endregion
842
859
  //#region lib/types/locales/en.js
@@ -847,6 +864,13 @@ window.__ModuleLoader__.load({
847
864
  "close": "Close",
848
865
  "copy": "Copy",
849
866
  "copied": "Copied",
867
+ "copy.failed": "Copy failed",
868
+ "copy.value": "Copy value",
869
+ "copy.json": "Copy JSON",
870
+ "copy.path": "Copy property path",
871
+ "copy.prettyJson": "Copy pretty JSON",
872
+ "copy.compactJson": "Copy compact JSON",
873
+ "copy.optionsHint": "{action}; right-click for copy options",
850
874
  "retry": "Retry",
851
875
  "loading": "Loading…",
852
876
  "load.failed": "Failed to load",
@@ -863,9 +887,17 @@ window.__ModuleLoader__.load({
863
887
  "collapse": "Collapse",
864
888
  "expand": "Expand",
865
889
  "back": "Back",
890
+ "brand.localBuild": "DSH Local Build",
866
891
  "unknown": "Unknown",
867
892
  "none": "None",
868
- "truncated": "Truncated"
893
+ "truncated": "Truncated",
894
+ "json.collapseNode": "Collapse JSON node",
895
+ "json.expandNode": "Expand JSON node",
896
+ "json.label": "JSON",
897
+ "markdown.footnotes": "Footnotes",
898
+ "markdown.truncatedCharacters": "… truncated at {total} characters",
899
+ "number.thousand": "{value}K",
900
+ "number.million": "{value}M"
869
901
  };
870
902
  //#endregion
871
903
  //#region lib/types/locales/settings.js
@@ -876,7 +908,7 @@ window.__ModuleLoader__.load({
876
908
  const en = { "language.title": "Language" };
877
909
  //#endregion
878
910
  //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/locale/src/client/LanguageRow.module.css.mjs
879
- const css = ".hVGvvW_row{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:16px 0;display:flex}.hVGvvW_rowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}.hVGvvW_title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}.hVGvvW_selector{background:var(--dsw-alias-bg-module-platform);height:36px;font:inherit;color:var(--dsw-alias-label-primary);cursor:pointer;border:none;border-radius:18px;align-items:center;gap:12px;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}.hVGvvW_selector:hover{background:var(--dsw-alias-interactive-bg-hover)}.hVGvvW_chevron{flex:none}";
911
+ const css = ".hVGvvW_row{border-bottom:.5px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:16px 0;display:flex}.hVGvvW_rowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}.hVGvvW_title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}.hVGvvW_selector{background:var(--dsw-alias-bg-module-platform);height:36px;font:inherit;color:var(--dsw-alias-label-primary);cursor:pointer;border:none;border-radius:18px;align-items:center;gap:12px;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}.hVGvvW_selector:hover{background:var(--dsw-alias-interactive-bg-hover)}.hVGvvW_chevron{flex:none}";
880
912
  const tagId = "@deepseek-ai/dsh-client-locale/LanguageRow.module.css";
881
913
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
882
914
  const tag = document.createElement("style");
@@ -959,7 +991,7 @@ window.__ModuleLoader__.load({
959
991
  * @returns the store handle.
960
992
  */
961
993
  function createLanguageRowStore() {
962
- return (0, _deepseek_ai_dsh_client_runtime_client.defineStore)({
994
+ return (0, _deepseek_ai_dsh_client_store.defineStore)({
963
995
  init: () => ({
964
996
  active: "",
965
997
  options: [],
@@ -976,12 +1008,12 @@ window.__ModuleLoader__.load({
976
1008
  //#endregion
977
1009
  //#region lib/types/client/index.js
978
1010
  /**
979
- * English is both the locale the UI opens in when the browser names no shipped
1011
+ * English is both the locale the UI opens in when the browser names no registered
980
1012
  * language (and for non-browser runs), and the dictionary consulted after the
981
1013
  * active locale misses a key. One constant serves both because the shipped
982
1014
  * `zh`/`en` dictionaries carry identical key sets, so neither direction can
983
1015
  * leave a key unresolved; the residual case points at English rather than
984
- * zh because a browser naming neither shipped language is the reader least
1016
+ * zh because a browser naming no registered language is the reader least
985
1017
  * likely to read Chinese.
986
1018
  */
987
1019
  const FALLBACK_LOCALE = "en";
@@ -989,55 +1021,65 @@ window.__ModuleLoader__.load({
989
1021
  const COMMON_NS = "common";
990
1022
  /** Namespace owning this feature's settings-row copy. */
991
1023
  const SETTINGS_NS = "settings.locale";
992
- /** The two shipped locales. */
993
- const LOCALES = Object.freeze([{
994
- id: "zh",
995
- label: "中文"
996
- }, {
997
- id: "en",
998
- label: "English"
999
- }]);
1000
- /**
1001
- * `<html lang>` tag per shipped locale. The locale id is the app's own
1002
- * vocabulary (primary subtag); the document attribute wants a BCP 47 tag,
1003
- * which assistive technology and browser features (pronunciation rules,
1004
- * translation offers, font fallback, spell check) read to pick their own
1005
- * behavior. `zh` alone leaves the script ambiguous, so the shipped Chinese
1006
- * copy names the variant it actually is.
1007
- */
1008
- const DOCUMENT_LANGUAGE = {
1009
- zh: "zh-CN",
1010
- en: "en"
1024
+ /** The two locales and dictionaries shipped by this package. */
1025
+ const BUILT_IN_LOCALE_METADATA = {
1026
+ zh: {
1027
+ label: "中文",
1028
+ fallback: "en"
1029
+ },
1030
+ en: { label: "English" }
1011
1031
  };
1032
+ const BUILT_IN_LOCALES = Object.freeze(LOCALE_IDS.map((id) => Object.freeze({
1033
+ id,
1034
+ ...BUILT_IN_LOCALE_METADATA[id]
1035
+ })));
1036
+ /** Case-insensitive key for BCP 47-style ids. */
1037
+ function localeKey(value) {
1038
+ return value.toLowerCase();
1039
+ }
1040
+ /** Validate and detach a language-pack contribution from its mutable input. */
1041
+ function normalizeLanguage(input) {
1042
+ if (!LOCALE_ID_PATTERN.test(input.id)) throw new Error(`locale id "${input.id}" is not a BCP 47-style tag`);
1043
+ if (input.label.trim() === "") throw new Error("locale label must not be empty");
1044
+ if (!LOCALE_ID_PATTERN.test(input.fallback)) throw new Error(`locale fallback "${input.fallback}" is not a BCP 47-style tag`);
1045
+ return Object.freeze({
1046
+ id: input.id,
1047
+ label: input.label,
1048
+ fallback: input.fallback
1049
+ });
1050
+ }
1012
1051
  /**
1013
- * Point `<html lang>` at the active locale. Called on every locale change,
1014
- * so the attribute tracks the UI instead of standing at whatever the served
1015
- * markup happened to declare.
1016
- * @param active - the active locale id.
1052
+ * Point `<html lang>` at the active locale, keeping the served document in
1053
+ * sync with locale snapshot changes.
1054
+ * @param snapshot - current locale state, including the active definition.
1017
1055
  */
1018
- function syncDocumentLanguage(active) {
1056
+ function syncDocumentLanguage(snapshot) {
1019
1057
  if (typeof document === "undefined") return;
1020
- document.documentElement.lang = DOCUMENT_LANGUAGE[active];
1058
+ document.documentElement.lang = snapshot.active === "zh" ? "zh-CN" : snapshot.active;
1021
1059
  }
1022
1060
  /**
1023
- * Dictionary registry plus locale preference. Lookup chain per key: the
1024
- * entry's namespace in the active locale -> that namespace's en fallback ->
1025
- * the shared common namespace (active, then en) -> the key itself (missing
1026
- * text stays visible, fail loud in the UI rather than blank). Reads go
1027
- * through {@link getLocale}; writes only through {@link setLocale};
1028
- * continuous sync through the `locale/change` event, or through the
1029
- * LocaleFace getSnapshot/subscribe pair the render machinery consumes
1030
- * (installed via `ctx.slots.installLocale`).
1061
+ * Dictionary registry plus locale preference. Lookup walks the active
1062
+ * language's declared fallback chain in the entry namespace, then repeats it
1063
+ * in the shared common namespace before showing the key itself. Reads go
1064
+ * through {@link getLocale}; preferences change only through
1065
+ * {@link setLocale}, while language packs extend the catalog through
1066
+ * {@link addLanguage}. Continuous sync uses the `locale/change` event or
1067
+ * the LocaleFace getSnapshot/subscribe pair installed through
1068
+ * `ctx.slots.installLocale`.
1031
1069
  */
1032
1070
  var LocaleRuntime = class {
1033
1071
  dicts = /* @__PURE__ */ new Map();
1034
1072
  bound = /* @__PURE__ */ new Map();
1073
+ catalog = /* @__PURE__ */ new Map();
1074
+ fallbackChains = /* @__PURE__ */ new Map();
1035
1075
  snapshot;
1036
1076
  listeners = /* @__PURE__ */ new Set();
1037
1077
  ctx;
1038
1078
  host;
1039
1079
  /** Browser-derived locale standing wherever no explicit Host selection does. */
1040
1080
  provisional;
1081
+ /** Last explicit selection, including one awaiting an external registration. */
1082
+ preference;
1041
1083
  /**
1042
1084
  * @param ctx - owning context (change events are emitted on it; the scope
1043
1085
  * listener is released through ctx.effect on dispose).
@@ -1047,10 +1089,12 @@ window.__ModuleLoader__.load({
1047
1089
  constructor(ctx, host) {
1048
1090
  this.ctx = ctx;
1049
1091
  this.host = host;
1050
- this.provisional = resolveInitialLocale();
1092
+ for (const locale of BUILT_IN_LOCALES) this.catalog.set(localeKey(locale.id), locale);
1093
+ const locales = this.localeList();
1094
+ this.provisional = resolveInitialLocale(locales);
1051
1095
  this.snapshot = Object.freeze({
1052
1096
  active: this.provisional,
1053
- locales: LOCALES,
1097
+ locales,
1054
1098
  revision: 0
1055
1099
  });
1056
1100
  if (host !== void 0) {
@@ -1078,7 +1122,7 @@ window.__ModuleLoader__.load({
1078
1122
  /**
1079
1123
  * LocaleFace subscribe: notified on every snapshot change (locale switch
1080
1124
  * or dictionary registration — registrations bump the revision so already
1081
- * rendered outlets pick up late-arriving dictionaries).
1125
+ * rendered outlets pick up late-arriving dictionaries and locale definitions).
1082
1126
  * @param fn - change callback.
1083
1127
  * @returns unsubscribe.
1084
1128
  */
@@ -1101,12 +1145,49 @@ window.__ModuleLoader__.load({
1101
1145
  * @param id - a registered locale id; unknown ids throw.
1102
1146
  */
1103
1147
  setLocale(id) {
1104
- const match = this.snapshot.locales.find((l) => l.id === id);
1148
+ const match = this.catalog.get(localeKey(id));
1105
1149
  if (match === void 0) throw new Error(`locale "${id}" is not registered`);
1150
+ this.preference = match.id;
1106
1151
  if (this.snapshot.active !== match.id) this.publish(match.id, true);
1107
1152
  this.host?.set(LOCALE_PREFERENCE_FIELD, match.id);
1108
1153
  }
1109
1154
  /**
1155
+ * Add one selectable language to the shared catalog. Its fallback must
1156
+ * already be registered, and following fallback definitions must terminate
1157
+ * at English. Dictionaries may register before or after this definition.
1158
+ * Registration rechecks an unresolved Host preference and the browser's
1159
+ * ordered language list. The caller owns the returned disposer; removing an
1160
+ * active language falls back without clearing the stored id.
1161
+ * @param input - stable id, self-described label, and fallback language id.
1162
+ * @returns idempotent disposer removing this exact definition.
1163
+ * @throws when fields are malformed, the id is occupied, or the fallback
1164
+ * target is unknown or creates a cycle.
1165
+ */
1166
+ addLanguage(input) {
1167
+ const candidate = normalizeLanguage(input);
1168
+ const key = localeKey(candidate.id);
1169
+ if (this.catalog.has(key)) throw new Error(`locale "${candidate.id}" is already registered`);
1170
+ const fallback = this.catalog.get(localeKey(candidate.fallback));
1171
+ if (fallback === void 0) throw new Error(`locale fallback "${candidate.fallback}" is not registered`);
1172
+ const language = Object.freeze({
1173
+ ...candidate,
1174
+ fallback: fallback.id
1175
+ });
1176
+ this.catalog.set(key, language);
1177
+ try {
1178
+ this.assertFallbackChain(language.id);
1179
+ } catch (error) {
1180
+ this.catalog.delete(key);
1181
+ throw error;
1182
+ }
1183
+ this.publishCatalog();
1184
+ return () => {
1185
+ if (this.catalog.get(key) !== language) return;
1186
+ this.catalog.delete(key);
1187
+ this.publishCatalog();
1188
+ };
1189
+ }
1190
+ /**
1110
1191
  * Adopt the scope's accepted durable selection without writing it back; an
1111
1192
  * absent selection returns to the browser-derived locale.
1112
1193
  * @param host - the constructor-narrowed scope driving this adoption.
@@ -1114,19 +1195,74 @@ window.__ModuleLoader__.load({
1114
1195
  adopt(host) {
1115
1196
  const section = host.getSnapshot().value;
1116
1197
  if (section === void 0) return;
1117
- const target = section.preference ?? this.provisional;
1198
+ this.preference = section.preference;
1199
+ const target = this.resolveActive();
1118
1200
  if (this.snapshot.active === target) return;
1119
1201
  this.publish(target, true);
1120
1202
  }
1203
+ /** Recompute browser fallback and publish the current catalog. */
1204
+ publishCatalog() {
1205
+ this.fallbackChains.clear();
1206
+ const locales = this.localeList();
1207
+ this.provisional = resolveInitialLocale(locales);
1208
+ const active = this.resolveActive();
1209
+ this.publish(active, active !== this.snapshot.active, locales);
1210
+ }
1211
+ /** Resolve an explicit preference only while its definition is available. */
1212
+ resolveActive() {
1213
+ if (this.preference === void 0) return this.provisional;
1214
+ return this.catalog.get(localeKey(this.preference))?.id ?? this.provisional;
1215
+ }
1216
+ /** Snapshot the catalog in registration order. */
1217
+ localeList() {
1218
+ return Object.freeze([...this.catalog.values()]);
1219
+ }
1220
+ /** Fail a new definition whose complete fallback path does not reach English. */
1221
+ assertFallbackChain(start) {
1222
+ const seen = /* @__PURE__ */ new Set();
1223
+ let current = this.catalog.get(localeKey(start));
1224
+ while (current !== void 0) {
1225
+ const key = localeKey(current.id);
1226
+ if (seen.has(key)) throw new Error(`locale fallback cycle includes "${current.id}"`);
1227
+ seen.add(key);
1228
+ if (key === localeKey("en")) return;
1229
+ /* v8 ignore next -- English is the only built-in terminal and every
1230
+ * language accepted by addLanguage has a required fallback. */
1231
+ if (current.fallback === void 0) throw new Error(`locale "${current.id}" fallback chain does not reach "en"`);
1232
+ const next = this.catalog.get(localeKey(current.fallback));
1233
+ if (next === void 0) throw new Error(`locale fallback "${current.fallback}" is not registered`);
1234
+ current = next;
1235
+ }
1236
+ }
1237
+ /** Resolve a lookup chain, falling directly to English across an unload gap. */
1238
+ fallbackChain(start) {
1239
+ const startKey = localeKey(start);
1240
+ const cached = this.fallbackChains.get(startKey);
1241
+ if (cached !== void 0) return cached;
1242
+ const chain = [];
1243
+ const seen = /* @__PURE__ */ new Set();
1244
+ let current = this.catalog.get(startKey);
1245
+ while (current !== void 0 && !seen.has(localeKey(current.id))) {
1246
+ const key = localeKey(current.id);
1247
+ seen.add(key);
1248
+ chain.push(current.id);
1249
+ current = current.fallback === void 0 ? void 0 : this.catalog.get(localeKey(current.fallback));
1250
+ }
1251
+ if (!seen.has(localeKey("en"))) chain.push("en");
1252
+ const resolved = Object.freeze(chain);
1253
+ this.fallbackChains.set(startKey, resolved);
1254
+ return resolved;
1255
+ }
1121
1256
  register(ns, localeOrDicts, dict) {
1122
1257
  const pairs = typeof localeOrDicts === "string" ? [[localeOrDicts, dict]] : Object.entries(localeOrDicts);
1258
+ for (const [locale] of pairs) if (!LOCALE_ID_PATTERN.test(locale)) throw new Error(`locale id "${locale}" is not a BCP 47-style tag`);
1123
1259
  let locales = this.dicts.get(ns);
1124
1260
  if (!locales) {
1125
1261
  locales = /* @__PURE__ */ new Map();
1126
1262
  this.dicts.set(ns, locales);
1127
1263
  }
1128
- for (const [locale] of pairs) if (locales.has(locale)) throw new Error(`locale namespace "${ns}" already has locale "${locale}"`);
1129
- for (const [locale, entries] of pairs) locales.set(locale, entries);
1264
+ for (const [locale] of pairs) if (locales.has(localeKey(locale))) throw new Error(`locale namespace "${ns}" already has locale "${locale}"`);
1265
+ for (const [locale, entries] of pairs) locales.set(localeKey(locale), entries);
1130
1266
  this.publish(this.snapshot.active, false);
1131
1267
  return () => {
1132
1268
  const owner = this.dicts.get(ns);
@@ -1134,9 +1270,12 @@ window.__ModuleLoader__.load({
1134
1270
  * first register and never removed, so the disposer always finds it. */
1135
1271
  if (!owner) return;
1136
1272
  let removed = false;
1137
- for (const [locale, entries] of pairs) if (owner.get(locale) === entries) {
1138
- owner.delete(locale);
1139
- removed = true;
1273
+ for (const [locale, entries] of pairs) {
1274
+ const key = localeKey(locale);
1275
+ if (owner.get(key) === entries) {
1276
+ owner.delete(key);
1277
+ removed = true;
1278
+ }
1140
1279
  }
1141
1280
  if (removed) this.publish(this.snapshot.active, false);
1142
1281
  };
@@ -1151,13 +1290,17 @@ window.__ModuleLoader__.load({
1151
1290
  return t;
1152
1291
  }
1153
1292
  translate(ns, key, params) {
1154
- const template = this.lookup(ns, key) ?? (ns !== "common" ? this.lookup("common", key) : void 0) ?? key;
1293
+ const chain = this.fallbackChain(this.snapshot.active);
1294
+ const template = this.lookup(ns, key, chain) ?? (ns !== "common" ? this.lookup("common", key, chain) : void 0) ?? key;
1155
1295
  if (!params) return template;
1156
1296
  return template.replace(/\{(\w+)\}/g, (match, name) => name in params ? String(params[name]) : match);
1157
1297
  }
1158
- lookup(ns, key) {
1298
+ lookup(ns, key, chain) {
1159
1299
  const locales = this.dicts.get(ns);
1160
- return locales?.get(this.snapshot.active)?.[key] ?? locales?.get("en")?.[key];
1300
+ for (const locale of chain) {
1301
+ const value = locales?.get(localeKey(locale))?.[key];
1302
+ if (value !== void 0) return value;
1303
+ }
1161
1304
  }
1162
1305
  /**
1163
1306
  * Advance the snapshot revision and notify LocaleFace subscribers (render
@@ -1166,10 +1309,10 @@ window.__ModuleLoader__.load({
1166
1309
  * registration-heavy boot cannot storm event listeners (which may
1167
1310
  * re-register slots in response).
1168
1311
  */
1169
- publish(active, localeChanged) {
1312
+ publish(active, localeChanged, locales = this.snapshot.locales) {
1170
1313
  this.snapshot = Object.freeze({
1171
1314
  active,
1172
- locales: this.snapshot.locales,
1315
+ locales,
1173
1316
  revision: this.snapshot.revision + 1
1174
1317
  });
1175
1318
  if (localeChanged) this.ctx.emit("locale/change", this.snapshot);
@@ -1184,30 +1327,35 @@ window.__ModuleLoader__.load({
1184
1327
  * The browser's own language wins over {@link FALLBACK_LOCALE}; an explicit
1185
1328
  * Host preference may replace this provisional value after plugin activation.
1186
1329
  */
1187
- function resolveInitialLocale() {
1188
- return detectBrowserLocale() ?? "en";
1330
+ function resolveInitialLocale(locales) {
1331
+ return detectBrowserLocale(locales) ?? "en";
1189
1332
  }
1190
1333
  /**
1191
- * The first shipped locale the browser asks for, matched on the primary
1192
- * subtag so every regional variant lands on its language (`zh-Hans-CN` -> zh,
1193
- * `en-GB` -> en). `window` is the browser test, not `navigator`: Node exposes
1194
- * a global `navigator` reporting the machine's own language, which would
1195
- * otherwise decide the locale for non-browser runs (node e2e booting the
1196
- * client tree). `navigator.language` trails the ordered `languages` list and
1197
- * covers its absence on hosts that expose only the single tag.
1334
+ * The first registered locale the browser asks for. Each browser tag first
1335
+ * matches a locale id exactly, then its primary subtag, so an exact regional
1336
+ * registration wins before a language-wide fallback.
1337
+ * `window` is the browser test, not `navigator`: Node exposes a global
1338
+ * `navigator` reporting the machine's own language, which must not decide the
1339
+ * locale for non-browser runs. `navigator.language` trails the ordered
1340
+ * `languages` list and covers hosts exposing only the single tag.
1341
+ * @param locales - definitions currently available to the browser.
1342
+ * @returns the first matching locale id, or undefined.
1198
1343
  */
1199
- function detectBrowserLocale() {
1344
+ function detectBrowserLocale(locales) {
1200
1345
  if (typeof window === "undefined") return void 0;
1201
- for (const tag of [...navigator.languages ?? [], navigator.language]) {
1202
- const primary = tag.toLowerCase().split("-")[0];
1203
- const match = LOCALES.find((locale) => locale.id === primary);
1204
- if (match) return match.id;
1346
+ const languages = navigator.languages;
1347
+ for (const tag of [...languages ?? [], navigator.language]) {
1348
+ const requested = localeKey(tag);
1349
+ const exact = locales.find((locale) => localeKey(locale.id) === requested);
1350
+ if (exact !== void 0) return exact.id;
1351
+ const primary = requested.split("-")[0];
1352
+ const match = locales.find((locale) => localeKey(locale.id).split("-")[0] === primary);
1353
+ if (match !== void 0) return match.id;
1205
1354
  }
1206
1355
  }
1207
1356
  /** Required services: slot registration plus the settings transport. */
1208
1357
  const inject = [
1209
1358
  "slots",
1210
- "connection",
1211
1359
  "remote",
1212
1360
  "settingsScope"
1213
1361
  ];
@@ -1231,18 +1379,19 @@ window.__ModuleLoader__.load({
1231
1379
  ctx.slots.installLocale(locale);
1232
1380
  const store = createLanguageRowStore();
1233
1381
  let bound;
1234
- const sync = (snapshot) => {
1235
- syncDocumentLanguage(snapshot.active);
1382
+ const sync = () => {
1383
+ const snapshot = locale.getSnapshot();
1384
+ syncDocumentLanguage(snapshot);
1236
1385
  bound?.sync(snapshot.active, snapshot.locales.map((l) => ({
1237
1386
  id: l.id,
1238
1387
  label: l.label
1239
1388
  })), snapshot.revision);
1240
1389
  };
1241
- ctx.on("locale/change", sync);
1242
- syncDocumentLanguage(locale.getLocale().active);
1390
+ ctx.effect(() => locale.subscribe(sync), "locale: language row and document synchronization");
1391
+ sync();
1243
1392
  const injected = (actions) => {
1244
1393
  bound = actions;
1245
- sync(locale.getLocale());
1394
+ sync();
1246
1395
  return { setLocale: (id) => {
1247
1396
  locale.setLocale(id);
1248
1397
  } };
@@ -73,21 +73,26 @@ window.__ModuleLoader__.load({
73
73
  const graph = wire;
74
74
  if (typeof graph.rev !== "string") throw new Error("client-modules: boot manifest rev must be a string");
75
75
  if (!Array.isArray(graph.entries)) throw new Error("client-modules: boot manifest entries must be an array");
76
- const modules = [];
76
+ if (!Array.isArray(graph.batches)) throw new Error("client-modules: boot manifest batches must be an array");
77
+ const moduleFields = [];
77
78
  const plugins = [];
79
+ const seenEntryIds = /* @__PURE__ */ new Set();
78
80
  for (const value of graph.entries) {
79
81
  if (typeof value !== "object" || value === null) throw new Error("client-modules: boot manifest entry is not an object");
80
82
  const row = value;
81
83
  const where = typeof row.id === "string" ? `"${row.id}"` : JSON.stringify(row);
82
84
  if (typeof row.id !== "string" || typeof row.url !== "string" || typeof row.rev !== "string") throw new Error(`client-modules: boot manifest entry ${where} must carry string id/url/rev`);
85
+ if (seenEntryIds.has(row.id)) throw new Error(`client-modules: duplicate graph entry "${row.id}"`);
86
+ seenEntryIds.add(row.id);
83
87
  const subject = `boot manifest entry ${where}`;
84
88
  const inject = optionalStringArray(subject, "inject", row.inject);
85
89
  const external = optionalStringArray(subject, "external", row.external);
86
90
  if (row.immediately !== void 0 && typeof row.immediately !== "boolean") throw new Error(`client-modules: boot manifest entry ${where} immediately must be a boolean`);
87
- modules.push({
91
+ moduleFields.push({
88
92
  id: row.id,
89
93
  url: row.url,
90
94
  rev: row.rev,
95
+ inject: inject === void 0 ? [] : [...inject],
91
96
  external: external === void 0 ? [] : [...external]
92
97
  });
93
98
  plugins.push({
@@ -96,6 +101,33 @@ window.__ModuleLoader__.load({
96
101
  immediately: row.immediately === true
97
102
  });
98
103
  }
104
+ const entryIds = new Set(moduleFields.map((row) => row.id));
105
+ const initialUrls = /* @__PURE__ */ new Map();
106
+ const batchUrls = /* @__PURE__ */ new Set();
107
+ for (const value of graph.batches) {
108
+ if (typeof value !== "object" || value === null) throw new Error("client-modules: boot manifest batch is not an object");
109
+ const batch = value;
110
+ const phase = batch.phase;
111
+ if (phase !== "bootstrap" && phase !== "application") throw new Error(`client-modules: boot manifest batch phase must be "bootstrap" or "application", received ${JSON.stringify(phase)}`);
112
+ if (typeof batch.url !== "string" || typeof batch.rev !== "string") throw new Error(`client-modules: boot manifest ${phase} batch must carry string url/rev`);
113
+ if (batchUrls.has(batch.url)) throw new Error(`client-modules: boot manifest carries duplicate batch URL ${JSON.stringify(batch.url)}`);
114
+ batchUrls.add(batch.url);
115
+ const entries = optionalStringArray(`boot manifest ${phase} batch`, "entries", batch.entries);
116
+ if (entries === void 0 || entries.length === 0) throw new Error(`client-modules: boot manifest ${phase} batch entries must be a non-empty string array`);
117
+ for (const id of entries) {
118
+ if (!entryIds.has(id)) throw new Error(`client-modules: boot manifest ${phase} batch names unknown entry "${id}"`);
119
+ if (initialUrls.has(id)) throw new Error(`client-modules: boot manifest entry "${id}" belongs to more than one batch`);
120
+ initialUrls.set(id, batch.url);
121
+ }
122
+ }
123
+ const modules = moduleFields.map((row) => {
124
+ const initialUrl = initialUrls.get(row.id);
125
+ if (initialUrl === void 0) throw new Error(`client-modules: boot manifest entry "${row.id}" belongs to no initial-load batch`);
126
+ return {
127
+ ...row,
128
+ initialUrl
129
+ };
130
+ });
99
131
  return {
100
132
  rev: graph.rev,
101
133
  modules,
@@ -125,6 +157,11 @@ window.__ModuleLoader__.load({
125
157
  }, { once: true });
126
158
  document.head.append(el);
127
159
  });
160
+ /** Replace the rev query while preserving absolute, protocol-relative, or path-relative form. */
161
+ function atRevision(url, rev) {
162
+ if (!/[?&]rev=[^&#]*/.test(url)) throw new Error(`client-modules: bundle URL ${url} has no revision`);
163
+ return url.replace(/([?&]rev=)[^&#]*/, `$1${encodeURIComponent(rev)}`);
164
+ }
128
165
  /**
129
166
  * Claim and inventory the <style> tags a factory injected during
130
167
  * materialization: preset-emitted tags arrive pre-tagged with data-plugin;
@@ -151,8 +188,10 @@ window.__ModuleLoader__.load({
151
188
  seed;
152
189
  factories = /* @__PURE__ */ new Map();
153
190
  bootstrapIds = /* @__PURE__ */ new Set();
154
- /** In-flight prefetch (script load) per id; concurrent callers share it. */
191
+ /** In-flight script transport per URL; every row in one batch shares it. */
155
192
  pendingArrival = /* @__PURE__ */ new Map();
193
+ /** Single-resource combo URL selected by HMR after invalidating one row. */
194
+ reloadUrls = /* @__PURE__ */ new Map();
156
195
  /** Materialization re-entrancy guard: factory-form CJS cannot deliver partial exports, so a cycle is fatal. */
157
196
  materializing = /* @__PURE__ */ new Set();
158
197
  graphRows = /* @__PURE__ */ new Map();
@@ -194,28 +233,38 @@ window.__ModuleLoader__.load({
194
233
  }
195
234
  /** Load one graph row so its factory is registered (idempotent per in-flight arrival). */
196
235
  arrive(row) {
197
- const { id, url } = row;
198
- const pending = this.pendingArrival.get(id);
199
- if (pending !== void 0) return pending;
236
+ const { id } = row;
200
237
  if (this.loadCache.has(id) || this.factories.has(id)) return Promise.resolve();
201
- const task = this.loadBundle(url).then(() => {
238
+ const reloadUrl = this.reloadUrls.get(id);
239
+ const url = reloadUrl ?? row.initialUrl;
240
+ let transport = this.pendingArrival.get(url);
241
+ if (transport === void 0) {
242
+ transport = this.loadBundle(url).finally(() => {
243
+ this.pendingArrival.delete(url);
244
+ });
245
+ this.pendingArrival.set(url, transport);
246
+ }
247
+ return transport.then(() => {
202
248
  if (!this.factories.has(id)) throw new Error(`client-modules: bundle ${url} loaded without registering "${id}" via __ModuleLoader__.load`);
203
- }).finally(() => {
204
- this.pendingArrival.delete(id);
249
+ if (reloadUrl !== void 0 && this.reloadUrls.get(id) === reloadUrl) this.reloadUrls.delete(id);
205
250
  });
206
- this.pendingArrival.set(id, task);
207
- return task;
208
251
  }
209
- /** Register each unresolved dynamic request before registering its consumer. */
210
- async arriveGraphRow(row, open = []) {
252
+ /** Register each injected package and unresolved dynamic request before its consumer. */
253
+ async arriveGraphRow(row, open = [], visited = /* @__PURE__ */ new Set()) {
211
254
  const cycleStart = open.indexOf(row.id);
212
255
  if (cycleStart !== -1) throw new Error(`client-modules: module arrival cycle ${[...open.slice(cycleStart), row.id].join(" -> ")} (the host must reject this graph before serving it)`);
256
+ if (visited.has(row.id)) return;
257
+ visited.add(row.id);
213
258
  const next = [...open, row.id];
214
259
  for (const request of row.external) {
215
260
  const id = stripClientSuffix(request);
216
261
  if (this.seed.has(request) || this.loadCache.has(id)) continue;
217
262
  const dependency = this.graphRows.get(id);
218
- if (dependency !== void 0) await this.arriveGraphRow(dependency, next);
263
+ if (dependency !== void 0) await this.arriveGraphRow(dependency, next, visited);
264
+ }
265
+ for (const packageName of row.inject) {
266
+ const dependency = this.graphRows.get(packageName);
267
+ if (dependency !== void 0) await this.arriveGraphRow(dependency, [], visited);
219
268
  }
220
269
  await this.arrive(row);
221
270
  }
@@ -276,9 +325,12 @@ window.__ModuleLoader__.load({
276
325
  if (row === void 0) throw new Error(`client-modules: prefetch("${id}") — not a graph entry`);
277
326
  await this.arriveGraphRow(row);
278
327
  }
279
- invalidate(id) {
328
+ invalidate(id, rev) {
280
329
  const normalized = stripClientSuffix(id);
281
330
  if (this.bootstrapIds.has(normalized)) return;
331
+ const row = this.graphRows.get(normalized);
332
+ if (row !== void 0) this.reloadUrls.set(normalized, atRevision(row.url, rev ?? row.rev));
333
+ else this.reloadUrls.delete(normalized);
282
334
  this.factories.delete(normalized);
283
335
  this.loadCache.delete(normalized);
284
336
  }