@lmzhen/dsh-evolution-settings-ui 0.16.0 → 0.17.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,6 +31,13 @@ writes and carries the two decisions plus a preview. It reads the approval plugi
31
31
  (`src/client/api.ts` + `approval-routes.ts`; the path literals are compared against the host table by a spec),
32
32
  wears the same card shell and button vocabulary as the parameter cards, and invents no state of its own.
33
33
 
34
+ The card rides that keyed slot with a key of its own (`approval-pending`): it has no namespace, so no
35
+ namespace-driven loop can name it — and a keyed slot renders ONLY the keys its section asks for, so
36
+ `SettingsSection` names it explicitly and puts it FIRST (it is the one card in this section that waits on a
37
+ decision). A registration nobody asks for renders nowhere: 0.16.0 shipped exactly that, which is why
38
+ `tests/section.client.spec.ts` now drives the real `apply()` and requires the section's `renderSlot` calls to cover
39
+ every key the bundle registers into the slot.
40
+
34
41
  Its states are deliberately distinct: **reading** ("正在读取待批项…"), **empty** ("没有待批的写入") and
35
42
  **failed** (the reason plus a retry). Merging the first two is what once made a slow answer read as lost data
36
43
  (E7), so the state machine lives in `pending-state.ts` and the three are asserted without a browser.
package/lib/client.js CHANGED
@@ -214,55 +214,6 @@ window.__ModuleLoader__.load({
214
214
  }
215
215
  ]
216
216
  },
