@linxin666/dsh-client-ui-task-board 0.3.6 → 0.3.10

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 (51) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +9 -4
  3. package/README.zh.md +9 -4
  4. package/lib/client.js +742 -75
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +698 -190
  7. package/lib/types/client/TaskBoardSettingsCard.d.ts +2 -1
  8. package/lib/types/client/board/TaskBoard.d.ts +3 -0
  9. package/lib/types/client/board-mount.d.ts +4 -1
  10. package/lib/types/client/host-api.d.ts +2 -2
  11. package/lib/types/client/index.d.ts +10 -2
  12. package/lib/types/client/locales.d.ts +21 -0
  13. package/lib/types/client/settings-form.d.ts +35 -43
  14. package/lib/types/client/sidebar-entry-core.d.ts +10 -0
  15. package/lib/types/client/sidebar-entry.d.ts +7 -1
  16. package/lib/types/core/controller.d.ts +11 -2
  17. package/lib/types/core/freeze-snapshot.d.ts +71 -0
  18. package/lib/types/core/handover.d.ts +58 -0
  19. package/lib/types/core/tasks.d.ts +70 -1
  20. package/lib/types/core/use-cases/task-update.d.ts +16 -7
  21. package/lib/types/host-ledger.d.ts +17 -2
  22. package/lib/types/host-runner.d.ts +43 -23
  23. package/lib/types/host-service.d.ts +8 -5
  24. package/lib/types/index.d.ts +9 -1
  25. package/lib/types/protocol.d.ts +15 -2
  26. package/package.json +26 -17
  27. package/src/client/TaskBoardSettingsCard.tsx +2 -1
  28. package/src/client/board/NewTaskModal.tsx +52 -0
  29. package/src/client/board/TaskBoard.tsx +5 -3
  30. package/src/client/board/TaskCard.tsx +3 -0
  31. package/src/client/board/TaskDetail.tsx +54 -0
  32. package/src/client/board-mount.tsx +11 -1
  33. package/src/client/host-api.ts +5 -5
  34. package/src/client/index.ts +76 -10
  35. package/src/client/locales.ts +55 -2
  36. package/src/client/settings-form.ts +78 -92
  37. package/src/client/sidebar-entry-core.ts +33 -7
  38. package/src/client/sidebar-entry.ts +7 -1
  39. package/src/core/controller.ts +35 -10
  40. package/src/core/freeze-snapshot.ts +213 -0
  41. package/src/core/handover.ts +117 -0
  42. package/src/core/store.ts +48 -1
  43. package/src/core/tasks.ts +87 -0
  44. package/src/core/use-cases/task-update.ts +30 -10
  45. package/src/dsh-home.ts +1 -1
  46. package/src/host-ledger.ts +137 -59
  47. package/src/host-routes.ts +1 -1
  48. package/src/host-runner.ts +211 -97
  49. package/src/host-service.ts +14 -10
  50. package/src/index.ts +24 -10
  51. package/src/protocol.ts +98 -14
package/lib/client.js CHANGED
@@ -7,7 +7,6 @@ window.__ModuleLoader__.load({
7
7
  let react_dom_client = require("react-dom/client");
8
8
  let react = require("react");
9
9
  let react_jsx_runtime = require("react/jsx-runtime");
10
- let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
11
10
  //#region src/core/tasks.ts
12
11
  /** Statuses a settled task may be archived from. */
13
12
  const ARCHIVABLE_STATUSES = ["done", "failed"];
@@ -67,6 +66,20 @@ window.__ModuleLoader__.load({
67
66
  const trimmed = value?.trim();
68
67
  return trimmed === void 0 || trimmed === "" ? void 0 : trimmed;
69
68
  }
69
+ /**
70
+ * Build the persisted freeze snapshot from a sanitized input, stamping the
71
+ * freeze instant (shared by the create and update use cases).
72
+ */
73
+ function freezeOf(input, now) {
74
+ return {
75
+ goal: input.goal,
76
+ progress: input.progress,
77
+ next: input.next,
78
+ frozenAt: now,
79
+ ...input.redacted === true ? { redacted: true } : {},
80
+ ...input.frozenBy === void 0 || input.frozenBy === "" ? {} : { frozenBy: input.frozenBy }
81
+ };
82
+ }
70
83
  /** Create a task from user input. */
71
84
  function createTask(input, now, id) {
72
85
  return {
@@ -80,7 +93,12 @@ window.__ModuleLoader__.load({
80
93
  executions: [],
81
94
  workspaceId: normalizeTargetId(input.workspaceId),
82
95
  mode: normalizeTargetId(input.mode),
83
- permission: isTaskPermission(input.permission) ? input.permission : void 0
96
+ permission: isTaskPermission(input.permission) ? input.permission : void 0,
97
+ ...input.freeze === void 0 ? {} : { freeze: freezeOf(input.freeze, now) },
98
+ ...input.handover === void 0 ? {} : { handover: {
99
+ ...input.handover,
100
+ bundledAt: now
101
+ } }
84
102
  };
85
103
  }
86
104
  /** Clone a task with an updated status and a fresh updatedAt. */
@@ -455,13 +473,11 @@ window.__ModuleLoader__.load({
455
473
  * fresh updatedAt. Pure ledger transition (no persistence or notify — the
456
474
  * controller orchestrates those).
457
475
  *
458
- * An explicit `undefined` in the patch clears an execution-target field
459
- * (the task falls back to the runtime default); content fields are trimmed
460
- * and never cleared. An unknown permission string is ignored so stale UI
461
- * can never persist a value the execution service rejects.
476
+ * An explicit `undefined` in the patch clears the field (the task falls
477
+ * back to the runtime default); an unknown permission string is ignored so
478
+ * stale UI can never persist a value the execution service rejects.
462
479
  */
463
- /**
464
- * The fields that edit the task's content (what the user reads and what the
480
+ /** The fields that edit the task's content (what the user reads and what the
465
481
  * next execution sends). Unlike the execution targets they stay editable only
466
482
  * while the task has never started executing — after the first run the
467
483
  * recorded prompt is the record of what actually ran, so it becomes read-only.
@@ -495,12 +511,13 @@ window.__ModuleLoader__.load({
495
511
  function applyUpdateTask(tasks, id, patch, now) {
496
512
  return tasks.map((task) => {
497
513
  if (task.id !== id) return task;
514
+ const { freeze: freezePatch, handover: handoverPatch, ...rest } = patch;
498
515
  const workspaceId = "workspaceId" in patch ? normalizeTargetId(patch.workspaceId) : void 0;
499
516
  const mode = "mode" in patch ? normalizeTargetId(patch.mode) : void 0;
500
517
  const permission = "permission" in patch ? normalizePermission(task.permission, patch.permission) : void 0;
501
518
  const next = {
502
519
  ...task,
503
- ...patch,
520
+ ...rest,
504
521
  updatedAt: now
505
522
  };
506
523
  for (const field of TASK_CONTENT_FIELDS) {
@@ -508,6 +525,12 @@ window.__ModuleLoader__.load({
508
525
  const value = patch[field];
509
526
  next[field] = value === void 0 ? task[field] : value.trim();
510
527
  }
528
+ next.freeze = freezePatch == null ? void 0 : freezeOf(freezePatch, now);
529
+ next.handover = handoverPatch == null ? void 0 : {
530
+ ...handoverPatch,
531
+ bundledAt: now
532
+ };
533
+ if ("permission" in patch && patch.permission !== void 0 && patch.permission !== task.permission || "handover" in patch) next.permissionConfirmedAt = void 0;
511
534
  if (workspaceId !== void 0 || "workspaceId" in patch) next.workspaceId = workspaceId;
512
535
  if (mode !== void 0 || "mode" in patch) next.mode = mode;
513
536
  if (permission !== void 0 || "permission" in patch) next.permission = permission;
@@ -516,6 +539,9 @@ window.__ModuleLoader__.load({
516
539
  }
517
540
  //#endregion
518
541
  //#region src/core/controller.ts
542
+ function currentOf(sessions) {
543
+ return sessions?.list.getSnapshot().current;
544
+ }
519
545
  /** The selected task (resolved from the ledger), or undefined. */
520
546
  function selectedTaskOf(snapshot) {
521
547
  if (snapshot.selectedTaskId === void 0) return void 0;
@@ -818,6 +844,26 @@ window.__ModuleLoader__.load({
818
844
  return await this.commitRemote({
819
845
  kind: "run",
820
846
  taskId: id
847
+ }, id, currentOf(this.deps.sessions));
848
+ }
849
+ /**
850
+ * Confirm a card's above-default permission binding through the Host
851
+ * (resolves the pending-confirmation transaction; no-op otherwise).
852
+ */
853
+ async confirmPermission(id) {
854
+ if (this.tasks.find((candidate) => candidate.id === id) === void 0) return false;
855
+ if (this.deps.transport === void 0) {
856
+ this.tasks = this.tasks.map((candidate) => candidate.id === id ? {
857
+ ...candidate,
858
+ permissionConfirmedAt: this.now(),
859
+ updatedAt: this.now()
860
+ } : candidate);
861
+ this.persistAndNotify();
862
+ return true;
863
+ }
864
+ return await this.commitRemote({
865
+ kind: "confirm-permission",
866
+ taskId: id
821
867
  }, id);
822
868
  }
823
869
  /** Re-run a settled task through the Host (the Host replans and executes). */
@@ -828,7 +874,7 @@ window.__ModuleLoader__.load({
828
874
  await this.commitRemote({
829
875
  kind: "rerun",
830
876
  taskId: id
831
- }, id);
877
+ }, id, currentOf(this.deps.sessions));
832
878
  }
833
879
  /**
834
880
  * Session-list notifications fire for all kinds of incidental churn
@@ -845,10 +891,10 @@ window.__ModuleLoader__.load({
845
891
  if (this.deps.transport === void 0) this.deps.store.save(this.tasks);
846
892
  this.notify();
847
893
  }
848
- async commitRemote(action, taskId) {
894
+ async commitRemote(action, taskId, initiator) {
849
895
  if (this.deps.transport === void 0) return true;
850
- if (taskId === void 0) return await this.performRemote(action);
851
- const operation = (this.taskQueues.get(taskId) ?? Promise.resolve()).catch(() => {}).then(async () => await this.performRemote(action));
896
+ if (taskId === void 0) return await this.performRemote(action, initiator);
897
+ const operation = (this.taskQueues.get(taskId) ?? Promise.resolve()).catch(() => {}).then(async () => await this.performRemote(action, initiator));
852
898
  const tail = operation.then(() => {}, () => {});
853
899
  this.taskQueues.set(taskId, tail);
854
900
  this.pendingTaskIds.add(taskId);
@@ -863,13 +909,13 @@ window.__ModuleLoader__.load({
863
909
  }
864
910
  }
865
911
  }
866
- async performRemote(action) {
912
+ async performRemote(action, initiator) {
867
913
  const transport = this.deps.transport;
868
914
  if (transport === void 0) return true;
869
915
  this.transportError = void 0;
870
916
  this.notify();
871
917
  try {
872
- return this.acceptRemote(await transport.action(action)) || await this.refreshRemote();
918
+ return this.acceptRemote(await transport.action(action, initiator)) || await this.refreshRemote();
873
919
  } catch (error) {
874
920
  await this.refreshRemote(messageOf(error));
875
921
  return false;
@@ -955,6 +1001,248 @@ window.__ModuleLoader__.load({
955
1001
  for (const fn of [...this.listeners]) fn();
956
1002
  }
957
1003
  };
1004
+ /** Marker replacing every sensitive match. */
1005
+ const REDACTED_MARKER = "[REDACTED]";
1006
+ const BEGIN = "<<<FREEZE";
1007
+ const END = ">>>FREEZE";
1008
+ const SECTION_KEYS = /* @__PURE__ */ new Map([
1009
+ ["目标:", "goal"],
1010
+ ["进度:", "progress"],
1011
+ ["下一步:", "next"]
1012
+ ]);
1013
+ const SECTION_ORDER = [
1014
+ "goal",
1015
+ "progress",
1016
+ "next"
1017
+ ];
1018
+ const SECTION_NAMES = {
1019
+ goal: "目标",
1020
+ progress: "进度",
1021
+ next: "下一步"
1022
+ };
1023
+ /**
1024
+ * Sensitive patterns, each matched globally over every field body:
1025
+ * PEM private key blocks (whole block collapses to one marker), Bearer
1026
+ * credentials, OpenAI sk-, GitHub ghp_, GitLab glpat-, Slack xox* tokens,
1027
+ * and AWS access key ids.
1028
+ */
1029
+ const SENSITIVE_PATTERNS = [
1030
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
1031
+ /Bearer\s+[A-Za-z0-9._~+\/=:-]{8,}/gi,
1032
+ /sk-[A-Za-z0-9_-]{8,}/g,
1033
+ /ghp_[A-Za-z0-9]{20,}/g,
1034
+ /glpat-[A-Za-z0-9_-]{15,}/g,
1035
+ /xox[bpars]-[A-Za-z0-9-]{10,}/g,
1036
+ /AKIA[0-9A-Z]{16}/g
1037
+ ];
1038
+ function fail(code, message) {
1039
+ return {
1040
+ ok: false,
1041
+ error: {
1042
+ code,
1043
+ message
1044
+ }
1045
+ };
1046
+ }
1047
+ function utf8Bytes(text) {
1048
+ return new TextEncoder().encode(text).length;
1049
+ }
1050
+ /** Redact sensitive patterns to the marker; reports whether any hit occurred. */
1051
+ function redactSensitive(text) {
1052
+ let out = text;
1053
+ let redacted = false;
1054
+ for (const pattern of SENSITIVE_PATTERNS) {
1055
+ if (pattern.test(out)) {
1056
+ redacted = true;
1057
+ out = out.replace(pattern, REDACTED_MARKER);
1058
+ }
1059
+ pattern.lastIndex = 0;
1060
+ }
1061
+ return {
1062
+ text: out,
1063
+ redacted
1064
+ };
1065
+ }
1066
+ /**
1067
+ * True when any line of the text starts with "/" (a DSH command line).
1068
+ * Leading horizontal whitespace before the slash counts too: a frozen
1069
+ * body line like ` /kill` is still a command, not prose.
1070
+ */
1071
+ function hasSlashCommandLines(text) {
1072
+ return text.split(/\r?\n/).some((line) => /^[ \t]+\//.test(line) || line.startsWith("/"));
1073
+ }
1074
+ /**
1075
+ * Sanitize a structured freeze snapshot (the create/update action payload):
1076
+ * shape check, slash-command taint rejection, sensitive redaction, and the
1077
+ * per-field byte cap - the same gate parseFreezeRequest applies to
1078
+ * free-text freeze requests, exposed for the action data plane (issue #4).
1079
+ * @param value - the freeze object carried by an action or read back from disk.
1080
+ * @param extraKeys - keys allowed alongside goal/progress/next (e.g. the
1081
+ * protocol redacted flag, the ledger frozenAt stamp) and preserved verbatim
1082
+ * when present; their validation stays with the caller.
1083
+ */
1084
+ function sanitizeFreezeSnapshot(value, extraKeys = []) {
1085
+ const bad = (code, message) => ({
1086
+ ok: false,
1087
+ error: {
1088
+ code,
1089
+ message
1090
+ }
1091
+ });
1092
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return bad("invalid-freeze", "冻结快照必须是 goal/progress/next 字符串对象");
1093
+ const record = value;
1094
+ const allowed = [
1095
+ "goal",
1096
+ "progress",
1097
+ "next",
1098
+ ...extraKeys
1099
+ ];
1100
+ if (!Object.keys(record).every((key) => allowed.includes(key))) return bad("invalid-freeze", "冻结快照包含未知字段");
1101
+ for (const key of SECTION_ORDER) if (typeof record[key] !== "string") return bad("invalid-freeze", "冻结快照字段 " + SECTION_NAMES[key] + " 必须是字符串");
1102
+ for (const key of SECTION_ORDER) if (hasSlashCommandLines(record[key])) return bad("dsh-command-line", "冻结文本的" + SECTION_NAMES[key] + "包含以 / 开头的命令行,整体拒绝");
1103
+ let redacted = false;
1104
+ const snapshot = {
1105
+ goal: "",
1106
+ progress: "",
1107
+ next: ""
1108
+ };
1109
+ for (const key of SECTION_ORDER) {
1110
+ const result = redactSensitive(record[key]);
1111
+ snapshot[key] = result.text;
1112
+ redacted = redacted || result.redacted;
1113
+ }
1114
+ for (const key of SECTION_ORDER) if (utf8Bytes(snapshot[key]) > 8192) return bad("field-too-large", "冻结快照字段 " + SECTION_NAMES[key] + " 超过 8 KiB 上限");
1115
+ const extras = {};
1116
+ for (const key of extraKeys) if (key in record) extras[key] = record[key];
1117
+ return {
1118
+ ok: true,
1119
+ snapshot: {
1120
+ goal: snapshot.goal,
1121
+ progress: snapshot.progress,
1122
+ next: snapshot.next
1123
+ },
1124
+ redacted,
1125
+ extras
1126
+ };
1127
+ }
1128
+ /**
1129
+ * Parse the freeze-request format: a <<<FREEZE ... >>>FREEZE block whose body
1130
+ * is 目标: / 进度: / 下一步: section headers, each followed by body lines.
1131
+ * The gate applies before returning: slash-command taint rejects the whole
1132
+ * request, sensitive patterns are redacted to markers, and each field is
1133
+ * capped at FREEZE_FIELD_BYTE_LIMIT UTF-8 bytes.
1134
+ */
1135
+ function parseFreezeRequest(input) {
1136
+ const beginIndex = input.indexOf(BEGIN);
1137
+ if (beginIndex === -1) return fail("missing-block", "未找到冻结请求块(需要 <<<FREEZE ... >>>FREEZE)");
1138
+ const bodyStart = beginIndex + 9;
1139
+ const endIndex = input.indexOf(END, bodyStart);
1140
+ if (endIndex === -1) return fail("unterminated-block", "冻结请求块未闭合(缺少 >>>FREEZE)");
1141
+ const block = input.slice(bodyStart, endIndex);
1142
+ const raw = /* @__PURE__ */ new Map();
1143
+ let current;
1144
+ for (const line of block.split(/\r?\n/)) {
1145
+ const key = SECTION_KEYS.get(line);
1146
+ if (key !== void 0) {
1147
+ if (raw.has(key)) return fail("duplicate-section", `重复的段落标题:${SECTION_NAMES[key]}`);
1148
+ raw.set(key, []);
1149
+ current = key;
1150
+ } else if (current !== void 0) raw.get(current).push(line);
1151
+ }
1152
+ const snapshot = {
1153
+ goal: "",
1154
+ progress: "",
1155
+ next: ""
1156
+ };
1157
+ for (const key of SECTION_ORDER) {
1158
+ if (!raw.has(key)) return fail("missing-section", `冻结请求缺少段落:${SECTION_NAMES[key]}`);
1159
+ const lines = raw.get(key);
1160
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
1161
+ snapshot[key] = lines.join("\n");
1162
+ }
1163
+ for (const key of SECTION_ORDER) if (hasSlashCommandLines(snapshot[key])) return fail("dsh-command-line", `冻结文本的${SECTION_NAMES[key]}包含以 / 开头的命令行,整体拒绝`);
1164
+ let redacted = false;
1165
+ for (const key of SECTION_ORDER) {
1166
+ const result = redactSensitive(snapshot[key]);
1167
+ snapshot[key] = result.text;
1168
+ redacted = redacted || result.redacted;
1169
+ }
1170
+ for (const key of SECTION_ORDER) if (utf8Bytes(snapshot[key]) > 8192) return fail("field-too-large", `冻结快照字段 ${SECTION_NAMES[key]} 超过 8 KiB 上限`);
1171
+ return {
1172
+ ok: true,
1173
+ snapshot: {
1174
+ goal: snapshot.goal,
1175
+ progress: snapshot.progress,
1176
+ next: snapshot.next
1177
+ },
1178
+ warnings: redacted ? ["redacted"] : []
1179
+ };
1180
+ }
1181
+ /** The board's notion of the deployment session-default permission (fail-safe default). */
1182
+ const DEFAULT_SESSION_PERMISSION = "read-only";
1183
+ /** Permission elevation rank (higher = more authority). */
1184
+ const PERMISSION_RANK = /* @__PURE__ */ new Map([
1185
+ ["read-only", 0],
1186
+ ["workspace-write", 1],
1187
+ ["danger-full-access", 2]
1188
+ ]);
1189
+ function byteLength(value) {
1190
+ return new TextEncoder().encode(value).length;
1191
+ }
1192
+ /**
1193
+ * Gate a handover bundle from the wire or disk: exact keys, string targets
1194
+ * under the byte cap, a known permission, and a bounded string reference
1195
+ * list. Returns the sanitized bundle, or undefined when rejected.
1196
+ */
1197
+ function sanitizeHandover(value) {
1198
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
1199
+ const bundle = value;
1200
+ if (!Object.keys(bundle).every((key) => [
1201
+ "workspaceId",
1202
+ "mode",
1203
+ "permission",
1204
+ "references"
1205
+ ].includes(key))) return void 0;
1206
+ if (!Array.isArray(bundle.references)) return void 0;
1207
+ if (bundle.references.length > 32) return void 0;
1208
+ let total = 0;
1209
+ for (const reference of bundle.references) {
1210
+ if (typeof reference !== "string" || reference === "") return void 0;
1211
+ const size = byteLength(reference);
1212
+ if (size > 512) return void 0;
1213
+ total += size;
1214
+ if (total > 8192) return void 0;
1215
+ }
1216
+ for (const key of ["workspaceId", "mode"]) {
1217
+ const target = bundle[key];
1218
+ if (target !== void 0 && (typeof target !== "string" || byteLength(target) > 256)) return void 0;
1219
+ }
1220
+ if (bundle.permission !== void 0 && !isTaskPermission(bundle.permission)) return void 0;
1221
+ const workspaceId = bundle.workspaceId;
1222
+ const mode = bundle.mode;
1223
+ return {
1224
+ ...typeof workspaceId === "string" ? { workspaceId } : {},
1225
+ ...typeof mode === "string" ? { mode } : {},
1226
+ ...bundle.permission === void 0 ? {} : { permission: bundle.permission },
1227
+ references: [...bundle.references]
1228
+ };
1229
+ }
1230
+ /** The permission an execution session would actually run under. */
1231
+ function effectivePermission(task) {
1232
+ return task.handover?.permission ?? task.permission;
1233
+ }
1234
+ /** Whether a binding's permission is elevated above the session default. */
1235
+ function exceedsSessionDefault(permission, sessionDefault) {
1236
+ if (permission === void 0) return false;
1237
+ return (PERMISSION_RANK.get(permission) ?? -1) > (PERMISSION_RANK.get(sessionDefault) ?? -1);
1238
+ }
1239
+ /**
1240
+ * The confirmation-gate predicate: an elevated permission without a human
1241
+ * confirmation stamp. Manual run/rerun and cron must refuse such a card.
1242
+ */
1243
+ function requiresPermissionConfirmation(task, sessionDefault = DEFAULT_SESSION_PERMISSION) {
1244
+ return exceedsSessionDefault(effectivePermission(task), sessionDefault) && task.permissionConfirmedAt === void 0;
1245
+ }
958
1246
  //#endregion
959
1247
  //#region src/core/store.ts
960
1248
  /**
@@ -998,6 +1286,9 @@ window.__ModuleLoader__.load({
998
1286
  if (entry.endedAt !== void 0 && typeof entry.endedAt !== "number") return false;
999
1287
  if (entry.result !== void 0 && entry.result !== "succeeded" && entry.result !== "failed" && entry.result !== "cancelled") return false;
1000
1288
  if (entry.error !== void 0 && typeof entry.error !== "string") return false;
1289
+ if (entry.initiatedBy !== void 0 && typeof entry.initiatedBy !== "string") return false;
1290
+ if (entry.frozenBy !== void 0 && typeof entry.frozenBy !== "string") return false;
1291
+ if (entry.frozenAt !== void 0 && typeof entry.frozenAt !== "number") return false;
1001
1292
  }
1002
1293
  return true;
1003
1294
  }
@@ -1022,6 +1313,49 @@ window.__ModuleLoader__.load({
1022
1313
  lastTriggeredAt: typeof rule.lastTriggeredAt === "number" ? rule.lastTriggeredAt : void 0
1023
1314
  };
1024
1315
  }
1316
+ /**
1317
+ * Repair a persisted freeze snapshot: shape + gate re-check (slash taint,
1318
+ * redaction idempotence, byte cap); a malformed or tainted snapshot is
1319
+ * dropped (undefined) rather than dropping the whole task row, mirroring
1320
+ * the schedule repair policy.
1321
+ */
1322
+ function normalizeFreeze(value) {
1323
+ const result = sanitizeFreezeSnapshot(value, [
1324
+ "frozenAt",
1325
+ "redacted",
1326
+ "frozenBy"
1327
+ ]);
1328
+ if (!result.ok) return void 0;
1329
+ const frozenAt = result.extras.frozenAt;
1330
+ if (typeof frozenAt !== "number" || !Number.isFinite(frozenAt)) return void 0;
1331
+ if (result.extras.redacted !== void 0 && result.extras.redacted !== true) return void 0;
1332
+ const frozenBy = result.extras.frozenBy;
1333
+ if (frozenBy !== void 0 && (typeof frozenBy !== "string" || frozenBy === "")) return void 0;
1334
+ return {
1335
+ goal: result.snapshot.goal,
1336
+ progress: result.snapshot.progress,
1337
+ next: result.snapshot.next,
1338
+ frozenAt,
1339
+ ...result.redacted || result.extras.redacted === true ? { redacted: true } : {},
1340
+ ...frozenBy === void 0 ? {} : { frozenBy }
1341
+ };
1342
+ }
1343
+ /**
1344
+ * Repair a persisted handover bundle: shape re-check through the same gate
1345
+ * as the wire path; a malformed bundle is dropped rather than dropping the
1346
+ * task row (mirroring the schedule/freeze repair policy).
1347
+ */
1348
+ function normalizeHandover(value) {
1349
+ if (typeof value !== "object" || value === null) return void 0;
1350
+ const { bundledAt, ...rest } = value;
1351
+ const bundle = sanitizeHandover(rest);
1352
+ if (bundle === void 0) return void 0;
1353
+ if (typeof bundledAt !== "number" || !Number.isFinite(bundledAt)) return void 0;
1354
+ return {
1355
+ ...bundle,
1356
+ bundledAt
1357
+ };
1358
+ }
1025
1359
  /** Parse + validate a persisted ledger document; invalid rows are dropped. */
1026
1360
  function parseLedger(raw) {
1027
1361
  if (raw === null) return [];
@@ -1051,6 +1385,9 @@ window.__ModuleLoader__.load({
1051
1385
  task.mode = normalizeTargetId(row.mode);
1052
1386
  task.archivedAt = typeof row.archivedAt === "number" && Number.isFinite(row.archivedAt) ? row.archivedAt : void 0;
1053
1387
  task.permission = isTaskPermission(row.permission) ? row.permission : void 0;
1388
+ task.freeze = normalizeFreeze(row.freeze);
1389
+ task.handover = normalizeHandover(row.handover);
1390
+ task.permissionConfirmedAt = typeof row.permissionConfirmedAt === "number" && Number.isFinite(row.permissionConfirmedAt) ? row.permissionConfirmedAt : void 0;
1054
1391
  tasks.push(task);
1055
1392
  }
1056
1393
  return tasks;
@@ -1175,6 +1512,24 @@ window.__ModuleLoader__.load({
1175
1512
  "new.submit": "创建",
1176
1513
  "new.cancel": "取消",
1177
1514
  "new.required": "标题不能为空",
1515
+ "new.freeze": "冻结快照(可选)",
1516
+ "new.freezePlaceholder": "粘贴会话输出的 <<<FREEZE … >>>FREEZE 冻结块,解析为目标/进度/下一步(可选)",
1517
+ "detail.freeze": "冻结快照",
1518
+ "detail.freeze.goal": "目标",
1519
+ "detail.freeze.progress": "进度",
1520
+ "detail.freeze.next": "下一步",
1521
+ "detail.freeze.frozenAt": "冻结于 {time}",
1522
+ "detail.freeze.frozenBy": "来源会话 {session}",
1523
+ "detail.freeze.redacted": "冻结文本命中敏感模式,已自动替换为 [REDACTED];接手前请人工复核。",
1524
+ "card.frozen": "冻结",
1525
+ "new.handover": "交接包引用(可选)",
1526
+ "new.handoverPlaceholder": "每行一个文档/脚本引用;填写后,上面选择的钉住三元组(工作区/模式/权限)会作为交接包附加到卡片",
1527
+ "detail.handover": "交接包",
1528
+ "detail.handover.bundledAt": "打包于 {time}",
1529
+ "detail.handover.references": "引用",
1530
+ "detail.permissionPending": "权限待确认:本卡有效权限({permission})高于会话默认,手动执行与定时调度都会被拒绝,需人工确认后才能运行。",
1531
+ "detail.permissionConfirm": "确认权限绑定",
1532
+ "detail.permissionConfirmed": "已确认权限绑定 · {time}",
1178
1533
  "detail.title": "任务详情",
1179
1534
  "detail.edit": "编辑",
1180
1535
  "detail.close": "关闭",
@@ -1193,6 +1548,7 @@ window.__ModuleLoader__.load({
1193
1548
  "detail.viewSession": "查看会话",
1194
1549
  "detail.noSession": "暂无会话",
1195
1550
  "detail.executionStarted": "已启动",
1551
+ "detail.execution.initiator": "发起会话 {session}",
1196
1552
  "detail.executionEnded": "已结束",
1197
1553
  "detail.result.succeeded": "成功",
1198
1554
  "detail.result.failed": "失败",
@@ -1253,7 +1609,7 @@ window.__ModuleLoader__.load({
1253
1609
  "settings.off": "关",
1254
1610
  "settings.overridden": "已覆盖",
1255
1611
  "settings.reset": "恢复默认",
1256
- "settings.notExposed": "当前 DSH 版本未向设置页暴露本插件的配置命名空间,表单不可用。可编辑 ~/.dsh/settings.yaml 直接配置,或为 dsh-host-apiproxy 的 WEB_SETTINGS_NAMESPACES 白名单补充本命名空间后重启。",
1612
+ "settings.notExposed": "当前 DSH 版本未向设置页暴露本插件的配置命名空间,表单不可用。可编辑 ~/.dsh/settings.yaml 直接配置,或将本命名空间加入 Host 设置白名单后重启。",
1257
1613
  "settings.readOnly": "当前部署的设置只读。",
1258
1614
  "settings.expand": "展开设置",
1259
1615
  "settings.collapse": "收起设置",
@@ -1299,6 +1655,24 @@ window.__ModuleLoader__.load({
1299
1655
  "new.submit": "Create",
1300
1656
  "new.cancel": "Cancel",
1301
1657
  "new.required": "Title is required",
1658
+ "new.freeze": "Frozen snapshot (optional)",
1659
+ "new.freezePlaceholder": "Paste a <<<FREEZE ... >>>FREEZE block from a session; it parses into goal/progress/next (optional)",
1660
+ "detail.freeze": "Frozen snapshot",
1661
+ "detail.freeze.goal": "Goal",
1662
+ "detail.freeze.progress": "Progress",
1663
+ "detail.freeze.next": "Next",
1664
+ "detail.freeze.frozenAt": "Frozen at {time}",
1665
+ "detail.freeze.frozenBy": "Source session {session}",
1666
+ "detail.freeze.redacted": "Sensitive patterns were detected in the frozen text and replaced with [REDACTED]; review manually before resuming.",
1667
+ "card.frozen": "frozen",
1668
+ "new.handover": "Handover references (optional)",
1669
+ "new.handoverPlaceholder": "One doc/script reference per line; when filled, the pinned triplet picked above (workspace/mode/permission) is attached to the card as a handover bundle",
1670
+ "detail.handover": "Handover bundle",
1671
+ "detail.handover.bundledAt": "Bundled at {time}",
1672
+ "detail.handover.references": "References",
1673
+ "detail.permissionPending": "Permission pending confirmation: this card's effective permission ({permission}) is above the session default; manual runs and cron are refused until a human confirms.",
1674
+ "detail.permissionConfirm": "Confirm permission binding",
1675
+ "detail.permissionConfirmed": "Permission confirmed · {time}",
1302
1676
  "detail.title": "Task Detail",
1303
1677
  "detail.edit": "Edit",
1304
1678
  "detail.close": "Close",
@@ -1317,6 +1691,7 @@ window.__ModuleLoader__.load({
1317
1691
  "detail.viewSession": "View Session",
1318
1692
  "detail.noSession": "No session",
1319
1693
  "detail.executionStarted": "Started",
1694
+ "detail.execution.initiator": "Initiated by session {session}",
1320
1695
  "detail.executionEnded": "Ended",
1321
1696
  "detail.result.succeeded": "Succeeded",
1322
1697
  "detail.result.failed": "Failed",
@@ -1377,7 +1752,7 @@ window.__ModuleLoader__.load({
1377
1752
  "settings.off": "Off",
1378
1753
  "settings.overridden": "Overridden",
1379
1754
  "settings.reset": "Reset to default",
1380
- "settings.notExposed": "This DSH version does not expose this plugin's settings namespace to the configuration page, so the form is unavailable. Edit ~/.dsh/settings.yaml directly, or add the namespace to dsh-host-apiproxy's WEB_SETTINGS_NAMESPACES allowlist and restart.",
1755
+ "settings.notExposed": "This DSH version does not expose this plugin's settings namespace to the configuration page, so the form is unavailable. Edit ~/.dsh/settings.yaml directly, or add the namespace to the Host settings allowlist and restart.",
1381
1756
  "settings.readOnly": "This deployment stores settings read-only.",
1382
1757
  "settings.expand": "Show settings",
1383
1758
  "settings.collapse": "Hide settings",
@@ -1392,8 +1767,21 @@ window.__ModuleLoader__.load({
1392
1767
  function dictionary() {
1393
1768
  return (typeof document !== "undefined" ? document.documentElement.lang : "zh").toLowerCase().startsWith("en") ? en : zh;
1394
1769
  }
1770
+ /**
1771
+ * SDK translate seat wired by the browser apply() once ctx.locale is bound
1772
+ * (setRuntimeTranslate). When present it reads the ACTIVE locale at call
1773
+ * time, so plain-DOM surfaces (sidebar row, toggles) follow a runtime
1774
+ * language switch; the document-language pick above stays only as the
1775
+ * unwired fallback (locale service absent, module-scope early callers).
1776
+ */
1777
+ let runtimeT;
1778
+ /** Wire the SDK translate seat; pass undefined to restore the document-language pick. */
1779
+ function setRuntimeTranslate(t) {
1780
+ runtimeT = t;
1781
+ }
1395
1782
  /** Translate a key with optional {name} template params. */
1396
1783
  function t(key, params) {
1784
+ if (runtimeT !== void 0) return runtimeT(key, params);
1397
1785
  let text = dictionary()[key];
1398
1786
  if (params !== void 0) for (const [name, value] of Object.entries(params)) text = text.replaceAll(`{${name}}`, value);
1399
1787
  return text;
@@ -1603,6 +1991,9 @@ window.__ModuleLoader__.load({
1603
1991
  const [scheduleEnabled, setScheduleEnabled] = (0, react.useState)(false);
1604
1992
  const [scheduleCron, setScheduleCron] = (0, react.useState)("");
1605
1993
  const [scheduleError, setScheduleError] = (0, react.useState)(void 0);
1994
+ const [freezeText, setFreezeText] = (0, react.useState)("");
1995
+ const [freezeError, setFreezeError] = (0, react.useState)(void 0);
1996
+ const [handoverText, setHandoverText] = (0, react.useState)("");
1606
1997
  const [error, setError] = (0, react.useState)(void 0);
1607
1998
  const [pending, setPending] = (0, react.useState)(false);
1608
1999
  const [options, setOptions] = (0, react.useState)(controller.getSnapshot().executionOptions);
@@ -1615,11 +2006,32 @@ window.__ModuleLoader__.load({
1615
2006
  return;
1616
2007
  }
1617
2008
  }
2009
+ let freeze = void 0;
2010
+ if (freezeText.trim() !== "") {
2011
+ const parsed = parseFreezeRequest(freezeText);
2012
+ if (!parsed.ok) {
2013
+ setFreezeError(parsed.error.message);
2014
+ return;
2015
+ }
2016
+ freeze = {
2017
+ ...parsed.snapshot,
2018
+ ...parsed.warnings.includes("redacted") ? { redacted: true } : {}
2019
+ };
2020
+ }
2021
+ const references = handoverText.split("\n").map((line) => line.trim()).filter((line) => line !== "");
2022
+ const handover = references.length === 0 ? void 0 : {
2023
+ references,
2024
+ workspaceId: workspaceId === "" ? void 0 : workspaceId,
2025
+ mode: mode === "" ? void 0 : mode,
2026
+ permission: permission === "" ? void 0 : permission
2027
+ };
1618
2028
  setPending(true);
1619
2029
  if (await controller.createTaskConfirmed({
1620
2030
  title,
1621
2031
  description,
1622
2032
  prompt,
2033
+ freeze,
2034
+ handover,
1623
2035
  workspaceId: workspaceId === "" ? void 0 : workspaceId,
1624
2036
  mode: mode === "" ? void 0 : mode,
1625
2037
  permission: permission === "" ? void 0 : permission,
@@ -1658,6 +2070,43 @@ window.__ModuleLoader__.load({
1658
2070
  onDescriptionChange: setDescription,
1659
2071
  onPromptChange: setPrompt
1660
2072
  }),
2073
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2074
+ className: board_module_css_default.field,
2075
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2076
+ className: board_module_css_default.fieldLabel,
2077
+ children: t("new.freeze")
2078
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2079
+ className: board_module_css_default.input,
2080
+ rows: 4,
2081
+ value: freezeText,
2082
+ placeholder: t("new.freezePlaceholder"),
2083
+ spellCheck: false,
2084
+ onChange: (event) => {
2085
+ setFreezeText(event.target.value);
2086
+ setFreezeError(void 0);
2087
+ }
2088
+ })]
2089
+ }),
2090
+ freezeError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2091
+ className: board_module_css_default.formError,
2092
+ children: freezeError
2093
+ }),
2094
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2095
+ className: board_module_css_default.field,
2096
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2097
+ className: board_module_css_default.fieldLabel,
2098
+ children: t("new.handover")
2099
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2100
+ className: board_module_css_default.input,
2101
+ rows: 3,
2102
+ value: handoverText,
2103
+ placeholder: t("new.handoverPlaceholder"),
2104
+ spellCheck: false,
2105
+ onChange: (event) => {
2106
+ setHandoverText(event.target.value);
2107
+ }
2108
+ })]
2109
+ }),
1661
2110
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1662
2111
  className: board_module_css_default.field,
1663
2112
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1867,6 +2316,11 @@ window.__ModuleLoader__.load({
1867
2316
  formatTime(task.updatedAt)
1868
2317
  ]
1869
2318
  }),
2319
+ task.freeze !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2320
+ className: board_module_css_default.cardSchedule,
2321
+ title: task.freeze.goal,
2322
+ children: t("card.frozen")
2323
+ }),
1870
2324
  !archived && task.schedule?.enabled === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1871
2325
  className: board_module_css_default.cardSchedule,
1872
2326
  title: task.schedule.nextRunAt !== void 0 ? `${t("card.scheduled")} · ${formatHostTimestamp(task.schedule.nextRunAt, timeZone)}` : t("card.scheduled"),
@@ -2039,6 +2493,11 @@ window.__ModuleLoader__.load({
2039
2493
  execution.endedAt !== void 0 && ` · ${t("detail.executionEnded")} ${formatTime(execution.endedAt, timeZone)}`
2040
2494
  ]
2041
2495
  }),
2496
+ execution.initiatedBy !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2497
+ className: board_module_css_default.executionTimes,
2498
+ title: execution.initiatedBy,
2499
+ children: t("detail.execution.initiator", { session: execution.initiatedBy })
2500
+ }),
2042
2501
  execution.sessionId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2043
2502
  type: "button",
2044
2503
  className: board_module_css_default.linkButton,
@@ -2299,6 +2758,7 @@ window.__ModuleLoader__.load({
2299
2758
  const pending = snapshot.pendingTaskIds.includes(current.id);
2300
2759
  const transportError = snapshot.transportError;
2301
2760
  const timeZone = snapshot.host?.scheduler.timeZone;
2761
+ const permissionPending = requiresPermissionConfirmation(current, snapshot.host?.sessionDefaultPermission);
2302
2762
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2303
2763
  className: board_module_css_default.modalBackdrop,
2304
2764
  onMouseDown: (event) => {
@@ -2358,6 +2818,107 @@ window.__ModuleLoader__.load({
2358
2818
  children: current.description !== "" ? current.description : "—"
2359
2819
  })]
2360
2820
  }),
2821
+ current.freeze !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2822
+ className: board_module_css_default.detailSection,
2823
+ "data-dsh-part": "freeze",
2824
+ children: [
2825
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.freeze") }),
2826
+ current.freeze.redacted === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2827
+ className: board_module_css_default.formError,
2828
+ children: t("detail.freeze.redacted")
2829
+ }),
2830
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2831
+ className: board_module_css_default.detailText,
2832
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("detail.freeze.goal") })
2833
+ }),
2834
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
2835
+ className: board_module_css_default.promptBlock,
2836
+ children: current.freeze.goal
2837
+ }),
2838
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2839
+ className: board_module_css_default.detailText,
2840
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("detail.freeze.progress") })
2841
+ }),
2842
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
2843
+ className: board_module_css_default.promptBlock,
2844
+ children: current.freeze.progress
2845
+ }),
2846
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2847
+ className: board_module_css_default.detailText,
2848
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("detail.freeze.next") })
2849
+ }),
2850
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
2851
+ className: board_module_css_default.promptBlock,
2852
+ children: current.freeze.next
2853
+ }),
2854
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2855
+ className: board_module_css_default.detailMeta,
2856
+ children: t("detail.freeze.frozenAt", { time: formatHostTimestamp(current.freeze.frozenAt, timeZone) })
2857
+ }),
2858
+ current.freeze.frozenBy !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2859
+ className: board_module_css_default.detailMeta,
2860
+ children: t("detail.freeze.frozenBy", { session: current.freeze.frozenBy })
2861
+ })
2862
+ ]
2863
+ }),
2864
+ current.handover !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2865
+ className: board_module_css_default.detailSection,
2866
+ "data-dsh-part": "handover",
2867
+ children: [
2868
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.handover") }),
2869
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2870
+ className: board_module_css_default.detailText,
2871
+ children: [
2872
+ t("new.workspace"),
2873
+ ": ",
2874
+ current.handover.workspaceId ?? t("exec.workspace.recent"),
2875
+ " · ",
2876
+ t("new.mode"),
2877
+ ": ",
2878
+ current.handover.mode ?? t("exec.mode.default"),
2879
+ " · ",
2880
+ t("new.permission"),
2881
+ ": ",
2882
+ current.handover.permission === void 0 ? t("exec.permission.default") : t(`exec.permission.${current.handover.permission}`)
2883
+ ]
2884
+ }),
2885
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2886
+ className: board_module_css_default.detailText,
2887
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("detail.handover.references") })
2888
+ }),
2889
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
2890
+ className: board_module_css_default.executionList,
2891
+ children: current.handover.references.map((reference) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
2892
+ className: board_module_css_default.executionRow,
2893
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: reference })
2894
+ }, reference))
2895
+ }),
2896
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2897
+ className: board_module_css_default.detailMeta,
2898
+ children: t("detail.handover.bundledAt", { time: formatHostTimestamp(current.handover.bundledAt, timeZone) })
2899
+ })
2900
+ ]
2901
+ }),
2902
+ permissionPending && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2903
+ className: board_module_css_default.detailSection,
2904
+ "data-dsh-part": "permission-gate",
2905
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2906
+ className: board_module_css_default.formError,
2907
+ children: t("detail.permissionPending", { permission: t(`exec.permission.${current.handover?.permission ?? current.permission}`) })
2908
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2909
+ type: "button",
2910
+ className: board_module_css_default.primaryButton,
2911
+ disabled: pending,
2912
+ onClick: () => {
2913
+ controller.confirmPermission(current.id);
2914
+ },
2915
+ children: t("detail.permissionConfirm")
2916
+ })]
2917
+ }),
2918
+ current.permissionConfirmedAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2919
+ className: board_module_css_default.detailMeta,
2920
+ children: t("detail.permissionConfirmed", { time: formatHostTimestamp(current.permissionConfirmedAt, timeZone) })
2921
+ }),
2361
2922
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2362
2923
  className: board_module_css_default.detailSection,
