@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.
Files changed (50) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +211 -0
  3. package/cordis.patch.yml +12 -0
  4. package/lib/client.js +2592 -0
  5. package/lib/index.js +54 -0
  6. package/lib/invariant.js +6 -0
  7. package/lib/types/client/PluginSettingsCard.d.ts +77 -0
  8. package/lib/types/client/TaskBoardSettingsCard.d.ts +51 -0
  9. package/lib/types/client/board/ConfirmDialog.d.ts +12 -0
  10. package/lib/types/client/board/NewTaskModal.d.ts +6 -0
  11. package/lib/types/client/board/TaskBoard.d.ts +5 -0
  12. package/lib/types/client/board/TaskCard.d.ts +12 -0
  13. package/lib/types/client/board/TaskDetail.d.ts +7 -0
  14. package/lib/types/client/board-mount.d.ts +10 -0
  15. package/lib/types/client/index.d.ts +43 -0
  16. package/lib/types/client/locales.d.ts +105 -0
  17. package/lib/types/client/settings-form.d.ts +118 -0
  18. package/lib/types/client/sidebar-entry.d.ts +25 -0
  19. package/lib/types/core/controller.d.ts +129 -0
  20. package/lib/types/core/execution.d.ts +136 -0
  21. package/lib/types/core/schedule.d.ts +40 -0
  22. package/lib/types/core/scheduler.d.ts +49 -0
  23. package/lib/types/core/store.d.ts +36 -0
  24. package/lib/types/core/tasks.d.ts +111 -0
  25. package/lib/types/index.d.ts +44 -0
  26. package/lib/types/invariant.d.ts +3 -0
  27. package/package.json +85 -0
  28. package/src/client/PluginSettingsCard.tsx +207 -0
  29. package/src/client/TaskBoardSettingsCard.tsx +124 -0
  30. package/src/client/board/ConfirmDialog.tsx +40 -0
  31. package/src/client/board/NewTaskModal.tsx +82 -0
  32. package/src/client/board/TaskBoard.tsx +107 -0
  33. package/src/client/board/TaskCard.tsx +62 -0
  34. package/src/client/board/TaskDetail.tsx +286 -0
  35. package/src/client/board-mount.tsx +74 -0
  36. package/src/client/board.module.css +849 -0
  37. package/src/client/css-modules.d.ts +5 -0
  38. package/src/client/index.ts +167 -0
  39. package/src/client/locales.ts +211 -0
  40. package/src/client/settings-card.module.css +261 -0
  41. package/src/client/settings-form.ts +294 -0
  42. package/src/client/sidebar-entry.ts +118 -0
  43. package/src/core/controller.ts +379 -0
  44. package/src/core/execution.ts +259 -0
  45. package/src/core/schedule.ts +140 -0
  46. package/src/core/scheduler.ts +104 -0
  47. package/src/core/store.ts +183 -0
  48. package/src/core/tasks.ts +204 -0
  49. package/src/index.ts +94 -0
  50. 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