@linxin666/dsh-client-ui-task-board 0.3.5 → 0.3.9

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 (49) 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 +947 -354
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +712 -180
  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/TaskForm.d.ts +28 -0
  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 +19 -0
  13. package/lib/types/client/settings-form.d.ts +35 -43
  14. package/lib/types/core/controller.d.ts +11 -2
  15. package/lib/types/core/freeze-snapshot.d.ts +71 -0
  16. package/lib/types/core/handover.d.ts +58 -0
  17. package/lib/types/core/tasks.d.ts +70 -1
  18. package/lib/types/core/use-cases/task-update.d.ts +16 -7
  19. package/lib/types/host-ledger.d.ts +17 -2
  20. package/lib/types/host-runner.d.ts +44 -17
  21. package/lib/types/host-service.d.ts +8 -5
  22. package/lib/types/index.d.ts +9 -1
  23. package/lib/types/protocol.d.ts +15 -2
  24. package/package.json +26 -17
  25. package/src/client/TaskBoardSettingsCard.tsx +2 -1
  26. package/src/client/board/EditTaskModal.tsx +19 -55
  27. package/src/client/board/NewTaskModal.tsx +59 -42
  28. package/src/client/board/TaskBoard.tsx +5 -3
  29. package/src/client/board/TaskCard.tsx +3 -0
  30. package/src/client/board/TaskDetail.tsx +54 -0
  31. package/src/client/board/TaskForm.tsx +111 -0
  32. package/src/client/board-mount.tsx +7 -1
  33. package/src/client/host-api.ts +5 -5
  34. package/src/client/index.ts +67 -7
  35. package/src/client/locales.ts +40 -2
  36. package/src/client/settings-form.ts +78 -92
  37. package/src/core/controller.ts +36 -16
  38. package/src/core/freeze-snapshot.ts +213 -0
  39. package/src/core/handover.ts +117 -0
  40. package/src/core/store.ts +48 -1
  41. package/src/core/tasks.ts +87 -0
  42. package/src/core/use-cases/task-update.ts +30 -10
  43. package/src/dsh-home.ts +1 -1
  44. package/src/host-ledger.ts +137 -59
  45. package/src/host-routes.ts +1 -1
  46. package/src/host-runner.ts +223 -83
  47. package/src/host-service.ts +14 -10
  48. package/src/index.ts +24 -10
  49. 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;
@@ -615,7 +641,6 @@ window.__ModuleLoader__.load({
615
641
  this.notify();
616
642
  }
617
643
  closeBoard() {
618
- if (!this.boardOpen) return;
619
644
  this.boardOpen = false;
620
645
  this.notify();
621
646
  }
@@ -803,6 +828,7 @@ window.__ModuleLoader__.load({
803
828
  * @param sessionId - the execution session to open.
804
829
  */
805
830
  openSession(sessionId) {
831
+ this.closeBoard();
806
832
  this.deps.sessions.open(sessionId);
807
833
  }
808
834
  /**
@@ -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",
@@ -1498,6 +1873,95 @@ window.__ModuleLoader__.load({
1498
1873
  }
1499
1874
  ];
1500
1875
  //#endregion
1876
+ //#region src/client/board/TaskForm.tsx
1877
+ /** Modal overlay: closes on backdrop press, submits through the form. */
1878
+ function ModalShell({ ariaLabel, title, error, pending, submitLabel, onSubmit, onClose, children }) {
1879
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1880
+ className: board_module_css_default.modalBackdrop,
1881
+ onMouseDown: (event) => {
1882
+ if (event.target === event.currentTarget) onClose();
1883
+ },
1884
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
1885
+ className: board_module_css_default.modal,
1886
+ role: "dialog",
1887
+ "aria-label": ariaLabel,
1888
+ onSubmit: (event) => {
1889
+ event.preventDefault();
1890
+ onSubmit();
1891
+ },
1892
+ children: [
1893
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1894
+ className: board_module_css_default.modalTitle,
1895
+ children: title
1896
+ }),
1897
+ children,
1898
+ error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1899
+ className: board_module_css_default.formError,
1900
+ children: error
1901
+ }),
1902
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
1903
+ className: board_module_css_default.modalFooter,
1904
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1905
+ type: "button",
1906
+ className: board_module_css_default.ghostButton,
1907
+ onClick: onClose,
1908
+ children: t("new.cancel")
1909
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1910
+ type: "submit",
1911
+ className: board_module_css_default.primaryButton,
1912
+ disabled: pending,
1913
+ children: submitLabel
1914
+ })]
1915
+ })
1916
+ ]
1917
+ })
1918
+ });
1919
+ }
1920
+ /** Title + description + prompt fields shared by the new and edit task forms. */
1921
+ function TaskContentFields({ title, description, prompt, onTitleChange, onDescriptionChange, onPromptChange }) {
1922
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1923
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1924
+ className: board_module_css_default.field,
1925
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1926
+ className: board_module_css_default.fieldLabel,
1927
+ children: t("new.title")
1928
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1929
+ className: board_module_css_default.input,
1930
+ value: title,
1931
+ autoFocus: true,
1932
+ placeholder: t("new.titlePlaceholder"),
1933
+ onChange: (event) => onTitleChange(event.target.value)
1934
+ })]
1935
+ }),
1936
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1937
+ className: board_module_css_default.field,
1938
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1939
+ className: board_module_css_default.fieldLabel,
1940
+ children: t("new.description")
1941
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1942
+ className: board_module_css_default.input,
1943
+ rows: 3,
1944
+ value: description,
1945
+ placeholder: t("new.descriptionPlaceholder"),
1946
+ onChange: (event) => onDescriptionChange(event.target.value)
1947
+ })]
1948
+ }),
1949
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1950
+ className: board_module_css_default.field,
1951
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1952
+ className: board_module_css_default.fieldLabel,
1953
+ children: t("new.prompt")
1954
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1955
+ className: board_module_css_default.input,
1956
+ rows: 4,
1957
+ value: prompt,
1958
+ placeholder: t("new.promptPlaceholder"),
1959
+ onChange: (event) => onPromptChange(event.target.value)
1960
+ })]
1961
+ })
1962
+ ] });
1963
+ }
1964
+ //#endregion
1501
1965
  //#region src/client/board/NewTaskModal.tsx