217
- {
218
- namespace: "evolution-memory",
219
- fields: [
220
- {
221
- id: "memoryChars",
222
- group: "memory",
223
- doc: "Character budget the memory store enforces for MEMORY.md.",
224
- label: "记忆库上限",
225
- hint: "长期记忆文件(MEMORY.md)最多多少字符。",
226
- control: "number",
227
- unit: "字符",
228
- values: [],
229
- valueLabels: []
230
- },
231
- {
232
- id: "userChars",
233
- group: "memory",
234
- doc: "Character budget the memory store enforces for USER.md.",
235
- label: "用户画像上限",
236
- hint: "用户画像文件(USER.md)最多多少字符。",
237
- control: "number",
238
- unit: "字符",
239
- values: [],
240
- valueLabels: []
241
- },
242
- {
243
- id: "addDatePrefix",
244
- group: "memory",
245
- doc: "Prefix stored memory entries with their date heading.",
246
- label: "记忆条目加日期",
247
- hint: "写入时在条目开头加上日期。",
248
- control: "switch",
249
- unit: "",
250
- values: [],
251
- valueLabels: []
252
- },
253
- {
254
- id: "maxConsolidationFailures",
255
- group: "memory",
256
- doc: "Consolidation failures one turn tolerates before the tool gives up.",
257
- label: "合并失败重试",
258
- hint: "合并记忆连续失败多少次后停止。",
259
- control: "number",
260
- unit: "次",
261
- values: [],
262
- valueLabels: []
263
- }
264
- ]
265
- },
266
217
  {
267
218
  namespace: "evolution-review",
268
219
  fields: [
@@ -354,7 +305,70 @@ window.__ModuleLoader__.load({
354
305
  ]
355
306
  },
356
307
  {
357
- namespace: "evolution-skills",
308
+ namespace: "memory-files",
309
+ fields: [
310
+ {
311
+ id: "memoryChars",
312
+ group: "memory",
313
+ doc: "Character budget the memory store enforces for MEMORY.md.",
314
+ label: "记忆库上限",
315
+ hint: "长期记忆文件(MEMORY.md)最多多少字符。",
316
+ control: "number",
317
+ unit: "字符",
318
+ values: [],
319
+ valueLabels: []
320
+ },
321
+ {
322
+ id: "userChars",
323
+ group: "memory",
324
+ doc: "Character budget the memory store enforces for USER.md.",
325
+ label: "用户画像上限",
326
+ hint: "用户画像文件(USER.md)最多多少字符。",
327
+ control: "number",
328
+ unit: "字符",
329
+ values: [],
330
+ valueLabels: []
331
+ },
332
+ {
333
+ id: "addDatePrefix",
334
+ group: "memory",
335
+ doc: "Prefix stored memory entries with their date heading.",
336
+ label: "记忆条目加日期",
337
+ hint: "写入时在条目开头加上日期。",
338
+ control: "switch",
339
+ unit: "",
340
+ values: [],
341
+ valueLabels: []
342
+ },
343
+ {
344
+ id: "maxConsolidationFailures",
345
+ group: "memory",
346
+ doc: "Consolidation failures one turn tolerates before the tool gives up.",
347
+ label: "合并失败重试",
348
+ hint: "合并记忆连续失败多少次后停止。",
349
+ control: "number",
350
+ unit: "次",
351
+ values: [],
352
+ valueLabels: []
353
+ }
354
+ ]
355
+ },
356
+ {
357
+ namespace: "tool-memory",
358
+ fields: [{
359
+ id: "entryPreviewChars",
360
+ group: "memory",
361
+ doc: "Characters of one memory entry shown in a tool result preview.",
362
+ label: "记忆预览长度",
363
+ hint: "列表里每条记忆最多显示多少字符。",
364
+ control: "number",
365
+ unit: "字符",
366
+ values: [],
367
+ valueLabels: []
368
+ }]
369
+ },
370
+ {
371
+ namespace: "tool-skill-manage",
358
372
  fields: [
359
373
  {
360
374
  id: "skillContentChars",
@@ -475,20 +489,37 @@ window.__ModuleLoader__.load({
475
489
  valueLabels: []
476
490
  }
477
491
  ]
492
+ }
493
+ ];
494
+ /** Every (bundle, row) pair the Plugins page can open a configuration page for. */
495
+ const CLIENT_ROW_SEATS = [
496
+ {
497
+ bundle: "@lmzhen/dsh-evolution-all",
498
+ rows: [
499
+ "evolution-curator",
500
+ "evolution-review",
501
+ "memory-files",
502
+ "tool-memory",
503
+ "tool-skill-manage"
504
+ ]
478
505
  },
479
506
  {
480
- namespace: "evolution-tool-memory",
481
- fields: [{
482
- id: "entryPreviewChars",
483
- group: "memory",
484
- doc: "Characters of one memory entry shown in a tool result preview.",
485
- label: "记忆预览长度",
486
- hint: "列表里每条记忆最多显示多少字符。",
487
- control: "number",
488
- unit: "字符",
489
- values: [],
490
- valueLabels: []
491
- }]
507
+ bundle: "@lmzhen/dsh-evolution-host",
508
+ rows: [
509
+ "evolution-curator",
510
+ "evolution-review",
511
+ "memory-files"
512
+ ]
513
+ },
514
+ {
515
+ bundle: "@lmzhen/dsh-evolution-preset",
516
+ rows: [
517
+ "evolution-curator",
518
+ "evolution-review",
519
+ "memory-files",
520
+ "tool-memory",
521
+ "tool-skill-manage"
522
+ ]
492
523
  }
493
524
  ];
494
525
  //#endregion
@@ -769,11 +800,12 @@ window.__ModuleLoader__.load({
769
800
  /** Chinese copy (the family's primary language). */
770
801
  const zh = {
771
802
  title: "自进化",
772
- subtitle: "下面按功能列出你能改的参数;没有列出的由安装时的配置决定。",
803
+ subtitle: "待你决定的写入在最上面;下面按功能列出你能改的参数,没有列出的由安装时的配置决定。",
773
804
  overridden: "我改过",
774
805
  deployment: "默认",
775
806
  loading: "正在读取设置…",
776
807
  unavailable: "这个功能没有启动(对应的插件没有安装)",
808
+ seatMissing: "这个界面没有提供设置服务,参数只能看不能改",
777
809
  readonly: "本次会话不允许写入设置(临时运行模式)",
778
810
  apply: "保存",
779
811
  reset: "恢复默认值",
@@ -814,11 +846,12 @@ window.__ModuleLoader__.load({
814
846
  /** English copy, key for key with {@link zh}. */
815
847
  const en = {
816
848
  title: "Self-evolution",
817
- subtitle: "Changeable parameters are listed by feature below; anything not listed comes from the deployment configuration.",
849
+ subtitle: "Writes waiting for your decision come first; the changeable parameters are listed by feature below, and anything not listed comes from the deployment configuration.",
818
850
  overridden: "edited by you",
819
851
  deployment: "default",
820
852
  loading: "reading settings…",
821
853
  unavailable: "this feature is not running (its plugin is not installed)",
854
+ seatMissing: "this shell serves no settings surface, so the parameters cannot be edited here",
822
855
  readonly: "settings cannot be written in this session (temporary mode)",
823
856
  apply: "Save",
824
857
  reset: "Reset to default",
@@ -863,10 +896,10 @@ window.__ModuleLoader__.load({
863
896
  */
864
897
  const NAMESPACE_TITLES = Object.freeze({
865
898
  "evolution-review": "cardReview",
866
- "evolution-memory": "cardMemory",
867
- "evolution-tool-memory": "cardToolMemory",
899
+ "memory-files": "cardMemory",
900
+ "tool-memory": "cardToolMemory",
868
901
  "evolution-curator": "cardCurator",
869
- "evolution-skills": "cardSkills"
902
+ "tool-skill-manage": "cardSkills"
870
903
  });
871
904
  /**
872
905
  * Resolve one message without the locale seat (tests and the fallback path).
@@ -910,7 +943,7 @@ window.__ModuleLoader__.load({
910
943
  * settings scope and its snapshot are declared by the shape the renderer hands
911
944
  * over (\`getSnapshot\`/\`subscribe\`, status/value/user/writable).
912
945
  *
913
- * Layout and behaviour follow the platform's own settings fields: a collapsed card
946
+ * header (namespace title, change count, chevron), then one block per field —
914
947
  * header (namespace title, change count, chevron), then one block per field —
915
948
  * label with its unit and source chip, a control HOLDING THE CURRENT VALUE, the
916
949
  * Chinese hint — and one 放弃修改/保存 pair at the card's foot. The control is
@@ -944,7 +977,7 @@ window.__ModuleLoader__.load({
944
977
  }
945
978
  }
946
979
  /** Whether a value is a plain section object. */
947
- function isSection(value) {
980
+ function isSection$1(value) {
948
981
  return typeof value === "object" && value !== null && !Array.isArray(value);
949
982
  }
950
983
  /** One field's control, typed from the registry. */
@@ -1004,22 +1037,21 @@ window.__ModuleLoader__.load({
1004
1037
  }, t("reset"))) : null);
1005
1038
  }
1006
1039
  /**
1007
- * Render one namespace's card.
1008
- * @param props - the card data, its callbacks, and the bound section hook.
1040
+ * Render one row's card body from a supplied snapshot.
1041
+ * @param props - the card data, its callbacks, and the row state to render.
1009
1042
  * @returns the card.
1010
1043
  */
1011
- function ParamCard(props) {
1012
- const { t, fields, write, clear, namespace } = props;
1044
+ function ParamCardView(props) {
1045
+ const { t, fields, write, clear, namespace, snapshot } = props;
1013
1046
  const [open, setOpen] = (0, react.useState)(false);
1014
1047
  const [draft, setDraft] = (0, react.useState)({});
1015
1048
  const [busy, setBusy] = (0, react.useState)(false);
1016
1049
  const [error, setError] = (0, react.useState)("");
1017
1050
  const [pending, setPending] = (0, react.useState)(null);
1018
- const snapshot = props.useParamSection((state) => state);
1019
1051
  if (snapshot.status === "loading") return (0, react.createElement)("p", { className: "evolution-param-note" }, t("loading"));
1020
- if (snapshot.status === "unavailable") return (0, react.createElement)("p", { className: "evolution-param-note" }, t("unavailable"));
1021
- const user = isSection(snapshot.user) ? snapshot.user : {};
1022
- const value = isSection(snapshot.value) ? snapshot.value : {};
1052
+ if (snapshot.status === "unavailable") return (0, react.createElement)("p", { className: "evolution-param-note" }, t(snapshot.reason === "seat-missing" ? "seatMissing" : "unavailable"));
1053
+ const user = isSection$1(snapshot.user) ? snapshot.user : {};
1054
+ const value = isSection$1(snapshot.value) ? snapshot.value : {};
1023
1055
  const disabled = !snapshot.writable;
1024
1056
  const locked = disabled || busy;
1025
1057
  const titleKey = NAMESPACE_TITLES[namespace];
@@ -1105,14 +1137,174 @@ window.__ModuleLoader__.load({
1105
1137
  onClick: save
1106
1138
  }, busy ? t("saving") : t("apply")))) : null);
1107
1139
  }
1140
+ /**
1141
+ * The card as our own section renders it: the row state comes from the hook seat the
1142
+ * renderer binds out of the inject face's `hooks` compartment (components never hold a
1143
+ * subscription of their own).
1144
+ * @param props - the card data, its callbacks, and the bound row hook.
1145
+ * @returns the card.
1146
+ */
1147
+ function ParamCard(props) {
1148
+ return ParamCardView({
1149
+ namespace: props.namespace,
1150
+ fields: props.fields,
1151
+ t: props.t,
1152
+ write: props.write,
1153
+ clear: props.clear,
1154
+ snapshot: props.useParamSection((state) => state)
1155
+ });
1156
+ }
1157
+ //#endregion
1158
+ //#region src/client/source.ts
1159
+ /** What a card shows when this UI found no settings seat at all. */
1160
+ const SEAT_MISSING = Object.freeze({
1161
+ status: "unavailable",
1162
+ value: void 0,
1163
+ user: void 0,
1164
+ writable: false,
1165
+ reason: "seat-missing"
1166
+ });
1167
+ /**
1168
+ * Whether a value is a plain section object.
1169
+ * @param value - the value to test.
1170
+ * @returns true for a non-null, non-array object.
1171
+ */
1172
+ function isSection(value) {
1173
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1174
+ }
1175
+ /**
1176
+ * Project one platform snapshot into the card's own shape.
1177
+ * @param snapshot - the seat's snapshot.
1178
+ * @returns the card's snapshot, with the reason an unavailable one is unavailable.
1179
+ */
1180
+ function sectionSnapshotOf(snapshot) {
1181
+ return {
1182
+ status: snapshot.status,
1183
+ value: isSection(snapshot.value) ? snapshot.value : void 0,
1184
+ user: snapshot.user,
1185
+ writable: snapshot.writable,
1186
+ ...snapshot.status === "unavailable" ? { reason: "not-served" } : {}
1187
+ };
1188
+ }
1189
+ /**
1190
+ * The live source one row's card binds.
1191
+ * @param seat - the probed settings seat, when the deployment composed it.
1192
+ * @param namespace - the row's entry id.
1193
+ * @returns the source; without a seat it reports the stated empty state and never notifies.
1194
+ */
1195
+ function sourceFor(seat, namespace) {
1196
+ if (seat === void 0) return {
1197
+ getSnapshot: () => SEAT_MISSING,
1198
+ subscribe: () => () => {}
1199
+ };
1200
+ const form = seat.get(namespace);
1201
+ return {
1202
+ getSnapshot: () => sectionSnapshotOf(form.getSnapshot()),
1203
+ subscribe: (listener) => form.subscribe(listener)
1204
+ };
1205
+ }
1206
+ /**
1207
+ * The write pair for one row.
1208
+ * @param seat - the probed settings seat, when the deployment composed it.
1209
+ * @param namespace - the row's entry id.
1210
+ * @returns the two calls a card's save/reset path uses; without a seat they resolve without writing.
1211
+ */
1212
+ function writeFaceFor(seat, namespace) {
1213
+ const form = () => seat?.get(namespace);
1214
+ return {
1215
+ write: async (field, value) => {
1216
+ await form()?.set(field, value);
1217
+ },
1218
+ clear: async (field) => {
1219
+ await form()?.unset(field);
1220
+ }
1221
+ };
1222
+ }
1223
+ //#endregion
1224
+ //#region src/client/RowConfigCard.ts
1225
+ /**
1226
+ * The Plugins page's per-row configuration entry for a family row.
1227
+ *
1228
+ * The page owns the settings subscription — it re-renders the entry — and passes the row's
1229
+ * form in, so this component renders the SAME card body as our own section, fed by the
1230
+ * page's state instead of the bound hook and therefore holding no subscription of its own.
1231
+ * `summary` answers the page's one-line slot: a row's description fallback, which stays
1232
+ * empty until the row actually overrides something, exactly as the page expects.
1233
+ * @module @lmzhen/dsh-evolution-settings-ui/client
1234
+ */
1235
+ /**
1236
+ * Count the keys this row's user layer holds.
1237
+ * @param user - the raw user section, as the page reports it.
1238
+ * @param fields - the row's fields.
1239
+ * @returns how many of them the user overrode.
1240
+ */
1241
+ function overridden(user, fields) {
1242
+ const layer = isSection(user) ? user : {};
1243
+ return fields.filter((field) => Object.hasOwn(layer, field.id)).length;
1244
+ }
1245
+ /**
1246
+ * Render one row's configuration entry.
1247
+ * @param props - the page's view/form seat plus this row's face.
1248
+ * @returns the entry; `summary` renders nothing for an untouched row (the page then keeps the row's own description).
1249
+ */
1250
+ function RowConfigCard(props) {
1251
+ const { t, fields, namespace, view, form } = props;
1252
+ if (form === void 0) return view === "summary" ? null : (0, react.createElement)("p", { className: "evolution-param-note" }, t("seatMissing"));
1253
+ const changed = overridden(form.state.user, fields);
1254
+ if (view === "summary") return changed === 0 ? null : (0, react.createElement)("span", null, t("changed").replace("{n}", String(changed)));
1255
+ return ParamCardView({
1256
+ namespace,
1257
+ fields,
1258
+ t,
1259
+ snapshot: sectionSnapshotOf(form.state),
1260
+ write: async (field, value) => {
1261
+ await form.mutate([{
1262
+ op: "set",
1263
+ path: [field],
1264
+ value
1265
+ }], form.state.revision);
1266
+ },
1267
+ clear: async (field) => {
1268
+ await form.mutate([{
1269
+ op: "unset",
1270
+ path: [field]
1271
+ }], form.state.revision);
1272
+ }
1273
+ });
1274
+ }
1108
1275
  //#endregion
1109
1276
  //#region src/client/seam.ts
1110
1277
  /** Our card slot, hosted by the section this bundle registers. */
1111
1278
  const CARD_SLOT = "evolution.namespace.card";
1279
+ /** The Plugins page's per-row configuration seat (declared by `ui-plugin-manager`). */
1280
+ const ROW_CONFIG_SLOT = "plugins.row.config";
1281
+ /**
1282
+ * The key the Plugins page files a row's configuration under.
1283
+ *
1284
+ * Spelled here because the page's own helper cannot be imported (a value import from
1285
+ * another plugin's client half is forbidden):
1286
+ * `ui-plugin-manager/src/client/config-ledger.ts:36` is the definition this mirrors.
1287
+ * @param bundle - the bundle package name that declares the row.
1288
+ * @param rowId - the row id that bundle's patch declares.
1289
+ * @returns the `plugins.row.config` key.
1290
+ */
1291
+ function rowConfigKey(bundle, rowId) {
1292
+ return bundle + "#" + rowId;
1293
+ }
1294
+ /**
1295
+ * The pending-window card's slot key.
1296
+ *
1297
+ * The section pairs cards by key and the parameter cards use their settings namespace; this card has no
1298
+ * namespace (its data comes from the host's approval routes), so it carries a key of its own. The constant
1299
+ * lives HERE and not in `index.ts` because the SECTION has to name it too, and `index.ts` already imports
1300
+ * the section — one definition in the module both sides already share is what keeps that pair acyclic.
1301
+ */
1302
+ const PENDING_CARD_KEY = "approval-pending";
1112
1303
  //#endregion
1113
1304
  //#region src/client/SettingsSection.ts
1114
1305
  /**
1115
- * The section shell: title, one-line explanation, then one card per namespace.
1306
+ * The section shell: title, one-line explanation, then one card per namespace — with the staged-write
1307
+ * window's card ahead of them all, because it is the only card here that waits on a decision.
1116
1308
  *
1117
1309
  * This is the shape the platform's own settings sections use — a heading plus the
1118
1310
  * child slots it declared in its registration. The cards themselves are separate
@@ -1126,7 +1318,10 @@ window.__ModuleLoader__.load({
1126
1318
  * @returns the section element.
1127
1319
  */
1128
1320
  function SettingsSection(face) {
1129
- return (0, react.createElement)("div", { className: "evolution-params" }, (0, react.createElement)("h2", { className: "evolution-params-title" }, face.t("title")), (0, react.createElement)("p", { className: "evolution-params-subtitle" }, face.t("subtitle")), ...CLIENT_PARAM_SECTIONS.map((section_) => (0, react.createElement)("div", {
1321
+ return (0, react.createElement)("div", { className: "evolution-params" }, (0, react.createElement)("h2", { className: "evolution-params-title" }, face.t("title")), (0, react.createElement)("p", { className: "evolution-params-subtitle" }, face.t("subtitle")), (0, react.createElement)("div", {
1322
+ key: PENDING_CARD_KEY,
1323
+ className: "evolution-params-group"
1324
+ }, face.renderSlot(CARD_SLOT, {}, { entryKey: PENDING_CARD_KEY })), ...CLIENT_PARAM_SECTIONS.map((section_) => (0, react.createElement)("div", {
1130
1325
  key: section_.namespace,
1131
1326
  className: "evolution-params-group"
1132
1327
  }, face.renderSlot(CARD_SLOT, {}, { entryKey: section_.namespace }))));
@@ -1225,24 +1420,20 @@ window.__ModuleLoader__.load({
1225
1420
  }
1226
1421
  //#endregion
1227
1422
  //#region src/client/index.ts
1228
- /** Required client services: the slot registry, the settings transport, the locale seat. */
1229
- const inject = [
1230
- "slots",
1231
- "settingsScope",
1232
- "locale"
1233
- ];
1234
- /** Section id and nav order (the platform's own sections sit at 0/10/15/20). */
1235
- const SECTION_ID = "evolution";
1236
- const SECTION_ORDER = 25;
1237
1423
  /**
1238
- * The pending-window card's slot key.
1424
+ * Required client services: the slot registry and the locale seat.
1239
1425
  *
1240
- * The section pairs cards by key and the parameter cards use their settings namespace; this card has no
1241
- * namespace (its data comes from the host's approval routes), so it carries a key of its own.
1426
+ * The settings seat (`configForms`) is deliberately NOT here. A required inject for a
1427
+ * service the deployment did not compose leaves this whole browser half PENDING forever —
1428
+ * no error, no section, no UI — and the family is also installed without a settings
1429
+ * surface. It is probed instead, and the cards say what is missing.
1242
1430
  */
1243
- const PENDING_CARD_KEY = "approval-pending";
1431
+ const inject = ["slots", "locale"];
1432
+ /** Section id and nav order (the platform's own sections sit at 0/10/15/20). */
1433
+ const SECTION_ID = "evolution";
1434
+ const SECTION_ORDER = 25;
1244
1435
  /**
1245
- * Register the locale namespace, the section and its cards.
1436
+ * Register the locale namespace, the section, its cards, and the per-row configuration entries.
1246
1437
  * @param ctx - client cordis context.
1247
1438
  */
1248
1439
  function apply(ctx) {
@@ -1271,6 +1462,7 @@ window.__ModuleLoader__.load({
1271
1462
  }
1272
1463
  return bound(key);
1273
1464
  };
1465
+ const configForms = ctx.get("configForms");
1274
1466
  seam.slots.inject("settings.section", function* () {
1275
1467
  yield seam.slots.register({
1276
1468
  name: "settings.section",
@@ -1286,14 +1478,14 @@ window.__ModuleLoader__.load({
1286
1478
  });
1287
1479
  seam.slots.inject(CARD_SLOT, function* () {
1288
1480
  for (const section of CLIENT_PARAM_SECTIONS) {
1289
- const scope = seam.settingsScope.bind({ namespace: section.namespace });
1290
- const source = scope;
1481
+ const source = sourceFor(configForms, section.namespace);
1482
+ const { write, clear } = writeFaceFor(configForms, section.namespace);
1291
1483
  const face = () => ({
1292
1484
  namespace: section.namespace,
1293
1485
  fields: section.fields,
1294
1486
  t,
1295
- write: (field, value) => scope.set(field, value),
1296
- clear: (field) => scope.unset(field),
1487
+ write,
1488
+ clear,
1297
1489
  hooks: { paramSection: source }
1298
1490
  });
1299
1491
  yield seam.slots.register({
@@ -1312,6 +1504,22 @@ window.__ModuleLoader__.load({
1312
1504
  })
1313
1505
  }, PendingCard);
1314
1506
  });
1507
+ seam.slots.inject(ROW_CONFIG_SLOT, function* () {
1508
+ for (const seat of CLIENT_ROW_SEATS) for (const rowId of seat.rows) {
1509
+ const section = CLIENT_PARAM_SECTIONS.find((candidate) => candidate.namespace === rowId);
1510
+ if (section === void 0) continue;
1511
+ const face = () => ({
1512
+ namespace: rowId,
1513
+ fields: section.fields,
1514
+ t
1515
+ });
1516
+ yield seam.slots.register({
1517
+ name: ROW_CONFIG_SLOT,
1518
+ key: rowConfigKey(seat.bundle, rowId),
1519
+ inject: face
1520
+ }, RowConfigCard);
1521
+ }
1522
+ });
1315
1523
  }
1316
1524
  //#endregion
1317
1525
  exports.PENDING_CARD_KEY = PENDING_CARD_KEY;
@@ -6,7 +6,7 @@
6
6
  * settings scope and its snapshot are declared by the shape the renderer hands
7
7
  * over (\`getSnapshot\`/\`subscribe\`, status/value/user/writable).
8
8
  *
9
- * Layout and behaviour follow the platform's own settings fields: a collapsed card
9
+ * header (namespace title, change count, chevron), then one block per field —
10
10
  * header (namespace title, change count, chevron), then one block per field —
11
11
  * label with its unit and source chip, a control HOLDING THE CURRENT VALUE, the
12
12
  * Chinese hint — and one 放弃修改/保存 pair at the card's foot. The control is
@@ -49,5 +49,28 @@ export type ParamCardProps = Omit<ParamCardFace, 'hooks'> & {
49
49
  * @param props - the card data, its callbacks, and the bound section hook.
50
50
  * @returns the card.
51
51
  */
52
+ /** Props of the shared body: the card's own data plus the row state it renders. */
53
+ export interface ParamCardViewProps {
54
+ namespace: string;
55
+ fields: readonly ClientParamField[];
56
+ t: (key: MessageKey) => string;
57
+ write: (field: string, value: unknown) => Promise<void>;
58
+ clear: (field: string) => Promise<void>;
59
+ /** The current row state, from whichever source owns it (the bound hook or the page). */
60
+ snapshot: ParamSectionSnapshot;
61
+ }
62
+ /**
63
+ * Render one row's card body from a supplied snapshot.
64
+ * @param props - the card data, its callbacks, and the row state to render.
65
+ * @returns the card.
66
+ */
67
+ export declare function ParamCardView(props: ParamCardViewProps): ReactNode;
68
+ /**
69
+ * The card as our own section renders it: the row state comes from the hook seat the
70
+ * renderer binds out of the inject face's `hooks` compartment (components never hold a
71
+ * subscription of their own).
72
+ * @param props - the card data, its callbacks, and the bound row hook.
73
+ * @returns the card.
74
+ */
52
75
  export declare function ParamCard(props: ParamCardProps): ReactNode;
53
76
  //# sourceMappingURL=ParamCard.d.ts.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The Plugins page's per-row configuration entry for a family row.
3
+ *
4
+ * The page owns the settings subscription — it re-renders the entry — and passes the row's
5
+ * form in, so this component renders the SAME card body as our own section, fed by the
6
+ * page's state instead of the bound hook and therefore holding no subscription of its own.
7
+ * `summary` answers the page's one-line slot: a row's description fallback, which stays
8
+ * empty until the row actually overrides something, exactly as the page expects.
9
+ * @module @lmzhen/dsh-evolution-settings-ui/client
10
+ */
11
+ import { type ReactNode } from 'react';
12
+ import type { ClientParamField } from './generated-params.ts';
13
+ import type { MessageKey } from './messages.ts';
14
+ import type { RowConfigViewProps } from './seam.ts';
15
+ /** The inject face every row registration carries (the page supplies `view` and `form`). */
16
+ export interface RowConfigFace {
17
+ /** The row's entry id, for the card title. */
18
+ namespace: string;
19
+ fields: readonly ClientParamField[];
20
+ t: (key: MessageKey) => string;
21
+ }
22
+ /** Props one row entry is rendered with: the page's seat plus this row's own face. */
23
+ export type RowConfigCardProps = RowConfigViewProps & RowConfigFace;
24
+ /**
25
+ * Render one row's configuration entry.
26
+ * @param props - the page's view/form seat plus this row's face.
27
+ * @returns the entry; `summary` renders nothing for an untouched row (the page then keeps the row's own description).
28
+ */
29
+ export declare function RowConfigCard(props: RowConfigCardProps): ReactNode;
30
+ //# sourceMappingURL=RowConfigCard.d.ts.map
@@ -1,5 +1,6 @@
1
1
  /**
2
- * The section shell: title, one-line explanation, then one card per namespace.
2
+ * The section shell: title, one-line explanation, then one card per namespace — with the staged-write
3
+ * window's card ahead of them all, because it is the only card here that waits on a decision.
3
4
  *
4
5
  * This is the shape the platform's own settings sections use — a heading plus the
5
6
  * child slots it declared in its registration. The cards themselves are separate
@@ -29,4 +29,15 @@ export interface ClientParamSection {
29
29
  fields: readonly ClientParamField[];
30
30
  }
31
31
  export declare const CLIENT_PARAM_SECTIONS: readonly ClientParamSection[];
32
+ /**
33
+ * One bundle that mounts family rows: the plugins.row.config key of a row is
34
+ * <bundle package name>#<row id>. The names are the SOURCE-plane spellings -
35
+ * the release rescopes them with every other package literal.
36
+ */
37
+ export interface ClientRowSeat {
38
+ bundle: string;
39
+ rows: readonly string[];
40
+ }
41
+ /** Every (bundle, row) pair the Plugins page can open a configuration page for. */
42
+ export declare const CLIENT_ROW_SEATS: readonly ClientRowSeat[];
32
43
  //# sourceMappingURL=generated-params.d.ts.map
@@ -1,35 +1,49 @@
1
1
  /**
2
- * Browser half: one settings section ("自进化") hosting one card per namespace.
2
+ * Browser half: one settings section ("自进化") hosting one card per family row, plus the
3
+ * Plugins page's per-row configuration entry for each of those rows.
3
4
  *
4
5
  * Until 0.7.0 the cards lived inside the platform's own 插件 section, because
5
6
  * this bundle injected `@deepseek-ai/dsh-client-ui-settings-plugins` and claimed
6
7
  * its keyed `settings.plugin.item` slot. The parameters are not a plugin
7
8
  * inventory though — the other feature areas (market, skins, Web plugins, side
8
- * cards) each register their OWN section, so this bundle now does the same:
9
- * inject the settings shell, register `settings.section`, and host the cards in
10
- * a keyed child slot of that section.
9
+ * cards) each register their OWN section, so this bundle does the same: inject
10
+ * the settings shell, register `settings.section`, and host the cards in a keyed
11
+ * child slot of that section.
11
12
  *
12
- * The join key is still the namespace itself: the Host half of each owning plugin
13
- * registers it, this half claims it in the keyed card slot, and the section pairs
14
- * them. A namespace the Host does not serve renders nothing, and a deployment that
15
- * drops this row shows no section at all: the pair carries no Host-side behaviour.
13
+ * Since G2 that section is one of TWO render sites for the same card body. The other is
14
+ * the platform's Plugins page: each family row can be opened there, and the page renders
15
+ * whatever a plugin registered under key `<bundle package name>#<row id>`
16
+ * (`plugins.row.config`). The page owns that entry's subscription and hands the row's form
17
+ * in, so that path needs no seat of ours; our own section binds the same card to the
18
+ * client settings seat (`configForms`).
19
+ *
20
+ * The join key is the row's ENTRY ID — the platform derives a settings namespace from the
21
+ * Loader entry id, and that id is the registry's owner name — so the card, the seat lookup
22
+ * and the page key all spell one id. The legacy namespace strings in the registry
23
+ * (`evolution-memory`, …) are the G3 migration source, never a live lookup key.
24
+ *
25
+ * A row the Host does not serve renders nothing, and a deployment that drops this row
26
+ * shows no section at all: the pair carries no Host-side behaviour.
16
27
  * @module @lmzhen/dsh-evolution-settings-ui/client
17
28
  */
18
29
  import type { Context as ClientContext } from '@deepseek-ai/cordis';
19
- /** Required client services: the slot registry, the settings transport, the locale seat. */
30
+ import { PENDING_CARD_KEY } from './seam.ts';
31
+ /**
32
+ * Required client services: the slot registry and the locale seat.
33
+ *
34
+ * The settings seat (`configForms`) is deliberately NOT here. A required inject for a
35
+ * service the deployment did not compose leaves this whole browser half PENDING forever —
36
+ * no error, no section, no UI — and the family is also installed without a settings
37
+ * surface. It is probed instead, and the cards say what is missing.
38
+ */
20
39
  export declare const inject: string[];
21
40
  /** Section id and nav order (the platform's own sections sit at 0/10/15/20). */
22
41
  export declare const SECTION_ID = "evolution";
23
42
  export declare const SECTION_ORDER = 25;
43
+ /** The pending-window card's key, re-exported from `seam.ts` (the section names it as well). */
44
+ export { PENDING_CARD_KEY };
24
45
  /**
25
- * The pending-window card's slot key.
26
- *
27
- * The section pairs cards by key and the parameter cards use their settings namespace; this card has no
28
- * namespace (its data comes from the host's approval routes), so it carries a key of its own.
29
- */
30
- export declare const PENDING_CARD_KEY = "approval-pending";
31
- /**
32
- * Register the locale namespace, the section and its cards.
46
+ * Register the locale namespace, the section, its cards, and the per-row configuration entries.
33
47
  * @param ctx - client cordis context.
34
48
  */
35
49
  export declare function apply(ctx: ClientContext): void;
@@ -13,11 +13,12 @@ export declare const NS = "evolution-settings";
13
13
  /** Chinese copy (the family's primary language). */
14
14
  export declare const zh: {
15
15
  readonly title: "自进化";
16
- readonly subtitle: "下面按功能列出你能改的参数;没有列出的由安装时的配置决定。";
16
+ readonly subtitle: "待你决定的写入在最上面;下面按功能列出你能改的参数,没有列出的由安装时的配置决定。";
17
17
  readonly overridden: "我改过";
18
18
  readonly deployment: "默认";
19
19
  readonly loading: "正在读取设置…";
20
20
  readonly unavailable: "这个功能没有启动(对应的插件没有安装)";
21
+ readonly seatMissing: "这个界面没有提供设置服务,参数只能看不能改";
21
22
  readonly readonly: "本次会话不允许写入设置(临时运行模式)";
22
23
  readonly apply: "保存";
23
24
  readonly reset: "恢复默认值";
@@ -2,39 +2,104 @@
2
2
  * Structural view of the client seams this bundle uses.
3
3
  *
4
4
  * The package is distributed OUTSIDE the platform repository, so it declares the
5
- * two seams it consumes instead of importing another plugin's values (forbidden
6
- * by the client bundle-purity rule) or its types (which would drag the platform
7
- * sources into this project). The shapes are the shell's contract: a
8
- * `settings.section` registration hosting a keyed card slot, the inject faces
9
- * both hand back, the per-namespace settings scope the write path fences with the
10
- * revision it read, and the locale seat the section's copy resolves through.
5
+ * seams it consumes instead of importing another plugin's values (forbidden by the
6
+ * client bundle-purity rule) or its types (which would drag the platform sources
7
+ * into this project). The shapes are the shell's contract: a `settings.section`
8
+ * registration hosting a keyed card slot, the keyed `plugins.row.config` seat the
9
+ * Plugins page opens per row, the client settings seat (`configForms`, one form per
10
+ * Host plugin entry id), and the locale seat the section's copy resolves through.
11
11
  * @module @lmzhen/dsh-evolution-settings-ui/client
12
12
  */
13
13
  import type { ReactNode } from 'react';
14
14
  /** Our card slot, hosted by the section this bundle registers. */
15
15
  export declare const CARD_SLOT = "evolution.namespace.card";
16
- /** Snapshot of one settings namespace, as the client scope reports it. */
16
+ /** The Plugins page's per-row configuration seat (declared by `ui-plugin-manager`). */
17
+ export declare const ROW_CONFIG_SLOT = "plugins.row.config";
18
+ /**
19
+ * The key the Plugins page files a row's configuration under.
20
+ *
21
+ * Spelled here because the page's own helper cannot be imported (a value import from
22
+ * another plugin's client half is forbidden):
23
+ * `ui-plugin-manager/src/client/config-ledger.ts:36` is the definition this mirrors.
24
+ * @param bundle - the bundle package name that declares the row.
25
+ * @param rowId - the row id that bundle's patch declares.
26
+ * @returns the `plugins.row.config` key.
27
+ */
28
+ export declare function rowConfigKey(bundle: string, rowId: string): string;
29
+ /**
30
+ * The pending-window card's slot key.
31
+ *
32
+ * The section pairs cards by key and the parameter cards use their settings namespace; this card has no
33
+ * namespace (its data comes from the host's approval routes), so it carries a key of its own. The constant
34
+ * lives HERE and not in `index.ts` because the SECTION has to name it too, and `index.ts` already imports
35
+ * the section — one definition in the module both sides already share is what keeps that pair acyclic.
36
+ */
37
+ export declare const PENDING_CARD_KEY = "approval-pending";
38
+ /** Snapshot of one settings row, as the client seat reports it. */
17
39
  export interface ParamSectionSnapshot {
18
40
  status: 'loading' | 'ready' | 'unavailable';
19
41
  value: Record<string, unknown> | undefined;
20
42
  /** Raw user section: a key's PRESENCE marks the override, not its value. */
21
43
  user: unknown;
22
44
  writable: boolean;
45
+ /**
46
+ * Why an `unavailable` snapshot is unavailable: this bundle found no settings seat at
47
+ * all, or the seat is there and serves no such row. The two read differently to the
48
+ * operator (one is the UI's own gap, the other is a row that is not composed).
49
+ */
50
+ reason?: 'seat-missing' | 'not-served';
23
51
  }
24
52
  /** The reactive source the renderer binds to a `use<Name>` seat. */
25
53
  export interface ParamSectionSource {
26
54
  getSnapshot(): ParamSectionSnapshot;
27
55
  subscribe(listener: () => void): () => void;
28
56
  }
29
- /** One namespace's write face. */
30
- export interface SettingsScopeLike {
31
- set(field: string, value: unknown): Promise<void>;
32
- unset(field: string): Promise<void>;
57
+ /** One ordered field operation, as the client settings seat takes them. */
58
+ export interface SettingsPathOpLike {
59
+ readonly op: 'set' | 'unset';
60
+ readonly path: readonly string[];
61
+ readonly value?: unknown;
62
+ }
63
+ /** One row's client form state, as `configForms.get(entryId)` reports it. */
64
+ export interface ConfigFormSnapshotLike<T = Record<string, unknown>> {
65
+ status: 'loading' | 'ready' | 'unavailable';
66
+ value: T | undefined;
67
+ /** Composition layer the Host resolved `value` over; what a cleared field reverts to. */
68
+ base: unknown;
69
+ /** Raw user layer: a key's PRESENCE marks the override. */
70
+ user: unknown;
71
+ revision: number | undefined;
72
+ writable: boolean;
73
+ mode: 'host' | 'memory';
74
+ }
75
+ /** One row's client write face (0.2.x `configForms`). */
76
+ export interface ConfigFormLike<T = Record<string, unknown>> {
77
+ getSnapshot(): ConfigFormSnapshotLike<T>;
78
+ subscribe(listener: () => void): () => void;
79
+ set(field: string, value: unknown): Promise<boolean>;
80
+ unset(field: string): Promise<boolean>;
81
+ mutate(ops: readonly SettingsPathOpLike[], expectedRevision?: number): Promise<boolean>;
82
+ }
83
+ /**
84
+ * The client settings seat, probed rather than injected: a deployment may compose the
85
+ * family without the settings surface, and a REQUIRED inject for a missing service
86
+ * leaves the whole browser half pending forever (no error, no UI).
87
+ */
88
+ export interface ConfigFormsSeat {
89
+ /** One Host plugin entry id to its form. */
90
+ get<T>(entryId: string): ConfigFormLike<T>;
33
91
  }
34
92
  /** Registration options the keyed card slot under our own section takes. */
35
93
  export interface SlotRegisterOptions {
36
94
  name: typeof CARD_SLOT;
37
- /** The settings namespace this card edits — the slot's key. */
95
+ /** The settings row this card edits — the slot's key. */
96
+ key: string;
97
+ inject: () => unknown;
98
+ }
99
+ /** Registration options the Plugins page's per-row config seat takes. */
100
+ export interface RowSeatRegisterOptions {
101
+ name: typeof ROW_CONFIG_SLOT;
102
+ /** `<bundle package name>#<row id>`, as the page's ledger builds it. */
38
103
  key: string;
39
104
  inject: () => unknown;
40
105
  }
@@ -64,6 +129,23 @@ export interface SectionFace {
64
129
  /** Render one child slot; `filter` selects keyed entries (e.g. `{ entryKey }`). */
65
130
  renderSlot: (name: string, props?: Record<string, unknown>, filter?: Record<string, unknown>) => ReactNode;
66
131
  }
132
+ /**
133
+ * The values and commands the Plugins page passes to a `plugins.row.config` entry:
134
+ * the page owns the subscription (it re-renders the entry), so the entry renders from
135
+ * `state` and writes through `mutate`. Both views receive this face; `summary` uses it
136
+ * only to say whether anything is overridden.
137
+ */
138
+ export interface RowConfigViewProps {
139
+ /** `summary` is the row's one-line description fallback; `page` is the whole form. */
140
+ readonly view: 'summary' | 'page';
141
+ /** Absent when the Host serves no configuration for this row. */
142
+ readonly form?: RowConfigForm | undefined;
143
+ }
144
+ /** The row form the page derived from its settings seat. */
145
+ export interface RowConfigForm {
146
+ readonly state: ConfigFormSnapshotLike;
147
+ readonly mutate: (ops: readonly SettingsPathOpLike[], revision?: number) => Promise<boolean>;
148
+ }
67
149
  /** The locale seat: register this bundle's namespace, bind a translator. */
68
150
  export interface LocaleSeat {
69
151
  register(namespace: string, dictionaries: {
@@ -76,12 +158,7 @@ export interface LocaleSeat {
76
158
  export interface ClientSeam {
77
159
  slots: {
78
160
  inject(name: string, callback: () => Generator<unknown, void, unknown>): unknown;
79
- register(options: SlotRegisterOptions | SectionRegisterOptions, component: (props: never) => unknown): () => void;
80
- };
81
- settingsScope: {
82
- bind(spec: {
83
- namespace: string;
84
- }): SettingsScopeLike;
161
+ register(options: SlotRegisterOptions | RowSeatRegisterOptions | SectionRegisterOptions, component: (props: never) => unknown): () => void;
85
162
  };
86
163
  locale: LocaleSeat;
87
164
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The adapter between the platform's client settings seat and this bundle's cards.
3
+ *
4
+ * One place maps `configForms.get(entryId)` to the three things a card needs — a
5
+ * snapshot, a subscription and the two write calls — so our own section's cards and the
6
+ * Plugins page's per-row entry cannot drift apart in how they read or write a row.
7
+ * @module @lmzhen/dsh-evolution-settings-ui/client
8
+ */
9
+ import type { ConfigFormSnapshotLike, ConfigFormsSeat, ParamSectionSnapshot, ParamSectionSource } from './seam.ts';
10
+ /** What a card shows when this UI found no settings seat at all. */
11
+ export declare const SEAT_MISSING: ParamSectionSnapshot;
12
+ /**
13
+ * Whether a value is a plain section object.
14
+ * @param value - the value to test.
15
+ * @returns true for a non-null, non-array object.
16
+ */
17
+ export declare function isSection(value: unknown): value is Record<string, unknown>;
18
+ /**
19
+ * Project one platform snapshot into the card's own shape.
20
+ * @param snapshot - the seat's snapshot.
21
+ * @returns the card's snapshot, with the reason an unavailable one is unavailable.
22
+ */
23
+ export declare function sectionSnapshotOf(snapshot: ConfigFormSnapshotLike): ParamSectionSnapshot;
24
+ /**
25
+ * The live source one row's card binds.
26
+ * @param seat - the probed settings seat, when the deployment composed it.
27
+ * @param namespace - the row's entry id.
28
+ * @returns the source; without a seat it reports the stated empty state and never notifies.
29
+ */
30
+ export declare function sourceFor(seat: ConfigFormsSeat | undefined, namespace: string): ParamSectionSource;
31
+ /**
32
+ * The write pair for one row.
33
+ * @param seat - the probed settings seat, when the deployment composed it.
34
+ * @param namespace - the row's entry id.
35
+ * @returns the two calls a card's save/reset path uses; without a seat they resolve without writing.
36
+ */
37
+ export declare function writeFaceFor(seat: ConfigFormsSeat | undefined, namespace: string): {
38
+ write: (field: string, value: unknown) => Promise<void>;
39
+ clear: (field: string) => Promise<void>;
40
+ };
41
+ //# sourceMappingURL=source.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-settings-ui",
3
3
  "description": "Web settings cards for the evolution family's parameter namespaces (community build)",
4
- "version": "0.16.0",
4
+ "version": "0.17.0-rc.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },