@william2000/dsh-nova-ui-task-board 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -7,1572 +7,1813 @@ 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
- //#region src/client/board-controller.ts
11
- /** 当前选中的任务(按 selectedTaskId 从账本解析),无则 undefined。 */
12
- function selectedTaskOf(snapshot) {
13
- if (snapshot.selectedTaskId === void 0) return void 0;
14
- return snapshot.tasks.find((task) => task.id === snapshot.selectedTaskId);
10
+ //#region src/core/model.ts
11
+ /** 任务生命周期状态,与看板列一一对应(§10.1);`proposed` 为对话流转候选(D7/P3.1)。 */
12
+ const TASK_STATUSES = [
13
+ "proposed",
14
+ "backlog",
15
+ "todo",
16
+ "running",
17
+ "done",
18
+ "failed"
19
+ ];
20
+ /** 任务来源(§9.2,v1.0 字段,P3 随对话流转启用)。 */
21
+ const TASK_SOURCES = [
22
+ "manual",
23
+ "conversation",
24
+ "github_issue",
25
+ "bookmark_collector",
26
+ "feishu",
27
+ "requirement",
28
+ "other"
29
+ ];
30
+ /** 执行会话钉住的权限预设 id(`/permission <id>`,§9.2)。 */
31
+ const TASK_PERMISSIONS = [
32
+ "read-only",
33
+ "workspace-write",
34
+ "danger-full-access"
35
+ ];
36
+ /** 执行结果(§9.3)。 */
37
+ const EXECUTION_RESULTS = [
38
+ "succeeded",
39
+ "failed",
40
+ "cancelled"
41
+ ];
42
+ /** 评论类型(§9.5 comments:user_feedback/ai_log/system_event)。 */
43
+ const COMMENT_TYPES = [
44
+ "user_feedback",
45
+ "ai_log",
46
+ "system_event"
47
+ ];
48
+ /** 产物类型(§9.5 artifacts;会话 transcript 即默认产物 `session`)。 */
49
+ const ARTIFACT_TYPES = [
50
+ "session",
51
+ "link",
52
+ "file",
53
+ "other"
54
+ ];
55
+ /** 来源会话引用写入 metadata 的键(§9.2/§14.4:可跳回对话)。 */
56
+ const SOURCE_CONVERSATION_META_KEY = "sourceConversationId";
57
+ /** 有限数字守卫。 */
58
+ function isFiniteNumber(value) {
59
+ return typeof value === "number" && Number.isFinite(value);
15
60
  }
16
- /** 未确认候选任务数(T011 §12.7 入口角标:proposed 且未归档)。 */
17
- function proposedCountOf(snapshot) {
18
- let count = 0;
19
- for (const task of snapshot.tasks) if (task.status === "proposed" && task.archivedAt === void 0) count += 1;
20
- return count;
61
+ /** 非空字符串(trim 后)守卫,空串/空白清除钉住字段(对齐参考实现 normalizeTargetId)。 */
62
+ function normalizeOptionalString(value) {
63
+ if (typeof value !== "string") return void 0;
64
+ const trimmed = value.trim();
65
+ return trimmed === "" ? void 0 : trimmed;
21
66
  }
22
- function randomUuid() {
23
- const randomUUID = globalThis.crypto?.randomUUID;
24
- if (randomUUID !== void 0) return randomUUID.call(globalThis.crypto);
25
- return `t-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
67
+ /** 是否为已知任务状态。 */
68
+ function isTaskStatus(value) {
69
+ return typeof value === "string" && TASK_STATUSES.includes(value);
26
70
  }
27
- function messageOf(error) {
28
- return error instanceof Error ? error.message : String(error);
71
+ /** 是否为已知任务来源。 */
72
+ function isTaskSource(value) {
73
+ return typeof value === "string" && TASK_SOURCES.includes(value);
74
+ }
75
+ /** 是否为已知权限预设。 */
76
+ function isTaskPermission(value) {
77
+ return typeof value === "string" && TASK_PERMISSIONS.includes(value);
78
+ }
79
+ /** 是否为已知执行结果。 */
80
+ function isExecutionResult(value) {
81
+ return typeof value === "string" && EXECUTION_RESULTS.includes(value);
82
+ }
83
+ /** 是否为已知评论类型。 */
84
+ function isCommentType(value) {
85
+ return typeof value === "string" && COMMENT_TYPES.includes(value);
86
+ }
87
+ /** 是否为已知产物类型。 */
88
+ function isArtifactType(value) {
89
+ return typeof value === "string" && ARTIFACT_TYPES.includes(value);
29
90
  }
30
91
  /**
31
- * 身份保持合并:同一账本代数且非倒退 revision 下,JSON 未变化的任务行保留
32
- * 原对象引用——兄弟卡片状态变化不会全量替换任务对象,TaskCard memo 边界
33
- * 生效(§12.2 卡片按任务粒度 memo 化)。代数变化/倒退则整体替换。
92
+ * cron 表达式的基础形状校验(§9.4:5 cron「分 时 日 月 周」)。
93
+ * 这是结构级校验:段数、字符集(数字、`* , - / ?` 及名字字母)。cron 的到期
94
+ * 计算与完整合法性由 T006 的调度器实现;本层只保证持久化的规则形状可解析。
34
95
  */
35
- function mergeTasksIdentityPreserving(prev, next, preserve) {
36
- if (!preserve) return [...next];
37
- const prevById = /* @__PURE__ */ new Map();
38
- for (const task of prev) prevById.set(task.id, task);
39
- return next.map((task) => {
40
- const old = prevById.get(task.id);
41
- return old !== void 0 && JSON.stringify(old) === JSON.stringify(task) ? old : task;
42
- });
96
+ const CRON_FIELD_RE = /^[0-9A-Za-z*,\-\/?]+$/;
97
+ function isPlausibleCron(cron) {
98
+ if (typeof cron !== "string" || cron.trim() === "") return false;
99
+ const fields = cron.trim().split(/\s+/);
100
+ if (fields.length !== 5) return false;
101
+ return fields.every((field) => CRON_FIELD_RE.test(field));
43
102
  }
44
103
  /**
45
- * 看板控制器(见模块注释)。所有变更 bump 快照并通知订阅者;UI 与 DOM 挂载
46
- * 通过订阅重渲染。生产路径恒为 Host 背书(transport 必传)。
104
+ * 归一化一条持久化的执行安排(§9.4 判别联合):
105
+ * - `kind: 'one-shot'` → one-shot 分支;`runAt` 非有限数则整条丢弃;
106
+ * - `kind: 'cron'` 或缺省 kind(v1/v2 旧形状)→ cron 分支;cron 形状非法则整条
107
+ * 丢弃(「修复或丢弃 schedule、绝不丢整行」——坏 schedule 不拖垮任务行);
108
+ * - 其他 kind → 丢弃。
47
109
  */
48
- var BoardController = class {
49
- deps;
50
- tasks = [];
51
- boardOpen = false;
52
- archiveView = false;
53
- groupMode = "manual";
54
- selectedTaskId;
55
- executionOptions = {
56
- workspaces: [],
57
- presets: []
58
- };
59
- listeners = /* @__PURE__ */ new Set();
60
- disposers = [];
61
- now;
62
- uuid;
63
- pendingTaskIds = /* @__PURE__ */ new Set();
64
- taskQueues = /* @__PURE__ */ new Map();
65
- transportError;
66
- hostState;
67
- remoteSubscribed = false;
68
- remoteInitialization;
69
- started = false;
70
- lastCurrent;
71
- /** @param deps - 传输、会话导航面、时钟与 id 铸造。 */
72
- constructor(deps) {
73
- this.deps = deps;
74
- this.now = deps.now ?? (() => Date.now());
75
- this.uuid = deps.uuid ?? randomUuid;
76
- }
77
- /** 启动:初始化 Host 同步 + 订阅 SSE + 会话导航。幂等。 */
78
- start() {
79
- if (this.started) return;
80
- this.started = true;
81
- this.initializeRemote();
82
- this.disposers.push(this.deps.sessions.list.subscribe(() => {
83
- this.onSessionsChanged();
84
- }));
85
- this.notify();
86
- }
87
- /** 停止全部订阅并释放状态(幂等)。 */
88
- dispose() {
89
- for (const dispose of this.disposers.splice(0)) dispose();
90
- this.listeners.clear();
91
- }
92
- getSnapshot() {
93
- return {
94
- tasks: this.tasks,
95
- boardOpen: this.boardOpen,
96
- archiveView: this.archiveView,
97
- groupMode: this.groupMode,
98
- ...this.selectedTaskId === void 0 ? {} : { selectedTaskId: this.selectedTaskId },
99
- executionOptions: this.executionOptions,
100
- pendingTaskIds: [...this.pendingTaskIds],
101
- executionCapable: true,
102
- ...this.transportError === void 0 ? {} : { transportError: this.transportError },
103
- ...this.hostState === void 0 ? {} : { host: this.hostState }
104
- };
105
- }
106
- subscribe(listener) {
107
- this.listeners.add(listener);
108
- return () => {
109
- this.listeners.delete(listener);
110
+ function normalizeSchedule(value) {
111
+ if (typeof value !== "object" || value === null) return void 0;
112
+ const rule = value;
113
+ if (rule.kind === "one-shot") {
114
+ if (!isFiniteNumber(rule.runAt)) return void 0;
115
+ const schedule = {
116
+ kind: "one-shot",
117
+ runAt: rule.runAt
110
118
  };
119
+ if (isFiniteNumber(rule.firedAt)) schedule.firedAt = rule.firedAt;
120
+ return schedule;
111
121
  }
112
- /** 生产变更是否由 Host 传输确认(T004 true;保留语义供测试/回退判断)。 */
113
- isHostBacked() {
114
- return this.deps.transport !== void 0;
122
+ if (rule.kind !== void 0 && rule.kind !== "cron") return void 0;
123
+ if (!isPlausibleCron(rule.cron)) return void 0;
124
+ const schedule = {
125
+ kind: "cron",
126
+ enabled: rule.enabled === true,
127
+ cron: rule.cron
128
+ };
129
+ if (isFiniteNumber(rule.nextRunAt)) schedule.nextRunAt = rule.nextRunAt;
130
+ if (isFiniteNumber(rule.lastTriggeredAt)) schedule.lastTriggeredAt = rule.lastTriggeredAt;
131
+ return schedule;
132
+ }
133
+ /** 归一化一条执行记录(§9.3);结构非法返回 undefined。 */
134
+ function normalizeExecution(value) {
135
+ if (typeof value !== "object" || value === null) return void 0;
136
+ const entry = value;
137
+ if (typeof entry.id !== "string" || entry.id === "") return void 0;
138
+ if (!isFiniteNumber(entry.startedAt)) return void 0;
139
+ if (entry.sessionId !== void 0 && typeof entry.sessionId !== "string") return void 0;
140
+ if (entry.endedAt !== void 0 && !isFiniteNumber(entry.endedAt)) return void 0;
141
+ if (entry.result !== void 0 && !isExecutionResult(entry.result)) return void 0;
142
+ if (entry.error !== void 0 && typeof entry.error !== "string") return void 0;
143
+ const execution = {
144
+ id: entry.id,
145
+ startedAt: entry.startedAt
146
+ };
147
+ if (typeof entry.sessionId === "string") execution.sessionId = entry.sessionId;
148
+ if (isFiniteNumber(entry.endedAt)) execution.endedAt = entry.endedAt;
149
+ if (isExecutionResult(entry.result)) execution.result = entry.result;
150
+ if (typeof entry.error === "string") execution.error = entry.error;
151
+ return execution;
152
+ }
153
+ /**
154
+ * 归一化标签数组(§9.2 默认 []):仅保留字符串元素,逐项 trim、丢弃空白项、
155
+ * 按首次出现顺序去重;缺省/非法 → []。标签是展示/过滤维度的原始字符串,
156
+ * 大小写敏感(`API` 与 `api` 是两个标签),去重不折叠大小写。
157
+ */
158
+ function normalizeTags(value) {
159
+ if (!Array.isArray(value)) return [];
160
+ const seen = /* @__PURE__ */ new Set();
161
+ const tags = [];
162
+ for (const tag of value) {
163
+ if (typeof tag !== "string") continue;
164
+ const trimmed = tag.trim();
165
+ if (trimmed === "" || seen.has(trimmed)) continue;
166
+ seen.add(trimmed);
167
+ tags.push(trimmed);
115
168
  }
116
- /** 显式重试与 Host 的首次/全量同步(错误条「重试」按钮)。 */
117
- async retryHostSync() {
118
- return await this.initializeRemote();
169
+ return tags;
170
+ }
171
+ /** 通用字符串数组归一化(trim、丢空白、首现去重、条目长度封顶、数量封顶)。 */
172
+ function normalizeStringList(value, maxItems, maxLength) {
173
+ if (!Array.isArray(value)) return [];
174
+ const seen = /* @__PURE__ */ new Set();
175
+ const items = [];
176
+ for (const item of value) {
177
+ if (typeof item !== "string") continue;
178
+ const trimmed = item.trim().slice(0, maxLength);
179
+ if (trimmed === "" || seen.has(trimmed)) continue;
180
+ seen.add(trimmed);
181
+ items.push(trimmed);
182
+ if (items.length >= maxItems) break;
119
183
  }
120
- openBoard() {
121
- if (this.boardOpen) return;
122
- this.lastCurrent = this.deps.sessions.list.getSnapshot().current;
123
- this.boardOpen = true;
124
- this.notify();
184
+ return items;
185
+ }
186
+ /** 归一化来源元数据(§14.1/14.2):仅保留字符串键值对;缺省/非法 undefined。 */
187
+ function normalizeMetadata(value) {
188
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
189
+ const metadata = {};
190
+ for (const [key, entry] of Object.entries(value)) {
191
+ if (typeof entry !== "string" || key.trim() === "") continue;
192
+ metadata[key.trim().slice(0, 64)] = entry.slice(0, 2048);
125
193
  }
126
- closeBoard() {
127
- if (!this.boardOpen) return;
128
- this.boardOpen = false;
129
- this.notify();
194
+ return Object.keys(metadata).length > 0 ? metadata : void 0;
195
+ }
196
+ /** 归一化一条评论(§9.5);结构非法返回 undefined。 */
197
+ function normalizeComment(value) {
198
+ if (typeof value !== "object" || value === null) return void 0;
199
+ const entry = value;
200
+ if (typeof entry.id !== "string" || entry.id === "") return void 0;
201
+ if (typeof entry.body !== "string" || entry.body === "") return void 0;
202
+ if (!isFiniteNumber(entry.createdAt)) return void 0;
203
+ if (entry.author !== void 0 && typeof entry.author !== "string") return void 0;
204
+ if (entry.type !== void 0 && !isCommentType(entry.type)) return void 0;
205
+ return {
206
+ id: entry.id,
207
+ author: typeof entry.author === "string" && entry.author.trim() !== "" ? entry.author.slice(0, 64) : "user",
208
+ body: entry.body.slice(0, 4e3),
209
+ type: isCommentType(entry.type) ? entry.type : "user_feedback",
210
+ createdAt: entry.createdAt
211
+ };
212
+ }
213
+ /** 归一化评论集合(§9.5 comments,默认 []);非法条目丢弃。 */
214
+ function normalizeComments(value) {
215
+ if (!Array.isArray(value)) return [];
216
+ const comments = [];
217
+ for (const entry of value) {
218
+ const comment = normalizeComment(entry);
219
+ if (comment !== void 0) comments.push(comment);
130
220
  }
131
- toggleBoard() {
132
- if (this.boardOpen) this.closeBoard();
133
- else this.openBoard();
221
+ return comments;
222
+ }
223
+ /** 归一化一条产物(§9.5 artifacts);结构非法返回 undefined。 */
224
+ function normalizeArtifact(value) {
225
+ if (typeof value !== "object" || value === null) return void 0;
226
+ const entry = value;
227
+ if (typeof entry.id !== "string" || entry.id === "") return void 0;
228
+ if (!isArtifactType(entry.type)) return void 0;
229
+ if (typeof entry.title !== "string" || entry.title === "") return void 0;
230
+ if (!isFiniteNumber(entry.createdAt)) return void 0;
231
+ if (entry.url !== void 0 && typeof entry.url !== "string") return void 0;
232
+ if (entry.contentRef !== void 0 && typeof entry.contentRef !== "string") return void 0;
233
+ const artifact = {
234
+ id: entry.id,
235
+ type: entry.type,
236
+ title: entry.title.slice(0, 500),
237
+ createdAt: entry.createdAt
238
+ };
239
+ if (typeof entry.url === "string" && entry.url.trim() !== "") artifact.url = entry.url.slice(0, 2048);
240
+ if (typeof entry.contentRef === "string" && entry.contentRef.trim() !== "") artifact.contentRef = entry.contentRef.slice(0, 512);
241
+ return artifact;
242
+ }
243
+ /** 归一化产物集合(§9.5 artifacts,默认 []);非法条目丢弃。 */
244
+ function normalizeArtifacts(value) {
245
+ if (!Array.isArray(value)) return [];
246
+ const artifacts = [];
247
+ for (const entry of value) {
248
+ const artifact = normalizeArtifact(entry);
249
+ if (artifact !== void 0) artifacts.push(artifact);
134
250
  }
135
- /**
136
- * 切换主看板/归档视图。离开归档视图时若选中任务仍是归档任务则清除选中
137
- * ——详情不得悬停在已离开看板的任务上。
138
- */
139
- toggleArchiveView() {
140
- this.archiveView = !this.archiveView;
141
- if (!this.archiveView && this.selectedTaskId !== void 0) {
142
- if (this.tasks.find((task) => task.id === this.selectedTaskId)?.archivedAt !== void 0) this.selectedTaskId = void 0;
143
- }
144
- this.notify();
251
+ return artifacts;
252
+ }
253
+ /** 归一化上下文快照(§9.5 context_snapshot);结构非法返回 undefined。 */
254
+ function normalizeContextSnapshot(value) {
255
+ if (typeof value !== "object" || value === null) return void 0;
256
+ const entry = value;
257
+ if (!isFiniteNumber(entry.updatedAt)) return void 0;
258
+ if (entry.goal !== void 0 && typeof entry.goal !== "string") return void 0;
259
+ if (entry.lastAiSummary !== void 0 && typeof entry.lastAiSummary !== "string") return void 0;
260
+ if (entry.latestUserFeedback !== void 0 && typeof entry.latestUserFeedback !== "string") return void 0;
261
+ const snapshot = {
262
+ keyDecisions: normalizeStringList(entry.keyDecisions, 50, 500),
263
+ filePaths: normalizeStringList(entry.filePaths, 100, 500),
264
+ relatedLinks: normalizeStringList(entry.relatedLinks, 20, 2048),
265
+ updatedAt: entry.updatedAt
266
+ };
267
+ if (typeof entry.goal === "string" && entry.goal.trim() !== "") snapshot.goal = entry.goal.trim().slice(0, 2e3);
268
+ if (typeof entry.lastAiSummary === "string" && entry.lastAiSummary.trim() !== "") snapshot.lastAiSummary = entry.lastAiSummary.trim().slice(0, 8e3);
269
+ if (typeof entry.latestUserFeedback === "string" && entry.latestUserFeedback.trim() !== "") snapshot.latestUserFeedback = entry.latestUserFeedback.trim().slice(0, 4e3);
270
+ return snapshot;
271
+ }
272
+ /**
273
+ * 归一化一条任务行(§9.2)。
274
+ *
275
+ * 结构非法(id/title/description/prompt/createdAt/updatedAt/executions 任一不
276
+ * 符合)→ 返回 undefined(整行丢弃,HostLedger 记入 scheduler.error);
277
+ * 语义非法 → 就地修复:
278
+ * - 未知状态 → `todo`(未来版本的未知状态落入待办而非丢行,对齐参考实现);
279
+ * - 未知 source/permission → undefined;空白 workspaceId/mode/project/parentId
280
+ * → undefined;archivedAt 非有限数 → undefined;
281
+ * - tags 非字符串数组 → [];order 非有限数 → 0(T009 重算列内唯一);
282
+ * - schedule 交给 normalizeSchedule(修复或丢弃,不丢行)。
283
+ */
284
+ function normalizeTask(value) {
285
+ if (typeof value !== "object" || value === null) return void 0;
286
+ const record = value;
287
+ if (typeof record.id !== "string" || record.id === "") return void 0;
288
+ if (typeof record.title !== "string") return void 0;
289
+ if (typeof record.description !== "string") return void 0;
290
+ if (typeof record.prompt !== "string") return void 0;
291
+ if (!isFiniteNumber(record.createdAt) || !isFiniteNumber(record.updatedAt)) return void 0;
292
+ if (!Array.isArray(record.executions)) return void 0;
293
+ const executions = [];
294
+ for (const entry of record.executions) {
295
+ const execution = normalizeExecution(entry);
296
+ if (execution === void 0) return void 0;
297
+ executions.push(execution);
145
298
  }
146
- /**
147
- * 切换列内排序视图模式(T009 §12.10):手动顺序/按项目分组/按标签分组。
148
- * 分组是视图层能力,不改底层 order 与归属数据;模式仅会话级记忆(刷新重置)。
149
- */
150
- setGroupMode(mode) {
151
- if (this.groupMode === mode) return;
152
- this.groupMode = mode;
153
- this.notify();
299
+ const task = {
300
+ id: record.id,
301
+ title: record.title,
302
+ description: record.description,
303
+ prompt: record.prompt,
304
+ status: isTaskStatus(record.status) ? record.status : "todo",
305
+ createdAt: record.createdAt,
306
+ updatedAt: record.updatedAt,
307
+ executions,
308
+ tags: normalizeTags(record.tags),
309
+ comments: normalizeComments(record.comments),
310
+ artifacts: normalizeArtifacts(record.artifacts),
311
+ order: isFiniteNumber(record.order) ? record.order : 0
312
+ };
313
+ if (isTaskSource(record.source)) task.source = record.source;
314
+ const metadata = normalizeMetadata(record.metadata);
315
+ if (metadata !== void 0) task.metadata = metadata;
316
+ const contextSnapshot = normalizeContextSnapshot(record.contextSnapshot);
317
+ if (contextSnapshot !== void 0) task.contextSnapshot = contextSnapshot;
318
+ const schedule = normalizeSchedule(record.schedule);
319
+ if (schedule !== void 0) task.schedule = schedule;
320
+ const workspaceId = normalizeOptionalString(record.workspaceId);
321
+ if (workspaceId !== void 0) task.workspaceId = workspaceId;
322
+ const mode = normalizeOptionalString(record.mode);
323
+ if (mode !== void 0) task.mode = mode;
324
+ if (isTaskPermission(record.permission)) task.permission = record.permission;
325
+ if (isFiniteNumber(record.archivedAt)) task.archivedAt = record.archivedAt;
326
+ const project = normalizeOptionalString(record.project);
327
+ if (project !== void 0) task.project = project;
328
+ const parentId = normalizeOptionalString(record.parentId);
329
+ if (parentId !== void 0) task.parentId = parentId;
330
+ return task;
331
+ }
332
+ //#endregion
333
+ //#region src/core/cron.ts
334
+ /** 每字段的闭区间,按 cron 顺序。 */
335
+ const FIELD_RANGES = [
336
+ [0, 59],
337
+ [0, 23],
338
+ [1, 31],
339
+ [1, 12],
340
+ [0, 7]
341
+ ];
342
+ /**
343
+ * 解析一个 5 段 cron 表达式。
344
+ * @returns 各字段匹配集合;表达式非法时返回 null。
345
+ */
346
+ function parseCron(expr) {
347
+ const fields = expr.trim().split(/\s+/);
348
+ if (fields.length !== 5) return null;
349
+ const sets = [];
350
+ for (let index = 0; index < 5; index += 1) {
351
+ const [min, max] = FIELD_RANGES[index];
352
+ const set = /* @__PURE__ */ new Set();
353
+ if (!parseField(fields[index], min, max, set)) return null;
354
+ sets.push(set);
154
355
  }
155
- openTask(id) {
156
- if (this.tasks.some((task) => task.id === id)) {
157
- this.selectedTaskId = id;
158
- this.notify();
159
- }
356
+ const weekdays = /* @__PURE__ */ new Set();
357
+ for (const day of sets[4]) weekdays.add(day === 7 ? 0 : day);
358
+ return {
359
+ minutes: sets[0],
360
+ hours: sets[1],
361
+ days: sets[2],
362
+ months: sets[3],
363
+ weekdays,
364
+ dayWildcard: fields[2] === "*",
365
+ weekdayWildcard: fields[4] === "*"
366
+ };
367
+ }
368
+ /** 表达式是否可解析(语法合法)。 */
369
+ function isValidCron(expr) {
370
+ return parseCron(expr) !== null;
371
+ }
372
+ /** 解析一个逗号列表字段,写入匹配集合;非法返回 false。 */
373
+ function parseField(field, min, max, out) {
374
+ if (field === "*") {
375
+ for (let value = min; value <= max; value += 1) out.add(value);
376
+ return true;
160
377
  }
161
- closeTask() {
162
- if (this.selectedTaskId === void 0) return;
163
- this.selectedTaskId = void 0;
164
- this.notify();
378
+ for (const part of field.split(",")) {
379
+ if (part === "") return false;
380
+ const [range, stepRaw] = part.split("/");
381
+ let low;
382
+ let high;
383
+ if (range === "*") {
384
+ low = min;
385
+ high = max;
386
+ } else if (range.includes("-")) {
387
+ const [a, b] = range.split("-");
388
+ if (a === "" || b === "" || !isDigits(a) || !isDigits(b)) return false;
389
+ low = Number(a);
390
+ high = Number(b);
391
+ } else if (isDigits(range)) {
392
+ low = Number(range);
393
+ high = Number(range);
394
+ } else return false;
395
+ if (low < min || high > max || low > high) return false;
396
+ const step = stepRaw === void 0 ? 1 : isDigits(stepRaw) ? Number(stepRaw) : NaN;
397
+ if (!Number.isInteger(step) || step < 1) return false;
398
+ for (let value = low; value <= high; value += step) out.add(value);
165
399
  }
166
- /**
167
- * 经 Host 创建任务;**只有 Host 确认后**才在账本可见并返回任务(NewTaskModal
168
- * 据此决定关闭/重试)。失败返回 undefined 并置 transportError。
169
- */
170
- async createTaskConfirmed(input) {
171
- const id = this.uuid();
172
- return await this.commitRemote({
173
- kind: "create",
174
- id,
175
- input
176
- }, id) ? this.tasks.find((task) => task.id === id) : void 0;
177
- }
178
- /**
179
- * propose(T011 §11.2/§14.4):对话提取/手动转任务 → 创建 proposed 候选任务。
180
- * 输入沿用 NewTaskInput + sourceConversationId/sourceMessageId(来源引用写入
181
- * metadata,可跳回对话);清洗与命令字段拒绝在 Host(transitions/protocol)。
182
- * 只有 Host 确认后才在账本可见并返回候选;失败返回 undefined 并置 transportError。
183
- */
184
- async proposeTask(input) {
185
- const id = this.uuid();
186
- return await this.commitRemote({
187
- kind: "propose",
188
- id,
189
- input
190
- }, id) ? this.tasks.find((task) => task.id === id) : void 0;
191
- }
192
- /**
193
- * confirm(T011 §10.2/§11.2):确认候选任务 → 落所选列(backlog/todo)。
194
- * confirm 是候选进入正式看板的唯一路径(防注入闸门)。返回 Host 是否确认。
195
- */
196
- confirmTask(id, target) {
197
- return this.commitRemote({
198
- kind: "confirm",
199
- taskId: id,
200
- target
201
- }, id);
202
- }
203
- /** dismiss(T011 §10.2/§11.2):拒绝/忽略候选任务(删除)。 */
204
- dismissTask(id) {
205
- this.commitRemote({
206
- kind: "dismiss",
207
- taskId: id
208
- }, id);
209
- }
210
- /**
211
- * start-split(T012 §11.2/§12.9/§14.5):需求文档自动拆分。提交需求(标题 +
212
- * 粘贴文本或工作区文件路径)后,Host 启动独立拆分会话驱动 agent 按 §14.5
213
- * 方法论拆分;结算后父需求任务 + 批量候选进入「待确认」列(source=requirement,
214
- * 人工确认闸门复用 T011——confirm 仍是唯一 promote 路径)。返回 Host 是否确认
215
- * 受理(受理后拆分异步进行,SSE revision 变化时批量候选到达待确认列)。
216
- */
217
- async startRequirementSplit(input) {
218
- return await this.commitRemote({
219
- kind: "start-split",
220
- id: this.uuid(),
221
- input
222
- });
223
- }
224
- updateTask(id, patch) {
225
- this.commitRemote({
226
- kind: "update",
227
- taskId: id,
228
- patch
229
- }, id);
400
+ return true;
401
+ }
402
+ function isDigits(value) {
403
+ return /^\d+$/.test(value);
404
+ }
405
+ //#endregion
406
+ //#region src/protocol.ts
407
+ /** 本项目 API 前缀(AGENTS.md D2,2026-08-22 定值;参考实现占用 /api/task-board)。 */
408
+ const TASK_BOARD_API_PREFIX = "/api/nova-task-board";
409
+ //#endregion
410
+ //#region src/client/host-api.ts
411
+ /** 单次 Host 请求超时(§17 性能边界;超时按传输错误暴露,可重试)。 */
412
+ const REQUEST_TIMEOUT_MS = 15e3;
413
+ /**
414
+ * Host 业务拒绝(HTTP 4xx,§11.1):与传输层错误区分——业务拒绝由操作修正,
415
+ * 重试无意义;错误条据此**不显示「重试连接 Host」**,只对传输失败(网络/
416
+ * 超时/解析)保留重试入口(board-controller instanceof 判定 retryable)。
417
+ */
418
+ var TaskBoardRequestError = class extends Error {
419
+ status;
420
+ constructor(message, status) {
421
+ super(message);
422
+ this.name = "TaskBoardRequestError";
423
+ this.status = status;
230
424
  }
231
- /**
232
- * 跨列移动(§10.2/§11.2 + T009 order 扩展):backlog/todo 互移、done/failed
233
- * 拖回 backlog/todo 为重开(仅改状态不触发执行,区别于 rerun)。`order` 可选
234
- * = 目标列落位下标(0-based,缺省追加目标列末尾);同列拖拽排序请用
235
- * `reorderTask`。返回 Host 是否确认。
236
- */
237
- moveTask(id, status, order) {
238
- return this.commitRemote({
239
- kind: "move",
240
- taskId: id,
241
- status,
242
- ...order === void 0 ? {} : { order }
243
- }, id);
425
+ };
426
+ /** v1 迁移标记:本项目账本的 ledgerId(Host 确认后写入,防止重复导入)。 */
427
+ const IMPORT_MARKER = "dsh.novaTaskBoard.v2.hostImported";
428
+ /** v1 迁移 sourceId(一次迁移一个,写入后持久)。 */
429
+ const SOURCE_KEY = "dsh.novaTaskBoard.v2.sourceId";
430
+ /** v1 迁移 requestId(跨 Host 重启重试保持幂等)。 */
431
+ const IMPORT_REQUEST_KEY = "dsh.novaTaskBoard.v2.importRequestId";
432
+ /** 浏览器环境可用的 uuid(无 crypto 时退化为时间戳 + 随机串)。 */
433
+ function uuid() {
434
+ return globalThis.crypto?.randomUUID?.() ?? `browser-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
435
+ }
436
+ /** JSON 响应体;非 2xx Host 的错误信息抛错(§11.1 400/403/413/415,
437
+ * 以 TaskBoardRequestError 标识业务拒绝,与传输错误区分)。 */
438
+ async function readJson(response) {
439
+ const body = await response.json();
440
+ if (!response.ok) throw new TaskBoardRequestError(body.error ?? `task-board request failed: ${response.status}`, response.status);
441
+ return body;
442
+ }
443
+ /**
444
+ * 真实 HTTP + SSE 传输(同源 `/api/nova-task-board/*`)。
445
+ * @param markers - 迁移标记存储(缺省用全局 localStorage)。
446
+ */
447
+ var HttpTaskBoardHostTransport = class {
448
+ markers;
449
+ constructor(markers = globalThis.localStorage) {
450
+ this.markers = markers;
244
451
  }
245
- /**
246
- * 列内排序/分组落位(T009 §11.2 reorder):调整列内 order(`status` 须与任务
247
- * 当前状态一致,Host 防竞态校验);`order` = 0-based 落位下标(Host 重算同列
248
- * 唯一)。分组视图跨组落位可携带归属变更 `ownership`(`project` 空串清除 /
249
- * `tags` 整体替换),与 order 原子落账。返回 Host 是否确认。
250
- */
251
- reorderTask(id, status, order, ownership) {
252
- return this.commitRemote({
253
- kind: "reorder",
254
- taskId: id,
255
- status,
256
- order,
257
- ...ownership?.project !== void 0 ? { project: ownership.project } : {},
258
- ...ownership?.tags !== void 0 ? { tags: ownership.tags } : {}
259
- }, id);
452
+ async bootstrap(legacy) {
453
+ const initial = await this.state();
454
+ const ledgerId = initial.scheduler.ledgerId;
455
+ if (legacy.length > 0 && ledgerId !== void 0 && this.markers?.getItem(IMPORT_MARKER) !== ledgerId) {
456
+ let sourceId = this.markers?.getItem(SOURCE_KEY);
457
+ if (sourceId === null || sourceId === void 0 || sourceId === "") {
458
+ sourceId = uuid();
459
+ this.markers?.setItem(SOURCE_KEY, sourceId);
460
+ }
461
+ let requestId = this.markers?.getItem(IMPORT_REQUEST_KEY);
462
+ if (requestId === null || requestId === void 0 || requestId === "") {
463
+ requestId = uuid();
464
+ this.markers?.setItem(IMPORT_REQUEST_KEY, requestId);
465
+ }
466
+ const snapshot = await this.post(requestId, {
467
+ kind: "import",
468
+ sourceId,
469
+ tasks: [...legacy]
470
+ });
471
+ this.markers?.setItem(IMPORT_MARKER, snapshot.scheduler.ledgerId ?? ledgerId);
472
+ return snapshot;
473
+ }
474
+ return initial;
260
475
  }
261
- deleteTask(id) {
262
- this.commitRemote({
263
- kind: "delete",
264
- taskId: id
265
- }, id);
476
+ async state() {
477
+ return await this.request(`${TASK_BOARD_API_PREFIX}/state`, { cache: "no-store" });
266
478
  }
267
- /** 归档已结算任务(done/failed)。Host 守卫拒绝非法归档并置错误条。 */
268
- archiveTask(id) {
269
- this.commitRemote({
270
- kind: "archive",
271
- taskId: id
272
- }, id);
479
+ async action(action) {
480
+ return await this.post(uuid(), action);
273
481
  }
274
- /** 恢复归档任务回原列。确认后关闭详情(任务已离开归档视图)。 */
275
- restoreTask(id) {
276
- this.commitRemote({
277
- kind: "restore",
278
- taskId: id
279
- }, id).then((restored) => {
280
- if (restored && this.selectedTaskId === id) this.closeTask();
482
+ async post(requestId, action) {
483
+ const envelope = {
484
+ requestId,
485
+ action
486
+ };
487
+ return await this.request(`${TASK_BOARD_API_PREFIX}/action`, {
488
+ method: "POST",
489
+ headers: { "content-type": "application/json" },
490
+ body: JSON.stringify(envelope)
281
491
  });
282
492
  }
283
- /**
284
- * 执行任务(T005 §13.2):协议 `run` 开执行记录,Host 端 runner 异步创建
285
- * 会话并发送 Prompt;本方法只提交,结算由 Host 轮询完成。
286
- */
287
- async runTask(id) {
288
- return await this.commitRemote({
289
- kind: "run",
290
- taskId: id
291
- }, id);
292
- }
293
- /** 重新执行(§13.2):协议 `rerun`,Host 侧先落回 todo 再开启执行。 */
294
- async rerunTask(id) {
295
- await this.commitRemote({
296
- kind: "rerun",
297
- taskId: id
298
- }, id);
299
- }
300
- /**
301
- * 设定/修改/关闭任务的 cron 周期定时(T006 §11.2 set-schedule)。patch 只含
302
- * enabled/cron;nextRunAt/lastTriggeredAt 由 Host 调度器维护(浏览器不可写,
303
- * §9.4)。Host 拒绝(非法 cron / 归档 / proposed 等)→ transportError,账本不变。
304
- */
305
- setSchedule(id, patch) {
306
- this.commitRemote({
307
- kind: "set-schedule",
308
- taskId: id,
309
- patch
310
- }, id);
493
+ /** 同源 fetch,带超时(超时按传输错误抛,控制器转为可重试的错误条)。 */
494
+ async request(url, init) {
495
+ const controller = new AbortController();
496
+ const timeout = globalThis.setTimeout(() => {
497
+ controller.abort();
498
+ }, REQUEST_TIMEOUT_MS);
499
+ try {
500
+ return await readJson(await fetch(url, {
501
+ ...init,
502
+ signal: controller.signal
503
+ }));
504
+ } catch (error) {
505
+ if (controller.signal.aborted) throw new Error(`task-board Host request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s`);
506
+ throw error;
507
+ } finally {
508
+ globalThis.clearTimeout(timeout);
509
+ }
311
510
  }
312
511
  /**
313
- * 设定/修改/清除任务的一次性计划(T007 §11.2 set-one-shot)。patch 只含
314
- * runAt(缺省或 0 = 取消);firedAt 由 Host 调度器维护(浏览器不可写,
315
- * §9.4)。设定自动清除 cron 分支、反之亦然(互斥 D6)。Host 拒绝(非有限
316
- * runAt / 归档 / proposed 等)→ transportError,账本不变。
512
+ * SSE 订阅:消息帧解析为 `{revision, scheduler, power}`(解析失败按同步信号
513
+ * 处理,重拉兜底);`onopen`(含断线重连成功)与页面重新可见 同步信号,
514
+ * 控制器据此重拉全量 state。
317
515
  */
318
- setOneShot(id, patch) {
319
- this.commitRemote({
320
- kind: "set-one-shot",
321
- taskId: id,
322
- patch
323
- }, id);
324
- }
325
- /** 替换执行选项集的一部分(工作区列表/预设名册来自运行时,非账本)。 */
326
- setExecutionOptions(patch) {
327
- this.executionOptions = {
328
- ...this.executionOptions,
329
- ...patch
516
+ subscribe(listener) {
517
+ const events = new EventSource(`${TASK_BOARD_API_PREFIX}/events`);
518
+ events.onmessage = (message) => {
519
+ try {
520
+ const parsed = JSON.parse(message.data);
521
+ if (parsed === null || typeof parsed !== "object" || typeof parsed.revision !== "number") throw new Error("invalid event frame");
522
+ listener(parsed);
523
+ } catch {
524
+ listener();
525
+ }
526
+ };
527
+ events.onopen = () => {
528
+ listener();
529
+ };
530
+ const onVisible = () => {
531
+ if (document.visibilityState === "visible") listener();
532
+ };
533
+ document.addEventListener("visibilitychange", onVisible);
534
+ return () => {
535
+ document.removeEventListener("visibilitychange", onVisible);
536
+ events.close();
330
537
  };
331
- this.notify();
332
- }
333
- /** 跳转执行会话 transcript(选择会话 → current 变化 → 看板自动关闭)。 */
334
- openSession(sessionId) {
335
- this.deps.sessions.open(sessionId);
336
- }
337
- onSessionsChanged() {
338
- if (!this.boardOpen) return;
339
- const current = this.deps.sessions.list.getSnapshot().current;
340
- if (current !== this.lastCurrent) this.closeBoard();
341
- this.lastCurrent = current;
342
538
  }
343
- /**
344
- * 提交远程动作:按任务串行化(同任务并发动作排队,防竞态);提交期间任务
345
- * 进入 pendingTaskIds(只做展示)。Host 拒绝 → transportError,状态不变。
346
- * @returns Host 是否确认(快照已应用)。
347
- */
348
- async commitRemote(action, taskId) {
349
- if (this.deps.transport === void 0) return true;
350
- if (taskId === void 0) return await this.performRemote(action);
351
- const operation = (this.taskQueues.get(taskId) ?? Promise.resolve()).catch(() => {}).then(async () => await this.performRemote(action));
352
- const tail = operation.then(() => {}, () => {});
353
- this.taskQueues.set(taskId, tail);
354
- this.pendingTaskIds.add(taskId);
355
- this.notify();
356
- try {
357
- return await operation;
358
- } finally {
359
- if (this.taskQueues.get(taskId) === tail) {
360
- this.taskQueues.delete(taskId);
361
- this.pendingTaskIds.delete(taskId);
362
- this.notify();
363
- }
364
- }
539
+ };
540
+ //#endregion
541
+ //#region src/client/board-controller.ts
542
+ /** 当前选中的任务(按 selectedTaskId 从账本解析),无则 undefined。 */
543
+ function selectedTaskOf(snapshot) {
544
+ if (snapshot.selectedTaskId === void 0) return void 0;
545
+ return snapshot.tasks.find((task) => task.id === snapshot.selectedTaskId);
546
+ }
547
+ /** 未确认候选任务数(T011 §12.7 入口角标:proposed 且未归档)。 */
548
+ function proposedCountOf(snapshot) {
549
+ let count = 0;
550
+ for (const task of snapshot.tasks) if (task.status === "proposed" && task.archivedAt === void 0) count += 1;
551
+ return count;
552
+ }
553
+ function randomUuid() {
554
+ const randomUUID = globalThis.crypto?.randomUUID;
555
+ if (randomUUID !== void 0) return randomUUID.call(globalThis.crypto);
556
+ return `t-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
557
+ }
558
+ function messageOf(error) {
559
+ return error instanceof Error ? error.message : String(error);
560
+ }
561
+ /**
562
+ * 身份保持合并:同一账本代数且非倒退 revision 下,JSON 未变化的任务行保留
563
+ * 原对象引用——兄弟卡片状态变化不会全量替换任务对象,TaskCard 的 memo 边界
564
+ * 生效(§12.2 卡片按任务粒度 memo 化)。代数变化/倒退则整体替换。
565
+ */
566
+ function mergeTasksIdentityPreserving(prev, next, preserve) {
567
+ if (!preserve) return [...next];
568
+ const prevById = /* @__PURE__ */ new Map();
569
+ for (const task of prev) prevById.set(task.id, task);
570
+ return next.map((task) => {
571
+ const old = prevById.get(task.id);
572
+ return old !== void 0 && JSON.stringify(old) === JSON.stringify(task) ? old : task;
573
+ });
574
+ }
575
+ /**
576
+ * 看板控制器(见模块注释)。所有变更 bump 快照并通知订阅者;UI 与 DOM 挂载
577
+ * 通过订阅重渲染。生产路径恒为 Host 背书(transport 必传)。
578
+ */
579
+ var BoardController = class {
580
+ deps;
581
+ tasks = [];
582
+ boardOpen = false;
583
+ archiveView = false;
584
+ groupMode = "manual";
585
+ selectedTaskId;
586
+ executionOptions = {
587
+ workspaces: [],
588
+ presets: []
589
+ };
590
+ listeners = /* @__PURE__ */ new Set();
591
+ disposers = [];
592
+ now;
593
+ uuid;
594
+ pendingTaskIds = /* @__PURE__ */ new Set();
595
+ taskQueues = /* @__PURE__ */ new Map();
596
+ transportFailure;
597
+ hostState;
598
+ remoteSubscribed = false;
599
+ remoteInitialization;
600
+ started = false;
601
+ lastCurrent;
602
+ /** @param deps - 传输、会话导航面、时钟与 id 铸造。 */
603
+ constructor(deps) {
604
+ this.deps = deps;
605
+ this.now = deps.now ?? (() => Date.now());
606
+ this.uuid = deps.uuid ?? randomUuid;
365
607
  }
366
- async performRemote(action) {
367
- const transport = this.deps.transport;
368
- if (transport === void 0) return true;
369
- this.transportError = void 0;
608
+ /** 启动:初始化 Host 同步 + 订阅 SSE + 会话导航。幂等。 */
609
+ start() {
610
+ if (this.started) return;
611
+ this.started = true;
612
+ this.initializeRemote();
613
+ this.disposers.push(this.deps.sessions.list.subscribe(() => {
614
+ this.onSessionsChanged();
615
+ }));
370
616
  this.notify();
371
- try {
372
- return this.acceptRemote(await transport.action(action)) || await this.refreshRemote();
373
- } catch (error) {
374
- await this.refreshRemote(messageOf(error));
375
- return false;
376
- }
377
617
  }
378
- async initializeRemote() {
379
- if (this.remoteInitialization !== void 0) return await this.remoteInitialization;
380
- const initialization = this.doInitializeRemote();
381
- this.remoteInitialization = initialization;
382
- try {
383
- return await initialization;
384
- } finally {
385
- if (this.remoteInitialization === initialization) this.remoteInitialization = void 0;
386
- }
618
+ /** 停止全部订阅并释放状态(幂等)。 */
619
+ dispose() {
620
+ for (const dispose of this.disposers.splice(0)) dispose();
621
+ this.listeners.clear();
387
622
  }
388
- async doInitializeRemote() {
389
- const transport = this.deps.transport;
390
- if (transport === void 0) return true;
391
- try {
392
- this.acceptRemote(await transport.bootstrap(this.deps.legacy?.() ?? []));
393
- return true;
394
- } catch (error) {
395
- this.transportError = messageOf(error);
396
- this.notify();
397
- return false;
398
- } finally {
399
- try {
400
- this.ensureRemoteSubscription();
401
- } catch (error) {
402
- console.error("[dsh-task-board] SSE subscription failed", error);
403
- }
404
- }
623
+ getSnapshot() {
624
+ return {
625
+ tasks: this.tasks,
626
+ boardOpen: this.boardOpen,
627
+ archiveView: this.archiveView,
628
+ groupMode: this.groupMode,
629
+ ...this.selectedTaskId === void 0 ? {} : { selectedTaskId: this.selectedTaskId },
630
+ executionOptions: this.executionOptions,
631
+ pendingTaskIds: [...this.pendingTaskIds],
632
+ executionCapable: true,
633
+ ...this.transportFailure === void 0 ? {} : {
634
+ transportError: this.transportFailure.message,
635
+ transportErrorRetryable: this.transportFailure.retryable
636
+ },
637
+ ...this.hostState === void 0 ? {} : { host: this.hostState }
638
+ };
405
639
  }
406
- /** 幂等建立 SSE 订阅(bootstrap 失败也要订阅,见 doInitializeRemote 注释)。 */
407
- ensureRemoteSubscription() {
408
- if (this.remoteSubscribed) return;
409
- this.remoteSubscribed = true;
410
- this.disposers.push(this.deps.transport.subscribe((event) => {
411
- this.onRemoteEvent(event);
412
- }));
640
+ subscribe(listener) {
641
+ this.listeners.add(listener);
642
+ return () => {
643
+ this.listeners.delete(listener);
644
+ };
645
+ }
646
+ /** 生产变更是否由 Host 传输确认(T004 恒 true;保留语义供测试/回退判断)。 */
647
+ isHostBacked() {
648
+ return this.deps.transport !== void 0;
649
+ }
650
+ /** 显式重试与 Host 的首次/全量同步(错误条「重试」按钮)。 */
651
+ async retryHostSync() {
652
+ return await this.initializeRemote();
653
+ }
654
+ openBoard() {
655
+ if (this.boardOpen) return;
656
+ this.lastCurrent = this.deps.sessions.list.getSnapshot().current;
657
+ this.boardOpen = true;
658
+ this.notify();
659
+ }
660
+ closeBoard() {
661
+ if (!this.boardOpen) return;
662
+ this.boardOpen = false;
663
+ this.notify();
664
+ }
665
+ toggleBoard() {
666
+ if (this.boardOpen) this.closeBoard();
667
+ else this.openBoard();
413
668
  }
414
669
  /**
415
- * SSE 帧处理(§11.1):revision 与已应用的一致 → 就地更新 scheduler/power
416
- * (不动任务列表,memo 边界保持);否则(帧不完整/新 revision/同步信号)重拉
417
- * 全量 state。
670
+ * 切换主看板/归档视图。离开归档视图时若选中任务仍是归档任务则清除选中
671
+ * ——详情不得悬停在已离开看板的任务上。
418
672
  */
419
- onRemoteEvent(event) {
420
- if (event !== void 0 && this.hostState !== void 0 && event.revision === this.hostState.revision && typeof event.scheduler === "object" && event.scheduler !== null && typeof event.power === "object" && event.power !== null) {
421
- this.hostState = {
422
- revision: event.revision,
423
- scheduler: event.scheduler,
424
- power: event.power
425
- };
426
- this.notify();
427
- return;
673
+ toggleArchiveView() {
674
+ this.archiveView = !this.archiveView;
675
+ if (!this.archiveView && this.selectedTaskId !== void 0) {
676
+ if (this.tasks.find((task) => task.id === this.selectedTaskId)?.archivedAt !== void 0) this.selectedTaskId = void 0;
428
677
  }
429
- this.refreshRemote();
678
+ this.notify();
430
679
  }
431
- async refreshRemote(preserveError) {
432
- const transport = this.deps.transport;
433
- if (transport === void 0) return true;
434
- try {
435
- this.acceptRemote(await transport.state());
436
- if (preserveError !== void 0) {
437
- this.transportError = preserveError;
438
- this.notify();
439
- }
440
- return true;
441
- } catch (error) {
442
- this.transportError = preserveError ?? messageOf(error);
680
+ /**
681
+ * 切换列内排序视图模式(T009 §12.10):手动顺序/按项目分组/按标签分组。
682
+ * 分组是视图层能力,不改底层 order 与归属数据;模式仅会话级记忆(刷新重置)。
683
+ */
684
+ setGroupMode(mode) {
685
+ if (this.groupMode === mode) return;
686
+ this.groupMode = mode;
687
+ this.notify();
688
+ }
689
+ openTask(id) {
690
+ if (this.tasks.some((task) => task.id === id)) {
691
+ this.selectedTaskId = id;
443
692
  this.notify();
444
- return false;
445
693
  }
446
694
  }
695
+ closeTask() {
696
+ if (this.selectedTaskId === void 0) return;
697
+ this.selectedTaskId = void 0;
698
+ this.notify();
699
+ }
447
700
  /**
448
- * 应用 Host 快照。同账本代数且 revision 非倒退 → 身份保持合并(memo 前提);
449
- * 代数变化(迁移/重建)→ 整体替换。选中任务被删除/归档(非归档视图)时清除
450
- * 选中。返回快照是否被接受。
701
+ * Host 创建任务;**只有 Host 确认后**才在账本可见并返回任务(NewTaskModal
702
+ * 据此决定关闭/重试)。失败返回 undefined 并置 transportError。
451
703
  */
452
- acceptRemote(snapshot) {
453
- const current = this.hostState;
454
- const nextLedgerId = snapshot.scheduler.ledgerId;
455
- const sameGeneration = current === void 0 || current.scheduler.ledgerId === nextLedgerId;
456
- if (sameGeneration && current !== void 0 && snapshot.revision < current.revision) return false;
457
- const preserve = sameGeneration && current !== void 0 && snapshot.revision >= current.revision;
458
- this.tasks = mergeTasksIdentityPreserving(this.tasks, snapshot.tasks, preserve);
459
- this.hostState = {
460
- revision: snapshot.revision,
461
- scheduler: snapshot.scheduler,
462
- power: snapshot.power
463
- };
464
- this.transportError = void 0;
465
- if (this.selectedTaskId !== void 0 && !this.tasks.some((task) => task.id === this.selectedTaskId)) this.selectedTaskId = void 0;
466
- if (!this.archiveView && this.selectedTaskId !== void 0 && this.tasks.find((task) => task.id === this.selectedTaskId)?.archivedAt !== void 0) this.selectedTaskId = void 0;
467
- this.notify();
468
- return true;
704
+ async createTaskConfirmed(input) {
705
+ const id = this.uuid();
706
+ return await this.commitRemote({
707
+ kind: "create",
708
+ id,
709
+ input
710
+ }, id) ? this.tasks.find((task) => task.id === id) : void 0;
469
711
  }
470
- notify() {
471
- for (const listener of [...this.listeners]) listener();
712
+ /**
713
+ * propose(T011 §11.2/§14.4):对话提取/手动转任务 创建 proposed 候选任务。
714
+ * 输入沿用 NewTaskInput + sourceConversationId/sourceMessageId(来源引用写入
715
+ * metadata,可跳回对话);清洗与命令字段拒绝在 Host(transitions/protocol)。
716
+ * 只有 Host 确认后才在账本可见并返回候选;失败返回 undefined 并置 transportError。
717
+ */
718
+ async proposeTask(input) {
719
+ const id = this.uuid();
720
+ return await this.commitRemote({
721
+ kind: "propose",
722
+ id,
723
+ input
724
+ }, id) ? this.tasks.find((task) => task.id === id) : void 0;
472
725
  }
473
- };
474
- //#endregion
475
- //#region src/client/apply-guard.ts
476
- /** Claims the plugin apply slot. Returns true when this call won the slot. */
477
- function claimNovaTaskBoardApply() {
478
- if (globalThis.__dshNovaTaskBoardApplied === true) return false;
479
- globalThis.__dshNovaTaskBoardApplied = true;
480
- return true;
481
- }
482
- /**
483
- * Releases the claim. Called from the client fiber cleanup so that a
484
- * hot-reloaded bundle (the loader unloads the old plugin fiber and invokes
485
- * the rebuilt one in the same page) can claim again instead of being
486
- * silently dropped.
487
- */
488
- function releaseNovaTaskBoardApply() {
489
- globalThis.__dshNovaTaskBoardApplied = void 0;
490
- }
491
- //#endregion
492
- //#region src/client/grouping.ts
493
- /** 全部模式(顶部切换按钮按此顺序渲染)。 */
494
- const GROUP_MODES = [
495
- "manual",
496
- "project",
497
- "tag"
498
- ];
499
- /** 与 Host transitions.byOrderStable 一致的列内排序(order 升序,createdAt/id 决胜)。 */
500
- function byOrderStable(a, b) {
501
- return a.order - b.order || a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
502
- }
503
- /** 列内任务按 order 升序排序(原数组不改动)。 */
504
- function sortByOrder(tasks) {
505
- return [...tasks].sort(byOrderStable);
506
- }
507
- /** 任务在给定视图模式下的组 key(手动模式无组概念,返回 undefined)。 */
508
- function groupKeyOf(task, mode) {
509
- if (mode === "project") return task.project ?? "";
510
- if (mode === "tag") return task.tags.length > 0 ? task.tags[0] : "";
511
- }
512
- /** 任务是否属于某个组(按模式解释 key:项目精确匹配 / 标签命中 / 未分类无归属)。 */
513
- function groupContains(task, groupKey, mode) {
514
- if (mode === "project") return groupKey === "" ? task.project === void 0 : task.project === groupKey;
515
- if (mode === "tag") return groupKey === "" ? task.tags.length === 0 : task.tags.includes(groupKey);
516
- return false;
517
- }
518
- /**
519
- * 拖拽目标组的归属变换(视图层预览用,与提交的 reorder 归属补丁一致,§12.10):
520
- * - 按项目分组:拖到组 G → project = G;拖到「未分类」→ 清除 project;
521
- * - 按标签分组:拖到组 G → G 成为首选标签(tags[0],G 已含于 tags 时仅调整顺序);
522
- * 拖到「未分类」→ 移除首选标签(tags.shift(),若 tags 为空则归「未分类」);
523
- * - 手动模式或非组落位 → 原样返回。
524
- */
525
- function applyOwnershipTransform(task, groupKey, mode) {
526
- if (mode === "manual" || groupKey === void 0) return task;
527
- if (mode === "project") return groupKey === "" ? {
528
- ...task,
529
- project: void 0
530
- } : {
531
- ...task,
532
- project: groupKey
533
- };
534
- if (groupKey === "") return {
535
- ...task,
536
- tags: task.tags.slice(1)
537
- };
538
- return {
539
- ...task,
540
- tags: [groupKey, ...task.tags.filter((tag) => tag !== groupKey)]
541
- };
542
- }
543
- /**
544
- * 分组(§12.10):项目/标签模式下的归组展示,组内按 order 升序;标签模式多归组
545
- * (一个任务可出现在其全部标签组),无标签归「未分类」;未分类组恒在最后。
546
- * 手动模式不分组(调用方直接 sortByOrder)。
547
- */
548
- function groupTasks(tasks, mode) {
549
- if (mode === "project") {
550
- const byProject = /* @__PURE__ */ new Map();
551
- for (const task of tasks) {
552
- const key = task.project ?? "";
553
- const list = byProject.get(key);
554
- if (list === void 0) byProject.set(key, [task]);
555
- else list.push(task);
556
- }
557
- return groupsFromMap(byProject);
726
+ /**
727
+ * confirm(T011 §10.2/§11.2):确认候选任务 → 落所选列(backlog/todo)。
728
+ * confirm 是候选进入正式看板的唯一路径(防注入闸门)。返回 Host 是否确认。
729
+ */
730
+ confirmTask(id, target) {
731
+ return this.commitRemote({
732
+ kind: "confirm",
733
+ taskId: id,
734
+ target
735
+ }, id);
558
736
  }
559
- const byTag = /* @__PURE__ */ new Map();
560
- const ungrouped = [];
561
- for (const task of tasks) {
562
- if (task.tags.length === 0) {
563
- ungrouped.push(task);
564
- continue;
565
- }
566
- for (const tag of task.tags) {
567
- const list = byTag.get(tag);
568
- if (list === void 0) byTag.set(tag, [task]);
569
- else list.push(task);
570
- }
737
+ /** dismiss(T011 §10.2/§11.2):拒绝/忽略候选任务(删除)。 */
738
+ dismissTask(id) {
739
+ this.commitRemote({
740
+ kind: "dismiss",
741
+ taskId: id
742
+ }, id);
571
743
  }
572
- return [...groupsFromMap(byTag), ...ungrouped.length > 0 ? [{
573
- key: "",
574
- label: "",
575
- tasks: sortByOrder(ungrouped)
576
- }] : []];
577
- }
578
- /** 按 label(zh locale)排序的组列表;「未分类」组(key 空串)恒在最后。 */
579
- function groupsFromMap(map) {
580
- const groups = [];
581
- for (const [key, list] of map) {
582
- if (key === "") continue;
583
- groups.push({
584
- key,
585
- label: key,
586
- tasks: sortByOrder(list)
744
+ /**
745
+ * start-split(T012 §11.2/§12.9/§14.5):需求文档自动拆分。提交需求(标题 +
746
+ * 粘贴文本或工作区文件路径)后,Host 启动独立拆分会话驱动 agent 按 §14.5
747
+ * 方法论拆分;结算后父需求任务 + 批量候选进入「待确认」列(source=requirement,
748
+ * 人工确认闸门复用 T011——confirm 仍是唯一 promote 路径)。返回 Host 是否确认
749
+ * 受理(受理后拆分异步进行,SSE revision 变化时批量候选到达待确认列)。
750
+ */
751
+ async startRequirementSplit(input) {
752
+ return await this.commitRemote({
753
+ kind: "start-split",
754
+ id: this.uuid(),
755
+ input
587
756
  });
588
757
  }
589
- groups.sort((a, b) => a.label.localeCompare(b.label, "zh"));
590
- const ungrouped = map.get("");
591
- if (ungrouped !== void 0) groups.push({
592
- key: "",
593
- label: "",
594
- tasks: sortByOrder(ungrouped)
595
- });
596
- return groups;
597
- }
598
- //#endregion
599
- //#region src/client/drag-utils.ts
600
- /**
601
- * 拖拽合法性(§10.2/§11.2):running 不可作目标;同列 = 列内排序;跨列目标仅限
602
- * backlog/todo(done/failed 拖回为重开)。proposed/归档不可作为来源由调用方
603
- * (卡片 draggable=false)保证。
604
- */
605
- function isLegalDrop(origin, targetStatus) {
606
- if (targetStatus === "running") return false;
607
- if (targetStatus === origin.fromStatus) return true;
608
- return targetStatus === "backlog" || targetStatus === "todo";
609
- }
610
- /** 目标组尾在基准列(按 order 升序)中的最后一张卡的下标;组空返回 -1。 */
611
- function lastGroupIndex(column, groupKey, mode) {
612
- let last = -1;
613
- for (let index = 0; index < column.length; index += 1) if (groupContains(column[index], groupKey, mode)) last = index;
614
- return last;
615
- }
616
- /**
617
- * 计算落位下标(0-based,基准 = 剔除拖拽卡片后的目标列,按 order 稳定升序;
618
- * 与 Host applyReposition 的 insert 基准一致):
619
- * - 锚定卡片:卡片下标(after +1);
620
- * - 分组视图组尾:目标组最后一张卡下标 +1(组空回退列尾);
621
- * - 列尾:可见列表最后一张卡下标 +1(过滤定位;无可见卡则列尾)。
622
- */
623
- function computeDropIndex(restColumn, target, mode, lastVisibleId) {
624
- if (target.anchorId !== void 0) {
625
- const anchor = restColumn.findIndex((task) => task.id === target.anchorId);
626
- if (anchor !== -1) return anchor + (target.position === "after" ? 1 : 0);
758
+ updateTask(id, patch) {
759
+ this.commitRemote({
760
+ kind: "update",
761
+ taskId: id,
762
+ patch
763
+ }, id);
627
764
  }
628
- if (target.groupKey !== void 0 && mode !== "manual") {
629
- const last = lastGroupIndex(restColumn, target.groupKey, mode);
630
- if (last !== -1) return last + 1;
765
+ /**
766
+ * 跨列移动(§10.2/§11.2 + T009 order 扩展):backlog/todo 互移、done/failed
767
+ * 拖回 backlog/todo 为重开(仅改状态不触发执行,区别于 rerun)。`order` 可选
768
+ * = 目标列落位下标(0-based,缺省追加目标列末尾);同列拖拽排序请用
769
+ * `reorderTask`。返回 Host 是否确认。
770
+ */
771
+ moveTask(id, status, order) {
772
+ return this.commitRemote({
773
+ kind: "move",
774
+ taskId: id,
775
+ status,
776
+ ...order === void 0 ? {} : { order }
777
+ }, id);
631
778
  }
632
- if (lastVisibleId !== void 0) {
633
- const index = restColumn.findIndex((task) => task.id === lastVisibleId);
634
- if (index !== -1) return index + 1;
779
+ /**
780
+ * 列内排序/分组落位(T009 §11.2 reorder):调整列内 order(`status` 须与任务
781
+ * 当前状态一致,Host 防竞态校验);`order` = 0-based 落位下标(Host 重算同列
782
+ * 唯一)。分组视图跨组落位可携带归属变更 `ownership`(`project` 空串清除 /
783
+ * `tags` 整体替换),与 order 原子落账。返回 Host 是否确认。
784
+ */
785
+ reorderTask(id, status, order, ownership) {
786
+ return this.commitRemote({
787
+ kind: "reorder",
788
+ taskId: id,
789
+ status,
790
+ order,
791
+ ...ownership?.project !== void 0 ? { project: ownership.project } : {},
792
+ ...ownership?.tags !== void 0 ? { tags: ownership.tags } : {}
793
+ }, id);
635
794
  }
636
- return restColumn.length;
637
- }
638
- /**
639
- * 可见列表内的落位下标(拖拽 pending 渲染用;list = 目标列/目标组的可见列表,
640
- * **已剔除拖拽卡片**):锚定卡片 before/after,否则列表尾。
641
- */
642
- function visibleDropIndex(list, target) {
643
- if (target.anchorId !== void 0) {
644
- const index = list.findIndex((task) => task.id === target.anchorId);
645
- if (index !== -1) return Math.min(index + (target.position === "after" ? 1 : 0), list.length);
795
+ deleteTask(id) {
796
+ this.commitRemote({
797
+ kind: "delete",
798
+ taskId: id
799
+ }, id);
646
800
  }
647
- return list.length;
648
- }
649
- /**
650
- * 构建拖拽提交的 action(§11.2/D10):
651
- * - 同列 → `reorder`(status 与来源一致;分组视图跨组落位携带归属变更——
652
- * 项目模式 project = 组名 / ''(清除);标签模式 tags 使目标标签成为首选或
653
- * 移除首选(未分类));
654
- * - 跨列 → `move`(status 为目标列 + 落位下标,缺省语义由 Host 追加末尾;
655
- * done/failed 重开不触发执行)。
656
- * 非法目标/拖到自己返回 undefined(调用方提示拒绝)。
657
- */
658
- function buildDropAction(origin, target, mode, tasks, lastVisibleId) {
659
- if (!isLegalDrop(origin, target.status)) return void 0;
660
- if (target.anchorId === origin.taskId) return void 0;
661
- const task = tasks.find((candidate) => candidate.id === origin.taskId);
662
- if (task === void 0) return void 0;
663
- const index = computeDropIndex(sortByOrder(tasks.filter((candidate) => candidate.status === target.status)).filter((candidate) => candidate.id !== origin.taskId), target, mode, lastVisibleId);
664
- if (target.status === origin.fromStatus) {
665
- let project;
666
- let tags;
667
- if (mode === "project" && target.groupKey !== void 0 && target.groupKey !== origin.fromGroupKey) project = target.groupKey === "" ? "" : target.groupKey;
668
- else if (mode === "tag" && target.groupKey !== void 0 && target.groupKey !== origin.fromGroupKey) tags = target.groupKey === "" ? task.tags.slice(1) : [target.groupKey, ...task.tags.filter((tag) => tag !== target.groupKey)];
669
- return {
670
- kind: "reorder",
671
- taskId: origin.taskId,
672
- status: target.status,
673
- order: index,
674
- ...project !== void 0 ? { project } : {},
675
- ...tags !== void 0 ? { tags } : {}
801
+ /**
802
+ * 停止运行中的执行(§11.2 cancel-execution,用户手动取消):Host 把目标执行
803
+ * 结算为 cancelled → 任务回落 todo,running 独占随即释放——随后可移动/删除/
804
+ * 归档。`executionId` 缺省 = 最后一条未结算执行;底层会话由 Host 尽力停止。
805
+ * 返回 Host 是否确认。
806
+ */
807
+ cancelExecution(id, executionId) {
808
+ return this.commitRemote({
809
+ kind: "cancel-execution",
810
+ taskId: id,
811
+ ...executionId === void 0 ? {} : { executionId }
812
+ }, id);
813
+ }
814
+ /** 归档已结算任务(done/failed)。Host 守卫拒绝非法归档并置错误条。 */
815
+ archiveTask(id) {
816
+ this.commitRemote({
817
+ kind: "archive",
818
+ taskId: id
819
+ }, id);
820
+ }
821
+ /** 恢复归档任务回原列。确认后关闭详情(任务已离开归档视图)。 */
822
+ restoreTask(id) {
823
+ this.commitRemote({
824
+ kind: "restore",
825
+ taskId: id
826
+ }, id).then((restored) => {
827
+ if (restored && this.selectedTaskId === id) this.closeTask();
828
+ });
829
+ }
830
+ /**
831
+ * 执行任务(T005 §13.2):协议 `run` 开执行记录,Host 端 runner 异步创建
832
+ * 会话并发送 Prompt;本方法只提交,结算由 Host 轮询完成。
833
+ */
834
+ async runTask(id) {
835
+ return await this.commitRemote({
836
+ kind: "run",
837
+ taskId: id
838
+ }, id);
839
+ }
840
+ /** 重新执行(§13.2):协议 `rerun`,Host 侧先落回 todo 再开启执行。 */
841
+ async rerunTask(id) {
842
+ await this.commitRemote({
843
+ kind: "rerun",
844
+ taskId: id
845
+ }, id);
846
+ }
847
+ /**
848
+ * 设定/修改/关闭任务的 cron 周期定时(T006 §11.2 set-schedule)。patch 只含
849
+ * enabled/cron;nextRunAt/lastTriggeredAt 由 Host 调度器维护(浏览器不可写,
850
+ * §9.4)。Host 拒绝(非法 cron / 归档 / proposed 等)→ transportError,账本不变。
851
+ */
852
+ setSchedule(id, patch) {
853
+ this.commitRemote({
854
+ kind: "set-schedule",
855
+ taskId: id,
856
+ patch
857
+ }, id);
858
+ }
859
+ /**
860
+ * 设定/修改/清除任务的一次性计划(T007 §11.2 set-one-shot)。patch 只含
861
+ * runAt(缺省或 0 = 取消);firedAt 由 Host 调度器维护(浏览器不可写,
862
+ * §9.4)。设定自动清除 cron 分支、反之亦然(互斥 D6)。Host 拒绝(非有限
863
+ * runAt / 归档 / proposed 等)→ transportError,账本不变。
864
+ */
865
+ setOneShot(id, patch) {
866
+ this.commitRemote({
867
+ kind: "set-one-shot",
868
+ taskId: id,
869
+ patch
870
+ }, id);
871
+ }
872
+ /** 替换执行选项集的一部分(工作区列表/预设名册来自运行时,非账本)。 */
873
+ setExecutionOptions(patch) {
874
+ this.executionOptions = {
875
+ ...this.executionOptions,
876
+ ...patch
676
877
  };
878
+ this.notify();
677
879
  }
678
- return {
679
- kind: "move",
680
- taskId: origin.taskId,
681
- status: target.status,
682
- order: index
683
- };
684
- }
685
- //#endregion
686
- //#region src/client/filter.ts
687
- /** 空过滤(看板初始状态)。 */
688
- const EMPTY_FILTER = {
689
- query: "",
690
- project: { kind: "all" },
691
- tags: []
692
- };
693
- /** 单条任务是否命中过滤(多条件 AND、标签内 OR)。 */
694
- function matchesFilter(task, filter) {
695
- const query = filter.query.trim().toLowerCase();
696
- if (query !== "" && !task.title.toLowerCase().includes(query) && !task.description.toLowerCase().includes(query)) return false;
697
- if (filter.project.kind === "none") {
698
- if (task.project !== void 0) return false;
699
- } else if (filter.project.kind === "name") {
700
- if (task.project !== filter.project.name) return false;
880
+ /** 跳转执行会话 transcript(选择会话 → current 变化 → 看板自动关闭)。 */
881
+ openSession(sessionId) {
882
+ this.deps.sessions.open(sessionId);
883
+ }
884
+ onSessionsChanged() {
885
+ if (!this.boardOpen) return;
886
+ const current = this.deps.sessions.list.getSnapshot().current;
887
+ if (current !== this.lastCurrent) this.closeBoard();
888
+ this.lastCurrent = current;
889
+ }
890
+ /**
891
+ * 提交远程动作:按任务串行化(同任务并发动作排队,防竞态);提交期间任务
892
+ * 进入 pendingTaskIds(只做展示)。Host 拒绝 → transportError,状态不变。
893
+ * @returns Host 是否确认(快照已应用)。
894
+ */
895
+ async commitRemote(action, taskId) {
896
+ if (this.deps.transport === void 0) return true;
897
+ if (taskId === void 0) return await this.performRemote(action);
898
+ const operation = (this.taskQueues.get(taskId) ?? Promise.resolve()).catch(() => {}).then(async () => await this.performRemote(action));
899
+ const tail = operation.then(() => {}, () => {});
900
+ this.taskQueues.set(taskId, tail);
901
+ this.pendingTaskIds.add(taskId);
902
+ this.notify();
903
+ try {
904
+ return await operation;
905
+ } finally {
906
+ if (this.taskQueues.get(taskId) === tail) {
907
+ this.taskQueues.delete(taskId);
908
+ this.pendingTaskIds.delete(taskId);
909
+ this.notify();
910
+ }
911
+ }
912
+ }
913
+ async performRemote(action) {
914
+ const transport = this.deps.transport;
915
+ if (transport === void 0) return true;
916
+ this.transportFailure = void 0;
917
+ this.notify();
918
+ try {
919
+ return this.acceptRemote(await transport.action(action)) || await this.refreshRemote();
920
+ } catch (error) {
921
+ await this.refreshRemote(messageOf(error), !(error instanceof TaskBoardRequestError));
922
+ return false;
923
+ }
924
+ }
925
+ async initializeRemote() {
926
+ if (this.remoteInitialization !== void 0) return await this.remoteInitialization;
927
+ const initialization = this.doInitializeRemote();
928
+ this.remoteInitialization = initialization;
929
+ try {
930
+ return await initialization;
931
+ } finally {
932
+ if (this.remoteInitialization === initialization) this.remoteInitialization = void 0;
933
+ }
934
+ }
935
+ async doInitializeRemote() {
936
+ const transport = this.deps.transport;
937
+ if (transport === void 0) return true;
938
+ try {
939
+ this.acceptRemote(await transport.bootstrap(this.deps.legacy?.() ?? []));
940
+ return true;
941
+ } catch (error) {
942
+ this.transportFailure = {
943
+ message: messageOf(error),
944
+ retryable: !(error instanceof TaskBoardRequestError)
945
+ };
946
+ this.notify();
947
+ return false;
948
+ } finally {
949
+ try {
950
+ this.ensureRemoteSubscription();
951
+ } catch (error) {
952
+ console.error("[dsh-task-board] SSE subscription failed", error);
953
+ }
954
+ }
955
+ }
956
+ /** 幂等建立 SSE 订阅(bootstrap 失败也要订阅,见 doInitializeRemote 注释)。 */
957
+ ensureRemoteSubscription() {
958
+ if (this.remoteSubscribed) return;
959
+ this.remoteSubscribed = true;
960
+ this.disposers.push(this.deps.transport.subscribe((event) => {
961
+ this.onRemoteEvent(event);
962
+ }));
963
+ }
964
+ /**
965
+ * SSE 帧处理(§11.1):revision 与已应用的一致 → 就地更新 scheduler/power
966
+ * (不动任务列表,memo 边界保持);否则(帧不完整/新 revision/同步信号)重拉
967
+ * 全量 state。
968
+ */
969
+ onRemoteEvent(event) {
970
+ if (event !== void 0 && this.hostState !== void 0 && event.revision === this.hostState.revision && typeof event.scheduler === "object" && event.scheduler !== null && typeof event.power === "object" && event.power !== null) {
971
+ this.hostState = {
972
+ revision: event.revision,
973
+ scheduler: event.scheduler,
974
+ power: event.power
975
+ };
976
+ this.notify();
977
+ return;
978
+ }
979
+ this.refreshRemote();
980
+ }
981
+ async refreshRemote(preserveMessage, preserveRetryable) {
982
+ const transport = this.deps.transport;
983
+ if (transport === void 0) return true;
984
+ try {
985
+ this.acceptRemote(await transport.state());
986
+ if (preserveMessage !== void 0) {
987
+ this.transportFailure = {
988
+ message: preserveMessage,
989
+ retryable: preserveRetryable ?? false
990
+ };
991
+ this.notify();
992
+ }
993
+ return true;
994
+ } catch (error) {
995
+ this.transportFailure = {
996
+ message: preserveMessage ?? messageOf(error),
997
+ retryable: preserveMessage === void 0 ? !(error instanceof TaskBoardRequestError) : preserveRetryable ?? false
998
+ };
999
+ this.notify();
1000
+ return false;
1001
+ }
1002
+ }
1003
+ /**
1004
+ * 应用 Host 快照。同账本代数且 revision 非倒退 → 身份保持合并(memo 前提);
1005
+ * 代数变化(迁移/重建)→ 整体替换。选中任务被删除/归档(非归档视图)时清除
1006
+ * 选中。返回快照是否被接受。
1007
+ */
1008
+ acceptRemote(snapshot) {
1009
+ const current = this.hostState;
1010
+ const nextLedgerId = snapshot.scheduler.ledgerId;
1011
+ const sameGeneration = current === void 0 || current.scheduler.ledgerId === nextLedgerId;
1012
+ if (sameGeneration && current !== void 0 && snapshot.revision < current.revision) return false;
1013
+ const preserve = sameGeneration && current !== void 0 && snapshot.revision >= current.revision;
1014
+ this.tasks = mergeTasksIdentityPreserving(this.tasks, snapshot.tasks, preserve);
1015
+ this.hostState = {
1016
+ revision: snapshot.revision,
1017
+ scheduler: snapshot.scheduler,
1018
+ power: snapshot.power
1019
+ };
1020
+ this.transportFailure = void 0;
1021
+ if (this.selectedTaskId !== void 0 && !this.tasks.some((task) => task.id === this.selectedTaskId)) this.selectedTaskId = void 0;
1022
+ if (!this.archiveView && this.selectedTaskId !== void 0 && this.tasks.find((task) => task.id === this.selectedTaskId)?.archivedAt !== void 0) this.selectedTaskId = void 0;
1023
+ this.notify();
1024
+ return true;
1025
+ }
1026
+ notify() {
1027
+ for (const listener of [...this.listeners]) listener();
701
1028
  }
702
- if (filter.tags.length > 0 && !filter.tags.some((tag) => task.tags.includes(tag))) return false;
703
- return true;
704
- }
705
- /** 对任务集执行过滤(保持入参顺序与对象引用,memo 边界不受影响)。 */
706
- function filterTasks(tasks, filter) {
707
- return tasks.filter((task) => matchesFilter(task, filter));
708
- }
709
- /** 过滤是否处于激活状态(任一维度有约束;用于「清除全部」显隐与命中计数)。 */
710
- function isFilterActive(filter) {
711
- return filter.query.trim() !== "" || filter.project.kind !== "all" || filter.tags.length > 0;
712
- }
713
- /** 从任务集推导去重后的项目名列表(过滤下拉选项),按 zh locale 排序。 */
714
- function distinctProjects(tasks) {
715
- const set = /* @__PURE__ */ new Set();
716
- for (const task of tasks) if (task.project !== void 0) set.add(task.project);
717
- return [...set].sort((a, b) => a.localeCompare(b, "zh"));
718
- }
719
- /** 从任务集推导去重后的标签列表(多选 chips 选项),按 zh locale 排序。 */
720
- function distinctTags(tasks) {
721
- const set = /* @__PURE__ */ new Set();
722
- for (const task of tasks) for (const tag of task.tags) set.add(tag);
723
- return [...set].sort((a, b) => a.localeCompare(b, "zh"));
724
- }
725
- //#endregion
726
- //#region src/client/locales.ts
727
- /**
728
- * Nova task-board copy: zh-first dictionaries with an English fallback,
729
- * selected by the document language. Kept dependency-free (no dsh locale
730
- * service) so the DOM-injected entry row and the standalone board tree share
731
- * one tiny lookup. Key set 以 zh 为准,en 对齐(对齐参考实现的组织方式)。
732
- *
733
- * T004 补全看板/详情/新建/状态/执行设置文案;定时编辑器(T006/T007)与
734
- * 待确认列(T011)的编辑/确认文案随对应任务补充。
735
- */
736
- /** zh dictionary (key-set source of truth). */
737
- const zh = {
738
- "entry.label": "Nova 任务看板",
739
- "board.title": "Nova 任务看板",
740
- "board.close": "返回会话",
741
- "board.new": "新建任务",
742
- "board.search": "筛选任务…",
743
- "board.empty": "这个状态还没有任务",
744
- "board.archive": "归档",
745
- "board.archiveView": "归档 ({count})",
746
- "board.backToBoard": "返回看板",
747
- "archive.empty": "没有已归档的任务",
748
- "board.status": "状态",
749
- "board.status.proposed": "待确认",
750
- "board.status.backlog": "待规划",
751
- "board.status.todo": "待办",
752
- "board.status.running": "进行中",
753
- "board.status.done": "已完成",
754
- "board.status.failed": "已失败",
755
- "board.runs": "次执行",
756
- "board.pending": "正在提交",
757
- "board.updated": "更新于",
758
- "board.created": "创建于",
759
- "board.hostError": "Host 操作失败:{error}",
760
- "board.retryHost": "重试连接 Host",
761
- "board.hostMeta": "Host 时区 {timeZone} · revision {revision}",
762
- "new.title": "标题",
763
- "new.titlePlaceholder": "一句话描述要做什么",
764
- "new.description": "描述",
765
- "new.descriptionPlaceholder": "补充背景、范围与验收(可选)",
766
- "new.prompt": "执行 Prompt",
767
- "new.promptPlaceholder": "发给 agent 的完整指令(留空则使用标题)",
768
- "new.submit": "创建",
769
- "new.cancel": "取消",
770
- "new.required": "标题不能为空",
771
- "detail.title": "任务详情",
772
- "detail.close": "关闭",
773
- "detail.prompt": "执行 Prompt",
774
- "detail.description": "描述",
775
- "detail.execution": "执行记录",
776
- "detail.noExecution": "尚未执行",
777
- "detail.run": "执行",
778
- "detail.rerun": "重新执行",
779
- "detail.delete": "删除",
780
- "detail.archive": "归档",
781
- "detail.restore": "恢复",
782
- "detail.archivedAt": "已归档 · {time}",
783
- "detail.viewSession": "查看会话",
784
- "detail.executionStarted": "已启动",
785
- "detail.executionEnded": "已结束",
786
- "detail.result.succeeded": "成功",
787
- "detail.result.failed": "失败",
788
- "detail.result.cancelled": "已取消",
789
- "detail.result.running": "进行中",
790
- "delete.title": "删除任务",
791
- "delete.confirm": "确定删除「{name}」吗?删除后不可恢复。",
792
- "delete.ok": "删除",
793
- "delete.cancel": "取消",
794
- "status.move.backlog": "移到待规划",
795
- "status.move.todo": "移到待办",
796
- "time.justNow": "刚刚",
797
- "card.scheduled": "定时",
798
- "card.oneShot": "计划",
799
- "detail.schedule": "执行安排",
800
- "detail.schedule.nextRun": "下次运行",
801
- "detail.schedule.lastTriggered": "上次运行",
802
- "detail.schedule.notScheduled": "未安排",
803
- "detail.schedule.dueSoon": "即将运行",
804
- "detail.schedule.enable": "启用定时执行",
805
- "detail.schedule.cron": "cron 表达式",
806
- "detail.schedule.invalid": "cron 表达式不合法",
807
- "detail.schedule.presets": "预设",
808
- "detail.schedule.preset.daily9": "每天 09:00",
809
- "detail.schedule.preset.hourly": "每小时整点",
810
- "detail.schedule.preset.tenMin": "每 10 分钟",
811
- "detail.schedule.preset.weeklyMon9": "每周一 09:00",
812
- "detail.schedule.modeCron": "周期定时",
813
- "detail.schedule.modeOneShot": "一次性计划",
814
- "detail.oneShot.runAt": "计划时刻",
815
- "detail.oneShot.remaining": "剩余",
816
- "detail.oneShot.cancel": "取消计划",
817
- "detail.oneShot.executed": "已执行 · {time}",
818
- "detail.oneShot.skipped": "已过期/已跳过",
819
- "detail.oneShot.reschedule": "可重新设定",
820
- "detail.oneShot.invalid": "计划时刻不合法",
821
- "detail.oneShot.dueSoon": "即将触发",
822
- "detail.oneShot.preset.tenMin": "10 分钟后",
823
- "detail.oneShot.preset.tonight21": "今晚 21:00",
824
- "detail.oneShot.preset.tomorrow9": "明天 09:00",
825
- "detail.executionSettings": "执行设置",
826
- "exec.hint": "执行时生效:工作区决定执行会话落在哪个工作区;模式决定会话的 agent 预设;权限经 /permission 命令应用到会话。留空则使用运行时默认。",
827
- "new.workspace": "工作区",
828
- "new.mode": "模式",
829
- "new.permission": "权限",
830
- "exec.workspace.recent": "最近使用(默认)",
831
- "exec.mode.default": "部署默认",
832
- "exec.mode.defaultSuffix": "(默认)",
833
- "exec.mode.brokenSuffix": "(不可用)",
834
- "exec.mode.removed": "(已移除)",
835
- "exec.permission.default": "会话默认",
836
- "exec.permission.read-only": "只读",
837
- "exec.permission.workspace-write": "工作区可写",
838
- "exec.permission.danger-full-access": "完全访问",
839
- "filter.project": "项目",
840
- "filter.projectAll": "全部项目",
841
- "filter.projectNone": "未分类(无项目)",
842
- "filter.tags": "标签",
843
- "filter.clearAll": "清除全部",
844
- "filter.hits": "命中 {count}",
845
- "filter.removeTag": "删除标签 {tag}",
846
- "board.group.label": "排序/分组",
847
- "board.group.manual": "手动顺序",
848
- "board.group.project": "按项目分组",
849
- "board.group.tag": "按标签分组",
850
- "board.group.ungrouped": "未分类",
851
- "board.dragRejected": "不能拖拽到该列(running 等非法目标)",
852
- "detail.projectTags": "项目与标签",
853
- "detail.projectPlaceholder": "所属项目(可留空)",
854
- "new.tagsPlaceholder": "输入标签后回车",
855
- "confirm.title": "确认候选任务",
856
- "confirm.subtitle": "候选任务未确认前不可执行、不可定时、不可归档。可编辑后选择落列确认,或拒绝删除。",
857
- "confirm.target": "确认后落列",
858
- "confirm.toBacklog": "待规划(backlog)",
859
- "confirm.toTodo": "待办(todo)",
860
- "confirm.submit": "确认",
861
- "confirm.dismiss": "拒绝",
862
- "confirm.dismissTitle": "拒绝候选任务",
863
- "confirm.dismissMessage": "确定拒绝「{name}」吗?该候选任务将被删除。",
864
- "card.proposedBadge": "待确认",
865
- "card.source": "来源",
866
- "source.conversation": "对话",
867
- "source.requirement": "需求拆分",
868
- "entry.proposedCount": "{count} 个候选待确认",
869
- "convert.title": "转为任务",
870
- "convert.fromConversation": "从对话提取",
871
- "convert.submitProposed": "创建为候选",
872
- "convert.submitDirect": "直接创建",
873
- "convert.directTarget": "落列",
874
- "convert.directBacklog": "待规划",
875
- "convert.directTodo": "待办",
876
- "convert.proposedHint": "创建为候选:进入看板「待确认」列,确认后方可执行。",
877
- "chat.extractedHint": "已从对话提取 {count} 个候选任务(看板「待确认」列),可随时确认或拒绝。",
878
- "detail.jumpToConversation": "跳回对话",
879
- "detail.sourceConversation": "来源会话",
880
- "split.entry": "从需求拆分",
881
- "split.subtitle": "提交一份需求文档(粘贴文本或工作区文件路径),系统将主动拆分为一组可独立执行/验收的子任务候选,进入「待确认」列;确认后子任务可独立执行并保留父需求引用。",
882
- "split.recursiveTitle": "递归拆分",
883
- "split.recursiveHint": "将针对该任务继续拆分:新建父需求任务「需求:<标题>」挂到其下,子任务再指向新父任务。",
884
- "split.requirementTitle": "需求标题",
885
- "split.requirementTitlePlaceholder": "如「XX 系统登录模块」",
886
- "split.source": "需求来源",
887
- "split.sourceText": "粘贴文本",
888
- "split.sourceFile": "工作区文件路径",
889
- "split.text": "需求文本",
890
- "split.textPlaceholder": "粘贴需求文档内容…",
891
- "split.file": "文件路径",
892
- "split.filePlaceholder": "相对工作区的文件路径,如 docs/requirement.md",
893
- "split.required": "请填写需求标题与需求内容(文本或文件路径)",
894
- "split.submit": "发起拆分",
895
- "split.asyncHint": "提交后在独立会话中拆分,完成后进入「待确认」列(未确认前不可执行)。",
896
- "split.batchButton": "批量确认",
897
- "split.batchTitle": "批量确认候选任务",
898
- "split.batchSubtitle": "逐条编辑(标题/描述/Prompt/项目/标签)、增删条目;或批量确认到所选列、整体拒绝。",
899
- "split.addItem": "添加条目",
900
- "split.added": "新增",
901
- "split.removeItem": "移除",
902
- "split.confirmAll": "确认全部到{target}",
903
- "split.dismissAll": "全部拒绝",
904
- "split.dismissAllTitle": "拒绝全部候选",
905
- "split.dismissAllMessage": "确定拒绝全部 {count} 个候选任务吗?将被删除。",
906
- "split.dismissItemTitle": "移除候选任务",
907
- "split.dismissItemMessage": "确定移除「{name}」吗?该候选任务将被删除。",
908
- "split.empty": "没有可批量确认的候选任务",
909
- "split.parentOf": "所属需求",
910
- "split.parentMissing": "(父任务已删除)",
911
- "split.children": "子任务",
912
- "settings.title": "Nova 任务看板",
913
- "settings.description": "控制 Host Nova 任务看板与 agent 播报。",
914
- "settings.enabled": "启用 Nova 任务看板",
915
- "settings.enabledHint": "关闭后隐藏侧边栏入口与看板视图。",
916
- "settings.announceToAgent": "向 agent 播报 Nova 任务看板",
917
- "settings.announceToAgentHint": "开启:每条 agent 系统提示都会包含本看板的说明;关闭:不播报。",
918
- "settings.notExposed": "当前 DSH 版本未向设置页暴露本插件的配置命名空间,表单不可用。可编辑 ~/.dsh/settings.yaml 直接配置。",
919
- "settings.readOnly": "当前部署的设置只读。"
920
1029
  };
921
- /** en dictionary, complete against the zh key set. */
922
- const en = {
923
- "entry.label": "Nova Task Board",
924
- "board.title": "Nova Task Board",
925
- "board.close": "Back to chat",
926
- "board.new": "New Task",
927
- "board.search": "Filter tasks…",
928
- "board.empty": "No tasks in this column",
929
- "board.archive": "Archive",
930
- "board.archiveView": "Archived ({count})",
931
- "board.backToBoard": "Back to board",
932
- "archive.empty": "No archived tasks",
933
- "board.status": "Status",
934
- "board.status.proposed": "Proposed",
935
- "board.status.backlog": "Backlog",
936
- "board.status.todo": "To Do",
937
- "board.status.running": "In Progress",
938
- "board.status.done": "Done",
939
- "board.status.failed": "Failed",
940
- "board.runs": "runs",
941
- "board.pending": "Submitting",
942
- "board.updated": "Updated",
943
- "board.created": "Created",
944
- "board.hostError": "Host action failed: {error}",
945
- "board.retryHost": "Retry Host connection",
946
- "board.hostMeta": "Host time zone {timeZone} · revision {revision}",
947
- "new.title": "Title",
948
- "new.titlePlaceholder": "What should be done, in one line",
949
- "new.description": "Description",
950
- "new.descriptionPlaceholder": "Background, scope, acceptance criteria (optional)",
951
- "new.prompt": "Run Prompt",
952
- "new.promptPlaceholder": "The full instruction sent to the agent (title is used when blank)",
953
- "new.submit": "Create",
954
- "new.cancel": "Cancel",
955
- "new.required": "Title is required",
956
- "detail.title": "Task Detail",
957
- "detail.close": "Close",
958
- "detail.prompt": "Run Prompt",
959
- "detail.description": "Description",
960
- "detail.execution": "Execution History",
961
- "detail.noExecution": "Not executed yet",
962
- "detail.run": "Run",
963
- "detail.rerun": "Run Again",
964
- "detail.delete": "Delete",
965
- "detail.archive": "Archive",
966
- "detail.restore": "Restore",
967
- "detail.archivedAt": "Archived · {time}",
968
- "detail.viewSession": "View Session",
969
- "detail.executionStarted": "Started",
970
- "detail.executionEnded": "Ended",
971
- "detail.result.succeeded": "Succeeded",
972
- "detail.result.failed": "Failed",
973
- "detail.result.cancelled": "Cancelled",
974
- "detail.result.running": "Running",
975
- "delete.title": "Delete Task",
976
- "delete.confirm": "Delete \"{name}\"? This cannot be undone.",
977
- "delete.ok": "Delete",
978
- "delete.cancel": "Cancel",
979
- "status.move.backlog": "Move to Backlog",
980
- "status.move.todo": "Move to To Do",
981
- "time.justNow": "just now",
982
- "card.scheduled": "scheduled",
983
- "card.oneShot": "planned",
984
- "detail.schedule": "Schedule",
985
- "detail.schedule.nextRun": "Next run",
986
- "detail.schedule.lastTriggered": "Last run",
987
- "detail.schedule.notScheduled": "Not scheduled",
988
- "detail.schedule.dueSoon": "Due soon",
989
- "detail.schedule.enable": "Enable scheduled runs",
990
- "detail.schedule.cron": "cron expression",
991
- "detail.schedule.invalid": "Invalid cron expression",
992
- "detail.schedule.presets": "Presets",
993
- "detail.schedule.preset.daily9": "Daily 09:00",
994
- "detail.schedule.preset.hourly": "Every hour",
995
- "detail.schedule.preset.tenMin": "Every 10 min",
996
- "detail.schedule.preset.weeklyMon9": "Mon 09:00",
997
- "detail.schedule.modeCron": "Recurring",
998
- "detail.schedule.modeOneShot": "One-shot",
999
- "detail.oneShot.runAt": "Run at",
1000
- "detail.oneShot.remaining": "in",
1001
- "detail.oneShot.cancel": "Cancel plan",
1002
- "detail.oneShot.executed": "Executed · {time}",
1003
- "detail.oneShot.skipped": "Expired / skipped",
1004
- "detail.oneShot.reschedule": "reschedule",
1005
- "detail.oneShot.invalid": "Invalid plan time",
1006
- "detail.oneShot.dueSoon": "Due soon",
1007
- "detail.oneShot.preset.tenMin": "In 10 minutes",
1008
- "detail.oneShot.preset.tonight21": "Tonight 21:00",
1009
- "detail.oneShot.preset.tomorrow9": "Tomorrow 09:00",
1010
- "detail.executionSettings": "Execution Settings",
1011
- "exec.hint": "Applied when the task runs: the workspace decides where the execution session lands; the mode composes the session's agent preset; the permission is applied through the /permission command. Blank = runtime default.",
1012
- "new.workspace": "Workspace",
1013
- "new.mode": "Mode",
1014
- "new.permission": "Permission",
1015
- "exec.workspace.recent": "Most recent (default)",
1016
- "exec.mode.default": "Deployment default",
1017
- "exec.mode.defaultSuffix": " (default)",
1018
- "exec.mode.brokenSuffix": " (unavailable)",
1019
- "exec.mode.removed": " (removed)",
1020
- "exec.permission.default": "Session default",
1021
- "exec.permission.read-only": "Read-only",
1022
- "exec.permission.workspace-write": "Workspace Write",
1023
- "exec.permission.danger-full-access": "Full Access",
1024
- "filter.project": "Project",
1025
- "filter.projectAll": "All projects",
1026
- "filter.projectNone": "Unassigned",
1027
- "filter.tags": "Tags",
1028
- "filter.clearAll": "Clear all",
1029
- "filter.hits": "{count} hits",
1030
- "filter.removeTag": "Remove tag {tag}",
1031
- "board.group.label": "Sort & group",
1032
- "board.group.manual": "Manual",
1033
- "board.group.project": "By project",
1034
- "board.group.tag": "By tag",
1035
- "board.group.ungrouped": "Unassigned",
1036
- "board.dragRejected": "Cannot drop here (illegal target)",
1037
- "detail.projectTags": "Project & Tags",
1038
- "detail.projectPlaceholder": "Project (optional)",
1039
- "new.tagsPlaceholder": "Type a tag and press Enter",
1040
- "confirm.title": "Confirm candidate task",
1041
- "confirm.subtitle": "Candidates cannot run, be scheduled, or be archived until confirmed. Edit and pick a target column, or dismiss.",
1042
- "confirm.target": "Confirm into",
1043
- "confirm.toBacklog": "Backlog",
1044
- "confirm.toTodo": "To Do",
1045
- "confirm.submit": "Confirm",
1046
- "confirm.dismiss": "Dismiss",
1047
- "confirm.dismissTitle": "Dismiss candidate task",
1048
- "confirm.dismissMessage": "Dismiss \"{name}\"? The candidate task will be deleted.",
1049
- "card.proposedBadge": "Pending",
1050
- "card.source": "Source",
1051
- "source.conversation": "Chat",
1052
- "source.requirement": "Requirement",
1053
- "entry.proposedCount": "{count} candidate(s) pending",
1054
- "convert.title": "Turn into task",
1055
- "convert.fromConversation": "From conversation",
1056
- "convert.submitProposed": "Propose",
1057
- "convert.submitDirect": "Create directly",
1058
- "convert.directTarget": "Into",
1059
- "convert.directBacklog": "Backlog",
1060
- "convert.directTodo": "To Do",
1061
- "convert.proposedHint": "Proposing adds the task to the \"Pending\" column; it runs only after confirmation.",
1062
- "chat.extractedHint": "Extracted {count} candidate task(s) from the conversation (see the board \"Pending\" column).",
1063
- "detail.jumpToConversation": "Back to chat",
1064
- "detail.sourceConversation": "Source session",
1065
- "split.entry": "Split from requirement",
1066
- "split.subtitle": "Submit a requirement document (pasted text or a workspace file path); the system proactively splits it into independently executable/verifiable subtask candidates in the \"Pending\" column. Confirmed subtasks run independently and keep a reference to the parent requirement.",
1067
- "split.recursiveTitle": "Recursive split",
1068
- "split.recursiveHint": "Continue splitting this task: a new parent requirement task \"Requirement: <title>\" is created under it, and the subtasks point to the new parent.",
1069
- "split.requirementTitle": "Requirement title",
1070
- "split.requirementTitlePlaceholder": "e.g. \"Login module\"",
1071
- "split.source": "Requirement source",
1072
- "split.sourceText": "Pasted text",
1073
- "split.sourceFile": "Workspace file path",
1074
- "split.text": "Requirement text",
1075
- "split.textPlaceholder": "Paste the requirement document…",
1076
- "split.file": "File path",
1077
- "split.filePlaceholder": "Path relative to the workspace, e.g. docs/requirement.md",
1078
- "split.required": "Requirement title and content (text or file path) are required",
1079
- "split.submit": "Start split",
1080
- "split.asyncHint": "Splitting runs in a dedicated session; results land in the \"Pending\" column (not executable until confirmed).",
1081
- "split.batchButton": "Batch confirm",
1082
- "split.batchTitle": "Batch confirm candidates",
1083
- "split.batchSubtitle": "Edit items (title/description/prompt/project/tags), add or remove rows; confirm all into the chosen column, or dismiss all.",
1084
- "split.addItem": "Add item",
1085
- "split.added": "New",
1086
- "split.removeItem": "Remove",
1087
- "split.confirmAll": "Confirm all into {target}",
1088
- "split.dismissAll": "Dismiss all",
1089
- "split.dismissAllTitle": "Dismiss all candidates",
1090
- "split.dismissAllMessage": "Dismiss all {count} candidate(s)? They will be deleted.",
1091
- "split.dismissItemTitle": "Remove candidate",
1092
- "split.dismissItemMessage": "Remove \"{name}\"? The candidate task will be deleted.",
1093
- "split.empty": "No candidates to batch-confirm",
1094
- "split.parentOf": "Parent requirement",
1095
- "split.parentMissing": "(parent task deleted)",
1096
- "split.children": "Subtasks",
1097
- "settings.title": "Nova Task Board",
1098
- "settings.description": "Configure the Host Nova task board and agent announcement.",
1099
- "settings.enabled": "Enable the Nova task board",
1100
- "settings.enabledHint": "When off, the sidebar entry and board view are hidden.",
1101
- "settings.announceToAgent": "Announce the Nova task board to agents",
1102
- "settings.announceToAgentHint": "On: every agent system prompt includes a note about this board. Off: no announcement.",
1103
- "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.",
1104
- "settings.readOnly": "This deployment stores settings read-only."
1105
- };
1106
- /** Active dictionary, picked by the document language at call time. */
1107
- function dictionary() {
1108
- return (typeof document !== "undefined" ? document.documentElement.lang : "zh").toLowerCase().startsWith("en") ? en : zh;
1109
- }
1110
- /** Translate a key with optional {name} template params. */
1111
- function t(key, params) {
1112
- let text = dictionary()[key];
1113
- if (params !== void 0) for (const [name, value] of Object.entries(params)) text = text.replaceAll(`{${name}}`, value);
1114
- return text;
1030
+ //#endregion
1031
+ //#region src/client/apply-guard.ts
1032
+ /** Claims the plugin apply slot. Returns true when this call won the slot. */
1033
+ function claimNovaTaskBoardApply() {
1034
+ if (globalThis.__dshNovaTaskBoardApplied === true) return false;
1035
+ globalThis.__dshNovaTaskBoardApplied = true;
1036
+ return true;
1115
1037
  }
1116
1038
  /**
1117
- * Locale dictionary for the "Nova 插件" settings section (the first-level nav
1118
- * entry that hosts the dsh-nova-ui family plugin cards). Kept as its own
1119
- * namespace (`nova-plugins`) so the section copy never collides with the
1120
- * task-board card's `nova-task-board` namespace (AGENTS.md D4). Key set 以 zh 为准。
1039
+ * Releases the claim. Called from the client fiber cleanup so that a
1040
+ * hot-reloaded bundle (the loader unloads the old plugin fiber and invokes
1041
+ * the rebuilt one in the same page) can claim again instead of being
1042
+ * silently dropped.
1121
1043
  */
1122
- const groupZh = {
1123
- title: "Nova 插件",
1124
- description: "统一管理 Nova 系列插件(dsh-nova-ui 全家桶)的启用与配置。"
1125
- };
1126
- /** en dictionary, complete against the groupZh key set. */
1127
- const groupEn = {
1128
- title: "Nova Plugins",
1129
- description: "Enable and configure the dsh-nova-ui family plugins from one place."
1130
- };
1044
+ function releaseNovaTaskBoardApply() {
1045
+ globalThis.__dshNovaTaskBoardApplied = void 0;
1046
+ }
1131
1047
  //#endregion
1132
- //#region \0dsh-css:packages/dsh-nova-ui-task-board/src/client/board.module.css.mjs
1133
- const css = "[data-pane=conversation],[class*=centerCol]{position:relative}[data-dsh-nova-taskboard-view]{z-index:60;background:var(--dsw-alias-bg-base);display:none;position:absolute;inset:0;container:a-UL2G_nova-task-board-view/inline-size}html[data-dsh-nova-taskboard-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [data-dsh-nova-taskboard-view]{display:block}html[data-dsh-nova-taskboard-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [data-pane=conversation]>:not([data-dsh-nova-taskboard-view]),html[data-dsh-nova-taskboard-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [class*=centerCol]>:not([data-dsh-nova-taskboard-view]){display:none!important}.a-UL2G_entry{width:100%;height:32px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:8px;align-items:center;gap:8px;padding:0 12px;font-size:13px;display:flex}.a-UL2G_entry:hover{background:var(--dsw-specific-sidebar-nav-item-hover);color:var(--dsw-alias-label-primary)}.a-UL2G_entry[data-active]{background:var(--dsw-specific-sidebar-nav-item-active);color:var(--dsw-alias-label-primary);font-weight:600}.a-UL2G_entryIcon{flex:none;justify-content:center;align-items:center;display:inline-flex}.a-UL2G_entryLabel{text-overflow:ellipsis;overflow:hidden}[data-dsh-frame][data-sidebar-collapsed] .a-UL2G_entry{justify-content:center;width:100%;padding:0}[data-dsh-frame][data-sidebar-collapsed] .a-UL2G_entryLabel{display:none}.a-UL2G_boardView{height:100%;min-height:0}.a-UL2G_board{box-sizing:border-box;background:var(--dsw-alias-bg-base);min-width:0;height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex-direction:column;gap:12px;padding:14px 16px 16px;display:flex}.a-UL2G_boardHeader{flex-wrap:wrap;flex:none;align-items:center;gap:10px;display:flex}.a-UL2G_boardTitle{color:var(--dsw-alias-label-primary);white-space:nowrap;margin:0;font-size:16px;font-weight:700}.a-UL2G_backButton{align-items:center;gap:4px;display:inline-flex}.a-UL2G_hostMeta{color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-overflow:ellipsis;margin-left:auto;font-size:11px;overflow:hidden}.a-UL2G_search{min-width:120px;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;flex:0 260px;padding:6px 10px;font-size:13px}.a-UL2G_search::placeholder{color:var(--dsw-alias-label-tertiary)}.a-UL2G_filterBar{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-wrap:wrap;flex:none;align-items:center;gap:14px;padding:8px 10px;display:flex}.a-UL2G_filterItem{align-items:center;gap:8px;min-width:0;display:inline-flex}.a-UL2G_filterLabel{color:var(--dsw-alias-label-tertiary);flex:none;font-size:12px;font-weight:600}.a-UL2G_filterItem .a-UL2G_select{max-width:220px}.a-UL2G_filterChips{flex-wrap:wrap;align-items:center;gap:6px;max-height:64px;display:inline-flex;overflow-y:auto}.a-UL2G_filterChip{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;white-space:nowrap;border-radius:999px;padding:2px 9px;font-family:inherit;font-size:12px;line-height:1.5;transition:background-color .12s,color .12s,border-color .12s}.a-UL2G_filterChip:hover{border-color:var(--dsw-alias-border-l3)}.a-UL2G_filterChipActive{color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-button-info-fill);border-color:#0000}.a-UL2G_filterActions{align-items:center;gap:10px;margin-left:auto;display:inline-flex}.a-UL2G_filterHitCount{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-size:12px}.a-UL2G_cardChips{flex-wrap:wrap;align-items:center;gap:4px;display:flex}.a-UL2G_cardProject{color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-state-business-primary);white-space:nowrap;text-overflow:ellipsis;border-radius:999px;max-width:140px;padding:1px 8px;font-size:11px;font-weight:600;line-height:1.5;overflow:hidden}.a-UL2G_cardTag{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover);border:1px solid var(--dsw-alias-border-l2);white-space:nowrap;text-overflow:ellipsis;border-radius:999px;max-width:110px;padding:1px 8px;font-size:11px;line-height:1.5;overflow:hidden}.a-UL2G_cardTagMore{color:var(--dsw-alias-label-tertiary);white-space:nowrap;border-radius:999px;padding:1px 6px;font-size:11px;line-height:1.5}.a-UL2G_cardParent{color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-state-business-primary);white-space:nowrap;text-overflow:ellipsis;cursor:pointer;border-radius:999px;max-width:180px;padding:1px 8px;font-size:11px;line-height:1.5;display:inline-block;overflow:hidden}.a-UL2G_cardParent:hover{filter:brightness(1.1);text-decoration:underline}.a-UL2G_splitRows{flex-direction:column;gap:12px;max-height:min(46vh,420px);display:flex;overflow-y:auto}.a-UL2G_splitRow{background:var(--dsw-alias-bg-l2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;flex-direction:column;gap:8px;padding:10px 12px;display:flex}.a-UL2G_splitRowHeader{justify-content:space-between;align-items:center;gap:8px;display:flex}.a-UL2G_splitRowIndex{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}.a-UL2G_tagEditor{flex-direction:column;gap:6px;display:flex}.a-UL2G_tagEditor .a-UL2G_input{max-width:280px}.a-UL2G_tagEditorChip{cursor:default;align-items:center;gap:4px;display:inline-flex}.a-UL2G_tagEditorRemove{width:16px;height:16px;color:inherit;cursor:pointer;background:0 0;border:none;border-radius:50%;justify-content:center;align-items:center;padding:0;font-size:13px;line-height:1;display:inline-flex}.a-UL2G_tagEditorRemove:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.a-UL2G_tagEditorRemove:disabled{opacity:.45;cursor:default}.a-UL2G_columns{overscroll-behavior-inline:contain;scrollbar-color:var(--dsw-alias-border-l3) var(--dsw-alias-interactive-bg-hover);scrollbar-width:thin;flex:1;grid-auto-columns:minmax(220px,1fr);grid-auto-flow:column;gap:12px;min-height:0;padding-bottom:6px;display:grid;overflow:auto hidden}.a-UL2G_columns::-webkit-scrollbar{height:10px}.a-UL2G_columns::-webkit-scrollbar-track{background:var(--dsw-alias-interactive-bg-hover);border-radius:999px}.a-UL2G_columns::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l3);background-clip:content-box;border:2px solid #0000;border-radius:999px}.a-UL2G_columns::-webkit-scrollbar-thumb:hover{background:var(--dsw-alias-border-l4);background-clip:content-box}.a-UL2G_column{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:12px;flex-direction:column;min-height:0;display:flex;overflow:hidden}.a-UL2G_columnHeader{flex:none;align-items:center;gap:6px;padding:10px 12px;display:flex}.a-UL2G_columnTitle{color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:13px;font-weight:700;overflow:hidden}.a-UL2G_columnCount{min-width:0;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;flex:none;padding:1px 8px;font-size:12px}.a-UL2G_statusDot{border-radius:50%;flex:none;width:8px;height:8px}.a-UL2G_statusDot[data-status=backlog]{background:var(--dsw-alias-label-tertiary)}.a-UL2G_statusDot[data-status=todo]{background:var(--dsw-alias-state-business-primary)}.a-UL2G_statusDot[data-status=running]{background:var(--dsw-alias-state-warn-primary)}.a-UL2G_statusDot[data-status=done]{background:var(--dsw-alias-state-success-primary)}.a-UL2G_statusDot[data-status=failed]{background:var(--dsw-alias-state-error-primary)}.a-UL2G_cards{flex-direction:column;flex:1;gap:8px;min-height:0;padding:2px 8px 10px;display:flex;overflow-y:auto}.a-UL2G_columnEmpty{text-align:center;color:var(--dsw-alias-label-tertiary);padding:24px 8px;font-size:12px}.a-UL2G_card{text-align:left;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;color:var(--dsw-alias-label-primary);border-radius:10px;flex-direction:column;gap:6px;padding:10px 12px;font-family:inherit;transition:box-shadow .12s,border-color .12s,transform .12s;display:flex}.a-UL2G_card:hover{box-shadow:var(--dsw-shadow-lv2);border-color:var(--dsw-alias-border-l3);transform:translateY(-1px)}.a-UL2G_card[data-status=running]{border-color:var(--dsw-alias-state-warn-primary)}.a-UL2G_cardTitle{-webkit-line-clamp:2;-webkit-box-orient:vertical;font-size:13px;font-weight:600;line-height:1.35;display:-webkit-box;overflow:hidden}.a-UL2G_cardExcerpt{color:var(--dsw-alias-label-secondary);-webkit-line-clamp:2;-webkit-box-orient:vertical;font-size:12px;line-height:1.4;display:-webkit-box;overflow:hidden}.a-UL2G_cardMeta{color:var(--dsw-alias-label-tertiary);align-items:center;gap:8px;font-size:11px;display:flex}.a-UL2G_cardTime{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.a-UL2G_cardSchedule{white-space:nowrap;min-width:0;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;flex:none;padding:2px 6px;font-size:12px;line-height:1}.a-UL2G_cardRun{flex:none}.a-UL2G_cardRun[data-result=failed]{color:var(--dsw-alias-state-error-primary)}.a-UL2G_cardRun[data-result=succeeded]{color:var(--dsw-alias-state-success-primary)}.a-UL2G_cardSession{color:var(--dsw-alias-state-business-primary);flex:none}.a-UL2G_cardRunningLabel{color:var(--dsw-alias-state-warn-primary);font-size:11px}.a-UL2G_cardSpinner{border:2px solid var(--dsw-alias-state-warn-primary);border-top-color:#0000;border-radius:50%;flex:none;width:10px;height:10px;animation:.8s linear infinite a-UL2G_dshNovaTbSpin}@keyframes a-UL2G_dshNovaTbSpin{to{transform:rotate(360deg)}}.a-UL2G_primaryButton{color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-button-info-fill);cursor:pointer;white-space:nowrap;border:none;border-radius:8px;padding:6px 14px;font-size:13px;font-weight:600}.a-UL2G_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-info-hover)}.a-UL2G_primaryButton:disabled{opacity:.5;cursor:default}.a-UL2G_ghostButton{color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;white-space:nowrap;background:0 0;border-radius:8px;padding:5px 12px;font-size:12px}.a-UL2G_ghostButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.a-UL2G_ghostButton:disabled{opacity:.45;cursor:default}.a-UL2G_dangerButton{color:#fff;background:var(--dsw-alias-state-error-primary);cursor:pointer;white-space:nowrap;border:none;border-radius:8px;padding:6px 14px;font-size:13px;font-weight:600}.a-UL2G_dangerButton:hover:not(:disabled){filter:brightness(1.08)}.a-UL2G_dangerButton:active:not(:disabled){filter:brightness(.94)}.a-UL2G_dangerButton:disabled{opacity:.5;cursor:default}.a-UL2G_iconButton{width:26px;height:26px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;font-size:13px;display:inline-flex}.a-UL2G_iconButton:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.a-UL2G_linkButton{color:var(--dsw-alias-state-business-primary);cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:0;font-size:12px}.a-UL2G_linkButton:hover{text-decoration:underline}.a-UL2G_modalBackdrop{z-index:1300;background:var(--dsw-alias-bg-mask-1);justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.a-UL2G_modal{background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);width:min(520px,100vw - 48px);max-height:calc(100vh - 96px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);border-radius:14px;flex-direction:column;gap:12px;padding:18px;display:flex;overflow-y:auto}.a-UL2G_modalTitle{margin:0;font-size:15px;font-weight:700}.a-UL2G_confirmMessage{color:var(--dsw-alias-label-secondary);white-space:pre-wrap;overflow-wrap:anywhere;margin:0;font-size:13px;line-height:1.5}.a-UL2G_modalFooter{justify-content:flex-end;gap:10px;margin-top:4px;display:flex}.a-UL2G_field{flex-direction:column;gap:5px;display:flex}.a-UL2G_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}.a-UL2G_input{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);resize:vertical;border-radius:8px;outline:none;padding:7px 10px;font-family:inherit;font-size:13px}.a-UL2G_input:focus{border-color:var(--dsw-alias-state-business-primary)}.a-UL2G_select{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;max-width:100%;padding:7px 10px;font-family:inherit;font-size:13px}.a-UL2G_input::placeholder{color:var(--dsw-alias-label-tertiary)}.a-UL2G_formError{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}.a-UL2G_detail{background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);width:min(640px,100vw - 48px);max-height:calc(100vh - 80px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);border-radius:14px;flex-direction:column;display:flex;overflow:hidden}.a-UL2G_detailHeader{border-bottom:1px solid var(--dsw-alias-separator-primary);flex:none;align-items:center;gap:10px;padding:14px 18px;display:flex}.a-UL2G_detailTitle{overflow-wrap:anywhere;flex:1;margin:0;font-size:15px;font-weight:700}.a-UL2G_statusBadge{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:2px 10px;font-size:12px}.a-UL2G_statusBadge[data-status=running]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}.a-UL2G_statusBadge[data-status=done]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.a-UL2G_statusBadge[data-status=failed]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.a-UL2G_detailBody{flex-direction:column;flex:1;gap:16px;padding:14px 18px;display:flex;overflow-y:auto}.a-UL2G_detailSection{flex-direction:column;gap:6px;display:flex}.a-UL2G_detailSection h4{color:var(--dsw-alias-label-tertiary);text-transform:none;margin:0;font-size:12px;font-weight:700}.a-UL2G_detailText{color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere;margin:0;font-size:13px;line-height:1.55}.a-UL2G_promptBlock{font-size:12.5px;line-height:1.5;font-family:var(--dsw-font-markdown-code-block-small);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-markdown-code-block);border:1px solid var(--dsw-alias-border-l1);white-space:pre-wrap;overflow-wrap:anywhere;border-radius:8px;max-height:240px;margin:0;padding:10px 12px;overflow-y:auto}.a-UL2G_executionList{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}.a-UL2G_executionRow{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;flex-wrap:wrap;align-items:center;gap:10px;padding:8px 10px;display:flex}.a-UL2G_executionBadge{color:var(--dsw-alias-state-warn-primary);background:var(--dsw-alias-state-warn-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:600}.a-UL2G_executionBadge[data-result=succeeded]{color:var(--dsw-alias-state-success-primary);background:0 0}.a-UL2G_executionBadge[data-result=failed]{color:var(--dsw-alias-state-error-primary);background:0 0}.a-UL2G_executionBadge[data-result=cancelled]{color:var(--dsw-alias-label-tertiary);background:0 0}.a-UL2G_executionTimes{color:var(--dsw-alias-label-secondary);font-size:12px}.a-UL2G_executionError{width:100%;color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;font-size:12px}.a-UL2G_moveRow{flex-wrap:wrap;gap:8px;display:flex}.a-UL2G_scheduleMode{background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;gap:4px;margin-bottom:10px;padding:3px;display:inline-flex}.a-UL2G_scheduleModeButton{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:7px;padding:4px 12px;font-size:12px}.a-UL2G_scheduleModeButton:hover:not(.a-UL2G_scheduleModeActive){color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.a-UL2G_scheduleModeActive{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-fill-cs-primary);box-shadow:var(--dsw-alias-shadow-fs-1)}.a-UL2G_dangerGhostButton{color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-primary);cursor:pointer;white-space:nowrap;background:0 0;border-radius:8px;padding:5px 12px;font-size:12px}.a-UL2G_dangerGhostButton:hover:not(:disabled){background:var(--dsw-alias-state-error-tint)}.a-UL2G_dangerGhostButton:disabled{opacity:.45;cursor:default}.a-UL2G_scheduleToggle{color:var(--dsw-alias-label-primary);cursor:pointer;user-select:none;align-items:center;gap:8px;font-size:13px;display:flex}.a-UL2G_scheduleToggle input{accent-color:var(--dsw-alias-state-business-primary)}.a-UL2G_scheduleRow{align-items:center;gap:8px;display:flex}.a-UL2G_scheduleInput{min-width:0;font-family:var(--dsw-font-markdown-code-block-small);flex:1;font-size:12.5px}.a-UL2G_scheduleInputInvalid,.a-UL2G_scheduleInputInvalid:focus{border-color:var(--dsw-alias-state-error-primary)}.a-UL2G_schedulePreset{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;flex:none;padding:7px 8px;font-size:12.5px}.a-UL2G_scheduleMeta{color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;margin:0;font-size:12px}.a-UL2G_detailFooter{border-top:1px solid var(--dsw-alias-separator-primary);flex:none;align-items:center;gap:10px;padding:12px 18px;display:flex}.a-UL2G_detailMeta{color:var(--dsw-alias-label-tertiary);margin-left:auto;font-size:11px}@container a-UL2G_nova-task-board-view (width<=720px){.a-UL2G_board{gap:10px;padding:10px}.a-UL2G_columns{gap:10px}}@container a-UL2G_nova-task-board-view (width<=600px){.a-UL2G_boardHeader{flex-wrap:wrap;gap:8px}.a-UL2G_search{flex:calc(100% - 72px);min-width:0}.a-UL2G_boardHeader>button{flex:1 1 0;min-width:max-content}}.a-UL2G_entry:focus-visible,.a-UL2G_card:focus-visible,.a-UL2G_primaryButton:focus-visible,.a-UL2G_ghostButton:focus-visible,.a-UL2G_dangerButton:focus-visible,.a-UL2G_iconButton:focus-visible,.a-UL2G_linkButton:focus-visible,.a-UL2G_search:focus-visible,.a-UL2G_input:focus-visible,.a-UL2G_select:focus-visible,.a-UL2G_schedulePreset:focus-visible,.a-UL2G_scheduleToggle input:focus-visible,.a-UL2G_filterChip:focus-visible,.a-UL2G_tagEditorRemove:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}.a-UL2G_entry,.a-UL2G_primaryButton,.a-UL2G_ghostButton,.a-UL2G_dangerButton,.a-UL2G_iconButton,.a-UL2G_linkButton,.a-UL2G_search,.a-UL2G_input,.a-UL2G_select,.a-UL2G_schedulePreset,.a-UL2G_scheduleToggle input,.a-UL2G_filterChip,.a-UL2G_tagEditorRemove{transition:background-color .12s,color .12s,border-color .12s,outline-color .12s,box-shadow .12s,transform .12s}.a-UL2G_card:active{box-shadow:var(--dsw-shadow-lv1);transform:translateY(0)}.a-UL2G_entry:active,.a-UL2G_primaryButton:active:not(:disabled),.a-UL2G_ghostButton:active:not(:disabled),.a-UL2G_dangerButton:active:not(:disabled),.a-UL2G_iconButton:active:not(:disabled),.a-UL2G_linkButton:active:not(:disabled){transform:translateY(1px)}.a-UL2G_entry[data-active]:hover{background:var(--dsw-specific-sidebar-nav-item-active)}.a-UL2G_iconButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.a-UL2G_linkButton:hover:not(:disabled){text-decoration:underline}.a-UL2G_iconButton:disabled,.a-UL2G_linkButton:disabled{opacity:.45;cursor:default}.a-UL2G_search:focus,.a-UL2G_select:focus,.a-UL2G_schedulePreset:focus{border-color:var(--dsw-alias-state-business-primary)}@media (prefers-reduced-motion:reduce){.a-UL2G_entry,.a-UL2G_card,.a-UL2G_primaryButton,.a-UL2G_ghostButton,.a-UL2G_dangerButton,.a-UL2G_iconButton,.a-UL2G_linkButton,.a-UL2G_search,.a-UL2G_input,.a-UL2G_select,.a-UL2G_schedulePreset,.a-UL2G_scheduleToggle input,.a-UL2G_filterChip,.a-UL2G_tagEditorRemove{transition:none}.a-UL2G_cardSpinner{animation:none}}.a-UL2G_groupToggle{background:var(--dsw-alias-interactive-bg-hover);border-radius:8px;flex:none;align-items:center;gap:2px;padding:2px;display:inline-flex}.a-UL2G_groupToggleButton{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:6px;padding:4px 10px;font-size:12px}.a-UL2G_groupToggleButton:hover:not(:disabled){color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-active)}.a-UL2G_groupToggleActive,.a-UL2G_groupToggleActive:hover:not(:disabled){color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);box-shadow:var(--dsw-shadow-lv1);font-weight:600}.a-UL2G_group{border:1px dashed #0000;border-radius:10px;flex-direction:column;gap:6px;padding:6px 8px;display:flex}.a-UL2G_group[data-drag-over]{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-interactive-bg-hover)}.a-UL2G_group[data-drag-rejected]{border-color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-state-error-surface,transparent)}.a-UL2G_groupHeader{flex:none;align-items:center;gap:6px;padding:2px 2px 0;display:flex}.a-UL2G_groupTitle{color:var(--dsw-alias-label-secondary);text-overflow:ellipsis;white-space:nowrap;flex:1;font-size:12px;font-weight:700;overflow:hidden}.a-UL2G_groupCount{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;flex:none;padding:0 7px;font-size:11px}.a-UL2G_groupCards{flex-direction:column;gap:8px;display:flex}.a-UL2G_column[data-drag-over]{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 1px var(--dsw-alias-state-business-primary)}.a-UL2G_column[data-drag-rejected]{border-color:var(--dsw-alias-state-error-primary)}.a-UL2G_card[data-draggable]{cursor:grab}.a-UL2G_card[data-draggable]:active{cursor:grabbing}.a-UL2G_cardDragging{opacity:.45;box-shadow:var(--dsw-shadow-lv2)}.a-UL2G_dropIndicator{background:var(--dsw-alias-state-business-primary);pointer-events:none;border-radius:999px;flex:none;height:3px;margin:0 2px}.a-UL2G_groupToggleButton:focus-visible,.a-UL2G_groupToggleButton:hover:not(:disabled){outline:none}.a-UL2G_groupToggleButton:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary)}.a-UL2G_statusDot[data-status=proposed]{background:var(--dsw-alias-brand-primary,#7c6cf0)}.a-UL2G_cardProposedBadge{color:var(--dsw-alias-label-primary-foreground,#fff);background:var(--dsw-alias-brand-primary,#7c6cf0);white-space:nowrap;border-radius:999px;flex:none;align-self:flex-start;padding:1px 8px;font-size:11px;font-weight:600;line-height:1.5}.a-UL2G_cardSource{color:var(--dsw-alias-label-tertiary);white-space:nowrap;flex:none;font-size:11px}.a-UL2G_entryBadge{text-align:center;min-width:18px;color:var(--dsw-alias-label-primary-foreground,#fff);background:var(--dsw-alias-state-error-primary,#e5484d);white-space:nowrap;border-radius:999px;flex:none;margin-left:auto;padding:0 6px;font-size:11px;font-weight:700;line-height:18px}.a-UL2G_entryBadge[data-count=\"0\"]{display:none}.a-UL2G_confirmTargetRow{flex-wrap:wrap;gap:8px;display:flex}.a-UL2G_confirmTargetButton{color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;white-space:nowrap;background:0 0;border-radius:8px;padding:6px 14px;font-family:inherit;font-size:13px}.a-UL2G_confirmTargetButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.a-UL2G_confirmTargetActive,.a-UL2G_confirmTargetActive:hover:not(:disabled){color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-button-info-fill);border-color:#0000;font-weight:600}.a-UL2G_confirmTargetButton:disabled{opacity:.45;cursor:default}.a-UL2G_sourceRow{color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;align-items:center;gap:8px;font-size:12px;display:flex}.a-UL2G_messageAction{z-index:20;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);box-shadow:var(--dsw-shadow-lv1);cursor:pointer;white-space:nowrap;border-radius:8px;padding:3px 9px;font-family:inherit;font-size:11px;font-weight:600;position:absolute;top:6px;right:8px}.a-UL2G_messageAction:hover{background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l3)}.a-UL2G_messageAction:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}";
1134
- const tagId = "@william2000/dsh-nova-ui-task-board/board.module.css";
1135
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1136
- const tag = document.createElement("style");
1137
- tag.dataset.plugin = "@william2000/dsh-nova-ui-task-board";
1138
- tag.dataset.pluginCss = tagId;
1139
- tag.textContent = css;
1140
- document.head.appendChild(tag);
1048
+ //#region src/client/grouping.ts
1049
+ /** 全部模式(顶部切换按钮按此顺序渲染)。 */
1050
+ const GROUP_MODES = [
1051
+ "manual",
1052
+ "project",
1053
+ "tag"
1054
+ ];
1055
+ /** 与 Host transitions.byOrderStable 一致的列内排序(order 升序,createdAt/id 决胜)。 */
1056
+ function byOrderStable(a, b) {
1057
+ return a.order - b.order || a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
1141
1058
  }
1142
- var board_module_css_default = {
1143
- "backButton": "a-UL2G_backButton",
1144
- "board": "a-UL2G_board",
1145
- "boardHeader": "a-UL2G_boardHeader",
1146
- "boardTitle": "a-UL2G_boardTitle",
1147
- "boardView": "a-UL2G_boardView",
1148
- "card": "a-UL2G_card",
1149
- "cardChips": "a-UL2G_cardChips",
1150
- "cardDragging": "a-UL2G_cardDragging",
1151
- "cardExcerpt": "a-UL2G_cardExcerpt",
1152
- "cardMeta": "a-UL2G_cardMeta",
1153
- "cardParent": "a-UL2G_cardParent",
1154
- "cardProject": "a-UL2G_cardProject",
1155
- "cardProposedBadge": "a-UL2G_cardProposedBadge",
1156
- "cardRun": "a-UL2G_cardRun",
1157
- "cardRunningLabel": "a-UL2G_cardRunningLabel",
1158
- "cardSchedule": "a-UL2G_cardSchedule",
1159
- "cardSession": "a-UL2G_cardSession",
1160
- "cardSource": "a-UL2G_cardSource",
1161
- "cardSpinner": "a-UL2G_cardSpinner",
1162
- "cardTag": "a-UL2G_cardTag",
1163
- "cardTagMore": "a-UL2G_cardTagMore",
1164
- "cardTime": "a-UL2G_cardTime",
1165
- "cardTitle": "a-UL2G_cardTitle",
1166
- "cards": "a-UL2G_cards",
1167
- "column": "a-UL2G_column",
1168
- "columnCount": "a-UL2G_columnCount",
1169
- "columnEmpty": "a-UL2G_columnEmpty",
1170
- "columnHeader": "a-UL2G_columnHeader",
1171
- "columnTitle": "a-UL2G_columnTitle",
1172
- "columns": "a-UL2G_columns",
1173
- "confirmMessage": "a-UL2G_confirmMessage",
1174
- "confirmTargetActive": "a-UL2G_confirmTargetActive",
1175
- "confirmTargetButton": "a-UL2G_confirmTargetButton",
1176
- "confirmTargetRow": "a-UL2G_confirmTargetRow",
1177
- "dangerButton": "a-UL2G_dangerButton",
1178
- "dangerGhostButton": "a-UL2G_dangerGhostButton",
1179
- "detail": "a-UL2G_detail",
1180
- "detailBody": "a-UL2G_detailBody",
1181
- "detailFooter": "a-UL2G_detailFooter",
1182
- "detailHeader": "a-UL2G_detailHeader",
1183
- "detailMeta": "a-UL2G_detailMeta",
1184
- "detailSection": "a-UL2G_detailSection",
1185
- "detailText": "a-UL2G_detailText",
1186
- "detailTitle": "a-UL2G_detailTitle",
1187
- "dropIndicator": "a-UL2G_dropIndicator",
1188
- "dshNovaTbSpin": "a-UL2G_dshNovaTbSpin",
1189
- "entry": "a-UL2G_entry",
1190
- "entryBadge": "a-UL2G_entryBadge",
1191
- "entryIcon": "a-UL2G_entryIcon",
1192
- "entryLabel": "a-UL2G_entryLabel",
1193
- "executionBadge": "a-UL2G_executionBadge",
1194
- "executionError": "a-UL2G_executionError",
1195
- "executionList": "a-UL2G_executionList",
1196
- "executionRow": "a-UL2G_executionRow",
1197
- "executionTimes": "a-UL2G_executionTimes",
1198
- "field": "a-UL2G_field",
1199
- "fieldLabel": "a-UL2G_fieldLabel",
1200
- "filterActions": "a-UL2G_filterActions",
1201
- "filterBar": "a-UL2G_filterBar",
1202
- "filterChip": "a-UL2G_filterChip",
1203
- "filterChipActive": "a-UL2G_filterChipActive",
1204
- "filterChips": "a-UL2G_filterChips",
1205
- "filterHitCount": "a-UL2G_filterHitCount",
1206
- "filterItem": "a-UL2G_filterItem",
1207
- "filterLabel": "a-UL2G_filterLabel",
1208
- "formError": "a-UL2G_formError",
1209
- "ghostButton": "a-UL2G_ghostButton",
1210
- "group": "a-UL2G_group",
1211
- "groupCards": "a-UL2G_groupCards",
1212
- "groupCount": "a-UL2G_groupCount",
1213
- "groupHeader": "a-UL2G_groupHeader",
1214
- "groupTitle": "a-UL2G_groupTitle",
1215
- "groupToggle": "a-UL2G_groupToggle",
1216
- "groupToggleActive": "a-UL2G_groupToggleActive",
1217
- "groupToggleButton": "a-UL2G_groupToggleButton",
1218
- "hostMeta": "a-UL2G_hostMeta",
1219
- "iconButton": "a-UL2G_iconButton",
1220
- "input": "a-UL2G_input",
1221
- "linkButton": "a-UL2G_linkButton",
1222
- "messageAction": "a-UL2G_messageAction",
1223
- "modal": "a-UL2G_modal",
1224
- "modalBackdrop": "a-UL2G_modalBackdrop",
1225
- "modalFooter": "a-UL2G_modalFooter",
1226
- "modalTitle": "a-UL2G_modalTitle",
1227
- "moveRow": "a-UL2G_moveRow",
1228
- "nova-task-board-view": "a-UL2G_nova-task-board-view",
1229
- "primaryButton": "a-UL2G_primaryButton",
1230
- "promptBlock": "a-UL2G_promptBlock",
1231
- "scheduleInput": "a-UL2G_scheduleInput",
1232
- "scheduleInputInvalid": "a-UL2G_scheduleInputInvalid",
1233
- "scheduleMeta": "a-UL2G_scheduleMeta",
1234
- "scheduleMode": "a-UL2G_scheduleMode",
1235
- "scheduleModeActive": "a-UL2G_scheduleModeActive",
1236
- "scheduleModeButton": "a-UL2G_scheduleModeButton",
1237
- "schedulePreset": "a-UL2G_schedulePreset",
1238
- "scheduleRow": "a-UL2G_scheduleRow",
1239
- "scheduleToggle": "a-UL2G_scheduleToggle",
1240
- "search": "a-UL2G_search",
1241
- "select": "a-UL2G_select",
1242
- "sourceRow": "a-UL2G_sourceRow",
1243
- "splitRow": "a-UL2G_splitRow",
1244
- "splitRowHeader": "a-UL2G_splitRowHeader",
1245
- "splitRowIndex": "a-UL2G_splitRowIndex",
1246
- "splitRows": "a-UL2G_splitRows",
1247
- "statusBadge": "a-UL2G_statusBadge",
1248
- "statusDot": "a-UL2G_statusDot",
1249
- "tagEditor": "a-UL2G_tagEditor",
1250
- "tagEditorChip": "a-UL2G_tagEditorChip",
1251
- "tagEditorRemove": "a-UL2G_tagEditorRemove"
1252
- };
1253
- //#endregion
1254
- //#region src/core/model.ts
1255
- /** 任务生命周期状态,与看板列一一对应(§10.1);`proposed` 为对话流转候选(D7/P3.1)。 */
1256
- const TASK_STATUSES = [
1257
- "proposed",
1258
- "backlog",
1259
- "todo",
1260
- "running",
1261
- "done",
1262
- "failed"
1263
- ];
1264
- /** 任务来源(§9.2,v1.0 字段,P3 随对话流转启用)。 */
1265
- const TASK_SOURCES = [
1266
- "manual",
1267
- "conversation",
1268
- "github_issue",
1269
- "bookmark_collector",
1270
- "feishu",
1271
- "requirement",
1272
- "other"
1273
- ];
1274
- /** 执行会话钉住的权限预设 id(`/permission <id>`,§9.2)。 */
1275
- const TASK_PERMISSIONS = [
1276
- "read-only",
1277
- "workspace-write",
1278
- "danger-full-access"
1279
- ];
1280
- /** 执行结果(§9.3)。 */
1281
- const EXECUTION_RESULTS = [
1282
- "succeeded",
1283
- "failed",
1284
- "cancelled"
1285
- ];
1286
- /** 评论类型(§9.5 comments:user_feedback/ai_log/system_event)。 */
1287
- const COMMENT_TYPES = [
1288
- "user_feedback",
1289
- "ai_log",
1290
- "system_event"
1291
- ];
1292
- /** 产物类型(§9.5 artifacts;会话 transcript 即默认产物 `session`)。 */
1293
- const ARTIFACT_TYPES = [
1294
- "session",
1295
- "link",
1296
- "file",
1297
- "other"
1298
- ];
1299
- /** 来源会话引用写入 metadata 的键(§9.2/§14.4:可跳回对话)。 */
1300
- const SOURCE_CONVERSATION_META_KEY = "sourceConversationId";
1301
- /** 有限数字守卫。 */
1302
- function isFiniteNumber(value) {
1303
- return typeof value === "number" && Number.isFinite(value);
1304
- }
1305
- /** 非空字符串(trim 后)守卫,空串/空白清除钉住字段(对齐参考实现 normalizeTargetId)。 */
1306
- function normalizeOptionalString(value) {
1307
- if (typeof value !== "string") return void 0;
1308
- const trimmed = value.trim();
1309
- return trimmed === "" ? void 0 : trimmed;
1310
- }
1311
- /** 是否为已知任务状态。 */
1312
- function isTaskStatus(value) {
1313
- return typeof value === "string" && TASK_STATUSES.includes(value);
1314
- }
1315
- /** 是否为已知任务来源。 */
1316
- function isTaskSource(value) {
1317
- return typeof value === "string" && TASK_SOURCES.includes(value);
1318
- }
1319
- /** 是否为已知权限预设。 */
1320
- function isTaskPermission(value) {
1321
- return typeof value === "string" && TASK_PERMISSIONS.includes(value);
1322
- }
1323
- /** 是否为已知执行结果。 */
1324
- function isExecutionResult(value) {
1325
- return typeof value === "string" && EXECUTION_RESULTS.includes(value);
1326
- }
1327
- /** 是否为已知评论类型。 */
1328
- function isCommentType(value) {
1329
- return typeof value === "string" && COMMENT_TYPES.includes(value);
1059
+ /** 列内任务按 order 升序排序(原数组不改动)。 */
1060
+ function sortByOrder(tasks) {
1061
+ return [...tasks].sort(byOrderStable);
1330
1062
  }
1331
- /** 是否为已知产物类型。 */
1332
- function isArtifactType(value) {
1333
- return typeof value === "string" && ARTIFACT_TYPES.includes(value);
1063
+ /** 任务在给定视图模式下的组 key(手动模式无组概念,返回 undefined)。 */
1064
+ function groupKeyOf(task, mode) {
1065
+ if (mode === "project") return task.project ?? "";
1066
+ if (mode === "tag") return task.tags.length > 0 ? task.tags[0] : "";
1334
1067
  }
1335
- /**
1336
- * cron 表达式的基础形状校验(§9.4:5 cron「分 时 日 月 周」)。
1337
- * 这是结构级校验:段数、字符集(数字、`* , - / ?` 及名字字母)。cron 的到期
1338
- * 计算与完整合法性由 T006 的调度器实现;本层只保证持久化的规则形状可解析。
1339
- */
1340
- const CRON_FIELD_RE = /^[0-9A-Za-z*,\-\/?]+$/;
1341
- function isPlausibleCron(cron) {
1342
- if (typeof cron !== "string" || cron.trim() === "") return false;
1343
- const fields = cron.trim().split(/\s+/);
1344
- if (fields.length !== 5) return false;
1345
- return fields.every((field) => CRON_FIELD_RE.test(field));
1068
+ /** 任务是否属于某个组(按模式解释 key:项目精确匹配 / 标签命中 / 未分类无归属)。 */
1069
+ function groupContains(task, groupKey, mode) {
1070
+ if (mode === "project") return groupKey === "" ? task.project === void 0 : task.project === groupKey;
1071
+ if (mode === "tag") return groupKey === "" ? task.tags.length === 0 : task.tags.includes(groupKey);
1072
+ return false;
1346
1073
  }
1347
1074
  /**
1348
- * 归一化一条持久化的执行安排(§9.4 判别联合):
1349
- * - `kind: 'one-shot'`one-shot 分支;`runAt` 非有限数则整条丢弃;
1350
- * - `kind: 'cron'` 或缺省 kind(v1/v2 旧形状)→ cron 分支;cron 形状非法则整条
1351
- * 丢弃(「修复或丢弃 schedule、绝不丢整行」——坏 schedule 不拖垮任务行);
1352
- * - 其他 kind 丢弃。
1075
+ * 拖拽目标组的归属变换(视图层预览用,与提交的 reorder 归属补丁一致,§12.10):
1076
+ * - 按项目分组:拖到组 Gproject = G;拖到「未分类」→ 清除 project;
1077
+ * - 按标签分组:拖到组 G G 成为首选标签(tags[0],G 已含于 tags 时仅调整顺序);
1078
+ * 拖到「未分类」→ 移除首选标签(tags.shift(),若 tags 为空则归「未分类」);
1079
+ * - 手动模式或非组落位原样返回。
1353
1080
  */
1354
- function normalizeSchedule(value) {
1355
- if (typeof value !== "object" || value === null) return void 0;
1356
- const rule = value;
1357
- if (rule.kind === "one-shot") {
1358
- if (!isFiniteNumber(rule.runAt)) return void 0;
1359
- const schedule = {
1360
- kind: "one-shot",
1361
- runAt: rule.runAt
1362
- };
1363
- if (isFiniteNumber(rule.firedAt)) schedule.firedAt = rule.firedAt;
1364
- return schedule;
1365
- }
1366
- if (rule.kind !== void 0 && rule.kind !== "cron") return void 0;
1367
- if (!isPlausibleCron(rule.cron)) return void 0;
1368
- const schedule = {
1369
- kind: "cron",
1370
- enabled: rule.enabled === true,
1371
- cron: rule.cron
1081
+ function applyOwnershipTransform(task, groupKey, mode) {
1082
+ if (mode === "manual" || groupKey === void 0) return task;
1083
+ if (mode === "project") return groupKey === "" ? {
1084
+ ...task,
1085
+ project: void 0
1086
+ } : {
1087
+ ...task,
1088
+ project: groupKey
1372
1089
  };
1373
- if (isFiniteNumber(rule.nextRunAt)) schedule.nextRunAt = rule.nextRunAt;
1374
- if (isFiniteNumber(rule.lastTriggeredAt)) schedule.lastTriggeredAt = rule.lastTriggeredAt;
1375
- return schedule;
1376
- }
1377
- /** 归一化一条执行记录(§9.3);结构非法返回 undefined。 */
1378
- function normalizeExecution(value) {
1379
- if (typeof value !== "object" || value === null) return void 0;
1380
- const entry = value;
1381
- if (typeof entry.id !== "string" || entry.id === "") return void 0;
1382
- if (!isFiniteNumber(entry.startedAt)) return void 0;
1383
- if (entry.sessionId !== void 0 && typeof entry.sessionId !== "string") return void 0;
1384
- if (entry.endedAt !== void 0 && !isFiniteNumber(entry.endedAt)) return void 0;
1385
- if (entry.result !== void 0 && !isExecutionResult(entry.result)) return void 0;
1386
- if (entry.error !== void 0 && typeof entry.error !== "string") return void 0;
1387
- const execution = {
1388
- id: entry.id,
1389
- startedAt: entry.startedAt
1090
+ if (groupKey === "") return {
1091
+ ...task,
1092
+ tags: task.tags.slice(1)
1093
+ };
1094
+ return {
1095
+ ...task,
1096
+ tags: [groupKey, ...task.tags.filter((tag) => tag !== groupKey)]
1390
1097
  };
1391
- if (typeof entry.sessionId === "string") execution.sessionId = entry.sessionId;
1392
- if (isFiniteNumber(entry.endedAt)) execution.endedAt = entry.endedAt;
1393
- if (isExecutionResult(entry.result)) execution.result = entry.result;
1394
- if (typeof entry.error === "string") execution.error = entry.error;
1395
- return execution;
1396
1098
  }
1397
1099
  /**
1398
- * 归一化标签数组(§9.2 默认 []):仅保留字符串元素,逐项 trim、丢弃空白项、
1399
- * 按首次出现顺序去重;缺省/非法 → []。标签是展示/过滤维度的原始字符串,
1400
- * 大小写敏感(`API` 与 `api` 是两个标签),去重不折叠大小写。
1100
+ * 分组(§12.10):项目/标签模式下的归组展示,组内按 order 升序;标签模式多归组
1101
+ * (一个任务可出现在其全部标签组),无标签归「未分类」;未分类组恒在最后。
1102
+ * 手动模式不分组(调用方直接 sortByOrder)。
1401
1103
  */
1402
- function normalizeTags(value) {
1403
- if (!Array.isArray(value)) return [];
1404
- const seen = /* @__PURE__ */ new Set();
1405
- const tags = [];
1406
- for (const tag of value) {
1407
- if (typeof tag !== "string") continue;
1408
- const trimmed = tag.trim();
1409
- if (trimmed === "" || seen.has(trimmed)) continue;
1410
- seen.add(trimmed);
1411
- tags.push(trimmed);
1412
- }
1413
- return tags;
1414
- }
1415
- /** 通用字符串数组归一化(trim、丢空白、首现去重、条目长度封顶、数量封顶)。 */
1416
- function normalizeStringList(value, maxItems, maxLength) {
1417
- if (!Array.isArray(value)) return [];
1418
- const seen = /* @__PURE__ */ new Set();
1419
- const items = [];
1420
- for (const item of value) {
1421
- if (typeof item !== "string") continue;
1422
- const trimmed = item.trim().slice(0, maxLength);
1423
- if (trimmed === "" || seen.has(trimmed)) continue;
1424
- seen.add(trimmed);
1425
- items.push(trimmed);
1426
- if (items.length >= maxItems) break;
1104
+ function groupTasks(tasks, mode) {
1105
+ if (mode === "project") {
1106
+ const byProject = /* @__PURE__ */ new Map();
1107
+ for (const task of tasks) {
1108
+ const key = task.project ?? "";
1109
+ const list = byProject.get(key);
1110
+ if (list === void 0) byProject.set(key, [task]);
1111
+ else list.push(task);
1112
+ }
1113
+ return groupsFromMap(byProject);
1427
1114
  }
1428
- return items;
1115
+ const byTag = /* @__PURE__ */ new Map();
1116
+ const ungrouped = [];
1117
+ for (const task of tasks) {
1118
+ if (task.tags.length === 0) {
1119
+ ungrouped.push(task);
1120
+ continue;
1121
+ }
1122
+ for (const tag of task.tags) {
1123
+ const list = byTag.get(tag);
1124
+ if (list === void 0) byTag.set(tag, [task]);
1125
+ else list.push(task);
1126
+ }
1127
+ }
1128
+ return [...groupsFromMap(byTag), ...ungrouped.length > 0 ? [{
1129
+ key: "",
1130
+ label: "",
1131
+ tasks: sortByOrder(ungrouped)
1132
+ }] : []];
1429
1133
  }
1430
- /** 归一化来源元数据(§14.1/14.2):仅保留字符串键值对;缺省/非法 undefined。 */
1431
- function normalizeMetadata(value) {
1432
- if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
1433
- const metadata = {};
1434
- for (const [key, entry] of Object.entries(value)) {
1435
- if (typeof entry !== "string" || key.trim() === "") continue;
1436
- metadata[key.trim().slice(0, 64)] = entry.slice(0, 2048);
1134
+ /** label(zh locale)排序的组列表;「未分类」组(key 空串)恒在最后。 */
1135
+ function groupsFromMap(map) {
1136
+ const groups = [];
1137
+ for (const [key, list] of map) {
1138
+ if (key === "") continue;
1139
+ groups.push({
1140
+ key,
1141
+ label: key,
1142
+ tasks: sortByOrder(list)
1143
+ });
1437
1144
  }
1438
- return Object.keys(metadata).length > 0 ? metadata : void 0;
1145
+ groups.sort((a, b) => a.label.localeCompare(b.label, "zh"));
1146
+ const ungrouped = map.get("");
1147
+ if (ungrouped !== void 0) groups.push({
1148
+ key: "",
1149
+ label: "",
1150
+ tasks: sortByOrder(ungrouped)
1151
+ });
1152
+ return groups;
1439
1153
  }
1440
- /** 归一化一条评论(§9.5);结构非法返回 undefined。 */
1441
- function normalizeComment(value) {
1442
- if (typeof value !== "object" || value === null) return void 0;
1443
- const entry = value;
1444
- if (typeof entry.id !== "string" || entry.id === "") return void 0;
1445
- if (typeof entry.body !== "string" || entry.body === "") return void 0;
1446
- if (!isFiniteNumber(entry.createdAt)) return void 0;
1447
- if (entry.author !== void 0 && typeof entry.author !== "string") return void 0;
1448
- if (entry.type !== void 0 && !isCommentType(entry.type)) return void 0;
1449
- return {
1450
- id: entry.id,
1451
- author: typeof entry.author === "string" && entry.author.trim() !== "" ? entry.author.slice(0, 64) : "user",
1452
- body: entry.body.slice(0, 4e3),
1453
- type: isCommentType(entry.type) ? entry.type : "user_feedback",
1454
- createdAt: entry.createdAt
1455
- };
1154
+ //#endregion
1155
+ //#region src/client/drag-utils.ts
1156
+ /**
1157
+ * 拖拽合法性(§10.2/§11.2):running 不可作目标;同列 = 列内排序;跨列目标仅限
1158
+ * backlog/todo(done/failed 拖回为重开)。proposed/归档不可作为来源由调用方
1159
+ * (卡片 draggable=false)保证。
1160
+ */
1161
+ function isLegalDrop(origin, targetStatus) {
1162
+ if (targetStatus === "running") return false;
1163
+ if (targetStatus === origin.fromStatus) return true;
1164
+ return targetStatus === "backlog" || targetStatus === "todo";
1456
1165
  }
1457
- /** 归一化评论集合(§9.5 comments,默认 []);非法条目丢弃。 */
1458
- function normalizeComments(value) {
1459
- if (!Array.isArray(value)) return [];
1460
- const comments = [];
1461
- for (const entry of value) {
1462
- const comment = normalizeComment(entry);
1463
- if (comment !== void 0) comments.push(comment);
1166
+ /** 目标组尾在基准列(按 order 升序)中的最后一张卡的下标;组空返回 -1。 */
1167
+ function lastGroupIndex(column, groupKey, mode) {
1168
+ let last = -1;
1169
+ for (let index = 0; index < column.length; index += 1) if (groupContains(column[index], groupKey, mode)) last = index;
1170
+ return last;
1171
+ }
1172
+ /**
1173
+ * 计算落位下标(0-based,基准 = 剔除拖拽卡片后的目标列,按 order 稳定升序;
1174
+ * 与 Host applyReposition 的 insert 基准一致):
1175
+ * - 锚定卡片:卡片下标(after +1);
1176
+ * - 分组视图组尾:目标组最后一张卡下标 +1(组空回退列尾);
1177
+ * - 列尾:可见列表最后一张卡下标 +1(过滤定位;无可见卡则列尾)。
1178
+ */
1179
+ function computeDropIndex(restColumn, target, mode, lastVisibleId) {
1180
+ if (target.anchorId !== void 0) {
1181
+ const anchor = restColumn.findIndex((task) => task.id === target.anchorId);
1182
+ if (anchor !== -1) return anchor + (target.position === "after" ? 1 : 0);
1464
1183
  }
1465
- return comments;
1184
+ if (target.groupKey !== void 0 && mode !== "manual") {
1185
+ const last = lastGroupIndex(restColumn, target.groupKey, mode);
1186
+ if (last !== -1) return last + 1;
1187
+ }
1188
+ if (lastVisibleId !== void 0) {
1189
+ const index = restColumn.findIndex((task) => task.id === lastVisibleId);
1190
+ if (index !== -1) return index + 1;
1191
+ }
1192
+ return restColumn.length;
1466
1193
  }
1467
- /** 归一化一条产物(§9.5 artifacts);结构非法返回 undefined。 */
1468
- function normalizeArtifact(value) {
1469
- if (typeof value !== "object" || value === null) return void 0;
1470
- const entry = value;
1471
- if (typeof entry.id !== "string" || entry.id === "") return void 0;
1472
- if (!isArtifactType(entry.type)) return void 0;
1473
- if (typeof entry.title !== "string" || entry.title === "") return void 0;
1474
- if (!isFiniteNumber(entry.createdAt)) return void 0;
1475
- if (entry.url !== void 0 && typeof entry.url !== "string") return void 0;
1476
- if (entry.contentRef !== void 0 && typeof entry.contentRef !== "string") return void 0;
1477
- const artifact = {
1478
- id: entry.id,
1479
- type: entry.type,
1480
- title: entry.title.slice(0, 500),
1481
- createdAt: entry.createdAt
1194
+ /**
1195
+ * 可见列表内的落位下标(拖拽 pending 渲染用;list = 目标列/目标组的可见列表,
1196
+ * **已剔除拖拽卡片**):锚定卡片 before/after,否则列表尾。
1197
+ */
1198
+ function visibleDropIndex(list, target) {
1199
+ if (target.anchorId !== void 0) {
1200
+ const index = list.findIndex((task) => task.id === target.anchorId);
1201
+ if (index !== -1) return Math.min(index + (target.position === "after" ? 1 : 0), list.length);
1202
+ }
1203
+ return list.length;
1204
+ }
1205
+ /**
1206
+ * 构建拖拽提交的 action(§11.2/D10):
1207
+ * - 同列 → `reorder`(status 与来源一致;分组视图跨组落位携带归属变更——
1208
+ * 项目模式 project = 组名 / ''(清除);标签模式 tags 使目标标签成为首选或
1209
+ * 移除首选(未分类));
1210
+ * - 跨列 → `move`(status 为目标列 + 落位下标,缺省语义由 Host 追加末尾;
1211
+ * done/failed 重开不触发执行)。
1212
+ * 非法目标/拖到自己返回 undefined(调用方提示拒绝)。
1213
+ */
1214
+ function buildDropAction(origin, target, mode, tasks, lastVisibleId) {
1215
+ if (!isLegalDrop(origin, target.status)) return void 0;
1216
+ if (target.anchorId === origin.taskId) return void 0;
1217
+ const task = tasks.find((candidate) => candidate.id === origin.taskId);
1218
+ if (task === void 0) return void 0;
1219
+ const index = computeDropIndex(sortByOrder(tasks.filter((candidate) => candidate.status === target.status)).filter((candidate) => candidate.id !== origin.taskId), target, mode, lastVisibleId);
1220
+ if (target.status === origin.fromStatus) {
1221
+ let project;
1222
+ let tags;
1223
+ if (mode === "project" && target.groupKey !== void 0 && target.groupKey !== origin.fromGroupKey) project = target.groupKey === "" ? "" : target.groupKey;
1224
+ else if (mode === "tag" && target.groupKey !== void 0 && target.groupKey !== origin.fromGroupKey) tags = target.groupKey === "" ? task.tags.slice(1) : [target.groupKey, ...task.tags.filter((tag) => tag !== target.groupKey)];
1225
+ return {
1226
+ kind: "reorder",
1227
+ taskId: origin.taskId,
1228
+ status: target.status,
1229
+ order: index,
1230
+ ...project !== void 0 ? { project } : {},
1231
+ ...tags !== void 0 ? { tags } : {}
1232
+ };
1233
+ }
1234
+ return {
1235
+ kind: "move",
1236
+ taskId: origin.taskId,
1237
+ status: target.status,
1238
+ order: index
1482
1239
  };
1483
- if (typeof entry.url === "string" && entry.url.trim() !== "") artifact.url = entry.url.slice(0, 2048);
1484
- if (typeof entry.contentRef === "string" && entry.contentRef.trim() !== "") artifact.contentRef = entry.contentRef.slice(0, 512);
1485
- return artifact;
1486
1240
  }
1487
- /** 归一化产物集合(§9.5 artifacts,默认 []);非法条目丢弃。 */
1488
- function normalizeArtifacts(value) {
1489
- if (!Array.isArray(value)) return [];
1490
- const artifacts = [];
1491
- for (const entry of value) {
1492
- const artifact = normalizeArtifact(entry);
1493
- if (artifact !== void 0) artifacts.push(artifact);
1241
+ //#endregion
1242
+ //#region src/client/filter.ts
1243
+ /** 空过滤(看板初始状态)。 */
1244
+ const EMPTY_FILTER = {
1245
+ query: "",
1246
+ project: { kind: "all" },
1247
+ tags: []
1248
+ };
1249
+ /** 单条任务是否命中过滤(多条件 AND、标签内 OR)。 */
1250
+ function matchesFilter(task, filter) {
1251
+ const query = filter.query.trim().toLowerCase();
1252
+ if (query !== "" && !task.title.toLowerCase().includes(query) && !task.description.toLowerCase().includes(query)) return false;
1253
+ if (filter.project.kind === "none") {
1254
+ if (task.project !== void 0) return false;
1255
+ } else if (filter.project.kind === "name") {
1256
+ if (task.project !== filter.project.name) return false;
1494
1257
  }
1495
- return artifacts;
1258
+ if (filter.tags.length > 0 && !filter.tags.some((tag) => task.tags.includes(tag))) return false;
1259
+ return true;
1260
+ }
1261
+ /** 对任务集执行过滤(保持入参顺序与对象引用,memo 边界不受影响)。 */
1262
+ function filterTasks(tasks, filter) {
1263
+ return tasks.filter((task) => matchesFilter(task, filter));
1264
+ }
1265
+ /** 过滤是否处于激活状态(任一维度有约束;用于「清除全部」显隐与命中计数)。 */
1266
+ function isFilterActive(filter) {
1267
+ return filter.query.trim() !== "" || filter.project.kind !== "all" || filter.tags.length > 0;
1268
+ }
1269
+ /** 从任务集推导去重后的项目名列表(过滤下拉选项),按 zh locale 排序。 */
1270
+ function distinctProjects(tasks) {
1271
+ const set = /* @__PURE__ */ new Set();
1272
+ for (const task of tasks) if (task.project !== void 0) set.add(task.project);
1273
+ return [...set].sort((a, b) => a.localeCompare(b, "zh"));
1274
+ }
1275
+ /** 从任务集推导去重后的标签列表(多选 chips 选项),按 zh locale 排序。 */
1276
+ function distinctTags(tasks) {
1277
+ const set = /* @__PURE__ */ new Set();
1278
+ for (const task of tasks) for (const tag of task.tags) set.add(tag);
1279
+ return [...set].sort((a, b) => a.localeCompare(b, "zh"));
1280
+ }
1281
+ //#endregion
1282
+ //#region src/client/locales.ts
1283
+ /**
1284
+ * Nova task-board copy: zh-first dictionaries with an English fallback,
1285
+ * selected by the document language. Kept dependency-free (no dsh locale
1286
+ * service) so the DOM-injected entry row and the standalone board tree share
1287
+ * one tiny lookup. Key set 以 zh 为准,en 对齐(对齐参考实现的组织方式)。
1288
+ *
1289
+ * T004 补全看板/详情/新建/状态/执行设置文案;定时编辑器(T006/T007)与
1290
+ * 待确认列(T011)的编辑/确认文案随对应任务补充。
1291
+ */
1292
+ /** zh dictionary (key-set source of truth). */
1293
+ const zh = {
1294
+ "entry.label": "Nova 任务看板",
1295
+ "board.title": "Nova 任务看板",
1296
+ "board.close": "返回会话",
1297
+ "board.new": "新建任务",
1298
+ "board.search": "筛选任务…",
1299
+ "board.empty": "这个状态还没有任务",
1300
+ "board.archive": "归档",
1301
+ "board.archiveView": "归档 ({count})",
1302
+ "board.backToBoard": "返回看板",
1303
+ "archive.empty": "没有已归档的任务",
1304
+ "board.status": "状态",
1305
+ "board.status.proposed": "待确认",
1306
+ "board.status.backlog": "待规划",
1307
+ "board.status.todo": "待办",
1308
+ "board.status.running": "进行中",
1309
+ "board.status.done": "已完成",
1310
+ "board.status.failed": "已失败",
1311
+ "board.runs": "次执行",
1312
+ "board.pending": "正在提交",
1313
+ "board.updated": "更新于",
1314
+ "board.created": "创建于",
1315
+ "board.hostError": "Host 操作失败:{error}",
1316
+ "board.retryHost": "重试连接 Host",
1317
+ "board.hostMeta": "Host 时区 {timeZone} · revision {revision}",
1318
+ "new.title": "标题",
1319
+ "new.titlePlaceholder": "一句话描述要做什么",
1320
+ "new.description": "描述",
1321
+ "new.descriptionPlaceholder": "补充背景、范围与验收(可选)",
1322
+ "new.prompt": "执行 Prompt",
1323
+ "new.promptPlaceholder": "发给 agent 的完整指令(留空则使用标题)",
1324
+ "new.submit": "创建",
1325
+ "new.cancel": "取消",
1326
+ "new.required": "标题不能为空",
1327
+ "detail.title": "任务详情",
1328
+ "detail.close": "关闭",
1329
+ "detail.prompt": "执行 Prompt",
1330
+ "detail.description": "描述",
1331
+ "detail.execution": "执行记录",
1332
+ "detail.noExecution": "尚未执行",
1333
+ "detail.run": "执行",
1334
+ "detail.rerun": "重新执行",
1335
+ "detail.stop": "停止执行",
1336
+ "detail.delete": "删除",
1337
+ "detail.archive": "归档",
1338
+ "detail.restore": "恢复",
1339
+ "detail.archivedAt": "已归档 · {time}",
1340
+ "detail.viewSession": "查看会话",
1341
+ "detail.executionStarted": "已启动",
1342
+ "detail.executionEnded": "已结束",
1343
+ "detail.result.succeeded": "成功",
1344
+ "detail.result.failed": "失败",
1345
+ "detail.result.cancelled": "已取消",
1346
+ "detail.result.running": "进行中",
1347
+ "delete.title": "删除任务",
1348
+ "delete.confirm": "确定删除「{name}」吗?删除后不可恢复。",
1349
+ "delete.ok": "删除",
1350
+ "delete.cancel": "取消",
1351
+ "stop.title": "停止执行",
1352
+ "stop.confirm": "确定停止「{name}」的执行吗?执行将结算为已取消,任务回到待办。",
1353
+ "stop.ok": "停止",
1354
+ "status.move.backlog": "移到待规划",
1355
+ "status.move.todo": "移到待办",
1356
+ "time.justNow": "刚刚",
1357
+ "card.scheduled": "定时",
1358
+ "card.oneShot": "计划",
1359
+ "detail.schedule": "执行安排",
1360
+ "detail.schedule.nextRun": "下次运行",
1361
+ "detail.schedule.lastTriggered": "上次运行",
1362
+ "detail.schedule.notScheduled": "未安排",
1363
+ "detail.schedule.dueSoon": "即将运行",
1364
+ "detail.schedule.enable": "启用定时执行",
1365
+ "detail.schedule.cron": "cron 表达式",
1366
+ "detail.schedule.invalid": "cron 表达式不合法",
1367
+ "detail.schedule.presets": "预设",
1368
+ "detail.schedule.preset.daily9": "每天 09:00",
1369
+ "detail.schedule.preset.hourly": "每小时整点",
1370
+ "detail.schedule.preset.tenMin": "每 10 分钟",
1371
+ "detail.schedule.preset.weeklyMon9": "每周一 09:00",
1372
+ "detail.schedule.modeCron": "周期定时",
1373
+ "detail.schedule.modeOneShot": "一次性计划",
1374
+ "detail.oneShot.runAt": "计划时刻",
1375
+ "detail.oneShot.remaining": "剩余",
1376
+ "detail.oneShot.cancel": "取消计划",
1377
+ "detail.oneShot.executed": "已执行 · {time}",
1378
+ "detail.oneShot.skipped": "已过期/已跳过",
1379
+ "detail.oneShot.reschedule": "可重新设定",
1380
+ "detail.oneShot.invalid": "计划时刻不合法",
1381
+ "detail.oneShot.dueSoon": "即将触发",
1382
+ "detail.oneShot.preset.tenMin": "10 分钟后",
1383
+ "detail.oneShot.preset.tonight21": "今晚 21:00",
1384
+ "detail.oneShot.preset.tomorrow9": "明天 09:00",
1385
+ "detail.executionSettings": "执行设置",
1386
+ "exec.hint": "执行时生效:工作区决定执行会话落在哪个工作区;模式决定会话的 agent 预设;权限经 /permission 命令应用到会话。留空则使用运行时默认。",
1387
+ "new.workspace": "工作区",
1388
+ "new.mode": "模式",
1389
+ "new.permission": "权限",
1390
+ "exec.workspace.recent": "最近使用(默认)",
1391
+ "exec.mode.default": "部署默认",
1392
+ "exec.mode.defaultSuffix": "(默认)",
1393
+ "exec.mode.brokenSuffix": "(不可用)",
1394
+ "exec.mode.removed": "(已移除)",
1395
+ "exec.permission.default": "会话默认",
1396
+ "exec.permission.read-only": "只读",
1397
+ "exec.permission.workspace-write": "工作区可写",
1398
+ "exec.permission.danger-full-access": "完全访问",
1399
+ "filter.project": "项目",
1400
+ "filter.projectAll": "全部项目",
1401
+ "filter.projectNone": "未分类(无项目)",
1402
+ "filter.tags": "标签",
1403
+ "filter.clearAll": "清除全部",
1404
+ "filter.hits": "命中 {count}",
1405
+ "filter.removeTag": "删除标签 {tag}",
1406
+ "board.group.label": "排序/分组",
1407
+ "board.group.manual": "手动顺序",
1408
+ "board.group.project": "按项目分组",
1409
+ "board.group.tag": "按标签分组",
1410
+ "board.group.ungrouped": "未分类",
1411
+ "board.dragRejected": "不能拖拽到该列(running 等非法目标)",
1412
+ "detail.projectTags": "项目与标签",
1413
+ "detail.projectPlaceholder": "所属项目(可留空)",
1414
+ "new.tagsPlaceholder": "输入标签后回车",
1415
+ "confirm.title": "确认候选任务",
1416
+ "confirm.subtitle": "候选任务未确认前不可执行、不可定时、不可归档。可编辑后选择落列确认,或拒绝删除。",
1417
+ "confirm.target": "确认后落列",
1418
+ "confirm.toBacklog": "待规划(backlog)",
1419
+ "confirm.toTodo": "待办(todo)",
1420
+ "confirm.submit": "确认",
1421
+ "confirm.dismiss": "拒绝",
1422
+ "confirm.dismissTitle": "拒绝候选任务",
1423
+ "confirm.dismissMessage": "确定拒绝「{name}」吗?该候选任务将被删除。",
1424
+ "card.proposedBadge": "待确认",
1425
+ "card.source": "来源",
1426
+ "source.conversation": "对话",
1427
+ "source.requirement": "需求拆分",
1428
+ "entry.proposedCount": "{count} 个候选待确认",
1429
+ "convert.title": "转为任务",
1430
+ "convert.fromConversation": "从对话提取",
1431
+ "convert.submitProposed": "创建为候选",
1432
+ "convert.submitDirect": "直接创建",
1433
+ "convert.directTarget": "落列",
1434
+ "convert.directBacklog": "待规划",
1435
+ "convert.directTodo": "待办",
1436
+ "convert.proposedHint": "创建为候选:进入看板「待确认」列,确认后方可执行。",
1437
+ "chat.extractedHint": "已从对话提取 {count} 个候选任务(看板「待确认」列),可随时确认或拒绝。",
1438
+ "detail.jumpToConversation": "跳回对话",
1439
+ "detail.sourceConversation": "来源会话",
1440
+ "split.entry": "从需求拆分",
1441
+ "split.subtitle": "提交一份需求文档(粘贴文本或工作区文件路径),系统将主动拆分为一组可独立执行/验收的子任务候选,进入「待确认」列;确认后子任务可独立执行并保留父需求引用。",
1442
+ "split.recursiveTitle": "递归拆分",
1443
+ "split.recursiveHint": "将针对该任务继续拆分:新建父需求任务「需求:<标题>」挂到其下,子任务再指向新父任务。",
1444
+ "split.requirementTitle": "需求标题",
1445
+ "split.requirementTitlePlaceholder": "如「XX 系统登录模块」",
1446
+ "split.source": "需求来源",
1447
+ "split.sourceText": "粘贴文本",
1448
+ "split.sourceFile": "工作区文件路径",
1449
+ "split.text": "需求文本",
1450
+ "split.textPlaceholder": "粘贴需求文档内容…",
1451
+ "split.file": "文件路径",
1452
+ "split.filePlaceholder": "相对工作区的文件路径,如 docs/requirement.md",
1453
+ "split.required": "请填写需求标题与需求内容(文本或文件路径)",
1454
+ "split.submit": "发起拆分",
1455
+ "split.asyncHint": "提交后在独立会话中拆分,完成后进入「待确认」列(未确认前不可执行)。",
1456
+ "split.batchButton": "批量确认",
1457
+ "split.batchTitle": "批量确认候选任务",
1458
+ "split.batchSubtitle": "逐条编辑(标题/描述/Prompt/项目/标签)、增删条目;或批量确认到所选列、整体拒绝。",
1459
+ "split.addItem": "添加条目",
1460
+ "split.added": "新增",
1461
+ "split.removeItem": "移除",
1462
+ "split.confirmAll": "确认全部到{target}",
1463
+ "split.dismissAll": "全部拒绝",
1464
+ "split.dismissAllTitle": "拒绝全部候选",
1465
+ "split.dismissAllMessage": "确定拒绝全部 {count} 个候选任务吗?将被删除。",
1466
+ "split.dismissItemTitle": "移除候选任务",
1467
+ "split.dismissItemMessage": "确定移除「{name}」吗?该候选任务将被删除。",
1468
+ "split.empty": "没有可批量确认的候选任务",
1469
+ "split.parentOf": "所属需求",
1470
+ "split.parentMissing": "(父任务已删除)",
1471
+ "split.children": "子任务",
1472
+ "settings.title": "Nova 任务看板",
1473
+ "settings.description": "控制 Host Nova 任务看板与 agent 播报。",
1474
+ "settings.enabled": "启用 Nova 任务看板",
1475
+ "settings.enabledHint": "关闭后隐藏侧边栏入口与看板视图。",
1476
+ "settings.announceToAgent": "向 agent 播报 Nova 任务看板",
1477
+ "settings.announceToAgentHint": "开启:每条 agent 系统提示都会包含本看板的说明;关闭:不播报。",
1478
+ "settings.notExposed": "当前 DSH 版本未向设置页暴露本插件的配置命名空间,表单不可用。可编辑 ~/.dsh/settings.yaml 直接配置。",
1479
+ "settings.readOnly": "当前部署的设置只读。"
1480
+ };
1481
+ /** en dictionary, complete against the zh key set. */
1482
+ const en = {
1483
+ "entry.label": "Nova Task Board",
1484
+ "board.title": "Nova Task Board",
1485
+ "board.close": "Back to chat",
1486
+ "board.new": "New Task",
1487
+ "board.search": "Filter tasks…",
1488
+ "board.empty": "No tasks in this column",
1489
+ "board.archive": "Archive",
1490
+ "board.archiveView": "Archived ({count})",
1491
+ "board.backToBoard": "Back to board",
1492
+ "archive.empty": "No archived tasks",
1493
+ "board.status": "Status",
1494
+ "board.status.proposed": "Proposed",
1495
+ "board.status.backlog": "Backlog",
1496
+ "board.status.todo": "To Do",
1497
+ "board.status.running": "In Progress",
1498
+ "board.status.done": "Done",
1499
+ "board.status.failed": "Failed",
1500
+ "board.runs": "runs",
1501
+ "board.pending": "Submitting",
1502
+ "board.updated": "Updated",
1503
+ "board.created": "Created",
1504
+ "board.hostError": "Host action failed: {error}",
1505
+ "board.retryHost": "Retry Host connection",
1506
+ "board.hostMeta": "Host time zone {timeZone} · revision {revision}",
1507
+ "new.title": "Title",
1508
+ "new.titlePlaceholder": "What should be done, in one line",
1509
+ "new.description": "Description",
1510
+ "new.descriptionPlaceholder": "Background, scope, acceptance criteria (optional)",
1511
+ "new.prompt": "Run Prompt",
1512
+ "new.promptPlaceholder": "The full instruction sent to the agent (title is used when blank)",
1513
+ "new.submit": "Create",
1514
+ "new.cancel": "Cancel",
1515
+ "new.required": "Title is required",
1516
+ "detail.title": "Task Detail",
1517
+ "detail.close": "Close",
1518
+ "detail.prompt": "Run Prompt",
1519
+ "detail.description": "Description",
1520
+ "detail.execution": "Execution History",
1521
+ "detail.noExecution": "Not executed yet",
1522
+ "detail.run": "Run",
1523
+ "detail.rerun": "Run Again",
1524
+ "detail.stop": "Stop",
1525
+ "detail.delete": "Delete",
1526
+ "detail.archive": "Archive",
1527
+ "detail.restore": "Restore",
1528
+ "detail.archivedAt": "Archived · {time}",
1529
+ "detail.viewSession": "View Session",
1530
+ "detail.executionStarted": "Started",
1531
+ "detail.executionEnded": "Ended",
1532
+ "detail.result.succeeded": "Succeeded",
1533
+ "detail.result.failed": "Failed",
1534
+ "detail.result.cancelled": "Cancelled",
1535
+ "detail.result.running": "Running",
1536
+ "delete.title": "Delete Task",
1537
+ "delete.confirm": "Delete \"{name}\"? This cannot be undone.",
1538
+ "delete.ok": "Delete",
1539
+ "delete.cancel": "Cancel",
1540
+ "stop.title": "Stop Execution",
1541
+ "stop.confirm": "Stop execution of \"{name}\"? It will be settled as cancelled and the task returns to To Do.",
1542
+ "stop.ok": "Stop",
1543
+ "status.move.backlog": "Move to Backlog",
1544
+ "status.move.todo": "Move to To Do",
1545
+ "time.justNow": "just now",
1546
+ "card.scheduled": "scheduled",
1547
+ "card.oneShot": "planned",
1548
+ "detail.schedule": "Schedule",
1549
+ "detail.schedule.nextRun": "Next run",
1550
+ "detail.schedule.lastTriggered": "Last run",
1551
+ "detail.schedule.notScheduled": "Not scheduled",
1552
+ "detail.schedule.dueSoon": "Due soon",
1553
+ "detail.schedule.enable": "Enable scheduled runs",
1554
+ "detail.schedule.cron": "cron expression",
1555
+ "detail.schedule.invalid": "Invalid cron expression",
1556
+ "detail.schedule.presets": "Presets",
1557
+ "detail.schedule.preset.daily9": "Daily 09:00",
1558
+ "detail.schedule.preset.hourly": "Every hour",
1559
+ "detail.schedule.preset.tenMin": "Every 10 min",
1560
+ "detail.schedule.preset.weeklyMon9": "Mon 09:00",
1561
+ "detail.schedule.modeCron": "Recurring",
1562
+ "detail.schedule.modeOneShot": "One-shot",
1563
+ "detail.oneShot.runAt": "Run at",
1564
+ "detail.oneShot.remaining": "in",
1565
+ "detail.oneShot.cancel": "Cancel plan",
1566
+ "detail.oneShot.executed": "Executed · {time}",
1567
+ "detail.oneShot.skipped": "Expired / skipped",
1568
+ "detail.oneShot.reschedule": "reschedule",
1569
+ "detail.oneShot.invalid": "Invalid plan time",
1570
+ "detail.oneShot.dueSoon": "Due soon",
1571
+ "detail.oneShot.preset.tenMin": "In 10 minutes",
1572
+ "detail.oneShot.preset.tonight21": "Tonight 21:00",
1573
+ "detail.oneShot.preset.tomorrow9": "Tomorrow 09:00",
1574
+ "detail.executionSettings": "Execution Settings",
1575
+ "exec.hint": "Applied when the task runs: the workspace decides where the execution session lands; the mode composes the session's agent preset; the permission is applied through the /permission command. Blank = runtime default.",
1576
+ "new.workspace": "Workspace",
1577
+ "new.mode": "Mode",
1578
+ "new.permission": "Permission",
1579
+ "exec.workspace.recent": "Most recent (default)",
1580
+ "exec.mode.default": "Deployment default",
1581
+ "exec.mode.defaultSuffix": " (default)",
1582
+ "exec.mode.brokenSuffix": " (unavailable)",
1583
+ "exec.mode.removed": " (removed)",
1584
+ "exec.permission.default": "Session default",
1585
+ "exec.permission.read-only": "Read-only",
1586
+ "exec.permission.workspace-write": "Workspace Write",
1587
+ "exec.permission.danger-full-access": "Full Access",
1588
+ "filter.project": "Project",
1589
+ "filter.projectAll": "All projects",
1590
+ "filter.projectNone": "Unassigned",
1591
+ "filter.tags": "Tags",
1592
+ "filter.clearAll": "Clear all",
1593
+ "filter.hits": "{count} hits",
1594
+ "filter.removeTag": "Remove tag {tag}",
1595
+ "board.group.label": "Sort & group",
1596
+ "board.group.manual": "Manual",
1597
+ "board.group.project": "By project",
1598
+ "board.group.tag": "By tag",
1599
+ "board.group.ungrouped": "Unassigned",
1600
+ "board.dragRejected": "Cannot drop here (illegal target)",
1601
+ "detail.projectTags": "Project & Tags",
1602
+ "detail.projectPlaceholder": "Project (optional)",
1603
+ "new.tagsPlaceholder": "Type a tag and press Enter",
1604
+ "confirm.title": "Confirm candidate task",
1605
+ "confirm.subtitle": "Candidates cannot run, be scheduled, or be archived until confirmed. Edit and pick a target column, or dismiss.",
1606
+ "confirm.target": "Confirm into",
1607
+ "confirm.toBacklog": "Backlog",
1608
+ "confirm.toTodo": "To Do",
1609
+ "confirm.submit": "Confirm",
1610
+ "confirm.dismiss": "Dismiss",
1611
+ "confirm.dismissTitle": "Dismiss candidate task",
1612
+ "confirm.dismissMessage": "Dismiss \"{name}\"? The candidate task will be deleted.",
1613
+ "card.proposedBadge": "Pending",
1614
+ "card.source": "Source",
1615
+ "source.conversation": "Chat",
1616
+ "source.requirement": "Requirement",
1617
+ "entry.proposedCount": "{count} candidate(s) pending",
1618
+ "convert.title": "Turn into task",
1619
+ "convert.fromConversation": "From conversation",
1620
+ "convert.submitProposed": "Propose",
1621
+ "convert.submitDirect": "Create directly",
1622
+ "convert.directTarget": "Into",
1623
+ "convert.directBacklog": "Backlog",
1624
+ "convert.directTodo": "To Do",
1625
+ "convert.proposedHint": "Proposing adds the task to the \"Pending\" column; it runs only after confirmation.",
1626
+ "chat.extractedHint": "Extracted {count} candidate task(s) from the conversation (see the board \"Pending\" column).",
1627
+ "detail.jumpToConversation": "Back to chat",
1628
+ "detail.sourceConversation": "Source session",
1629
+ "split.entry": "Split from requirement",
1630
+ "split.subtitle": "Submit a requirement document (pasted text or a workspace file path); the system proactively splits it into independently executable/verifiable subtask candidates in the \"Pending\" column. Confirmed subtasks run independently and keep a reference to the parent requirement.",
1631
+ "split.recursiveTitle": "Recursive split",
1632
+ "split.recursiveHint": "Continue splitting this task: a new parent requirement task \"Requirement: <title>\" is created under it, and the subtasks point to the new parent.",
1633
+ "split.requirementTitle": "Requirement title",
1634
+ "split.requirementTitlePlaceholder": "e.g. \"Login module\"",
1635
+ "split.source": "Requirement source",
1636
+ "split.sourceText": "Pasted text",
1637
+ "split.sourceFile": "Workspace file path",
1638
+ "split.text": "Requirement text",
1639
+ "split.textPlaceholder": "Paste the requirement document…",
1640
+ "split.file": "File path",
1641
+ "split.filePlaceholder": "Path relative to the workspace, e.g. docs/requirement.md",
1642
+ "split.required": "Requirement title and content (text or file path) are required",
1643
+ "split.submit": "Start split",
1644
+ "split.asyncHint": "Splitting runs in a dedicated session; results land in the \"Pending\" column (not executable until confirmed).",
1645
+ "split.batchButton": "Batch confirm",
1646
+ "split.batchTitle": "Batch confirm candidates",
1647
+ "split.batchSubtitle": "Edit items (title/description/prompt/project/tags), add or remove rows; confirm all into the chosen column, or dismiss all.",
1648
+ "split.addItem": "Add item",
1649
+ "split.added": "New",
1650
+ "split.removeItem": "Remove",
1651
+ "split.confirmAll": "Confirm all into {target}",
1652
+ "split.dismissAll": "Dismiss all",
1653
+ "split.dismissAllTitle": "Dismiss all candidates",
1654
+ "split.dismissAllMessage": "Dismiss all {count} candidate(s)? They will be deleted.",
1655
+ "split.dismissItemTitle": "Remove candidate",
1656
+ "split.dismissItemMessage": "Remove \"{name}\"? The candidate task will be deleted.",
1657
+ "split.empty": "No candidates to batch-confirm",
1658
+ "split.parentOf": "Parent requirement",
1659
+ "split.parentMissing": "(parent task deleted)",
1660
+ "split.children": "Subtasks",
1661
+ "settings.title": "Nova Task Board",
1662
+ "settings.description": "Configure the Host Nova task board and agent announcement.",
1663
+ "settings.enabled": "Enable the Nova task board",
1664
+ "settings.enabledHint": "When off, the sidebar entry and board view are hidden.",
1665
+ "settings.announceToAgent": "Announce the Nova task board to agents",
1666
+ "settings.announceToAgentHint": "On: every agent system prompt includes a note about this board. Off: no announcement.",
1667
+ "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.",
1668
+ "settings.readOnly": "This deployment stores settings read-only."
1669
+ };
1670
+ /** Active dictionary, picked by the document language at call time. */
1671
+ function dictionary() {
1672
+ return (typeof document !== "undefined" ? document.documentElement.lang : "zh").toLowerCase().startsWith("en") ? en : zh;
1496
1673
  }
1497
- /** 归一化上下文快照(§9.5 context_snapshot);结构非法返回 undefined。 */
1498
- function normalizeContextSnapshot(value) {
1499
- if (typeof value !== "object" || value === null) return void 0;
1500
- const entry = value;
1501
- if (!isFiniteNumber(entry.updatedAt)) return void 0;
1502
- if (entry.goal !== void 0 && typeof entry.goal !== "string") return void 0;
1503
- if (entry.lastAiSummary !== void 0 && typeof entry.lastAiSummary !== "string") return void 0;
1504
- if (entry.latestUserFeedback !== void 0 && typeof entry.latestUserFeedback !== "string") return void 0;
1505
- const snapshot = {
1506
- keyDecisions: normalizeStringList(entry.keyDecisions, 50, 500),
1507
- filePaths: normalizeStringList(entry.filePaths, 100, 500),
1508
- relatedLinks: normalizeStringList(entry.relatedLinks, 20, 2048),
1509
- updatedAt: entry.updatedAt
1510
- };
1511
- if (typeof entry.goal === "string" && entry.goal.trim() !== "") snapshot.goal = entry.goal.trim().slice(0, 2e3);
1512
- if (typeof entry.lastAiSummary === "string" && entry.lastAiSummary.trim() !== "") snapshot.lastAiSummary = entry.lastAiSummary.trim().slice(0, 8e3);
1513
- if (typeof entry.latestUserFeedback === "string" && entry.latestUserFeedback.trim() !== "") snapshot.latestUserFeedback = entry.latestUserFeedback.trim().slice(0, 4e3);
1514
- return snapshot;
1674
+ /** Translate a key with optional {name} template params. */
1675
+ function t(key, params) {
1676
+ let text = dictionary()[key];
1677
+ if (params !== void 0) for (const [name, value] of Object.entries(params)) text = text.replaceAll(`{${name}}`, value);
1678
+ return text;
1515
1679
  }
1516
1680
  /**
1517
- * 归一化一条任务行(§9.2)。
1518
- *
1519
- * 结构非法(id/title/description/prompt/createdAt/updatedAt/executions 任一不
1520
- * 符合)→ 返回 undefined(整行丢弃,HostLedger 记入 scheduler.error);
1521
- * 语义非法 → 就地修复:
1522
- * - 未知状态 → `todo`(未来版本的未知状态落入待办而非丢行,对齐参考实现);
1523
- * - 未知 source/permission → undefined;空白 workspaceId/mode/project/parentId
1524
- * → undefined;archivedAt 非有限数 → undefined;
1525
- * - tags 非字符串数组 → [];order 非有限数 → 0(T009 重算列内唯一);
1526
- * - schedule 交给 normalizeSchedule(修复或丢弃,不丢行)。
1681
+ * Locale dictionary for the "Nova 插件" settings section (the first-level nav
1682
+ * entry that hosts the dsh-nova-ui family plugin cards). Kept as its own
1683
+ * namespace (`nova-plugins`) so the section copy never collides with the
1684
+ * task-board card's `nova-task-board` namespace (AGENTS.md D4). Key set 以 zh 为准。
1527
1685
  */
1528
- function normalizeTask(value) {
1529
- if (typeof value !== "object" || value === null) return void 0;
1530
- const record = value;
1531
- if (typeof record.id !== "string" || record.id === "") return void 0;
1532
- if (typeof record.title !== "string") return void 0;
1533
- if (typeof record.description !== "string") return void 0;
1534
- if (typeof record.prompt !== "string") return void 0;
1535
- if (!isFiniteNumber(record.createdAt) || !isFiniteNumber(record.updatedAt)) return void 0;
1536
- if (!Array.isArray(record.executions)) return void 0;
1537
- const executions = [];
1538
- for (const entry of record.executions) {
1539
- const execution = normalizeExecution(entry);
1540
- if (execution === void 0) return void 0;
1541
- executions.push(execution);
1542
- }
1543
- const task = {
1544
- id: record.id,
1545
- title: record.title,
1546
- description: record.description,
1547
- prompt: record.prompt,
1548
- status: isTaskStatus(record.status) ? record.status : "todo",
1549
- createdAt: record.createdAt,
1550
- updatedAt: record.updatedAt,
1551
- executions,
1552
- tags: normalizeTags(record.tags),
1553
- comments: normalizeComments(record.comments),
1554
- artifacts: normalizeArtifacts(record.artifacts),
1555
- order: isFiniteNumber(record.order) ? record.order : 0
1556
- };
1557
- if (isTaskSource(record.source)) task.source = record.source;
1558
- const metadata = normalizeMetadata(record.metadata);
1559
- if (metadata !== void 0) task.metadata = metadata;
1560
- const contextSnapshot = normalizeContextSnapshot(record.contextSnapshot);
1561
- if (contextSnapshot !== void 0) task.contextSnapshot = contextSnapshot;
1562
- const schedule = normalizeSchedule(record.schedule);
1563
- if (schedule !== void 0) task.schedule = schedule;
1564
- const workspaceId = normalizeOptionalString(record.workspaceId);
1565
- if (workspaceId !== void 0) task.workspaceId = workspaceId;
1566
- const mode = normalizeOptionalString(record.mode);
1567
- if (mode !== void 0) task.mode = mode;
1568
- if (isTaskPermission(record.permission)) task.permission = record.permission;
1569
- if (isFiniteNumber(record.archivedAt)) task.archivedAt = record.archivedAt;
1570
- const project = normalizeOptionalString(record.project);
1571
- if (project !== void 0) task.project = project;
1572
- const parentId = normalizeOptionalString(record.parentId);
1573
- if (parentId !== void 0) task.parentId = parentId;
1574
- return task;
1686
+ const groupZh = {
1687
+ title: "Nova 插件",
1688
+ description: "统一管理 Nova 系列插件(dsh-nova-ui 全家桶)的启用与配置。"
1689
+ };
1690
+ /** en dictionary, complete against the groupZh key set. */
1691
+ const groupEn = {
1692
+ title: "Nova Plugins",
1693
+ description: "Enable and configure the dsh-nova-ui family plugins from one place."
1694
+ };
1695
+ //#endregion
1696
+ //#region \0dsh-css:packages/dsh-nova-ui-task-board/src/client/board.module.css.mjs
1697
+ const css = "[data-pane=conversation],[class*=centerCol]{position:relative}[data-dsh-nova-taskboard-view]{z-index:60;background:var(--dsw-alias-bg-base);display:none;position:absolute;inset:0;container:a-UL2G_nova-task-board-view/inline-size}html[data-dsh-nova-taskboard-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [data-dsh-nova-taskboard-view]{display:block}html[data-dsh-nova-taskboard-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [data-pane=conversation]>:not([data-dsh-nova-taskboard-view]),html[data-dsh-nova-taskboard-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [class*=centerCol]>:not([data-dsh-nova-taskboard-view]){display:none!important}.a-UL2G_entry{width:100%;height:32px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:8px;align-items:center;gap:8px;padding:0 12px;font-size:13px;display:flex}.a-UL2G_entry:hover{background:var(--dsw-specific-sidebar-nav-item-hover);color:var(--dsw-alias-label-primary)}.a-UL2G_entry[data-active]{background:var(--dsw-specific-sidebar-nav-item-active);color:var(--dsw-alias-label-primary);font-weight:600}.a-UL2G_entryIcon{flex:none;justify-content:center;align-items:center;display:inline-flex}.a-UL2G_entryLabel{text-overflow:ellipsis;overflow:hidden}[data-dsh-frame][data-sidebar-collapsed] .a-UL2G_entry{justify-content:center;width:100%;padding:0}[data-dsh-frame][data-sidebar-collapsed] .a-UL2G_entryLabel{display:none}.a-UL2G_boardView{height:100%;min-height:0}.a-UL2G_board{box-sizing:border-box;background:var(--dsw-alias-bg-base);min-width:0;height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex-direction:column;gap:12px;padding:14px 16px 16px;display:flex}.a-UL2G_boardHeader{flex-wrap:wrap;flex:none;align-items:center;gap:10px;display:flex}.a-UL2G_boardTitle{color:var(--dsw-alias-label-primary);white-space:nowrap;margin:0;font-size:16px;font-weight:700}.a-UL2G_backButton{align-items:center;gap:4px;display:inline-flex}.a-UL2G_hostMeta{color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-overflow:ellipsis;margin-left:auto;font-size:11px;overflow:hidden}.a-UL2G_search{min-width:120px;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;flex:0 260px;padding:6px 10px;font-size:13px}.a-UL2G_search::placeholder{color:var(--dsw-alias-label-tertiary)}.a-UL2G_filterBar{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-wrap:wrap;flex:none;align-items:center;gap:14px;padding:8px 10px;display:flex}.a-UL2G_filterItem{align-items:center;gap:8px;min-width:0;display:inline-flex}.a-UL2G_filterLabel{color:var(--dsw-alias-label-tertiary);flex:none;font-size:12px;font-weight:600}.a-UL2G_filterItem .a-UL2G_select{max-width:220px}.a-UL2G_filterChips{flex-wrap:wrap;align-items:center;gap:6px;max-height:64px;display:inline-flex;overflow-y:auto}.a-UL2G_filterChip{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;white-space:nowrap;border-radius:999px;padding:2px 9px;font-family:inherit;font-size:12px;line-height:1.5;transition:background-color .12s,color .12s,border-color .12s}.a-UL2G_filterChip:hover{border-color:var(--dsw-alias-border-l3)}.a-UL2G_filterChipActive{color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-button-info-fill);border-color:#0000}.a-UL2G_filterActions{align-items:center;gap:10px;margin-left:auto;display:inline-flex}.a-UL2G_filterHitCount{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-size:12px}.a-UL2G_cardChips{flex-wrap:wrap;align-items:center;gap:4px;display:flex}.a-UL2G_cardProject{color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-state-business-primary);white-space:nowrap;text-overflow:ellipsis;border-radius:999px;max-width:140px;padding:1px 8px;font-size:11px;font-weight:600;line-height:1.5;overflow:hidden}.a-UL2G_cardTag{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover);border:1px solid var(--dsw-alias-border-l2);white-space:nowrap;text-overflow:ellipsis;border-radius:999px;max-width:110px;padding:1px 8px;font-size:11px;line-height:1.5;overflow:hidden}.a-UL2G_cardTagMore{color:var(--dsw-alias-label-tertiary);white-space:nowrap;border-radius:999px;padding:1px 6px;font-size:11px;line-height:1.5}.a-UL2G_cardParent{color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-state-business-primary);white-space:nowrap;text-overflow:ellipsis;cursor:pointer;border-radius:999px;max-width:180px;padding:1px 8px;font-size:11px;line-height:1.5;display:inline-block;overflow:hidden}.a-UL2G_cardParent:hover{filter:brightness(1.1);text-decoration:underline}.a-UL2G_splitRows{flex-direction:column;gap:12px;max-height:min(46vh,420px);display:flex;overflow-y:auto}.a-UL2G_splitRow{background:var(--dsw-alias-bg-l2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;flex-direction:column;gap:8px;padding:10px 12px;display:flex}.a-UL2G_splitRowHeader{justify-content:space-between;align-items:center;gap:8px;display:flex}.a-UL2G_splitRowIndex{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}.a-UL2G_tagEditor{flex-direction:column;gap:6px;display:flex}.a-UL2G_tagEditor .a-UL2G_input{max-width:280px}.a-UL2G_tagEditorChip{cursor:default;align-items:center;gap:4px;display:inline-flex}.a-UL2G_tagEditorRemove{width:16px;height:16px;color:inherit;cursor:pointer;background:0 0;border:none;border-radius:50%;justify-content:center;align-items:center;padding:0;font-size:13px;line-height:1;display:inline-flex}.a-UL2G_tagEditorRemove:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.a-UL2G_tagEditorRemove:disabled{opacity:.45;cursor:default}.a-UL2G_columns{overscroll-behavior-inline:contain;scrollbar-color:var(--dsw-alias-border-l3) var(--dsw-alias-interactive-bg-hover);scrollbar-width:thin;flex:1;grid-auto-columns:minmax(220px,1fr);grid-auto-flow:column;gap:12px;min-height:0;padding-bottom:6px;display:grid;overflow:auto hidden}.a-UL2G_columns::-webkit-scrollbar{height:10px}.a-UL2G_columns::-webkit-scrollbar-track{background:var(--dsw-alias-interactive-bg-hover);border-radius:999px}.a-UL2G_columns::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l3);background-clip:content-box;border:2px solid #0000;border-radius:999px}.a-UL2G_columns::-webkit-scrollbar-thumb:hover{background:var(--dsw-alias-border-l4);background-clip:content-box}.a-UL2G_column{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:12px;flex-direction:column;min-height:0;display:flex;overflow:hidden}.a-UL2G_columnHeader{flex:none;align-items:center;gap:6px;padding:10px 12px;display:flex}.a-UL2G_columnTitle{color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:13px;font-weight:700;overflow:hidden}.a-UL2G_columnCount{min-width:0;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;flex:none;padding:1px 8px;font-size:12px}.a-UL2G_statusDot{border-radius:50%;flex:none;width:8px;height:8px}.a-UL2G_statusDot[data-status=backlog]{background:var(--dsw-alias-label-tertiary)}.a-UL2G_statusDot[data-status=todo]{background:var(--dsw-alias-state-business-primary)}.a-UL2G_statusDot[data-status=running]{background:var(--dsw-alias-state-warn-primary)}.a-UL2G_statusDot[data-status=done]{background:var(--dsw-alias-state-success-primary)}.a-UL2G_statusDot[data-status=failed]{background:var(--dsw-alias-state-error-primary)}.a-UL2G_cards{flex-direction:column;flex:1;gap:8px;min-height:0;padding:2px 8px 10px;display:flex;overflow-y:auto}.a-UL2G_columnEmpty{text-align:center;color:var(--dsw-alias-label-tertiary);padding:24px 8px;font-size:12px}.a-UL2G_card{text-align:left;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;color:var(--dsw-alias-label-primary);border-radius:10px;flex-direction:column;gap:6px;padding:10px 12px;font-family:inherit;transition:box-shadow .12s,border-color .12s,transform .12s;display:flex}.a-UL2G_card:hover{box-shadow:var(--dsw-shadow-lv2);border-color:var(--dsw-alias-border-l3);transform:translateY(-1px)}.a-UL2G_card[data-status=running]{border-color:var(--dsw-alias-state-warn-primary)}.a-UL2G_cardTitle{-webkit-line-clamp:2;-webkit-box-orient:vertical;font-size:13px;font-weight:600;line-height:1.35;display:-webkit-box;overflow:hidden}.a-UL2G_cardExcerpt{color:var(--dsw-alias-label-secondary);-webkit-line-clamp:2;-webkit-box-orient:vertical;font-size:12px;line-height:1.4;display:-webkit-box;overflow:hidden}.a-UL2G_cardMeta{color:var(--dsw-alias-label-tertiary);align-items:center;gap:8px;font-size:11px;display:flex}.a-UL2G_cardTime{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.a-UL2G_cardSchedule{white-space:nowrap;min-width:0;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;flex:none;padding:2px 6px;font-size:12px;line-height:1}.a-UL2G_cardRun{flex:none}.a-UL2G_cardRun[data-result=failed]{color:var(--dsw-alias-state-error-primary)}.a-UL2G_cardRun[data-result=succeeded]{color:var(--dsw-alias-state-success-primary)}.a-UL2G_cardSession{color:var(--dsw-alias-state-business-primary);flex:none}.a-UL2G_cardRunningLabel{color:var(--dsw-alias-state-warn-primary);font-size:11px}.a-UL2G_cardSpinner{border:2px solid var(--dsw-alias-state-warn-primary);border-top-color:#0000;border-radius:50%;flex:none;width:10px;height:10px;animation:.8s linear infinite a-UL2G_dshNovaTbSpin}@keyframes a-UL2G_dshNovaTbSpin{to{transform:rotate(360deg)}}.a-UL2G_primaryButton{color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-button-info-fill);cursor:pointer;white-space:nowrap;border:none;border-radius:8px;padding:6px 14px;font-size:13px;font-weight:600}.a-UL2G_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-info-hover)}.a-UL2G_primaryButton:disabled{opacity:.5;cursor:default}.a-UL2G_ghostButton{color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;white-space:nowrap;background:0 0;border-radius:8px;padding:5px 12px;font-size:12px}.a-UL2G_ghostButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.a-UL2G_ghostButton:disabled{opacity:.45;cursor:default}.a-UL2G_dangerButton{color:#fff;background:var(--dsw-alias-state-error-primary);cursor:pointer;white-space:nowrap;border:none;border-radius:8px;padding:6px 14px;font-size:13px;font-weight:600}.a-UL2G_dangerButton:hover:not(:disabled){filter:brightness(1.08)}.a-UL2G_dangerButton:active:not(:disabled){filter:brightness(.94)}.a-UL2G_dangerButton:disabled{opacity:.5;cursor:default}.a-UL2G_iconButton{width:26px;height:26px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;font-size:13px;display:inline-flex}.a-UL2G_iconButton:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.a-UL2G_linkButton{color:var(--dsw-alias-state-business-primary);cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:0;font-size:12px}.a-UL2G_linkButton:hover{text-decoration:underline}.a-UL2G_modalBackdrop{z-index:1300;background:var(--dsw-alias-bg-mask-1);justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.a-UL2G_modal{background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);width:min(520px,100vw - 48px);max-height:calc(100vh - 96px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);border-radius:14px;flex-direction:column;gap:12px;padding:18px;display:flex;overflow-y:auto}.a-UL2G_modalTitle{margin:0;font-size:15px;font-weight:700}.a-UL2G_confirmMessage{color:var(--dsw-alias-label-secondary);white-space:pre-wrap;overflow-wrap:anywhere;margin:0;font-size:13px;line-height:1.5}.a-UL2G_modalFooter{justify-content:flex-end;gap:10px;margin-top:4px;display:flex}.a-UL2G_field{flex-direction:column;gap:5px;display:flex}.a-UL2G_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}.a-UL2G_input{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);resize:vertical;border-radius:8px;outline:none;padding:7px 10px;font-family:inherit;font-size:13px}.a-UL2G_input:focus{border-color:var(--dsw-alias-state-business-primary)}.a-UL2G_select{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;max-width:100%;padding:7px 10px;font-family:inherit;font-size:13px}.a-UL2G_input::placeholder{color:var(--dsw-alias-label-tertiary)}.a-UL2G_formError{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}.a-UL2G_detail{background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);width:min(640px,100vw - 48px);max-height:calc(100vh - 80px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);border-radius:14px;flex-direction:column;display:flex;overflow:hidden}.a-UL2G_detailHeader{border-bottom:1px solid var(--dsw-alias-separator-primary);flex:none;align-items:center;gap:10px;padding:14px 18px;display:flex}.a-UL2G_detailTitle{overflow-wrap:anywhere;flex:1;margin:0;font-size:15px;font-weight:700}.a-UL2G_statusBadge{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:2px 10px;font-size:12px}.a-UL2G_statusBadge[data-status=running]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}.a-UL2G_statusBadge[data-status=done]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.a-UL2G_statusBadge[data-status=failed]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.a-UL2G_detailBody{flex-direction:column;flex:1;gap:16px;padding:14px 18px;display:flex;overflow-y:auto}.a-UL2G_detailSection{flex-direction:column;gap:6px;display:flex}.a-UL2G_detailSection h4{color:var(--dsw-alias-label-tertiary);text-transform:none;margin:0;font-size:12px;font-weight:700}.a-UL2G_detailText{color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere;margin:0;font-size:13px;line-height:1.55}.a-UL2G_promptBlock{font-size:12.5px;line-height:1.5;font-family:var(--dsw-font-markdown-code-block-small);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-markdown-code-block);border:1px solid var(--dsw-alias-border-l1);white-space:pre-wrap;overflow-wrap:anywhere;border-radius:8px;max-height:240px;margin:0;padding:10px 12px;overflow-y:auto}.a-UL2G_executionList{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}.a-UL2G_executionRow{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;flex-wrap:wrap;align-items:center;gap:10px;padding:8px 10px;display:flex}.a-UL2G_executionBadge{color:var(--dsw-alias-state-warn-primary);background:var(--dsw-alias-state-warn-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:600}.a-UL2G_executionBadge[data-result=succeeded]{color:var(--dsw-alias-state-success-primary);background:0 0}.a-UL2G_executionBadge[data-result=failed]{color:var(--dsw-alias-state-error-primary);background:0 0}.a-UL2G_executionBadge[data-result=cancelled]{color:var(--dsw-alias-label-tertiary);background:0 0}.a-UL2G_executionTimes{color:var(--dsw-alias-label-secondary);font-size:12px}.a-UL2G_executionError{width:100%;color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;font-size:12px}.a-UL2G_moveRow{flex-wrap:wrap;gap:8px;display:flex}.a-UL2G_scheduleMode{background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;gap:4px;margin-bottom:10px;padding:3px;display:inline-flex}.a-UL2G_scheduleModeButton{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:7px;padding:4px 12px;font-size:12px}.a-UL2G_scheduleModeButton:hover:not(.a-UL2G_scheduleModeActive){color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.a-UL2G_scheduleModeActive{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-fill-cs-primary);box-shadow:var(--dsw-alias-shadow-fs-1)}.a-UL2G_dangerGhostButton{color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-primary);cursor:pointer;white-space:nowrap;background:0 0;border-radius:8px;padding:5px 12px;font-size:12px}.a-UL2G_dangerGhostButton:hover:not(:disabled){background:var(--dsw-alias-state-error-tint)}.a-UL2G_dangerGhostButton:disabled{opacity:.45;cursor:default}.a-UL2G_scheduleToggle{color:var(--dsw-alias-label-primary);cursor:pointer;user-select:none;align-items:center;gap:8px;font-size:13px;display:flex}.a-UL2G_scheduleToggle input{accent-color:var(--dsw-alias-state-business-primary)}.a-UL2G_scheduleRow{align-items:center;gap:8px;display:flex}.a-UL2G_scheduleInput{min-width:0;font-family:var(--dsw-font-markdown-code-block-small);flex:1;font-size:12.5px}.a-UL2G_scheduleInputInvalid,.a-UL2G_scheduleInputInvalid:focus{border-color:var(--dsw-alias-state-error-primary)}.a-UL2G_schedulePreset{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;flex:none;padding:7px 8px;font-size:12.5px}.a-UL2G_scheduleMeta{color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;margin:0;font-size:12px}.a-UL2G_detailFooter{border-top:1px solid var(--dsw-alias-separator-primary);flex:none;align-items:center;gap:10px;padding:12px 18px;display:flex}.a-UL2G_detailMeta{color:var(--dsw-alias-label-tertiary);margin-left:auto;font-size:11px}@container a-UL2G_nova-task-board-view (width<=720px){.a-UL2G_board{gap:10px;padding:10px}.a-UL2G_columns{gap:10px}}@container a-UL2G_nova-task-board-view (width<=600px){.a-UL2G_boardHeader{flex-wrap:wrap;gap:8px}.a-UL2G_search{flex:calc(100% - 72px);min-width:0}.a-UL2G_boardHeader>button{flex:1 1 0;min-width:max-content}}.a-UL2G_entry:focus-visible,.a-UL2G_card:focus-visible,.a-UL2G_primaryButton:focus-visible,.a-UL2G_ghostButton:focus-visible,.a-UL2G_dangerButton:focus-visible,.a-UL2G_iconButton:focus-visible,.a-UL2G_linkButton:focus-visible,.a-UL2G_search:focus-visible,.a-UL2G_input:focus-visible,.a-UL2G_select:focus-visible,.a-UL2G_schedulePreset:focus-visible,.a-UL2G_scheduleToggle input:focus-visible,.a-UL2G_filterChip:focus-visible,.a-UL2G_tagEditorRemove:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}.a-UL2G_entry,.a-UL2G_primaryButton,.a-UL2G_ghostButton,.a-UL2G_dangerButton,.a-UL2G_iconButton,.a-UL2G_linkButton,.a-UL2G_search,.a-UL2G_input,.a-UL2G_select,.a-UL2G_schedulePreset,.a-UL2G_scheduleToggle input,.a-UL2G_filterChip,.a-UL2G_tagEditorRemove{transition:background-color .12s,color .12s,border-color .12s,outline-color .12s,box-shadow .12s,transform .12s}.a-UL2G_card:active{box-shadow:var(--dsw-shadow-lv1);transform:translateY(0)}.a-UL2G_entry:active,.a-UL2G_primaryButton:active:not(:disabled),.a-UL2G_ghostButton:active:not(:disabled),.a-UL2G_dangerButton:active:not(:disabled),.a-UL2G_iconButton:active:not(:disabled),.a-UL2G_linkButton:active:not(:disabled){transform:translateY(1px)}.a-UL2G_entry[data-active]:hover{background:var(--dsw-specific-sidebar-nav-item-active)}.a-UL2G_iconButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.a-UL2G_linkButton:hover:not(:disabled){text-decoration:underline}.a-UL2G_iconButton:disabled,.a-UL2G_linkButton:disabled{opacity:.45;cursor:default}.a-UL2G_search:focus,.a-UL2G_select:focus,.a-UL2G_schedulePreset:focus{border-color:var(--dsw-alias-state-business-primary)}@media (prefers-reduced-motion:reduce){.a-UL2G_entry,.a-UL2G_card,.a-UL2G_primaryButton,.a-UL2G_ghostButton,.a-UL2G_dangerButton,.a-UL2G_iconButton,.a-UL2G_linkButton,.a-UL2G_search,.a-UL2G_input,.a-UL2G_select,.a-UL2G_schedulePreset,.a-UL2G_scheduleToggle input,.a-UL2G_filterChip,.a-UL2G_tagEditorRemove{transition:none}.a-UL2G_cardSpinner{animation:none}}.a-UL2G_groupToggle{background:var(--dsw-alias-interactive-bg-hover);border-radius:8px;flex:none;align-items:center;gap:2px;padding:2px;display:inline-flex}.a-UL2G_groupToggleButton{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:6px;padding:4px 10px;font-size:12px}.a-UL2G_groupToggleButton:hover:not(:disabled){color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-active)}.a-UL2G_groupToggleActive,.a-UL2G_groupToggleActive:hover:not(:disabled){color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);box-shadow:var(--dsw-shadow-lv1);font-weight:600}.a-UL2G_group{border:1px dashed #0000;border-radius:10px;flex-direction:column;gap:6px;padding:6px 8px;display:flex}.a-UL2G_group[data-drag-over]{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-interactive-bg-hover)}.a-UL2G_group[data-drag-rejected]{border-color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-state-error-surface,transparent)}.a-UL2G_groupHeader{flex:none;align-items:center;gap:6px;padding:2px 2px 0;display:flex}.a-UL2G_groupTitle{color:var(--dsw-alias-label-secondary);text-overflow:ellipsis;white-space:nowrap;flex:1;font-size:12px;font-weight:700;overflow:hidden}.a-UL2G_groupCount{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;flex:none;padding:0 7px;font-size:11px}.a-UL2G_groupCards{flex-direction:column;gap:8px;display:flex}.a-UL2G_column[data-drag-over]{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 1px var(--dsw-alias-state-business-primary)}.a-UL2G_column[data-drag-rejected]{border-color:var(--dsw-alias-state-error-primary)}.a-UL2G_card[data-draggable]{cursor:grab}.a-UL2G_card[data-draggable]:active{cursor:grabbing}.a-UL2G_cardDragging{opacity:.45;box-shadow:var(--dsw-shadow-lv2)}.a-UL2G_dropIndicator{background:var(--dsw-alias-state-business-primary);pointer-events:none;border-radius:999px;flex:none;height:3px;margin:0 2px}.a-UL2G_groupToggleButton:focus-visible,.a-UL2G_groupToggleButton:hover:not(:disabled){outline:none}.a-UL2G_groupToggleButton:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary)}.a-UL2G_statusDot[data-status=proposed]{background:var(--dsw-alias-brand-primary,#7c6cf0)}.a-UL2G_cardProposedBadge{color:var(--dsw-alias-label-primary-foreground,#fff);background:var(--dsw-alias-brand-primary,#7c6cf0);white-space:nowrap;border-radius:999px;flex:none;align-self:flex-start;padding:1px 8px;font-size:11px;font-weight:600;line-height:1.5}.a-UL2G_cardSource{color:var(--dsw-alias-label-tertiary);white-space:nowrap;flex:none;font-size:11px}.a-UL2G_entryBadge{text-align:center;min-width:18px;color:var(--dsw-alias-label-primary-foreground,#fff);background:var(--dsw-alias-state-error-primary,#e5484d);white-space:nowrap;border-radius:999px;flex:none;margin-left:auto;padding:0 6px;font-size:11px;font-weight:700;line-height:18px}.a-UL2G_entryBadge[data-count=\"0\"]{display:none}.a-UL2G_confirmTargetRow{flex-wrap:wrap;gap:8px;display:flex}.a-UL2G_confirmTargetButton{color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;white-space:nowrap;background:0 0;border-radius:8px;padding:6px 14px;font-family:inherit;font-size:13px}.a-UL2G_confirmTargetButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.a-UL2G_confirmTargetActive,.a-UL2G_confirmTargetActive:hover:not(:disabled){color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-button-info-fill);border-color:#0000;font-weight:600}.a-UL2G_confirmTargetButton:disabled{opacity:.45;cursor:default}.a-UL2G_sourceRow{color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;align-items:center;gap:8px;font-size:12px;display:flex}.a-UL2G_messageAction{z-index:20;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);box-shadow:var(--dsw-shadow-lv1);cursor:pointer;white-space:nowrap;border-radius:8px;padding:3px 9px;font-family:inherit;font-size:11px;font-weight:600;position:absolute;top:6px;right:8px}.a-UL2G_messageAction:hover{background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l3)}.a-UL2G_messageAction:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}";
1698
+ const tagId = "@william2000/dsh-nova-ui-task-board/board.module.css";
1699
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1700
+ const tag = document.createElement("style");
1701
+ tag.dataset.plugin = "@william2000/dsh-nova-ui-task-board";
1702
+ tag.dataset.pluginCss = tagId;
1703
+ tag.textContent = css;
1704
+ document.head.appendChild(tag);
1575
1705
  }
1706
+ var board_module_css_default = {
1707
+ "backButton": "a-UL2G_backButton",
1708
+ "board": "a-UL2G_board",
1709
+ "boardHeader": "a-UL2G_boardHeader",
1710
+ "boardTitle": "a-UL2G_boardTitle",
1711
+ "boardView": "a-UL2G_boardView",
1712
+ "card": "a-UL2G_card",
1713
+ "cardChips": "a-UL2G_cardChips",
1714
+ "cardDragging": "a-UL2G_cardDragging",
1715
+ "cardExcerpt": "a-UL2G_cardExcerpt",
1716
+ "cardMeta": "a-UL2G_cardMeta",
1717
+ "cardParent": "a-UL2G_cardParent",
1718
+ "cardProject": "a-UL2G_cardProject",
1719
+ "cardProposedBadge": "a-UL2G_cardProposedBadge",
1720
+ "cardRun": "a-UL2G_cardRun",
1721
+ "cardRunningLabel": "a-UL2G_cardRunningLabel",
1722
+ "cardSchedule": "a-UL2G_cardSchedule",
1723
+ "cardSession": "a-UL2G_cardSession",
1724
+ "cardSource": "a-UL2G_cardSource",
1725
+ "cardSpinner": "a-UL2G_cardSpinner",
1726
+ "cardTag": "a-UL2G_cardTag",
1727
+ "cardTagMore": "a-UL2G_cardTagMore",
1728
+ "cardTime": "a-UL2G_cardTime",
1729
+ "cardTitle": "a-UL2G_cardTitle",
1730
+ "cards": "a-UL2G_cards",
1731
+ "column": "a-UL2G_column",
1732
+ "columnCount": "a-UL2G_columnCount",
1733
+ "columnEmpty": "a-UL2G_columnEmpty",
1734
+ "columnHeader": "a-UL2G_columnHeader",
1735
+ "columnTitle": "a-UL2G_columnTitle",
1736
+ "columns": "a-UL2G_columns",
1737
+ "confirmMessage": "a-UL2G_confirmMessage",
1738
+ "confirmTargetActive": "a-UL2G_confirmTargetActive",
1739
+ "confirmTargetButton": "a-UL2G_confirmTargetButton",
1740
+ "confirmTargetRow": "a-UL2G_confirmTargetRow",
1741
+ "dangerButton": "a-UL2G_dangerButton",
1742
+ "dangerGhostButton": "a-UL2G_dangerGhostButton",
1743
+ "detail": "a-UL2G_detail",
1744
+ "detailBody": "a-UL2G_detailBody",
1745
+ "detailFooter": "a-UL2G_detailFooter",
1746
+ "detailHeader": "a-UL2G_detailHeader",
1747
+ "detailMeta": "a-UL2G_detailMeta",
1748
+ "detailSection": "a-UL2G_detailSection",
1749
+ "detailText": "a-UL2G_detailText",
1750
+ "detailTitle": "a-UL2G_detailTitle",
1751
+ "dropIndicator": "a-UL2G_dropIndicator",
1752
+ "dshNovaTbSpin": "a-UL2G_dshNovaTbSpin",
1753
+ "entry": "a-UL2G_entry",
1754
+ "entryBadge": "a-UL2G_entryBadge",
1755
+ "entryIcon": "a-UL2G_entryIcon",
1756
+ "entryLabel": "a-UL2G_entryLabel",
1757
+ "executionBadge": "a-UL2G_executionBadge",
1758
+ "executionError": "a-UL2G_executionError",
1759
+ "executionList": "a-UL2G_executionList",
1760
+ "executionRow": "a-UL2G_executionRow",
1761
+ "executionTimes": "a-UL2G_executionTimes",
1762
+ "field": "a-UL2G_field",
1763
+ "fieldLabel": "a-UL2G_fieldLabel",
1764
+ "filterActions": "a-UL2G_filterActions",
1765
+ "filterBar": "a-UL2G_filterBar",
1766
+ "filterChip": "a-UL2G_filterChip",
1767
+ "filterChipActive": "a-UL2G_filterChipActive",
1768
+ "filterChips": "a-UL2G_filterChips",
1769
+ "filterHitCount": "a-UL2G_filterHitCount",
1770
+ "filterItem": "a-UL2G_filterItem",
1771
+ "filterLabel": "a-UL2G_filterLabel",
1772
+ "formError": "a-UL2G_formError",
1773
+ "ghostButton": "a-UL2G_ghostButton",
1774
+ "group": "a-UL2G_group",
1775
+ "groupCards": "a-UL2G_groupCards",
1776
+ "groupCount": "a-UL2G_groupCount",
1777
+ "groupHeader": "a-UL2G_groupHeader",
1778
+ "groupTitle": "a-UL2G_groupTitle",
1779
+ "groupToggle": "a-UL2G_groupToggle",
1780
+ "groupToggleActive": "a-UL2G_groupToggleActive",
1781
+ "groupToggleButton": "a-UL2G_groupToggleButton",
1782
+ "hostMeta": "a-UL2G_hostMeta",
1783
+ "iconButton": "a-UL2G_iconButton",
1784
+ "input": "a-UL2G_input",
1785
+ "linkButton": "a-UL2G_linkButton",
1786
+ "messageAction": "a-UL2G_messageAction",
1787
+ "modal": "a-UL2G_modal",
1788
+ "modalBackdrop": "a-UL2G_modalBackdrop",
1789
+ "modalFooter": "a-UL2G_modalFooter",
1790
+ "modalTitle": "a-UL2G_modalTitle",
1791
+ "moveRow": "a-UL2G_moveRow",
1792
+ "nova-task-board-view": "a-UL2G_nova-task-board-view",
1793
+ "primaryButton": "a-UL2G_primaryButton",
1794
+ "promptBlock": "a-UL2G_promptBlock",
1795
+ "scheduleInput": "a-UL2G_scheduleInput",
1796
+ "scheduleInputInvalid": "a-UL2G_scheduleInputInvalid",
1797
+ "scheduleMeta": "a-UL2G_scheduleMeta",
1798
+ "scheduleMode": "a-UL2G_scheduleMode",
1799
+ "scheduleModeActive": "a-UL2G_scheduleModeActive",
1800
+ "scheduleModeButton": "a-UL2G_scheduleModeButton",
1801
+ "schedulePreset": "a-UL2G_schedulePreset",
1802
+ "scheduleRow": "a-UL2G_scheduleRow",
1803
+ "scheduleToggle": "a-UL2G_scheduleToggle",
1804
+ "search": "a-UL2G_search",
1805
+ "select": "a-UL2G_select",
1806
+ "sourceRow": "a-UL2G_sourceRow",
1807
+ "splitRow": "a-UL2G_splitRow",
1808
+ "splitRowHeader": "a-UL2G_splitRowHeader",
1809
+ "splitRowIndex": "a-UL2G_splitRowIndex",
1810
+ "splitRows": "a-UL2G_splitRows",
1811
+ "statusBadge": "a-UL2G_statusBadge",
1812
+ "statusDot": "a-UL2G_statusDot",
1813
+ "tagEditor": "a-UL2G_tagEditor",
1814
+ "tagEditorChip": "a-UL2G_tagEditorChip",
1815
+ "tagEditorRemove": "a-UL2G_tagEditorRemove"
1816
+ };
1576
1817
  //#endregion
1577
1818
  //#region src/client/board/ConfirmDialog.tsx
1578
1819
  /**
@@ -2930,79 +3171,6 @@ window.__ModuleLoader__.load({
2930
3171
  /** Memoized card: re-renders only when the card's own props change. */
2931
3172
  const TaskCard = (0, react.memo)(TaskCardInner);
2932
3173
  //#endregion
2933
- //#region src/core/cron.ts
2934
- /** 每字段的闭区间,按 cron 顺序。 */
2935
- const FIELD_RANGES = [
2936
- [0, 59],
2937
- [0, 23],
2938
- [1, 31],
2939
- [1, 12],
2940
- [0, 7]
2941
- ];
2942
- /**
2943
- * 解析一个 5 段 cron 表达式。
2944
- * @returns 各字段匹配集合;表达式非法时返回 null。
2945
- */
2946
- function parseCron(expr) {
2947
- const fields = expr.trim().split(/\s+/);
2948
- if (fields.length !== 5) return null;
2949
- const sets = [];
2950
- for (let index = 0; index < 5; index += 1) {
2951
- const [min, max] = FIELD_RANGES[index];
2952
- const set = /* @__PURE__ */ new Set();
2953
- if (!parseField(fields[index], min, max, set)) return null;
2954
- sets.push(set);
2955
- }
2956
- const weekdays = /* @__PURE__ */ new Set();
2957
- for (const day of sets[4]) weekdays.add(day === 7 ? 0 : day);
2958
- return {
2959
- minutes: sets[0],
2960
- hours: sets[1],
2961
- days: sets[2],
2962
- months: sets[3],
2963
- weekdays,
2964
- dayWildcard: fields[2] === "*",
2965
- weekdayWildcard: fields[4] === "*"
2966
- };
2967
- }
2968
- /** 表达式是否可解析(语法合法)。 */
2969
- function isValidCron(expr) {
2970
- return parseCron(expr) !== null;
2971
- }
2972
- /** 解析一个逗号列表字段,写入匹配集合;非法返回 false。 */
2973
- function parseField(field, min, max, out) {
2974
- if (field === "*") {
2975
- for (let value = min; value <= max; value += 1) out.add(value);
2976
- return true;
2977
- }
2978
- for (const part of field.split(",")) {
2979
- if (part === "") return false;
2980
- const [range, stepRaw] = part.split("/");
2981
- let low;
2982
- let high;
2983
- if (range === "*") {
2984
- low = min;
2985
- high = max;
2986
- } else if (range.includes("-")) {
2987
- const [a, b] = range.split("-");
2988
- if (a === "" || b === "" || !isDigits(a) || !isDigits(b)) return false;
2989
- low = Number(a);
2990
- high = Number(b);
2991
- } else if (isDigits(range)) {
2992
- low = Number(range);
2993
- high = Number(range);
2994
- } else return false;
2995
- if (low < min || high > max || low > high) return false;
2996
- const step = stepRaw === void 0 ? 1 : isDigits(stepRaw) ? Number(stepRaw) : NaN;
2997
- if (!Number.isInteger(step) || step < 1) return false;
2998
- for (let value = low; value <= high; value += step) out.add(value);
2999
- }
3000
- return true;
3001
- }
3002
- function isDigits(value) {
3003
- return /^\d+$/.test(value);
3004
- }
3005
- //#endregion
3006
3174
  //#region src/client/schedule-presets.ts
3007
3175
  /** 常用 cron 定时预设(cron → locale label)。 */
3008
3176
  const SCHEDULE_PRESETS = [
@@ -3071,6 +3239,9 @@ window.__ModuleLoader__.load({
3071
3239
  * - 执行 Prompt 为空时以标题呈现;归档任务隐藏执行设置(只读,§19-6);
3072
3240
  * - running/pending 时相关按钮禁用;执行按钮经 `executionCapable`(T005 起恒
3073
3241
  * true,协议已并入 run/rerun)门控;
3242
+ * - running/带未结算执行时提供「停止执行」(§11.2 cancel-execution,二次确认):
3243
+ * 结算 cancelled → 任务回落 todo、running 独占释放,随后可移动/删除/归档;
3244
+ * 同一状态下面板不再给出会被 Host 拒绝的「删除」按钮(§12.5 按钮禁用);
3074
3245
  * - 执行历史:结果徽标 + 起止时间 + 一键跳转会话 transcript + 错误信息
3075
3246
  * (§19-5;会话跳转经 sessions.open,会话由 HostRunner 创建,T005);
3076
3247
  * - 状态移动仅提供合法目标(待规划/待办;done/failed 移回视为重开,不触发执行);
@@ -3611,6 +3782,7 @@ window.__ModuleLoader__.load({
3611
3782
  /** 任务详情浮层。 */
3612
3783
  function TaskDetail({ controller, task }) {
3613
3784
  const [confirmDelete, setConfirmDelete] = (0, react.useState)(false);
3785
+ const [confirmStop, setConfirmStop] = (0, react.useState)(false);
3614
3786
  const [showSplit, setShowSplit] = (0, react.useState)(false);
3615
3787
  const [latest, setLatest] = (0, react.useState)(task);
3616
3788
  (0, react.useEffect)(() => {
@@ -3619,11 +3791,12 @@ window.__ModuleLoader__.load({
3619
3791
  const current = latest;
3620
3792
  const snapshot = controller.getSnapshot();
3621
3793
  const running = current.status === "running";
3794
+ const openExecution = current.executions.some((execution) => execution.endedAt === void 0);
3622
3795
  const archived = current.archivedAt !== void 0;
3623
3796
  const pending = snapshot.pendingTaskIds.includes(current.id);
3624
3797
  const transportError = snapshot.transportError;
3625
3798
  const timeZone = snapshot.host?.scheduler.timeZone;
3626
- const canRun = snapshot.executionCapable && !running && !pending;
3799
+ const canRun = snapshot.executionCapable && !running && !openExecution && !pending;
3627
3800
  const parent = current.parentId === void 0 ? void 0 : snapshot.tasks.find((candidate) => candidate.id === current.parentId);
3628
3801
  const children = snapshot.tasks.filter((candidate) => candidate.parentId === current.id && candidate.archivedAt === void 0);
3629
3802
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -3669,7 +3842,7 @@ window.__ModuleLoader__.load({
3669
3842
  children: [
3670
3843
  t("board.hostError", { error: transportError }),
3671
3844
  " ",
3672
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3845
+ snapshot.transportErrorRetryable === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3673
3846
  type: "button",
3674
3847
  className: board_module_css_default.linkButton,
3675
3848
  onClick: () => {
@@ -3798,6 +3971,15 @@ window.__ModuleLoader__.load({
3798
3971
  },
3799
3972
  children: t("split.entry")
3800
3973
  }),
3974
+ !archived && (running || openExecution) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3975
+ type: "button",
3976
+ className: board_module_css_default.dangerButton,
3977
+ disabled: pending,
3978
+ onClick: () => {
3979
+ setConfirmStop(true);
3980
+ },
3981
+ children: t("detail.stop")
3982
+ }),
3801
3983
  !archived && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3802
3984
  type: "button",
3803
3985
  className: board_module_css_default.primaryButton,
@@ -3829,7 +4011,7 @@ window.__ModuleLoader__.load({
3829
4011
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3830
4012
  type: "button",
3831
4013
  className: board_module_css_default.dangerButton,
3832
- disabled: pending,
4014
+ disabled: pending || running || openExecution,
3833
4015
  onClick: () => {
3834
4016
  setConfirmDelete(true);
3835
4017
  },
@@ -3861,6 +4043,19 @@ window.__ModuleLoader__.load({
3861
4043
  controller.deleteTask(current.id);
3862
4044
  }
3863
4045
  }),
4046
+ confirmStop && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmDialog, {
4047
+ title: t("stop.title"),
4048
+ message: t("stop.confirm", { name: current.title }),
4049
+ confirmLabel: t("stop.ok"),
4050
+ danger: true,
4051
+ onCancel: () => {
4052
+ setConfirmStop(false);
4053
+ },
4054
+ onConfirm: () => {
4055
+ setConfirmStop(false);
4056
+ controller.cancelExecution(current.id);
4057
+ }
4058
+ }),
3864
4059
  showSplit && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RequirementSplitModal, {
3865
4060
  controller,
3866
4061
  prefill: {
@@ -4489,7 +4684,7 @@ window.__ModuleLoader__.load({
4489
4684
  children: [
4490
4685
  t("board.hostError", { error: snapshot.transportError }),
4491
4686
  " ",
4492
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4687
+ snapshot.transportErrorRetryable === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4493
4688
  type: "button",
4494
4689
  className: board_module_css_default.linkButton,
4495
4690
  onClick: () => {
@@ -5164,127 +5359,6 @@ window.__ModuleLoader__.load({
5164
5359
  };
5165
5360
  }
5166
5361
  //#endregion
5167
- //#region src/protocol.ts
5168
- /** 本项目 API 前缀(AGENTS.md D2,2026-08-22 定值;参考实现占用 /api/task-board)。 */
5169
- const TASK_BOARD_API_PREFIX = "/api/nova-task-board";
5170
- //#endregion
5171
- //#region src/client/host-api.ts
5172
- /** 单次 Host 请求超时(§17 性能边界;超时按传输错误暴露,可重试)。 */
5173
- const REQUEST_TIMEOUT_MS = 15e3;
5174
- /** v1 迁移标记:本项目账本的 ledgerId(Host 确认后写入,防止重复导入)。 */
5175
- const IMPORT_MARKER = "dsh.novaTaskBoard.v2.hostImported";
5176
- /** v1 迁移 sourceId(一次迁移一个,写入后持久)。 */
5177
- const SOURCE_KEY = "dsh.novaTaskBoard.v2.sourceId";
5178
- /** v1 迁移 requestId(跨 Host 重启重试保持幂等)。 */
5179
- const IMPORT_REQUEST_KEY = "dsh.novaTaskBoard.v2.importRequestId";
5180
- /** 浏览器环境可用的 uuid(无 crypto 时退化为时间戳 + 随机串)。 */
5181
- function uuid() {
5182
- return globalThis.crypto?.randomUUID?.() ?? `browser-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
5183
- }
5184
- /** 读 JSON 响应体;非 2xx 按 Host 的错误信息抛错(§11.1 400/403/413/415)。 */
5185
- async function readJson(response) {
5186
- const body = await response.json();
5187
- if (!response.ok) throw new Error(body.error ?? `task-board request failed: ${response.status}`);
5188
- return body;
5189
- }
5190
- /**
5191
- * 真实 HTTP + SSE 传输(同源 `/api/nova-task-board/*`)。
5192
- * @param markers - 迁移标记存储(缺省用全局 localStorage)。
5193
- */
5194
- var HttpTaskBoardHostTransport = class {
5195
- markers;
5196
- constructor(markers = globalThis.localStorage) {
5197
- this.markers = markers;
5198
- }
5199
- async bootstrap(legacy) {
5200
- const initial = await this.state();
5201
- const ledgerId = initial.scheduler.ledgerId;
5202
- if (legacy.length > 0 && ledgerId !== void 0 && this.markers?.getItem(IMPORT_MARKER) !== ledgerId) {
5203
- let sourceId = this.markers?.getItem(SOURCE_KEY);
5204
- if (sourceId === null || sourceId === void 0 || sourceId === "") {
5205
- sourceId = uuid();
5206
- this.markers?.setItem(SOURCE_KEY, sourceId);
5207
- }
5208
- let requestId = this.markers?.getItem(IMPORT_REQUEST_KEY);
5209
- if (requestId === null || requestId === void 0 || requestId === "") {
5210
- requestId = uuid();
5211
- this.markers?.setItem(IMPORT_REQUEST_KEY, requestId);
5212
- }
5213
- const snapshot = await this.post(requestId, {
5214
- kind: "import",
5215
- sourceId,
5216
- tasks: [...legacy]
5217
- });
5218
- this.markers?.setItem(IMPORT_MARKER, snapshot.scheduler.ledgerId ?? ledgerId);
5219
- return snapshot;
5220
- }
5221
- return initial;
5222
- }
5223
- async state() {
5224
- return await this.request(`${TASK_BOARD_API_PREFIX}/state`, { cache: "no-store" });
5225
- }
5226
- async action(action) {
5227
- return await this.post(uuid(), action);
5228
- }
5229
- async post(requestId, action) {
5230
- const envelope = {
5231
- requestId,
5232
- action
5233
- };
5234
- return await this.request(`${TASK_BOARD_API_PREFIX}/action`, {
5235
- method: "POST",
5236
- headers: { "content-type": "application/json" },
5237
- body: JSON.stringify(envelope)
5238
- });
5239
- }
5240
- /** 同源 fetch,带超时(超时按传输错误抛,控制器转为可重试的错误条)。 */
5241
- async request(url, init) {
5242
- const controller = new AbortController();
5243
- const timeout = globalThis.setTimeout(() => {
5244
- controller.abort();
5245
- }, REQUEST_TIMEOUT_MS);
5246
- try {
5247
- return await readJson(await fetch(url, {
5248
- ...init,
5249
- signal: controller.signal
5250
- }));
5251
- } catch (error) {
5252
- if (controller.signal.aborted) throw new Error(`task-board Host request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s`);
5253
- throw error;
5254
- } finally {
5255
- globalThis.clearTimeout(timeout);
5256
- }
5257
- }
5258
- /**
5259
- * SSE 订阅:消息帧解析为 `{revision, scheduler, power}`(解析失败按同步信号
5260
- * 处理,重拉兜底);`onopen`(含断线重连成功)与页面重新可见 → 同步信号,
5261
- * 控制器据此重拉全量 state。
5262
- */
5263
- subscribe(listener) {
5264
- const events = new EventSource(`${TASK_BOARD_API_PREFIX}/events`);
5265
- events.onmessage = (message) => {
5266
- try {
5267
- const parsed = JSON.parse(message.data);
5268
- if (parsed === null || typeof parsed !== "object" || typeof parsed.revision !== "number") throw new Error("invalid event frame");
5269
- listener(parsed);
5270
- } catch {
5271
- listener();
5272
- }
5273
- };
5274
- events.onopen = () => {
5275
- listener();
5276
- };
5277
- const onVisible = () => {
5278
- if (document.visibilityState === "visible") listener();
5279
- };
5280
- document.addEventListener("visibilitychange", onVisible);
5281
- return () => {
5282
- document.removeEventListener("visibilitychange", onVisible);
5283
- events.close();
5284
- };
5285
- }
5286
- };
5287
- //#endregion
5288
5362
  //#region src/client/legacy-store.ts
5289
5363
  /** 参考实现的 v1 浏览器账本键(只读来源;其内部键名 `dsh.taskBoard.v1`)。 */
5290
5364
  const LEGACY_V1_STORAGE_KEY = "dsh.taskBoard.v1";