1502
1966
  /**
1503
1967
  * New-task modal: title + description + the prompt that execution will send.
@@ -1514,6 +1978,9 @@ window.__ModuleLoader__.load({
1514
1978
  const [scheduleEnabled, setScheduleEnabled] = (0, react.useState)(false);
1515
1979
  const [scheduleCron, setScheduleCron] = (0, react.useState)("");
1516
1980
  const [scheduleError, setScheduleError] = (0, react.useState)(void 0);
1981
+ const [freezeText, setFreezeText] = (0, react.useState)("");
1982
+ const [freezeError, setFreezeError] = (0, react.useState)(void 0);
1983
+ const [handoverText, setHandoverText] = (0, react.useState)("");
1517
1984
  const [error, setError] = (0, react.useState)(void 0);
1518
1985
  const [pending, setPending] = (0, react.useState)(false);
1519
1986
  const [options, setOptions] = (0, react.useState)(controller.getSnapshot().executionOptions);
@@ -1526,11 +1993,32 @@ window.__ModuleLoader__.load({
1526
1993
  return;
1527
1994
  }
1528
1995
  }
1996
+ let freeze = void 0;
1997
+ if (freezeText.trim() !== "") {
1998
+ const parsed = parseFreezeRequest(freezeText);
1999
+ if (!parsed.ok) {
2000
+ setFreezeError(parsed.error.message);
2001
+ return;
2002
+ }
2003
+ freeze = {
2004
+ ...parsed.snapshot,
2005
+ ...parsed.warnings.includes("redacted") ? { redacted: true } : {}
2006
+ };
2007
+ }
2008
+ const references = handoverText.split("\n").map((line) => line.trim()).filter((line) => line !== "");
2009
+ const handover = references.length === 0 ? void 0 : {
2010
+ references,
2011
+ workspaceId: workspaceId === "" ? void 0 : workspaceId,
2012
+ mode: mode === "" ? void 0 : mode,
2013
+ permission: permission === "" ? void 0 : permission
2014
+ };
1529
2015
  setPending(true);
1530
2016
  if (await controller.createTaskConfirmed({
1531
2017
  title,
1532
2018
  description,
1533
2019
  prompt,
2020
+ freeze,
2021
+ handover,
1534
2022
  workspaceId: workspaceId === "" ? void 0 : workspaceId,
1535
2023
  mode: mode === "" ? void 0 : mode,
1536
2024
  permission: permission === "" ? void 0 : permission,
@@ -1547,216 +2035,192 @@ window.__ModuleLoader__.load({
1547
2035
  };
1548
2036
  /** Next-run preview for a valid armed cron (creation-time only). */
1549
2037
  const scheduleNextRun = scheduleEnabled && scheduleCron.trim() !== "" && isValidCron(scheduleCron) ? nextRunAtMs(scheduleCron, Date.now()) : void 0;
