@william2000/dsh-nova-ui-task-board 0.1.1 → 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 +1519 -1382
- package/lib/client.js.map +1 -1
- package/lib/index.js +78 -1
- package/lib/types/client/NovaPluginsSection.d.ts +27 -0
- package/lib/types/client/board-controller.d.ts +17 -1
- package/lib/types/client/host-api.d.ts +9 -0
- package/lib/types/client/index.d.ts +11 -8
- package/lib/types/client/locales.d.ts +18 -0
- package/lib/types/core/transitions.d.ts +18 -0
- package/lib/types/host-runner.d.ts +11 -0
- package/lib/types/host-service.d.ts +12 -0
- package/lib/types/index.d.ts +1 -1
- package/lib/types/protocol.d.ts +4 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -7,695 +7,1259 @@ 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/
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
/**
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
92
|
+
* cron 表达式的基础形状校验(§9.4:5 段 cron「分 时 日 月 周」)。
|
|
93
|
+
* 这是结构级校验:段数、字符集(数字、`* , - / ?` 及名字字母)。cron 的到期
|
|
94
|
+
* 计算与完整合法性由 T006 的调度器实现;本层只保证持久化的规则形状可解析。
|
|
34
95
|
*/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
*
|
|
46
|
-
*
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
262
|
-
this.
|
|
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
|
-
|
|
268
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
* runAt / 归档 / proposed 等)→ transportError,账本不变。
|
|
512
|
+
* SSE 订阅:消息帧解析为 `{revision, scheduler, power}`(解析失败按同步信号
|
|
513
|
+
* 处理,重拉兜底);`onopen`(含断线重连成功)与页面重新可见 → 同步信号,
|
|
514
|
+
* 控制器据此重拉全量 state。
|
|
317
515
|
*/
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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;
|
|
607
|
+
}
|
|
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
|
+
}));
|
|
355
616
|
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
|
-
}
|
|
365
617
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
this.
|
|
618
|
+
/** 停止全部订阅并释放状态(幂等)。 */
|
|
619
|
+
dispose() {
|
|
620
|
+
for (const dispose of this.disposers.splice(0)) dispose();
|
|
621
|
+
this.listeners.clear();
|
|
622
|
+
}
|
|
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
|
+
};
|
|
639
|
+
}
|
|
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;
|
|
370
658
|
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
659
|
}
|
|
378
|
-
|
|
379
|
-
if (this.
|
|
380
|
-
|
|
381
|
-
this.
|
|
382
|
-
try {
|
|
383
|
-
return await initialization;
|
|
384
|
-
} finally {
|
|
385
|
-
if (this.remoteInitialization === initialization) this.remoteInitialization = void 0;
|
|
386
|
-
}
|
|
660
|
+
closeBoard() {
|
|
661
|
+
if (!this.boardOpen) return;
|
|
662
|
+
this.boardOpen = false;
|
|
663
|
+
this.notify();
|
|
387
664
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
try {
|
|
392
|
-
this.acceptRemote(await transport.bootstrap(this.deps.legacy?.() ?? []));
|
|
393
|
-
if (!this.remoteSubscribed) {
|
|
394
|
-
this.remoteSubscribed = true;
|
|
395
|
-
this.disposers.push(transport.subscribe((event) => {
|
|
396
|
-
this.onRemoteEvent(event);
|
|
397
|
-
}));
|
|
398
|
-
}
|
|
399
|
-
return true;
|
|
400
|
-
} catch (error) {
|
|
401
|
-
this.transportError = messageOf(error);
|
|
402
|
-
this.notify();
|
|
403
|
-
return false;
|
|
404
|
-
}
|
|
665
|
+
toggleBoard() {
|
|
666
|
+
if (this.boardOpen) this.closeBoard();
|
|
667
|
+
else this.openBoard();
|
|
405
668
|
}
|
|
406
669
|
/**
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
* 全量 state。
|
|
670
|
+
* 切换主看板/归档视图。离开归档视图时若选中任务仍是归档任务则清除选中
|
|
671
|
+
* ——详情不得悬停在已离开看板的任务上。
|
|
410
672
|
*/
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
scheduler: event.scheduler,
|
|
416
|
-
power: event.power
|
|
417
|
-
};
|
|
418
|
-
this.notify();
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
this.refreshRemote();
|
|
422
|
-
}
|
|
423
|
-
async refreshRemote(preserveError) {
|
|
424
|
-
const transport = this.deps.transport;
|
|
425
|
-
if (transport === void 0) return true;
|
|
426
|
-
try {
|
|
427
|
-
this.acceptRemote(await transport.state());
|
|
428
|
-
if (preserveError !== void 0) {
|
|
429
|
-
this.transportError = preserveError;
|
|
430
|
-
this.notify();
|
|
431
|
-
}
|
|
432
|
-
return true;
|
|
433
|
-
} catch (error) {
|
|
434
|
-
this.transportError = preserveError ?? messageOf(error);
|
|
435
|
-
this.notify();
|
|
436
|
-
return false;
|
|
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;
|
|
437
677
|
}
|
|
678
|
+
this.notify();
|
|
438
679
|
}
|
|
439
680
|
/**
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
* 选中。返回快照是否被接受。
|
|
681
|
+
* 切换列内排序视图模式(T009 §12.10):手动顺序/按项目分组/按标签分组。
|
|
682
|
+
* 分组是视图层能力,不改底层 order 与归属数据;模式仅会话级记忆(刷新重置)。
|
|
443
683
|
*/
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const sameGeneration = current === void 0 || current.scheduler.ledgerId === nextLedgerId;
|
|
448
|
-
if (sameGeneration && current !== void 0 && snapshot.revision < current.revision) return false;
|
|
449
|
-
const preserve = sameGeneration && current !== void 0 && snapshot.revision >= current.revision;
|
|
450
|
-
this.tasks = mergeTasksIdentityPreserving(this.tasks, snapshot.tasks, preserve);
|
|
451
|
-
this.hostState = {
|
|
452
|
-
revision: snapshot.revision,
|
|
453
|
-
scheduler: snapshot.scheduler,
|
|
454
|
-
power: snapshot.power
|
|
455
|
-
};
|
|
456
|
-
this.transportError = void 0;
|
|
457
|
-
if (this.selectedTaskId !== void 0 && !this.tasks.some((task) => task.id === this.selectedTaskId)) this.selectedTaskId = void 0;
|
|
458
|
-
if (!this.archiveView && this.selectedTaskId !== void 0 && this.tasks.find((task) => task.id === this.selectedTaskId)?.archivedAt !== void 0) this.selectedTaskId = void 0;
|
|
684
|
+
setGroupMode(mode) {
|
|
685
|
+
if (this.groupMode === mode) return;
|
|
686
|
+
this.groupMode = mode;
|
|
459
687
|
this.notify();
|
|
460
|
-
return true;
|
|
461
688
|
}
|
|
462
|
-
|
|
463
|
-
|
|
689
|
+
openTask(id) {
|
|
690
|
+
if (this.tasks.some((task) => task.id === id)) {
|
|
691
|
+
this.selectedTaskId = id;
|
|
692
|
+
this.notify();
|
|
693
|
+
}
|
|
464
694
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
function claimNovaTaskBoardApply() {
|
|
470
|
-
if (globalThis.__dshNovaTaskBoardApplied === true) return false;
|
|
471
|
-
globalThis.__dshNovaTaskBoardApplied = true;
|
|
472
|
-
return true;
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* Releases the claim. Called from the client fiber cleanup so that a
|
|
476
|
-
* hot-reloaded bundle (the loader unloads the old plugin fiber and invokes
|
|
477
|
-
* the rebuilt one in the same page) can claim again instead of being
|
|
478
|
-
* silently dropped.
|
|
479
|
-
*/
|
|
480
|
-
function releaseNovaTaskBoardApply() {
|
|
481
|
-
globalThis.__dshNovaTaskBoardApplied = void 0;
|
|
482
|
-
}
|
|
483
|
-
//#endregion
|
|
484
|
-
//#region src/client/grouping.ts
|
|
485
|
-
/** 全部模式(顶部切换按钮按此顺序渲染)。 */
|
|
486
|
-
const GROUP_MODES = [
|
|
487
|
-
"manual",
|
|
488
|
-
"project",
|
|
489
|
-
"tag"
|
|
490
|
-
];
|
|
491
|
-
/** 与 Host transitions.byOrderStable 一致的列内排序(order 升序,createdAt/id 决胜)。 */
|
|
492
|
-
function byOrderStable(a, b) {
|
|
493
|
-
return a.order - b.order || a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
494
|
-
}
|
|
495
|
-
/** 列内任务按 order 升序排序(原数组不改动)。 */
|
|
496
|
-
function sortByOrder(tasks) {
|
|
497
|
-
return [...tasks].sort(byOrderStable);
|
|
498
|
-
}
|
|
499
|
-
/** 任务在给定视图模式下的组 key(手动模式无组概念,返回 undefined)。 */
|
|
500
|
-
function groupKeyOf(task, mode) {
|
|
501
|
-
if (mode === "project") return task.project ?? "";
|
|
502
|
-
if (mode === "tag") return task.tags.length > 0 ? task.tags[0] : "";
|
|
503
|
-
}
|
|
504
|
-
/** 任务是否属于某个组(按模式解释 key:项目精确匹配 / 标签命中 / 未分类无归属)。 */
|
|
505
|
-
function groupContains(task, groupKey, mode) {
|
|
506
|
-
if (mode === "project") return groupKey === "" ? task.project === void 0 : task.project === groupKey;
|
|
507
|
-
if (mode === "tag") return groupKey === "" ? task.tags.length === 0 : task.tags.includes(groupKey);
|
|
508
|
-
return false;
|
|
509
|
-
}
|
|
510
|
-
/**
|
|
511
|
-
* 拖拽目标组的归属变换(视图层预览用,与提交的 reorder 归属补丁一致,§12.10):
|
|
512
|
-
* - 按项目分组:拖到组 G → project = G;拖到「未分类」→ 清除 project;
|
|
513
|
-
* - 按标签分组:拖到组 G → G 成为首选标签(tags[0],G 已含于 tags 时仅调整顺序);
|
|
514
|
-
* 拖到「未分类」→ 移除首选标签(tags.shift(),若 tags 为空则归「未分类」);
|
|
515
|
-
* - 手动模式或非组落位 → 原样返回。
|
|
516
|
-
*/
|
|
517
|
-
function applyOwnershipTransform(task, groupKey, mode) {
|
|
518
|
-
if (mode === "manual" || groupKey === void 0) return task;
|
|
519
|
-
if (mode === "project") return groupKey === "" ? {
|
|
520
|
-
...task,
|
|
521
|
-
project: void 0
|
|
522
|
-
} : {
|
|
523
|
-
...task,
|
|
524
|
-
project: groupKey
|
|
525
|
-
};
|
|
526
|
-
if (groupKey === "") return {
|
|
527
|
-
...task,
|
|
528
|
-
tags: task.tags.slice(1)
|
|
529
|
-
};
|
|
530
|
-
return {
|
|
531
|
-
...task,
|
|
532
|
-
tags: [groupKey, ...task.tags.filter((tag) => tag !== groupKey)]
|
|
533
|
-
};
|
|
534
|
-
}
|
|
535
|
-
/**
|
|
536
|
-
* 分组(§12.10):项目/标签模式下的归组展示,组内按 order 升序;标签模式多归组
|
|
537
|
-
* (一个任务可出现在其全部标签组),无标签归「未分类」;未分类组恒在最后。
|
|
538
|
-
* 手动模式不分组(调用方直接 sortByOrder)。
|
|
539
|
-
*/
|
|
540
|
-
function groupTasks(tasks, mode) {
|
|
541
|
-
if (mode === "project") {
|
|
542
|
-
const byProject = /* @__PURE__ */ new Map();
|
|
543
|
-
for (const task of tasks) {
|
|
544
|
-
const key = task.project ?? "";
|
|
545
|
-
const list = byProject.get(key);
|
|
546
|
-
if (list === void 0) byProject.set(key, [task]);
|
|
547
|
-
else list.push(task);
|
|
548
|
-
}
|
|
549
|
-
return groupsFromMap(byProject);
|
|
695
|
+
closeTask() {
|
|
696
|
+
if (this.selectedTaskId === void 0) return;
|
|
697
|
+
this.selectedTaskId = void 0;
|
|
698
|
+
this.notify();
|
|
550
699
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
700
|
+
/**
|
|
701
|
+
* 经 Host 创建任务;**只有 Host 确认后**才在账本可见并返回任务(NewTaskModal
|
|
702
|
+
* 据此决定关闭/重试)。失败返回 undefined 并置 transportError。
|
|
703
|
+
*/
|
|
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;
|
|
563
711
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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;
|
|
725
|
+
}
|
|
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);
|
|
736
|
+
}
|
|
737
|
+
/** dismiss(T011 §10.2/§11.2):拒绝/忽略候选任务(删除)。 */
|
|
738
|
+
dismissTask(id) {
|
|
739
|
+
this.commitRemote({
|
|
740
|
+
kind: "dismiss",
|
|
741
|
+
taskId: id
|
|
742
|
+
}, id);
|
|
743
|
+
}
|
|
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
|
|
579
756
|
});
|
|
580
757
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
});
|
|
588
|
-
return groups;
|
|
589
|
-
}
|
|
590
|
-
//#endregion
|
|
591
|
-
//#region src/client/drag-utils.ts
|
|
592
|
-
/**
|
|
593
|
-
* 拖拽合法性(§10.2/§11.2):running 不可作目标;同列 = 列内排序;跨列目标仅限
|
|
594
|
-
* backlog/todo(done/failed 拖回为重开)。proposed/归档不可作为来源由调用方
|
|
595
|
-
* (卡片 draggable=false)保证。
|
|
596
|
-
*/
|
|
597
|
-
function isLegalDrop(origin, targetStatus) {
|
|
598
|
-
if (targetStatus === "running") return false;
|
|
599
|
-
if (targetStatus === origin.fromStatus) return true;
|
|
600
|
-
return targetStatus === "backlog" || targetStatus === "todo";
|
|
601
|
-
}
|
|
602
|
-
/** 目标组尾在基准列(按 order 升序)中的最后一张卡的下标;组空返回 -1。 */
|
|
603
|
-
function lastGroupIndex(column, groupKey, mode) {
|
|
604
|
-
let last = -1;
|
|
605
|
-
for (let index = 0; index < column.length; index += 1) if (groupContains(column[index], groupKey, mode)) last = index;
|
|
606
|
-
return last;
|
|
607
|
-
}
|
|
608
|
-
/**
|
|
609
|
-
* 计算落位下标(0-based,基准 = 剔除拖拽卡片后的目标列,按 order 稳定升序;
|
|
610
|
-
* 与 Host applyReposition 的 insert 基准一致):
|
|
611
|
-
* - 锚定卡片:卡片下标(after +1);
|
|
612
|
-
* - 分组视图组尾:目标组最后一张卡下标 +1(组空回退列尾);
|
|
613
|
-
* - 列尾:可见列表最后一张卡下标 +1(过滤定位;无可见卡则列尾)。
|
|
614
|
-
*/
|
|
615
|
-
function computeDropIndex(restColumn, target, mode, lastVisibleId) {
|
|
616
|
-
if (target.anchorId !== void 0) {
|
|
617
|
-
const anchor = restColumn.findIndex((task) => task.id === target.anchorId);
|
|
618
|
-
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);
|
|
619
764
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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);
|
|
623
778
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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);
|
|
627
794
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
*/
|
|
634
|
-
function visibleDropIndex(list, target) {
|
|
635
|
-
if (target.anchorId !== void 0) {
|
|
636
|
-
const index = list.findIndex((task) => task.id === target.anchorId);
|
|
637
|
-
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);
|
|
638
800
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
kind: "
|
|
663
|
-
taskId:
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
|
668
877
|
};
|
|
878
|
+
this.notify();
|
|
669
879
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
status: target.status,
|
|
674
|
-
order: index
|
|
675
|
-
};
|
|
676
|
-
}
|
|
677
|
-
//#endregion
|
|
678
|
-
//#region src/client/filter.ts
|
|
679
|
-
/** 空过滤(看板初始状态)。 */
|
|
680
|
-
const EMPTY_FILTER = {
|
|
681
|
-
query: "",
|
|
682
|
-
project: { kind: "all" },
|
|
683
|
-
tags: []
|
|
684
|
-
};
|
|
685
|
-
/** 单条任务是否命中过滤(多条件 AND、标签内 OR)。 */
|
|
686
|
-
function matchesFilter(task, filter) {
|
|
687
|
-
const query = filter.query.trim().toLowerCase();
|
|
688
|
-
if (query !== "" && !task.title.toLowerCase().includes(query) && !task.description.toLowerCase().includes(query)) return false;
|
|
689
|
-
if (filter.project.kind === "none") {
|
|
690
|
-
if (task.project !== void 0) return false;
|
|
691
|
-
} else if (filter.project.kind === "name") {
|
|
692
|
-
if (task.project !== filter.project.name) return false;
|
|
880
|
+
/** 跳转执行会话 transcript(选择会话 → current 变化 → 看板自动关闭)。 */
|
|
881
|
+
openSession(sessionId) {
|
|
882
|
+
this.deps.sessions.open(sessionId);
|
|
693
883
|
}
|
|
694
|
-
|
|
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();
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
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;
|
|
695
1036
|
return true;
|
|
696
1037
|
}
|
|
697
|
-
/**
|
|
698
|
-
|
|
1038
|
+
/**
|
|
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.
|
|
1043
|
+
*/
|
|
1044
|
+
function releaseNovaTaskBoardApply() {
|
|
1045
|
+
globalThis.__dshNovaTaskBoardApplied = void 0;
|
|
1046
|
+
}
|
|
1047
|
+
//#endregion
|
|
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);
|
|
1058
|
+
}
|
|
1059
|
+
/** 列内任务按 order 升序排序(原数组不改动)。 */
|
|
1060
|
+
function sortByOrder(tasks) {
|
|
1061
|
+
return [...tasks].sort(byOrderStable);
|
|
1062
|
+
}
|
|
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] : "";
|
|
1067
|
+
}
|
|
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;
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* 拖拽目标组的归属变换(视图层预览用,与提交的 reorder 归属补丁一致,§12.10):
|
|
1076
|
+
* - 按项目分组:拖到组 G → project = G;拖到「未分类」→ 清除 project;
|
|
1077
|
+
* - 按标签分组:拖到组 G → G 成为首选标签(tags[0],G 已含于 tags 时仅调整顺序);
|
|
1078
|
+
* 拖到「未分类」→ 移除首选标签(tags.shift(),若 tags 为空则归「未分类」);
|
|
1079
|
+
* - 手动模式或非组落位 → 原样返回。
|
|
1080
|
+
*/
|
|
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
|
|
1089
|
+
};
|
|
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)]
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* 分组(§12.10):项目/标签模式下的归组展示,组内按 order 升序;标签模式多归组
|
|
1101
|
+
* (一个任务可出现在其全部标签组),无标签归「未分类」;未分类组恒在最后。
|
|
1102
|
+
* 手动模式不分组(调用方直接 sortByOrder)。
|
|
1103
|
+
*/
|
|
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);
|
|
1114
|
+
}
|
|
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
|
+
}] : []];
|
|
1133
|
+
}
|
|
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
|
+
});
|
|
1144
|
+
}
|
|
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;
|
|
1153
|
+
}
|
|
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";
|
|
1165
|
+
}
|
|
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);
|
|
1183
|
+
}
|
|
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;
|
|
1193
|
+
}
|
|
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
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
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;
|
|
1257
|
+
}
|
|
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) {
|
|
699
1263
|
return tasks.filter((task) => matchesFilter(task, filter));
|
|
700
1264
|
}
|
|
701
1265
|
/** 过滤是否处于激活状态(任一维度有约束;用于「清除全部」显隐与命中计数)。 */
|
|
@@ -727,8 +1291,8 @@ window.__ModuleLoader__.load({
|
|
|
727
1291
|
*/
|
|
728
1292
|
/** zh dictionary (key-set source of truth). */
|
|
729
1293
|
const zh = {
|
|
730
|
-
"entry.label": "任务看板",
|
|
731
|
-
"board.title": "任务看板",
|
|
1294
|
+
"entry.label": "Nova 任务看板",
|
|
1295
|
+
"board.title": "Nova 任务看板",
|
|
732
1296
|
"board.close": "返回会话",
|
|
733
1297
|
"board.new": "新建任务",
|
|
734
1298
|
"board.search": "筛选任务…",
|
|
@@ -768,6 +1332,7 @@ window.__ModuleLoader__.load({
|
|
|
768
1332
|
"detail.noExecution": "尚未执行",
|
|
769
1333
|
"detail.run": "执行",
|
|
770
1334
|
"detail.rerun": "重新执行",
|
|
1335
|
+
"detail.stop": "停止执行",
|
|
771
1336
|
"detail.delete": "删除",
|
|
772
1337
|
"detail.archive": "归档",
|
|
773
1338
|
"detail.restore": "恢复",
|
|
@@ -783,6 +1348,9 @@ window.__ModuleLoader__.load({
|
|
|
783
1348
|
"delete.confirm": "确定删除「{name}」吗?删除后不可恢复。",
|
|
784
1349
|
"delete.ok": "删除",
|
|
785
1350
|
"delete.cancel": "取消",
|
|
1351
|
+
"stop.title": "停止执行",
|
|
1352
|
+
"stop.confirm": "确定停止「{name}」的执行吗?执行将结算为已取消,任务回到待办。",
|
|
1353
|
+
"stop.ok": "停止",
|
|
786
1354
|
"status.move.backlog": "移到待规划",
|
|
787
1355
|
"status.move.todo": "移到待办",
|
|
788
1356
|
"time.justNow": "刚刚",
|
|
@@ -901,19 +1469,19 @@ window.__ModuleLoader__.load({
|
|
|
901
1469
|
"split.parentOf": "所属需求",
|
|
902
1470
|
"split.parentMissing": "(父任务已删除)",
|
|
903
1471
|
"split.children": "子任务",
|
|
904
|
-
"settings.title": "任务看板",
|
|
905
|
-
"settings.description": "控制 Host 任务看板与 agent 播报。",
|
|
906
|
-
"settings.enabled": "
|
|
1472
|
+
"settings.title": "Nova 任务看板",
|
|
1473
|
+
"settings.description": "控制 Host Nova 任务看板与 agent 播报。",
|
|
1474
|
+
"settings.enabled": "启用 Nova 任务看板",
|
|
907
1475
|
"settings.enabledHint": "关闭后隐藏侧边栏入口与看板视图。",
|
|
908
|
-
"settings.announceToAgent": "向 agent
|
|
1476
|
+
"settings.announceToAgent": "向 agent 播报 Nova 任务看板",
|
|
909
1477
|
"settings.announceToAgentHint": "开启:每条 agent 系统提示都会包含本看板的说明;关闭:不播报。",
|
|
910
1478
|
"settings.notExposed": "当前 DSH 版本未向设置页暴露本插件的配置命名空间,表单不可用。可编辑 ~/.dsh/settings.yaml 直接配置。",
|
|
911
1479
|
"settings.readOnly": "当前部署的设置只读。"
|
|
912
1480
|
};
|
|
913
1481
|
/** en dictionary, complete against the zh key set. */
|
|
914
1482
|
const en = {
|
|
915
|
-
"entry.label": "Task Board",
|
|
916
|
-
"board.title": "Task Board",
|
|
1483
|
+
"entry.label": "Nova Task Board",
|
|
1484
|
+
"board.title": "Nova Task Board",
|
|
917
1485
|
"board.close": "Back to chat",
|
|
918
1486
|
"board.new": "New Task",
|
|
919
1487
|
"board.search": "Filter tasks…",
|
|
@@ -953,6 +1521,7 @@ window.__ModuleLoader__.load({
|
|
|
953
1521
|
"detail.noExecution": "Not executed yet",
|
|
954
1522
|
"detail.run": "Run",
|
|
955
1523
|
"detail.rerun": "Run Again",
|
|
1524
|
+
"detail.stop": "Stop",
|
|
956
1525
|
"detail.delete": "Delete",
|
|
957
1526
|
"detail.archive": "Archive",
|
|
958
1527
|
"detail.restore": "Restore",
|
|
@@ -968,6 +1537,9 @@ window.__ModuleLoader__.load({
|
|
|
968
1537
|
"delete.confirm": "Delete \"{name}\"? This cannot be undone.",
|
|
969
1538
|
"delete.ok": "Delete",
|
|
970
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",
|
|
971
1543
|
"status.move.backlog": "Move to Backlog",
|
|
972
1544
|
"status.move.todo": "Move to To Do",
|
|
973
1545
|
"time.justNow": "just now",
|
|
@@ -1001,555 +1573,247 @@ window.__ModuleLoader__.load({
|
|
|
1001
1573
|
"detail.oneShot.preset.tomorrow9": "Tomorrow 09:00",
|
|
1002
1574
|
"detail.executionSettings": "Execution Settings",
|
|
1003
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.",
|
|
1004
|
-
"new.workspace": "Workspace",
|
|
1005
|
-
"new.mode": "Mode",
|
|
1006
|
-
"new.permission": "Permission",
|
|
1007
|
-
"exec.workspace.recent": "Most recent (default)",
|
|
1008
|
-
"exec.mode.default": "Deployment default",
|
|
1009
|
-
"exec.mode.defaultSuffix": " (default)",
|
|
1010
|
-
"exec.mode.brokenSuffix": " (unavailable)",
|
|
1011
|
-
"exec.mode.removed": " (removed)",
|
|
1012
|
-
"exec.permission.default": "Session default",
|
|
1013
|
-
"exec.permission.read-only": "Read-only",
|
|
1014
|
-
"exec.permission.workspace-write": "Workspace Write",
|
|
1015
|
-
"exec.permission.danger-full-access": "Full Access",
|
|
1016
|
-
"filter.project": "Project",
|
|
1017
|
-
"filter.projectAll": "All projects",
|
|
1018
|
-
"filter.projectNone": "Unassigned",
|
|
1019
|
-
"filter.tags": "Tags",
|
|
1020
|
-
"filter.clearAll": "Clear all",
|
|
1021
|
-
"filter.hits": "{count} hits",
|
|
1022
|
-
"filter.removeTag": "Remove tag {tag}",
|
|
1023
|
-
"board.group.label": "Sort & group",
|
|
1024
|
-
"board.group.manual": "Manual",
|
|
1025
|
-
"board.group.project": "By project",
|
|
1026
|
-
"board.group.tag": "By tag",
|
|
1027
|
-
"board.group.ungrouped": "Unassigned",
|
|
1028
|
-
"board.dragRejected": "Cannot drop here (illegal target)",
|
|
1029
|
-
"detail.projectTags": "Project & Tags",
|
|
1030
|
-
"detail.projectPlaceholder": "Project (optional)",
|
|
1031
|
-
"new.tagsPlaceholder": "Type a tag and press Enter",
|
|
1032
|
-
"confirm.title": "Confirm candidate task",
|
|
1033
|
-
"confirm.subtitle": "Candidates cannot run, be scheduled, or be archived until confirmed. Edit and pick a target column, or dismiss.",
|
|
1034
|
-
"confirm.target": "Confirm into",
|
|
1035
|
-
"confirm.toBacklog": "Backlog",
|
|
1036
|
-
"confirm.toTodo": "To Do",
|
|
1037
|
-
"confirm.submit": "Confirm",
|
|
1038
|
-
"confirm.dismiss": "Dismiss",
|
|
1039
|
-
"confirm.dismissTitle": "Dismiss candidate task",
|
|
1040
|
-
"confirm.dismissMessage": "Dismiss \"{name}\"? The candidate task will be deleted.",
|
|
1041
|
-
"card.proposedBadge": "Pending",
|
|
1042
|
-
"card.source": "Source",
|
|
1043
|
-
"source.conversation": "Chat",
|
|
1044
|
-
"source.requirement": "Requirement",
|
|
1045
|
-
"entry.proposedCount": "{count} candidate(s) pending",
|
|
1046
|
-
"convert.title": "Turn into task",
|
|
1047
|
-
"convert.fromConversation": "From conversation",
|
|
1048
|
-
"convert.submitProposed": "Propose",
|
|
1049
|
-
"convert.submitDirect": "Create directly",
|
|
1050
|
-
"convert.directTarget": "Into",
|
|
1051
|
-
"convert.directBacklog": "Backlog",
|
|
1052
|
-
"convert.directTodo": "To Do",
|
|
1053
|
-
"convert.proposedHint": "Proposing adds the task to the \"Pending\" column; it runs only after confirmation.",
|
|
1054
|
-
"chat.extractedHint": "Extracted {count} candidate task(s) from the conversation (see the board \"Pending\" column).",
|
|
1055
|
-
"detail.jumpToConversation": "Back to chat",
|
|
1056
|
-
"detail.sourceConversation": "Source session",
|
|
1057
|
-
"split.entry": "Split from requirement",
|
|
1058
|
-
"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.",
|
|
1059
|
-
"split.recursiveTitle": "Recursive split",
|
|
1060
|
-
"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.",
|
|
1061
|
-
"split.requirementTitle": "Requirement title",
|
|
1062
|
-
"split.requirementTitlePlaceholder": "e.g. \"Login module\"",
|
|
1063
|
-
"split.source": "Requirement source",
|
|
1064
|
-
"split.sourceText": "Pasted text",
|
|
1065
|
-
"split.sourceFile": "Workspace file path",
|
|
1066
|
-
"split.text": "Requirement text",
|
|
1067
|
-
"split.textPlaceholder": "Paste the requirement document…",
|
|
1068
|
-
"split.file": "File path",
|
|
1069
|
-
"split.filePlaceholder": "Path relative to the workspace, e.g. docs/requirement.md",
|
|
1070
|
-
"split.required": "Requirement title and content (text or file path) are required",
|
|
1071
|
-
"split.submit": "Start split",
|
|
1072
|
-
"split.asyncHint": "Splitting runs in a dedicated session; results land in the \"Pending\" column (not executable until confirmed).",
|
|
1073
|
-
"split.batchButton": "Batch confirm",
|
|
1074
|
-
"split.batchTitle": "Batch confirm candidates",
|
|
1075
|
-
"split.batchSubtitle": "Edit items (title/description/prompt/project/tags), add or remove rows; confirm all into the chosen column, or dismiss all.",
|
|
1076
|
-
"split.addItem": "Add item",
|
|
1077
|
-
"split.added": "New",
|
|
1078
|
-
"split.removeItem": "Remove",
|
|
1079
|
-
"split.confirmAll": "Confirm all into {target}",
|
|
1080
|
-
"split.dismissAll": "Dismiss all",
|
|
1081
|
-
"split.dismissAllTitle": "Dismiss all candidates",
|
|
1082
|
-
"split.dismissAllMessage": "Dismiss all {count} candidate(s)? They will be deleted.",
|
|
1083
|
-
"split.dismissItemTitle": "Remove candidate",
|
|
1084
|
-
"split.dismissItemMessage": "Remove \"{name}\"? The candidate task will be deleted.",
|
|
1085
|
-
"split.empty": "No candidates to batch-confirm",
|
|
1086
|
-
"split.parentOf": "Parent requirement",
|
|
1087
|
-
"split.parentMissing": "(parent task deleted)",
|
|
1088
|
-
"split.children": "Subtasks",
|
|
1089
|
-
"settings.title": "Task Board",
|
|
1090
|
-
"settings.description": "Configure the Host task board and agent announcement.",
|
|
1091
|
-
"settings.enabled": "Enable the task board",
|
|
1092
|
-
"settings.enabledHint": "When off, the sidebar entry and board view are hidden.",
|
|
1093
|
-
"settings.announceToAgent": "Announce the task board to agents",
|
|
1094
|
-
"settings.announceToAgentHint": "On: every agent system prompt includes a note about this board. Off: no announcement.",
|
|
1095
|
-
"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.",
|
|
1096
|
-
"settings.readOnly": "This deployment stores settings read-only."
|
|
1097
|
-
};
|
|
1098
|
-
/** Active dictionary, picked by the document language at call time. */
|
|
1099
|
-
function dictionary() {
|
|
1100
|
-
return (typeof document !== "undefined" ? document.documentElement.lang : "zh").toLowerCase().startsWith("en") ? en : zh;
|
|
1101
|
-
}
|
|
1102
|
-
/** Translate a key with optional {name} template params. */
|
|
1103
|
-
function t(key, params) {
|
|
1104
|
-
let text = dictionary()[key];
|
|
1105
|
-
if (params !== void 0) for (const [name, value] of Object.entries(params)) text = text.replaceAll(`{${name}}`, value);
|
|
1106
|
-
return text;
|
|
1107
|
-
}
|
|
1108
|
-
//#endregion
|
|
1109
|
-
//#region \0dsh-css:packages/dsh-nova-ui-task-board/src/client/board.module.css.mjs
|
|
1110
|
-
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}";
|
|
1111
|
-
const tagId = "@william2000/dsh-nova-ui-task-board/board.module.css";
|
|
1112
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
1113
|
-
const tag = document.createElement("style");
|
|
1114
|
-
tag.dataset.plugin = "@william2000/dsh-nova-ui-task-board";
|
|
1115
|
-
tag.dataset.pluginCss = tagId;
|
|
1116
|
-
tag.textContent = css;
|
|
1117
|
-
document.head.appendChild(tag);
|
|
1118
|
-
}
|
|
1119
|
-
var board_module_css_default = {
|
|
1120
|
-
"backButton": "a-UL2G_backButton",
|
|
1121
|
-
"board": "a-UL2G_board",
|
|
1122
|
-
"boardHeader": "a-UL2G_boardHeader",
|
|
1123
|
-
"boardTitle": "a-UL2G_boardTitle",
|
|
1124
|
-
"boardView": "a-UL2G_boardView",
|
|
1125
|
-
"card": "a-UL2G_card",
|
|
1126
|
-
"cardChips": "a-UL2G_cardChips",
|
|
1127
|
-
"cardDragging": "a-UL2G_cardDragging",
|
|
1128
|
-
"cardExcerpt": "a-UL2G_cardExcerpt",
|
|
1129
|
-
"cardMeta": "a-UL2G_cardMeta",
|
|
1130
|
-
"cardParent": "a-UL2G_cardParent",
|
|
1131
|
-
"cardProject": "a-UL2G_cardProject",
|
|
1132
|
-
"cardProposedBadge": "a-UL2G_cardProposedBadge",
|
|
1133
|
-
"cardRun": "a-UL2G_cardRun",
|
|
1134
|
-
"cardRunningLabel": "a-UL2G_cardRunningLabel",
|
|
1135
|
-
"cardSchedule": "a-UL2G_cardSchedule",
|
|
1136
|
-
"cardSession": "a-UL2G_cardSession",
|
|
1137
|
-
"cardSource": "a-UL2G_cardSource",
|
|
1138
|
-
"cardSpinner": "a-UL2G_cardSpinner",
|
|
1139
|
-
"cardTag": "a-UL2G_cardTag",
|
|
1140
|
-
"cardTagMore": "a-UL2G_cardTagMore",
|
|
1141
|
-
"cardTime": "a-UL2G_cardTime",
|
|
1142
|
-
"cardTitle": "a-UL2G_cardTitle",
|
|
1143
|
-
"cards": "a-UL2G_cards",
|
|
1144
|
-
"column": "a-UL2G_column",
|
|
1145
|
-
"columnCount": "a-UL2G_columnCount",
|
|
1146
|
-
"columnEmpty": "a-UL2G_columnEmpty",
|
|
1147
|
-
"columnHeader": "a-UL2G_columnHeader",
|
|
1148
|
-
"columnTitle": "a-UL2G_columnTitle",
|
|
1149
|
-
"columns": "a-UL2G_columns",
|
|
1150
|
-
"confirmMessage": "a-UL2G_confirmMessage",
|
|
1151
|
-
"confirmTargetActive": "a-UL2G_confirmTargetActive",
|
|
1152
|
-
"confirmTargetButton": "a-UL2G_confirmTargetButton",
|
|
1153
|
-
"confirmTargetRow": "a-UL2G_confirmTargetRow",
|
|
1154
|
-
"dangerButton": "a-UL2G_dangerButton",
|
|
1155
|
-
"dangerGhostButton": "a-UL2G_dangerGhostButton",
|
|
1156
|
-
"detail": "a-UL2G_detail",
|
|
1157
|
-
"detailBody": "a-UL2G_detailBody",
|
|
1158
|
-
"detailFooter": "a-UL2G_detailFooter",
|
|
1159
|
-
"detailHeader": "a-UL2G_detailHeader",
|
|
1160
|
-
"detailMeta": "a-UL2G_detailMeta",
|
|
1161
|
-
"detailSection": "a-UL2G_detailSection",
|
|
1162
|
-
"detailText": "a-UL2G_detailText",
|
|
1163
|
-
"detailTitle": "a-UL2G_detailTitle",
|
|
1164
|
-
"dropIndicator": "a-UL2G_dropIndicator",
|
|
1165
|
-
"dshNovaTbSpin": "a-UL2G_dshNovaTbSpin",
|
|
1166
|
-
"entry": "a-UL2G_entry",
|
|
1167
|
-
"entryBadge": "a-UL2G_entryBadge",
|
|
1168
|
-
"entryIcon": "a-UL2G_entryIcon",
|
|
1169
|
-
"entryLabel": "a-UL2G_entryLabel",
|
|
1170
|
-
"executionBadge": "a-UL2G_executionBadge",
|
|
1171
|
-
"executionError": "a-UL2G_executionError",
|
|
1172
|
-
"executionList": "a-UL2G_executionList",
|
|
1173
|
-
"executionRow": "a-UL2G_executionRow",
|
|
1174
|
-
"executionTimes": "a-UL2G_executionTimes",
|
|
1175
|
-
"field": "a-UL2G_field",
|
|
1176
|
-
"fieldLabel": "a-UL2G_fieldLabel",
|
|
1177
|
-
"filterActions": "a-UL2G_filterActions",
|
|
1178
|
-
"filterBar": "a-UL2G_filterBar",
|
|
1179
|
-
"filterChip": "a-UL2G_filterChip",
|
|
1180
|
-
"filterChipActive": "a-UL2G_filterChipActive",
|
|
1181
|
-
"filterChips": "a-UL2G_filterChips",
|
|
1182
|
-
"filterHitCount": "a-UL2G_filterHitCount",
|
|
1183
|
-
"filterItem": "a-UL2G_filterItem",
|
|
1184
|
-
"filterLabel": "a-UL2G_filterLabel",
|
|
1185
|
-
"formError": "a-UL2G_formError",
|
|
1186
|
-
"ghostButton": "a-UL2G_ghostButton",
|
|
1187
|
-
"group": "a-UL2G_group",
|
|
1188
|
-
"groupCards": "a-UL2G_groupCards",
|
|
1189
|
-
"groupCount": "a-UL2G_groupCount",
|
|
1190
|
-
"groupHeader": "a-UL2G_groupHeader",
|
|
1191
|
-
"groupTitle": "a-UL2G_groupTitle",
|
|
1192
|
-
"groupToggle": "a-UL2G_groupToggle",
|
|
1193
|
-
"groupToggleActive": "a-UL2G_groupToggleActive",
|
|
1194
|
-
"groupToggleButton": "a-UL2G_groupToggleButton",
|
|
1195
|
-
"hostMeta": "a-UL2G_hostMeta",
|
|
1196
|
-
"iconButton": "a-UL2G_iconButton",
|
|
1197
|
-
"input": "a-UL2G_input",
|
|
1198
|
-
"linkButton": "a-UL2G_linkButton",
|
|
1199
|
-
"messageAction": "a-UL2G_messageAction",
|
|
1200
|
-
"modal": "a-UL2G_modal",
|
|
1201
|
-
"modalBackdrop": "a-UL2G_modalBackdrop",
|
|
1202
|
-
"modalFooter": "a-UL2G_modalFooter",
|
|
1203
|
-
"modalTitle": "a-UL2G_modalTitle",
|
|
1204
|
-
"moveRow": "a-UL2G_moveRow",
|
|
1205
|
-
"nova-task-board-view": "a-UL2G_nova-task-board-view",
|
|
1206
|
-
"primaryButton": "a-UL2G_primaryButton",
|
|
1207
|
-
"promptBlock": "a-UL2G_promptBlock",
|
|
1208
|
-
"scheduleInput": "a-UL2G_scheduleInput",
|
|
1209
|
-
"scheduleInputInvalid": "a-UL2G_scheduleInputInvalid",
|
|
1210
|
-
"scheduleMeta": "a-UL2G_scheduleMeta",
|
|
1211
|
-
"scheduleMode": "a-UL2G_scheduleMode",
|
|
1212
|
-
"scheduleModeActive": "a-UL2G_scheduleModeActive",
|
|
1213
|
-
"scheduleModeButton": "a-UL2G_scheduleModeButton",
|
|
1214
|
-
"schedulePreset": "a-UL2G_schedulePreset",
|
|
1215
|
-
"scheduleRow": "a-UL2G_scheduleRow",
|
|
1216
|
-
"scheduleToggle": "a-UL2G_scheduleToggle",
|
|
1217
|
-
"search": "a-UL2G_search",
|
|
1218
|
-
"select": "a-UL2G_select",
|
|
1219
|
-
"sourceRow": "a-UL2G_sourceRow",
|
|
1220
|
-
"splitRow": "a-UL2G_splitRow",
|
|
1221
|
-
"splitRowHeader": "a-UL2G_splitRowHeader",
|
|
1222
|
-
"splitRowIndex": "a-UL2G_splitRowIndex",
|
|
1223
|
-
"splitRows": "a-UL2G_splitRows",
|
|
1224
|
-
"statusBadge": "a-UL2G_statusBadge",
|
|
1225
|
-
"statusDot": "a-UL2G_statusDot",
|
|
1226
|
-
"tagEditor": "a-UL2G_tagEditor",
|
|
1227
|
-
"tagEditorChip": "a-UL2G_tagEditorChip",
|
|
1228
|
-
"tagEditorRemove": "a-UL2G_tagEditorRemove"
|
|
1229
|
-
};
|
|
1230
|
-
//#endregion
|
|
1231
|
-
//#region src/core/model.ts
|
|
1232
|
-
/** 任务生命周期状态,与看板列一一对应(§10.1);`proposed` 为对话流转候选(D7/P3.1)。 */
|
|
1233
|
-
const TASK_STATUSES = [
|
|
1234
|
-
"proposed",
|
|
1235
|
-
"backlog",
|
|
1236
|
-
"todo",
|
|
1237
|
-
"running",
|
|
1238
|
-
"done",
|
|
1239
|
-
"failed"
|
|
1240
|
-
];
|
|
1241
|
-
/** 任务来源(§9.2,v1.0 字段,P3 随对话流转启用)。 */
|
|
1242
|
-
const TASK_SOURCES = [
|
|
1243
|
-
"manual",
|
|
1244
|
-
"conversation",
|
|
1245
|
-
"github_issue",
|
|
1246
|
-
"bookmark_collector",
|
|
1247
|
-
"feishu",
|
|
1248
|
-
"requirement",
|
|
1249
|
-
"other"
|
|
1250
|
-
];
|
|
1251
|
-
/** 执行会话钉住的权限预设 id(`/permission <id>`,§9.2)。 */
|
|
1252
|
-
const TASK_PERMISSIONS = [
|
|
1253
|
-
"read-only",
|
|
1254
|
-
"workspace-write",
|
|
1255
|
-
"danger-full-access"
|
|
1256
|
-
];
|
|
1257
|
-
/** 执行结果(§9.3)。 */
|
|
1258
|
-
const EXECUTION_RESULTS = [
|
|
1259
|
-
"succeeded",
|
|
1260
|
-
"failed",
|
|
1261
|
-
"cancelled"
|
|
1262
|
-
];
|
|
1263
|
-
/** 评论类型(§9.5 comments:user_feedback/ai_log/system_event)。 */
|
|
1264
|
-
const COMMENT_TYPES = [
|
|
1265
|
-
"user_feedback",
|
|
1266
|
-
"ai_log",
|
|
1267
|
-
"system_event"
|
|
1268
|
-
];
|
|
1269
|
-
/** 产物类型(§9.5 artifacts;会话 transcript 即默认产物 `session`)。 */
|
|
1270
|
-
const ARTIFACT_TYPES = [
|
|
1271
|
-
"session",
|
|
1272
|
-
"link",
|
|
1273
|
-
"file",
|
|
1274
|
-
"other"
|
|
1275
|
-
];
|
|
1276
|
-
/** 来源会话引用写入 metadata 的键(§9.2/§14.4:可跳回对话)。 */
|
|
1277
|
-
const SOURCE_CONVERSATION_META_KEY = "sourceConversationId";
|
|
1278
|
-
/** 有限数字守卫。 */
|
|
1279
|
-
function isFiniteNumber(value) {
|
|
1280
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
1281
|
-
}
|
|
1282
|
-
/** 非空字符串(trim 后)守卫,空串/空白清除钉住字段(对齐参考实现 normalizeTargetId)。 */
|
|
1283
|
-
function normalizeOptionalString(value) {
|
|
1284
|
-
if (typeof value !== "string") return void 0;
|
|
1285
|
-
const trimmed = value.trim();
|
|
1286
|
-
return trimmed === "" ? void 0 : trimmed;
|
|
1287
|
-
}
|
|
1288
|
-
/** 是否为已知任务状态。 */
|
|
1289
|
-
function isTaskStatus(value) {
|
|
1290
|
-
return typeof value === "string" && TASK_STATUSES.includes(value);
|
|
1291
|
-
}
|
|
1292
|
-
/** 是否为已知任务来源。 */
|
|
1293
|
-
function isTaskSource(value) {
|
|
1294
|
-
return typeof value === "string" && TASK_SOURCES.includes(value);
|
|
1295
|
-
}
|
|
1296
|
-
/** 是否为已知权限预设。 */
|
|
1297
|
-
function isTaskPermission(value) {
|
|
1298
|
-
return typeof value === "string" && TASK_PERMISSIONS.includes(value);
|
|
1299
|
-
}
|
|
1300
|
-
/** 是否为已知执行结果。 */
|
|
1301
|
-
function isExecutionResult(value) {
|
|
1302
|
-
return typeof value === "string" && EXECUTION_RESULTS.includes(value);
|
|
1303
|
-
}
|
|
1304
|
-
/** 是否为已知评论类型。 */
|
|
1305
|
-
function isCommentType(value) {
|
|
1306
|
-
return typeof value === "string" && COMMENT_TYPES.includes(value);
|
|
1307
|
-
}
|
|
1308
|
-
/** 是否为已知产物类型。 */
|
|
1309
|
-
function isArtifactType(value) {
|
|
1310
|
-
return typeof value === "string" && ARTIFACT_TYPES.includes(value);
|
|
1311
|
-
}
|
|
1312
|
-
/**
|
|
1313
|
-
* cron 表达式的基础形状校验(§9.4:5 段 cron「分 时 日 月 周」)。
|
|
1314
|
-
* 这是结构级校验:段数、字符集(数字、`* , - / ?` 及名字字母)。cron 的到期
|
|
1315
|
-
* 计算与完整合法性由 T006 的调度器实现;本层只保证持久化的规则形状可解析。
|
|
1316
|
-
*/
|
|
1317
|
-
const CRON_FIELD_RE = /^[0-9A-Za-z*,\-\/?]+$/;
|
|
1318
|
-
function isPlausibleCron(cron) {
|
|
1319
|
-
if (typeof cron !== "string" || cron.trim() === "") return false;
|
|
1320
|
-
const fields = cron.trim().split(/\s+/);
|
|
1321
|
-
if (fields.length !== 5) return false;
|
|
1322
|
-
return fields.every((field) => CRON_FIELD_RE.test(field));
|
|
1323
|
-
}
|
|
1324
|
-
/**
|
|
1325
|
-
* 归一化一条持久化的执行安排(§9.4 判别联合):
|
|
1326
|
-
* - `kind: 'one-shot'` → one-shot 分支;`runAt` 非有限数则整条丢弃;
|
|
1327
|
-
* - `kind: 'cron'` 或缺省 kind(v1/v2 旧形状)→ cron 分支;cron 形状非法则整条
|
|
1328
|
-
* 丢弃(「修复或丢弃 schedule、绝不丢整行」——坏 schedule 不拖垮任务行);
|
|
1329
|
-
* - 其他 kind → 丢弃。
|
|
1330
|
-
*/
|
|
1331
|
-
function normalizeSchedule(value) {
|
|
1332
|
-
if (typeof value !== "object" || value === null) return void 0;
|
|
1333
|
-
const rule = value;
|
|
1334
|
-
if (rule.kind === "one-shot") {
|
|
1335
|
-
if (!isFiniteNumber(rule.runAt)) return void 0;
|
|
1336
|
-
const schedule = {
|
|
1337
|
-
kind: "one-shot",
|
|
1338
|
-
runAt: rule.runAt
|
|
1339
|
-
};
|
|
1340
|
-
if (isFiniteNumber(rule.firedAt)) schedule.firedAt = rule.firedAt;
|
|
1341
|
-
return schedule;
|
|
1342
|
-
}
|
|
1343
|
-
if (rule.kind !== void 0 && rule.kind !== "cron") return void 0;
|
|
1344
|
-
if (!isPlausibleCron(rule.cron)) return void 0;
|
|
1345
|
-
const schedule = {
|
|
1346
|
-
kind: "cron",
|
|
1347
|
-
enabled: rule.enabled === true,
|
|
1348
|
-
cron: rule.cron
|
|
1349
|
-
};
|
|
1350
|
-
if (isFiniteNumber(rule.nextRunAt)) schedule.nextRunAt = rule.nextRunAt;
|
|
1351
|
-
if (isFiniteNumber(rule.lastTriggeredAt)) schedule.lastTriggeredAt = rule.lastTriggeredAt;
|
|
1352
|
-
return schedule;
|
|
1353
|
-
}
|
|
1354
|
-
/** 归一化一条执行记录(§9.3);结构非法返回 undefined。 */
|
|
1355
|
-
function normalizeExecution(value) {
|
|
1356
|
-
if (typeof value !== "object" || value === null) return void 0;
|
|
1357
|
-
const entry = value;
|
|
1358
|
-
if (typeof entry.id !== "string" || entry.id === "") return void 0;
|
|
1359
|
-
if (!isFiniteNumber(entry.startedAt)) return void 0;
|
|
1360
|
-
if (entry.sessionId !== void 0 && typeof entry.sessionId !== "string") return void 0;
|
|
1361
|
-
if (entry.endedAt !== void 0 && !isFiniteNumber(entry.endedAt)) return void 0;
|
|
1362
|
-
if (entry.result !== void 0 && !isExecutionResult(entry.result)) return void 0;
|
|
1363
|
-
if (entry.error !== void 0 && typeof entry.error !== "string") return void 0;
|
|
1364
|
-
const execution = {
|
|
1365
|
-
id: entry.id,
|
|
1366
|
-
startedAt: entry.startedAt
|
|
1367
|
-
};
|
|
1368
|
-
if (typeof entry.sessionId === "string") execution.sessionId = entry.sessionId;
|
|
1369
|
-
if (isFiniteNumber(entry.endedAt)) execution.endedAt = entry.endedAt;
|
|
1370
|
-
if (isExecutionResult(entry.result)) execution.result = entry.result;
|
|
1371
|
-
if (typeof entry.error === "string") execution.error = entry.error;
|
|
1372
|
-
return execution;
|
|
1373
|
-
}
|
|
1374
|
-
/**
|
|
1375
|
-
* 归一化标签数组(§9.2 默认 []):仅保留字符串元素,逐项 trim、丢弃空白项、
|
|
1376
|
-
* 按首次出现顺序去重;缺省/非法 → []。标签是展示/过滤维度的原始字符串,
|
|
1377
|
-
* 大小写敏感(`API` 与 `api` 是两个标签),去重不折叠大小写。
|
|
1378
|
-
*/
|
|
1379
|
-
function normalizeTags(value) {
|
|
1380
|
-
if (!Array.isArray(value)) return [];
|
|
1381
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1382
|
-
const tags = [];
|
|
1383
|
-
for (const tag of value) {
|
|
1384
|
-
if (typeof tag !== "string") continue;
|
|
1385
|
-
const trimmed = tag.trim();
|
|
1386
|
-
if (trimmed === "" || seen.has(trimmed)) continue;
|
|
1387
|
-
seen.add(trimmed);
|
|
1388
|
-
tags.push(trimmed);
|
|
1389
|
-
}
|
|
1390
|
-
return tags;
|
|
1391
|
-
}
|
|
1392
|
-
/** 通用字符串数组归一化(trim、丢空白、首现去重、条目长度封顶、数量封顶)。 */
|
|
1393
|
-
function normalizeStringList(value, maxItems, maxLength) {
|
|
1394
|
-
if (!Array.isArray(value)) return [];
|
|
1395
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1396
|
-
const items = [];
|
|
1397
|
-
for (const item of value) {
|
|
1398
|
-
if (typeof item !== "string") continue;
|
|
1399
|
-
const trimmed = item.trim().slice(0, maxLength);
|
|
1400
|
-
if (trimmed === "" || seen.has(trimmed)) continue;
|
|
1401
|
-
seen.add(trimmed);
|
|
1402
|
-
items.push(trimmed);
|
|
1403
|
-
if (items.length >= maxItems) break;
|
|
1404
|
-
}
|
|
1405
|
-
return items;
|
|
1406
|
-
}
|
|
1407
|
-
/** 归一化来源元数据(§14.1/14.2):仅保留字符串键值对;缺省/非法 → undefined。 */
|
|
1408
|
-
function normalizeMetadata(value) {
|
|
1409
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
1410
|
-
const metadata = {};
|
|
1411
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
1412
|
-
if (typeof entry !== "string" || key.trim() === "") continue;
|
|
1413
|
-
metadata[key.trim().slice(0, 64)] = entry.slice(0, 2048);
|
|
1414
|
-
}
|
|
1415
|
-
return Object.keys(metadata).length > 0 ? metadata : void 0;
|
|
1416
|
-
}
|
|
1417
|
-
/** 归一化一条评论(§9.5);结构非法返回 undefined。 */
|
|
1418
|
-
function normalizeComment(value) {
|
|
1419
|
-
if (typeof value !== "object" || value === null) return void 0;
|
|
1420
|
-
const entry = value;
|
|
1421
|
-
if (typeof entry.id !== "string" || entry.id === "") return void 0;
|
|
1422
|
-
if (typeof entry.body !== "string" || entry.body === "") return void 0;
|
|
1423
|
-
if (!isFiniteNumber(entry.createdAt)) return void 0;
|
|
1424
|
-
if (entry.author !== void 0 && typeof entry.author !== "string") return void 0;
|
|
1425
|
-
if (entry.type !== void 0 && !isCommentType(entry.type)) return void 0;
|
|
1426
|
-
return {
|
|
1427
|
-
id: entry.id,
|
|
1428
|
-
author: typeof entry.author === "string" && entry.author.trim() !== "" ? entry.author.slice(0, 64) : "user",
|
|
1429
|
-
body: entry.body.slice(0, 4e3),
|
|
1430
|
-
type: isCommentType(entry.type) ? entry.type : "user_feedback",
|
|
1431
|
-
createdAt: entry.createdAt
|
|
1432
|
-
};
|
|
1433
|
-
}
|
|
1434
|
-
/** 归一化评论集合(§9.5 comments,默认 []);非法条目丢弃。 */
|
|
1435
|
-
function normalizeComments(value) {
|
|
1436
|
-
if (!Array.isArray(value)) return [];
|
|
1437
|
-
const comments = [];
|
|
1438
|
-
for (const entry of value) {
|
|
1439
|
-
const comment = normalizeComment(entry);
|
|
1440
|
-
if (comment !== void 0) comments.push(comment);
|
|
1441
|
-
}
|
|
1442
|
-
return comments;
|
|
1443
|
-
}
|
|
1444
|
-
/** 归一化一条产物(§9.5 artifacts);结构非法返回 undefined。 */
|
|
1445
|
-
function normalizeArtifact(value) {
|
|
1446
|
-
if (typeof value !== "object" || value === null) return void 0;
|
|
1447
|
-
const entry = value;
|
|
1448
|
-
if (typeof entry.id !== "string" || entry.id === "") return void 0;
|
|
1449
|
-
if (!isArtifactType(entry.type)) return void 0;
|
|
1450
|
-
if (typeof entry.title !== "string" || entry.title === "") return void 0;
|
|
1451
|
-
if (!isFiniteNumber(entry.createdAt)) return void 0;
|
|
1452
|
-
if (entry.url !== void 0 && typeof entry.url !== "string") return void 0;
|
|
1453
|
-
if (entry.contentRef !== void 0 && typeof entry.contentRef !== "string") return void 0;
|
|
1454
|
-
const artifact = {
|
|
1455
|
-
id: entry.id,
|
|
1456
|
-
type: entry.type,
|
|
1457
|
-
title: entry.title.slice(0, 500),
|
|
1458
|
-
createdAt: entry.createdAt
|
|
1459
|
-
};
|
|
1460
|
-
if (typeof entry.url === "string" && entry.url.trim() !== "") artifact.url = entry.url.slice(0, 2048);
|
|
1461
|
-
if (typeof entry.contentRef === "string" && entry.contentRef.trim() !== "") artifact.contentRef = entry.contentRef.slice(0, 512);
|
|
1462
|
-
return artifact;
|
|
1463
|
-
}
|
|
1464
|
-
/** 归一化产物集合(§9.5 artifacts,默认 []);非法条目丢弃。 */
|
|
1465
|
-
function normalizeArtifacts(value) {
|
|
1466
|
-
if (!Array.isArray(value)) return [];
|
|
1467
|
-
const artifacts = [];
|
|
1468
|
-
for (const entry of value) {
|
|
1469
|
-
const artifact = normalizeArtifact(entry);
|
|
1470
|
-
if (artifact !== void 0) artifacts.push(artifact);
|
|
1471
|
-
}
|
|
1472
|
-
return artifacts;
|
|
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;
|
|
1473
1673
|
}
|
|
1474
|
-
/**
|
|
1475
|
-
function
|
|
1476
|
-
|
|
1477
|
-
const
|
|
1478
|
-
|
|
1479
|
-
if (entry.goal !== void 0 && typeof entry.goal !== "string") return void 0;
|
|
1480
|
-
if (entry.lastAiSummary !== void 0 && typeof entry.lastAiSummary !== "string") return void 0;
|
|
1481
|
-
if (entry.latestUserFeedback !== void 0 && typeof entry.latestUserFeedback !== "string") return void 0;
|
|
1482
|
-
const snapshot = {
|
|
1483
|
-
keyDecisions: normalizeStringList(entry.keyDecisions, 50, 500),
|
|
1484
|
-
filePaths: normalizeStringList(entry.filePaths, 100, 500),
|
|
1485
|
-
relatedLinks: normalizeStringList(entry.relatedLinks, 20, 2048),
|
|
1486
|
-
updatedAt: entry.updatedAt
|
|
1487
|
-
};
|
|
1488
|
-
if (typeof entry.goal === "string" && entry.goal.trim() !== "") snapshot.goal = entry.goal.trim().slice(0, 2e3);
|
|
1489
|
-
if (typeof entry.lastAiSummary === "string" && entry.lastAiSummary.trim() !== "") snapshot.lastAiSummary = entry.lastAiSummary.trim().slice(0, 8e3);
|
|
1490
|
-
if (typeof entry.latestUserFeedback === "string" && entry.latestUserFeedback.trim() !== "") snapshot.latestUserFeedback = entry.latestUserFeedback.trim().slice(0, 4e3);
|
|
1491
|
-
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;
|
|
1492
1679
|
}
|
|
1493
1680
|
/**
|
|
1494
|
-
*
|
|
1495
|
-
*
|
|
1496
|
-
*
|
|
1497
|
-
*
|
|
1498
|
-
* 语义非法 → 就地修复:
|
|
1499
|
-
* - 未知状态 → `todo`(未来版本的未知状态落入待办而非丢行,对齐参考实现);
|
|
1500
|
-
* - 未知 source/permission → undefined;空白 workspaceId/mode/project/parentId
|
|
1501
|
-
* → undefined;archivedAt 非有限数 → undefined;
|
|
1502
|
-
* - tags 非字符串数组 → [];order 非有限数 → 0(T009 重算列内唯一);
|
|
1503
|
-
* - 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 为准。
|
|
1504
1685
|
*/
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
const execution = normalizeExecution(entry);
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
prompt: record.prompt,
|
|
1525
|
-
status: isTaskStatus(record.status) ? record.status : "todo",
|
|
1526
|
-
createdAt: record.createdAt,
|
|
1527
|
-
updatedAt: record.updatedAt,
|
|
1528
|
-
executions,
|
|
1529
|
-
tags: normalizeTags(record.tags),
|
|
1530
|
-
comments: normalizeComments(record.comments),
|
|
1531
|
-
artifacts: normalizeArtifacts(record.artifacts),
|
|
1532
|
-
order: isFiniteNumber(record.order) ? record.order : 0
|
|
1533
|
-
};
|
|
1534
|
-
if (isTaskSource(record.source)) task.source = record.source;
|
|
1535
|
-
const metadata = normalizeMetadata(record.metadata);
|
|
1536
|
-
if (metadata !== void 0) task.metadata = metadata;
|
|
1537
|
-
const contextSnapshot = normalizeContextSnapshot(record.contextSnapshot);
|
|
1538
|
-
if (contextSnapshot !== void 0) task.contextSnapshot = contextSnapshot;
|
|
1539
|
-
const schedule = normalizeSchedule(record.schedule);
|
|
1540
|
-
if (schedule !== void 0) task.schedule = schedule;
|
|
1541
|
-
const workspaceId = normalizeOptionalString(record.workspaceId);
|
|
1542
|
-
if (workspaceId !== void 0) task.workspaceId = workspaceId;
|
|
1543
|
-
const mode = normalizeOptionalString(record.mode);
|
|
1544
|
-
if (mode !== void 0) task.mode = mode;
|
|
1545
|
-
if (isTaskPermission(record.permission)) task.permission = record.permission;
|
|
1546
|
-
if (isFiniteNumber(record.archivedAt)) task.archivedAt = record.archivedAt;
|
|
1547
|
-
const project = normalizeOptionalString(record.project);
|
|
1548
|
-
if (project !== void 0) task.project = project;
|
|
1549
|
-
const parentId = normalizeOptionalString(record.parentId);
|
|
1550
|
-
if (parentId !== void 0) task.parentId = parentId;
|
|
1551
|
-
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);
|
|
1552
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
|
+
};
|
|
1553
1817
|
//#endregion
|
|
1554
1818
|
//#region src/client/board/ConfirmDialog.tsx
|
|
1555
1819
|
/**
|
|
@@ -2907,79 +3171,6 @@ window.__ModuleLoader__.load({
|
|
|
2907
3171
|
/** Memoized card: re-renders only when the card's own props change. */
|
|
2908
3172
|
const TaskCard = (0, react.memo)(TaskCardInner);
|
|
2909
3173
|
//#endregion
|
|
2910
|
-
//#region src/core/cron.ts
|
|
2911
|
-
/** 每字段的闭区间,按 cron 顺序。 */
|
|
2912
|
-
const FIELD_RANGES = [
|
|
2913
|
-
[0, 59],
|
|
2914
|
-
[0, 23],
|
|
2915
|
-
[1, 31],
|
|
2916
|
-
[1, 12],
|
|
2917
|
-
[0, 7]
|
|
2918
|
-
];
|
|
2919
|
-
/**
|
|
2920
|
-
* 解析一个 5 段 cron 表达式。
|
|
2921
|
-
* @returns 各字段匹配集合;表达式非法时返回 null。
|
|
2922
|
-
*/
|
|
2923
|
-
function parseCron(expr) {
|
|
2924
|
-
const fields = expr.trim().split(/\s+/);
|
|
2925
|
-
if (fields.length !== 5) return null;
|
|
2926
|
-
const sets = [];
|
|
2927
|
-
for (let index = 0; index < 5; index += 1) {
|
|
2928
|
-
const [min, max] = FIELD_RANGES[index];
|
|
2929
|
-
const set = /* @__PURE__ */ new Set();
|
|
2930
|
-
if (!parseField(fields[index], min, max, set)) return null;
|
|
2931
|
-
sets.push(set);
|
|
2932
|
-
}
|
|
2933
|
-
const weekdays = /* @__PURE__ */ new Set();
|
|
2934
|
-
for (const day of sets[4]) weekdays.add(day === 7 ? 0 : day);
|
|
2935
|
-
return {
|
|
2936
|
-
minutes: sets[0],
|
|
2937
|
-
hours: sets[1],
|
|
2938
|
-
days: sets[2],
|
|
2939
|
-
months: sets[3],
|
|
2940
|
-
weekdays,
|
|
2941
|
-
dayWildcard: fields[2] === "*",
|
|
2942
|
-
weekdayWildcard: fields[4] === "*"
|
|
2943
|
-
};
|
|
2944
|
-
}
|
|
2945
|
-
/** 表达式是否可解析(语法合法)。 */
|
|
2946
|
-
function isValidCron(expr) {
|
|
2947
|
-
return parseCron(expr) !== null;
|
|
2948
|
-
}
|
|
2949
|
-
/** 解析一个逗号列表字段,写入匹配集合;非法返回 false。 */
|
|
2950
|
-
function parseField(field, min, max, out) {
|
|
2951
|
-
if (field === "*") {
|
|
2952
|
-
for (let value = min; value <= max; value += 1) out.add(value);
|
|
2953
|
-
return true;
|
|
2954
|
-
}
|
|
2955
|
-
for (const part of field.split(",")) {
|
|
2956
|
-
if (part === "") return false;
|
|
2957
|
-
const [range, stepRaw] = part.split("/");
|
|
2958
|
-
let low;
|
|
2959
|
-
let high;
|
|
2960
|
-
if (range === "*") {
|
|
2961
|
-
low = min;
|
|
2962
|
-
high = max;
|
|
2963
|
-
} else if (range.includes("-")) {
|
|
2964
|
-
const [a, b] = range.split("-");
|
|
2965
|
-
if (a === "" || b === "" || !isDigits(a) || !isDigits(b)) return false;
|
|
2966
|
-
low = Number(a);
|
|
2967
|
-
high = Number(b);
|
|
2968
|
-
} else if (isDigits(range)) {
|
|
2969
|
-
low = Number(range);
|
|
2970
|
-
high = Number(range);
|
|
2971
|
-
} else return false;
|
|
2972
|
-
if (low < min || high > max || low > high) return false;
|
|
2973
|
-
const step = stepRaw === void 0 ? 1 : isDigits(stepRaw) ? Number(stepRaw) : NaN;
|
|
2974
|
-
if (!Number.isInteger(step) || step < 1) return false;
|
|
2975
|
-
for (let value = low; value <= high; value += step) out.add(value);
|
|
2976
|
-
}
|
|
2977
|
-
return true;
|
|
2978
|
-
}
|
|
2979
|
-
function isDigits(value) {
|
|
2980
|
-
return /^\d+$/.test(value);
|
|
2981
|
-
}
|
|
2982
|
-
//#endregion
|
|
2983
3174
|
//#region src/client/schedule-presets.ts
|
|
2984
3175
|
/** 常用 cron 定时预设(cron → locale label)。 */
|
|
2985
3176
|
const SCHEDULE_PRESETS = [
|
|
@@ -3048,6 +3239,9 @@ window.__ModuleLoader__.load({
|
|
|
3048
3239
|
* - 执行 Prompt 为空时以标题呈现;归档任务隐藏执行设置(只读,§19-6);
|
|
3049
3240
|
* - running/pending 时相关按钮禁用;执行按钮经 `executionCapable`(T005 起恒
|
|
3050
3241
|
* true,协议已并入 run/rerun)门控;
|
|
3242
|
+
* - running/带未结算执行时提供「停止执行」(§11.2 cancel-execution,二次确认):
|
|
3243
|
+
* 结算 cancelled → 任务回落 todo、running 独占释放,随后可移动/删除/归档;
|
|
3244
|
+
* 同一状态下面板不再给出会被 Host 拒绝的「删除」按钮(§12.5 按钮禁用);
|
|
3051
3245
|
* - 执行历史:结果徽标 + 起止时间 + 一键跳转会话 transcript + 错误信息
|
|
3052
3246
|
* (§19-5;会话跳转经 sessions.open,会话由 HostRunner 创建,T005);
|
|
3053
3247
|
* - 状态移动仅提供合法目标(待规划/待办;done/failed 移回视为重开,不触发执行);
|
|
@@ -3588,6 +3782,7 @@ window.__ModuleLoader__.load({
|
|
|
3588
3782
|
/** 任务详情浮层。 */
|
|
3589
3783
|
function TaskDetail({ controller, task }) {
|
|
3590
3784
|
const [confirmDelete, setConfirmDelete] = (0, react.useState)(false);
|
|
3785
|
+
const [confirmStop, setConfirmStop] = (0, react.useState)(false);
|
|
3591
3786
|
const [showSplit, setShowSplit] = (0, react.useState)(false);
|
|
3592
3787
|
const [latest, setLatest] = (0, react.useState)(task);
|
|
3593
3788
|
(0, react.useEffect)(() => {
|
|
@@ -3596,11 +3791,12 @@ window.__ModuleLoader__.load({
|
|
|
3596
3791
|
const current = latest;
|
|
3597
3792
|
const snapshot = controller.getSnapshot();
|
|
3598
3793
|
const running = current.status === "running";
|
|
3794
|
+
const openExecution = current.executions.some((execution) => execution.endedAt === void 0);
|
|
3599
3795
|
const archived = current.archivedAt !== void 0;
|
|
3600
3796
|
const pending = snapshot.pendingTaskIds.includes(current.id);
|
|
3601
3797
|
const transportError = snapshot.transportError;
|
|
3602
3798
|
const timeZone = snapshot.host?.scheduler.timeZone;
|
|
3603
|
-
const canRun = snapshot.executionCapable && !running && !pending;
|
|
3799
|
+
const canRun = snapshot.executionCapable && !running && !openExecution && !pending;
|
|
3604
3800
|
const parent = current.parentId === void 0 ? void 0 : snapshot.tasks.find((candidate) => candidate.id === current.parentId);
|
|
3605
3801
|
const children = snapshot.tasks.filter((candidate) => candidate.parentId === current.id && candidate.archivedAt === void 0);
|
|
3606
3802
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -3646,7 +3842,7 @@ window.__ModuleLoader__.load({
|
|
|
3646
3842
|
children: [
|
|
3647
3843
|
t("board.hostError", { error: transportError }),
|
|
3648
3844
|
" ",
|
|
3649
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3845
|
+
snapshot.transportErrorRetryable === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3650
3846
|
type: "button",
|
|
3651
3847
|
className: board_module_css_default.linkButton,
|
|
3652
3848
|
onClick: () => {
|
|
@@ -3775,6 +3971,15 @@ window.__ModuleLoader__.load({
|
|
|
3775
3971
|
},
|
|
3776
3972
|
children: t("split.entry")
|
|
3777
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
|
+
}),
|
|
3778
3983
|
!archived && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3779
3984
|
type: "button",
|
|
3780
3985
|
className: board_module_css_default.primaryButton,
|
|
@@ -3806,7 +4011,7 @@ window.__ModuleLoader__.load({
|
|
|
3806
4011
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3807
4012
|
type: "button",
|
|
3808
4013
|
className: board_module_css_default.dangerButton,
|
|
3809
|
-
disabled: pending,
|
|
4014
|
+
disabled: pending || running || openExecution,
|
|
3810
4015
|
onClick: () => {
|
|
3811
4016
|
setConfirmDelete(true);
|
|
3812
4017
|
},
|
|
@@ -3838,6 +4043,19 @@ window.__ModuleLoader__.load({
|
|
|
3838
4043
|
controller.deleteTask(current.id);
|
|
3839
4044
|
}
|
|
3840
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
|
+
}),
|
|
3841
4059
|
showSplit && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RequirementSplitModal, {
|
|
3842
4060
|
controller,
|
|
3843
4061
|
prefill: {
|
|
@@ -4466,7 +4684,7 @@ window.__ModuleLoader__.load({
|
|
|
4466
4684
|
children: [
|
|
4467
4685
|
t("board.hostError", { error: snapshot.transportError }),
|
|
4468
4686
|
" ",
|
|
4469
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4687
|
+
snapshot.transportErrorRetryable === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4470
4688
|
type: "button",
|
|
4471
4689
|
className: board_module_css_default.linkButton,
|
|
4472
4690
|
onClick: () => {
|
|
@@ -5141,127 +5359,6 @@ window.__ModuleLoader__.load({
|
|
|
5141
5359
|
};
|
|
5142
5360
|
}
|
|
5143
5361
|
//#endregion
|
|
5144
|
-
//#region src/protocol.ts
|
|
5145
|
-
/** 本项目 API 前缀(AGENTS.md D2,2026-08-22 定值;参考实现占用 /api/task-board)。 */
|
|
5146
|
-
const TASK_BOARD_API_PREFIX = "/api/nova-task-board";
|
|
5147
|
-
//#endregion
|
|
5148
|
-
//#region src/client/host-api.ts
|
|
5149
|
-
/** 单次 Host 请求超时(§17 性能边界;超时按传输错误暴露,可重试)。 */
|
|
5150
|
-
const REQUEST_TIMEOUT_MS = 15e3;
|
|
5151
|
-
/** v1 迁移标记:本项目账本的 ledgerId(Host 确认后写入,防止重复导入)。 */
|
|
5152
|
-
const IMPORT_MARKER = "dsh.novaTaskBoard.v2.hostImported";
|
|
5153
|
-
/** v1 迁移 sourceId(一次迁移一个,写入后持久)。 */
|
|
5154
|
-
const SOURCE_KEY = "dsh.novaTaskBoard.v2.sourceId";
|
|
5155
|
-
/** v1 迁移 requestId(跨 Host 重启重试保持幂等)。 */
|
|
5156
|
-
const IMPORT_REQUEST_KEY = "dsh.novaTaskBoard.v2.importRequestId";
|
|
5157
|
-
/** 浏览器环境可用的 uuid(无 crypto 时退化为时间戳 + 随机串)。 */
|
|
5158
|
-
function uuid() {
|
|
5159
|
-
return globalThis.crypto?.randomUUID?.() ?? `browser-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
5160
|
-
}
|
|
5161
|
-
/** 读 JSON 响应体;非 2xx 按 Host 的错误信息抛错(§11.1 400/403/413/415)。 */
|
|
5162
|
-
async function readJson(response) {
|
|
5163
|
-
const body = await response.json();
|
|
5164
|
-
if (!response.ok) throw new Error(body.error ?? `task-board request failed: ${response.status}`);
|
|
5165
|
-
return body;
|
|
5166
|
-
}
|
|
5167
|
-
/**
|
|
5168
|
-
* 真实 HTTP + SSE 传输(同源 `/api/nova-task-board/*`)。
|
|
5169
|
-
* @param markers - 迁移标记存储(缺省用全局 localStorage)。
|
|
5170
|
-
*/
|
|
5171
|
-
var HttpTaskBoardHostTransport = class {
|
|
5172
|
-
markers;
|
|
5173
|
-
constructor(markers = globalThis.localStorage) {
|
|
5174
|
-
this.markers = markers;
|
|
5175
|
-
}
|
|
5176
|
-
async bootstrap(legacy) {
|
|
5177
|
-
const initial = await this.state();
|
|
5178
|
-
const ledgerId = initial.scheduler.ledgerId;
|
|
5179
|
-
if (legacy.length > 0 && ledgerId !== void 0 && this.markers?.getItem(IMPORT_MARKER) !== ledgerId) {
|
|
5180
|
-
let sourceId = this.markers?.getItem(SOURCE_KEY);
|
|
5181
|
-
if (sourceId === null || sourceId === void 0 || sourceId === "") {
|
|
5182
|
-
sourceId = uuid();
|
|
5183
|
-
this.markers?.setItem(SOURCE_KEY, sourceId);
|
|
5184
|
-
}
|
|
5185
|
-
let requestId = this.markers?.getItem(IMPORT_REQUEST_KEY);
|
|
5186
|
-
if (requestId === null || requestId === void 0 || requestId === "") {
|
|
5187
|
-
requestId = uuid();
|
|
5188
|
-
this.markers?.setItem(IMPORT_REQUEST_KEY, requestId);
|
|
5189
|
-
}
|
|
5190
|
-
const snapshot = await this.post(requestId, {
|
|
5191
|
-
kind: "import",
|
|
5192
|
-
sourceId,
|
|
5193
|
-
tasks: [...legacy]
|
|
5194
|
-
});
|
|
5195
|
-
this.markers?.setItem(IMPORT_MARKER, snapshot.scheduler.ledgerId ?? ledgerId);
|
|
5196
|
-
return snapshot;
|
|
5197
|
-
}
|
|
5198
|
-
return initial;
|
|
5199
|
-
}
|
|
5200
|
-
async state() {
|
|
5201
|
-
return await this.request(`${TASK_BOARD_API_PREFIX}/state`, { cache: "no-store" });
|
|
5202
|
-
}
|
|
5203
|
-
async action(action) {
|
|
5204
|
-
return await this.post(uuid(), action);
|
|
5205
|
-
}
|
|
5206
|
-
async post(requestId, action) {
|
|
5207
|
-
const envelope = {
|
|
5208
|
-
requestId,
|
|
5209
|
-
action
|
|
5210
|
-
};
|
|
5211
|
-
return await this.request(`${TASK_BOARD_API_PREFIX}/action`, {
|
|
5212
|
-
method: "POST",
|
|
5213
|
-
headers: { "content-type": "application/json" },
|
|
5214
|
-
body: JSON.stringify(envelope)
|
|
5215
|
-
});
|
|
5216
|
-
}
|
|
5217
|
-
/** 同源 fetch,带超时(超时按传输错误抛,控制器转为可重试的错误条)。 */
|
|
5218
|
-
async request(url, init) {
|
|
5219
|
-
const controller = new AbortController();
|
|
5220
|
-
const timeout = globalThis.setTimeout(() => {
|
|
5221
|
-
controller.abort();
|
|
5222
|
-
}, REQUEST_TIMEOUT_MS);
|
|
5223
|
-
try {
|
|
5224
|
-
return await readJson(await fetch(url, {
|
|
5225
|
-
...init,
|
|
5226
|
-
signal: controller.signal
|
|
5227
|
-
}));
|
|
5228
|
-
} catch (error) {
|
|
5229
|
-
if (controller.signal.aborted) throw new Error(`task-board Host request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s`);
|
|
5230
|
-
throw error;
|
|
5231
|
-
} finally {
|
|
5232
|
-
globalThis.clearTimeout(timeout);
|
|
5233
|
-
}
|
|
5234
|
-
}
|
|
5235
|
-
/**
|
|
5236
|
-
* SSE 订阅:消息帧解析为 `{revision, scheduler, power}`(解析失败按同步信号
|
|
5237
|
-
* 处理,重拉兜底);`onopen`(含断线重连成功)与页面重新可见 → 同步信号,
|
|
5238
|
-
* 控制器据此重拉全量 state。
|
|
5239
|
-
*/
|
|
5240
|
-
subscribe(listener) {
|
|
5241
|
-
const events = new EventSource(`${TASK_BOARD_API_PREFIX}/events`);
|
|
5242
|
-
events.onmessage = (message) => {
|
|
5243
|
-
try {
|
|
5244
|
-
const parsed = JSON.parse(message.data);
|
|
5245
|
-
if (parsed === null || typeof parsed !== "object" || typeof parsed.revision !== "number") throw new Error("invalid event frame");
|
|
5246
|
-
listener(parsed);
|
|
5247
|
-
} catch {
|
|
5248
|
-
listener();
|
|
5249
|
-
}
|
|
5250
|
-
};
|
|
5251
|
-
events.onopen = () => {
|
|
5252
|
-
listener();
|
|
5253
|
-
};
|
|
5254
|
-
const onVisible = () => {
|
|
5255
|
-
if (document.visibilityState === "visible") listener();
|
|
5256
|
-
};
|
|
5257
|
-
document.addEventListener("visibilitychange", onVisible);
|
|
5258
|
-
return () => {
|
|
5259
|
-
document.removeEventListener("visibilitychange", onVisible);
|
|
5260
|
-
events.close();
|
|
5261
|
-
};
|
|
5262
|
-
}
|
|
5263
|
-
};
|
|
5264
|
-
//#endregion
|
|
5265
5362
|
//#region src/client/legacy-store.ts
|
|
5266
5363
|
/** 参考实现的 v1 浏览器账本键(只读来源;其内部键名 `dsh.taskBoard.v1`)。 */
|
|
5267
5364
|
const LEGACY_V1_STORAGE_KEY = "dsh.taskBoard.v1";
|
|
@@ -5467,6 +5564,22 @@ window.__ModuleLoader__.load({
|
|
|
5467
5564
|
});
|
|
5468
5565
|
}
|
|
5469
5566
|
//#endregion
|
|
5567
|
+
//#region src/client/NovaPluginsSection.tsx
|
|
5568
|
+
/**
|
|
5569
|
+
* Render the Nova plugins section content column: a heading, a lede, and the
|
|
5570
|
+
* family plugin cards contributed into `nova.plugin.item`.
|
|
5571
|
+
* @param props - composed slot props (close, renderSlot, t).
|
|
5572
|
+
* @returns the section.
|
|
5573
|
+
*/
|
|
5574
|
+
function NovaPluginsSection(props) {
|
|
5575
|
+
const { renderSlot, t } = props;
|
|
5576
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
5577
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: t("title") }),
|
|
5578
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("description") }),
|
|
5579
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: renderSlot("nova.plugin.item", {}) })
|
|
5580
|
+
] });
|
|
5581
|
+
}
|
|
5582
|
+
//#endregion
|
|
5470
5583
|
//#region src/client/NovaTaskBoardSettingsCard.tsx
|
|
5471
5584
|
/**
|
|
5472
5585
|
* Nova task-board settings card skeleton (T001).
|
|
@@ -5527,8 +5640,12 @@ window.__ModuleLoader__.load({
|
|
|
5527
5640
|
}
|
|
5528
5641
|
//#endregion
|
|
5529
5642
|
//#region src/client/index.ts
|
|
5530
|
-
/** Locale namespace this plugin owns. */
|
|
5643
|
+
/** Locale namespace this plugin owns (the task-board card copy). */
|
|
5531
5644
|
const NS = "nova-task-board";
|
|
5645
|
+
/** Locale namespace of the "Nova 插件" section heading/lede. */
|
|
5646
|
+
const GROUP_NS = "nova-plugins";
|
|
5647
|
+
/** First-level settings nav id hosting the Nova family plugin cards. */
|
|
5648
|
+
const GROUP_SECTION_ID = "nova-plugins";
|
|
5532
5649
|
/** Settings namespace the settings card edits (the Host plugin registers it). */
|
|
5533
5650
|
const TASK_BOARD_NS = "nova-task-board";
|
|
5534
5651
|
/** Required services (fiber inject waiting — the runtime must be up first). */
|
|
@@ -5552,10 +5669,30 @@ window.__ModuleLoader__.load({
|
|
|
5552
5669
|
zh,
|
|
5553
5670
|
en
|
|
5554
5671
|
}), "nova-task-board: dictionaries");
|
|
5672
|
+
ctx.effect(() => ctx.locale.register(GROUP_NS, {
|
|
5673
|
+
zh: groupZh,
|
|
5674
|
+
en: groupEn
|
|
5675
|
+
}), "nova-plugins: dictionaries");
|
|
5676
|
+
ctx.slots.inject("settings.section", () => {
|
|
5677
|
+
const unregister = ctx.slots.register({
|
|
5678
|
+
name: "settings.section",
|
|
5679
|
+
id: GROUP_SECTION_ID,
|
|
5680
|
+
order: 112,
|
|
5681
|
+
label: () => ctx.locale.bind(GROUP_NS)("title"),
|
|
5682
|
+
locale: GROUP_NS,
|
|
5683
|
+
children: { "nova.plugin.item": {
|
|
5684
|
+
kind: "list",
|
|
5685
|
+
scope: "root"
|
|
5686
|
+
} }
|
|
5687
|
+
}, NovaPluginsSection);
|
|
5688
|
+
return () => {
|
|
5689
|
+
unregister();
|
|
5690
|
+
};
|
|
5691
|
+
});
|
|
5555
5692
|
const settingsScope = (ctx.get("webUiSettings") ?? ctx.settingsScope).bind({ namespace: TASK_BOARD_NS });
|
|
5556
|
-
ctx.slots.inject("
|
|
5693
|
+
ctx.slots.inject("nova.plugin.item", () => {
|
|
5557
5694
|
const unregister = ctx.slots.register({
|
|
5558
|
-
name: "
|
|
5695
|
+
name: "nova.plugin.item",
|
|
5559
5696
|
id: "nova-task-board",
|
|
5560
5697
|
order: 110,
|
|
5561
5698
|
locale: NS,
|