2363
2924
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.prompt") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
@@ -2503,11 +3064,13 @@ window.__ModuleLoader__.load({
2503
3064
  * active. Cards open the task detail (never execute directly); the header
2504
3065
  * offers filter, new-task, and a back-to-chat escape.
2505
3066
  */
2506
- /** Case-insensitive title/description match. */
3067
+ /** Case-insensitive title/description/freeze-snapshot match. */
2507
3068
  function matchesFilter(task, filter) {
2508
3069
  if (filter.trim() === "") return true;
2509
3070
  const needle = filter.trim().toLowerCase();
2510
- return task.title.toLowerCase().includes(needle) || task.description.toLowerCase().includes(needle);
3071
+ const haystacks = [task.title, task.description];
3072
+ if (task.freeze !== void 0) haystacks.push(task.freeze.goal, task.freeze.progress, task.freeze.next);
3073
+ return haystacks.some((text) => text.toLowerCase().includes(needle));
2511
3074
  }
2512
3075
  /**
2513
3076
  * Memoized per-card adapter: with a stable `onOpen` from the board and an
@@ -2733,11 +3296,19 @@ window.__ModuleLoader__.load({
2733
3296
  * Mount the board React tree into the center column and bind its visibility
2734
3297
  * to the controller's boardOpen state.
2735
3298
  * @param controller - the board controller driving the view.
3299
+ * @param locale - locale-change source; when given, re-renders a mounted board
3300
+ * on a Language switch.
2736
3301
  * @returns disposer unmounting the tree and restoring the column.
2737
3302
  */
2738
- function mountBoard(controller) {
3303
+ function mountBoard(controller, locale) {
2739
3304
  let root;
2740
3305
  let container;
3306
+ let unsubscribeLocale;
3307
+ try {
3308
+ unsubscribeLocale = locale?.subscribe(() => {
3309
+ if (root !== void 0) root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskBoard, { controller }));
3310
+ });
3311
+ } catch {}
2741
3312
  const ensure = () => {
2742
3313
  if (container !== void 0) {
2743
3314
  if (container.isConnected) return;
@@ -2790,6 +3361,7 @@ window.__ModuleLoader__.load({
2790
3361
  document.removeEventListener(ACTIVATE_EVENT, onOtherActivate);
2791
3362
  waitObserver.disconnect();
2792
3363
  unsubscribe();
3364
+ unsubscribeLocale?.();
2793
3365
  document.documentElement.removeAttribute(ACTIVE_ATTR);
2794
3366
  root?.unmount();
2795
3367
  root = void 0;
@@ -2821,11 +3393,23 @@ window.__ModuleLoader__.load({
2821
3393
  entry.setAttribute("data-dsh-part", "sidebar-entry");
2822
3394
  }
2823
3395
  entry.className = options.css["entry"] ?? "";
2824
- entry.setAttribute("aria-label", options.label());
2825
- if (options.tooltip !== void 0) entry.setAttribute("title", options.tooltip());
2826
- entry.innerHTML = "<span class=\"" + (options.css["entryIcon"] ?? "") + "\">" + options.icon + "</span><span class=\"" + (options.css["entryLabel"] ?? "") + "\">" + options.label() + "</span>";
3396
+ const labelSpan = document.createElement("span");
3397
+ labelSpan.className = options.css["entryLabel"] ?? "";
3398
+ const iconSpan = document.createElement("span");
3399
+ iconSpan.className = options.css["entryIcon"] ?? "";
3400
+ iconSpan.innerHTML = options.icon;
3401
+ entry.append(iconSpan, labelSpan);
3402
+ const applyLabel = () => {
3403
+ entry.setAttribute("aria-label", options.label());
3404
+ if (options.tooltip !== void 0) entry.setAttribute("title", options.tooltip());
3405
+ labelSpan.textContent = options.label();
3406
+ };
3407
+ applyLabel();
2827
3408
  entry.addEventListener("click", options.onToggle);
2828
- return entry;
3409
+ return {
3410
+ entry,
3411
+ applyLabel
3412
+ };
2829
3413
  }
2830
3414
  /** Re-insert the entry after the New Session row (before the browser region). */
2831
3415
  function placeEntry(root, entry, options) {
@@ -2848,9 +3432,13 @@ window.__ModuleLoader__.load({
2848
3432
  */
2849
3433
  function mountSidebarEntry$1(options) {
2850
3434
  if (typeof document !== "undefined" && document.querySelector(options.rowSelector) !== null) return () => {};
2851
- const entry = createEntry(options);
3435
+ const { entry, applyLabel } = createEntry(options);
2852
3436
  let root;
2853
3437
  let placed = false;
3438
+ let unsubscribeRefresh;
3439
+ if (options.refresh !== void 0) try {
3440
+ unsubscribeRefresh = options.refresh.subscribe(applyLabel);
3441
+ } catch {}
2854
3442
  const tryPlace = () => {
2855
3443
  if (root !== void 0 && !root.isConnected) {
2856
3444
  rootObserver.disconnect();
@@ -2899,6 +3487,7 @@ window.__ModuleLoader__.load({
2899
3487
  return () => {
2900
3488
  waitObserver.disconnect();
2901
3489
  rootObserver.disconnect();
3490
+ unsubscribeRefresh?.();
2902
3491
  unsubscribeActive?.();
2903
3492
  entry.remove();
2904
3493
  };
@@ -2913,9 +3502,11 @@ window.__ModuleLoader__.load({
2913
3502
  * Mount the sidebar entry, waiting for the shell to render and self-healing
2914
3503
  * on later React re-renders.
2915
3504
  * @param controller - the board controller the entry toggles.
3505
+ * @param locale - locale-change source; when given, re-applies the label on
3506
+ * a Language switch (the plain-DOM row otherwise keeps the mount-time copy).
2916
3507
  * @returns disposer removing the entry and its observers.
2917
3508
  */
2918
- function mountSidebarEntry(controller) {
3509
+ function mountSidebarEntry(controller, locale) {
2919
3510
  return mountSidebarEntry$1({
2920
3511
  rowAttribute: "data-dsh-taskboard-entry",
2921
3512
  rowSelector: ENTRY_SELECTOR,
@@ -2923,6 +3514,7 @@ window.__ModuleLoader__.load({
2923
3514
  icon: ICON,
2924
3515
  css: board_module_css_default,
2925
3516
  label: () => t("entry.label"),
3517
+ refresh: locale === void 0 ? void 0 : { subscribe: (listener) => locale.subscribe(listener) },
2926
3518
  onToggle: () => {
2927
3519
  controller.toggleBoard();
2928
3520
  },
@@ -3341,6 +3933,19 @@ window.__ModuleLoader__.load({
3341
3933
  });
3342
3934
  }
3343
3935
  //#endregion
3936
+ //#region \0dsh-store-engine
3937
+ const platform = ["@deepseek-ai/dsh-client", "-store"].join("");
3938
+ const legacy = ["@deepseek-ai/dsh-client-runtime", "/client"].join("");
3939
+ let engine;
3940
+ try {
3941
+ engine = require(platform);
3942
+ } catch {
3943
+ engine = require(legacy);
3944
+ }
3945
+ const createSnapshotStore = engine.createSnapshotStore;
3946
+ engine.defineStore;
3947
+ engine.shallowEqual;
3948
+ //#endregion
3344
3949
  //#region src/client/settings-form.ts
3345
3950
  /** A boolean field, edited through true/false draft text. */
3346
3951
  function booleanField(field) {
@@ -3400,7 +4005,7 @@ window.__ModuleLoader__.load({
3400
4005
  }
3401
4006
  /** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
3402
4007
  bind(project) {
3403
- const store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(project());
4008
+ const store = createSnapshotStore(project());
3404
4009
  this.listeners.add(() => {
3405
4010
  store.set(project());
3406
4011
  });
@@ -3415,7 +4020,7 @@ window.__ModuleLoader__.load({
3415
4020
  exposed: snapshot.status === "ready",
3416
4021
  writable: snapshot.writable,
3417
4022
  dirty: plan.length > 0,
3418
- invalid: plan.some((item) => item.run === void 0),
4023
+ invalid: plan.some((item) => item.judge === void 0),
3419
4024
  saving: this.saving,
3420
4025
  failed: this.failed,
3421
4026
  ...this.failedReason === void 0 ? {} : { failedReason: this.failedReason }
@@ -3465,45 +4070,52 @@ window.__ModuleLoader__.load({
3465
4070
  };
3466
4071
  }
3467
4072
  /**
3468
- * Write every staged edit, then re-seed from what the Host accepted.
4073
+ * Write every staged edit in one atomic scope mutation, then re-seed from
4074
+ * what the Host accepted.
3469
4075
  *
3470
- * When the scope carries the optional batch surface (the dsh-web
3471
- * bridge scope), every planned write rides one mutation so cross-field
3472
- * validate hooks (baseURL+model) judge the batch as a unit instead of
3473
- * deadlocking on per-field writes. Otherwise the per-field loop runs.
3474
- * A field lands only when the Host reports it held the staged value; a
3475
- * landed field's draft is dropped, a failed one stays staged for the user.
3476
- * @returns settlement after every write and the read-back.
4076
+ * The whole batch rides one mutate, so cross-field validate hooks
4077
+ * (baseURL+model) judge it as a unit: the Host either applies every write
4078
+ * or refuses the batch. The 0.1.2 scope contract never rejects a refused
4079
+ * mutation — the scope recovers with a fresh Host view and resolves — so
4080
+ * resolution alone proves nothing: the outcome is judged by reading the
4081
+ * settled snapshot back, one planned write at a time, and one missed write
4082
+ * fails the whole save. A scope that still rejects on refusal (the dsh-web
4083
+ * bridge scope) reports through the same failure path with its rejection
4084
+ * message. A save that did not land keeps its drafts, so the user can
4085
+ * correct them instead of retyping.
4086
+ * @returns settlement after the mutation and the read-back.
3477
4087
  */
3478
4088
  async save() {
3479
4089
  const plan = this.plan();
3480
- const valid = plan.filter((item) => item.run !== void 0);
4090
+ const valid = plan.filter((item) => item.judge !== void 0);
3481
4091
  if (plan.length === 0 || this.saving || valid.length !== plan.length) return;
3482
- const plannedWrites = valid.map((item) => item.op);
3483
4092
  const pending = /* @__PURE__ */ new Map();
3484
4093
  for (const item of plan) pending.set(item.field, this.staged.get(item.field));
3485
4094
  this.saving = true;
3486
4095
  this.failed = false;
3487
4096
  this.failedReason = void 0;
3488
4097
  this.publish();
3489
- const landed = /* @__PURE__ */ new Set();
3490
- const batch = this.batchedScope();
3491
- if (batch !== void 0) {
3492
- const result = await batch.mutate(plannedWrites);
3493
- if (result.ok) {
3494
- for (const field of result.fields) if (field.landed) landed.add(field.field);
3495
- } else this.failedReason = result.message;
3496
- } else for (const item of valid) if (await item.run()) landed.add(item.field);
3497
- for (const [field, before] of pending) if (landed.has(field) && this.staged.get(field) === before) this.staged.delete(field);
4098
+ const ops = valid.map((item) => item.op.op === "set" ? {
4099
+ op: "set",
4100
+ path: [item.field],
4101
+ value: item.op.value
4102
+ } : {
4103
+ op: "unset",
4104
+ path: [item.field]
4105
+ });
4106
+ let failedReason;
4107
+ try {
4108
+ await this.scope.mutate(ops);
4109
+ } catch (error) {
4110
+ failedReason = error instanceof Error ? error.message : String(error);
4111
+ }
4112
+ const landed = failedReason === void 0 && valid.every((item) => item.judge());
4113
+ for (const [field, before] of pending) if (landed && this.staged.get(field) === before) this.staged.delete(field);
3498
4114
  this.saving = false;
3499
- this.failed = landed.size !== pending.size;
4115
+ this.failed = !landed;
4116
+ this.failedReason = failedReason;
3500
4117
  this.publish();
3501
4118
  }
3502
- /** The scope's batch surface when it supports one; undefined conservatively otherwise. */
3503
- batchedScope() {
3504
- const candidate = this.scope;
3505
- return typeof candidate?.mutate === "function" ? candidate : void 0;
3506
- }
3507
4119
  /**
3508
4120
  * Every staged edit a save would write. An entry whose draft is not a value
3509
4121
  * its field accepts carries no write: the form is still dirty, and the save
@@ -3522,7 +4134,7 @@ window.__ModuleLoader__.load({
3522
4134
  field,
3523
4135
  op: "unset"
3524
4136
  },
3525
- run: () => this.clear(field)
4137
+ judge: () => this.landedUnset(field)
3526
4138
  });
3527
4139
  continue;
3528
4140
  }
@@ -3534,7 +4146,7 @@ window.__ModuleLoader__.load({
3534
4146
  field,
3535
4147
  op: "unset"
3536
4148
  },
3537
- run: void 0
4149
+ judge: void 0
3538
4150
  });
3539
4151
  else if (write.kind === "clear") plan.push({
3540
4152
  field,
@@ -3542,7 +4154,7 @@ window.__ModuleLoader__.load({
3542
4154
  field,
3543
4155
  op: "unset"
3544
4156
  },
3545
- run: () => this.clear(field)
4157
+ judge: () => this.landedUnset(field)
3546
4158
  });
3547
4159
  else plan.push({
3548
4160
  field,
@@ -3551,20 +4163,26 @@ window.__ModuleLoader__.load({
3551
4163
  op: "set",
3552
4164
  value: write.value
3553
4165
  },
3554
- run: () => this.store(field, write.value)
4166
+ judge: () => this.landedSet(field, write.value)
3555
4167
  });
3556
4168
  }
3557
4169
  return plan;
3558
4170
  }
3559
- async clear(field) {
3560
- await this.scope.unset(field);
3561
- return !this.stored(field);
3562
- }
3563
- async store(field, value) {
3564
- await this.scope.set(field, value);
4171
+ /**
4172
+ * Read-back judgment for a planned set: the user layer must hold the
4173
+ * intended value once the mutation has settled.
4174
+ */
4175
+ landedSet(field, value) {
3565
4176
  if (this.specOf(field).secret) return true;
3566
4177
  return this.userLayer()?.[field] === value;
3567
4178
  }
4179
+ /**
4180
+ * Read-back judgment for a planned unset: the field must be gone from the
4181
+ * user layer once the mutation has settled.
4182
+ */
4183
+ landedUnset(field) {
4184
+ return !this.stored(field);
4185
+ }
3568
4186
  stage(field, edit) {
3569
4187
  this.staged.set(field, edit);
3570
4188
  this.failed = false;
@@ -3784,13 +4402,14 @@ window.__ModuleLoader__.load({
3784
4402
  async state() {
3785
4403
  return await this.request(`${TASK_BOARD_API_PREFIX}/state`, { cache: "no-store" });
3786
4404
  }
3787
- async action(action) {
3788
- return await this.post(uuid(), action);
4405
+ async action(action, initiator) {
4406
+ return await this.post(uuid(), action, initiator);
3789
4407
  }
3790
- async post(requestId, action) {
4408
+ async post(requestId, action, initiator) {
3791
4409
  const envelope = {
3792
4410
  requestId,
3793
- action
4411
+ action,
4412
+ ...initiator === void 0 || initiator === "" ? {} : { initiator }
3794
4413
  };
3795
4414
  return await this.request(`${TASK_BOARD_API_PREFIX}/action`, {
3796
4415
  method: "POST",
@@ -3844,7 +4463,7 @@ window.__ModuleLoader__.load({
3844
4463
  /** The building package's version, when the bundle carries it. */
3845
4464
  function bakedVersion() {
3846
4465
  try {
3847
- return "0.3.6";
4466
+ return "0.3.10";
3848
4467
  } catch {
3849
4468
  return;
3850
4469
  }
@@ -3912,7 +4531,14 @@ window.__ModuleLoader__.load({
3912
4531
  const NS = "task-board";
3913
4532
  /** Settings namespace the settings card edits (the Host plugin registers it). */
3914
4533
  const TASK_BOARD_NS = "task-board";
3915
- /** Required services (fiber inject waiting — the runtime must be up first). */
4534
+ /**
4535
+ * Required services (fiber inject waiting — the runtime must be up first).
4536
+ * The generated remote faces are probed at use time instead of injected:
4537
+ * `remote.agentPresets` only registers on 0.1.2-alpha.2 hosts (the
4538
+ * api-remotes contribution), so a hard wait would pend the entry forever
4539
+ * on hosts below that cohort, which serve the same roster through the
4540
+ * connection RPC face.
4541
+ */
3916
4542
  const inject = [
3917
4543
  "slots",
3918
4544
  "sessions",
@@ -3923,6 +4549,44 @@ window.__ModuleLoader__.load({
3923
4549
  "remote"
3924
4550
  ];
3925
4551
  /**
4552
+ * Read the agent-preset roster through whichever face the running host
4553
+ * serves: the generated api-remotes face (`remote.agentPresets`,
4554
+ * 0.1.2-alpha.2) or the connection RPC face
4555
+ * (`connection.api.agentPresets`, hosts below that cohort). Answers
4556
+ * undefined when the host serves neither, so the caller leaves the picker
4557
+ * options untouched instead of erroring.
4558
+ */
4559
+ async function readPresetRoster(ctx, remote) {
4560
+ let remotes;
4561
+ try {
4562
+ remotes = remote.agentPresets;
4563
+ } catch {
4564
+ remotes = void 0;
4565
+ }
4566
+ if (remotes !== void 0) {
4567
+ const response = await remotes.list();
4568
+ if (!response.ok) return {
4569
+ ok: false,
4570
+ presets: []
4571
+ };
4572
+ return {
4573
+ ok: true,
4574
+ presets: response.value.presets
4575
+ };
4576
+ }
4577
+ const legacy = ctx.get("connection").api?.agentPresets;
4578
+ if (legacy === void 0) return void 0;
4579
+ const response = await legacy.list({});
4580
+ if (!response.result.ok || response.result.value === void 0) return {
4581
+ ok: false,
4582
+ presets: []
4583
+ };
4584
+ return {
4585
+ ok: true,
4586
+ presets: response.result.value.presets ?? []
4587
+ };
4588
+ }
4589
+ /**
3926
4590
  * Mount the task board.
3927
4591
  * @param ctx - client root context (services: sessions, workspaces).
3928
4592
  */
@@ -3940,6 +4604,9 @@ window.__ModuleLoader__.load({
3940
4604
  return () => {};
3941
4605
  }
3942
4606
  }, "task-board: dictionaries");
4607
+ try {
4608
+ setRuntimeTranslate(ctx.locale.bind(NS));
4609
+ } catch {}
3943
4610
  const settingsScope = (ctx.get("webUiSettings") ?? ctx.settingsScope).bind({ namespace: TASK_BOARD_NS });
3944
4611
  const settingsCard = new TaskBoardSettingsCardController(settingsScope);
3945
4612
  ctx.slots.inject("web-ui.plugin.item", () => {
@@ -3964,7 +4631,7 @@ window.__ModuleLoader__.load({
3964
4631
  if (uiDisposer !== void 0) return;
3965
4632
  const sessions = ctx.get("sessions");
3966
4633
  const workspaces = ctx.get("workspaces");
3967
- const connection = ctx.get("connection");
4634
+ const remote = ctx.get("remote");
3968
4635
  const controller = new BoardController({
3969
4636
  store: new LocalStorageTaskStore(),
3970
4637
  transport: new HttpTaskBoardHostTransport(),
@@ -3986,9 +4653,9 @@ window.__ModuleLoader__.load({
3986
4653
  disposers.push(workspaces.list.subscribe(pushWorkspaceOptions));
3987
4654
  const pushPresetOptions = async () => {
3988
4655
  try {
3989
- const response = await connection.api.agentPresets.list({});
3990
- if (!response.result.ok) return;
3991
- controller.setExecutionOptions({ presets: response.result.value.presets.map((preset) => ({
4656
+ const roster = await readPresetRoster(ctx, remote);
4657
+ if (roster === void 0 || !roster.ok) return;
4658
+ controller.setExecutionOptions({ presets: roster.presets.map((preset) => ({
3992
4659
  id: preset.id,
3993
4660
  name: preset.name,
3994
4661
  description: preset.description,
@@ -4004,8 +4671,8 @@ window.__ModuleLoader__.load({
4004
4671
  pushPresetOptions();
4005
4672
  }));
4006
4673
  try {
4007
- disposers.push(mountSidebarEntry(controller));
4008
- disposers.push(mountBoard(controller));
4674
+ disposers.push(mountSidebarEntry(controller, ctx.locale));
4675
+ disposers.push(mountBoard(controller, ctx.locale));
4009
4676
  } catch (error) {
4010
4677
  console.error("[dsh-task-board] mount failed:", error);
4011
4678
  }