1550
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1551
- className: board_module_css_default.modalBackdrop,
1552
- onMouseDown: (event) => {
1553
- if (event.target === event.currentTarget) onClose();
2038
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ModalShell, {
2039
+ ariaLabel: t("board.new"),
2040
+ title: t("board.new"),
2041
+ error,
2042
+ pending,
2043
+ submitLabel: t("new.submit"),
2044
+ onSubmit: () => {
2045
+ submit();
1554
2046
  },
1555
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
1556
- className: board_module_css_default.modal,
1557
- role: "dialog",
1558
- "aria-label": t("board.new"),
1559
- onSubmit: (event) => {
1560
- event.preventDefault();
1561
- submit();
1562
- },
1563
- children: [
1564
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1565
- className: board_module_css_default.modalTitle,
1566
- children: t("board.new")
1567
- }),
1568
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1569
- className: board_module_css_default.field,
1570
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1571
- className: board_module_css_default.fieldLabel,
1572
- children: t("new.title")
1573
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1574
- className: board_module_css_default.input,
1575
- value: title,
1576
- autoFocus: true,
1577
- placeholder: t("new.titlePlaceholder"),
1578
- onChange: (event) => {
1579
- setTitle(event.target.value);
1580
- setError(void 0);
1581
- }
1582
- })]
1583
- }),
1584
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1585
- className: board_module_css_default.field,
1586
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1587
- className: board_module_css_default.fieldLabel,
1588
- children: t("new.description")
1589
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1590
- className: board_module_css_default.input,
1591
- rows: 3,
1592
- value: description,
1593
- placeholder: t("new.descriptionPlaceholder"),
1594
- onChange: (event) => {
1595
- setDescription(event.target.value);
1596
- }
1597
- })]
1598
- }),
1599
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1600
- className: board_module_css_default.field,
1601
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1602
- className: board_module_css_default.fieldLabel,
1603
- children: t("new.prompt")
1604
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1605
- className: board_module_css_default.input,
1606
- rows: 4,
1607
- value: prompt,
1608
- placeholder: t("new.promptPlaceholder"),
1609
- onChange: (event) => {
1610
- setPrompt(event.target.value);
1611
- }
1612
- })]
1613
- }),
1614
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1615
- className: board_module_css_default.field,
1616
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1617
- className: board_module_css_default.fieldLabel,
1618
- children: t("new.workspace")
1619
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1620
- className: board_module_css_default.select,
1621
- value: workspaceId,
1622
- onChange: (event) => {
1623
- setWorkspaceId(event.target.value);
1624
- },
1625
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1626
- value: "",
1627
- children: t("exec.workspace.recent")
1628
- }), options.workspaces.map((workspace) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1629
- value: workspace.workspaceId,
1630
- children: workspace.title
1631
- }, workspace.workspaceId))]
1632
- })]
1633
- }),
1634
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1635
- className: board_module_css_default.field,
1636
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1637
- className: board_module_css_default.fieldLabel,
1638
- children: t("new.mode")
1639
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1640
- className: board_module_css_default.select,
1641
- value: mode,
1642
- onChange: (event) => {
1643
- setMode(event.target.value);
1644
- },
1645
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1646
- value: "",
1647
- children: t("exec.mode.default")
1648
- }), options.presets.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1649
- value: preset.id,
1650
- disabled: preset.broken !== void 0,
1651
- children: [
1652
- preset.name ?? preset.id,
1653
- preset.isDefault ? t("exec.mode.defaultSuffix") : "",
1654
- preset.broken !== void 0 ? t("exec.mode.brokenSuffix") : ""
1655
- ]
1656
- }, preset.id))]
1657
- })]
1658
- }),
1659
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1660
- className: board_module_css_default.field,
1661
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1662
- className: board_module_css_default.fieldLabel,
1663
- children: t("new.permission")
1664
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1665
- className: board_module_css_default.select,
1666
- value: permission,
1667
- onChange: (event) => {
1668
- setPermission(event.target.value);
1669
- },
1670
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1671
- value: "",
1672
- children: t("exec.permission.default")
1673
- }), TASK_PERMISSIONS.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1674
- value: id,
1675
- children: t(`exec.permission.${id}`)
1676
- }, id))]
1677
- })]
1678
- }),
1679
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1680
- className: board_module_css_default.detailSection,
1681
- children: [
1682
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.schedule") }),
1683
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1684
- className: board_module_css_default.scheduleToggle,
2047
+ onClose,
2048
+ children: [
2049
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskContentFields, {
2050
+ title,
2051
+ description,
2052
+ prompt,
2053
+ onTitleChange: (value) => {
2054
+ setTitle(value);
2055
+ setError(void 0);
2056
+ },
2057
+ onDescriptionChange: setDescription,
2058
+ onPromptChange: setPrompt
2059
+ }),
2060
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2061
+ className: board_module_css_default.field,
2062
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2063
+ className: board_module_css_default.fieldLabel,
2064
+ children: t("new.freeze")
2065
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2066
+ className: board_module_css_default.input,
2067
+ rows: 4,
2068
+ value: freezeText,
2069
+ placeholder: t("new.freezePlaceholder"),
2070
+ spellCheck: false,
2071
+ onChange: (event) => {
2072
+ setFreezeText(event.target.value);
2073
+ setFreezeError(void 0);
2074
+ }
2075
+ })]
2076
+ }),
2077
+ freezeError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2078
+ className: board_module_css_default.formError,
2079
+ children: freezeError
2080
+ }),
2081
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2082
+ className: board_module_css_default.field,
2083
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2084
+ className: board_module_css_default.fieldLabel,
2085
+ children: t("new.handover")
2086
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2087
+ className: board_module_css_default.input,
2088
+ rows: 3,
2089
+ value: handoverText,
2090
+ placeholder: t("new.handoverPlaceholder"),
2091
+ spellCheck: false,
2092
+ onChange: (event) => {
2093
+ setHandoverText(event.target.value);
2094
+ }
2095
+ })]
2096
+ }),
2097
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2098
+ className: board_module_css_default.field,
2099
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2100
+ className: board_module_css_default.fieldLabel,
2101
+ children: t("new.workspace")
2102
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2103
+ className: board_module_css_default.select,
2104
+ value: workspaceId,
2105
+ onChange: (event) => {
2106
+ setWorkspaceId(event.target.value);
2107
+ },
2108
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2109
+ value: "",
2110
+ children: t("exec.workspace.recent")
2111
+ }), options.workspaces.map((workspace) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2112
+ value: workspace.workspaceId,
2113
+ children: workspace.title
2114
+ }, workspace.workspaceId))]
2115
+ })]
2116
+ }),
2117
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2118
+ className: board_module_css_default.field,
2119
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2120
+ className: board_module_css_default.fieldLabel,
2121
+ children: t("new.mode")
2122
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2123
+ className: board_module_css_default.select,
2124
+ value: mode,
2125
+ onChange: (event) => {
2126
+ setMode(event.target.value);
2127
+ },
2128
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2129
+ value: "",
2130
+ children: t("exec.mode.default")
2131
+ }), options.presets.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
2132
+ value: preset.id,
2133
+ disabled: preset.broken !== void 0,
2134
+ children: [
2135
+ preset.name ?? preset.id,
2136
+ preset.isDefault ? t("exec.mode.defaultSuffix") : "",
2137
+ preset.broken !== void 0 ? t("exec.mode.brokenSuffix") : ""
2138
+ ]
2139
+ }, preset.id))]
2140
+ })]
2141
+ }),
2142
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2143
+ className: board_module_css_default.field,
2144
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2145
+ className: board_module_css_default.fieldLabel,
2146
+ children: t("new.permission")
2147
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2148
+ className: board_module_css_default.select,
2149
+ value: permission,
2150
+ onChange: (event) => {
2151
+ setPermission(event.target.value);
2152
+ },
2153
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2154
+ value: "",
2155
+ children: t("exec.permission.default")
2156
+ }), TASK_PERMISSIONS.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2157
+ value: id,
2158
+ children: t(`exec.permission.${id}`)
2159
+ }, id))]
2160
+ })]
2161
+ }),
2162
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2163
+ className: board_module_css_default.detailSection,
2164
+ children: [
2165
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.schedule") }),
2166
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2167
+ className: board_module_css_default.scheduleToggle,
2168
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2169
+ type: "checkbox",
2170
+ checked: scheduleEnabled,
2171
+ onChange: (event) => {
2172
+ setScheduleEnabled(event.target.checked);
2173
+ if (!event.target.checked) setScheduleError(void 0);
2174
+ }
2175
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("detail.schedule.enable") })]
2176
+ }),
2177
+ scheduleEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2178
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2179
+ className: board_module_css_default.scheduleRow,
1685
2180
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1686
- type: "checkbox",
1687
- checked: scheduleEnabled,
2181
+ className: `${board_module_css_default.input} ${board_module_css_default.scheduleInput}${scheduleError !== void 0 ? ` ${board_module_css_default.scheduleInputInvalid}` : ""}`,
2182
+ value: scheduleCron,
2183
+ placeholder: "0 9 * * *",
2184
+ spellCheck: false,
2185
+ "aria-label": t("detail.schedule.cron"),
1688
2186
  onChange: (event) => {
1689
- setScheduleEnabled(event.target.checked);
1690
- if (!event.target.checked) setScheduleError(void 0);
2187
+ setScheduleCron(event.target.value);
2188
+ setScheduleError(void 0);
1691
2189
  }
