@linxin666/dsh-client-ui-task-board 0.1.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/LICENSE +29 -0
- package/README.md +211 -0
- package/cordis.patch.yml +12 -0
- package/lib/client.js +2592 -0
- package/lib/index.js +54 -0
- package/lib/invariant.js +6 -0
- package/lib/types/client/PluginSettingsCard.d.ts +77 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +51 -0
- package/lib/types/client/board/ConfirmDialog.d.ts +12 -0
- package/lib/types/client/board/NewTaskModal.d.ts +6 -0
- package/lib/types/client/board/TaskBoard.d.ts +5 -0
- package/lib/types/client/board/TaskCard.d.ts +12 -0
- package/lib/types/client/board/TaskDetail.d.ts +7 -0
- package/lib/types/client/board-mount.d.ts +10 -0
- package/lib/types/client/index.d.ts +43 -0
- package/lib/types/client/locales.d.ts +105 -0
- package/lib/types/client/settings-form.d.ts +118 -0
- package/lib/types/client/sidebar-entry.d.ts +25 -0
- package/lib/types/core/controller.d.ts +129 -0
- package/lib/types/core/execution.d.ts +136 -0
- package/lib/types/core/schedule.d.ts +40 -0
- package/lib/types/core/scheduler.d.ts +49 -0
- package/lib/types/core/store.d.ts +36 -0
- package/lib/types/core/tasks.d.ts +111 -0
- package/lib/types/index.d.ts +44 -0
- package/lib/types/invariant.d.ts +3 -0
- package/package.json +85 -0
- package/src/client/PluginSettingsCard.tsx +207 -0
- package/src/client/TaskBoardSettingsCard.tsx +124 -0
- package/src/client/board/ConfirmDialog.tsx +40 -0
- package/src/client/board/NewTaskModal.tsx +82 -0
- package/src/client/board/TaskBoard.tsx +107 -0
- package/src/client/board/TaskCard.tsx +62 -0
- package/src/client/board/TaskDetail.tsx +286 -0
- package/src/client/board-mount.tsx +74 -0
- package/src/client/board.module.css +849 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/index.ts +167 -0
- package/src/client/locales.ts +211 -0
- package/src/client/settings-card.module.css +261 -0
- package/src/client/settings-form.ts +294 -0
- package/src/client/sidebar-entry.ts +118 -0
- package/src/core/controller.ts +379 -0
- package/src/core/execution.ts +259 -0
- package/src/core/schedule.ts +140 -0
- package/src/core/scheduler.ts +104 -0
- package/src/core/store.ts +183 -0
- package/src/core/tasks.ts +204 -0
- package/src/index.ts +94 -0
- package/src/invariant.ts +4 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,2592 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@linxin666/dsh-client-ui-task-board",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react_dom_client = require("react-dom/client");
|
|
8
|
+
let react = require("react");
|
|
9
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
|
+
let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
|
|
11
|
+
//#region src/core/schedule.ts
|
|
12
|
+
/** Inclusive ranges per field, in cron order. */
|
|
13
|
+
const FIELD_RANGES = [
|
|
14
|
+
[0, 59],
|
|
15
|
+
[0, 23],
|
|
16
|
+
[1, 31],
|
|
17
|
+
[1, 12],
|
|
18
|
+
[0, 7]
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* Parse a 5-field cron expression.
|
|
22
|
+
* @returns the match sets, or null when the expression is invalid.
|
|
23
|
+
*/
|
|
24
|
+
function parseCron(expr) {
|
|
25
|
+
const fields = expr.trim().split(/\s+/);
|
|
26
|
+
if (fields.length !== 5) return null;
|
|
27
|
+
const sets = [];
|
|
28
|
+
for (let index = 0; index < 5; index++) {
|
|
29
|
+
const [min, max] = FIELD_RANGES[index];
|
|
30
|
+
const set = /* @__PURE__ */ new Set();
|
|
31
|
+
if (!parseField(fields[index], min, max, set)) return null;
|
|
32
|
+
sets.push(set);
|
|
33
|
+
}
|
|
34
|
+
const weekdays = /* @__PURE__ */ new Set();
|
|
35
|
+
for (const day of sets[4]) weekdays.add(day === 7 ? 0 : day);
|
|
36
|
+
return {
|
|
37
|
+
minutes: sets[0],
|
|
38
|
+
hours: sets[1],
|
|
39
|
+
days: sets[2],
|
|
40
|
+
months: sets[3],
|
|
41
|
+
weekdays,
|
|
42
|
+
dayWildcard: fields[2] === "*",
|
|
43
|
+
weekdayWildcard: fields[4] === "*"
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Whether the expression parses. */
|
|
47
|
+
function isValidCron(expr) {
|
|
48
|
+
return parseCron(expr) !== null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Compute the next matching instant after `fromMs` (ms epoch), in local time,
|
|
52
|
+
* at minute granularity, strictly greater than `fromMs`. Returns the ms epoch
|
|
53
|
+
* of the matching minute's start, or undefined when nothing matches within
|
|
54
|
+
* 366 days (e.g. `0 0 30 2 *`).
|
|
55
|
+
*/
|
|
56
|
+
function nextRunAtMs(expr, fromMs) {
|
|
57
|
+
const schedule = parseCron(expr);
|
|
58
|
+
if (schedule === null) return void 0;
|
|
59
|
+
const from = new Date(fromMs);
|
|
60
|
+
const scan = new Date(from.getFullYear(), from.getMonth(), from.getDate(), from.getHours(), from.getMinutes() + 1, 0, 0);
|
|
61
|
+
const limitMs = fromMs + 366 * 24 * 60 * 60 * 1e3;
|
|
62
|
+
while (scan.getTime() <= limitMs) {
|
|
63
|
+
if (matches(schedule, scan)) return scan.getTime();
|
|
64
|
+
scan.setMinutes(scan.getMinutes() + 1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Parse one comma-list field into the match set. */
|
|
68
|
+
function parseField(field, min, max, out) {
|
|
69
|
+
if (field === "*") {
|
|
70
|
+
for (let value = min; value <= max; value++) out.add(value);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
for (const part of field.split(",")) {
|
|
74
|
+
if (part === "") return false;
|
|
75
|
+
const [range, stepRaw] = part.split("/");
|
|
76
|
+
let low;
|
|
77
|
+
let high;
|
|
78
|
+
if (range === "*") {
|
|
79
|
+
low = min;
|
|
80
|
+
high = max;
|
|
81
|
+
} else if (range.includes("-")) {
|
|
82
|
+
const [a, b] = range.split("-");
|
|
83
|
+
if (a === "" || b === "" || !isDigits(a) || !isDigits(b)) return false;
|
|
84
|
+
low = Number(a);
|
|
85
|
+
high = Number(b);
|
|
86
|
+
} else if (isDigits(range)) {
|
|
87
|
+
low = Number(range);
|
|
88
|
+
high = Number(range);
|
|
89
|
+
} else return false;
|
|
90
|
+
if (low < min || high > max || low > high) return false;
|
|
91
|
+
const step = stepRaw === void 0 ? 1 : isDigits(stepRaw) ? Number(stepRaw) : NaN;
|
|
92
|
+
if (!Number.isInteger(step) || step < 1) return false;
|
|
93
|
+
for (let value = low; value <= high; value += step) out.add(value);
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
/** Day/weekday OR semantics: a restricted day field alone gates, and vice versa. */
|
|
98
|
+
function matches(schedule, date) {
|
|
99
|
+
if (!schedule.minutes.has(date.getMinutes())) return false;
|
|
100
|
+
if (!schedule.hours.has(date.getHours())) return false;
|
|
101
|
+
if (!schedule.months.has(date.getMonth() + 1)) return false;
|
|
102
|
+
const dayMatches = schedule.days.has(date.getDate());
|
|
103
|
+
const weekdayMatches = schedule.weekdays.has(date.getDay());
|
|
104
|
+
if (schedule.dayWildcard) return weekdayMatches;
|
|
105
|
+
if (schedule.weekdayWildcard) return dayMatches;
|
|
106
|
+
return dayMatches || weekdayMatches;
|
|
107
|
+
}
|
|
108
|
+
function isDigits(value) {
|
|
109
|
+
return /^\d+$/.test(value);
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/core/tasks.ts
|
|
113
|
+
/** The five kanban columns, in display order. */
|
|
114
|
+
const COLUMNS = [
|
|
115
|
+
{
|
|
116
|
+
status: "backlog",
|
|
117
|
+
label: "待规划"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
status: "todo",
|
|
121
|
+
label: "待办"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
status: "running",
|
|
125
|
+
label: "进行中"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
status: "done",
|
|
129
|
+
label: "已完成"
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
status: "failed",
|
|
133
|
+
label: "已失败"
|
|
134
|
+
}
|
|
135
|
+
];
|
|
136
|
+
/** Statuses a user may move a card to manually (execution states are owned by the runner). */
|
|
137
|
+
const MANUAL_STATUSES = ["backlog", "todo"];
|
|
138
|
+
/** All valid statuses (closed union guard). */
|
|
139
|
+
const ALL_STATUSES = [
|
|
140
|
+
"backlog",
|
|
141
|
+
"todo",
|
|
142
|
+
"running",
|
|
143
|
+
"done",
|
|
144
|
+
"failed"
|
|
145
|
+
];
|
|
146
|
+
/** Brand an unknown string as a status; undefined when it is not one. */
|
|
147
|
+
function isTaskStatus(value) {
|
|
148
|
+
return typeof value === "string" && ALL_STATUSES.includes(value);
|
|
149
|
+
}
|
|
150
|
+
/** Create a task from user input. */
|
|
151
|
+
function createTask(input, now, id) {
|
|
152
|
+
return {
|
|
153
|
+
id,
|
|
154
|
+
title: input.title.trim(),
|
|
155
|
+
description: input.description.trim(),
|
|
156
|
+
prompt: input.prompt.trim(),
|
|
157
|
+
status: "todo",
|
|
158
|
+
createdAt: now,
|
|
159
|
+
updatedAt: now,
|
|
160
|
+
executions: []
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** Clone a task with an updated status and a fresh updatedAt. */
|
|
164
|
+
function withStatus(task, status, now) {
|
|
165
|
+
return {
|
|
166
|
+
...task,
|
|
167
|
+
status,
|
|
168
|
+
updatedAt: now
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Merge a schedule patch into a task's schedule rule (creating it when
|
|
173
|
+
* absent), with a fresh updatedAt. Keys present in the patch overwrite the
|
|
174
|
+
* current value — including explicit `undefined`, which clears a field (used
|
|
175
|
+
* to disarm `nextRunAt`); absent keys keep their current value.
|
|
176
|
+
*/
|
|
177
|
+
function withSchedule(task, patch, now) {
|
|
178
|
+
const current = task.schedule;
|
|
179
|
+
const schedule = {
|
|
180
|
+
enabled: current?.enabled ?? false,
|
|
181
|
+
cron: current?.cron ?? "",
|
|
182
|
+
nextRunAt: current?.nextRunAt,
|
|
183
|
+
lastTriggeredAt: current?.lastTriggeredAt
|
|
184
|
+
};
|
|
185
|
+
if ("enabled" in patch) schedule.enabled = patch.enabled ?? false;
|
|
186
|
+
if ("cron" in patch) schedule.cron = patch.cron ?? "";
|
|
187
|
+
if ("nextRunAt" in patch) schedule.nextRunAt = patch.nextRunAt;
|
|
188
|
+
if ("lastTriggeredAt" in patch) schedule.lastTriggeredAt = patch.lastTriggeredAt;
|
|
189
|
+
return {
|
|
190
|
+
...task,
|
|
191
|
+
updatedAt: now,
|
|
192
|
+
schedule
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Open a fresh execution on a task: move it to 'running' and append a
|
|
197
|
+
* running execution record. Returns the new task and the new execution.
|
|
198
|
+
*/
|
|
199
|
+
function startExecution(task, now, executionId) {
|
|
200
|
+
const execution = {
|
|
201
|
+
id: executionId,
|
|
202
|
+
sessionId: void 0,
|
|
203
|
+
startedAt: now,
|
|
204
|
+
endedAt: void 0,
|
|
205
|
+
result: void 0,
|
|
206
|
+
error: void 0
|
|
207
|
+
};
|
|
208
|
+
return {
|
|
209
|
+
task: {
|
|
210
|
+
...task,
|
|
211
|
+
status: "running",
|
|
212
|
+
updatedAt: now,
|
|
213
|
+
executions: [...task.executions, execution]
|
|
214
|
+
},
|
|
215
|
+
execution
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Settle a running execution: record the outcome and move the task into the
|
|
220
|
+
* matching column. No-op (returns the input task) when the execution is not
|
|
221
|
+
* the task's latest or is already settled.
|
|
222
|
+
*/
|
|
223
|
+
function settleExecution(task, executionId, outcome, now, error) {
|
|
224
|
+
const index = task.executions.findIndex((execution) => execution.id === executionId);
|
|
225
|
+
if (index === -1) return task;
|
|
226
|
+
const execution = task.executions[index];
|
|
227
|
+
if (execution.endedAt !== void 0) return task;
|
|
228
|
+
const settled = {
|
|
229
|
+
...execution,
|
|
230
|
+
endedAt: now,
|
|
231
|
+
result: outcome,
|
|
232
|
+
error
|
|
233
|
+
};
|
|
234
|
+
const executions = [...task.executions];
|
|
235
|
+
executions[index] = settled;
|
|
236
|
+
const status = outcome === "succeeded" ? "done" : outcome === "failed" ? "failed" : task.status === "running" ? "todo" : task.status;
|
|
237
|
+
return {
|
|
238
|
+
...task,
|
|
239
|
+
status,
|
|
240
|
+
updatedAt: now,
|
|
241
|
+
executions
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/** A settled-execution summary string for the detail view. */
|
|
245
|
+
function executionLabel(execution) {
|
|
246
|
+
if (execution.result === "succeeded") return "succeeded";
|
|
247
|
+
if (execution.result === "failed") return "failed";
|
|
248
|
+
if (execution.result === "cancelled") return "cancelled";
|
|
249
|
+
return "running";
|
|
250
|
+
}
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region src/core/controller.ts
|
|
253
|
+
/** The selected task (resolved from the ledger), or undefined. */
|
|
254
|
+
function selectedTaskOf(snapshot) {
|
|
255
|
+
if (snapshot.selectedTaskId === void 0) return void 0;
|
|
256
|
+
return snapshot.tasks.find((task) => task.id === snapshot.selectedTaskId);
|
|
257
|
+
}
|
|
258
|
+
function randomUuid() {
|
|
259
|
+
const bytes = globalThis.crypto?.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
|
|
260
|
+
if (bytes === void 0) return `t-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
261
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
262
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
263
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
264
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
265
|
+
}
|
|
266
|
+
/** Read the current selection off a session-list snapshot (structural). */
|
|
267
|
+
function currentOf(sessions) {
|
|
268
|
+
return sessions.list.getSnapshot().current;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Board controller (see module doc). All mutations bump the snapshot and
|
|
272
|
+
* persist through the store; UI and DOM mounts subscribe and re-render.
|
|
273
|
+
*/
|
|
274
|
+
var BoardController = class {
|
|
275
|
+
deps;
|
|
276
|
+
tasks = [];
|
|
277
|
+
boardOpen = false;
|
|
278
|
+
selectedTaskId;
|
|
279
|
+
listeners = /* @__PURE__ */ new Set();
|
|
280
|
+
disposers = [];
|
|
281
|
+
now;
|
|
282
|
+
uuid;
|
|
283
|
+
/** @param deps - store, execution service, and the sessions navigation face. */
|
|
284
|
+
constructor(deps) {
|
|
285
|
+
this.deps = deps;
|
|
286
|
+
this.now = deps.now ?? (() => Date.now());
|
|
287
|
+
this.uuid = deps.uuid ?? randomUuid;
|
|
288
|
+
}
|
|
289
|
+
/** Load the persisted ledger and start the navigation/status subscriptions. */
|
|
290
|
+
start() {
|
|
291
|
+
this.tasks = this.deps.store.load();
|
|
292
|
+
this.reconcileRunningTasks();
|
|
293
|
+
this.disposers.push(this.deps.sessions.list.subscribe(() => {
|
|
294
|
+
this.onSessionsChanged();
|
|
295
|
+
}));
|
|
296
|
+
this.notify();
|
|
297
|
+
}
|
|
298
|
+
/** Stop all subscriptions and drop retained state (idempotent). */
|
|
299
|
+
dispose() {
|
|
300
|
+
for (const dispose of this.disposers.splice(0)) dispose();
|
|
301
|
+
this.listeners.clear();
|
|
302
|
+
if (this.reconcileTimer !== void 0) clearTimeout(this.reconcileTimer);
|
|
303
|
+
this.reconcileTimer = void 0;
|
|
304
|
+
}
|
|
305
|
+
getSnapshot() {
|
|
306
|
+
return {
|
|
307
|
+
tasks: this.tasks,
|
|
308
|
+
boardOpen: this.boardOpen,
|
|
309
|
+
selectedTaskId: this.selectedTaskId
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
subscribe(fn) {
|
|
313
|
+
this.listeners.add(fn);
|
|
314
|
+
return () => {
|
|
315
|
+
this.listeners.delete(fn);
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
openBoard() {
|
|
319
|
+
if (this.boardOpen) return;
|
|
320
|
+
this.lastCurrent = currentOf(this.deps.sessions);
|
|
321
|
+
this.boardOpen = true;
|
|
322
|
+
this.notify();
|
|
323
|
+
}
|
|
324
|
+
closeBoard() {
|
|
325
|
+
if (!this.boardOpen) return;
|
|
326
|
+
this.boardOpen = false;
|
|
327
|
+
this.notify();
|
|
328
|
+
}
|
|
329
|
+
toggleBoard() {
|
|
330
|
+
if (this.boardOpen) this.closeBoard();
|
|
331
|
+
else this.openBoard();
|
|
332
|
+
}
|
|
333
|
+
openTask(id) {
|
|
334
|
+
if (this.tasks.some((task) => task.id === id)) {
|
|
335
|
+
this.selectedTaskId = id;
|
|
336
|
+
this.notify();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
closeTask() {
|
|
340
|
+
if (this.selectedTaskId === void 0) return;
|
|
341
|
+
this.selectedTaskId = void 0;
|
|
342
|
+
this.notify();
|
|
343
|
+
}
|
|
344
|
+
createTask(input) {
|
|
345
|
+
if (input.title.trim() === "") return void 0;
|
|
346
|
+
const task = createTask(input, this.now(), this.uuid());
|
|
347
|
+
this.tasks = [...this.tasks, task];
|
|
348
|
+
this.persistAndNotify();
|
|
349
|
+
return task;
|
|
350
|
+
}
|
|
351
|
+
updateTask(id, patch) {
|
|
352
|
+
this.tasks = this.tasks.map((task) => task.id === id ? {
|
|
353
|
+
...task,
|
|
354
|
+
...patch,
|
|
355
|
+
updatedAt: this.now()
|
|
356
|
+
} : task);
|
|
357
|
+
this.persistAndNotify();
|
|
358
|
+
}
|
|
359
|
+
moveTask(id, status) {
|
|
360
|
+
this.tasks = this.tasks.map((task) => task.id === id ? withStatus(task, status, this.now()) : task);
|
|
361
|
+
this.persistAndNotify();
|
|
362
|
+
}
|
|
363
|
+
deleteTask(id) {
|
|
364
|
+
this.tasks = this.tasks.filter((task) => task.id !== id);
|
|
365
|
+
if (this.selectedTaskId === id) this.selectedTaskId = void 0;
|
|
366
|
+
this.persistAndNotify();
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Update a task's schedule rule. A blank or invalid cron expression is
|
|
370
|
+
* rejected (returns false, state untouched). When the rule ends up enabled
|
|
371
|
+
* the next run instant is computed immediately; a disabled rule carries no
|
|
372
|
+
* next-run instant.
|
|
373
|
+
* @param id - the task to schedule.
|
|
374
|
+
* @param patch - fields to change (absent fields keep their current value).
|
|
375
|
+
* @returns true when applied, false when rejected (invalid cron / unknown task).
|
|
376
|
+
*/
|
|
377
|
+
setSchedule(id, patch) {
|
|
378
|
+
const task = this.tasks.find((candidate) => candidate.id === id);
|
|
379
|
+
if (task === void 0) return false;
|
|
380
|
+
const current = task.schedule;
|
|
381
|
+
const cron = (patch.cron ?? current?.cron ?? "").trim();
|
|
382
|
+
if (cron === "" || !isValidCron(cron)) return false;
|
|
383
|
+
const enabled = patch.enabled ?? current?.enabled ?? false;
|
|
384
|
+
const nextRunAt = enabled ? nextRunAtMs(cron, this.now()) : void 0;
|
|
385
|
+
this.tasks = this.tasks.map((candidate) => candidate.id === id ? withSchedule(candidate, {
|
|
386
|
+
enabled,
|
|
387
|
+
cron,
|
|
388
|
+
nextRunAt
|
|
389
|
+
}, this.now()) : candidate);
|
|
390
|
+
this.persistAndNotify();
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Roll a task's schedule forward (scheduler callback): persist the next due
|
|
395
|
+
* instant and the trigger instant of this run. No-op when the task has no
|
|
396
|
+
* schedule rule (it was deleted mid-tick, for example).
|
|
397
|
+
*/
|
|
398
|
+
applyScheduleNextRun(id, nextRunAt, lastTriggeredAt) {
|
|
399
|
+
this.tasks = this.tasks.map((task) => task.id === id && task.schedule !== void 0 ? withSchedule(task, {
|
|
400
|
+
nextRunAt,
|
|
401
|
+
lastTriggeredAt
|
|
402
|
+
}, this.now()) : task);
|
|
403
|
+
this.persistAndNotify();
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Jump to an execution's session transcript. Selecting the session changes
|
|
407
|
+
* `current`, which closes the board (the conversation view takes over).
|
|
408
|
+
* @param sessionId - the execution session to open.
|
|
409
|
+
*/
|
|
410
|
+
openSession(sessionId) {
|
|
411
|
+
this.deps.sessions.open(sessionId);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Execute a task for real: move it to 'running', open an execution record,
|
|
415
|
+
* and hand off to the ExecutionService. A second call while the task is
|
|
416
|
+
* already running is ignored.
|
|
417
|
+
*/
|
|
418
|
+
async runTask(id) {
|
|
419
|
+
const task = this.tasks.find((candidate) => candidate.id === id);
|
|
420
|
+
if (task === void 0 || task.status === "running") return false;
|
|
421
|
+
const { task: next, execution } = startExecution(task, this.now(), this.uuid());
|
|
422
|
+
this.tasks = this.tasks.map((candidate) => candidate.id === id ? next : candidate);
|
|
423
|
+
this.persistAndNotify();
|
|
424
|
+
this.activeExecutionIds.add(execution.id);
|
|
425
|
+
await this.deps.exec.run(next, execution, (event) => {
|
|
426
|
+
this.handleExecutionEvent(event);
|
|
427
|
+
});
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
/** Re-run a settled task: move it back to 'todo' first, then execute. */
|
|
431
|
+
async rerunTask(id) {
|
|
432
|
+
const task = this.tasks.find((candidate) => candidate.id === id);
|
|
433
|
+
if (task === void 0) return;
|
|
434
|
+
if (task.status !== "running") {
|
|
435
|
+
this.tasks = this.tasks.map((candidate) => candidate.id === id ? withStatus(candidate, "todo", this.now()) : candidate);
|
|
436
|
+
this.persistAndNotify();
|
|
437
|
+
}
|
|
438
|
+
await this.runTask(id);
|
|
439
|
+
}
|
|
440
|
+
handleExecutionEvent(event) {
|
|
441
|
+
if (event.kind === "started") {
|
|
442
|
+
this.tasks = this.tasks.map((task) => task.id === event.taskId ? attachSessionId(task, event.executionId, event.sessionId, this.now()) : task);
|
|
443
|
+
this.persistAndNotify();
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
this.activeExecutionIds.delete(event.executionId);
|
|
447
|
+
this.tasks = this.tasks.map((task) => task.id === event.taskId ? settleExecution(task, event.executionId, event.outcome, this.now(), event.error) : task);
|
|
448
|
+
this.persistAndNotify();
|
|
449
|
+
}
|
|
450
|
+
/** Reconcile running tasks and close the board when the user navigates. */
|
|
451
|
+
onSessionsChanged() {
|
|
452
|
+
this.scheduleReconcile();
|
|
453
|
+
if (!this.boardOpen) return;
|
|
454
|
+
const current = currentOf(this.deps.sessions);
|
|
455
|
+
if (current !== this.lastCurrent) this.closeBoard();
|
|
456
|
+
this.lastCurrent = current;
|
|
457
|
+
}
|
|
458
|
+
lastCurrent = void 0;
|
|
459
|
+
/** Execution ids launched on this page; they settle via their live watch, never list reconciliation. */
|
|
460
|
+
activeExecutionIds = /* @__PURE__ */ new Set();
|
|
461
|
+
/** Debounce timer for {@link reconcileRunningTasks}. */
|
|
462
|
+
reconcileTimer = void 0;
|
|
463
|
+
/** Whether a reconcile pass is underway (single-flight guard). */
|
|
464
|
+
reconcileInFlight = false;
|
|
465
|
+
/**
|
|
466
|
+
* Debounce + single-flight trigger for the running-task reconciliation.
|
|
467
|
+
* Session-list notifications arrive in bursts (one per session status
|
|
468
|
+
* change); both guards together keep a burst from reading the history API
|
|
469
|
+
* once per running task.
|
|
470
|
+
*/
|
|
471
|
+
scheduleReconcile() {
|
|
472
|
+
if (this.reconcileTimer !== void 0) return;
|
|
473
|
+
this.reconcileTimer = setTimeout(() => {
|
|
474
|
+
this.reconcileTimer = void 0;
|
|
475
|
+
this.reconcileRunningTasks();
|
|
476
|
+
}, this.deps.reconcileDebounceMs ?? 350);
|
|
477
|
+
}
|
|
478
|
+
/** Settle tasks left 'running' whose sessions already finished. */
|
|
479
|
+
async reconcileRunningTasks() {
|
|
480
|
+
if (this.reconcileInFlight) return;
|
|
481
|
+
this.reconcileInFlight = true;
|
|
482
|
+
try {
|
|
483
|
+
const events = [];
|
|
484
|
+
for (const task of this.tasks) {
|
|
485
|
+
if (task.status !== "running") continue;
|
|
486
|
+
const execution = task.executions[task.executions.length - 1];
|
|
487
|
+
if (execution !== void 0 && this.activeExecutionIds.has(execution.id)) continue;
|
|
488
|
+
const event = await this.deps.exec.reconcile(task);
|
|
489
|
+
if (event !== void 0 && event.kind === "settled") events.push({
|
|
490
|
+
task,
|
|
491
|
+
event
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
if (events.length === 0) return;
|
|
495
|
+
let changed = false;
|
|
496
|
+
for (const { task, event } of events) {
|
|
497
|
+
const next = settleExecution(task, event.executionId, event.outcome, this.now(), event.error);
|
|
498
|
+
if (next === task) continue;
|
|
499
|
+
this.tasks = this.tasks.map((candidate) => candidate.id === task.id ? next : candidate);
|
|
500
|
+
changed = true;
|
|
501
|
+
}
|
|
502
|
+
if (changed) this.persistAndNotify();
|
|
503
|
+
} finally {
|
|
504
|
+
this.reconcileInFlight = false;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
persistAndNotify() {
|
|
508
|
+
this.deps.store.save(this.tasks);
|
|
509
|
+
this.notify();
|
|
510
|
+
}
|
|
511
|
+
notify() {
|
|
512
|
+
for (const fn of [...this.listeners]) fn();
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
/** Record which session ran an execution (once the execution service reports it). */
|
|
516
|
+
function attachSessionId(task, executionId, sessionId, now) {
|
|
517
|
+
return {
|
|
518
|
+
...task,
|
|
519
|
+
updatedAt: now,
|
|
520
|
+
executions: task.executions.map((execution) => execution.id === executionId ? {
|
|
521
|
+
...execution,
|
|
522
|
+
sessionId
|
|
523
|
+
} : execution)
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
//#endregion
|
|
527
|
+
//#region src/core/execution.ts
|
|
528
|
+
/** Human copy for a run failure. */
|
|
529
|
+
function messageOf(error) {
|
|
530
|
+
if (error instanceof Error) return error.message;
|
|
531
|
+
return String(error);
|
|
532
|
+
}
|
|
533
|
+
/** Whether a `turn/end` payload closed the turn with an error reason. */
|
|
534
|
+
function isErrorTurnEnd(data) {
|
|
535
|
+
if (typeof data !== "object" || data === null) return false;
|
|
536
|
+
const reason = data.reason;
|
|
537
|
+
return typeof reason === "object" && reason !== null && reason.kind === "error";
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Run one task to completion (or to a settled failure).
|
|
541
|
+
*
|
|
542
|
+
* @param task - the task being executed.
|
|
543
|
+
* @param execution - the freshly opened execution record (id + start time).
|
|
544
|
+
* @param onEvent - callback for started/settled events.
|
|
545
|
+
* @returns resolves when the run settles (or fails to start); never rejects —
|
|
546
|
+
* every failure path is reported as a settled event.
|
|
547
|
+
*/
|
|
548
|
+
var ExecutionService = class {
|
|
549
|
+
env;
|
|
550
|
+
/** @param env - the runtime faces (real or fake). */
|
|
551
|
+
constructor(env) {
|
|
552
|
+
this.env = env;
|
|
553
|
+
}
|
|
554
|
+
async run(task, execution, onEvent) {
|
|
555
|
+
try {
|
|
556
|
+
const sessionId = await this.connectSession();
|
|
557
|
+
onEvent({
|
|
558
|
+
kind: "started",
|
|
559
|
+
taskId: task.id,
|
|
560
|
+
executionId: execution.id,
|
|
561
|
+
sessionId
|
|
562
|
+
});
|
|
563
|
+
const driver = this.driverOf(sessionId);
|
|
564
|
+
if (driver === void 0) {
|
|
565
|
+
onEvent({
|
|
566
|
+
kind: "settled",
|
|
567
|
+
taskId: task.id,
|
|
568
|
+
executionId: execution.id,
|
|
569
|
+
outcome: "failed",
|
|
570
|
+
error: "execution session is not ready"
|
|
571
|
+
});
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
await driver.rename(task.title).catch(() => {});
|
|
575
|
+
const baseline = driver.getSnapshot().turnEnds.size;
|
|
576
|
+
const accepted = await this.sendPrompt(driver, task);
|
|
577
|
+
if (!accepted.ok) {
|
|
578
|
+
onEvent({
|
|
579
|
+
kind: "settled",
|
|
580
|
+
taskId: task.id,
|
|
581
|
+
executionId: execution.id,
|
|
582
|
+
outcome: "failed",
|
|
583
|
+
error: messageOf(accepted.error)
|
|
584
|
+
});
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
this.watchForSettlement(driver, task.id, execution.id, onEvent, baseline);
|
|
588
|
+
} catch (error) {
|
|
589
|
+
onEvent({
|
|
590
|
+
kind: "settled",
|
|
591
|
+
taskId: task.id,
|
|
592
|
+
executionId: execution.id,
|
|
593
|
+
outcome: "failed",
|
|
594
|
+
error: messageOf(error)
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Inspect a reloaded/background task that was left 'running' and emit a
|
|
600
|
+
* settled event when its session already finished.
|
|
601
|
+
*
|
|
602
|
+
* A session that was never opened keeps a cold conversation snapshot (the
|
|
603
|
+
* runtime only maintains the window for the staged/current session), so the
|
|
604
|
+
* settled outcome is decided by the strongest available signal, in order:
|
|
605
|
+
* 1. the list summary — missing session → cancelled; still running → pending;
|
|
606
|
+
* 2. a warm conversation snapshot → `lastAgentError` decides failed/succeeded;
|
|
607
|
+
* 3. the raw history tail (when a history face is wired) — a `turn/end`
|
|
608
|
+
* error reason proves failure;
|
|
609
|
+
* 4. otherwise a finished session counts as succeeded.
|
|
610
|
+
*
|
|
611
|
+
* @param task - a task whose latest execution has no endedAt.
|
|
612
|
+
* @returns a settled event when the session state proves completion, else undefined.
|
|
613
|
+
*/
|
|
614
|
+
async reconcile(task) {
|
|
615
|
+
const execution = task.executions[task.executions.length - 1];
|
|
616
|
+
if (execution === void 0 || execution.sessionId === void 0 || execution.endedAt !== void 0) return void 0;
|
|
617
|
+
const list = this.env.sessions.list.getSnapshot();
|
|
618
|
+
if (list.phase !== "ready") return void 0;
|
|
619
|
+
const summary = list.byId[execution.sessionId];
|
|
620
|
+
if (summary === void 0) return {
|
|
621
|
+
kind: "settled",
|
|
622
|
+
taskId: task.id,
|
|
623
|
+
executionId: execution.id,
|
|
624
|
+
outcome: "cancelled",
|
|
625
|
+
error: "execution session no longer exists"
|
|
626
|
+
};
|
|
627
|
+
if (summary.running) return void 0;
|
|
628
|
+
const driver = this.driverOf(execution.sessionId);
|
|
629
|
+
if (driver !== void 0) {
|
|
630
|
+
const snapshot = driver.getSnapshot();
|
|
631
|
+
if (snapshot.turnEnds.size > 0) {
|
|
632
|
+
const outcome = snapshot.lastAgentError !== null ? "failed" : "succeeded";
|
|
633
|
+
return {
|
|
634
|
+
kind: "settled",
|
|
635
|
+
taskId: task.id,
|
|
636
|
+
executionId: execution.id,
|
|
637
|
+
outcome,
|
|
638
|
+
error: snapshot.lastAgentError ?? void 0
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (await this.historyShowsFailure(execution.sessionId)) return {
|
|
643
|
+
kind: "settled",
|
|
644
|
+
taskId: task.id,
|
|
645
|
+
executionId: execution.id,
|
|
646
|
+
outcome: "failed",
|
|
647
|
+
error: "agent turn failed"
|
|
648
|
+
};
|
|
649
|
+
return {
|
|
650
|
+
kind: "settled",
|
|
651
|
+
taskId: task.id,
|
|
652
|
+
executionId: execution.id,
|
|
653
|
+
outcome: "succeeded"
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
/** Best-effort failure probe over the raw history tail (false when unavailable). */
|
|
657
|
+
async historyShowsFailure(sessionId) {
|
|
658
|
+
const history = this.env.history;
|
|
659
|
+
if (history === void 0) return false;
|
|
660
|
+
try {
|
|
661
|
+
const tail = await history.loadTail(sessionId);
|
|
662
|
+
if (tail === void 0) return false;
|
|
663
|
+
return tail.events.some((event) => event.type === "turn/end" && isErrorTurnEnd(event.data));
|
|
664
|
+
} catch (error) {
|
|
665
|
+
console.error("[dsh-task-board] history failure probe failed", error);
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
async connectSession() {
|
|
670
|
+
const workspace = this.env.workspaces.list.getSnapshot();
|
|
671
|
+
const workspaceId = workspace.recentWorkspaceId ?? workspace.items[0]?.workspaceId;
|
|
672
|
+
if (workspaceId === void 0) throw new Error("no workspace available to run the task in");
|
|
673
|
+
return this.env.workspaces.connectWorkspace(workspaceId);
|
|
674
|
+
}
|
|
675
|
+
driverOf(sessionId) {
|
|
676
|
+
return this.env.sessions.binding(sessionId)?.session;
|
|
677
|
+
}
|
|
678
|
+
async sendPrompt(driver, task) {
|
|
679
|
+
const text = task.prompt.trim() !== "" ? task.prompt : task.title;
|
|
680
|
+
try {
|
|
681
|
+
return await driver.prompt([{
|
|
682
|
+
type: "text",
|
|
683
|
+
text
|
|
684
|
+
}], "queue");
|
|
685
|
+
} catch (error) {
|
|
686
|
+
return {
|
|
687
|
+
ok: false,
|
|
688
|
+
error
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Subscribe to the execution session and settle the run once the accepted
|
|
694
|
+
* turn completes (turn counter advanced past the acceptance baseline and
|
|
695
|
+
* the session is no longer running). Never settles while the session is
|
|
696
|
+
* still running; unsubscribes on settle.
|
|
697
|
+
*/
|
|
698
|
+
watchForSettlement(driver, taskId, executionId, onEvent, baseline) {
|
|
699
|
+
let settled = false;
|
|
700
|
+
let unsubscribe = () => {};
|
|
701
|
+
const check = () => {
|
|
702
|
+
if (settled) return;
|
|
703
|
+
const snapshot = driver.getSnapshot();
|
|
704
|
+
if (snapshot.running || snapshot.turnEnds.size <= baseline) return;
|
|
705
|
+
settled = true;
|
|
706
|
+
unsubscribe();
|
|
707
|
+
onEvent({
|
|
708
|
+
kind: "settled",
|
|
709
|
+
taskId,
|
|
710
|
+
executionId,
|
|
711
|
+
outcome: snapshot.lastAgentError !== null ? "failed" : "succeeded",
|
|
712
|
+
error: snapshot.lastAgentError ?? void 0
|
|
713
|
+
});
|
|
714
|
+
};
|
|
715
|
+
unsubscribe = driver.subscribe(check);
|
|
716
|
+
check();
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
//#endregion
|
|
720
|
+
//#region src/core/scheduler.ts
|
|
721
|
+
/**
|
|
722
|
+
* Browser-side scheduler: the heartbeat behind scheduled task runs.
|
|
723
|
+
*
|
|
724
|
+
* The board is a pure client plugin with no server channel, so "定时任务"
|
|
725
|
+
* lives in the tab: a timer ticks every minute (plus immediately on tab
|
|
726
|
+
* visibility recovery) and triggers any task whose `schedule.nextRunAt` is
|
|
727
|
+
* due, rolling the schedule forward to the next cron match before triggering
|
|
728
|
+
* so the same tick never double-fires. Missed runs are skipped, never queued:
|
|
729
|
+
* a task still running at its due instant is skipped by the controller's
|
|
730
|
+
* runTask guard and simply waits for the next cron match.
|
|
731
|
+
*
|
|
732
|
+
* Framework-free: all runtime access flows through the injected deps
|
|
733
|
+
* (structural faces), so tests drive ticks directly without timers.
|
|
734
|
+
*/
|
|
735
|
+
/**
|
|
736
|
+
* The schedule heartbeat (see module doc). `tick` is public so tests and
|
|
737
|
+
* callers can drive a check without waiting for the interval.
|
|
738
|
+
*/
|
|
739
|
+
var SchedulerService = class {
|
|
740
|
+
deps;
|
|
741
|
+
timer;
|
|
742
|
+
disposed = false;
|
|
743
|
+
/** @param deps - tasks/clock/trigger/apply faces (see {@link SchedulerDeps}). */
|
|
744
|
+
constructor(deps) {
|
|
745
|
+
this.deps = deps;
|
|
746
|
+
}
|
|
747
|
+
/** Start ticking: one immediate check (catch-up after reload) + the interval. */
|
|
748
|
+
start() {
|
|
749
|
+
if (this.disposed) return;
|
|
750
|
+
this.tick();
|
|
751
|
+
this.timer = setInterval(() => {
|
|
752
|
+
this.tick();
|
|
753
|
+
}, this.deps.tickMs ?? 6e4);
|
|
754
|
+
this.deps.environment?.addEventListener("visibilitychange", this.onVisibility);
|
|
755
|
+
}
|
|
756
|
+
/** Stop ticking and drop listeners (idempotent). */
|
|
757
|
+
dispose() {
|
|
758
|
+
this.disposed = true;
|
|
759
|
+
if (this.timer !== void 0) clearInterval(this.timer);
|
|
760
|
+
this.timer = void 0;
|
|
761
|
+
this.deps.environment?.removeEventListener("visibilitychange", this.onVisibility);
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Check every enabled schedule and trigger the due ones. Idempotent per
|
|
765
|
+
* task per tick: the schedule is rolled forward only after runTask accepts
|
|
766
|
+
* the run, so a rejected run keeps its due slot and is retried on the next
|
|
767
|
+
* tick instead of being silently dropped.
|
|
768
|
+
*/
|
|
769
|
+
async tick() {
|
|
770
|
+
if (this.disposed) return;
|
|
771
|
+
if (this.deps.ready !== void 0 && !this.deps.ready()) return;
|
|
772
|
+
const now = this.deps.now();
|
|
773
|
+
for (const task of this.deps.tasks()) {
|
|
774
|
+
const schedule = task.schedule;
|
|
775
|
+
if (schedule === void 0 || !schedule.enabled) continue;
|
|
776
|
+
if (schedule.nextRunAt === void 0) {
|
|
777
|
+
const repaired = nextRunAtMs(schedule.cron, now);
|
|
778
|
+
if (repaired === void 0) continue;
|
|
779
|
+
this.deps.applySchedule(task.id, repaired, void 0);
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
if (schedule.nextRunAt > now) continue;
|
|
783
|
+
const next = nextRunAtMs(schedule.cron, schedule.nextRunAt);
|
|
784
|
+
if (await this.deps.runTask(task.id)) this.deps.applySchedule(task.id, next, now);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
onVisibility = () => {
|
|
788
|
+
this.tick();
|
|
789
|
+
};
|
|
790
|
+
};
|
|
791
|
+
//#endregion
|
|
792
|
+
//#region src/core/store.ts
|
|
793
|
+
/**
|
|
794
|
+
* Task persistence: a small storage seam with a localStorage backend.
|
|
795
|
+
*
|
|
796
|
+
* The task-board client plugin runs in the browser, and dsh exposes no
|
|
797
|
+
* browser-writable file channel (same conclusion the skin-center research
|
|
798
|
+
* reached for cordis.patch.yml), so tasks persist in the browser's
|
|
799
|
+
* localStorage under a versioned key — the same persistence mechanism dsh's
|
|
800
|
+
* own client snapshot stores use (`createSnapshotStore` persist). Data
|
|
801
|
+
* survives page refreshes and dsh restarts (same origin), and survives
|
|
802
|
+
* plugin uninstall (the key is simply left in place).
|
|
803
|
+
*
|
|
804
|
+
* The seam keeps the backend swappable (e.g. an IndexedDB or a host-file
|
|
805
|
+
* channel later); tests run against the in-memory backend and a jsdom
|
|
806
|
+
* localStorage backend.
|
|
807
|
+
*/
|
|
808
|
+
/** Storage key for the task ledger document. */
|
|
809
|
+
const DEFAULT_STORAGE_KEY = "dsh.taskBoard.v1";
|
|
810
|
+
/**
|
|
811
|
+
* Structural row check with the status left unvalidated (see {@link parseLedger}).
|
|
812
|
+
* The `schedule` field is deliberately NOT checked here: a malformed schedule
|
|
813
|
+
* never drops the task row — {@link normalizeSchedule} repairs or drops the
|
|
814
|
+
* schedule alone.
|
|
815
|
+
*/
|
|
816
|
+
function isTaskRecordShape(value) {
|
|
817
|
+
if (typeof value !== "object" || value === null) return false;
|
|
818
|
+
const record = value;
|
|
819
|
+
if (typeof record.id !== "string" || record.id === "") return false;
|
|
820
|
+
if (typeof record.title !== "string") return false;
|
|
821
|
+
if (typeof record.description !== "string") return false;
|
|
822
|
+
if (typeof record.prompt !== "string") return false;
|
|
823
|
+
if (typeof record.createdAt !== "number") return false;
|
|
824
|
+
if (typeof record.updatedAt !== "number") return false;
|
|
825
|
+
if (!Array.isArray(record.executions)) return false;
|
|
826
|
+
for (const execution of record.executions) {
|
|
827
|
+
if (typeof execution !== "object" || execution === null) return false;
|
|
828
|
+
const entry = execution;
|
|
829
|
+
if (typeof entry.id !== "string") return false;
|
|
830
|
+
if (entry.sessionId !== void 0 && typeof entry.sessionId !== "string") return false;
|
|
831
|
+
if (typeof entry.startedAt !== "number") return false;
|
|
832
|
+
if (entry.endedAt !== void 0 && typeof entry.endedAt !== "number") return false;
|
|
833
|
+
if (entry.result !== void 0 && entry.result !== "succeeded" && entry.result !== "failed" && entry.result !== "cancelled") return false;
|
|
834
|
+
if (entry.error !== void 0 && typeof entry.error !== "string") return false;
|
|
835
|
+
}
|
|
836
|
+
return true;
|
|
837
|
+
}
|
|
838
|
+
/** Normalize an unknown persisted status back into the closed status union. */
|
|
839
|
+
function normalizeStatus(status) {
|
|
840
|
+
return isTaskStatus(status) ? status : "todo";
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Repair a persisted schedule rule: drop rules without a usable cron string,
|
|
844
|
+
* coerce booleans/numbers, and leave `nextRunAt`/`lastTriggeredAt` undefined
|
|
845
|
+
* when missing (a fresh recompute or the next tick fixes them).
|
|
846
|
+
*/
|
|
847
|
+
function normalizeSchedule(schedule) {
|
|
848
|
+
if (typeof schedule !== "object" || schedule === null) return void 0;
|
|
849
|
+
const rule = schedule;
|
|
850
|
+
if (typeof rule.cron !== "string") return void 0;
|
|
851
|
+
if (rule.cron.trim() === "" || !isValidCron(rule.cron)) return void 0;
|
|
852
|
+
return {
|
|
853
|
+
enabled: rule.enabled === true,
|
|
854
|
+
cron: rule.cron,
|
|
855
|
+
nextRunAt: typeof rule.nextRunAt === "number" ? rule.nextRunAt : void 0,
|
|
856
|
+
lastTriggeredAt: typeof rule.lastTriggeredAt === "number" ? rule.lastTriggeredAt : void 0
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
/** Parse + validate a persisted ledger document; invalid rows are dropped. */
|
|
860
|
+
function parseLedger(raw) {
|
|
861
|
+
if (raw === null) return [];
|
|
862
|
+
let parsed;
|
|
863
|
+
try {
|
|
864
|
+
parsed = JSON.parse(raw);
|
|
865
|
+
} catch (error) {
|
|
866
|
+
console.error("[dsh-task-board] persisted task ledger is not valid JSON; starting empty", error);
|
|
867
|
+
return [];
|
|
868
|
+
}
|
|
869
|
+
if (!Array.isArray(parsed)) {
|
|
870
|
+
console.error("[dsh-task-board] persisted task ledger is not an array; starting empty");
|
|
871
|
+
return [];
|
|
872
|
+
}
|
|
873
|
+
const tasks = [];
|
|
874
|
+
for (const row of parsed) {
|
|
875
|
+
if (!isTaskRecordShape(row)) {
|
|
876
|
+
console.warn("[dsh-task-board] dropping invalid task row from persisted ledger", row);
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
const task = {
|
|
880
|
+
...row,
|
|
881
|
+
status: normalizeStatus(row.status)
|
|
882
|
+
};
|
|
883
|
+
task.schedule = normalizeSchedule(row.schedule);
|
|
884
|
+
tasks.push(task);
|
|
885
|
+
}
|
|
886
|
+
return tasks;
|
|
887
|
+
}
|
|
888
|
+
/** localStorage-backed store (the browser backend). */
|
|
889
|
+
var LocalStorageTaskStore = class {
|
|
890
|
+
key;
|
|
891
|
+
storage;
|
|
892
|
+
/**
|
|
893
|
+
* @param key - storage key for the ledger document.
|
|
894
|
+
* @param storage - storage backend (defaults to the global localStorage; tests inject fakes).
|
|
895
|
+
*/
|
|
896
|
+
constructor(key = DEFAULT_STORAGE_KEY, storage = globalThis.localStorage) {
|
|
897
|
+
this.key = key;
|
|
898
|
+
this.storage = storage;
|
|
899
|
+
}
|
|
900
|
+
load() {
|
|
901
|
+
if (this.storage === void 0) return [];
|
|
902
|
+
try {
|
|
903
|
+
return parseLedger(this.storage.getItem(this.key));
|
|
904
|
+
} catch (error) {
|
|
905
|
+
console.error("[dsh-task-board] task ledger read failed; starting empty", error);
|
|
906
|
+
return [];
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
save(tasks) {
|
|
910
|
+
if (this.storage === void 0) return;
|
|
911
|
+
try {
|
|
912
|
+
this.storage.setItem(this.key, JSON.stringify(tasks));
|
|
913
|
+
} catch (error) {
|
|
914
|
+
console.error("[dsh-task-board] task ledger write failed (persistence skipped)", error);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
clear() {
|
|
918
|
+
if (this.storage === void 0) return;
|
|
919
|
+
try {
|
|
920
|
+
this.storage.removeItem(this.key);
|
|
921
|
+
} catch (error) {
|
|
922
|
+
console.error("[dsh-task-board] task ledger clear failed", error);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
//#endregion
|
|
927
|
+
//#region src/client/locales.ts
|
|
928
|
+
/**
|
|
929
|
+
* Task-board copy: zh-first dictionaries with an English fallback, selected
|
|
930
|
+
* by the document language. Kept dependency-free (no dsh locale service) so
|
|
931
|
+
* the DOM-injected entry row and the standalone board tree share one tiny
|
|
932
|
+
* lookup.
|
|
933
|
+
*/
|
|
934
|
+
/** zh dictionary (key-set source of truth). */
|
|
935
|
+
const zh = {
|
|
936
|
+
"entry.label": "任务看板",
|
|
937
|
+
"board.title": "任务看板",
|
|
938
|
+
"board.close": "返回对话",
|
|
939
|
+
"board.new": "新建任务",
|
|
940
|
+
"board.search": "筛选任务…",
|
|
941
|
+
"board.empty": "这个状态还没有任务",
|
|
942
|
+
"board.filterAll": "全部",
|
|
943
|
+
"board.status": "状态",
|
|
944
|
+
"board.status.backlog": "待规划",
|
|
945
|
+
"board.status.todo": "待办",
|
|
946
|
+
"board.status.running": "进行中",
|
|
947
|
+
"board.status.done": "已完成",
|
|
948
|
+
"board.status.failed": "已失败",
|
|
949
|
+
"board.runs": "次执行",
|
|
950
|
+
"board.updated": "更新于",
|
|
951
|
+
"board.created": "创建于",
|
|
952
|
+
"new.title": "标题",
|
|
953
|
+
"new.titlePlaceholder": "一句话描述要做什么",
|
|
954
|
+
"new.description": "描述",
|
|
955
|
+
"new.descriptionPlaceholder": "补充背景、范围与验收(可选)",
|
|
956
|
+
"new.prompt": "执行 Prompt",
|
|
957
|
+
"new.promptPlaceholder": "发给 agent 的完整指令(留空则使用标题)",
|
|
958
|
+
"new.submit": "创建",
|
|
959
|
+
"new.cancel": "取消",
|
|
960
|
+
"new.required": "标题不能为空",
|
|
961
|
+
"detail.title": "任务详情",
|
|
962
|
+
"detail.close": "关闭",
|
|
963
|
+
"detail.prompt": "执行 Prompt",
|
|
964
|
+
"detail.description": "描述",
|
|
965
|
+
"detail.execution": "执行记录",
|
|
966
|
+
"detail.noExecution": "尚未执行",
|
|
967
|
+
"detail.run": "执行",
|
|
968
|
+
"detail.rerun": "重新执行",
|
|
969
|
+
"detail.delete": "删除",
|
|
970
|
+
"detail.viewSession": "查看会话",
|
|
971
|
+
"detail.noSession": "暂无会话",
|
|
972
|
+
"detail.executionStarted": "已启动",
|
|
973
|
+
"detail.executionEnded": "已结束",
|
|
974
|
+
"detail.result.succeeded": "成功",
|
|
975
|
+
"detail.result.failed": "失败",
|
|
976
|
+
"detail.result.cancelled": "已取消",
|
|
977
|
+
"detail.result.running": "进行中",
|
|
978
|
+
"delete.title": "删除任务",
|
|
979
|
+
"delete.confirm": "确定删除「{name}」吗?删除后不可恢复。",
|
|
980
|
+
"delete.ok": "删除",
|
|
981
|
+
"delete.cancel": "取消",
|
|
982
|
+
"status.move.backlog": "移到待规划",
|
|
983
|
+
"status.move.todo": "移到待办",
|
|
984
|
+
"exec.error.noWorkspace": "没有可用工作区,无法执行任务",
|
|
985
|
+
"exec.error.promptRejected": "Prompt 被拒绝",
|
|
986
|
+
"run.failed": "执行失败:{error}",
|
|
987
|
+
"time.justNow": "刚刚",
|
|
988
|
+
"detail.schedule": "定时运行",
|
|
989
|
+
"detail.schedule.enable": "启用定时执行",
|
|
990
|
+
"detail.schedule.cron": "Cron 表达式",
|
|
991
|
+
"detail.schedule.presets": "预设",
|
|
992
|
+
"detail.schedule.preset.daily9": "每天 09:00",
|
|
993
|
+
"detail.schedule.preset.hourly": "每小时",
|
|
994
|
+
"detail.schedule.preset.tenMin": "每 10 分钟",
|
|
995
|
+
"detail.schedule.preset.weeklyMon9": "每周一 09:00",
|
|
996
|
+
"detail.schedule.nextRun": "下次运行",
|
|
997
|
+
"detail.schedule.lastTriggered": "上次触发",
|
|
998
|
+
"detail.schedule.invalid": "Cron 表达式无效",
|
|
999
|
+
"detail.schedule.notScheduled": "尚未排程",
|
|
1000
|
+
"detail.schedule.dueSoon": "即将运行",
|
|
1001
|
+
"card.scheduled": "定时",
|
|
1002
|
+
"settings.title": "任务看板",
|
|
1003
|
+
"settings.description": "控制看板在 agent 系统提示中的播报行为。",
|
|
1004
|
+
"settings.enabled": "启用任务看板",
|
|
1005
|
+
"settings.enabledHint": "关闭后隐藏侧边栏入口与看板视图。",
|
|
1006
|
+
"settings.announceToAgent": "向 agent 播报任务看板",
|
|
1007
|
+
"settings.announceToAgentHint": "开启:每条 agent 系统提示都会包含本看板的说明;关闭:不播报,agent 仅在用户主动提及时了解看板。",
|
|
1008
|
+
"settings.inherit": "继承",
|
|
1009
|
+
"settings.on": "开",
|
|
1010
|
+
"settings.off": "关",
|
|
1011
|
+
"settings.overridden": "已覆盖",
|
|
1012
|
+
"settings.reset": "恢复默认",
|
|
1013
|
+
"settings.readOnly": "当前部署的设置只读。",
|
|
1014
|
+
"settings.expand": "展开设置",
|
|
1015
|
+
"settings.collapse": "收起设置",
|
|
1016
|
+
"settings.save": "保存",
|
|
1017
|
+
"settings.saving": "保存中…",
|
|
1018
|
+
"settings.discard": "放弃",
|
|
1019
|
+
"settings.unsaved": "未保存",
|
|
1020
|
+
"settings.saveFailed": "部署未接受这些值,已保留供你修改。",
|
|
1021
|
+
"settings.invalidNumber": "请输入数字,留空则使用默认值。"
|
|
1022
|
+
};
|
|
1023
|
+
/** en dictionary, complete against the zh key set. */
|
|
1024
|
+
const en = {
|
|
1025
|
+
"entry.label": "Task Board",
|
|
1026
|
+
"board.title": "Task Board",
|
|
1027
|
+
"board.close": "Back to chat",
|
|
1028
|
+
"board.new": "New Task",
|
|
1029
|
+
"board.search": "Filter tasks…",
|
|
1030
|
+
"board.empty": "No tasks in this column",
|
|
1031
|
+
"board.filterAll": "All",
|
|
1032
|
+
"board.status": "Status",
|
|
1033
|
+
"board.status.backlog": "Backlog",
|
|
1034
|
+
"board.status.todo": "To Do",
|
|
1035
|
+
"board.status.running": "In Progress",
|
|
1036
|
+
"board.status.done": "Done",
|
|
1037
|
+
"board.status.failed": "Failed",
|
|
1038
|
+
"board.runs": "runs",
|
|
1039
|
+
"board.updated": "Updated",
|
|
1040
|
+
"board.created": "Created",
|
|
1041
|
+
"new.title": "Title",
|
|
1042
|
+
"new.titlePlaceholder": "What should be done, in one line",
|
|
1043
|
+
"new.description": "Description",
|
|
1044
|
+
"new.descriptionPlaceholder": "Background, scope, acceptance criteria (optional)",
|
|
1045
|
+
"new.prompt": "Run Prompt",
|
|
1046
|
+
"new.promptPlaceholder": "The full instruction sent to the agent (title is used when blank)",
|
|
1047
|
+
"new.submit": "Create",
|
|
1048
|
+
"new.cancel": "Cancel",
|
|
1049
|
+
"new.required": "Title is required",
|
|
1050
|
+
"detail.title": "Task Detail",
|
|
1051
|
+
"detail.close": "Close",
|
|
1052
|
+
"detail.prompt": "Run Prompt",
|
|
1053
|
+
"detail.description": "Description",
|
|
1054
|
+
"detail.execution": "Execution History",
|
|
1055
|
+
"detail.noExecution": "Not executed yet",
|
|
1056
|
+
"detail.run": "Run",
|
|
1057
|
+
"detail.rerun": "Run Again",
|
|
1058
|
+
"detail.delete": "Delete",
|
|
1059
|
+
"detail.viewSession": "View Session",
|
|
1060
|
+
"detail.noSession": "No session",
|
|
1061
|
+
"detail.executionStarted": "Started",
|
|
1062
|
+
"detail.executionEnded": "Ended",
|
|
1063
|
+
"detail.result.succeeded": "Succeeded",
|
|
1064
|
+
"detail.result.failed": "Failed",
|
|
1065
|
+
"detail.result.cancelled": "Cancelled",
|
|
1066
|
+
"detail.result.running": "Running",
|
|
1067
|
+
"delete.title": "Delete Task",
|
|
1068
|
+
"delete.confirm": "Delete \"{name}\"? This cannot be undone.",
|
|
1069
|
+
"delete.ok": "Delete",
|
|
1070
|
+
"delete.cancel": "Cancel",
|
|
1071
|
+
"status.move.backlog": "Move to Backlog",
|
|
1072
|
+
"status.move.todo": "Move to To Do",
|
|
1073
|
+
"exec.error.noWorkspace": "No workspace is available to run the task",
|
|
1074
|
+
"exec.error.promptRejected": "Prompt rejected",
|
|
1075
|
+
"run.failed": "Run failed: {error}",
|
|
1076
|
+
"time.justNow": "just now",
|
|
1077
|
+
"detail.schedule": "Scheduled Runs",
|
|
1078
|
+
"detail.schedule.enable": "Enable scheduled runs",
|
|
1079
|
+
"detail.schedule.cron": "Cron expression",
|
|
1080
|
+
"detail.schedule.presets": "Presets",
|
|
1081
|
+
"detail.schedule.preset.daily9": "Every day 09:00",
|
|
1082
|
+
"detail.schedule.preset.hourly": "Every hour",
|
|
1083
|
+
"detail.schedule.preset.tenMin": "Every 10 minutes",
|
|
1084
|
+
"detail.schedule.preset.weeklyMon9": "Every Monday 09:00",
|
|
1085
|
+
"detail.schedule.nextRun": "Next run",
|
|
1086
|
+
"detail.schedule.lastTriggered": "Last triggered",
|
|
1087
|
+
"detail.schedule.invalid": "Invalid cron expression",
|
|
1088
|
+
"detail.schedule.notScheduled": "Not scheduled yet",
|
|
1089
|
+
"detail.schedule.dueSoon": "Due soon",
|
|
1090
|
+
"card.scheduled": "scheduled",
|
|
1091
|
+
"settings.title": "Task Board",
|
|
1092
|
+
"settings.description": "How the board announces itself in each agent system prompt.",
|
|
1093
|
+
"settings.enabled": "Enable the task board",
|
|
1094
|
+
"settings.enabledHint": "When off, the sidebar entry and board view are hidden.",
|
|
1095
|
+
"settings.announceToAgent": "Announce the task board to agents",
|
|
1096
|
+
"settings.announceToAgentHint": "On: every agent system prompt includes a note about this board. Off: no announcement; agents learn about the board only when you mention it.",
|
|
1097
|
+
"settings.inherit": "Inherit",
|
|
1098
|
+
"settings.on": "On",
|
|
1099
|
+
"settings.off": "Off",
|
|
1100
|
+
"settings.overridden": "Overridden",
|
|
1101
|
+
"settings.reset": "Reset to default",
|
|
1102
|
+
"settings.readOnly": "This deployment stores settings read-only.",
|
|
1103
|
+
"settings.expand": "Show settings",
|
|
1104
|
+
"settings.collapse": "Hide settings",
|
|
1105
|
+
"settings.save": "Save",
|
|
1106
|
+
"settings.saving": "Saving…",
|
|
1107
|
+
"settings.discard": "Discard",
|
|
1108
|
+
"settings.unsaved": "Unsaved",
|
|
1109
|
+
"settings.saveFailed": "The deployment did not accept these values; they were left for you to correct.",
|
|
1110
|
+
"settings.invalidNumber": "Enter a number, or leave blank to use the default."
|
|
1111
|
+
};
|
|
1112
|
+
/** Active dictionary, picked by the document language at call time. */
|
|
1113
|
+
function dictionary() {
|
|
1114
|
+
return (typeof document !== "undefined" ? document.documentElement.lang : "zh").toLowerCase().startsWith("en") ? en : zh;
|
|
1115
|
+
}
|
|
1116
|
+
/** Translate a key with optional {name} template params. */
|
|
1117
|
+
function t(key, params) {
|
|
1118
|
+
let text = dictionary()[key];
|
|
1119
|
+
if (params !== void 0) for (const [name, value] of Object.entries(params)) text = text.replaceAll(`{${name}}`, value);
|
|
1120
|
+
return text;
|
|
1121
|
+
}
|
|
1122
|
+
//#endregion
|
|
1123
|
+
//#region \0dsh-css:/Users/zcl/code/dsh-web-ui/packages/dsh-task-board/src/client/board.module.css.mjs
|
|
1124
|
+
const css$1 = "[data-pane=conversation]{position:relative}[data-dsh-taskboard-view]{z-index:5;display:none;position:absolute;inset:0}html[data-dsh-taskboard-active] [data-dsh-taskboard-view]{display:block}html[data-dsh-taskboard-active] [data-pane=conversation]>:not([data-dsh-taskboard-view]){display:none}.jckUcG_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}.jckUcG_entry:hover{background:var(--dsw-specific-sidebar-nav-item-hover);color:var(--dsw-alias-label-primary)}.jckUcG_entry[data-active]{background:var(--dsw-specific-sidebar-nav-item-active);color:var(--dsw-alias-label-primary);font-weight:600}.jckUcG_entryIcon{flex:none;justify-content:center;align-items:center;display:inline-flex}.jckUcG_entryLabel{text-overflow:ellipsis;overflow:hidden}[data-dsh-frame][data-sidebar-collapsed] .jckUcG_entry{justify-content:center;width:100%;padding:0}[data-dsh-frame][data-sidebar-collapsed] .jckUcG_entryLabel{display:none}.jckUcG_board{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}.jckUcG_boardHeader{flex:none;align-items:center;gap:10px;display:flex}.jckUcG_boardTitle{color:var(--dsw-alias-label-primary);white-space:nowrap;margin:0;font-size:16px;font-weight:700}.jckUcG_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}.jckUcG_search::placeholder{color:var(--dsw-alias-label-tertiary)}.jckUcG_columns{flex:1;grid-template-columns:repeat(5,minmax(0,1fr));gap:12px;min-height:0;display:grid}.jckUcG_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}.jckUcG_columnHeader{flex:none;align-items:center;gap:6px;padding:10px 12px;display:flex}.jckUcG_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}.jckUcG_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}.jckUcG_statusDot{border-radius:50%;flex:none;width:8px;height:8px}.jckUcG_statusDot[data-status=backlog]{background:var(--dsw-alias-label-tertiary)}.jckUcG_statusDot[data-status=todo]{background:var(--dsw-alias-state-business-primary)}.jckUcG_statusDot[data-status=running]{background:var(--dsw-alias-state-warn-primary)}.jckUcG_statusDot[data-status=done]{background:var(--dsw-alias-state-success-primary)}.jckUcG_statusDot[data-status=failed]{background:var(--dsw-alias-state-error-primary)}.jckUcG_cards{flex-direction:column;flex:1;gap:8px;min-height:0;padding:2px 8px 10px;display:flex;overflow-y:auto}.jckUcG_columnEmpty{text-align:center;color:var(--dsw-alias-label-tertiary);padding:24px 8px;font-size:12px}.jckUcG_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}.jckUcG_card:hover{box-shadow:var(--dsw-shadow-lv2);border-color:var(--dsw-alias-border-l3);transform:translateY(-1px)}.jckUcG_card[data-status=running]{border-color:var(--dsw-alias-state-warn-primary)}.jckUcG_cardTitle{-webkit-line-clamp:2;-webkit-box-orient:vertical;font-size:13px;font-weight:600;line-height:1.35;display:-webkit-box;overflow:hidden}.jckUcG_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}.jckUcG_cardMeta{color:var(--dsw-alias-label-tertiary);align-items:center;gap:8px;font-size:11px;display:flex}.jckUcG_cardTime{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.jckUcG_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}.jckUcG_cardRun{flex:none}.jckUcG_cardRun[data-result=failed]{color:var(--dsw-alias-state-error-primary)}.jckUcG_cardRun[data-result=succeeded]{color:var(--dsw-alias-state-success-primary)}.jckUcG_cardSession{color:var(--dsw-alias-state-business-primary);flex:none}.jckUcG_cardRunningLabel{color:var(--dsw-alias-state-warn-primary);font-size:11px}.jckUcG_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 jckUcG_dshTbSpin}@keyframes jckUcG_dshTbSpin{to{transform:rotate(360deg)}}.jckUcG_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}.jckUcG_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-info-hover)}.jckUcG_primaryButton:disabled{opacity:.5;cursor:default}.jckUcG_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}.jckUcG_ghostButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.jckUcG_ghostButton:disabled{opacity:.45;cursor:default}.jckUcG_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}.jckUcG_dangerButton:hover:not(:disabled){filter:brightness(1.08)}.jckUcG_dangerButton:active:not(:disabled){filter:brightness(.94)}.jckUcG_dangerButton:disabled{opacity:.5;cursor:default}.jckUcG_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}.jckUcG_iconButton:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.jckUcG_linkButton{color:var(--dsw-alias-state-business-primary);cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:0;font-size:12px}.jckUcG_linkButton:hover{text-decoration:underline}.jckUcG_modalBackdrop{z-index:40;background:var(--dsw-alias-bg-mask-1);justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.jckUcG_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}.jckUcG_modalTitle{margin:0;font-size:15px;font-weight:700}.jckUcG_confirmMessage{color:var(--dsw-alias-label-secondary);white-space:pre-wrap;overflow-wrap:anywhere;margin:0;font-size:13px;line-height:1.5}.jckUcG_modalFooter{justify-content:flex-end;gap:10px;margin-top:4px;display:flex}.jckUcG_field{flex-direction:column;gap:5px;display:flex}.jckUcG_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}.jckUcG_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}.jckUcG_input:focus{border-color:var(--dsw-alias-state-business-primary)}.jckUcG_input::placeholder{color:var(--dsw-alias-label-tertiary)}.jckUcG_formError{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}.jckUcG_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}.jckUcG_detailHeader{border-bottom:1px solid var(--dsw-alias-separator-primary);flex:none;align-items:center;gap:10px;padding:14px 18px;display:flex}.jckUcG_detailTitle{overflow-wrap:anywhere;flex:1;margin:0;font-size:15px;font-weight:700}.jckUcG_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}.jckUcG_statusBadge[data-status=running]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}.jckUcG_statusBadge[data-status=done]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.jckUcG_statusBadge[data-status=failed]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.jckUcG_detailBody{flex-direction:column;flex:1;gap:16px;padding:14px 18px;display:flex;overflow-y:auto}.jckUcG_detailSection{flex-direction:column;gap:6px;display:flex}.jckUcG_detailSection h4{color:var(--dsw-alias-label-tertiary);text-transform:none;margin:0;font-size:12px;font-weight:700}.jckUcG_detailText{color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere;margin:0;font-size:13px;line-height:1.55}.jckUcG_scheduleToggle{color:var(--dsw-alias-label-primary);cursor:pointer;user-select:none;align-items:center;gap:8px;font-size:13px;display:flex}.jckUcG_scheduleToggle input{accent-color:var(--dsw-alias-state-business-primary)}.jckUcG_scheduleRow{align-items:center;gap:8px;display:flex}.jckUcG_scheduleInput{min-width:0;font-family:var(--dsw-font-markdown-code-block-small);flex:1;font-size:12.5px}.jckUcG_scheduleInputInvalid,.jckUcG_scheduleInputInvalid:focus{border-color:var(--dsw-alias-state-error-primary)}.jckUcG_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}.jckUcG_scheduleMeta{color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;margin:0;font-size:12px}.jckUcG_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}.jckUcG_executionList{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}.jckUcG_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}.jckUcG_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}.jckUcG_executionBadge[data-result=succeeded]{color:var(--dsw-alias-state-success-primary);background:0 0}.jckUcG_executionBadge[data-result=failed]{color:var(--dsw-alias-state-error-primary);background:0 0}.jckUcG_executionBadge[data-result=cancelled]{color:var(--dsw-alias-label-tertiary);background:0 0}.jckUcG_executionTimes{color:var(--dsw-alias-label-secondary);font-size:12px}.jckUcG_executionError{width:100%;color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;font-size:12px}.jckUcG_moveRow{flex-wrap:wrap;gap:8px;display:flex}.jckUcG_detailFooter{border-top:1px solid var(--dsw-alias-separator-primary);flex:none;align-items:center;gap:10px;padding:12px 18px;display:flex}.jckUcG_detailMeta{color:var(--dsw-alias-label-tertiary);margin-left:auto;font-size:11px}.jckUcG_entry:focus-visible,.jckUcG_card:focus-visible,.jckUcG_primaryButton:focus-visible,.jckUcG_ghostButton:focus-visible,.jckUcG_dangerButton:focus-visible,.jckUcG_iconButton:focus-visible,.jckUcG_linkButton:focus-visible,.jckUcG_search:focus-visible,.jckUcG_input:focus-visible,.jckUcG_schedulePreset:focus-visible,.jckUcG_scheduleToggle input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}.jckUcG_entry,.jckUcG_primaryButton,.jckUcG_ghostButton,.jckUcG_dangerButton,.jckUcG_iconButton,.jckUcG_linkButton,.jckUcG_search,.jckUcG_input,.jckUcG_schedulePreset,.jckUcG_scheduleToggle input{transition:background-color .12s,color .12s,border-color .12s,outline-color .12s,box-shadow .12s,transform .12s}.jckUcG_card:active{box-shadow:var(--dsw-shadow-lv1);transform:translateY(0)}.jckUcG_entry:active,.jckUcG_primaryButton:active:not(:disabled),.jckUcG_ghostButton:active:not(:disabled),.jckUcG_dangerButton:active:not(:disabled),.jckUcG_iconButton:active:not(:disabled),.jckUcG_linkButton:active:not(:disabled){transform:translateY(1px)}.jckUcG_entry[data-active]:hover{background:var(--dsw-specific-sidebar-nav-item-active)}.jckUcG_iconButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.jckUcG_linkButton:hover:not(:disabled){text-decoration:underline}.jckUcG_iconButton:disabled,.jckUcG_linkButton:disabled{opacity:.45;cursor:default}.jckUcG_search:focus,.jckUcG_schedulePreset:focus{border-color:var(--dsw-alias-state-business-primary)}.jckUcG_scheduleToggle input{margin:0}@media (prefers-reduced-motion:reduce){.jckUcG_entry,.jckUcG_card,.jckUcG_primaryButton,.jckUcG_ghostButton,.jckUcG_dangerButton,.jckUcG_iconButton,.jckUcG_linkButton,.jckUcG_search,.jckUcG_input,.jckUcG_schedulePreset,.jckUcG_scheduleToggle input{transition:none}.jckUcG_cardSpinner{animation:none}}";
|
|
1125
|
+
const tagId$1 = "@linxin666/dsh-client-ui-task-board/board.module.css";
|
|
1126
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
1127
|
+
const tag = document.createElement("style");
|
|
1128
|
+
tag.dataset.plugin = "@linxin666/dsh-client-ui-task-board";
|
|
1129
|
+
tag.dataset.pluginCss = tagId$1;
|
|
1130
|
+
tag.textContent = css$1;
|
|
1131
|
+
document.head.appendChild(tag);
|
|
1132
|
+
}
|
|
1133
|
+
var board_module_css_default = {
|
|
1134
|
+
"board": "jckUcG_board",
|
|
1135
|
+
"boardHeader": "jckUcG_boardHeader",
|
|
1136
|
+
"boardTitle": "jckUcG_boardTitle",
|
|
1137
|
+
"card": "jckUcG_card",
|
|
1138
|
+
"cardExcerpt": "jckUcG_cardExcerpt",
|
|
1139
|
+
"cardMeta": "jckUcG_cardMeta",
|
|
1140
|
+
"cardRun": "jckUcG_cardRun",
|
|
1141
|
+
"cardRunningLabel": "jckUcG_cardRunningLabel",
|
|
1142
|
+
"cardSchedule": "jckUcG_cardSchedule",
|
|
1143
|
+
"cardSession": "jckUcG_cardSession",
|
|
1144
|
+
"cardSpinner": "jckUcG_cardSpinner",
|
|
1145
|
+
"cardTime": "jckUcG_cardTime",
|
|
1146
|
+
"cardTitle": "jckUcG_cardTitle",
|
|
1147
|
+
"cards": "jckUcG_cards",
|
|
1148
|
+
"column": "jckUcG_column",
|
|
1149
|
+
"columnCount": "jckUcG_columnCount",
|
|
1150
|
+
"columnEmpty": "jckUcG_columnEmpty",
|
|
1151
|
+
"columnHeader": "jckUcG_columnHeader",
|
|
1152
|
+
"columnTitle": "jckUcG_columnTitle",
|
|
1153
|
+
"columns": "jckUcG_columns",
|
|
1154
|
+
"confirmMessage": "jckUcG_confirmMessage",
|
|
1155
|
+
"dangerButton": "jckUcG_dangerButton",
|
|
1156
|
+
"detail": "jckUcG_detail",
|
|
1157
|
+
"detailBody": "jckUcG_detailBody",
|
|
1158
|
+
"detailFooter": "jckUcG_detailFooter",
|
|
1159
|
+
"detailHeader": "jckUcG_detailHeader",
|
|
1160
|
+
"detailMeta": "jckUcG_detailMeta",
|
|
1161
|
+
"detailSection": "jckUcG_detailSection",
|
|
1162
|
+
"detailText": "jckUcG_detailText",
|
|
1163
|
+
"detailTitle": "jckUcG_detailTitle",
|
|
1164
|
+
"dshTbSpin": "jckUcG_dshTbSpin",
|
|
1165
|
+
"entry": "jckUcG_entry",
|
|
1166
|
+
"entryIcon": "jckUcG_entryIcon",
|
|
1167
|
+
"entryLabel": "jckUcG_entryLabel",
|
|
1168
|
+
"executionBadge": "jckUcG_executionBadge",
|
|
1169
|
+
"executionError": "jckUcG_executionError",
|
|
1170
|
+
"executionList": "jckUcG_executionList",
|
|
1171
|
+
"executionRow": "jckUcG_executionRow",
|
|
1172
|
+
"executionTimes": "jckUcG_executionTimes",
|
|
1173
|
+
"field": "jckUcG_field",
|
|
1174
|
+
"fieldLabel": "jckUcG_fieldLabel",
|
|
1175
|
+
"formError": "jckUcG_formError",
|
|
1176
|
+
"ghostButton": "jckUcG_ghostButton",
|
|
1177
|
+
"iconButton": "jckUcG_iconButton",
|
|
1178
|
+
"input": "jckUcG_input",
|
|
1179
|
+
"linkButton": "jckUcG_linkButton",
|
|
1180
|
+
"modal": "jckUcG_modal",
|
|
1181
|
+
"modalBackdrop": "jckUcG_modalBackdrop",
|
|
1182
|
+
"modalFooter": "jckUcG_modalFooter",
|
|
1183
|
+
"modalTitle": "jckUcG_modalTitle",
|
|
1184
|
+
"moveRow": "jckUcG_moveRow",
|
|
1185
|
+
"primaryButton": "jckUcG_primaryButton",
|
|
1186
|
+
"promptBlock": "jckUcG_promptBlock",
|
|
1187
|
+
"scheduleInput": "jckUcG_scheduleInput",
|
|
1188
|
+
"scheduleInputInvalid": "jckUcG_scheduleInputInvalid",
|
|
1189
|
+
"scheduleMeta": "jckUcG_scheduleMeta",
|
|
1190
|
+
"schedulePreset": "jckUcG_schedulePreset",
|
|
1191
|
+
"scheduleRow": "jckUcG_scheduleRow",
|
|
1192
|
+
"scheduleToggle": "jckUcG_scheduleToggle",
|
|
1193
|
+
"search": "jckUcG_search",
|
|
1194
|
+
"statusBadge": "jckUcG_statusBadge",
|
|
1195
|
+
"statusDot": "jckUcG_statusDot"
|
|
1196
|
+
};
|
|
1197
|
+
//#endregion
|
|
1198
|
+
//#region src/client/board/NewTaskModal.tsx
|
|
1199
|
+
/**
|
|
1200
|
+
* New-task modal: title + description + the prompt that execution will send.
|
|
1201
|
+
* Creates through the controller (which persists immediately).
|
|
1202
|
+
*/
|
|
1203
|
+
/** New-task form overlay. */
|
|
1204
|
+
function NewTaskModal({ controller, onClose }) {
|
|
1205
|
+
const [title, setTitle] = (0, react.useState)("");
|
|
1206
|
+
const [description, setDescription] = (0, react.useState)("");
|
|
1207
|
+
const [prompt, setPrompt] = (0, react.useState)("");
|
|
1208
|
+
const [error, setError] = (0, react.useState)(void 0);
|
|
1209
|
+
const submit = () => {
|
|
1210
|
+
if (controller.createTask({
|
|
1211
|
+
title,
|
|
1212
|
+
description,
|
|
1213
|
+
prompt
|
|
1214
|
+
}) === void 0) {
|
|
1215
|
+
setError(t("new.required"));
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
onClose();
|
|
1219
|
+
};
|
|
1220
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1221
|
+
className: board_module_css_default.modalBackdrop,
|
|
1222
|
+
onMouseDown: (event) => {
|
|
1223
|
+
if (event.target === event.currentTarget) onClose();
|
|
1224
|
+
},
|
|
1225
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
1226
|
+
className: board_module_css_default.modal,
|
|
1227
|
+
role: "dialog",
|
|
1228
|
+
"aria-label": t("board.new"),
|
|
1229
|
+
onSubmit: (event) => {
|
|
1230
|
+
event.preventDefault();
|
|
1231
|
+
submit();
|
|
1232
|
+
},
|
|
1233
|
+
children: [
|
|
1234
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1235
|
+
className: board_module_css_default.modalTitle,
|
|
1236
|
+
children: t("board.new")
|
|
1237
|
+
}),
|
|
1238
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1239
|
+
className: board_module_css_default.field,
|
|
1240
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1241
|
+
className: board_module_css_default.fieldLabel,
|
|
1242
|
+
children: t("new.title")
|
|
1243
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1244
|
+
className: board_module_css_default.input,
|
|
1245
|
+
value: title,
|
|
1246
|
+
autoFocus: true,
|
|
1247
|
+
placeholder: t("new.titlePlaceholder"),
|
|
1248
|
+
onChange: (event) => {
|
|
1249
|
+
setTitle(event.target.value);
|
|
1250
|
+
setError(void 0);
|
|
1251
|
+
}
|
|
1252
|
+
})]
|
|
1253
|
+
}),
|
|
1254
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1255
|
+
className: board_module_css_default.field,
|
|
1256
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1257
|
+
className: board_module_css_default.fieldLabel,
|
|
1258
|
+
children: t("new.description")
|
|
1259
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
1260
|
+
className: board_module_css_default.input,
|
|
1261
|
+
rows: 3,
|
|
1262
|
+
value: description,
|
|
1263
|
+
placeholder: t("new.descriptionPlaceholder"),
|
|
1264
|
+
onChange: (event) => {
|
|
1265
|
+
setDescription(event.target.value);
|
|
1266
|
+
}
|
|
1267
|
+
})]
|
|
1268
|
+
}),
|
|
1269
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1270
|
+
className: board_module_css_default.field,
|
|
1271
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1272
|
+
className: board_module_css_default.fieldLabel,
|
|
1273
|
+
children: t("new.prompt")
|
|
1274
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
1275
|
+
className: board_module_css_default.input,
|
|
1276
|
+
rows: 4,
|
|
1277
|
+
value: prompt,
|
|
1278
|
+
placeholder: t("new.promptPlaceholder"),
|
|
1279
|
+
onChange: (event) => {
|
|
1280
|
+
setPrompt(event.target.value);
|
|
1281
|
+
}
|
|
1282
|
+
})]
|
|
1283
|
+
}),
|
|
1284
|
+
error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1285
|
+
className: board_module_css_default.formError,
|
|
1286
|
+
children: error
|
|
1287
|
+
}),
|
|
1288
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
|
|
1289
|
+
className: board_module_css_default.modalFooter,
|
|
1290
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1291
|
+
type: "button",
|
|
1292
|
+
className: board_module_css_default.ghostButton,
|
|
1293
|
+
onClick: onClose,
|
|
1294
|
+
children: t("new.cancel")
|
|
1295
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1296
|
+
type: "submit",
|
|
1297
|
+
className: board_module_css_default.primaryButton,
|
|
1298
|
+
children: t("new.submit")
|
|
1299
|
+
})]
|
|
1300
|
+
})
|
|
1301
|
+
]
|
|
1302
|
+
})
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
//#endregion
|
|
1306
|
+
//#region src/client/board/TaskCard.tsx
|
|
1307
|
+
/** Compact relative/absolute time label. */
|
|
1308
|
+
function formatTime(ms) {
|
|
1309
|
+
const date = new Date(ms);
|
|
1310
|
+
const minutes = Math.floor((Date.now() - ms) / 6e4);
|
|
1311
|
+
if (minutes < 1) return t("time.justNow");
|
|
1312
|
+
if (minutes < 60) return `${minutes}m`;
|
|
1313
|
+
if (minutes < 1440) return `${Math.floor(minutes / 60)}h`;
|
|
1314
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
1315
|
+
}
|
|
1316
|
+
/** One card in a column. */
|
|
1317
|
+
function TaskCard({ task, onClick }) {
|
|
1318
|
+
const latest = task.executions[task.executions.length - 1];
|
|
1319
|
+
const runs = task.executions.length;
|
|
1320
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1321
|
+
type: "button",
|
|
1322
|
+
className: board_module_css_default.card,
|
|
1323
|
+
"data-status": task.status,
|
|
1324
|
+
onClick,
|
|
1325
|
+
title: task.description !== "" ? task.description : task.title,
|
|
1326
|
+
children: [
|
|
1327
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1328
|
+
className: board_module_css_default.cardTitle,
|
|
1329
|
+
children: task.title
|
|
1330
|
+
}),
|
|
1331
|
+
task.description !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1332
|
+
className: board_module_css_default.cardExcerpt,
|
|
1333
|
+
children: task.description
|
|
1334
|
+
}),
|
|
1335
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1336
|
+
className: board_module_css_default.cardMeta,
|
|
1337
|
+
children: [
|
|
1338
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1339
|
+
className: board_module_css_default.cardTime,
|
|
1340
|
+
children: [
|
|
1341
|
+
t("board.updated"),
|
|
1342
|
+
" ",
|
|
1343
|
+
formatTime(task.updatedAt)
|
|
1344
|
+
]
|
|
1345
|
+
}),
|
|
1346
|
+
task.schedule?.enabled === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1347
|
+
className: board_module_css_default.cardSchedule,
|
|
1348
|
+
title: task.schedule.nextRunAt !== void 0 ? `${t("card.scheduled")} · ${new Date(task.schedule.nextRunAt).toLocaleString()}` : t("card.scheduled"),
|
|
1349
|
+
children: t("card.scheduled")
|
|
1350
|
+
}),
|
|
1351
|
+
latest !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1352
|
+
className: board_module_css_default.cardRun,
|
|
1353
|
+
"data-result": latest.result,
|
|
1354
|
+
children: [
|
|
1355
|
+
runs,
|
|
1356
|
+
" ",
|
|
1357
|
+
t("board.runs")
|
|
1358
|
+
]
|
|
1359
|
+
}),
|
|
1360
|
+
latest?.sessionId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1361
|
+
className: board_module_css_default.cardSession,
|
|
1362
|
+
title: latest.sessionId,
|
|
1363
|
+
children: "⌁"
|
|
1364
|
+
}),
|
|
1365
|
+
task.status === "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1366
|
+
className: board_module_css_default.cardSpinner,
|
|
1367
|
+
"aria-hidden": "true"
|
|
1368
|
+
})
|
|
1369
|
+
]
|
|
1370
|
+
}),
|
|
1371
|
+
latest !== void 0 && executionLabel(latest) === "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1372
|
+
className: board_module_css_default.cardRunningLabel,
|
|
1373
|
+
children: [t("detail.result.running"), "…"]
|
|
1374
|
+
})
|
|
1375
|
+
]
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
//#endregion
|
|
1379
|
+
//#region src/client/board/ConfirmDialog.tsx
|
|
1380
|
+
/**
|
|
1381
|
+
* Generic confirm dialog used by destructive actions (task delete).
|
|
1382
|
+
*/
|
|
1383
|
+
/** Small confirm overlay. */
|
|
1384
|
+
function ConfirmDialog({ title, message, confirmLabel, danger, onCancel, onConfirm }) {
|
|
1385
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1386
|
+
className: board_module_css_default.modalBackdrop,
|
|
1387
|
+
onMouseDown: (event) => {
|
|
1388
|
+
if (event.target === event.currentTarget) onCancel();
|
|
1389
|
+
},
|
|
1390
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1391
|
+
className: board_module_css_default.modal,
|
|
1392
|
+
role: "alertdialog",
|
|
1393
|
+
"aria-label": title,
|
|
1394
|
+
children: [
|
|
1395
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1396
|
+
className: board_module_css_default.modalTitle,
|
|
1397
|
+
children: title
|
|
1398
|
+
}),
|
|
1399
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1400
|
+
className: board_module_css_default.confirmMessage,
|
|
1401
|
+
children: message
|
|
1402
|
+
}),
|
|
1403
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
|
|
1404
|
+
className: board_module_css_default.modalFooter,
|
|
1405
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1406
|
+
type: "button",
|
|
1407
|
+
className: board_module_css_default.ghostButton,
|
|
1408
|
+
onClick: onCancel,
|
|
1409
|
+
children: t("delete.cancel")
|
|
1410
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1411
|
+
type: "button",
|
|
1412
|
+
className: danger ? board_module_css_default.dangerButton : board_module_css_default.primaryButton,
|
|
1413
|
+
onClick: onConfirm,
|
|
1414
|
+
children: confirmLabel
|
|
1415
|
+
})]
|
|
1416
|
+
})
|
|
1417
|
+
]
|
|
1418
|
+
})
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
//#endregion
|
|
1422
|
+
//#region src/client/board/TaskDetail.tsx
|
|
1423
|
+
/**
|
|
1424
|
+
* Task detail: the full view of one task — content, prompt, execution
|
|
1425
|
+
* history — and the only place execution can be triggered. Also offers
|
|
1426
|
+
* delete (with confirmation), manual status moves, and a jump to the
|
|
1427
|
+
* execution's session transcript.
|
|
1428
|
+
*/
|
|
1429
|
+
/** Execution outcome → locale key. */
|
|
1430
|
+
const RESULT_KEY = {
|
|
1431
|
+
succeeded: "detail.result.succeeded",
|
|
1432
|
+
failed: "detail.result.failed",
|
|
1433
|
+
cancelled: "detail.result.cancelled"
|
|
1434
|
+
};
|
|
1435
|
+
/** Status → locale key (detail badge). */
|
|
1436
|
+
const STATUS_KEY$1 = {
|
|
1437
|
+
backlog: "board.status.backlog",
|
|
1438
|
+
todo: "board.status.todo",
|
|
1439
|
+
running: "board.status.running",
|
|
1440
|
+
done: "board.status.done",
|
|
1441
|
+
failed: "board.status.failed"
|
|
1442
|
+
};
|
|
1443
|
+
/** One execution-history row. */
|
|
1444
|
+
function ExecutionRow({ execution, onOpen }) {
|
|
1445
|
+
const result = execution.result;
|
|
1446
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1447
|
+
className: board_module_css_default.executionRow,
|
|
1448
|
+
"data-result": result,
|
|
1449
|
+
children: [
|
|
1450
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1451
|
+
className: board_module_css_default.executionBadge,
|
|
1452
|
+
"data-result": result,
|
|
1453
|
+
children: result === void 0 ? t("detail.result.running") : t(RESULT_KEY[result])
|
|
1454
|
+
}),
|
|
1455
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1456
|
+
className: board_module_css_default.executionTimes,
|
|
1457
|
+
children: [
|
|
1458
|
+
t("detail.executionStarted"),
|
|
1459
|
+
" ",
|
|
1460
|
+
formatTime(execution.startedAt),
|
|
1461
|
+
execution.endedAt !== void 0 && ` · ${t("detail.executionEnded")} ${formatTime(execution.endedAt)}`
|
|
1462
|
+
]
|
|
1463
|
+
}),
|
|
1464
|
+
execution.sessionId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1465
|
+
type: "button",
|
|
1466
|
+
className: board_module_css_default.linkButton,
|
|
1467
|
+
onClick: () => {
|
|
1468
|
+
onOpen(execution.sessionId);
|
|
1469
|
+
},
|
|
1470
|
+
title: execution.sessionId,
|
|
1471
|
+
children: [t("detail.viewSession"), " ⌁"]
|
|
1472
|
+
}),
|
|
1473
|
+
execution.error !== void 0 && execution.error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1474
|
+
className: board_module_css_default.executionError,
|
|
1475
|
+
children: execution.error
|
|
1476
|
+
})
|
|
1477
|
+
]
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
/** Common scheduled-run presets (cron → locale label). */
|
|
1481
|
+
const SCHEDULE_PRESETS = [
|
|
1482
|
+
{
|
|
1483
|
+
cron: "0 9 * * *",
|
|
1484
|
+
label: "detail.schedule.preset.daily9"
|
|
1485
|
+
},
|
|
1486
|
+
{
|
|
1487
|
+
cron: "0 * * * *",
|
|
1488
|
+
label: "detail.schedule.preset.hourly"
|
|
1489
|
+
},
|
|
1490
|
+
{
|
|
1491
|
+
cron: "*/10 * * * *",
|
|
1492
|
+
label: "detail.schedule.preset.tenMin"
|
|
1493
|
+
},
|
|
1494
|
+
{
|
|
1495
|
+
cron: "0 9 * * 1",
|
|
1496
|
+
label: "detail.schedule.preset.weeklyMon9"
|
|
1497
|
+
}
|
|
1498
|
+
];
|
|
1499
|
+
/** The scheduled-runs editor: enable toggle, cron input + presets, next-run info. */
|
|
1500
|
+
function ScheduleSection({ controller, task }) {
|
|
1501
|
+
const schedule = task.schedule;
|
|
1502
|
+
const [cron, setCron] = (0, react.useState)(schedule?.cron ?? "0 9 * * *");
|
|
1503
|
+
const [enabled, setEnabled] = (0, react.useState)(schedule?.enabled ?? false);
|
|
1504
|
+
const [nextRunAt, setNextRunAt] = (0, react.useState)(schedule?.nextRunAt);
|
|
1505
|
+
const [lastTriggeredAt, setLastTriggeredAt] = (0, react.useState)(schedule?.lastTriggeredAt);
|
|
1506
|
+
const [error, setError] = (0, react.useState)(void 0);
|
|
1507
|
+
(0, react.useEffect)(() => {
|
|
1508
|
+
setCron(schedule?.cron ?? "0 9 * * *");
|
|
1509
|
+
setEnabled(schedule?.enabled ?? false);
|
|
1510
|
+
setNextRunAt(schedule?.nextRunAt);
|
|
1511
|
+
setLastTriggeredAt(schedule?.lastTriggeredAt);
|
|
1512
|
+
setError(void 0);
|
|
1513
|
+
}, [
|
|
1514
|
+
task.id,
|
|
1515
|
+
schedule?.enabled,
|
|
1516
|
+
schedule?.cron,
|
|
1517
|
+
schedule?.nextRunAt,
|
|
1518
|
+
schedule?.lastTriggeredAt
|
|
1519
|
+
]);
|
|
1520
|
+
/** Validate + persist the current cron text (Enter or blur). */
|
|
1521
|
+
const saveCron = (value) => {
|
|
1522
|
+
const trimmed = value.trim();
|
|
1523
|
+
setCron(trimmed);
|
|
1524
|
+
if (trimmed === "" || !isValidCron(trimmed)) {
|
|
1525
|
+
setError(t("detail.schedule.invalid"));
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
setError(void 0);
|
|
1529
|
+
controller.setSchedule(task.id, { cron: trimmed });
|
|
1530
|
+
};
|
|
1531
|
+
/** Arm/disarm the schedule (arming first persists the edited cron). */
|
|
1532
|
+
const toggleEnabled = (next) => {
|
|
1533
|
+
const trimmed = cron.trim();
|
|
1534
|
+
if (next && (trimmed === "" || !isValidCron(trimmed))) {
|
|
1535
|
+
setError(t("detail.schedule.invalid"));
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
setError(void 0);
|
|
1539
|
+
if (next && trimmed !== schedule?.cron) controller.setSchedule(task.id, { cron: trimmed });
|
|
1540
|
+
if (controller.setSchedule(task.id, { enabled: next })) setEnabled(next);
|
|
1541
|
+
};
|
|
1542
|
+
const applyPreset = (preset) => {
|
|
1543
|
+
if (preset === "") return;
|
|
1544
|
+
setCron(preset);
|
|
1545
|
+
setError(void 0);
|
|
1546
|
+
controller.setSchedule(task.id, { cron: preset });
|
|
1547
|
+
};
|
|
1548
|
+
const nextLabel = !enabled || nextRunAt === void 0 ? t("detail.schedule.notScheduled") : nextRunAt <= Date.now() ? t("detail.schedule.dueSoon") : new Date(nextRunAt).toLocaleString();
|
|
1549
|
+
const lastLabel = lastTriggeredAt === void 0 ? "—" : new Date(lastTriggeredAt).toLocaleString();
|
|
1550
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1551
|
+
className: board_module_css_default.detailSection,
|
|
1552
|
+
children: [
|
|
1553
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.schedule") }),
|
|
1554
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1555
|
+
className: board_module_css_default.scheduleToggle,
|
|
1556
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1557
|
+
type: "checkbox",
|
|
1558
|
+
checked: enabled,
|
|
1559
|
+
onChange: (event) => {
|
|
1560
|
+
toggleEnabled(event.target.checked);
|
|
1561
|
+
}
|
|
1562
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("detail.schedule.enable") })]
|
|
1563
|
+
}),
|
|
1564
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1565
|
+
className: board_module_css_default.scheduleRow,
|
|
1566
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1567
|
+
className: `${board_module_css_default.input} ${board_module_css_default.scheduleInput}${error !== void 0 ? ` ${board_module_css_default.scheduleInputInvalid}` : ""}`,
|
|
1568
|
+
value: cron,
|
|
1569
|
+
placeholder: "0 9 * * *",
|
|
1570
|
+
spellCheck: false,
|
|
1571
|
+
"aria-label": t("detail.schedule.cron"),
|
|
1572
|
+
onChange: (event) => {
|
|
1573
|
+
setCron(event.target.value);
|
|
1574
|
+
setError(void 0);
|
|
1575
|
+
},
|
|
1576
|
+
onBlur: () => {
|
|
1577
|
+
saveCron(cron);
|
|
1578
|
+
},
|
|
1579
|
+
onKeyDown: (event) => {
|
|
1580
|
+
if (event.key === "Enter") saveCron(cron);
|
|
1581
|
+
}
|
|
1582
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1583
|
+
className: board_module_css_default.schedulePreset,
|
|
1584
|
+
value: "",
|
|
1585
|
+
"aria-label": t("detail.schedule.presets"),
|
|
1586
|
+
onChange: (event) => {
|
|
1587
|
+
applyPreset(event.target.value);
|
|
1588
|
+
},
|
|
1589
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
1590
|
+
value: "",
|
|
1591
|
+
children: [t("detail.schedule.presets"), "…"]
|
|
1592
|
+
}), SCHEDULE_PRESETS.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1593
|
+
value: preset.cron,
|
|
1594
|
+
children: t(preset.label)
|
|
1595
|
+
}, preset.cron))]
|
|
1596
|
+
})]
|
|
1597
|
+
}),
|
|
1598
|
+
error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1599
|
+
className: board_module_css_default.formError,
|
|
1600
|
+
children: error
|
|
1601
|
+
}),
|
|
1602
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1603
|
+
className: board_module_css_default.scheduleMeta,
|
|
1604
|
+
children: [
|
|
1605
|
+
t("detail.schedule.nextRun"),
|
|
1606
|
+
" ",
|
|
1607
|
+
nextLabel,
|
|
1608
|
+
" · ",
|
|
1609
|
+
t("detail.schedule.lastTriggered"),
|
|
1610
|
+
" ",
|
|
1611
|
+
lastLabel
|
|
1612
|
+
]
|
|
1613
|
+
})
|
|
1614
|
+
]
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
/** Task detail overlay. */
|
|
1618
|
+
function TaskDetail({ controller, task }) {
|
|
1619
|
+
const [confirmDelete, setConfirmDelete] = (0, react.useState)(false);
|
|
1620
|
+
const running = task.status === "running";
|
|
1621
|
+
const [latest, setLatest] = (0, react.useState)(task);
|
|
1622
|
+
(0, react.useEffect)(() => {
|
|
1623
|
+
setLatest(task);
|
|
1624
|
+
}, [task]);
|
|
1625
|
+
const current = latest;
|
|
1626
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1627
|
+
className: board_module_css_default.modalBackdrop,
|
|
1628
|
+
onMouseDown: (event) => {
|
|
1629
|
+
if (event.target === event.currentTarget) controller.closeTask();
|
|
1630
|
+
},
|
|
1631
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1632
|
+
className: board_module_css_default.detail,
|
|
1633
|
+
role: "dialog",
|
|
1634
|
+
"aria-label": t("detail.title"),
|
|
1635
|
+
children: [
|
|
1636
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
1637
|
+
className: board_module_css_default.detailHeader,
|
|
1638
|
+
children: [
|
|
1639
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1640
|
+
className: board_module_css_default.detailTitle,
|
|
1641
|
+
children: current.title
|
|
1642
|
+
}),
|
|
1643
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1644
|
+
className: board_module_css_default.statusBadge,
|
|
1645
|
+
"data-status": current.status,
|
|
1646
|
+
children: t(STATUS_KEY$1[current.status])
|
|
1647
|
+
}),
|
|
1648
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1649
|
+
type: "button",
|
|
1650
|
+
className: board_module_css_default.iconButton,
|
|
1651
|
+
"aria-label": t("detail.close"),
|
|
1652
|
+
onClick: () => {
|
|
1653
|
+
controller.closeTask();
|
|
1654
|
+
},
|
|
1655
|
+
children: "×"
|
|
1656
|
+
})
|
|
1657
|
+
]
|
|
1658
|
+
}),
|
|
1659
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1660
|
+
className: board_module_css_default.detailBody,
|
|
1661
|
+
children: [
|
|
1662
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1663
|
+
className: board_module_css_default.detailSection,
|
|
1664
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.description") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1665
|
+
className: board_module_css_default.detailText,
|
|
1666
|
+
children: current.description !== "" ? current.description : "—"
|
|
1667
|
+
})]
|
|
1668
|
+
}),
|
|
1669
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1670
|
+
className: board_module_css_default.detailSection,
|
|
1671
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.prompt") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
1672
|
+
className: board_module_css_default.promptBlock,
|
|
1673
|
+
children: current.prompt !== "" ? current.prompt : current.title
|
|
1674
|
+
})]
|
|
1675
|
+
}),
|
|
1676
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScheduleSection, {
|
|
1677
|
+
controller,
|
|
1678
|
+
task: current
|
|
1679
|
+
}),
|
|
1680
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1681
|
+
className: board_module_css_default.detailSection,
|
|
1682
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.execution") }), current.executions.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1683
|
+
className: board_module_css_default.detailText,
|
|
1684
|
+
children: t("detail.noExecution")
|
|
1685
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1686
|
+
className: board_module_css_default.executionList,
|
|
1687
|
+
children: [...current.executions].reverse().map((execution) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExecutionRow, {
|
|
1688
|
+
execution,
|
|
1689
|
+
onOpen: (sessionId) => {
|
|
1690
|
+
controller.openSession(sessionId);
|
|
1691
|
+
}
|
|
1692
|
+
}, execution.id))
|
|
1693
|
+
})]
|
|
1694
|
+
}),
|
|
1695
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1696
|
+
className: board_module_css_default.detailSection,
|
|
1697
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("board.status") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1698
|
+
className: board_module_css_default.moveRow,
|
|
1699
|
+
children: MANUAL_STATUSES.map((status) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1700
|
+
type: "button",
|
|
1701
|
+
className: board_module_css_default.ghostButton,
|
|
1702
|
+
disabled: current.status === status || running,
|
|
1703
|
+
onClick: () => {
|
|
1704
|
+
controller.moveTask(current.id, status);
|
|
1705
|
+
},
|
|
1706
|
+
children: t(`status.move.${status}`)
|
|
1707
|
+
}, status))
|
|
1708
|
+
})]
|
|
1709
|
+
})
|
|
1710
|
+
]
|
|
1711
|
+
}),
|
|
1712
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
|
|
1713
|
+
className: board_module_css_default.detailFooter,
|
|
1714
|
+
children: [
|
|
1715
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1716
|
+
type: "button",
|
|
1717
|
+
className: board_module_css_default.primaryButton,
|
|
1718
|
+
disabled: running,
|
|
1719
|
+
onClick: () => {
|
|
1720
|
+
controller.closeTask();
|
|
1721
|
+
controller.rerunTask(current.id);
|
|
1722
|
+
},
|
|
1723
|
+
children: current.executions.length === 0 ? t("detail.run") : t("detail.rerun")
|
|
1724
|
+
}),
|
|
1725
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1726
|
+
type: "button",
|
|
1727
|
+
className: board_module_css_default.dangerButton,
|
|
1728
|
+
onClick: () => {
|
|
1729
|
+
setConfirmDelete(true);
|
|
1730
|
+
},
|
|
1731
|
+
children: t("detail.delete")
|
|
1732
|
+
}),
|
|
1733
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1734
|
+
className: board_module_css_default.detailMeta,
|
|
1735
|
+
children: [
|
|
1736
|
+
t("board.created"),
|
|
1737
|
+
" ",
|
|
1738
|
+
formatTime(current.createdAt)
|
|
1739
|
+
]
|
|
1740
|
+
})
|
|
1741
|
+
]
|
|
1742
|
+
})
|
|
1743
|
+
]
|
|
1744
|
+
}), confirmDelete && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmDialog, {
|
|
1745
|
+
title: t("delete.title"),
|
|
1746
|
+
message: t("delete.confirm", { name: current.title }),
|
|
1747
|
+
confirmLabel: t("delete.ok"),
|
|
1748
|
+
danger: true,
|
|
1749
|
+
onCancel: () => {
|
|
1750
|
+
setConfirmDelete(false);
|
|
1751
|
+
},
|
|
1752
|
+
onConfirm: () => {
|
|
1753
|
+
setConfirmDelete(false);
|
|
1754
|
+
controller.deleteTask(current.id);
|
|
1755
|
+
controller.closeTask();
|
|
1756
|
+
}
|
|
1757
|
+
})]
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
//#endregion
|
|
1761
|
+
//#region src/client/board/TaskBoard.tsx
|
|
1762
|
+
/**
|
|
1763
|
+
* Board view: the multi-column kanban that replaces the middle column while
|
|
1764
|
+
* active. Cards open the task detail (never execute directly); the header
|
|
1765
|
+
* offers filter, new-task, and a back-to-chat escape.
|
|
1766
|
+
*/
|
|
1767
|
+
/** Column status → locale key. */
|
|
1768
|
+
const STATUS_KEY = {
|
|
1769
|
+
backlog: "board.status.backlog",
|
|
1770
|
+
todo: "board.status.todo",
|
|
1771
|
+
running: "board.status.running",
|
|
1772
|
+
done: "board.status.done",
|
|
1773
|
+
failed: "board.status.failed"
|
|
1774
|
+
};
|
|
1775
|
+
/** Case-insensitive title/description match. */
|
|
1776
|
+
function matchesFilter(task, filter) {
|
|
1777
|
+
if (filter.trim() === "") return true;
|
|
1778
|
+
const needle = filter.trim().toLowerCase();
|
|
1779
|
+
return task.title.toLowerCase().includes(needle) || task.description.toLowerCase().includes(needle);
|
|
1780
|
+
}
|
|
1781
|
+
/** Board component; subscribes to the controller snapshot. */
|
|
1782
|
+
function TaskBoard({ controller }) {
|
|
1783
|
+
const [snapshot, setSnapshot] = (0, react.useState)(controller.getSnapshot());
|
|
1784
|
+
(0, react.useEffect)(() => controller.subscribe(() => setSnapshot(controller.getSnapshot())), [controller]);
|
|
1785
|
+
const [filter, setFilter] = (0, react.useState)("");
|
|
1786
|
+
const [showNew, setShowNew] = (0, react.useState)(false);
|
|
1787
|
+
const selected = selectedTaskOf(snapshot);
|
|
1788
|
+
const visible = snapshot.tasks.filter((task) => matchesFilter(task, filter));
|
|
1789
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1790
|
+
className: board_module_css_default.board,
|
|
1791
|
+
"data-dsh-taskboard-board": "",
|
|
1792
|
+
children: [
|
|
1793
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
1794
|
+
className: board_module_css_default.boardHeader,
|
|
1795
|
+
children: [
|
|
1796
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1797
|
+
className: board_module_css_default.boardTitle,
|
|
1798
|
+
children: t("board.title")
|
|
1799
|
+
}),
|
|
1800
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1801
|
+
className: board_module_css_default.search,
|
|
1802
|
+
type: "search",
|
|
1803
|
+
placeholder: t("board.search"),
|
|
1804
|
+
value: filter,
|
|
1805
|
+
onChange: (event) => {
|
|
1806
|
+
setFilter(event.target.value);
|
|
1807
|
+
},
|
|
1808
|
+
"aria-label": t("board.search")
|
|
1809
|
+
}),
|
|
1810
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1811
|
+
type: "button",
|
|
1812
|
+
className: board_module_css_default.primaryButton,
|
|
1813
|
+
onClick: () => {
|
|
1814
|
+
setShowNew(true);
|
|
1815
|
+
},
|
|
1816
|
+
children: ["+ ", t("board.new")]
|
|
1817
|
+
}),
|
|
1818
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1819
|
+
type: "button",
|
|
1820
|
+
className: board_module_css_default.ghostButton,
|
|
1821
|
+
onClick: () => {
|
|
1822
|
+
controller.closeBoard();
|
|
1823
|
+
},
|
|
1824
|
+
children: t("board.close")
|
|
1825
|
+
})
|
|
1826
|
+
]
|
|
1827
|
+
}),
|
|
1828
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1829
|
+
className: board_module_css_default.columns,
|
|
1830
|
+
children: COLUMNS.map((column) => {
|
|
1831
|
+
const tasks = visible.filter((task) => task.status === column.status);
|
|
1832
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1833
|
+
className: board_module_css_default.column,
|
|
1834
|
+
"data-status": column.status,
|
|
1835
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
1836
|
+
className: board_module_css_default.columnHeader,
|
|
1837
|
+
children: [
|
|
1838
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1839
|
+
className: board_module_css_default.statusDot,
|
|
1840
|
+
"data-status": column.status,
|
|
1841
|
+
"aria-hidden": "true"
|
|
1842
|
+
}),
|
|
1843
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1844
|
+
className: board_module_css_default.columnTitle,
|
|
1845
|
+
children: t(STATUS_KEY[column.status])
|
|
1846
|
+
}),
|
|
1847
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1848
|
+
className: board_module_css_default.columnCount,
|
|
1849
|
+
children: tasks.length
|
|
1850
|
+
})
|
|
1851
|
+
]
|
|
1852
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1853
|
+
className: board_module_css_default.cards,
|
|
1854
|
+
children: [tasks.map((task) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskCard, {
|
|
1855
|
+
task,
|
|
1856
|
+
onClick: () => {
|
|
1857
|
+
controller.openTask(task.id);
|
|
1858
|
+
}
|
|
1859
|
+
}, task.id)), tasks.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1860
|
+
className: board_module_css_default.columnEmpty,
|
|
1861
|
+
children: t("board.empty")
|
|
1862
|
+
})]
|
|
1863
|
+
})]
|
|
1864
|
+
}, column.status);
|
|
1865
|
+
})
|
|
1866
|
+
}),
|
|
1867
|
+
selected !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskDetail, {
|
|
1868
|
+
controller,
|
|
1869
|
+
task: selected
|
|
1870
|
+
}),
|
|
1871
|
+
showNew && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NewTaskModal, {
|
|
1872
|
+
controller,
|
|
1873
|
+
onClose: () => {
|
|
1874
|
+
setShowNew(false);
|
|
1875
|
+
}
|
|
1876
|
+
})
|
|
1877
|
+
]
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
//#endregion
|
|
1881
|
+
//#region src/client/board-mount.tsx
|
|
1882
|
+
/**
|
|
1883
|
+
* Board view mounting.
|
|
1884
|
+
*
|
|
1885
|
+
* The `conversation` slot is single-occupant (ui-conversation) and external
|
|
1886
|
+
* plugins cannot declare slots, so the board takes over the center column at
|
|
1887
|
+
* the DOM level: a container is appended inside the `[data-pane="conversation"]`
|
|
1888
|
+
* grid item (an extra trailing child React never manages), and a stylesheet
|
|
1889
|
+
* rule hides the conversation content while the board is active. Toggling is
|
|
1890
|
+
* a data attribute on <html> — no React involvement, so the conversation
|
|
1891
|
+
* subtree underneath stays mounted and stateful.
|
|
1892
|
+
*/
|
|
1893
|
+
const CONVERSATION_COLUMN_SELECTOR = "[data-pane=\"conversation\"]";
|
|
1894
|
+
const ACTIVE_ATTR = "data-dsh-taskboard-active";
|
|
1895
|
+
/** Find the center column, or undefined while the frame is not mounted. */
|
|
1896
|
+
function conversationColumn() {
|
|
1897
|
+
return document.querySelector(CONVERSATION_COLUMN_SELECTOR) ?? void 0;
|
|
1898
|
+
}
|
|
1899
|
+
/**
|
|
1900
|
+
* Mount the board React tree into the center column and bind its visibility
|
|
1901
|
+
* to the controller's boardOpen state.
|
|
1902
|
+
* @param controller - the board controller driving the view.
|
|
1903
|
+
* @returns disposer unmounting the tree and restoring the column.
|
|
1904
|
+
*/
|
|
1905
|
+
function mountBoard(controller) {
|
|
1906
|
+
let root;
|
|
1907
|
+
let container;
|
|
1908
|
+
const ensure = () => {
|
|
1909
|
+
if (container !== void 0) return;
|
|
1910
|
+
const column = conversationColumn();
|
|
1911
|
+
if (column === void 0) return;
|
|
1912
|
+
container = document.createElement("div");
|
|
1913
|
+
container.dataset.dshTaskboardView = "";
|
|
1914
|
+
container.className = board_module_css_default.boardView;
|
|
1915
|
+
column.appendChild(container);
|
|
1916
|
+
root = (0, react_dom_client.createRoot)(container);
|
|
1917
|
+
root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskBoard, { controller }));
|
|
1918
|
+
};
|
|
1919
|
+
const waitObserver = new MutationObserver(() => {
|
|
1920
|
+
ensure();
|
|
1921
|
+
});
|
|
1922
|
+
waitObserver.observe(document.body, {
|
|
1923
|
+
childList: true,
|
|
1924
|
+
subtree: true
|
|
1925
|
+
});
|
|
1926
|
+
const applyActive = () => {
|
|
1927
|
+
if (controller.getSnapshot().boardOpen) document.documentElement.setAttribute(ACTIVE_ATTR, "");
|
|
1928
|
+
else document.documentElement.removeAttribute(ACTIVE_ATTR);
|
|
1929
|
+
};
|
|
1930
|
+
const unsubscribe = controller.subscribe(applyActive);
|
|
1931
|
+
applyActive();
|
|
1932
|
+
ensure();
|
|
1933
|
+
return () => {
|
|
1934
|
+
waitObserver.disconnect();
|
|
1935
|
+
unsubscribe();
|
|
1936
|
+
document.documentElement.removeAttribute(ACTIVE_ATTR);
|
|
1937
|
+
root?.unmount();
|
|
1938
|
+
root = void 0;
|
|
1939
|
+
container?.remove();
|
|
1940
|
+
container = void 0;
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
//#endregion
|
|
1944
|
+
//#region src/client/sidebar-entry.ts
|
|
1945
|
+
/** The sidebar column is the grid item AppFrame renders with this pane marker. */
|
|
1946
|
+
const SIDEBAR_COLUMN_SELECTOR = "[data-pane=\"sidebar\"]";
|
|
1947
|
+
/** Inline icon (matches the shell's 16px nav-icon look). */
|
|
1948
|
+
const ICON = `<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="2.5" width="12" height="11" rx="1.5"/><path d="M2 6.5h12M6.5 6.5v7"/></svg>`;
|
|
1949
|
+
/** Find the sidebar shell root element, or undefined while not yet mounted. */
|
|
1950
|
+
function sidebarRoot() {
|
|
1951
|
+
return document.querySelector(SIDEBAR_COLUMN_SELECTOR)?.firstElementChild ?? void 0;
|
|
1952
|
+
}
|
|
1953
|
+
/** The New Session button: the shell's only direct-child button of the root. */
|
|
1954
|
+
function newSessionButton(root) {
|
|
1955
|
+
for (const child of root.children) if (child.tagName === "BUTTON") return child;
|
|
1956
|
+
}
|
|
1957
|
+
/** Build the entry row (a detached button; insert once the shell is up). */
|
|
1958
|
+
function createEntry(controller) {
|
|
1959
|
+
const entry = document.createElement("button");
|
|
1960
|
+
entry.type = "button";
|
|
1961
|
+
entry.dataset.dshTaskboardEntry = "";
|
|
1962
|
+
entry.className = board_module_css_default.entry;
|
|
1963
|
+
entry.setAttribute("aria-label", t("entry.label"));
|
|
1964
|
+
entry.innerHTML = `<span class="${board_module_css_default.entryIcon}">${ICON}</span><span class="${board_module_css_default.entryLabel}">${t("entry.label")}</span>`;
|
|
1965
|
+
entry.addEventListener("click", () => {
|
|
1966
|
+
controller.toggleBoard();
|
|
1967
|
+
});
|
|
1968
|
+
return entry;
|
|
1969
|
+
}
|
|
1970
|
+
/** Re-insert the entry after the New Session button (before the browser region). */
|
|
1971
|
+
function placeEntry(root, entry) {
|
|
1972
|
+
const button = newSessionButton(root);
|
|
1973
|
+
if (button === void 0) return false;
|
|
1974
|
+
if (entry.parentElement !== root) root.insertBefore(entry, button.nextElementSibling);
|
|
1975
|
+
return true;
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* Mount the sidebar entry, waiting for the shell to render and self-healing
|
|
1979
|
+
* on later React re-renders.
|
|
1980
|
+
* @param controller - the board controller the entry toggles.
|
|
1981
|
+
* @returns disposer removing the entry and its observers.
|
|
1982
|
+
*/
|
|
1983
|
+
function mountSidebarEntry(controller) {
|
|
1984
|
+
const entry = createEntry(controller);
|
|
1985
|
+
let root;
|
|
1986
|
+
let placed = false;
|
|
1987
|
+
const tryPlace = () => {
|
|
1988
|
+
if (placed) return;
|
|
1989
|
+
root ??= sidebarRoot();
|
|
1990
|
+
if (root === void 0) return;
|
|
1991
|
+
placed = placeEntry(root, entry);
|
|
1992
|
+
if (placed) rootObserver.observe(root, {
|
|
1993
|
+
childList: true,
|
|
1994
|
+
subtree: true
|
|
1995
|
+
});
|
|
1996
|
+
};
|
|
1997
|
+
const waitObserver = new MutationObserver(() => {
|
|
1998
|
+
tryPlace();
|
|
1999
|
+
});
|
|
2000
|
+
waitObserver.observe(document.body, {
|
|
2001
|
+
childList: true,
|
|
2002
|
+
subtree: true
|
|
2003
|
+
});
|
|
2004
|
+
const rootObserver = new MutationObserver(() => {
|
|
2005
|
+
if (root === void 0 || !root.isConnected) {
|
|
2006
|
+
placed = false;
|
|
2007
|
+
tryPlace();
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
if (!root.contains(entry)) placed = placeEntry(root, entry);
|
|
2011
|
+
});
|
|
2012
|
+
const unsubscribe = controller.subscribe(() => {
|
|
2013
|
+
entry.dataset.active = controller.getSnapshot().boardOpen ? "true" : void 0;
|
|
2014
|
+
});
|
|
2015
|
+
entry.dataset.active = controller.getSnapshot().boardOpen ? "true" : void 0;
|
|
2016
|
+
tryPlace();
|
|
2017
|
+
return () => {
|
|
2018
|
+
waitObserver.disconnect();
|
|
2019
|
+
rootObserver.disconnect();
|
|
2020
|
+
unsubscribe();
|
|
2021
|
+
entry.remove();
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
//#endregion
|
|
2025
|
+
//#region \0dsh-css:/Users/zcl/code/dsh-web-ui/packages/dsh-task-board/src/client/settings-card.module.css.mjs
|
|
2026
|
+
const css = ".uT8yYq_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;list-style:none;transition:border-color .16s,background .16s;overflow:hidden}.uT8yYq_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.uT8yYq_header{cursor:pointer;text-align:left;width:100%;font:inherit;background:0 0;border:0;align-items:center;gap:8px;padding:10px 14px;display:flex}.uT8yYq_header:hover{background:var(--dsw-alias-interactive-bg-hover)}.uT8yYq_headText{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}.uT8yYq_name{color:var(--dsw-alias-label-primary);font-weight:600}.uT8yYq_description{color:var(--dsw-alias-label-tertiary);font-size:12px}.uT8yYq_pending{color:var(--dsw-alias-state-warn-primary);font-size:12px}.uT8yYq_chevron{color:var(--dsw-alias-label-tertiary);transition:transform .12s}.uT8yYq_chevronOpen{transform:rotate(180deg)}.uT8yYq_body{flex-direction:column;gap:14px;padding:0 14px 14px;display:flex}.uT8yYq_readOnly{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px}.uT8yYq_footer{justify-content:flex-end;align-items:center;gap:8px;display:flex}.uT8yYq_failed{color:var(--dsw-alias-state-error-primary);margin:0 auto 0 0;font-size:12px}.uT8yYq_discard,.uT8yYq_save{font:inherit;cursor:pointer;border-radius:6px;padding:5px 12px;font-size:13px}.uT8yYq_discard{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.uT8yYq_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.uT8yYq_save{border:1px solid var(--dsw-alias-button-info-fill);background:var(--dsw-alias-button-info-fill);color:var(--dsw-alias-label-primary-foreground)}.uT8yYq_save:hover:not(:disabled){border-color:var(--dsw-alias-button-info-hover);background:var(--dsw-alias-button-info-hover)}.uT8yYq_discard:active:not(:disabled),.uT8yYq_save:active:not(:disabled){transform:translateY(1px)}.uT8yYq_discard:focus-visible:not(:disabled),.uT8yYq_save:focus-visible:not(:disabled){outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}.uT8yYq_discard:disabled,.uT8yYq_save:disabled{opacity:.5;cursor:default}.uT8yYq_field{flex-direction:column;gap:4px;display:flex}.uT8yYq_head{align-items:center;gap:8px;display:flex}.uT8yYq_label{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500}.uT8yYq_badges{align-items:center;gap:6px;display:flex}.uT8yYq_badge{background:var(--dsw-alias-interactive-bg-hover-accent);color:var(--dsw-alias-state-business-primary);border-radius:999px;padding:1px 6px;font-size:11px}.uT8yYq_reset{color:var(--dsw-alias-state-business-primary);cursor:pointer;background:0 0;border:0;padding:0;font-size:11px}.uT8yYq_reset:hover:not(:disabled){text-decoration:underline}.uT8yYq_reset:active:not(:disabled){opacity:.8;text-decoration:underline}.uT8yYq_reset:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}.uT8yYq_reset:disabled{opacity:.5;cursor:default}.uT8yYq_input,.uT8yYq_select{border:1px solid var(--dsw-alias-border-l2);font:inherit;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border-radius:6px;padding:6px 8px;font-size:13px}.uT8yYq_inputInvalid{border:1px solid var(--dsw-alias-state-error-primary);font:inherit;color:var(--dsw-alias-label-primary);border-radius:6px;padding:6px 8px;font-size:13px}.uT8yYq_input:disabled,.uT8yYq_select:disabled{opacity:.6}.uT8yYq_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}.uT8yYq_invalid{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}.uT8yYq_header:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}.uT8yYq_header:active{background:var(--dsw-alias-interactive-bg-hover)}.uT8yYq_header,.uT8yYq_discard,.uT8yYq_save,.uT8yYq_reset{transition:background-color .12s,color .12s,border-color .12s,transform .12s}@media (prefers-reduced-motion:reduce){.uT8yYq_card,.uT8yYq_header,.uT8yYq_chevron,.uT8yYq_discard,.uT8yYq_save,.uT8yYq_reset{transition:none}}";
|
|
2027
|
+
const tagId = "@linxin666/dsh-client-ui-task-board/settings-card.module.css";
|
|
2028
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
2029
|
+
const tag = document.createElement("style");
|
|
2030
|
+
tag.dataset.plugin = "@linxin666/dsh-client-ui-task-board";
|
|
2031
|
+
tag.dataset.pluginCss = tagId;
|
|
2032
|
+
tag.textContent = css;
|
|
2033
|
+
document.head.appendChild(tag);
|
|
2034
|
+
}
|
|
2035
|
+
var settings_card_module_css_default = {
|
|
2036
|
+
"badge": "uT8yYq_badge",
|
|
2037
|
+
"badges": "uT8yYq_badges",
|
|
2038
|
+
"body": "uT8yYq_body",
|
|
2039
|
+
"card": "uT8yYq_card",
|
|
2040
|
+
"cardOpen": "uT8yYq_cardOpen",
|
|
2041
|
+
"chevron": "uT8yYq_chevron",
|
|
2042
|
+
"chevronOpen": "uT8yYq_chevronOpen",
|
|
2043
|
+
"description": "uT8yYq_description",
|
|
2044
|
+
"discard": "uT8yYq_discard",
|
|
2045
|
+
"failed": "uT8yYq_failed",
|
|
2046
|
+
"field": "uT8yYq_field",
|
|
2047
|
+
"footer": "uT8yYq_footer",
|
|
2048
|
+
"head": "uT8yYq_head",
|
|
2049
|
+
"headText": "uT8yYq_headText",
|
|
2050
|
+
"header": "uT8yYq_header",
|
|
2051
|
+
"hint": "uT8yYq_hint",
|
|
2052
|
+
"input": "uT8yYq_input",
|
|
2053
|
+
"inputInvalid": "uT8yYq_inputInvalid",
|
|
2054
|
+
"invalid": "uT8yYq_invalid",
|
|
2055
|
+
"label": "uT8yYq_label",
|
|
2056
|
+
"name": "uT8yYq_name",
|
|
2057
|
+
"pending": "uT8yYq_pending",
|
|
2058
|
+
"readOnly": "uT8yYq_readOnly",
|
|
2059
|
+
"reset": "uT8yYq_reset",
|
|
2060
|
+
"save": "uT8yYq_save",
|
|
2061
|
+
"select": "uT8yYq_select"
|
|
2062
|
+
};
|
|
2063
|
+
//#endregion
|
|
2064
|
+
//#region src/client/PluginSettingsCard.tsx
|
|
2065
|
+
/**
|
|
2066
|
+
* Shared chrome for the plugin settings card: a disclosure header naming the
|
|
2067
|
+
* plugin and what its settings govern, the controls inside, and the save that
|
|
2068
|
+
* writes them. Renders nothing while the namespace is unavailable — a
|
|
2069
|
+
* deployment that does not compose the owning plugin should show no trace of
|
|
2070
|
+
* it. Mirrors the official ui-plugin-config PluginCard in a self-contained
|
|
2071
|
+
* slice (this package must not depend on a sibling UI package).
|
|
2072
|
+
*/
|
|
2073
|
+
/**
|
|
2074
|
+
* Render one plugin settings card.
|
|
2075
|
+
* @param props - the plugin's copy keys, its form state, and its controls.
|
|
2076
|
+
* @returns the card, or nothing when the namespace is unavailable.
|
|
2077
|
+
*/
|
|
2078
|
+
function PluginSettingsCard(props) {
|
|
2079
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
2080
|
+
const { state } = props;
|
|
2081
|
+
if (!state.available) return null;
|
|
2082
|
+
const title = props.t(props.titleKey);
|
|
2083
|
+
const blocked = !state.dirty || state.invalid || state.saving;
|
|
2084
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
2085
|
+
className: settings_card_module_css_default.card,
|
|
2086
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2087
|
+
type: "button",
|
|
2088
|
+
className: settings_card_module_css_default.header,
|
|
2089
|
+
"aria-expanded": open,
|
|
2090
|
+
"aria-label": `${props.t(open ? "settings.collapse" : "settings.expand")}: ${title}`,
|
|
2091
|
+
onClick: () => {
|
|
2092
|
+
setOpen(!open);
|
|
2093
|
+
},
|
|
2094
|
+
children: [
|
|
2095
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2096
|
+
className: settings_card_module_css_default.headText,
|
|
2097
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2098
|
+
className: settings_card_module_css_default.name,
|
|
2099
|
+
children: title
|
|
2100
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2101
|
+
className: settings_card_module_css_default.description,
|
|
2102
|
+
children: props.t(props.descriptionKey)
|
|
2103
|
+
})]
|
|
2104
|
+
}),
|
|
2105
|
+
state.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2106
|
+
className: settings_card_module_css_default.pending,
|
|
2107
|
+
children: props.t("settings.unsaved")
|
|
2108
|
+
}) : null,
|
|
2109
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2110
|
+
className: open ? settings_card_module_css_default.chevronOpen : settings_card_module_css_default.chevron,
|
|
2111
|
+
children: "▾"
|
|
2112
|
+
})
|
|
2113
|
+
]
|
|
2114
|
+
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2115
|
+
className: settings_card_module_css_default.body,
|
|
2116
|
+
children: [
|
|
2117
|
+
!state.writable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2118
|
+
className: settings_card_module_css_default.readOnly,
|
|
2119
|
+
role: "status",
|
|
2120
|
+
children: props.t("settings.readOnly")
|
|
2121
|
+
}) : null,
|
|
2122
|
+
props.children,
|
|
2123
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2124
|
+
className: settings_card_module_css_default.footer,
|
|
2125
|
+
children: [
|
|
2126
|
+
state.failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2127
|
+
className: settings_card_module_css_default.failed,
|
|
2128
|
+
role: "status",
|
|
2129
|
+
children: props.t("settings.saveFailed")
|
|
2130
|
+
}) : null,
|
|
2131
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2132
|
+
type: "button",
|
|
2133
|
+
className: settings_card_module_css_default.discard,
|
|
2134
|
+
disabled: !state.dirty || state.saving,
|
|
2135
|
+
onClick: props.onDiscard,
|
|
2136
|
+
children: props.t("settings.discard")
|
|
2137
|
+
}),
|
|
2138
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2139
|
+
type: "button",
|
|
2140
|
+
className: settings_card_module_css_default.save,
|
|
2141
|
+
disabled: blocked,
|
|
2142
|
+
onClick: props.onSave,
|
|
2143
|
+
children: props.t(!state.saving ? "settings.save" : "settings.saving")
|
|
2144
|
+
})
|
|
2145
|
+
]
|
|
2146
|
+
})
|
|
2147
|
+
]
|
|
2148
|
+
}) : null]
|
|
2149
|
+
});
|
|
2150
|
+
}
|
|
2151
|
+
/** A staged boolean field: 继承 / 开 / 关. */
|
|
2152
|
+
function BooleanField(props) {
|
|
2153
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2154
|
+
className: settings_card_module_css_default.field,
|
|
2155
|
+
children: [
|
|
2156
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2157
|
+
className: settings_card_module_css_default.head,
|
|
2158
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
2159
|
+
className: settings_card_module_css_default.label,
|
|
2160
|
+
htmlFor: props.id,
|
|
2161
|
+
children: props.label
|
|
2162
|
+
}), props.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2163
|
+
className: settings_card_module_css_default.badges,
|
|
2164
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2165
|
+
className: settings_card_module_css_default.badge,
|
|
2166
|
+
children: props.overriddenLabel
|
|
2167
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2168
|
+
type: "button",
|
|
2169
|
+
className: settings_card_module_css_default.reset,
|
|
2170
|
+
disabled: props.disabled,
|
|
2171
|
+
onClick: props.onReset,
|
|
2172
|
+
children: props.resetLabel
|
|
2173
|
+
})]
|
|
2174
|
+
}) : null]
|
|
2175
|
+
}),
|
|
2176
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
2177
|
+
id: props.id,
|
|
2178
|
+
className: settings_card_module_css_default.select,
|
|
2179
|
+
value: props.text,
|
|
2180
|
+
disabled: props.disabled,
|
|
2181
|
+
onChange: (event) => {
|
|
2182
|
+
props.onEdit(event.target.value);
|
|
2183
|
+
},
|
|
2184
|
+
children: [
|
|
2185
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2186
|
+
value: "",
|
|
2187
|
+
children: props.inheritLabel
|
|
2188
|
+
}),
|
|
2189
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2190
|
+
value: "true",
|
|
2191
|
+
children: props.onLabel
|
|
2192
|
+
}),
|
|
2193
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2194
|
+
value: "false",
|
|
2195
|
+
children: props.offLabel
|
|
2196
|
+
})
|
|
2197
|
+
]
|
|
2198
|
+
}),
|
|
2199
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2200
|
+
className: settings_card_module_css_default.hint,
|
|
2201
|
+
children: props.hint
|
|
2202
|
+
})
|
|
2203
|
+
]
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
//#endregion
|
|
2207
|
+
//#region src/client/settings-form.ts
|
|
2208
|
+
/** A boolean field, edited through true/false draft text. */
|
|
2209
|
+
function booleanField(field) {
|
|
2210
|
+
return {
|
|
2211
|
+
field,
|
|
2212
|
+
format: (value) => typeof value === "boolean" ? String(value) : "",
|
|
2213
|
+
parse: (text) => {
|
|
2214
|
+
if (text === "true") return {
|
|
2215
|
+
kind: "set",
|
|
2216
|
+
value: true
|
|
2217
|
+
};
|
|
2218
|
+
if (text === "false") return {
|
|
2219
|
+
kind: "set",
|
|
2220
|
+
value: false
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
2225
|
+
/**
|
|
2226
|
+
* Stages one card's edits over one settings namespace and writes them on save.
|
|
2227
|
+
*
|
|
2228
|
+
* The Host is the only authority on whether a value was accepted — its
|
|
2229
|
+
* validators own the constraints no schema can express — so the outcome is
|
|
2230
|
+
* read back from the section rather than predicted here. A save that did not
|
|
2231
|
+
* land keeps its drafts, so the user can correct them instead of retyping.
|
|
2232
|
+
*/
|
|
2233
|
+
var CardForm = class {
|
|
2234
|
+
scope;
|
|
2235
|
+
specs;
|
|
2236
|
+
staged = /* @__PURE__ */ new Map();
|
|
2237
|
+
listeners = /* @__PURE__ */ new Set();
|
|
2238
|
+
saving = false;
|
|
2239
|
+
failed = false;
|
|
2240
|
+
/** @param scope - the bound settings scope for this card's namespace. */
|
|
2241
|
+
constructor(scope, specs) {
|
|
2242
|
+
this.scope = scope;
|
|
2243
|
+
this.specs = new Map(specs.map((spec) => [spec.field, spec]));
|
|
2244
|
+
scope.subscribe(() => {
|
|
2245
|
+
this.publish();
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
/** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
|
|
2249
|
+
bind(project) {
|
|
2250
|
+
const store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(project());
|
|
2251
|
+
this.listeners.add(() => {
|
|
2252
|
+
store.set(project());
|
|
2253
|
+
});
|
|
2254
|
+
return store;
|
|
2255
|
+
}
|
|
2256
|
+
/** Read the card-level state: what the Host serves, and what a save would do. */
|
|
2257
|
+
shell() {
|
|
2258
|
+
const snapshot = this.scope.getSnapshot();
|
|
2259
|
+
const plan = this.plan();
|
|
2260
|
+
return {
|
|
2261
|
+
available: snapshot.status === "ready",
|
|
2262
|
+
writable: snapshot.writable,
|
|
2263
|
+
dirty: plan.length > 0,
|
|
2264
|
+
invalid: plan.some((item) => item.run === void 0),
|
|
2265
|
+
saving: this.saving,
|
|
2266
|
+
failed: this.failed
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
/** Read one field's state from the effective section and its staged draft. */
|
|
2270
|
+
field(field) {
|
|
2271
|
+
const spec = this.specOf(field);
|
|
2272
|
+
const staged = this.staged.get(field);
|
|
2273
|
+
if (staged === void 0) return {
|
|
2274
|
+
text: spec.format(this.sectionValue(field)),
|
|
2275
|
+
overridden: this.stored(field),
|
|
2276
|
+
invalid: false
|
|
2277
|
+
};
|
|
2278
|
+
const write = staged.clear ? { kind: "clear" } : spec.parse(staged.text);
|
|
2279
|
+
return {
|
|
2280
|
+
text: staged.text,
|
|
2281
|
+
overridden: write?.kind === "set",
|
|
2282
|
+
invalid: write === void 0
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
/** The actions the card's slot registration injects. */
|
|
2286
|
+
actions() {
|
|
2287
|
+
return {
|
|
2288
|
+
edit: (field, text) => {
|
|
2289
|
+
this.stage(field, {
|
|
2290
|
+
text,
|
|
2291
|
+
clear: false
|
|
2292
|
+
});
|
|
2293
|
+
},
|
|
2294
|
+
resetField: (field) => {
|
|
2295
|
+
this.stage(field, {
|
|
2296
|
+
text: this.specOf(field).format(this.baseValue(field)),
|
|
2297
|
+
clear: true
|
|
2298
|
+
});
|
|
2299
|
+
},
|
|
2300
|
+
save: () => {
|
|
2301
|
+
this.save();
|
|
2302
|
+
},
|
|
2303
|
+
discard: () => {
|
|
2304
|
+
if (this.staged.size === 0 && !this.failed) return;
|
|
2305
|
+
this.staged.clear();
|
|
2306
|
+
this.failed = false;
|
|
2307
|
+
this.publish();
|
|
2308
|
+
}
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
/**
|
|
2312
|
+
* Write every staged edit, then re-seed from what the Host accepted.
|
|
2313
|
+
* @returns settlement after every write and the read-back.
|
|
2314
|
+
*/
|
|
2315
|
+
async save() {
|
|
2316
|
+
const plan = this.plan();
|
|
2317
|
+
const writes = plan.flatMap((item) => item.run === void 0 ? [] : [item.run]);
|
|
2318
|
+
if (plan.length === 0 || this.saving || writes.length !== plan.length) return;
|
|
2319
|
+
this.saving = true;
|
|
2320
|
+
this.failed = false;
|
|
2321
|
+
this.publish();
|
|
2322
|
+
let landed = true;
|
|
2323
|
+
for (const write of writes) landed = await write() && landed;
|
|
2324
|
+
if (landed) this.staged.clear();
|
|
2325
|
+
this.saving = false;
|
|
2326
|
+
this.failed = !landed;
|
|
2327
|
+
this.publish();
|
|
2328
|
+
}
|
|
2329
|
+
/**
|
|
2330
|
+
* Every staged edit a save would write. An entry whose draft is not a value
|
|
2331
|
+
* its field accepts carries no write: the form is still dirty, and the save
|
|
2332
|
+
* refuses rather than dropping the edit. A staged edit that matches the
|
|
2333
|
+
* effective section is not a write at all.
|
|
2334
|
+
* @returns the planned writes, in the order the fields were staged.
|
|
2335
|
+
*/
|
|
2336
|
+
plan() {
|
|
2337
|
+
const plan = [];
|
|
2338
|
+
for (const [field, staged] of this.staged) {
|
|
2339
|
+
const spec = this.specOf(field);
|
|
2340
|
+
if (staged.clear) {
|
|
2341
|
+
if (this.stored(field)) plan.push({
|
|
2342
|
+
field,
|
|
2343
|
+
run: () => this.clear(field)
|
|
2344
|
+
});
|
|
2345
|
+
continue;
|
|
2346
|
+
}
|
|
2347
|
+
if (staged.text === spec.format(this.sectionValue(field))) continue;
|
|
2348
|
+
const write = spec.parse(staged.text);
|
|
2349
|
+
if (write === void 0) plan.push({
|
|
2350
|
+
field,
|
|
2351
|
+
run: void 0
|
|
2352
|
+
});
|
|
2353
|
+
else if (write.kind === "clear") plan.push({
|
|
2354
|
+
field,
|
|
2355
|
+
run: () => this.clear(field)
|
|
2356
|
+
});
|
|
2357
|
+
else plan.push({
|
|
2358
|
+
field,
|
|
2359
|
+
run: () => this.store(field, write.value)
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
return plan;
|
|
2363
|
+
}
|
|
2364
|
+
async clear(field) {
|
|
2365
|
+
await this.scope.unset(field);
|
|
2366
|
+
return !this.stored(field);
|
|
2367
|
+
}
|
|
2368
|
+
async store(field, value) {
|
|
2369
|
+
await this.scope.set(field, value);
|
|
2370
|
+
return this.userLayer()?.[field] === value;
|
|
2371
|
+
}
|
|
2372
|
+
stage(field, edit) {
|
|
2373
|
+
this.staged.set(field, edit);
|
|
2374
|
+
this.failed = false;
|
|
2375
|
+
this.publish();
|
|
2376
|
+
}
|
|
2377
|
+
specOf(field) {
|
|
2378
|
+
const spec = this.specs.get(field);
|
|
2379
|
+
if (spec === void 0) throw new Error(`settings card has no field ${field}`);
|
|
2380
|
+
return spec;
|
|
2381
|
+
}
|
|
2382
|
+
snapshotOf() {
|
|
2383
|
+
return this.scope.getSnapshot();
|
|
2384
|
+
}
|
|
2385
|
+
sectionValue(field) {
|
|
2386
|
+
return this.snapshotOf().value?.[field];
|
|
2387
|
+
}
|
|
2388
|
+
baseValue(field) {
|
|
2389
|
+
return this.snapshotOf().base?.[field];
|
|
2390
|
+
}
|
|
2391
|
+
userLayer() {
|
|
2392
|
+
return this.snapshotOf().user;
|
|
2393
|
+
}
|
|
2394
|
+
stored(field) {
|
|
2395
|
+
const user = this.userLayer();
|
|
2396
|
+
return user !== void 0 && Object.hasOwn(user, field);
|
|
2397
|
+
}
|
|
2398
|
+
publish() {
|
|
2399
|
+
for (const listener of this.listeners) listener();
|
|
2400
|
+
}
|
|
2401
|
+
};
|
|
2402
|
+
//#endregion
|
|
2403
|
+
//#region src/client/TaskBoardSettingsCard.tsx
|
|
2404
|
+
/** Bridges the `task-board` scope onto the card's staged form. */
|
|
2405
|
+
var TaskBoardSettingsCardController = class {
|
|
2406
|
+
form;
|
|
2407
|
+
store;
|
|
2408
|
+
/** @param scope - the bound settings scope for the `task-board` namespace. */
|
|
2409
|
+
constructor(scope) {
|
|
2410
|
+
this.form = new CardForm(scope, [booleanField("enabled"), booleanField("announceToAgent")]);
|
|
2411
|
+
this.store = this.form.bind(() => this.projection());
|
|
2412
|
+
}
|
|
2413
|
+
projection() {
|
|
2414
|
+
return {
|
|
2415
|
+
...this.form.shell(),
|
|
2416
|
+
enabled: this.form.field("enabled"),
|
|
2417
|
+
announceToAgent: this.form.field("announceToAgent")
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
/**
|
|
2421
|
+
* Build the face the card's slot registration injects.
|
|
2422
|
+
* @returns the card's snapshot and its form actions.
|
|
2423
|
+
*/
|
|
2424
|
+
inject() {
|
|
2425
|
+
return {
|
|
2426
|
+
hooks: { taskBoardSettingsCard: this.store },
|
|
2427
|
+
...this.form.actions()
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
};
|
|
2431
|
+
/**
|
|
2432
|
+
* Render the task-board card.
|
|
2433
|
+
* @param props - locale copy, the card snapshot, and its form actions.
|
|
2434
|
+
* @returns the card.
|
|
2435
|
+
*/
|
|
2436
|
+
function TaskBoardSettingsCard(props) {
|
|
2437
|
+
const { t } = props;
|
|
2438
|
+
const state = props.useTaskBoardSettingsCard((snapshot) => snapshot);
|
|
2439
|
+
const disabled = !state.writable;
|
|
2440
|
+
const fieldProps = {
|
|
2441
|
+
overriddenLabel: t("settings.overridden"),
|
|
2442
|
+
resetLabel: t("settings.reset"),
|
|
2443
|
+
invalidLabel: t("settings.invalidNumber"),
|
|
2444
|
+
disabled
|
|
2445
|
+
};
|
|
2446
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(PluginSettingsCard, {
|
|
2447
|
+
t,
|
|
2448
|
+
titleKey: "settings.title",
|
|
2449
|
+
descriptionKey: "settings.description",
|
|
2450
|
+
state,
|
|
2451
|
+
onSave: props.save,
|
|
2452
|
+
onDiscard: props.discard,
|
|
2453
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
|
|
2454
|
+
id: "settings-task-board-enabled",
|
|
2455
|
+
label: t("settings.enabled"),
|
|
2456
|
+
hint: t("settings.enabledHint"),
|
|
2457
|
+
inheritLabel: t("settings.inherit"),
|
|
2458
|
+
onLabel: t("settings.on"),
|
|
2459
|
+
offLabel: t("settings.off"),
|
|
2460
|
+
...fieldProps,
|
|
2461
|
+
...state.enabled,
|
|
2462
|
+
onEdit: (text) => {
|
|
2463
|
+
props.edit("enabled", text);
|
|
2464
|
+
},
|
|
2465
|
+
onReset: () => {
|
|
2466
|
+
props.resetField("enabled");
|
|
2467
|
+
}
|
|
2468
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
|
|
2469
|
+
id: "settings-task-board-announce",
|
|
2470
|
+
label: t("settings.announceToAgent"),
|
|
2471
|
+
hint: t("settings.announceToAgentHint"),
|
|
2472
|
+
inheritLabel: t("settings.inherit"),
|
|
2473
|
+
onLabel: t("settings.on"),
|
|
2474
|
+
offLabel: t("settings.off"),
|
|
2475
|
+
...fieldProps,
|
|
2476
|
+
...state.announceToAgent,
|
|
2477
|
+
onEdit: (text) => {
|
|
2478
|
+
props.edit("announceToAgent", text);
|
|
2479
|
+
},
|
|
2480
|
+
onReset: () => {
|
|
2481
|
+
props.resetField("announceToAgent");
|
|
2482
|
+
}
|
|
2483
|
+
})]
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
//#endregion
|
|
2487
|
+
//#region src/client/index.ts
|
|
2488
|
+
/** Locale namespace this plugin owns. */
|
|
2489
|
+
const NS = "task-board";
|
|
2490
|
+
/** Settings namespace the settings card edits (the Host plugin registers it). */
|
|
2491
|
+
const TASK_BOARD_NS = "task-board";
|
|
2492
|
+
/** Required services (fiber inject waiting — the runtime must be up first). */
|
|
2493
|
+
const inject = [
|
|
2494
|
+
"slots",
|
|
2495
|
+
"sessions",
|
|
2496
|
+
"workspaces",
|
|
2497
|
+
"connection",
|
|
2498
|
+
"settingsScope",
|
|
2499
|
+
"locale",
|
|
2500
|
+
"remote"
|
|
2501
|
+
];
|
|
2502
|
+
/**
|
|
2503
|
+
* Mount the task board.
|
|
2504
|
+
* @param ctx - client root context (services: sessions, workspaces).
|
|
2505
|
+
*/
|
|
2506
|
+
function apply(ctx) {
|
|
2507
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
2508
|
+
zh,
|
|
2509
|
+
en
|
|
2510
|
+
}), "task-board: dictionaries");
|
|
2511
|
+
const settingsScope = ctx.settingsScope.bind({ namespace: TASK_BOARD_NS });
|
|
2512
|
+
const settingsCard = new TaskBoardSettingsCardController(settingsScope);
|
|
2513
|
+
ctx.slots.inject("web-ui.plugin.item", () => ctx.slots.register({
|
|
2514
|
+
name: "web-ui.plugin.item",
|
|
2515
|
+
id: "task-board",
|
|
2516
|
+
order: 110,
|
|
2517
|
+
locale: NS,
|
|
2518
|
+
inject: () => settingsCard.inject()
|
|
2519
|
+
}, TaskBoardSettingsCard));
|
|
2520
|
+
let uiDisposer;
|
|
2521
|
+
const mountUi = () => {
|
|
2522
|
+
if (uiDisposer !== void 0) return;
|
|
2523
|
+
const sessions = ctx.sessions;
|
|
2524
|
+
const workspaces = ctx.workspaces;
|
|
2525
|
+
const connection = ctx.get("connection");
|
|
2526
|
+
const controller = new BoardController({
|
|
2527
|
+
store: new LocalStorageTaskStore(),
|
|
2528
|
+
exec: new ExecutionService({
|
|
2529
|
+
sessions: {
|
|
2530
|
+
list: sessions.list,
|
|
2531
|
+
binding: (id) => sessions.binding(id)
|
|
2532
|
+
},
|
|
2533
|
+
workspaces: {
|
|
2534
|
+
list: workspaces.list,
|
|
2535
|
+
connectWorkspace: (id) => workspaces.connectWorkspace(id)
|
|
2536
|
+
},
|
|
2537
|
+
history: { loadTail: async (sessionId) => {
|
|
2538
|
+
const response = await connection.api.sessions.history({
|
|
2539
|
+
sessionId,
|
|
2540
|
+
maxMessages: 20
|
|
2541
|
+
});
|
|
2542
|
+
return response.result.ok ? { events: response.result.value.events.map((entry) => entry.event) } : void 0;
|
|
2543
|
+
} }
|
|
2544
|
+
}),
|
|
2545
|
+
sessions: {
|
|
2546
|
+
list: sessions.list,
|
|
2547
|
+
open: (id) => sessions.open(id)
|
|
2548
|
+
}
|
|
2549
|
+
});
|
|
2550
|
+
controller.start();
|
|
2551
|
+
const scheduler = new SchedulerService({
|
|
2552
|
+
tasks: () => controller.getSnapshot().tasks,
|
|
2553
|
+
now: () => Date.now(),
|
|
2554
|
+
runTask: (id) => controller.runTask(id),
|
|
2555
|
+
applySchedule: (id, nextRunAt, lastTriggeredAt) => controller.applyScheduleNextRun(id, nextRunAt, lastTriggeredAt),
|
|
2556
|
+
ready: () => sessions.list.getSnapshot().phase === "ready",
|
|
2557
|
+
environment: {
|
|
2558
|
+
addEventListener: (type, listener) => document.addEventListener(type, listener),
|
|
2559
|
+
removeEventListener: (type, listener) => document.removeEventListener(type, listener)
|
|
2560
|
+
}
|
|
2561
|
+
});
|
|
2562
|
+
scheduler.start();
|
|
2563
|
+
const disposers = [];
|
|
2564
|
+
try {
|
|
2565
|
+
disposers.push(mountSidebarEntry(controller));
|
|
2566
|
+
disposers.push(mountBoard(controller));
|
|
2567
|
+
} catch (error) {
|
|
2568
|
+
console.error("[dsh-task-board] mount failed:", error);
|
|
2569
|
+
}
|
|
2570
|
+
uiDisposer = () => {
|
|
2571
|
+
for (const dispose of disposers.splice(0)) dispose();
|
|
2572
|
+
scheduler.dispose();
|
|
2573
|
+
controller.dispose();
|
|
2574
|
+
uiDisposer = void 0;
|
|
2575
|
+
};
|
|
2576
|
+
};
|
|
2577
|
+
const syncEnabled = () => {
|
|
2578
|
+
const snapshot = settingsScope.getSnapshot();
|
|
2579
|
+
if (snapshot.status === "ready" ? snapshot.value?.enabled ?? true : snapshot.status === "unavailable") mountUi();
|
|
2580
|
+
else uiDisposer?.();
|
|
2581
|
+
};
|
|
2582
|
+
settingsScope.subscribe(syncEnabled);
|
|
2583
|
+
syncEnabled();
|
|
2584
|
+
}
|
|
2585
|
+
//#endregion
|
|
2586
|
+
exports.apply = apply;
|
|
2587
|
+
exports.inject = inject;
|
|
2588
|
+
return module.exports;
|
|
2589
|
+
}
|
|
2590
|
+
});
|
|
2591
|
+
|
|
2592
|
+
//# sourceMappingURL=client.js.map
|