@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.
- package/README.i18n.yaml +2 -2
- package/README.md +9 -4
- package/README.zh.md +9 -4
- package/lib/client.js +742 -75
- package/lib/client.js.map +1 -1
- package/lib/index.js +698 -190
- package/lib/types/client/TaskBoardSettingsCard.d.ts +2 -1
- package/lib/types/client/board/TaskBoard.d.ts +3 -0
- package/lib/types/client/board-mount.d.ts +4 -1
- package/lib/types/client/host-api.d.ts +2 -2
- package/lib/types/client/index.d.ts +10 -2
- package/lib/types/client/locales.d.ts +21 -0
- package/lib/types/client/settings-form.d.ts +35 -43
- package/lib/types/client/sidebar-entry-core.d.ts +10 -0
- package/lib/types/client/sidebar-entry.d.ts +7 -1
- package/lib/types/core/controller.d.ts +11 -2
- package/lib/types/core/freeze-snapshot.d.ts +71 -0
- package/lib/types/core/handover.d.ts +58 -0
- package/lib/types/core/tasks.d.ts +70 -1
- package/lib/types/core/use-cases/task-update.d.ts +16 -7
- package/lib/types/host-ledger.d.ts +17 -2
- package/lib/types/host-runner.d.ts +43 -23
- package/lib/types/host-service.d.ts +8 -5
- package/lib/types/index.d.ts +9 -1
- package/lib/types/protocol.d.ts +15 -2
- package/package.json +26 -17
- package/src/client/TaskBoardSettingsCard.tsx +2 -1
- package/src/client/board/NewTaskModal.tsx +52 -0
- package/src/client/board/TaskBoard.tsx +5 -3
- package/src/client/board/TaskCard.tsx +3 -0
- package/src/client/board/TaskDetail.tsx +54 -0
- package/src/client/board-mount.tsx +11 -1
- package/src/client/host-api.ts +5 -5
- package/src/client/index.ts +76 -10
- package/src/client/locales.ts +55 -2
- package/src/client/settings-form.ts +78 -92
- package/src/client/sidebar-entry-core.ts +33 -7
- package/src/client/sidebar-entry.ts +7 -1
- package/src/core/controller.ts +35 -10
- package/src/core/freeze-snapshot.ts +213 -0
- package/src/core/handover.ts +117 -0
- package/src/core/store.ts +48 -1
- package/src/core/tasks.ts +87 -0
- package/src/core/use-cases/task-update.ts +30 -10
- package/src/dsh-home.ts +1 -1
- package/src/host-ledger.ts +137 -59
- package/src/host-routes.ts +1 -1
- package/src/host-runner.ts +211 -97
- package/src/host-service.ts +14 -10
- package/src/index.ts +24 -10
- 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
|
|
459
|
-
*
|
|
460
|
-
*
|
|
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
|
-
...
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
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
|
|
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 =
|
|
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.
|
|
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
|
|
4073
|
+
* Write every staged edit in one atomic scope mutation, then re-seed from
|
|
4074
|
+
* what the Host accepted.
|
|
3469
4075
|
*
|
|
3470
|
-
*
|
|
3471
|
-
*
|
|
3472
|
-
*
|
|
3473
|
-
*
|
|
3474
|
-
*
|
|
3475
|
-
*
|
|
3476
|
-
*
|
|
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.
|
|
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
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
}
|
|
3497
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4166
|
+
judge: () => this.landedSet(field, write.value)
|
|
3555
4167
|
});
|
|
3556
4168
|
}
|
|
3557
4169
|
return plan;
|
|
3558
4170
|
}
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
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.
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
3990
|
-
if (!
|
|
3991
|
-
controller.setExecutionOptions({ presets:
|
|
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
|
}
|