1692
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("detail.schedule.enable") })]
1693
- }),
1694
- scheduleEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1695
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1696
- className: board_module_css_default.scheduleRow,
1697
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1698
- className: `${board_module_css_default.input} ${board_module_css_default.scheduleInput}${scheduleError !== void 0 ? ` ${board_module_css_default.scheduleInputInvalid}` : ""}`,
1699
- value: scheduleCron,
1700
- placeholder: "0 9 * * *",
1701
- spellCheck: false,
1702
- "aria-label": t("detail.schedule.cron"),
1703
- onChange: (event) => {
1704
- setScheduleCron(event.target.value);
1705
- setScheduleError(void 0);
1706
- }
1707
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1708
- className: board_module_css_default.schedulePreset,
2190
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2191
+ className: board_module_css_default.schedulePreset,
2192
+ value: "",
2193
+ "aria-label": t("detail.schedule.presets"),
2194
+ onChange: (event) => {
2195
+ if (event.target.value === "") return;
2196
+ setScheduleCron(event.target.value);
2197
+ setScheduleError(void 0);
2198
+ },
2199
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1709
2200
  value: "",
1710
- "aria-label": t("detail.schedule.presets"),
1711
- onChange: (event) => {
1712
- if (event.target.value === "") return;
1713
- setScheduleCron(event.target.value);
1714
- setScheduleError(void 0);
1715
- },
1716
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1717
- value: "",
1718
- children: [t("detail.schedule.presets"), "…"]
1719
- }), SCHEDULE_PRESETS.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1720
- value: preset.cron,
1721
- children: t(preset.label)
1722
- }, preset.cron))]
1723
- })]
1724
- }),
1725
- scheduleError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1726
- className: board_module_css_default.formError,
1727
- children: scheduleError
1728
- }),
1729
- scheduleError === void 0 && scheduleNextRun !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1730
- className: board_module_css_default.scheduleMeta,
1731
- children: [
1732
- t("detail.schedule.nextRun"),
1733
- " ",
1734
- new Date(scheduleNextRun).toLocaleString()
1735
- ]
1736
- })
1737
- ] })
1738
- ]
1739
- }),
1740
- error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1741
- className: board_module_css_default.formError,
1742
- children: error
1743
- }),
1744
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
1745
- className: board_module_css_default.modalFooter,
1746
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1747
- type: "button",
1748
- className: board_module_css_default.ghostButton,
1749
- onClick: onClose,
1750
- children: t("new.cancel")
1751
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1752
- type: "submit",
1753
- className: board_module_css_default.primaryButton,
1754
- disabled: pending,
1755
- children: t("new.submit")
1756
- })]
1757
- })
1758
- ]
1759
- })
2201
+ children: [t("detail.schedule.presets"), "…"]
2202
+ }), SCHEDULE_PRESETS.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2203
+ value: preset.cron,
2204
+ children: t(preset.label)
2205
+ }, preset.cron))]
2206
+ })]
2207
+ }),
2208
+ scheduleError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2209
+ className: board_module_css_default.formError,
2210
+ children: scheduleError
2211
+ }),
2212
+ scheduleError === void 0 && scheduleNextRun !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2213
+ className: board_module_css_default.scheduleMeta,
2214
+ children: [
2215
+ t("detail.schedule.nextRun"),
2216
+ " ",
2217
+ new Date(scheduleNextRun).toLocaleString()
2218
+ ]
2219
+ })
2220
+ ] })
2221
+ ]
2222
+ })
2223
+ ]
1760
2224
  });
1761
2225
  }
1762
2226
  //#endregion
@@ -1839,6 +2303,11 @@ window.__ModuleLoader__.load({
1839
2303
  formatTime(task.updatedAt)
1840
2304
  ]
1841
2305
  }),
2306
+ task.freeze !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2307
+ className: board_module_css_default.cardSchedule,
2308
+ title: task.freeze.goal,
2309
+ children: t("card.frozen")
2310
+ }),
1842
2311
  !archived && task.schedule?.enabled === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1843
2312
  className: board_module_css_default.cardSchedule,
1844
2313
  title: task.schedule.nextRunAt !== void 0 ? `${t("card.scheduled")} · ${formatHostTimestamp(task.schedule.nextRunAt, timeZone)}` : t("card.scheduled"),
@@ -1953,89 +2422,26 @@ window.__ModuleLoader__.load({
1953
2422
  setPending(false);
1954
2423
  setError(controller.getSnapshot().transportError ?? t("new.required"));
1955
2424
  };
1956
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1957
- className: board_module_css_default.modalBackdrop,
1958
- onMouseDown: (event) => {
1959
- if (event.target === event.currentTarget) onClose();
2425
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModalShell, {
2426
+ ariaLabel: t("edit.title"),
2427
+ title: t("edit.title"),
2428
+ error,
2429
+ pending,
2430
+ submitLabel: t("edit.save"),
2431
+ onSubmit: () => {
2432
+ submit();
1960
2433
  },
1961
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
1962
- className: board_module_css_default.modal,
1963
- role: "dialog",
1964
- "aria-label": t("edit.title"),
1965
- onSubmit: (event) => {
1966
- event.preventDefault();
1967
- submit();
2434
+ onClose,
2435
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskContentFields, {
2436
+ title,
2437
+ description,
2438
+ prompt,
2439
+ onTitleChange: (value) => {
2440
+ setTitle(value);
2441
+ setError(void 0);
1968
2442
  },
1969
- children: [
1970
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1971
- className: board_module_css_default.modalTitle,
1972
- children: t("edit.title")
1973
- }),
1974
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1975
- className: board_module_css_default.field,
1976
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1977
- className: board_module_css_default.fieldLabel,
1978
- children: t("new.title")
1979
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1980
- className: board_module_css_default.input,
1981
- value: title,
1982
- autoFocus: true,
1983
- placeholder: t("new.titlePlaceholder"),
1984
- onChange: (event) => {
1985
- setTitle(event.target.value);
1986
- setError(void 0);
1987
- }
1988
- })]
1989
- }),
1990
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1991
- className: board_module_css_default.field,
1992
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1993
- className: board_module_css_default.fieldLabel,
1994
- children: t("new.description")
1995
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1996
- className: board_module_css_default.input,
1997
- rows: 3,
1998
- value: description,
1999
- placeholder: t("new.descriptionPlaceholder"),
2000
- onChange: (event) => {
2001
- setDescription(event.target.value);
2002
- }
2003
- })]
2004
- }),
2005
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2006
- className: board_module_css_default.field,
2007
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2008
- className: board_module_css_default.fieldLabel,
2009
- children: t("new.prompt")
2010
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2011
- className: board_module_css_default.input,
2012
- rows: 4,
2013
- value: prompt,
2014
- placeholder: t("new.promptPlaceholder"),
2015
- onChange: (event) => {
2016
- setPrompt(event.target.value);
2017
- }
2018
- })]
2019
- }),
2020
- error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2021
- className: board_module_css_default.formError,
2022
- children: error
2023
- }),
2024
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
2025
- className: board_module_css_default.modalFooter,
2026
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2027
- type: "button",
2028
- className: board_module_css_default.ghostButton,
2029
- onClick: onClose,
2030
- children: t("new.cancel")
2031
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2032
- type: "submit",
2033
- className: board_module_css_default.primaryButton,
2034
- disabled: pending,
2035
- children: t("edit.save")
2036
- })]
2037
- })
2038
- ]
2443
+ onDescriptionChange: setDescription,
2444
+ onPromptChange: setPrompt
2039
2445
  })
2040
2446
  });
2041
2447
  }
@@ -2074,6 +2480,11 @@ window.__ModuleLoader__.load({
2074
2480
  execution.endedAt !== void 0 && ` · ${t("detail.executionEnded")} ${formatTime(execution.endedAt, timeZone)}`
2075
2481
  ]
2076
2482
  }),
2483
+ execution.initiatedBy !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2484
+ className: board_module_css_default.executionTimes,
2485
+ title: execution.initiatedBy,
2486
+ children: t("detail.execution.initiator", { session: execution.initiatedBy })
2487
+ }),
2077
2488
  execution.sessionId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2078
2489
  type: "button",
2079
2490
  className: board_module_css_default.linkButton,
@@ -2334,6 +2745,7 @@ window.__ModuleLoader__.load({
2334
2745
  const pending = snapshot.pendingTaskIds.includes(current.id);
2335
2746
  const transportError = snapshot.transportError;
2336
2747
  const timeZone = snapshot.host?.scheduler.timeZone;
2748
+ const permissionPending = requiresPermissionConfirmation(current, snapshot.host?.sessionDefaultPermission);
2337
2749
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2338
2750
  className: board_module_css_default.modalBackdrop,
2339
2751
  onMouseDown: (event) => {
@@ -2393,6 +2805,107 @@ window.__ModuleLoader__.load({
2393
2805
  children: current.description !== "" ? current.description : "—"
2394
2806
  })]
2395
2807
  }),
2808
+ current.freeze !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2809
+ className: board_module_css_default.detailSection,
2810
+ "data-dsh-part": "freeze",
2811
+ children: [
2812
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.freeze") }),
2813
+ current.freeze.redacted === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2814
+ className: board_module_css_default.formError,
2815
+ children: t("detail.freeze.redacted")
2816
+ }),
2817
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2818
+ className: board_module_css_default.detailText,
2819
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("detail.freeze.goal") })
2820
+ }),
2821
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
2822
+ className: board_module_css_default.promptBlock,
2823
+ children: current.freeze.goal
2824
+ }),
2825
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2826
+ className: board_module_css_default.detailText,
2827
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("detail.freeze.progress") })
2828
+ }),
2829
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
2830
+ className: board_module_css_default.promptBlock,
2831
+ children: current.freeze.progress
2832
+ }),
2833
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2834
+ className: board_module_css_default.detailText,
2835
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("detail.freeze.next") })
2836
+ }),
2837
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
2838
+ className: board_module_css_default.promptBlock,
2839
+ children: current.freeze.next
2840
+ }),
2841
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2842
+ className: board_module_css_default.detailMeta,
2843
+ children: t("detail.freeze.frozenAt", { time: formatHostTimestamp(current.freeze.frozenAt, timeZone) })
2844
+ }),
2845
+ current.freeze.frozenBy !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2846
+ className: board_module_css_default.detailMeta,
2847
+ children: t("detail.freeze.frozenBy", { session: current.freeze.frozenBy })
2848
+ })
2849
+ ]
2850
+ }),
2851
+ current.handover !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2852
+ className: board_module_css_default.detailSection,
2853
+ "data-dsh-part": "handover",
2854
+ children: [
2855
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.handover") }),
2856
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2857
+ className: board_module_css_default.detailText,
2858
+ children: [
2859
+ t("new.workspace"),
2860
+ ": ",
2861
+ current.handover.workspaceId ?? t("exec.workspace.recent"),
2862
+ " · ",
2863
+ t("new.mode"),
2864
+ ": ",
2865
+ current.handover.mode ?? t("exec.mode.default"),
2866
+ " · ",
2867
+ t("new.permission"),
2868
+ ": ",
2869
+ current.handover.permission === void 0 ? t("exec.permission.default") : t(`exec.permission.${current.handover.permission}`)
2870
+ ]
2871
+ }),
2872
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2873
+ className: board_module_css_default.detailText,
2874
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("detail.handover.references") })
2875
+ }),
2876
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
2877
+ className: board_module_css_default.executionList,
2878
+ children: current.handover.references.map((reference) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
2879
+ className: board_module_css_default.executionRow,
2880
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: reference })
2881
+ }, reference))
2882
+ }),
2883
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2884
+ className: board_module_css_default.detailMeta,
2885
+ children: t("detail.handover.bundledAt", { time: formatHostTimestamp(current.handover.bundledAt, timeZone) })
2886
+ })
2887
+ ]
2888
+ }),
2889
+ permissionPending && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2890
+ className: board_module_css_default.detailSection,
2891
+ "data-dsh-part": "permission-gate",
2892
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2893
+ className: board_module_css_default.formError,
2894
+ children: t("detail.permissionPending", { permission: t(`exec.permission.${current.handover?.permission ?? current.permission}`) })
2895
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2896
+ type: "button",
2897
+ className: board_module_css_default.primaryButton,
2898
+ disabled: pending,
2899
+ onClick: () => {
2900
+ controller.confirmPermission(current.id);
2901
+ },
2902
+ children: t("detail.permissionConfirm")
2903
+ })]
2904
+ }),
2905
+ current.permissionConfirmedAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2906
+ className: board_module_css_default.detailMeta,
2907
+ children: t("detail.permissionConfirmed", { time: formatHostTimestamp(current.permissionConfirmedAt, timeZone) })
2908
+ }),
2396
2909
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2397
2910
  className: board_module_css_default.detailSection,
2398
2911
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.prompt") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
@@ -2538,11 +3051,13 @@ window.__ModuleLoader__.load({
2538
3051
  * active. Cards open the task detail (never execute directly); the header
2539
3052
  * offers filter, new-task, and a back-to-chat escape.
2540
3053
  */
2541
- /** Case-insensitive title/description match. */
3054
+ /** Case-insensitive title/description/freeze-snapshot match. */
2542
3055
  function matchesFilter(task, filter) {
2543
3056
  if (filter.trim() === "") return true;
2544
3057
  const needle = filter.trim().toLowerCase();
2545
- return task.title.toLowerCase().includes(needle) || task.description.toLowerCase().includes(needle);
3058
+ const haystacks = [task.title, task.description];
3059
+ if (task.freeze !== void 0) haystacks.push(task.freeze.goal, task.freeze.progress, task.freeze.next);
3060
+ return haystacks.some((text) => text.toLowerCase().includes(needle));
2546
3061
  }
2547
3062
  /**
2548
3063
  * Memoized per-card adapter: with a stable `onOpen` from the board and an
@@ -2774,7 +3289,13 @@ window.__ModuleLoader__.load({
2774
3289
  let root;
2775
3290
  let container;
2776
3291
  const ensure = () => {
2777
- if (container !== void 0) return;
3292
+ if (container !== void 0) {
3293
+ if (container.isConnected) return;
3294
+ root?.unmount();
3295
+ root = void 0;
3296
+ container.remove();
3297
+ container = void 0;
3298
+ }
2778
3299
  const column = conversationColumn();
2779
3300
  if (column === void 0) return;
2780
3301
  container = document.createElement("div");
@@ -3370,6 +3891,19 @@ window.__ModuleLoader__.load({
3370
3891
  });
3371
3892
  }
3372
3893
  //#endregion
3894
+ //#region \0dsh-store-engine
3895
+ const platform = ["@deepseek-ai/dsh-client", "-store"].join("");
3896
+ const legacy = ["@deepseek-ai/dsh-client-runtime", "/client"].join("");
3897
+ let engine;
3898
+ try {
3899
+ engine = require(platform);
3900
+ } catch {
3901
+ engine = require(legacy);
3902
+ }
3903
+ const createSnapshotStore = engine.createSnapshotStore;
3904
+ engine.defineStore;
3905
+ engine.shallowEqual;
3906
+ //#endregion
3373
3907
  //#region src/client/settings-form.ts
3374
3908
  /** A boolean field, edited through true/false draft text. */
3375
3909
  function booleanField(field) {
@@ -3429,7 +3963,7 @@ window.__ModuleLoader__.load({
3429
3963
  }
3430
3964
  /** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
3431
3965
  bind(project) {
3432
- const store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(project());
3966
+ const store = createSnapshotStore(project());
3433
3967
  this.listeners.add(() => {
3434
3968
  store.set(project());
3435
3969
  });
@@ -3444,7 +3978,7 @@ window.__ModuleLoader__.load({
3444
3978
  exposed: snapshot.status === "ready",
3445
3979
  writable: snapshot.writable,
3446
3980
  dirty: plan.length > 0,
3447
- invalid: plan.some((item) => item.run === void 0),
3981
+ invalid: plan.some((item) => item.judge === void 0),
3448
3982
  saving: this.saving,
3449
3983
  failed: this.failed,
3450
3984
  ...this.failedReason === void 0 ? {} : { failedReason: this.failedReason }
@@ -3494,45 +4028,52 @@ window.__ModuleLoader__.load({
3494
4028
  };
3495
4029
  }
3496
4030
  /**
3497
- * Write every staged edit, then re-seed from what the Host accepted.
4031
+ * Write every staged edit in one atomic scope mutation, then re-seed from
4032
+ * what the Host accepted.
3498
4033
  *
3499
- * When the scope carries the optional batch surface (the dsh-web
3500
- * bridge scope), every planned write rides one mutation so cross-field
3501
- * validate hooks (baseURL+model) judge the batch as a unit instead of
3502
- * deadlocking on per-field writes. Otherwise the per-field loop runs.
3503
- * A field lands only when the Host reports it held the staged value; a
3504
- * landed field's draft is dropped, a failed one stays staged for the user.
3505
- * @returns settlement after every write and the read-back.
4034
+ * The whole batch rides one mutate, so cross-field validate hooks
4035
+ * (baseURL+model) judge it as a unit: the Host either applies every write
4036
+ * or refuses the batch. The 0.1.2 scope contract never rejects a refused
4037
+ * mutation — the scope recovers with a fresh Host view and resolves — so
4038
+ * resolution alone proves nothing: the outcome is judged by reading the
4039
+ * settled snapshot back, one planned write at a time, and one missed write
4040
+ * fails the whole save. A scope that still rejects on refusal (the dsh-web
4041
+ * bridge scope) reports through the same failure path with its rejection
4042
+ * message. A save that did not land keeps its drafts, so the user can
4043
+ * correct them instead of retyping.
4044
+ * @returns settlement after the mutation and the read-back.
3506
4045
  */
3507
4046
  async save() {
3508
4047
  const plan = this.plan();
3509
- const valid = plan.filter((item) => item.run !== void 0);
4048
+ const valid = plan.filter((item) => item.judge !== void 0);
3510
4049
  if (plan.length === 0 || this.saving || valid.length !== plan.length) return;
3511
- const plannedWrites = valid.map((item) => item.op);
3512
4050
  const pending = /* @__PURE__ */ new Map();
3513
4051
  for (const item of plan) pending.set(item.field, this.staged.get(item.field));
3514
4052
  this.saving = true;
3515
4053
  this.failed = false;
3516
4054
  this.failedReason = void 0;
3517
4055
  this.publish();
3518
- const landed = /* @__PURE__ */ new Set();
3519
- const batch = this.batchedScope();
3520
- if (batch !== void 0) {
3521
- const result = await batch.mutate(plannedWrites);
3522
- if (result.ok) {
3523
- for (const field of result.fields) if (field.landed) landed.add(field.field);
3524
- } else this.failedReason = result.message;
3525
- } else for (const item of valid) if (await item.run()) landed.add(item.field);
3526
- for (const [field, before] of pending) if (landed.has(field) && this.staged.get(field) === before) this.staged.delete(field);
4056
+ const ops = valid.map((item) => item.op.op === "set" ? {
4057
+ op: "set",
4058
+ path: [item.field],
4059
+ value: item.op.value
4060
+ } : {
4061
+ op: "unset",
4062
+ path: [item.field]
4063
+ });
4064
+ let failedReason;
4065
+ try {
4066
+ await this.scope.mutate(ops);
4067
+ } catch (error) {
4068
+ failedReason = error instanceof Error ? error.message : String(error);
4069
+ }
4070
+ const landed = failedReason === void 0 && valid.every((item) => item.judge());
4071
+ for (const [field, before] of pending) if (landed && this.staged.get(field) === before) this.staged.delete(field);
3527
4072
  this.saving = false;
3528
- this.failed = landed.size !== pending.size;
4073
+ this.failed = !landed;
4074
+ this.failedReason = failedReason;
3529
4075
  this.publish();
3530
4076
  }
3531
- /** The scope's batch surface when it supports one; undefined conservatively otherwise. */
3532
- batchedScope() {
3533
- const candidate = this.scope;
3534
- return typeof candidate?.mutate === "function" ? candidate : void 0;
3535
- }
3536
4077
  /**
3537
4078
  * Every staged edit a save would write. An entry whose draft is not a value
3538
4079
  * its field accepts carries no write: the form is still dirty, and the save
@@ -3551,7 +4092,7 @@ window.__ModuleLoader__.load({
3551
4092
  field,
3552
4093
  op: "unset"
3553
4094
  },
3554
- run: () => this.clear(field)
4095
+ judge: () => this.landedUnset(field)
3555
4096
  });
3556
4097
  continue;
3557
4098
  }
@@ -3563,7 +4104,7 @@ window.__ModuleLoader__.load({
3563
4104
  field,
3564
4105
  op: "unset"
3565
4106
  },
3566
- run: void 0
4107
+ judge: void 0
3567
4108
  });
3568
4109
  else if (write.kind === "clear") plan.push({
3569
4110
  field,
@@ -3571,7 +4112,7 @@ window.__ModuleLoader__.load({
3571
4112
  field,
3572
4113
  op: "unset"
3573
4114
  },
3574
- run: () => this.clear(field)
4115
+ judge: () => this.landedUnset(field)
3575
4116
  });
3576
4117
  else plan.push({
3577
4118
  field,
@@ -3580,20 +4121,26 @@ window.__ModuleLoader__.load({
3580
4121
  op: "set",
3581
4122
  value: write.value
3582
4123
  },
3583
- run: () => this.store(field, write.value)
4124
+ judge: () => this.landedSet(field, write.value)
3584
4125
  });
3585
4126
  }
3586
4127
  return plan;
3587
4128
  }
3588
- async clear(field) {
3589
- await this.scope.unset(field);
3590
- return !this.stored(field);
3591
- }
3592
- async store(field, value) {
3593
- await this.scope.set(field, value);
4129
+ /**
4130
+ * Read-back judgment for a planned set: the user layer must hold the
4131
+ * intended value once the mutation has settled.
4132
+ */
4133
+ landedSet(field, value) {
3594
4134
  if (this.specOf(field).secret) return true;
3595
4135
  return this.userLayer()?.[field] === value;
3596
4136
  }
4137
+ /**
4138
+ * Read-back judgment for a planned unset: the field must be gone from the
4139
+ * user layer once the mutation has settled.
4140
+ */
4141
+ landedUnset(field) {
4142
+ return !this.stored(field);
4143
+ }
3597
4144
  stage(field, edit) {
3598
4145
  this.staged.set(field, edit);
3599
4146
  this.failed = false;
@@ -3813,13 +4360,14 @@ window.__ModuleLoader__.load({
3813
4360
  async state() {
3814
4361
  return await this.request(`${TASK_BOARD_API_PREFIX}/state`, { cache: "no-store" });
3815
4362
  }
3816
- async action(action) {
3817
- return await this.post(uuid(), action);
4363
+ async action(action, initiator) {
4364
+ return await this.post(uuid(), action, initiator);
3818
4365
  }
3819
- async post(requestId, action) {
4366
+ async post(requestId, action, initiator) {
3820
4367
  const envelope = {
3821
4368
  requestId,
3822
- action
4369
+ action,
4370
+ ...initiator === void 0 || initiator === "" ? {} : { initiator }
3823
4371
  };
3824
4372
  return await this.request(`${TASK_BOARD_API_PREFIX}/action`, {
3825
4373
  method: "POST",
@@ -3873,7 +4421,7 @@ window.__ModuleLoader__.load({
3873
4421
  /** The building package's version, when the bundle carries it. */
3874
4422
  function bakedVersion() {
3875
4423
  try {
3876
- return "0.3.5";
4424
+ return "0.3.9";
3877
4425
  } catch {
3878
4426
  return;
3879
4427
  }
@@ -3941,7 +4489,14 @@ window.__ModuleLoader__.load({
3941
4489
  const NS = "task-board";
3942
4490
  /** Settings namespace the settings card edits (the Host plugin registers it). */
3943
4491
  const TASK_BOARD_NS = "task-board";
3944
- /** Required services (fiber inject waiting — the runtime must be up first). */
4492
+ /**
4493
+ * Required services (fiber inject waiting — the runtime must be up first).
4494
+ * The generated remote faces are probed at use time instead of injected:
4495
+ * `remote.agentPresets` only registers on 0.1.2-alpha.2 hosts (the
4496
+ * api-remotes contribution), so a hard wait would pend the entry forever
4497
+ * on hosts below that cohort, which serve the same roster through the
4498
+ * connection RPC face.
4499
+ */
3945
4500
  const inject = [
3946
4501
  "slots",
3947
4502
  "sessions",
@@ -3952,6 +4507,44 @@ window.__ModuleLoader__.load({
3952
4507
  "remote"
3953
4508
  ];
3954
4509
  /**
4510
+ * Read the agent-preset roster through whichever face the running host
4511
+ * serves: the generated api-remotes face (`remote.agentPresets`,
4512
+ * 0.1.2-alpha.2) or the connection RPC face
4513
+ * (`connection.api.agentPresets`, hosts below that cohort). Answers
4514
+ * undefined when the host serves neither, so the caller leaves the picker
4515
+ * options untouched instead of erroring.
4516
+ */
4517
+ async function readPresetRoster(ctx, remote) {
4518
+ let remotes;
4519
+ try {
4520
+ remotes = remote.agentPresets;
4521
+ } catch {
4522
+ remotes = void 0;
4523
+ }
4524
+ if (remotes !== void 0) {
4525
+ const response = await remotes.list();
4526
+ if (!response.ok) return {
4527
+ ok: false,
4528
+ presets: []
4529
+ };
4530
+ return {
4531
+ ok: true,
4532
+ presets: response.value.presets
4533
+ };
4534
+ }
4535
+ const legacy = ctx.get("connection").api?.agentPresets;
4536
+ if (legacy === void 0) return void 0;
4537
+ const response = await legacy.list({});
4538
+ if (!response.result.ok || response.result.value === void 0) return {
4539
+ ok: false,
4540
+ presets: []
4541
+ };
4542
+ return {
4543
+ ok: true,
4544
+ presets: response.result.value.presets ?? []
4545
+ };
4546
+ }
4547
+ /**
3955
4548
  * Mount the task board.
3956
4549
  * @param ctx - client root context (services: sessions, workspaces).
3957
4550
  */
@@ -3993,7 +4586,7 @@ window.__ModuleLoader__.load({
3993
4586
  if (uiDisposer !== void 0) return;
3994
4587
  const sessions = ctx.get("sessions");
3995
4588
  const workspaces = ctx.get("workspaces");
3996
- const connection = ctx.get("connection");
4589
+ const remote = ctx.get("remote");
3997
4590
  const controller = new BoardController({
3998
4591
  store: new LocalStorageTaskStore(),
3999
4592
  transport: new HttpTaskBoardHostTransport(),
@@ -4015,9 +4608,9 @@ window.__ModuleLoader__.load({
4015
4608
  disposers.push(workspaces.list.subscribe(pushWorkspaceOptions));
4016
4609
  const pushPresetOptions = async () => {
4017
4610
  try {
4018
- const response = await connection.api.agentPresets.list({});
4019
- if (!response.result.ok) return;
4020
- controller.setExecutionOptions({ presets: response.result.value.presets.map((preset) => ({
4611
+ const roster = await readPresetRoster(ctx, remote);
4612
+ if (roster === void 0 || !roster.ok) return;
4613
+ controller.setExecutionOptions({ presets: roster.presets.map((preset) => ({
4021
4614
  id: preset.id,
4022
4615
  name: preset.name,
4023
4616
  description: preset.description,