@linxin666/dsh-client-ui-task-board 0.4.2 → 0.4.4

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 (73) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +21 -13
  3. package/README.zh.md +21 -13
  4. package/lib/client.js +1566 -929
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +934 -176
  7. package/lib/types/client/board/TaskForm.d.ts +19 -1
  8. package/lib/types/client/board/preset-label.d.ts +22 -0
  9. package/lib/types/client/host-api.d.ts +24 -1
  10. package/lib/types/client/index.d.ts +15 -1
  11. package/lib/types/client/locales.d.ts +38 -2
  12. package/lib/types/client/native-panel.d.ts +62 -0
  13. package/lib/types/client/schedule-zone.d.ts +47 -0
  14. package/lib/types/client/settings-entry-form.d.ts +49 -0
  15. package/lib/types/core/controller.d.ts +42 -2
  16. package/lib/types/core/degraded-reason.d.ts +46 -0
  17. package/lib/types/core/package-name.d.ts +9 -0
  18. package/lib/types/core/schedule.d.ts +95 -20
  19. package/lib/types/core/subtask.d.ts +30 -11
  20. package/lib/types/core/tasks.d.ts +26 -2
  21. package/lib/types/core/use-cases/task-create.d.ts +2 -1
  22. package/lib/types/core/use-cases/task-schedule.d.ts +8 -4
  23. package/lib/types/core/use-cases/task-update.d.ts +1 -1
  24. package/lib/types/host-ledger.d.ts +65 -4
  25. package/lib/types/host-runner.d.ts +76 -3
  26. package/lib/types/host-service.d.ts +77 -5
  27. package/lib/types/index.d.ts +11 -1
  28. package/lib/types/mount-once.d.ts +38 -7
  29. package/lib/types/protocol.d.ts +9 -3
  30. package/package.json +26 -23
  31. package/src/client/board/LinkSubtaskModal.tsx +21 -11
  32. package/src/client/board/NewTaskModal.tsx +271 -150
  33. package/src/client/board/TaskCard.tsx +24 -5
  34. package/src/client/board/TaskDetail.tsx +83 -33
  35. package/src/client/board/TaskForm.tsx +48 -3
  36. package/src/client/board/preset-label.ts +44 -0
  37. package/src/client/board.module.css +130 -136
  38. package/src/client/host-api.ts +109 -6
  39. package/src/client/index.ts +50 -18
  40. package/src/client/locales.ts +82 -8
  41. package/src/client/native-panel.tsx +124 -0
  42. package/src/client/schedule-zone.ts +135 -0
  43. package/src/client/settings-entry-form.ts +142 -0
  44. package/src/core/controller.ts +66 -4
  45. package/src/core/degraded-reason.ts +81 -0
  46. package/src/core/package-name.ts +9 -0
  47. package/src/core/schedule.ts +211 -55
  48. package/src/core/store.ts +10 -1
  49. package/src/core/subtask.ts +77 -22
  50. package/src/core/tasks.ts +38 -4
  51. package/src/core/use-cases/task-create.ts +14 -2
  52. package/src/core/use-cases/task-schedule.ts +17 -6
  53. package/src/core/use-cases/task-update.ts +4 -1
  54. package/src/host/agent-tools.ts +40 -8
  55. package/src/host-ledger.ts +223 -23
  56. package/src/host-routes.ts +37 -1
  57. package/src/host-runner.ts +164 -14
  58. package/src/host-service.ts +232 -39
  59. package/src/index.ts +23 -1
  60. package/src/mount-once.ts +107 -13
  61. package/src/protocol.ts +27 -8
  62. package/lib/types/client/board-mount.d.ts +0 -23
  63. package/lib/types/client/body-mutations.d.ts +0 -45
  64. package/lib/types/client/panel-mount-core.d.ts +0 -56
  65. package/lib/types/client/sidebar-entry-core.d.ts +0 -55
  66. package/lib/types/client/sidebar-entry.d.ts +0 -29
  67. package/lib/types/host/run-guarded.d.ts +0 -39
  68. package/src/client/board-mount.tsx +0 -43
  69. package/src/client/body-mutations.ts +0 -133
  70. package/src/client/panel-mount-core.ts +0 -155
  71. package/src/client/sidebar-entry-core.ts +0 -237
  72. package/src/client/sidebar-entry.ts +0 -53
  73. package/src/host/run-guarded.ts +0 -76
package/lib/client.js CHANGED
@@ -6,7 +6,6 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
8
  let react_jsx_runtime = require("react/jsx-runtime");
9
- let react_dom_client = require("react-dom/client");
10
9
  /**
11
10
  * Repair a persisted tag list: keep the well-formed entries, trim, drop
12
11
  * blanks and repeats, cap the count, and collapse a blank prompt line to
@@ -160,6 +159,7 @@ window.__ModuleLoader__.load({
160
159
  model: normalizeTargetId(input.model),
161
160
  reuseSession: input.reuseSession === true ? true : void 0,
162
161
  teamRun: input.teamRun === true ? true : void 0,
162
+ goalRun: input.goalRun === false ? false : void 0,
163
163
  ...input.freeze === void 0 ? {} : { freeze: freezeOf(input.freeze, now) },
164
164
  ...input.handover === void 0 ? {} : { handover: {
165
165
  ...input.handover,
@@ -187,11 +187,14 @@ window.__ModuleLoader__.load({
187
187
  const schedule = {
188
188
  enabled: current?.enabled ?? false,
189
189
  cron: current?.cron ?? "",
190
+ ...current?.timeZone === void 0 ? {} : { timeZone: current.timeZone },
190
191
  nextRunAt: current?.nextRunAt,
191
192
  lastTriggeredAt: current?.lastTriggeredAt
192
193
  };
193
194
  if ("enabled" in patch) schedule.enabled = patch.enabled ?? false;
194
195
  if ("cron" in patch) schedule.cron = patch.cron ?? "";
196
+ if ("timeZone" in patch) if (patch.timeZone === void 0) delete schedule.timeZone;
197
+ else schedule.timeZone = patch.timeZone;
195
198
  if ("nextRunAt" in patch) schedule.nextRunAt = patch.nextRunAt;
196
199
  if ("lastTriggeredAt" in patch) schedule.lastTriggeredAt = patch.lastTriggeredAt;
197
200
  return {
@@ -223,6 +226,22 @@ window.__ModuleLoader__.load({
223
226
  function isExecutionOutcome(value) {
224
227
  return value === "succeeded" || value === "failed" || value === "cancelled";
225
228
  }
229
+ /** Build the reusable index for a task list (ledger order preserved). */
230
+ function buildLineageIndex(tasks) {
231
+ const byId = /* @__PURE__ */ new Map();
232
+ const childrenByParent = /* @__PURE__ */ new Map();
233
+ for (const task of tasks) {
234
+ byId.set(task.id, task);
235
+ if (task.parentId === void 0) continue;
236
+ const siblings = childrenByParent.get(task.parentId);
237
+ if (siblings === void 0) childrenByParent.set(task.parentId, [task]);
238
+ else siblings.push(task);
239
+ }
240
+ return {
241
+ byId,
242
+ childrenByParent
243
+ };
244
+ }
226
245
  function indexOf(tasks) {
227
246
  return new Map(tasks.map((task) => [task.id, task]));
228
247
  }
@@ -232,8 +251,8 @@ window.__ModuleLoader__.load({
232
251
  * under a larger limit survives that limit being lowered, and restore and
233
252
  * inheritance must still see all of it.
234
253
  */
235
- function ancestorChain(tasks, task) {
236
- const index = indexOf(tasks);
254
+ function ancestorChain(tasks, task, lineage) {
255
+ const index = lineage?.byId ?? indexOf(tasks);
237
256
  const chain = [];
238
257
  const seen = /* @__PURE__ */ new Set([task.id]);
239
258
  let current = task.parentId === void 0 ? void 0 : index.get(task.parentId);
@@ -246,29 +265,33 @@ window.__ModuleLoader__.load({
246
265
  return chain;
247
266
  }
248
267
  /** Root-to-task depth: a root task is 0, its subtask 1, and so on. */
249
- function taskDepth(tasks, id) {
250
- const task = indexOf(tasks).get(id);
251
- return task === void 0 ? 0 : ancestorChain(tasks, task).length;
268
+ function taskDepth(tasks, id, lineage) {
269
+ const task = (lineage?.byId ?? indexOf(tasks)).get(id);
270
+ return task === void 0 ? 0 : ancestorChain(tasks, task, lineage).length;
252
271
  }
253
272
  /** Direct subtasks of a task, in ledger order. */
254
- function directSubtasks(tasks, id) {
273
+ function directSubtasks(tasks, id, lineage) {
274
+ if (lineage !== void 0) return [...lineage.childrenByParent.get(id) ?? []];
255
275
  return tasks.filter((task) => task.parentId === id);
256
276
  }
257
277
  /**
258
278
  * Descendants of a task in breadth-first order, bounded by maxSubtaskDepth
259
279
  * total depth. The visited set keeps a malformed ledger from looping.
260
280
  */
261
- function descendantTasks(tasks, id, maxSubtaskDepth) {
281
+ function descendantTasks(tasks, id, maxSubtaskDepth, lineage) {
262
282
  const visited = /* @__PURE__ */ new Set([id]);
263
283
  const found = [];
264
284
  let frontier = [id];
265
285
  for (let depth = 1; depth <= maxSubtaskDepth && frontier.length > 0; depth += 1) {
266
286
  const next = [];
267
- for (const parentId of frontier) for (const task of tasks) {
268
- if (task.parentId !== parentId || visited.has(task.id)) continue;
269
- visited.add(task.id);
270
- found.push(task);
271
- next.push(task.id);
287
+ for (const parentId of frontier) {
288
+ const children = lineage === void 0 ? tasks.filter((task) => task.parentId === parentId) : lineage.childrenByParent.get(parentId) ?? [];
289
+ for (const task of children) {
290
+ if (visited.has(task.id)) continue;
291
+ visited.add(task.id);
292
+ found.push(task);
293
+ next.push(task.id);
294
+ }
272
295
  }
273
296
  frontier = next;
274
297
  }
@@ -279,13 +302,13 @@ window.__ModuleLoader__.load({
279
302
  * The visited set makes the walk terminate on a cyclic hand-edited ledger
280
303
  * instead of recursing until the stack overflows.
281
304
  */
282
- function subtreeHeight(tasks, id, visited = /* @__PURE__ */ new Set()) {
305
+ function subtreeHeight(tasks, id, visited = /* @__PURE__ */ new Set(), lineage) {
283
306
  if (visited.has(id)) return 0;
284
307
  const seen = /* @__PURE__ */ new Set([...visited, id]);
285
- const children = directSubtasks(tasks, id);
308
+ const children = directSubtasks(tasks, id, lineage);
286
309
  if (children.length === 0) return 0;
287
310
  let height = 0;
288
- for (const child of children) height = Math.max(height, 1 + subtreeHeight(tasks, child.id, seen));
311
+ for (const child of children) height = Math.max(height, 1 + subtreeHeight(tasks, child.id, seen, lineage));
289
312
  return Math.min(height, 3);
290
313
  }
291
314
  /**
@@ -295,8 +318,8 @@ window.__ModuleLoader__.load({
295
318
  * and the resulting depth (parent depth + 1 + the child own subtree height)
296
319
  * must stay within maxSubtaskDepth.
297
320
  */
298
- function checkParentLink(tasks, childId, parentId, maxSubtaskDepth) {
299
- if (tasks.find((task) => task.id === childId) === void 0) return {
321
+ function checkParentLink(tasks, childId, parentId, maxSubtaskDepth, lineage) {
322
+ if ((lineage === void 0 ? tasks.find((task) => task.id === childId) : lineage.byId.get(childId)) === void 0) return {
300
323
  ok: false,
301
324
  reason: "unknown-task"
302
325
  };
@@ -305,7 +328,7 @@ window.__ModuleLoader__.load({
305
328
  ok: false,
306
329
  reason: "self-parent"
307
330
  };
308
- const parent = tasks.find((task) => task.id === parentId);
331
+ const parent = lineage === void 0 ? tasks.find((task) => task.id === parentId) : lineage.byId.get(parentId);
309
332
  if (parent === void 0) return {
310
333
  ok: false,
311
334
  reason: "unknown-parent"
@@ -314,11 +337,11 @@ window.__ModuleLoader__.load({
314
337
  ok: false,
315
338
  reason: "archived-parent"
316
339
  };
317
- if (descendantTasks(tasks, childId, 3).some((task) => task.id === parentId)) return {
340
+ if (descendantTasks(tasks, childId, 3, lineage).some((task) => task.id === parentId)) return {
318
341
  ok: false,
319
342
  reason: "cycle"
320
343
  };
321
- if (taskDepth(tasks, parentId) + 1 + subtreeHeight(tasks, childId) > maxSubtaskDepth) return {
344
+ if (taskDepth(tasks, parentId, lineage) + 1 + subtreeHeight(tasks, childId, /* @__PURE__ */ new Set(), lineage) > maxSubtaskDepth) return {
322
345
  ok: false,
323
346
  reason: "depth-exceeded"
324
347
  };
@@ -425,6 +448,112 @@ window.__ModuleLoader__.load({
425
448
  [1, 12],
426
449
  [0, 7]
427
450
  ];
451
+ /** IANA `Area/Location`, plus the bare `UTC` the platform reports. */
452
+ const IANA_ZONE = /^[A-Za-z][A-Za-z0-9_+.-]*(?:\/[A-Za-z0-9_+.-]+)+$/;
453
+ /** Cached formatters: constructing an Intl formatter dominates the cost of a resolve. */
454
+ const FORMATTERS = /* @__PURE__ */ new Map();
455
+ function zoneFormatter(timeZone) {
456
+ let formatter = FORMATTERS.get(timeZone);
457
+ if (formatter === void 0) {
458
+ formatter = new Intl.DateTimeFormat("en-US", {
459
+ timeZone,
460
+ hourCycle: "h23",
461
+ year: "numeric",
462
+ month: "2-digit",
463
+ day: "2-digit",
464
+ hour: "2-digit",
465
+ minute: "2-digit"
466
+ });
467
+ FORMATTERS.set(timeZone, formatter);
468
+ }
469
+ return formatter;
470
+ }
471
+ /**
472
+ * The wall-clock reading of one instant in an explicit zone. Reading through
473
+ * Intl (rather than the process `Date` accessors) is what makes the result
474
+ * independent of the Host's own TZ.
475
+ * @param timeZone - explicit IANA zone.
476
+ * @param epochMs - instant to read.
477
+ * @returns the zone-local calendar and clock fields.
478
+ */
479
+ function partsInZone(timeZone, epochMs) {
480
+ const fields = {};
481
+ for (const part of zoneFormatter(timeZone).formatToParts(new Date(epochMs))) if (part.type !== "literal") fields[part.type] = part.value;
482
+ return {
483
+ year: Number(fields.year),
484
+ month: Number(fields.month),
485
+ day: Number(fields.day),
486
+ hour: Number(fields.hour),
487
+ minute: Number(fields.minute)
488
+ };
489
+ }
490
+ /** The zone's UTC offset in effect at one instant, in milliseconds. */
491
+ function offsetAt(timeZone, epochMs) {
492
+ const parts = partsInZone(timeZone, epochMs);
493
+ return Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute) - epochMs;
494
+ }
495
+ /**
496
+ * Resolve a wall clock in an explicit zone to its instant.
497
+ *
498
+ * The offset is probed a day either side of the naive guess so both DST
499
+ * transitions are covered, then every candidate offset whose inverse maps back
500
+ * to the requested wall clock is kept. No survivor means the wall clock does
501
+ * not exist (a spring-forward gap); several means it is ambiguous (a fall-back
502
+ * overlap), and the earliest instant wins so the repeated clock is taken once.
503
+ * @param timeZone - explicit IANA zone.
504
+ * @param year - full calendar year.
505
+ * @param month - 1-12.
506
+ * @param day - day of month.
507
+ * @param hour - 0-23.
508
+ * @param minute - 0-59.
509
+ * @returns the instant in ms epoch, or undefined when the wall clock is skipped.
510
+ */
511
+ function zonedEpoch(timeZone, year, month, day, hour, minute) {
512
+ const naive = Date.UTC(year, month - 1, day, hour, minute);
513
+ const offsets = /* @__PURE__ */ new Set([
514
+ offsetAt(timeZone, naive - 864e5),
515
+ offsetAt(timeZone, naive),
516
+ offsetAt(timeZone, naive + 864e5)
517
+ ]);
518
+ let earliest;
519
+ for (const offset of offsets) {
520
+ const candidate = naive - offset;
521
+ const parts = partsInZone(timeZone, candidate);
522
+ if (parts.year !== year || parts.month !== month || parts.day !== day) continue;
523
+ if (parts.hour !== hour || parts.minute !== minute) continue;
524
+ if (earliest === void 0 || candidate < earliest) earliest = candidate;
525
+ }
526
+ return earliest;
527
+ }
528
+ /**
529
+ * The Host process's IANA zone, used for schedules that store no zone of their
530
+ * own (a ledger written before zones were persisted).
531
+ * @returns a zone name Intl accepts, never empty.
532
+ */
533
+ function resolveHostTimeZone() {
534
+ try {
535
+ const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
536
+ return typeof zone === "string" && zone !== "" ? zone : "UTC";
537
+ } catch {
538
+ return "UTC";
539
+ }
540
+ }
541
+ /**
542
+ * Whether a value names a zone this runtime can actually resolve. A stored
543
+ * zone is trusted only after it round-trips through Intl, so a typo or a zone
544
+ * that this ICU build does not know can never arm a schedule.
545
+ * @param value - candidate zone name.
546
+ * @returns true when the name is usable.
547
+ */
548
+ function isValidTimeZone(value) {
549
+ if (value.trim() !== value || value === "") return false;
550
+ if (value !== "UTC" && !IANA_ZONE.test(value)) return false;
551
+ try {
552
+ return new Intl.DateTimeFormat("en-US", { timeZone: value }).resolvedOptions().timeZone !== void 0;
553
+ } catch {
554
+ return false;
555
+ }
556
+ }
428
557
  /**
429
558
  * Parse a 5-field cron expression.
430
559
  * @returns the match sets, or null when the expression is invalid.
@@ -447,8 +576,8 @@ window.__ModuleLoader__.load({
447
576
  days: sets[2],
448
577
  months: sets[3],
449
578
  weekdays,
450
- dayWildcard: fields[2] === "*",
451
- weekdayWildcard: fields[4] === "*"
579
+ dayStarred: fields[2].startsWith("*"),
580
+ weekdayStarred: fields[4].startsWith("*")
452
581
  };
453
582
  }
454
583
  /** Whether the expression parses. */
@@ -456,52 +585,57 @@ window.__ModuleLoader__.load({
456
585
  return parseCron(expr) !== null;
457
586
  }
458
587
  /**
459
- * Compute the next matching instant after `fromMs` (ms epoch), in local time,
460
- * at minute granularity, strictly greater than `fromMs`. Returns the ms epoch
461
- * of the matching minute's start, or undefined when the calendar constraint
462
- * can never match (for example `0 0 30 2 *`). The five-year horizon includes
463
- * a full leap cycle, so a valid February 29 schedule remains reachable from
464
- * every non-leap year.
588
+ * Compute the next matching instant after `fromMs` strictly greater than it,
589
+ * interpreting the expression's wall clock in `timeZone` (the Host zone when
590
+ * omitted). Returns the ms epoch of the matching minute's start, or undefined
591
+ * when the calendar constraint can never match (for example `0 0 30 2 *`).
592
+ * The five-year horizon includes a full leap cycle, so a valid February 29
593
+ * schedule remains reachable from every non-leap year.
465
594
  *
466
595
  * Walks candidate year/month/day/hour/minute values straight from the parsed
467
596
  * field sets instead of scanning every minute: a sparse expression such as
468
597
  * `0 0 29 2 *` used to iterate ~1.5M wall-clock minutes before reaching the
469
- * next leap day. Wall-clock field construction + the final `matches` re-check
470
- * preserve the old minute scan's DST semantics exactly (nonexistent spring
471
- * minutes normalize forward and the repeated fall-back hour is never visited).
598
+ * next leap day. A candidate wall clock that the zone skips (spring-forward
599
+ * gap) is passed over, which is what makes the gap semantics fall out of the
600
+ * candidate walk rather than needing a separate rule.
601
+ * @param expr - 5-field cron expression.
602
+ * @param fromMs - exclusive lower bound, ms epoch.
603
+ * @param timeZone - explicit IANA zone; defaults to the Host zone.
604
+ * @returns the next trigger instant, or undefined when unreachable.
472
605
  */
473
- function nextRunAtMs(expr, fromMs) {
606
+ function nextRunAtMs(expr, fromMs, timeZone) {
474
607
  const schedule = parseCron(expr);
475
608
  if (schedule === null) return void 0;
476
609
  if (!hasPossibleCalendarDay(schedule)) return void 0;
477
- const from = new Date(fromMs);
610
+ const zone = timeZone ?? resolveHostTimeZone();
611
+ const start = partsInZone(zone, fromMs);
478
612
  const limitMs = fromMs + 5 * 366 * 24 * 60 * 60 * 1e3;
479
613
  const sortedMinutes = [...schedule.minutes].sort((a, b) => a - b);
480
614
  const sortedHours = [...schedule.hours].sort((a, b) => a - b);
481
615
  const sortedMonths = [...schedule.months].sort((a, b) => a - b);
482
- let year = from.getFullYear();
483
- let month = from.getMonth() + 1;
484
- let day = from.getDate();
485
- let hour = from.getHours();
486
- let minute = from.getMinutes() + 1;
487
- while (new Date(year, month - 1, 1, 0, 0, 0, 0).getTime() <= limitMs) {
616
+ let year = start.year;
617
+ let month = start.month;
618
+ let day = start.day;
619
+ let hour = start.hour;
620
+ let minute = start.minute + 1;
621
+ while (Date.UTC(year, month - 1, 1) <= limitMs + 864e5) {
488
622
  for (const candidateMonth of sortedMonths) {
489
623
  if (candidateMonth < month) continue;
490
- const daysInMonth = new Date(year, candidateMonth, 0).getDate();
624
+ const daysInMonth = new Date(Date.UTC(year, candidateMonth, 0)).getUTCDate();
491
625
  const dayStart = candidateMonth === month ? day : 1;
492
626
  for (let candidateDay = dayStart; candidateDay <= daysInMonth; candidateDay += 1) {
493
- if (!dayCandidate(schedule, new Date(year, candidateMonth - 1, candidateDay, 0, 0, 0, 0))) continue;
627
+ if (!dayCandidate(schedule, year, candidateMonth, candidateDay)) continue;
494
628
  const hourStart = candidateMonth === month && candidateDay === day ? hour : 0;
495
629
  for (const candidateHour of sortedHours) {
496
630
  if (candidateHour < hourStart) continue;
497
631
  const minuteStart = candidateMonth === month && candidateDay === day && candidateHour === hour ? minute : 0;
498
632
  for (const candidateMinute of sortedMinutes) {
499
633
  if (candidateMinute < minuteStart) continue;
500
- const candidate = new Date(year, candidateMonth - 1, candidateDay, candidateHour, candidateMinute, 0, 0);
501
- const time = candidate.getTime();
634
+ const time = zonedEpoch(zone, year, candidateMonth, candidateDay, candidateHour, candidateMinute);
635
+ if (time === void 0) continue;
502
636
  if (time <= fromMs) continue;
503
637
  if (time > limitMs) return void 0;
504
- if (matches(schedule, candidate)) return time;
638
+ return time;
505
639
  }
506
640
  }
507
641
  }
@@ -513,17 +647,20 @@ window.__ModuleLoader__.load({
513
647
  minute = 0;
514
648
  }
515
649
  }
516
- /** Day/weekday OR gate shared by {@link matches} and the candidate scan. */
517
- function dayCandidate(schedule, date) {
518
- const dayMatches = schedule.days.has(date.getDate());
519
- const weekdayMatches = schedule.weekdays.has(date.getDay());
520
- if (schedule.dayWildcard) return weekdayMatches;
521
- if (schedule.weekdayWildcard) return dayMatches;
522
- return dayMatches || weekdayMatches;
650
+ /**
651
+ * The Vixie day gate shared by the candidate walk. Both fields restricted is
652
+ * OR; any other combination is AND, because a starred field matches every
653
+ * value and so cannot widen the result.
654
+ */
655
+ function dayCandidate(schedule, year, month, day) {
656
+ const dayMatches = schedule.days.has(day);
657
+ const weekdayMatches = schedule.weekdays.has(new Date(Date.UTC(year, month - 1, day)).getUTCDay());
658
+ if (!schedule.dayStarred && !schedule.weekdayStarred) return dayMatches || weekdayMatches;
659
+ return dayMatches && weekdayMatches;
523
660
  }
524
661
  /** Reject impossible month/day pairs without spending the multi-year scan. */
525
662
  function hasPossibleCalendarDay(schedule) {
526
- if (schedule.dayWildcard || !schedule.weekdayWildcard) return true;
663
+ if (schedule.dayStarred || !schedule.weekdayStarred) return true;
527
664
  const maximumDay = /* @__PURE__ */ new Map([
528
665
  [1, 31],
529
666
  [2, 29],
@@ -574,13 +711,6 @@ window.__ModuleLoader__.load({
574
711
  }
575
712
  return true;
576
713
  }
577
- /** Day/weekday OR semantics: a restricted day field alone gates, and vice versa. */
578
- function matches(schedule, date) {
579
- if (!schedule.minutes.has(date.getMinutes())) return false;
580
- if (!schedule.hours.has(date.getHours())) return false;
581
- if (!schedule.months.has(date.getMonth() + 1)) return false;
582
- return dayCandidate(schedule, date);
583
- }
584
714
  function isDigits(value) {
585
715
  return /^\d+$/.test(value);
586
716
  }
@@ -606,8 +736,9 @@ window.__ModuleLoader__.load({
606
736
  * @param now - clock instant (ms epoch).
607
737
  * @param id - minted task id.
608
738
  * @param maxSubtaskDepth - deployment subtask depth limit.
739
+ * @param hostTimeZone - zone a rule that stores no zone of its own follows.
609
740
  */
610
- function applyCreateTask(tasks, input, now, id, maxSubtaskDepth = 1) {
741
+ function applyCreateTask(tasks, input, now, id, maxSubtaskDepth = 1, hostTimeZone = resolveHostTimeZone()) {
611
742
  if (input.title.trim() === "") return {
612
743
  task: void 0,
613
744
  tasks,
@@ -642,10 +773,18 @@ window.__ModuleLoader__.load({
642
773
  const requested = input.schedule;
643
774
  if (requested?.enabled === true && requested.cron.trim() !== "" && isValidCron(requested.cron)) {
644
775
  const cron = requested.cron.trim();
645
- task = withSchedule(task, {
776
+ const requestedZone = requested.timeZone;
777
+ if (requestedZone !== void 0 && !isValidTimeZone(requestedZone)) return {
778
+ task: void 0,
779
+ tasks,
780
+ error: "invalid schedule time zone"
781
+ };
782
+ const nextRunAt = nextRunAtMs(cron, now, requestedZone ?? hostTimeZone);
783
+ if (nextRunAt !== void 0) task = withSchedule(task, {
646
784
  enabled: true,
647
785
  cron,
648
- nextRunAt: nextRunAtMs(cron, now)
786
+ timeZone: requestedZone,
787
+ nextRunAt
649
788
  }, now);
650
789
  }
651
790
  return {
@@ -683,15 +822,17 @@ window.__ModuleLoader__.load({
683
822
  * the core cron parser (schedule.ts) and the withSchedule transition.
684
823
  */
685
824
  /**
686
- * Set an on-board task's schedule rule. A blank or invalid cron, or an
687
- * archived task, is rejected (state untouched); an enabled rule computes the
688
- * next run instant immediately, a disabled one carries no next-run instant.
825
+ * Set an on-board task's schedule rule. A blank or invalid cron, an unknown or
826
+ * archived task, or an unusable zone is rejected (state untouched); an enabled
827
+ * rule computes the next run instant immediately in the rule's own zone, a
828
+ * disabled one carries no next-run instant.
689
829
  * @param tasks - current ledger.
690
830
  * @param id - the task to schedule.
691
831
  * @param patch - rule fields to change (absent fields keep their current value).
692
832
  * @param now - clock instant (ms epoch).
833
+ * @param hostTimeZone - zone a rule with no stored zone follows.
693
834
  */
694
- function applySetSchedule(tasks, id, patch, now) {
835
+ function applySetSchedule(tasks, id, patch, now, hostTimeZone) {
695
836
  const task = tasks.find((candidate) => candidate.id === id);
696
837
  if (task === void 0 || task.archivedAt !== void 0) return {
697
838
  tasks,
@@ -703,8 +844,13 @@ window.__ModuleLoader__.load({
703
844
  tasks,
704
845
  applied: false
705
846
  };
847
+ const requestedZone = patch.timeZone === void 0 ? current?.timeZone : patch.timeZone ?? void 0;
848
+ if (requestedZone !== void 0 && !isValidTimeZone(requestedZone)) return {
849
+ tasks,
850
+ applied: false
851
+ };
706
852
  const enabled = patch.enabled ?? current?.enabled ?? false;
707
- const nextRunAt = enabled ? nextRunAtMs(cron, now) : void 0;
853
+ const nextRunAt = enabled ? nextRunAtMs(cron, now, requestedZone ?? hostTimeZone) : void 0;
708
854
  if (enabled && nextRunAt === void 0) return {
709
855
  tasks,
710
856
  applied: false
@@ -713,6 +859,7 @@ window.__ModuleLoader__.load({
713
859
  tasks: tasks.map((candidate) => candidate.id === id ? withSchedule(candidate, {
714
860
  enabled,
715
861
  cron,
862
+ timeZone: requestedZone ?? void 0,
716
863
  nextRunAt
717
864
  }, now) : candidate),
718
865
  applied: true
@@ -868,6 +1015,7 @@ window.__ModuleLoader__.load({
868
1015
  if ("permission" in patch && patch.permission !== void 0 && patch.permission !== task.permission || "handover" in patch) next.permissionConfirmedAt = void 0;
869
1016
  if ("reuseSession" in patch) next.reuseSession = patch.reuseSession === true ? true : void 0;
870
1017
  if ("teamRun" in patch) next.teamRun = patch.teamRun === true ? true : void 0;
1018
+ if ("goalRun" in patch) next.goalRun = patch.goalRun === false ? false : void 0;
871
1019
  if (workspaceId !== void 0 || "workspaceId" in patch) next.workspaceId = workspaceId;
872
1020
  if (mode !== void 0 || "mode" in patch) next.mode = mode;
873
1021
  if (permission !== void 0 || "permission" in patch) next.permission = permission;
@@ -880,6 +1028,8 @@ window.__ModuleLoader__.load({
880
1028
  function currentOf(sessions) {
881
1029
  return sessions?.current();
882
1030
  }
1031
+ /** Stable id shared by the board's sidebar panel row and its main-slot page. */
1032
+ const TASK_BOARD_PANEL_ID = "task-board";
883
1033
  /** The selected task (resolved from the ledger), or undefined. */
884
1034
  function selectedTaskOf(snapshot) {
885
1035
  if (snapshot.selectedTaskId === void 0) return void 0;
@@ -977,20 +1127,55 @@ window.__ModuleLoader__.load({
977
1127
  async retryHostSync() {
978
1128
  return await this.initializeRemote();
979
1129
  }
1130
+ /**
1131
+ * Show the board. The layout owns which panel the center column renders, so
1132
+ * the state flip and the panel selection travel together here; a composition
1133
+ * with no layout face (tests, a shell-less host) still flips the state.
1134
+ *
1135
+ * The selection is requested AFTER the snapshot flips so a subscriber
1136
+ * rendering against `boardOpen` never observes "open" while the shell still
1137
+ * shows the conversation.
1138
+ */
980
1139
  openBoard() {
981
1140
  if (this.boardOpen) return;
982
1141
  this.boardOpen = true;
983
1142
  this.notify();
1143
+ this.selectPanel(TASK_BOARD_PANEL_ID);
984
1144
  }
1145
+ /**
1146
+ * Return the center column to the conversation. The layout's own selection is
1147
+ * the source of truth for what the column renders, so this asks for the
1148
+ * conversation explicitly rather than only clearing local state.
1149
+ */
985
1150
  closeBoard() {
986
1151
  this.boardOpen = false;
987
1152
  this.notify();
1153
+ this.selectPanel(null);
988
1154
  }
989
1155
  toggleBoard() {
990
1156
  if (this.boardOpen) this.closeBoard();
991
1157
  else this.openBoard();
992
1158
  }
993
1159
  /**
1160
+ * Reflect a panel selection that came from OUTSIDE this controller (the user
1161
+ * clicked another sidebar row, or the layout dropped the panel id). Keeps
1162
+ * `boardOpen` aligned with what the column actually shows without asking the
1163
+ * layout to select anything back.
1164
+ * @param panelId - the layout's current panel id, or null for the conversation.
1165
+ */
1166
+ syncPanelSelection(panelId) {
1167
+ const open = panelId === TASK_BOARD_PANEL_ID;
1168
+ if (open === this.boardOpen) return;
1169
+ this.boardOpen = open;
1170
+ this.notify();
1171
+ }
1172
+ /** Ask the layout to select a panel; a shell that serves no layout face is a no-op. */
1173
+ selectPanel(panelId) {
1174
+ try {
1175
+ this.deps.panel?.select(panelId);
1176
+ } catch {}
1177
+ }
1178
+ /**
994
1179
  * Switch between the kanban columns and the archive view. Leaving the
995
1180
  * archive view with an archived task still selected closes the selection —
996
1181
  * the detail overlay must not linger over a task that is off-board.
@@ -1181,7 +1366,8 @@ window.__ModuleLoader__.load({
1181
1366
  * @returns true when applied, false when rejected (invalid cron / unknown task).
1182
1367
  */
1183
1368
  setSchedule(id, patch) {
1184
- const { tasks, applied } = applySetSchedule(this.tasks, id, patch, this.now());
1369
+ const hostTimeZone = this.hostState?.scheduler.timeZone ?? resolveHostTimeZone();
1370
+ const { tasks, applied } = applySetSchedule(this.tasks, id, patch, this.now(), hostTimeZone);
1185
1371
  if (!applied) return false;
1186
1372
  if (this.deps.transport !== void 0) {
1187
1373
  this.commitRemote({
@@ -1270,8 +1456,8 @@ window.__ModuleLoader__.load({
1270
1456
  * (background navigation, the Host runner creating and selecting a fresh
1271
1457
  * execution session, settlement, other plugins), so closing on `current`
1272
1458
  * changes would evict the board without the user asking. The board closes
1273
- * only on explicit user navigation: a sidebar session/workspace row click
1274
- * (board-mount onClickSidebarRow) or the board's own actions
1459
+ * only on explicit user navigation: selecting another panel (the layout owns
1460
+ * selection, and `syncPanelSelection` follows it) or the board's own actions
1275
1461
  * (openSession / close). Keeping the hook preserves the subscription
1276
1462
  * contract for future listeners.
1277
1463
  */
@@ -1708,6 +1894,7 @@ window.__ModuleLoader__.load({
1708
1894
  if (record.mode !== void 0 && typeof record.mode !== "string") return false;
1709
1895
  if (record.permission !== void 0 && typeof record.permission !== "string") return false;
1710
1896
  if (record.reuseSession !== void 0 && typeof record.reuseSession !== "boolean") return false;
1897
+ if (record.goalRun !== void 0 && typeof record.goalRun !== "boolean") return false;
1711
1898
  if (!Array.isArray(record.executions)) return false;
1712
1899
  for (const execution of record.executions) {
1713
1900
  if (typeof execution !== "object" || execution === null) return false;
@@ -1738,9 +1925,11 @@ window.__ModuleLoader__.load({
1738
1925
  const rule = schedule;
1739
1926
  if (typeof rule.cron !== "string") return void 0;
1740
1927
  if (rule.cron.trim() === "" || !isValidCron(rule.cron)) return void 0;
1928
+ const timeZone = typeof rule.timeZone === "string" && isValidTimeZone(rule.timeZone) ? rule.timeZone : void 0;
1741
1929
  return {
1742
1930
  enabled: rule.enabled === true,
1743
1931
  cron: rule.cron,
1932
+ ...timeZone === void 0 ? {} : { timeZone },
1744
1933
  nextRunAt: typeof rule.nextRunAt === "number" ? rule.nextRunAt : void 0,
1745
1934
  lastTriggeredAt: typeof rule.lastTriggeredAt === "number" ? rule.lastTriggeredAt : void 0
1746
1935
  };
@@ -1825,6 +2014,7 @@ window.__ModuleLoader__.load({
1825
2014
  task.archivedAt = typeof row.archivedAt === "number" && Number.isFinite(row.archivedAt) ? row.archivedAt : void 0;
1826
2015
  task.permission = isTaskPermission(row.permission) ? row.permission : void 0;
1827
2016
  task.reuseSession = row.reuseSession === true ? true : void 0;
2017
+ task.goalRun = row.goalRun === false ? false : void 0;
1828
2018
  task.freeze = normalizeFreeze(row.freeze);
1829
2019
  task.handover = normalizeHandover(row.handover);
1830
2020
  task.tags = normalizeTags(row.tags);
@@ -1942,8 +2132,11 @@ window.__ModuleLoader__.load({
1942
2132
  "board.updated": "更新于",
1943
2133
  "board.created": "创建于",
1944
2134
  "board.hostError": "Host 操作失败:{error}",
1945
- "board.hostError.notMounted": "任务看板的后台接口没有挂载:宿主可能没有加载该插件,也可能后台数据正被另一个 DSH 实例占用。重启 DSH 服务后再试",
2135
+ "board.hostError.notMounted": "任务看板的后台接口没有挂载:宿主没有加载这个插件(可能未安装、未启用,或该行启动失败)。重启 DSH 服务后再试",
2136
+ "board.hostError.ledgerLocked": "任务看板的账本已被另一个 DSH 进程占用(占用者 pid {pid}),本实例没有注册后台接口:关闭那个 DSH 实例,或等它退出后再点重试——反复重启本实例无效(宿主原因:{detail})",
2137
+ "board.hostError.degraded": "任务看板这一行在启动时降级,后台接口没有注册(宿主原因:{detail})",
1946
2138
  "board.hostError.unauthorized": "登录状态已失效,请刷新页面后重试",
2139
+ "board.hostError.forbidden": "当前环境未通过同源校验:桌面外壳或反向代理可能未传递浏览器标记,也没有携带宿主认证凭据",
1947
2140
  "board.hostError.locked": "任务看板的后台数据被占用:{detail}",
1948
2141
  "board.hostError.timeout": "后台 {seconds} 秒内没有响应,请稍后重试",
1949
2142
  "board.hostError.unreachable": "连接不上本机 DSH 服务,请确认它仍在运行",
@@ -1968,6 +2161,19 @@ window.__ModuleLoader__.load({
1968
2161
  "new.aiParseFailed": "解析失败:{error}",
1969
2162
  "board.retryHost": "重试连接 Host",
1970
2163
  "board.hostMeta": "Host 时区 {timeZone} · revision {revision}",
2164
+ "new.section.parse": "AI 解析",
2165
+ "new.section.content": "任务内容",
2166
+ "new.section.labels": "标签",
2167
+ "new.section.execution": "执行设置",
2168
+ "new.section.run": "运行方式",
2169
+ "new.section.handover": "交接与续跑",
2170
+ "new.section.schedule": "定时运行",
2171
+ "new.summary.none": "未设置",
2172
+ "new.summary.labelCount": "{count} 个标签",
2173
+ "new.summary.filled": "已填写",
2174
+ "new.summary.scheduleOff": "未启用",
2175
+ "new.summary.multiRound": "多轮 /goal",
2176
+ "new.summary.singleRound": "单轮",
1971
2177
  "new.title": "标题",
1972
2178
  "new.titlePlaceholder": "一句话描述要做什么",
1973
2179
  "new.description": "描述",
@@ -2059,13 +2265,31 @@ window.__ModuleLoader__.load({
2059
2265
  "detail.schedule.invalid": "Cron 表达式无效",
2060
2266
  "detail.schedule.notScheduled": "尚未排程",
2061
2267
  "detail.schedule.dueSoon": "即将运行",
2268
+ "detail.schedule.timeZone": "时区",
2269
+ "detail.schedule.timeZoneHost": "Host 时区({timeZone})",
2270
+ "detail.schedule.timeZoneHostUnknown": "Host 时区(未知)",
2271
+ "detail.schedule.timeZoneHint": "Cron 的墙上时间按此时区解读;夏令时跳变会跳过不存在的时刻,重复的时刻只触发一次。",
2272
+ "detail.schedule.timeZoneSearch": "搜索时区…",
2273
+ "detail.schedule.timeZoneNone": "没有匹配的时区",
2274
+ "detail.schedule.nextRunOverdue": "已逾期 {duration}",
2275
+ "detail.schedule.duration.days": "{count} 天",
2276
+ "detail.schedule.duration.hours": "{count} 小时",
2277
+ "detail.schedule.duration.minutes": "{count} 分钟",
2278
+ "detail.schedule.duration.seconds": "{count} 秒",
2062
2279
  "card.scheduled": "定时",
2063
2280
  "new.workspace": "工作区",
2064
- "new.mode": "模式",
2281
+ "new.agentPreset": "Agent 预设",
2065
2282
  "new.permission": "权限",
2066
2283
  "new.model": "模型",
2067
2284
  "exec.workspace.recent": "最近使用(默认)",
2068
- "exec.mode.default": "部署默认",
2285
+ "exec.mode.inherit": "继承(部署默认)",
2286
+ "exec.mode.inheritWith": "继承(部署默认:{preset})",
2287
+ "exec.mode.builtinGroup": "内置预设",
2288
+ "exec.mode.customGroup": "自定义预设",
2289
+ "preset.builtin.standard": "标准",
2290
+ "preset.builtin.ptc": "PTC(程序化工具调用)",
2291
+ "preset.builtin.minimal": "极简",
2292
+ "preset.builtin.cordis": "Cordis",
2069
2293
  "exec.mode.defaultSuffix": "(默认)",
2070
2294
  "exec.mode.brokenSuffix": "(不可用)",
2071
2295
  "exec.mode.removed": "(已移除)",
@@ -2081,8 +2305,10 @@ window.__ModuleLoader__.load({
2081
2305
  "exec.teamRunHint": "开启后:执行本任务只启动一个 Leader 会话,每个子任务由它派生为 teammate 并在该会话内并行工作(子任务自身的权限钉住不适用)。关闭则每个成员各开独立会话。",
2082
2306
  "exec.teamRunUnavailable": "当前部署未提供 Agent Teams 服务,无法启用。",
2083
2307
  "exec.reuseSessionHint": "开启后,本任务的后续执行在上一次会话里继续(该会话空闲且仍存在时),不再每次新建对话;每次复用时都会重新应用上面钉住的权限与模型。",
2308
+ "exec.goalRun": "以 dsh 内置的 /goal 开始执行任务",
2309
+ "exec.goalRunHint": "默认开启:执行时把任务目标作为持久目标交给 dsh 内置的 /goal,会话会自动续跑多轮,直到 agent 标记目标完成;看板在该目标真正结束后才结算本次执行。关闭则只执行一轮普通对话。",
2084
2310
  "detail.executionSettings": "执行设置",
2085
- "exec.hint": "执行时生效:工作区决定执行会话落在哪个工作区;模式决定会话的 agent 预设;权限经 /permission 命令应用到会话。留空则使用运行时默认。",
2311
+ "exec.hint": "执行时生效:工作区决定执行会话落在哪个工作区;Agent 预设决定会话的组合(工具面与提示词段);权限经 /permission 命令应用到会话。留空则继承部署默认。",
2086
2312
  "settings.title": "任务看板",
2087
2313
  "settings.description": "控制 Host 任务看板、agent 播报与运行期间的系统空闲睡眠保护。",
2088
2314
  "settings.enabled": "启用任务看板",
@@ -2164,8 +2390,11 @@ window.__ModuleLoader__.load({
2164
2390
  "board.updated": "Updated",
2165
2391
  "board.created": "Created",
2166
2392
  "board.hostError": "Host action failed: {error}",
2167
- "board.hostError.notMounted": "The task board Host API is not mounted: the Host may not have loaded the plugin, or another DSH instance holds the ledger. Restart the DSH service and try again",
2393
+ "board.hostError.notMounted": "The task board Host API is not mounted: the Host did not load this plugin (not installed, disabled, or its row failed to start). Restart the DSH service and try again",
2394
+ "board.hostError.ledgerLocked": "Another DSH process (pid {pid}) holds the task board ledger, so this instance registered no Host API: close that DSH instance, or wait for it to exit and retry. Restarting this instance cannot help (reason: {detail})",
2395
+ "board.hostError.degraded": "The task board row degraded during start, so its Host API was not registered: {detail}",
2168
2396
  "board.hostError.unauthorized": "This session is no longer signed in; reload the page and try again",
2397
+ "board.hostError.forbidden": "The request did not pass the same-origin fence: the desktop shell or reverse proxy may not have forwarded browser credentials",
2169
2398
  "board.hostError.locked": "The task board ledger is locked: {detail}",
2170
2399
  "board.hostError.timeout": "The Host did not answer within {seconds}s; try again shortly",
2171
2400
  "board.hostError.unreachable": "Cannot reach the local DSH service; make sure it is still running",
@@ -2190,6 +2419,19 @@ window.__ModuleLoader__.load({
2190
2419
  "new.aiParseFailed": "Parsing failed: {error}",
2191
2420
  "board.retryHost": "Retry Host connection",
2192
2421
  "board.hostMeta": "Host time zone {timeZone} · revision {revision}",
2422
+ "new.section.parse": "AI Parse",
2423
+ "new.section.content": "Task Content",
2424
+ "new.section.labels": "Tags",
2425
+ "new.section.execution": "Execution Settings",
2426
+ "new.section.run": "Run Mode",
2427
+ "new.section.handover": "Handover",
2428
+ "new.section.schedule": "Scheduled Runs",
2429
+ "new.summary.none": "Not set",
2430
+ "new.summary.labelCount": "{count} tags",
2431
+ "new.summary.filled": "Filled",
2432
+ "new.summary.scheduleOff": "Off",
2433
+ "new.summary.multiRound": "multi-round /goal",
2434
+ "new.summary.singleRound": "single round",
2193
2435
  "new.title": "Title",
2194
2436
  "new.titlePlaceholder": "What should be done, in one line",
2195
2437
  "new.description": "Description",
@@ -2281,13 +2523,31 @@ window.__ModuleLoader__.load({
2281
2523
  "detail.schedule.invalid": "Invalid cron expression",
2282
2524
  "detail.schedule.notScheduled": "Not scheduled yet",
2283
2525
  "detail.schedule.dueSoon": "Due soon",
2526
+ "detail.schedule.timeZone": "Time zone",
2527
+ "detail.schedule.timeZoneHost": "Host time zone ({timeZone})",
2528
+ "detail.schedule.timeZoneHostUnknown": "Host time zone (unknown)",
2529
+ "detail.schedule.timeZoneHint": "The cron wall clock is read in this zone; daylight-saving gaps are skipped and a repeated time fires once.",
2530
+ "detail.schedule.timeZoneSearch": "Search time zones…",
2531
+ "detail.schedule.timeZoneNone": "No matching time zone",
2532
+ "detail.schedule.nextRunOverdue": "Overdue by {duration}",
2533
+ "detail.schedule.duration.days": "{count}d",
2534
+ "detail.schedule.duration.hours": "{count}h",
2535
+ "detail.schedule.duration.minutes": "{count}m",
2536
+ "detail.schedule.duration.seconds": "{count}s",
2284
2537
  "card.scheduled": "scheduled",
2285
2538
  "new.workspace": "Workspace",
2286
- "new.mode": "Mode",
2539
+ "new.agentPreset": "Agent preset",
2287
2540
  "new.permission": "Permission",
2288
2541
  "new.model": "Model",
2289
2542
  "exec.workspace.recent": "Most recent (default)",
2290
- "exec.mode.default": "Deployment default",
2543
+ "exec.mode.inherit": "Inherit (deployment default)",
2544
+ "exec.mode.inheritWith": "Inherit (deployment default: {preset})",
2545
+ "exec.mode.builtinGroup": "Built-in presets",
2546
+ "exec.mode.customGroup": "Custom presets",
2547
+ "preset.builtin.standard": "Standard",
2548
+ "preset.builtin.ptc": "PTC (programmatic tool calling)",
2549
+ "preset.builtin.minimal": "Minimal",
2550
+ "preset.builtin.cordis": "Cordis",
2291
2551
  "exec.mode.defaultSuffix": " (default)",
2292
2552
  "exec.mode.brokenSuffix": " (unavailable)",
2293
2553
  "exec.mode.removed": " (removed)",
@@ -2303,8 +2563,10 @@ window.__ModuleLoader__.load({
2303
2563
  "exec.teamRunHint": "On: running this task starts one Lead session and spawns a teammate per subtask inside it, working in parallel (a subtask permission pin does not apply). Off: every member gets its own independent session.",
2304
2564
  "exec.teamRunUnavailable": "This deployment serves no Agent Teams service, so team runs cannot be enabled.",
2305
2565
  "exec.reuseSessionHint": "When on, later runs continue in the previous session (when that session is idle and still exists) instead of starting a new conversation each time; the pinned permission and model above are re-applied on every reuse.",
2566
+ "exec.goalRun": "Start the run with dsh's built-in /goal",
2567
+ "exec.goalRunHint": "On by default: the run arms dsh built-in /goal with the task objective, so the session keeps working automatic continuation rounds until the agent marks the goal complete, and the board settles the execution only when that goal really ends. Off: one plain turn.",
2306
2568
  "detail.executionSettings": "Execution Settings",
2307
- "exec.hint": "Applied when the task runs: the workspace decides where the execution session lands; the mode composes the session's agent preset; the permission is applied through the /permission command. Blank = runtime default.",
2569
+ "exec.hint": "Applied when the task runs: the workspace decides where the execution session lands; the agent preset composes the session (tool face and prompt sections); the permission is applied through the /permission command. Blank inherits the deployment default.",
2308
2570
  "settings.title": "Task Board",
2309
2571
  "settings.description": "Configure the Host task board, agent announcement, and idle-system-sleep protection while work is pending.",
2310
2572
  "settings.enabled": "Enable the task board",
@@ -2387,7 +2649,7 @@ window.__ModuleLoader__.load({
2387
2649
  }
2388
2650
  //#endregion
2389
2651
  //#region \0dsh-css:packages/dsh-task-board/src/client/board.module.css.mjs
2390
- const css$1 = "[data-pane=conversation],[class*=centerCol]{position:relative}[data-dsh-taskboard-view]{z-index:60;background:var(--dsw-alias-bg-base);display:none;position:absolute;inset:0;container:_7D6uKa_task-board-view/inline-size}html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [data-dsh-taskboard-view]{display:block}html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [data-pane=conversation]>:not([data-dsh-taskboard-view]),html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*=centerCol]>:not([data-dsh-taskboard-view]){display:none!important}._7D6uKa_entry{box-sizing:border-box;min-height:36px;color:var(--dsw-alias-label-primary);cursor:pointer;font:inherit;text-align:left;white-space:nowrap;background:0 0;border:none;border-radius:12px;align-items:center;gap:8px;margin:0 2px;padding:7px 8px;font-size:14px;line-height:22px;display:flex}._7D6uKa_entry:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}._7D6uKa_entry[data-active]{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);font-weight:600}._7D6uKa_entryIcon{flex:none;justify-content:center;align-items:center;width:16px;height:16px;display:inline-flex}._7D6uKa_entryIcon svg{width:16px;height:16px;display:block}._7D6uKa_entryLabel{text-overflow:ellipsis;overflow:hidden}[data-dsh-frame][data-sidebar-collapsed] ._7D6uKa_entry,[data-sidebar-collapsed] ._7D6uKa_entry{border-radius:12px;justify-content:center;width:36px;height:36px;margin:0 auto 12px;padding:0}[data-dsh-frame][data-sidebar-collapsed] ._7D6uKa_entryIcon,[data-sidebar-collapsed] ._7D6uKa_entryIcon,[data-dsh-frame][data-sidebar-collapsed] ._7D6uKa_entryIcon svg,[data-sidebar-collapsed] ._7D6uKa_entryIcon svg{width:18px;height:18px}[data-dsh-frame][data-sidebar-collapsed] ._7D6uKa_entryLabel,[data-sidebar-collapsed] ._7D6uKa_entryLabel{display:none}._7D6uKa_board{box-sizing:border-box;background:var(--dsw-alias-bg-base);min-width:0;height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex-direction:column;gap:12px;padding:14px 16px 16px;display:flex}._7D6uKa_boardHeader{flex:none;align-items:center;gap:10px;display:flex}._7D6uKa_boardTitle{color:var(--dsw-alias-label-primary);white-space:nowrap;margin:0;font-size:16px;font-weight:700}._7D6uKa_backButton{align-items:center;gap:4px;display:inline-flex}._7D6uKa_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}._7D6uKa_search::placeholder{color:var(--dsw-alias-label-tertiary)}._7D6uKa_columns{overscroll-behavior-inline:contain;scrollbar-color:var(--dsw-alias-border-l3) var(--dsw-alias-interactive-bg-hover);scrollbar-width:thin;flex:1;grid-auto-columns:minmax(220px,1fr);grid-auto-flow:column;gap:12px;min-height:0;padding-bottom:6px;display:grid;overflow:auto hidden}._7D6uKa_columns::-webkit-scrollbar{height:10px}._7D6uKa_columns::-webkit-scrollbar-track{background:var(--dsw-alias-interactive-bg-hover);border-radius:999px}._7D6uKa_columns::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l3);background-clip:content-box;border:2px solid #0000;border-radius:999px}._7D6uKa_columns::-webkit-scrollbar-thumb:hover{background:var(--dsw-alias-border-l4);background-clip:content-box}._7D6uKa_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}._7D6uKa_columnHeader{flex:none;align-items:center;gap:6px;padding:10px 12px;display:flex}._7D6uKa_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}._7D6uKa_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}._7D6uKa_statusDot{border-radius:50%;flex:none;width:8px;height:8px}._7D6uKa_statusDot[data-status=backlog]{background:var(--dsw-alias-label-tertiary)}._7D6uKa_statusDot[data-status=todo]{background:var(--dsw-alias-state-business-primary)}._7D6uKa_statusDot[data-status=running]{background:var(--dsw-alias-state-warn-primary)}._7D6uKa_statusDot[data-status=done]{background:var(--dsw-alias-state-success-primary)}._7D6uKa_statusDot[data-status=failed]{background:var(--dsw-alias-state-error-primary)}._7D6uKa_cards{flex-direction:column;flex:1;gap:8px;min-height:0;padding:2px 8px 10px;display:flex;overflow-y:auto}._7D6uKa_columnEmpty{text-align:center;color:var(--dsw-alias-label-tertiary);padding:24px 8px;font-size:12px}._7D6uKa_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}._7D6uKa_card:hover{box-shadow:var(--dsw-shadow-lv2);border-color:var(--dsw-alias-border-l3);transform:translateY(-1px)}._7D6uKa_card[data-status=running]{border-color:var(--dsw-alias-state-warn-primary)}._7D6uKa_cardTitle{-webkit-line-clamp:2;-webkit-box-orient:vertical;font-size:13px;font-weight:600;line-height:1.35;display:-webkit-box;overflow:hidden}._7D6uKa_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}._7D6uKa_cardMeta{color:var(--dsw-alias-label-tertiary);align-items:center;gap:8px;font-size:11px;display:flex}._7D6uKa_cardTime{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}._7D6uKa_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}._7D6uKa_cardRun{flex:none}._7D6uKa_cardRun[data-result=failed]{color:var(--dsw-alias-state-error-primary)}._7D6uKa_cardRun[data-result=succeeded]{color:var(--dsw-alias-state-success-primary)}._7D6uKa_cardSession{color:var(--dsw-alias-state-business-primary);flex:none}._7D6uKa_cardRunningLabel{color:var(--dsw-alias-state-warn-primary);font-size:11px}._7D6uKa_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 _7D6uKa_dshTbSpin}@keyframes _7D6uKa_dshTbSpin{to{transform:rotate(360deg)}}._7D6uKa_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}._7D6uKa_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-info-hover)}._7D6uKa_primaryButton:disabled{opacity:.5;cursor:default}._7D6uKa_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}._7D6uKa_ghostButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}._7D6uKa_ghostButton:disabled{opacity:.45;cursor:default}._7D6uKa_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}._7D6uKa_dangerButton:hover:not(:disabled){filter:brightness(1.08)}._7D6uKa_dangerButton:active:not(:disabled){filter:brightness(.94)}._7D6uKa_dangerButton:disabled{opacity:.5;cursor:default}._7D6uKa_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}._7D6uKa_iconButton:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}._7D6uKa_linkButton{color:var(--dsw-alias-state-business-primary);cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:0;font-size:12px}._7D6uKa_linkButton:hover{text-decoration:underline}._7D6uKa_modalBackdrop{z-index:1300;background:var(--dsw-alias-bg-mask-1);justify-content:center;align-items:center;display:flex;position:fixed;inset:0}._7D6uKa_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}._7D6uKa_modalTitle{margin:0;font-size:15px;font-weight:700}._7D6uKa_confirmMessage{color:var(--dsw-alias-label-secondary);white-space:pre-wrap;overflow-wrap:anywhere;margin:0;font-size:13px;line-height:1.5}._7D6uKa_modalFooter{justify-content:flex-end;gap:10px;margin-top:4px;display:flex}._7D6uKa_field{flex-direction:column;gap:5px;display:flex}._7D6uKa_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}._7D6uKa_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}._7D6uKa_input:focus{border-color:var(--dsw-alias-state-business-primary)}._7D6uKa_select{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;max-width:100%;padding:7px 10px;font-family:inherit;font-size:13px}._7D6uKa_input::placeholder{color:var(--dsw-alias-label-tertiary)}._7D6uKa_formError{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}._7D6uKa_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}._7D6uKa_detailHeader{border-bottom:1px solid var(--dsw-alias-separator-primary);flex:none;align-items:center;gap:10px;padding:14px 18px;display:flex}._7D6uKa_detailTitle{overflow-wrap:anywhere;flex:1;margin:0;font-size:15px;font-weight:700}._7D6uKa_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}._7D6uKa_statusBadge[data-status=running]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}._7D6uKa_statusBadge[data-status=done]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}._7D6uKa_statusBadge[data-status=failed]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}._7D6uKa_detailBody{flex-direction:column;flex:1;gap:16px;padding:14px 18px;display:flex;overflow-y:auto}._7D6uKa_detailSection{flex-direction:column;gap:6px;display:flex}._7D6uKa_detailSection h4{color:var(--dsw-alias-label-tertiary);text-transform:none;margin:0;font-size:12px;font-weight:700}._7D6uKa_detailText{color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere;margin:0;font-size:13px;line-height:1.55}._7D6uKa_scheduleToggle{color:var(--dsw-alias-label-primary);cursor:pointer;user-select:none;align-items:center;gap:8px;font-size:13px;display:flex}._7D6uKa_scheduleToggle input{accent-color:var(--dsw-alias-state-business-primary)}._7D6uKa_scheduleRow{align-items:center;gap:8px;display:flex}._7D6uKa_scheduleInput{min-width:0;font-family:var(--dsw-font-markdown-code-block-small);flex:1;font-size:12.5px}._7D6uKa_scheduleInputInvalid,._7D6uKa_scheduleInputInvalid:focus{border-color:var(--dsw-alias-state-error-primary)}._7D6uKa_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}._7D6uKa_scheduleMeta{color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;margin:0;font-size:12px}._7D6uKa_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}._7D6uKa_executionList{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}._7D6uKa_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}._7D6uKa_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}._7D6uKa_executionBadge[data-result=succeeded]{color:var(--dsw-alias-state-success-primary);background:0 0}._7D6uKa_executionBadge[data-result=failed]{color:var(--dsw-alias-state-error-primary);background:0 0}._7D6uKa_executionBadge[data-result=cancelled]{color:var(--dsw-alias-label-tertiary);background:0 0}._7D6uKa_executionTimes{color:var(--dsw-alias-label-secondary);font-size:12px}._7D6uKa_executionError{width:100%;color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;font-size:12px}._7D6uKa_moveRow{flex-wrap:wrap;gap:8px;display:flex}._7D6uKa_detailFooter{border-top:1px solid var(--dsw-alias-separator-primary);flex:none;align-items:center;gap:10px;padding:12px 18px;display:flex}._7D6uKa_detailMeta{color:var(--dsw-alias-label-tertiary);margin-left:auto;font-size:11px}@container _7D6uKa_task-board-view (width<=768px){._7D6uKa_board{gap:10px;padding:10px}._7D6uKa_boardHeader{flex-wrap:wrap;align-items:center;gap:8px}._7D6uKa_backButton{flex:none;order:1}._7D6uKa_boardTitle{flex:auto;order:2}._7D6uKa_boardHeader>._7D6uKa_detailMeta{flex:1 0 100%;order:3;margin-left:0}._7D6uKa_search{flex:1 0 100%;order:4;min-width:0}._7D6uKa_boardHeader>button:not(._7D6uKa_backButton){flex:1 1 0;order:5;min-width:0}._7D6uKa_columns{scroll-snap-type:inline mandatory;scrollbar-width:none;-webkit-overflow-scrolling:touch;grid-auto-columns:86cqw;gap:10px;padding-inline:2px 14cqw;scroll-padding-inline:2px}._7D6uKa_columns::-webkit-scrollbar{display:none}._7D6uKa_column{scroll-snap-align:start;scroll-snap-stop:always}}@container _7D6uKa_task-board-view (width<=720px){._7D6uKa_boardHeader>._7D6uKa_detailMeta{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}}@container _7D6uKa_task-board-view (width<=600px){._7D6uKa_board{padding-inline:8px}}@media (width<=768px){[data-dsh-taskboard-view]{height:100dvh}._7D6uKa_entry,._7D6uKa_card,._7D6uKa_primaryButton,._7D6uKa_ghostButton,._7D6uKa_dangerButton,._7D6uKa_iconButton,._7D6uKa_linkButton,._7D6uKa_search,._7D6uKa_input,._7D6uKa_select,._7D6uKa_schedulePreset,._7D6uKa_scheduleToggle{min-height:44px}._7D6uKa_search,._7D6uKa_input,._7D6uKa_select,._7D6uKa_schedulePreset{box-sizing:border-box;font-size:16px}._7D6uKa_modalBackdrop{justify-content:stretch;align-items:stretch;width:100vw;height:100dvh}._7D6uKa_modal,._7D6uKa_detail{box-sizing:border-box;border:0;border-radius:0;width:100vw;height:100dvh;max-height:none}._7D6uKa_modal{padding-top:max(16px, env(safe-area-inset-top));padding-right:max(16px, env(safe-area-inset-right));padding-bottom:max(16px, env(safe-area-inset-bottom));padding-left:max(16px, env(safe-area-inset-left))}._7D6uKa_modalFooter{z-index:1;background:var(--dsw-alias-bg-base);flex-wrap:wrap;padding-top:8px;position:sticky;bottom:0}._7D6uKa_modalFooter>button{flex:120px}._7D6uKa_detailHeader{padding-top:max(12px, env(safe-area-inset-top));padding-right:max(14px, env(safe-area-inset-right));padding-left:max(14px, env(safe-area-inset-left));flex-wrap:wrap}._7D6uKa_detailTitle{min-width:0}._7D6uKa_detailBody{overscroll-behavior-y:contain;padding-right:max(14px, env(safe-area-inset-right));padding-left:max(14px, env(safe-area-inset-left))}._7D6uKa_detailFooter{padding-right:max(14px, env(safe-area-inset-right));padding-bottom:max(12px, env(safe-area-inset-bottom));padding-left:max(14px, env(safe-area-inset-left));flex-wrap:wrap}._7D6uKa_detailFooter>button{flex:96px}._7D6uKa_detailFooter>._7D6uKa_detailMeta{text-align:end;flex:1 0 100%;margin-left:0}._7D6uKa_scheduleRow{flex-direction:column;align-items:stretch}._7D6uKa_schedulePreset{width:100%}}._7D6uKa_entry:focus-visible,._7D6uKa_card:focus-visible,._7D6uKa_primaryButton:focus-visible,._7D6uKa_ghostButton:focus-visible,._7D6uKa_dangerButton:focus-visible,._7D6uKa_iconButton:focus-visible,._7D6uKa_linkButton:focus-visible,._7D6uKa_search:focus-visible,._7D6uKa_input:focus-visible,._7D6uKa_select:focus-visible,._7D6uKa_schedulePreset:focus-visible,._7D6uKa_scheduleToggle input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}._7D6uKa_entry,._7D6uKa_primaryButton,._7D6uKa_ghostButton,._7D6uKa_dangerButton,._7D6uKa_iconButton,._7D6uKa_linkButton,._7D6uKa_search,._7D6uKa_input,._7D6uKa_select,._7D6uKa_schedulePreset,._7D6uKa_scheduleToggle input{transition:background-color .12s,color .12s,border-color .12s,outline-color .12s,box-shadow .12s,transform .12s}._7D6uKa_card:active{box-shadow:var(--dsw-shadow-lv1);transform:translateY(0)}._7D6uKa_entry:active,._7D6uKa_primaryButton:active:not(:disabled),._7D6uKa_ghostButton:active:not(:disabled),._7D6uKa_dangerButton:active:not(:disabled),._7D6uKa_iconButton:active:not(:disabled),._7D6uKa_linkButton:active:not(:disabled){transform:translateY(1px)}._7D6uKa_entry[data-active]:hover{background:var(--dsw-specific-sidebar-nav-item-active)}._7D6uKa_iconButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}._7D6uKa_linkButton:hover:not(:disabled){text-decoration:underline}._7D6uKa_iconButton:disabled,._7D6uKa_linkButton:disabled{opacity:.45;cursor:default}._7D6uKa_search:focus,._7D6uKa_select:focus,._7D6uKa_schedulePreset:focus{border-color:var(--dsw-alias-state-business-primary)}._7D6uKa_scheduleToggle input{margin:0}@media (prefers-reduced-motion:reduce){._7D6uKa_entry,._7D6uKa_card,._7D6uKa_primaryButton,._7D6uKa_ghostButton,._7D6uKa_dangerButton,._7D6uKa_iconButton,._7D6uKa_linkButton,._7D6uKa_search,._7D6uKa_input,._7D6uKa_select,._7D6uKa_schedulePreset,._7D6uKa_scheduleToggle input{transition:none}._7D6uKa_cardSpinner{animation:none}}._7D6uKa_cardTags{flex-wrap:wrap;gap:4px;display:flex}._7D6uKa_cardTag{border:1px solid var(--dsh-task-tag-border);background:var(--dsh-task-tag-fill);max-width:100%;color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;border-radius:999px;padding:0 7px;font-size:10px;line-height:16px;overflow:hidden}._7D6uKa_tagFilter{flex-wrap:wrap;align-items:center;gap:6px;margin:0 0 10px;display:flex}._7D6uKa_tagFilterLabel{color:var(--dsw-alias-label-tertiary);font-size:11px}._7D6uKa_tagChip{border:1px solid var(--dsh-task-tag-border);color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border-radius:999px;padding:1px 9px;font-family:inherit;font-size:11px;line-height:18px}._7D6uKa_tagChip[data-active=true]{background:var(--dsh-task-tag-fill);color:var(--dsw-alias-label-primary)}._7D6uKa_cardTag[data-tag-tone=\"0\"],._7D6uKa_tagChip[data-tag-tone=\"0\"]{--dsh-task-tag-fill:#4e93e82e;--dsh-task-tag-border:#4e93e866}._7D6uKa_cardTag[data-tag-tone=\"1\"],._7D6uKa_tagChip[data-tag-tone=\"1\"]{--dsh-task-tag-fill:#2ea36a2e;--dsh-task-tag-border:#2ea36a66}._7D6uKa_cardTag[data-tag-tone=\"2\"],._7D6uKa_tagChip[data-tag-tone=\"2\"]{--dsh-task-tag-fill:#d08a2a2e;--dsh-task-tag-border:#d08a2a66}._7D6uKa_cardTag[data-tag-tone=\"3\"],._7D6uKa_tagChip[data-tag-tone=\"3\"]{--dsh-task-tag-fill:#b456c82e;--dsh-task-tag-border:#b456c866}._7D6uKa_cardTag[data-tag-tone=\"4\"],._7D6uKa_tagChip[data-tag-tone=\"4\"]{--dsh-task-tag-fill:#cf5f7a2e;--dsh-task-tag-border:#cf5f7a66}._7D6uKa_cardTag[data-tag-tone=\"5\"],._7D6uKa_tagChip[data-tag-tone=\"5\"]{--dsh-task-tag-fill:#4a9fb52e;--dsh-task-tag-border:#4a9fb566}._7D6uKa_fieldHint{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.4}._7D6uKa_tagRow{align-items:center;gap:6px;display:flex}._7D6uKa_tagRow ._7D6uKa_input{flex:1 1 0;min-width:0}._7D6uKa_tagRow ._7D6uKa_ghostButton{flex:none}._7D6uKa_tagAddButton{align-self:flex-start}._7D6uKa_projectFilter{flex:none;align-items:center;gap:6px;display:flex}._7D6uKa_projectFilterLabel{color:var(--dsw-alias-label-secondary);white-space:nowrap;font-size:12px}._7D6uKa_projectDialog{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;flex-direction:column;flex:none;gap:8px;margin-bottom:8px;padding:10px 12px;display:flex}._7D6uKa_projectDialogActions{justify-content:flex-end;gap:8px;display:flex}._7D6uKa_aiParse{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;flex-direction:column;gap:6px;padding:10px 12px;display:flex}._7D6uKa_aiParseRow{align-items:center;gap:8px;display:flex}._7D6uKa_aiParseRow ._7D6uKa_select{flex:1 1 0;min-width:0}._7D6uKa_aiParseRow ._7D6uKa_ghostButton,._7D6uKa_aiParseRow ._7D6uKa_primaryButton{flex:none}._7D6uKa_cardSubtask{border:1px solid var(--dsw-alias-border-l2);max-width:100%;color:var(--dsw-alias-label-secondary);text-overflow:ellipsis;white-space:nowrap;background:0 0;border-radius:999px;padding:0 7px;font-size:10px;line-height:16px;overflow:hidden}._7D6uKa_cardSubtask[data-tone=failed]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}._7D6uKa_cardSubtask[data-tone=running]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}._7D6uKa_cardSubtask[data-tone=done]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}._7D6uKa_subtaskList{flex-direction:column;gap:6px;margin:0;padding:0;list-style:none;display:flex}._7D6uKa_subtaskRow{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;flex-wrap:wrap;align-items:center;gap:10px;padding:6px 10px;display:flex}._7D6uKa_subtaskRow ._7D6uKa_linkButton:first-child{text-align:left;overflow-wrap:anywhere;flex:auto;min-width:0}._7D6uKa_subtaskAddRow{flex-wrap:wrap;gap:8px;display:flex}._7D6uKa_pickList{flex-direction:column;gap:6px;max-height:320px;margin:0;padding:0;list-style:none;display:flex;overflow-y:auto}._7D6uKa_pickRow{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;align-items:center;gap:10px;padding:6px 10px;display:flex}._7D6uKa_pickTitle{min-width:0;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere;flex:auto;font-size:13px}";
2652
+ const css$1 = "[data-dsh-taskboard-view]{box-sizing:border-box;background:var(--dsw-alias-bg-base);flex-direction:column;width:100%;min-width:0;height:100%;min-height:0;display:flex;container:_7D6uKa_task-board-view/inline-size}._7D6uKa_board{box-sizing:border-box;background:var(--dsw-alias-bg-base);min-width:0;height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex-direction:column;gap:12px;padding:14px 16px 16px;display:flex}._7D6uKa_boardHeader{flex:none;align-items:center;gap:10px;display:flex}._7D6uKa_boardTitle{color:var(--dsw-alias-label-primary);white-space:nowrap;margin:0;font-size:16px;font-weight:700}._7D6uKa_backButton{align-items:center;gap:4px;display:inline-flex}._7D6uKa_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}._7D6uKa_search::placeholder{color:var(--dsw-alias-label-tertiary)}._7D6uKa_columns{overscroll-behavior-inline:contain;scrollbar-color:var(--dsw-alias-border-l3) var(--dsw-alias-interactive-bg-hover);scrollbar-width:thin;flex:1;grid-auto-columns:minmax(220px,1fr);grid-auto-flow:column;gap:12px;min-height:0;padding-bottom:6px;display:grid;overflow:auto hidden}._7D6uKa_columns::-webkit-scrollbar{height:10px}._7D6uKa_columns::-webkit-scrollbar-track{background:var(--dsw-alias-interactive-bg-hover);border-radius:999px}._7D6uKa_columns::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l3);background-clip:content-box;border:2px solid #0000;border-radius:999px}._7D6uKa_columns::-webkit-scrollbar-thumb:hover{background:var(--dsw-alias-border-l4);background-clip:content-box}._7D6uKa_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}._7D6uKa_columnHeader{flex:none;align-items:center;gap:6px;padding:10px 12px;display:flex}._7D6uKa_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}._7D6uKa_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}._7D6uKa_statusDot{border-radius:50%;flex:none;width:8px;height:8px}._7D6uKa_statusDot[data-status=backlog]{background:var(--dsw-alias-label-tertiary)}._7D6uKa_statusDot[data-status=todo]{background:var(--dsw-alias-state-business-primary)}._7D6uKa_statusDot[data-status=running]{background:var(--dsw-alias-state-warn-primary)}._7D6uKa_statusDot[data-status=done]{background:var(--dsw-alias-state-success-primary)}._7D6uKa_statusDot[data-status=failed]{background:var(--dsw-alias-state-error-primary)}._7D6uKa_cards{flex-direction:column;flex:1;gap:8px;min-height:0;padding:2px 8px 10px;display:flex;overflow-y:auto}._7D6uKa_columnEmpty{text-align:center;color:var(--dsw-alias-label-tertiary);padding:24px 8px;font-size:12px}._7D6uKa_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}._7D6uKa_card:hover{box-shadow:var(--dsw-shadow-lv2);border-color:var(--dsw-alias-border-l3);transform:translateY(-1px)}._7D6uKa_card[data-status=running]{border-color:var(--dsw-alias-state-warn-primary)}._7D6uKa_cardTitle{-webkit-line-clamp:2;-webkit-box-orient:vertical;font-size:13px;font-weight:600;line-height:1.35;display:-webkit-box;overflow:hidden}._7D6uKa_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}._7D6uKa_cardMeta{color:var(--dsw-alias-label-tertiary);align-items:center;gap:8px;font-size:11px;display:flex}._7D6uKa_cardTime{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}._7D6uKa_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}._7D6uKa_cardRun{flex:none}._7D6uKa_cardRun[data-result=failed]{color:var(--dsw-alias-state-error-primary)}._7D6uKa_cardRun[data-result=succeeded]{color:var(--dsw-alias-state-success-primary)}._7D6uKa_cardSession{color:var(--dsw-alias-state-business-primary);flex:none}._7D6uKa_cardRunningLabel{color:var(--dsw-alias-state-warn-primary);font-size:11px}._7D6uKa_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 _7D6uKa_dshTbSpin}@keyframes _7D6uKa_dshTbSpin{to{transform:rotate(360deg)}}._7D6uKa_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}._7D6uKa_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-info-hover)}._7D6uKa_primaryButton:disabled{opacity:.5;cursor:default}._7D6uKa_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}._7D6uKa_ghostButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}._7D6uKa_ghostButton:disabled{opacity:.45;cursor:default}._7D6uKa_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}._7D6uKa_dangerButton:hover:not(:disabled){filter:brightness(1.08)}._7D6uKa_dangerButton:active:not(:disabled){filter:brightness(.94)}._7D6uKa_dangerButton:disabled{opacity:.5;cursor:default}._7D6uKa_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}._7D6uKa_iconButton:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}._7D6uKa_linkButton{color:var(--dsw-alias-state-business-primary);cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:0;font-size:12px}._7D6uKa_linkButton:hover{text-decoration:underline}._7D6uKa_modalBackdrop{z-index:1300;background:var(--dsw-alias-bg-mask-1);justify-content:center;align-items:center;display:flex;position:fixed;inset:0}._7D6uKa_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:10px;padding:18px;display:flex;overflow:hidden}._7D6uKa_modalTitle{margin:0;font-size:15px;font-weight:700}._7D6uKa_confirmMessage{color:var(--dsw-alias-label-secondary);white-space:pre-wrap;overflow-wrap:anywhere;margin:0;font-size:13px;line-height:1.5}._7D6uKa_modalFooter{justify-content:flex-end;gap:10px;margin-top:4px;display:flex}._7D6uKa_modalBody{flex-direction:column;gap:8px;min-height:0;display:flex;overflow-y:auto}._7D6uKa_modalBody>*{flex:none}._7D6uKa_formSection{border:1px solid var(--dsw-alias-separator-primary);border-radius:10px;flex-direction:column;display:flex;overflow:hidden}._7D6uKa_formSectionHeader{width:100%;color:var(--dsw-alias-label-primary);text-align:left;cursor:pointer;background:0 0;border:0;align-items:center;gap:8px;padding:7px 10px;font-family:inherit;font-size:12.5px;font-weight:600;display:flex}._7D6uKa_formSectionHeader:hover{background:var(--dsw-alias-bg-mask-1)}._7D6uKa_formSectionChevron{border-top:4px solid #0000;border-bottom:4px solid #0000;border-left:5px solid var(--dsw-alias-label-tertiary);flex:none;width:0;height:0;margin-left:1px;transition:transform .12s}._7D6uKa_formSection[data-open=true] ._7D6uKa_formSectionChevron{transform:rotate(90deg)}._7D6uKa_formSectionTitle{flex:none}._7D6uKa_formSectionSummary{min-width:0;color:var(--dsw-alias-label-tertiary);text-align:right;text-overflow:ellipsis;white-space:nowrap;flex:auto;font-weight:400;overflow:hidden}._7D6uKa_formSectionBody{border-top:1px solid var(--dsw-alias-separator-primary);flex-direction:column;gap:8px;padding:8px 10px 10px;display:flex}._7D6uKa_field{flex-direction:column;gap:5px;display:flex}._7D6uKa_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}._7D6uKa_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}._7D6uKa_input:focus{border-color:var(--dsw-alias-state-business-primary)}._7D6uKa_select{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;max-width:100%;padding:7px 10px;font-family:inherit;font-size:13px}._7D6uKa_input::placeholder{color:var(--dsw-alias-label-tertiary)}._7D6uKa_formError{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}._7D6uKa_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}._7D6uKa_detailHeader{border-bottom:1px solid var(--dsw-alias-separator-primary);flex:none;align-items:center;gap:10px;padding:14px 18px;display:flex}._7D6uKa_detailTitle{overflow-wrap:anywhere;flex:1;margin:0;font-size:15px;font-weight:700}._7D6uKa_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}._7D6uKa_statusBadge[data-status=running]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}._7D6uKa_statusBadge[data-status=done]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}._7D6uKa_statusBadge[data-status=failed]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}._7D6uKa_detailBody{flex-direction:column;flex:1;gap:16px;padding:14px 18px;display:flex;overflow-y:auto}._7D6uKa_detailSection{flex-direction:column;gap:6px;display:flex}._7D6uKa_detailSection h4{color:var(--dsw-alias-label-tertiary);text-transform:none;margin:0;font-size:12px;font-weight:700}._7D6uKa_detailText{color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere;margin:0;font-size:13px;line-height:1.55}._7D6uKa_scheduleToggle{color:var(--dsw-alias-label-primary);cursor:pointer;user-select:none;align-items:center;gap:8px;font-size:13px;display:flex}._7D6uKa_scheduleToggle input{accent-color:var(--dsw-alias-state-business-primary)}._7D6uKa_scheduleRow{align-items:center;gap:8px;display:flex}._7D6uKa_scheduleInput{min-width:0;font-family:var(--dsw-font-markdown-code-block-small);flex:1;font-size:12.5px}._7D6uKa_scheduleInputInvalid,._7D6uKa_scheduleInputInvalid:focus{border-color:var(--dsw-alias-state-error-primary)}._7D6uKa_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}._7D6uKa_scheduleZone{color:var(--dsw-alias-label-secondary);align-items:center;gap:8px;font-size:12.5px;display:flex}._7D6uKa_scheduleZone select{flex:1;min-width:0}._7D6uKa_scheduleMeta{color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;margin:0;font-size:12px}._7D6uKa_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}._7D6uKa_executionList{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}._7D6uKa_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}._7D6uKa_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}._7D6uKa_executionBadge[data-result=succeeded]{color:var(--dsw-alias-state-success-primary);background:0 0}._7D6uKa_executionBadge[data-result=failed]{color:var(--dsw-alias-state-error-primary);background:0 0}._7D6uKa_executionBadge[data-result=cancelled]{color:var(--dsw-alias-label-tertiary);background:0 0}._7D6uKa_executionTimes{color:var(--dsw-alias-label-secondary);font-size:12px}._7D6uKa_executionError{width:100%;color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;font-size:12px}._7D6uKa_moveRow{flex-wrap:wrap;gap:8px;display:flex}._7D6uKa_detailFooter{border-top:1px solid var(--dsw-alias-separator-primary);flex:none;align-items:center;gap:10px;padding:12px 18px;display:flex}._7D6uKa_detailMeta{color:var(--dsw-alias-label-tertiary);margin-left:auto;font-size:11px}@container _7D6uKa_task-board-view (width<=768px){._7D6uKa_board{gap:10px;padding:10px}._7D6uKa_boardHeader{flex-wrap:wrap;align-items:center;gap:8px}._7D6uKa_backButton{flex:none;order:1}._7D6uKa_boardTitle{flex:auto;order:2}._7D6uKa_boardHeader>._7D6uKa_detailMeta{flex:1 0 100%;order:3;margin-left:0}._7D6uKa_search{flex:1 0 100%;order:4;min-width:0}._7D6uKa_boardHeader>button:not(._7D6uKa_backButton){flex:1 1 0;order:5;min-width:0}._7D6uKa_columns{scroll-snap-type:inline mandatory;scrollbar-width:none;-webkit-overflow-scrolling:touch;grid-auto-columns:86cqw;gap:10px;padding-inline:2px 14cqw;scroll-padding-inline:2px}._7D6uKa_columns::-webkit-scrollbar{display:none}._7D6uKa_column{scroll-snap-align:start;scroll-snap-stop:always}}@container _7D6uKa_task-board-view (width<=720px){._7D6uKa_boardHeader>._7D6uKa_detailMeta{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}}@container _7D6uKa_task-board-view (width<=600px){._7D6uKa_board{padding-inline:8px}}@media (width<=768px){[data-dsh-taskboard-view]{height:100dvh}._7D6uKa_card,._7D6uKa_primaryButton,._7D6uKa_ghostButton,._7D6uKa_dangerButton,._7D6uKa_iconButton,._7D6uKa_linkButton,._7D6uKa_search,._7D6uKa_input,._7D6uKa_select,._7D6uKa_schedulePreset,._7D6uKa_scheduleToggle{min-height:44px}._7D6uKa_search,._7D6uKa_input,._7D6uKa_select,._7D6uKa_schedulePreset{box-sizing:border-box;font-size:16px}._7D6uKa_modalBackdrop{justify-content:stretch;align-items:stretch;width:100vw;height:100dvh}._7D6uKa_modal,._7D6uKa_detail{box-sizing:border-box;border:0;border-radius:0;width:100vw;height:100dvh;max-height:none}._7D6uKa_modal{padding-top:max(16px, env(safe-area-inset-top));padding-right:max(16px, env(safe-area-inset-right));padding-bottom:max(16px, env(safe-area-inset-bottom));padding-left:max(16px, env(safe-area-inset-left))}._7D6uKa_modalFooter{z-index:1;background:var(--dsw-alias-bg-base);flex-wrap:wrap;padding-top:8px;position:sticky;bottom:0}._7D6uKa_modalFooter>button{flex:120px}._7D6uKa_detailHeader{padding-top:max(12px, env(safe-area-inset-top));padding-right:max(14px, env(safe-area-inset-right));padding-left:max(14px, env(safe-area-inset-left));flex-wrap:wrap}._7D6uKa_detailTitle{min-width:0}._7D6uKa_detailBody{overscroll-behavior-y:contain;padding-right:max(14px, env(safe-area-inset-right));padding-left:max(14px, env(safe-area-inset-left))}._7D6uKa_detailFooter{padding-right:max(14px, env(safe-area-inset-right));padding-bottom:max(12px, env(safe-area-inset-bottom));padding-left:max(14px, env(safe-area-inset-left));flex-wrap:wrap}._7D6uKa_detailFooter>button{flex:96px}._7D6uKa_detailFooter>._7D6uKa_detailMeta{text-align:end;flex:1 0 100%;margin-left:0}._7D6uKa_scheduleRow{flex-direction:column;align-items:stretch}._7D6uKa_schedulePreset{width:100%}}._7D6uKa_card:focus-visible,._7D6uKa_primaryButton:focus-visible,._7D6uKa_ghostButton:focus-visible,._7D6uKa_dangerButton:focus-visible,._7D6uKa_iconButton:focus-visible,._7D6uKa_linkButton:focus-visible,._7D6uKa_search:focus-visible,._7D6uKa_input:focus-visible,._7D6uKa_select:focus-visible,._7D6uKa_schedulePreset:focus-visible,._7D6uKa_scheduleToggle input:focus-visible,._7D6uKa_formSectionHeader:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}._7D6uKa_primaryButton,._7D6uKa_ghostButton,._7D6uKa_dangerButton,._7D6uKa_iconButton,._7D6uKa_linkButton,._7D6uKa_search,._7D6uKa_input,._7D6uKa_select,._7D6uKa_schedulePreset,._7D6uKa_scheduleToggle input,._7D6uKa_formSectionHeader,._7D6uKa_formSectionChevron{transition:background-color .12s,color .12s,border-color .12s,outline-color .12s,box-shadow .12s,transform .12s}._7D6uKa_card:active{box-shadow:var(--dsw-shadow-lv1);transform:translateY(0)}._7D6uKa_primaryButton:active:not(:disabled),._7D6uKa_ghostButton:active:not(:disabled),._7D6uKa_dangerButton:active:not(:disabled),._7D6uKa_iconButton:active:not(:disabled),._7D6uKa_linkButton:active:not(:disabled){transform:translateY(1px)}._7D6uKa_iconButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}._7D6uKa_linkButton:hover:not(:disabled){text-decoration:underline}._7D6uKa_iconButton:disabled,._7D6uKa_linkButton:disabled{opacity:.45;cursor:default}._7D6uKa_search:focus,._7D6uKa_select:focus,._7D6uKa_schedulePreset:focus{border-color:var(--dsw-alias-state-business-primary)}._7D6uKa_scheduleToggle input{margin:0}@media (prefers-reduced-motion:reduce){._7D6uKa_card,._7D6uKa_primaryButton,._7D6uKa_ghostButton,._7D6uKa_dangerButton,._7D6uKa_iconButton,._7D6uKa_linkButton,._7D6uKa_search,._7D6uKa_input,._7D6uKa_select,._7D6uKa_schedulePreset,._7D6uKa_scheduleToggle input,._7D6uKa_formSectionHeader,._7D6uKa_formSectionChevron{transition:none}._7D6uKa_cardSpinner{animation:none}}._7D6uKa_cardTags{flex-wrap:wrap;gap:4px;display:flex}._7D6uKa_cardTag{border:1px solid var(--dsh-task-tag-border);background:var(--dsh-task-tag-fill);max-width:100%;color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;border-radius:999px;padding:0 7px;font-size:10px;line-height:16px;overflow:hidden}._7D6uKa_tagFilter{flex-wrap:wrap;align-items:center;gap:6px;margin:0 0 10px;display:flex}._7D6uKa_tagFilterLabel{color:var(--dsw-alias-label-tertiary);font-size:11px}._7D6uKa_tagChip{border:1px solid var(--dsh-task-tag-border);color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border-radius:999px;padding:1px 9px;font-family:inherit;font-size:11px;line-height:18px}._7D6uKa_tagChip[data-active=true]{background:var(--dsh-task-tag-fill);color:var(--dsw-alias-label-primary)}._7D6uKa_cardTag[data-tag-tone=\"0\"],._7D6uKa_tagChip[data-tag-tone=\"0\"]{--dsh-task-tag-fill:#4e93e82e;--dsh-task-tag-border:#4e93e866}._7D6uKa_cardTag[data-tag-tone=\"1\"],._7D6uKa_tagChip[data-tag-tone=\"1\"]{--dsh-task-tag-fill:#2ea36a2e;--dsh-task-tag-border:#2ea36a66}._7D6uKa_cardTag[data-tag-tone=\"2\"],._7D6uKa_tagChip[data-tag-tone=\"2\"]{--dsh-task-tag-fill:#d08a2a2e;--dsh-task-tag-border:#d08a2a66}._7D6uKa_cardTag[data-tag-tone=\"3\"],._7D6uKa_tagChip[data-tag-tone=\"3\"]{--dsh-task-tag-fill:#b456c82e;--dsh-task-tag-border:#b456c866}._7D6uKa_cardTag[data-tag-tone=\"4\"],._7D6uKa_tagChip[data-tag-tone=\"4\"]{--dsh-task-tag-fill:#cf5f7a2e;--dsh-task-tag-border:#cf5f7a66}._7D6uKa_cardTag[data-tag-tone=\"5\"],._7D6uKa_tagChip[data-tag-tone=\"5\"]{--dsh-task-tag-fill:#4a9fb52e;--dsh-task-tag-border:#4a9fb566}._7D6uKa_fieldHint{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.4}._7D6uKa_tagRow{align-items:center;gap:6px;display:flex}._7D6uKa_tagRow ._7D6uKa_input{flex:1 1 0;min-width:0}._7D6uKa_tagRow ._7D6uKa_ghostButton{flex:none}._7D6uKa_tagAddButton{align-self:flex-start}._7D6uKa_projectFilter{flex:none;align-items:center;gap:6px;display:flex}._7D6uKa_projectFilterLabel{color:var(--dsw-alias-label-secondary);white-space:nowrap;font-size:12px}._7D6uKa_projectDialog{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;flex-direction:column;flex:none;gap:8px;margin-bottom:8px;padding:10px 12px;display:flex}._7D6uKa_projectDialogActions{justify-content:flex-end;gap:8px;display:flex}._7D6uKa_aiParse{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;flex-direction:column;gap:6px;padding:10px 12px;display:flex}._7D6uKa_aiParseRow{align-items:center;gap:8px;display:flex}._7D6uKa_aiParseRow ._7D6uKa_select{flex:1 1 0;min-width:0}._7D6uKa_aiParseRow ._7D6uKa_ghostButton,._7D6uKa_aiParseRow ._7D6uKa_primaryButton{flex:none}._7D6uKa_cardSubtask{border:1px solid var(--dsw-alias-border-l2);max-width:100%;color:var(--dsw-alias-label-secondary);text-overflow:ellipsis;white-space:nowrap;background:0 0;border-radius:999px;padding:0 7px;font-size:10px;line-height:16px;overflow:hidden}._7D6uKa_cardSubtask[data-tone=failed]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}._7D6uKa_cardSubtask[data-tone=running]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}._7D6uKa_cardSubtask[data-tone=done]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}._7D6uKa_subtaskList{flex-direction:column;gap:6px;margin:0;padding:0;list-style:none;display:flex}._7D6uKa_subtaskRow{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;flex-wrap:wrap;align-items:center;gap:10px;padding:6px 10px;display:flex}._7D6uKa_subtaskRow ._7D6uKa_linkButton:first-child{text-align:left;overflow-wrap:anywhere;flex:auto;min-width:0}._7D6uKa_subtaskAddRow{flex-wrap:wrap;gap:8px;display:flex}._7D6uKa_pickList{flex-direction:column;gap:6px;max-height:320px;margin:0;padding:0;list-style:none;display:flex;overflow-y:auto}._7D6uKa_pickRow{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;align-items:center;gap:10px;padding:6px 10px;display:flex}._7D6uKa_pickTitle{min-width:0;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere;flex:auto;font-size:13px}";
2391
2653
  const tagId$1 = "@linxin666/dsh-client-ui-task-board/packages/dsh-task-board/src/client/board.module.css";
2392
2654
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
2393
2655
  const tag = document.createElement("style");
@@ -2434,9 +2696,6 @@ window.__ModuleLoader__.load({
2434
2696
  "detailText": "_7D6uKa_detailText",
2435
2697
  "detailTitle": "_7D6uKa_detailTitle",
2436
2698
  "dshTbSpin": "_7D6uKa_dshTbSpin",
2437
- "entry": "_7D6uKa_entry",
2438
- "entryIcon": "_7D6uKa_entryIcon",
2439
- "entryLabel": "_7D6uKa_entryLabel",
2440
2699
  "executionBadge": "_7D6uKa_executionBadge",
2441
2700
  "executionError": "_7D6uKa_executionError",
2442
2701
  "executionList": "_7D6uKa_executionList",
@@ -2446,12 +2705,19 @@ window.__ModuleLoader__.load({
2446
2705
  "fieldHint": "_7D6uKa_fieldHint",
2447
2706
  "fieldLabel": "_7D6uKa_fieldLabel",
2448
2707
  "formError": "_7D6uKa_formError",
2708
+ "formSection": "_7D6uKa_formSection",
2709
+ "formSectionBody": "_7D6uKa_formSectionBody",
2710
+ "formSectionChevron": "_7D6uKa_formSectionChevron",
2711
+ "formSectionHeader": "_7D6uKa_formSectionHeader",
2712
+ "formSectionSummary": "_7D6uKa_formSectionSummary",
2713
+ "formSectionTitle": "_7D6uKa_formSectionTitle",
2449
2714
  "ghostButton": "_7D6uKa_ghostButton",
2450
2715
  "iconButton": "_7D6uKa_iconButton",
2451
2716
  "input": "_7D6uKa_input",
2452
2717
  "linkButton": "_7D6uKa_linkButton",
2453
2718
  "modal": "_7D6uKa_modal",
2454
2719
  "modalBackdrop": "_7D6uKa_modalBackdrop",
2720
+ "modalBody": "_7D6uKa_modalBody",
2455
2721
  "modalFooter": "_7D6uKa_modalFooter",
2456
2722
  "modalTitle": "_7D6uKa_modalTitle",
2457
2723
  "moveRow": "_7D6uKa_moveRow",
@@ -2470,6 +2736,7 @@ window.__ModuleLoader__.load({
2470
2736
  "schedulePreset": "_7D6uKa_schedulePreset",
2471
2737
  "scheduleRow": "_7D6uKa_scheduleRow",
2472
2738
  "scheduleToggle": "_7D6uKa_scheduleToggle",
2739
+ "scheduleZone": "_7D6uKa_scheduleZone",
2473
2740
  "search": "_7D6uKa_search",
2474
2741
  "select": "_7D6uKa_select",
2475
2742
  "statusBadge": "_7D6uKa_statusBadge",
@@ -2506,7 +2773,128 @@ window.__ModuleLoader__.load({
2506
2773
  }
2507
2774
  ];
2508
2775
  //#endregion
2776
+ //#region src/client/schedule-zone.ts
2777
+ /**
2778
+ * Shared scheduling helpers for the board's schedule editors (the new-task
2779
+ * dialog and the task detail panel): the time-zone picker inventory, the
2780
+ * relative next-run wording, and the combined absolute + relative label.
2781
+ *
2782
+ * Everything here is browser-side presentation built on the same pure engine
2783
+ * the Host schedules with (`core/schedule.ts`), so the preview a user sees is
2784
+ * computed by the code that will actually arm the rule.
2785
+ */
2786
+ /**
2787
+ * The runtime's zone inventory, read once. `Intl.supportedValuesOf` is not
2788
+ * available in every engine, so a missing inventory degrades to `UTC` rather
2789
+ * than offering a list that cannot resolve.
2790
+ *
2791
+ * The inventory is deliberately NOT truncated. A capped list silently omits
2792
+ * whole regions, and because a `<select>` whose value matches no option falls
2793
+ * back to its first option, an omitted zone would make the control show — and
2794
+ * then save — a different zone than the rule actually stores.
2795
+ */
2796
+ let inventoryCache;
2797
+ function zoneInventory() {
2798
+ if (inventoryCache !== void 0) return inventoryCache;
2799
+ let inventory = [];
2800
+ try {
2801
+ inventory = typeof Intl.supportedValuesOf === "function" ? Intl.supportedValuesOf("timeZone") : [];
2802
+ } catch {
2803
+ inventory = [];
2804
+ }
2805
+ inventoryCache = inventory.length > 0 ? inventory : ["UTC"];
2806
+ return inventoryCache;
2807
+ }
2808
+ /**
2809
+ * Zones offered by the picker: the Host zone first (the default, and the entry
2810
+ * a rule with no stored zone follows), then the runtime's own inventory.
2811
+ *
2812
+ * The Host entry is always present, even before the Host snapshot reports a
2813
+ * zone: it is the entry that stores no zone, so dropping it would leave a rule
2814
+ * unable to express "follow the Host" and would let the control silently
2815
+ * display an unrelated zone.
2816
+ *
2817
+ * A stored zone the inventory does not list (a rule written on another machine
2818
+ * or under a different ICU build) is appended, so opening the editor can never
2819
+ * silently rewrite the rule's zone.
2820
+ * @param hostTimeZone - zone the Host reported for this deployment, when known.
2821
+ * @param storedTimeZone - zone the edited rule already stores, when any.
2822
+ * @returns the de-duplicated choice list.
2823
+ */
2824
+ function zoneChoices(hostTimeZone, storedTimeZone) {
2825
+ const host = hostTimeZone !== void 0 && isValidTimeZone(hostTimeZone) ? hostTimeZone : void 0;
2826
+ const choices = [];
2827
+ const seen = /* @__PURE__ */ new Set();
2828
+ choices.push({
2829
+ id: "",
2830
+ label: host === void 0 ? t("detail.schedule.timeZoneHostUnknown") : t("detail.schedule.timeZoneHost", { timeZone: host })
2831
+ });
2832
+ if (host !== void 0) seen.add(host);
2833
+ for (const zone of zoneInventory()) {
2834
+ if (seen.has(zone) || !isValidTimeZone(zone)) continue;
2835
+ seen.add(zone);
2836
+ choices.push({
2837
+ id: zone,
2838
+ label: zone
2839
+ });
2840
+ }
2841
+ if (storedTimeZone !== void 0 && !seen.has(storedTimeZone) && isValidTimeZone(storedTimeZone)) choices.push({
2842
+ id: storedTimeZone,
2843
+ label: storedTimeZone
2844
+ });
2845
+ return choices;
2846
+ }
2847
+ /** Compact duration parts, largest unit first, with at most two units kept. */
2848
+ function durationParts(ms) {
2849
+ const total = Math.max(0, Math.floor(ms / 1e3));
2850
+ const days = Math.floor(total / 86400);
2851
+ const hours = Math.floor(total % 86400 / 3600);
2852
+ const minutes = Math.floor(total % 3600 / 60);
2853
+ const seconds = total % 60;
2854
+ const parts = [];
2855
+ if (days > 0) parts.push(t("detail.schedule.duration.days", { count: String(days) }));
2856
+ if (hours > 0) parts.push(t("detail.schedule.duration.hours", { count: String(hours) }));
2857
+ if (minutes > 0) parts.push(t("detail.schedule.duration.minutes", { count: String(minutes) }));
2858
+ if (parts.length < 2 && (seconds > 0 || parts.length === 0)) parts.push(t("detail.schedule.duration.seconds", { count: String(seconds) }));
2859
+ return parts.slice(0, 2).join(" ");
2860
+ }
2861
+ /**
2862
+ * The relative half of a next-run label: how far away the instant is, or how
2863
+ * far past it the board is running. `now` is passed in rather than sampled so
2864
+ * callers can share one clock reading across a row, and tests stay
2865
+ * deterministic.
2866
+ * @param targetMs - the instant being described.
2867
+ * @param now - the reference instant.
2868
+ * @returns localized relative wording.
2869
+ */
2870
+ function relativeTimeLabel(targetMs, now) {
2871
+ const delta = targetMs - now;
2872
+ if (delta < 0) return t("detail.schedule.nextRunOverdue", { duration: durationParts(-delta) });
2873
+ return `${durationParts(delta)}`;
2874
+ }
2875
+ /**
2876
+ * The full next-run label: the absolute wall clock in the schedule's own zone,
2877
+ * plus the relative distance in parentheses. Showing both matters because a
2878
+ * rule's zone can differ from the reader's, so the wall clock alone is
2879
+ * ambiguous and the distance alone loses the actual moment.
2880
+ * @param targetMs - the instant being described.
2881
+ * @param timeZone - zone to render the absolute half in.
2882
+ * @param formatAbsolute - formatter for the absolute half (keeps Intl caching in the caller).
2883
+ * @param now - the reference instant.
2884
+ * @param translate - translate function, bound so this module stays dependency-light.
2885
+ * @returns the combined label.
2886
+ */
2887
+ function nextRunLabel(targetMs, timeZone, formatAbsolute, now) {
2888
+ return `${formatAbsolute(targetMs, timeZone)} (${relativeTimeLabel(targetMs, now)})`;
2889
+ }
2890
+ //#endregion
2509
2891
  //#region src/client/board/TaskForm.tsx
2892
+ /**
2893
+ * Shared task-modal pieces: the overlay shell (backdrop, form, title, error,
2894
+ * footer) and the title/description/prompt field trio used by both the
2895
+ * NewTaskModal and the EditTaskModal. State stays in the owning modal; these
2896
+ * are controlled components.
2897
+ */
2510
2898
  /** DOM id shared by the tag-name inputs and their datalist (one board at a time). */
2511
2899
  const TAG_NAME_LIST_ID = "dsh-task-board-tag-names";
2512
2900
  /** Modal overlay: closes on backdrop press, submits through the form. */
@@ -2529,7 +2917,10 @@ window.__ModuleLoader__.load({
2529
2917
  className: board_module_css_default.modalTitle,
2530
2918
  children: title
2531
2919
  }),
2532
- children,
2920
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2921
+ className: board_module_css_default.modalBody,
2922
+ children
2923
+ }),
2533
2924
  error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2534
2925
  className: board_module_css_default.formError,
2535
2926
  children: error
@@ -2562,6 +2953,48 @@ window.__ModuleLoader__.load({
2562
2953
  })
2563
2954
  });
2564
2955
  }
2956
+ /**
2957
+ * One collapsible region of a task form (create/edit dialog UX): the form
2958
+ * groups its configuration into a few regions instead of stacking every field
2959
+ * in one long column, so a normal-sized dialog needs no scrollbar. A collapsed
2960
+ * region keeps a one-line summary of the values it holds, so nothing is hidden
2961
+ * without a trace; a region holding a blocking error is forced open by its
2962
+ * owner (forceOpen) rather than reporting the error out of sight.
2963
+ */
2964
+ function CollapsibleSection({ title, summary, defaultOpen = false, forceOpen = false, children }) {
2965
+ const [open, setOpen] = (0, react.useState)(defaultOpen);
2966
+ const expanded = forceOpen || open;
2967
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2968
+ className: board_module_css_default.formSection,
2969
+ "data-dsh-part": "form-section",
2970
+ "data-open": expanded ? "true" : void 0,
2971
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2972
+ type: "button",
2973
+ className: board_module_css_default.formSectionHeader,
2974
+ "aria-expanded": expanded,
2975
+ onClick: () => {
2976
+ setOpen((value) => !value);
2977
+ },
2978
+ children: [
2979
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2980
+ className: board_module_css_default.formSectionChevron,
2981
+ "aria-hidden": "true"
2982
+ }),
2983
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2984
+ className: board_module_css_default.formSectionTitle,
2985
+ children: title
2986
+ }),
2987
+ !expanded && summary !== void 0 && summary !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2988
+ className: board_module_css_default.formSectionSummary,
2989
+ children: summary
2990
+ })
2991
+ ]
2992
+ }), expanded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2993
+ className: board_module_css_default.formSectionBody,
2994
+ children
2995
+ })]
2996
+ });
2997
+ }
2565
2998
  /** Title + description + prompt fields shared by the new and edit task forms. */
2566
2999
  function TaskContentFields({ title, description, prompt, onTitleChange, onDescriptionChange, onPromptChange }) {
2567
3000
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
@@ -2585,7 +3018,7 @@ window.__ModuleLoader__.load({
2585
3018
  children: t("new.description")
2586
3019
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2587
3020
  className: board_module_css_default.input,
2588
- rows: 3,
3021
+ rows: 2,
2589
3022
  value: description,
2590
3023
  placeholder: t("new.descriptionPlaceholder"),
2591
3024
  onChange: (event) => onDescriptionChange(event.target.value)
@@ -2695,6 +3128,166 @@ window.__ModuleLoader__.load({
2695
3128
  return normalizeTags(tags) ?? [];
2696
3129
  }
2697
3130
  //#endregion
3131
+ //#region src/client/board/TaskCard.tsx
3132
+ /**
3133
+ * Task card: the board's column item. Clicking opens the task detail — it
3134
+ * never executes anything directly (detail holds the Run button).
3135
+ *
3136
+ * Memoized: the card re-renders only when its own task record changes, so a
3137
+ * status/filter update on one card (or scrolling) never re-renders every
3138
+ * card on the board. The per-card onClick is built with a stable task reference
3139
+ * by the board, so the memo boundary is effective.
3140
+ */
3141
+ /**
3142
+ * Built formatters, keyed by time zone ('' = the browser's own). Constructing
3143
+ * an Intl.DateTimeFormat is orders of magnitude more expensive than formatting
3144
+ * with one, and the board renders dozens of timestamps per SSE frame; the
3145
+ * cached formatter keeps that cost off the render path.
3146
+ */
3147
+ const HOST_TIMESTAMP_FORMATS = /* @__PURE__ */ new Map();
3148
+ function hostTimestampFormat(timeZone) {
3149
+ const key = timeZone ?? "";
3150
+ const cached = HOST_TIMESTAMP_FORMATS.get(key);
3151
+ if (cached !== void 0) return cached;
3152
+ const format = new Intl.DateTimeFormat(void 0, {
3153
+ dateStyle: "medium",
3154
+ timeStyle: "medium",
3155
+ ...timeZone === void 0 ? {} : { timeZone }
3156
+ });
3157
+ HOST_TIMESTAMP_FORMATS.set(key, format);
3158
+ return format;
3159
+ }
3160
+ /** Compact relative/absolute time label. */
3161
+ function formatHostTimestamp(ms, timeZone) {
3162
+ try {
3163
+ return hostTimestampFormat(timeZone).format(new Date(ms));
3164
+ } catch {
3165
+ return new Date(ms).toISOString();
3166
+ }
3167
+ }
3168
+ function formatTime(ms, timeZone) {
3169
+ const date = new Date(ms);
3170
+ const minutes = Math.floor((Date.now() - ms) / 6e4);
3171
+ if (minutes < 1) return t("time.justNow");
3172
+ if (minutes < 60) return `${minutes}m`;
3173
+ if (minutes < 1440) return `${Math.floor(minutes / 60)}h`;
3174
+ if (timeZone !== void 0) return formatHostTimestamp(ms, timeZone);
3175
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
3176
+ }
3177
+ function TaskCardInner({ task, pending, timeZone, onClick, subtaskCount = 0, isSubtask = false, subtasksDone = 0, subtasksRunning = 0, subtasksFailed = 0 }) {
3178
+ const latest = task.executions[task.executions.length - 1];
3179
+ const runs = task.executions.length;
3180
+ const archived = task.archivedAt !== void 0;
3181
+ const isDraggable = !archived && task.status !== "running" && !pending;
3182
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3183
+ type: "button",
3184
+ className: board_module_css_default.card,
3185
+ "data-status": archived ? "archived" : task.status,
3186
+ "data-dsh-part": "card",
3187
+ "data-pending": pending || void 0,
3188
+ draggable: isDraggable,
3189
+ onDragStart: isDraggable ? (event) => {
3190
+ event.dataTransfer.setData("text/plain", task.id);
3191
+ event.dataTransfer.effectAllowed = "move";
3192
+ } : void 0,
3193
+ onClick,
3194
+ title: task.description !== "" ? task.description : task.title,
3195
+ children: [
3196
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3197
+ className: board_module_css_default.cardTitle,
3198
+ children: task.title
3199
+ }),
3200
+ task.tags !== void 0 && task.tags.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3201
+ className: board_module_css_default.cardTags,
3202
+ children: task.tags.map((tag) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3203
+ className: board_module_css_default.cardTag,
3204
+ "data-tag-tone": tagTone(tag.name),
3205
+ "data-dsh-part": "tag-badge",
3206
+ "data-tag-hint": tag.promptPrefix === void 0 ? void 0 : tag.promptPrefix,
3207
+ title: tag.promptPrefix === void 0 ? tag.name : tag.promptPrefix,
3208
+ children: tag.name
3209
+ }, tag.name))
3210
+ }),
3211
+ task.description !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3212
+ className: board_module_css_default.cardExcerpt,
3213
+ children: task.description
3214
+ }),
3215
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3216
+ className: board_module_css_default.cardMeta,
3217
+ children: [
3218
+ isSubtask && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3219
+ className: board_module_css_default.cardSubtask,
3220
+ "data-dsh-part": "subtask-badge",
3221
+ children: t("card.subtask")
3222
+ }),
3223
+ subtaskCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3224
+ className: board_module_css_default.cardSubtask,
3225
+ "data-dsh-part": "subtask-count",
3226
+ "data-tone": subtasksFailed > 0 ? "failed" : subtasksRunning > 0 ? "running" : subtasksDone === subtaskCount ? "done" : void 0,
3227
+ title: t("card.subtasksBreakdown", {
3228
+ total: String(subtaskCount),
3229
+ done: String(subtasksDone),
3230
+ running: String(subtasksRunning),
3231
+ failed: String(subtasksFailed)
3232
+ }),
3233
+ children: [
3234
+ t("card.subtasks", { count: String(subtaskCount) }),
3235
+ subtasksFailed > 0 ? " · " + t("card.subtasksFailed", { count: String(subtasksFailed) }) : "",
3236
+ subtasksFailed === 0 && subtasksRunning > 0 ? " · " + t("card.subtasksRunning", { count: String(subtasksRunning) }) : ""
3237
+ ]
3238
+ }),
3239
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3240
+ className: board_module_css_default.cardTime,
3241
+ children: [
3242
+ t("board.updated"),
3243
+ " ",
3244
+ formatTime(task.updatedAt)
3245
+ ]
3246
+ }),
3247
+ task.freeze !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3248
+ className: board_module_css_default.cardSchedule,
3249
+ title: task.freeze.goal,
3250
+ children: t("card.frozen")
3251
+ }),
3252
+ !archived && task.schedule?.enabled === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3253
+ className: board_module_css_default.cardSchedule,
3254
+ title: task.schedule.nextRunAt !== void 0 ? `${t("card.scheduled")} · ${formatHostTimestamp(task.schedule.nextRunAt, timeZone)}` : t("card.scheduled"),
3255
+ children: t("card.scheduled")
3256
+ }),
3257
+ latest !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3258
+ className: board_module_css_default.cardRun,
3259
+ "data-result": archived ? void 0 : latest.result,
3260
+ children: [
3261
+ runs,
3262
+ " ",
3263
+ t("board.runs")
3264
+ ]
3265
+ }),
3266
+ latest?.sessionId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3267
+ className: board_module_css_default.cardSession,
3268
+ title: latest.sessionId,
3269
+ children: "⌁"
3270
+ }),
3271
+ !archived && (task.status === "running" || pending) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3272
+ className: board_module_css_default.cardSpinner,
3273
+ "aria-hidden": "true"
3274
+ })
3275
+ ]
3276
+ }),
3277
+ !archived && pending && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3278
+ className: board_module_css_default.cardRunningLabel,
3279
+ children: [t("board.pending"), "…"]
3280
+ }),
3281
+ !archived && latest !== void 0 && executionLabel(latest) === "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3282
+ className: board_module_css_default.cardRunningLabel,
3283
+ children: [latest.ownResult === void 0 ? t("detail.result.running") : t("detail.subtasks.waiting"), "…"]
3284
+ })
3285
+ ]
3286
+ });
3287
+ }
3288
+ /** Memoized card: re-renders only when the card's own task record changes. */
3289
+ const TaskCard = (0, react.memo)(TaskCardInner);
3290
+ //#endregion
2698
3291
  //#region src/client/board/parse-model-pref.ts
2699
3292
  /**
2700
3293
  * The model the user last picked for AI parsing, remembered per browser so the
@@ -2726,6 +3319,35 @@ window.__ModuleLoader__.load({
2726
3319
  } catch {}
2727
3320
  }
2728
3321
  //#endregion
3322
+ //#region src/client/board/preset-label.ts
3323
+ /** Built-in preset id -> display-name key (display sugar only). */
3324
+ const BUILTIN_PRESET_LABELS = {
3325
+ standard: "preset.builtin.standard",
3326
+ ptc: "preset.builtin.ptc",
3327
+ minimal: "preset.builtin.minimal",
3328
+ cordis: "preset.builtin.cordis"
3329
+ };
3330
+ /** Whether one roster row is a shipped built-in preset. */
3331
+ function isBuiltinPreset(id) {
3332
+ return BUILTIN_PRESET_LABELS[id] !== void 0;
3333
+ }
3334
+ /** Display label of one preset row: built-in label, roster name, or raw id. */
3335
+ function presetLabel(preset) {
3336
+ const builtin = BUILTIN_PRESET_LABELS[preset.id];
3337
+ if (builtin !== void 0 && (preset.name ?? "").trim() === "") return t(builtin);
3338
+ return preset.name ?? preset.id;
3339
+ }
3340
+ /**
3341
+ * Label of the empty ("inherit") choice: names the preset a run actually lands
3342
+ * on when the card pins nothing, which is the deployment default the roster
3343
+ * marks with `isDefault`.
3344
+ */
3345
+ function inheritPresetLabel(presets) {
3346
+ const fallback = presets.find((preset) => preset.isDefault);
3347
+ if (fallback === void 0) return t("exec.mode.inherit");
3348
+ return t("exec.mode.inheritWith", { preset: presetLabel(fallback) });
3349
+ }
3350
+ //#endregion
2729
3351
  //#region src/client/board/NewTaskModal.tsx
2730
3352
  /**
2731
3353
  * New-task modal: title + description + the prompt that execution will send.
@@ -2743,8 +3365,10 @@ window.__ModuleLoader__.load({
2743
3365
  const [model, setModel] = (0, react.useState)(initialTask?.model ?? parentTask?.model ?? "");
2744
3366
  const inheritedPermission = parentTask === void 0 ? void 0 : effectiveTaskPermission(parentTask);
2745
3367
  const [reuseSession, setReuseSession] = (0, react.useState)(initialTask?.reuseSession ?? false);
3368
+ const [goalRun, setGoalRun] = (0, react.useState)(initialTask?.goalRun ?? true);
2746
3369
  const [scheduleEnabled, setScheduleEnabled] = (0, react.useState)(initialTask?.schedule?.enabled ?? false);
2747
3370
  const [scheduleCron, setScheduleCron] = (0, react.useState)(initialTask?.schedule?.cron ?? "");
3371
+ const [scheduleZone, setScheduleZone] = (0, react.useState)(initialTask?.schedule?.timeZone ?? "");
2748
3372
  const [scheduleError, setScheduleError] = (0, react.useState)(void 0);
2749
3373
  const [freezeText, setFreezeText] = (0, react.useState)("");
2750
3374
  const [freezeError, setFreezeError] = (0, react.useState)(void 0);
@@ -2843,10 +3467,12 @@ window.__ModuleLoader__.load({
2843
3467
  permission: permission === "" ? void 0 : permission,
2844
3468
  model: model === "" ? void 0 : model,
2845
3469
  ...reuseSession ? { reuseSession: true } : {},
3470
+ ...goalRun ? {} : { goalRun: false },
2846
3471
  ...tagList.length > 0 ? { tags: tagList } : {},
2847
3472
  schedule: scheduleEnabled ? {
2848
3473
  enabled: true,
2849
- cron: scheduleCron.trim()
3474
+ cron: scheduleCron.trim(),
3475
+ ...scheduleZone === "" ? {} : { timeZone: scheduleZone }
2850
3476
  } : void 0
2851
3477
  });
2852
3478
  if (task === void 0) {
@@ -2861,9 +3487,30 @@ window.__ModuleLoader__.load({
2861
3487
  }
2862
3488
  onClose();
2863
3489
  };
2864
- /** Next-run preview for a valid armed cron (creation-time only). */
2865
- const scheduleNextRun = scheduleEnabled && scheduleCron.trim() !== "" && isValidCron(scheduleCron) ? nextRunAtMs(scheduleCron, Date.now()) : void 0;
3490
+ const hostTimeZone = controller.getSnapshot().host?.scheduler.timeZone;
3491
+ const scheduleTimeZone = scheduleZone === "" ? hostTimeZone : scheduleZone;
3492
+ const scheduleNextRun = scheduleEnabled && scheduleCron.trim() !== "" && isValidCron(scheduleCron) ? nextRunAtMs(scheduleCron, Date.now(), scheduleTimeZone) : void 0;
2866
3493
  const modalTitle = parentTask !== void 0 ? t("new.subtaskTitle") : isDuplicate ? t("new.duplicateTitle") : t("board.new");
3494
+ const builtinPresets = options.presets.filter((preset) => isBuiltinPreset(preset.id));
3495
+ const customPresets = options.presets.filter((preset) => !isBuiltinPreset(preset.id));
3496
+ const inheritLabel = inheritPresetLabel(options.presets);
3497
+ const selectedPreset = options.presets.find((preset) => preset.id === mode);
3498
+ const modeLabel = mode === "" ? inheritLabel : selectedPreset === void 0 ? mode : presetLabel(selectedPreset);
3499
+ const workspaceLabel = workspaceId === "" ? t("exec.workspace.recent") : options.workspaces.find((item) => item.workspaceId === workspaceId)?.title ?? workspaceId;
3500
+ const permissionLabel = permission === "" ? inheritedPermission === void 0 ? t("exec.permission.default") : t("exec.permission.inheritParent", { permission: t(`exec.permission.${inheritedPermission}`) }) : t(`exec.permission.${permission}`);
3501
+ const modelLabel = model === "" ? t("exec.model.default") : (options.models ?? []).find((item) => item.id === model)?.name ?? model;
3502
+ const executionSummary = [
3503
+ workspaceLabel,
3504
+ modeLabel,
3505
+ permissionLabel,
3506
+ ...model === "" ? [] : [modelLabel]
3507
+ ].join(" · ");
3508
+ const tagCount = cleanTags(tags).length;
3509
+ const labelsSummary = tagCount === 0 ? t("new.summary.none") : t("new.summary.labelCount", { count: String(tagCount) });
3510
+ const runSummary = [goalRun ? t("new.summary.multiRound") : t("new.summary.singleRound"), ...reuseSession ? [t("exec.reuseSession")] : []].join(" · ");
3511
+ const handoverSummary = freezeText.trim() !== "" || handoverText.trim() !== "" ? t("new.summary.filled") : t("new.summary.none");
3512
+ const scheduleSummary = !scheduleEnabled ? t("new.summary.scheduleOff") : scheduleCron.trim() === "" ? t("new.summary.none") : scheduleCron.trim();
3513
+ const parseSummary = parseText.trim() === "" ? t("new.summary.none") : t("new.summary.filled");
2867
3514
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ModalShell, {
2868
3515
  ariaLabel: modalTitle,
2869
3516
  title: modalTitle,
@@ -2881,68 +3528,73 @@ window.__ModuleLoader__.load({
2881
3528
  }
2882
3529
  },
2883
3530
  children: [
2884
- canParse && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2885
- className: board_module_css_default.aiParse,
2886
- "data-dsh-part": "ai-parse",
2887
- children: [
2888
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2889
- className: board_module_css_default.fieldLabel,
2890
- children: t("new.aiParse")
2891
- }),
2892
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2893
- className: board_module_css_default.fieldHint,
2894
- children: t("new.aiParseHint")
2895
- }),
2896
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2897
- className: board_module_css_default.input,
2898
- rows: 3,
2899
- value: parseText,
2900
- placeholder: t("new.aiParsePlaceholder"),
2901
- spellCheck: false,
2902
- onChange: (event) => {
2903
- setParseText(event.target.value);
2904
- setParseError(void 0);
2905
- }
2906
- }),
2907
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2908
- className: board_module_css_default.aiParseRow,
2909
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2910
- className: board_module_css_default.select,
2911
- value: parseModel,
2912
- "aria-label": t("new.aiParseModel"),
3531
+ canParse && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsibleSection, {
3532
+ title: t("new.section.parse"),
3533
+ summary: parseSummary,
3534
+ forceOpen: parseError !== void 0,
3535
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3536
+ className: board_module_css_default.aiParse,
3537
+ "data-dsh-part": "ai-parse",
3538
+ children: [
3539
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3540
+ className: board_module_css_default.fieldLabel,
3541
+ children: t("new.aiParse")
3542
+ }),
3543
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3544
+ className: board_module_css_default.fieldHint,
3545
+ children: t("new.aiParseHint")
3546
+ }),
3547
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
3548
+ className: board_module_css_default.input,
3549
+ rows: 3,
3550
+ value: parseText,
3551
+ placeholder: t("new.aiParsePlaceholder"),
3552
+ spellCheck: false,
2913
3553
  onChange: (event) => {
2914
- setParseModel(event.target.value);
2915
- writeParseModelPreference(event.target.value);
2916
- },
2917
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2918
- value: "",
2919
- children: t("exec.model.default")
2920
- }), parseModels.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2921
- value: option.id,
2922
- children: option.name ?? option.id
2923
- }, option.id))]
2924
- }), parsePending ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2925
- type: "button",
2926
- className: board_module_css_default.ghostButton,
2927
- onClick: () => {
2928
- parseAbort.current?.abort();
2929
- },
2930
- children: t("new.aiParseCancel")
2931
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2932
- type: "button",
2933
- className: board_module_css_default.primaryButton,
2934
- disabled: parseText.trim() === "",
2935
- onClick: () => {
2936
- runParse();
2937
- },
2938
- children: t("new.aiParseRun")
2939
- })]
2940
- }),
2941
- parseError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2942
- className: board_module_css_default.formError,
2943
- children: parseError
2944
- })
2945
- ]
3554
+ setParseText(event.target.value);
3555
+ setParseError(void 0);
3556
+ }
3557
+ }),
3558
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3559
+ className: board_module_css_default.aiParseRow,
3560
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3561
+ className: board_module_css_default.select,
3562
+ value: parseModel,
3563
+ "aria-label": t("new.aiParseModel"),
3564
+ onChange: (event) => {
3565
+ setParseModel(event.target.value);
3566
+ writeParseModelPreference(event.target.value);
3567
+ },
3568
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3569
+ value: "",
3570
+ children: t("exec.model.default")
3571
+ }), parseModels.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3572
+ value: option.id,
3573
+ children: option.name ?? option.id
3574
+ }, option.id))]
3575
+ }), parsePending ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3576
+ type: "button",
3577
+ className: board_module_css_default.ghostButton,
3578
+ onClick: () => {
3579
+ parseAbort.current?.abort();
3580
+ },
3581
+ children: t("new.aiParseCancel")
3582
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3583
+ type: "button",
3584
+ className: board_module_css_default.primaryButton,
3585
+ disabled: parseText.trim() === "",
3586
+ onClick: () => {
3587
+ runParse();
3588
+ },
3589
+ children: t("new.aiParseRun")
3590
+ })]
3591
+ }),
3592
+ parseError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3593
+ className: board_module_css_default.formError,
3594
+ children: parseError
3595
+ })
3596
+ ]
3597
+ })
2946
3598
  }),
2947
3599
  parentTask !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2948
3600
  className: board_module_css_default.detailText,
@@ -2955,409 +3607,333 @@ window.__ModuleLoader__.load({
2955
3607
  t("new.subtaskInherit")
2956
3608
  ]
2957
3609
  }),
2958
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskContentFields, {
2959
- title,
2960
- description,
2961
- prompt,
2962
- onTitleChange: (value) => {
2963
- setTitle(value);
2964
- setError(void 0);
2965
- },
2966
- onDescriptionChange: setDescription,
2967
- onPromptChange: setPrompt
2968
- }),
2969
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskTagFields, {
2970
- tags,
2971
- knownTags: collectKnownTags(controller.getSnapshot().tasks),
2972
- onChange: setTags
2973
- }),
2974
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2975
- className: board_module_css_default.field,
2976
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2977
- className: board_module_css_default.fieldLabel,
2978
- children: t("new.freeze")
2979
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2980
- className: board_module_css_default.input,
2981
- rows: 4,
2982
- value: freezeText,
2983
- placeholder: t("new.freezePlaceholder"),
2984
- spellCheck: false,
2985
- onChange: (event) => {
2986
- setFreezeText(event.target.value);
2987
- setFreezeError(void 0);
2988
- }
3610
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CollapsibleSection, {
3611
+ title: t("new.section.content"),
3612
+ defaultOpen: true,
3613
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskContentFields, {
3614
+ title,
3615
+ description,
3616
+ prompt,
3617
+ onTitleChange: (value) => {
3618
+ setTitle(value);
3619
+ setError(void 0);
3620
+ },
3621
+ onDescriptionChange: setDescription,
3622
+ onPromptChange: setPrompt
3623
+ }), isDuplicate && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3624
+ className: board_module_css_default.checkboxLabel,
3625
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3626
+ type: "checkbox",
3627
+ checked: archiveOriginal,
3628
+ onChange: (event) => {
3629
+ setArchiveOriginal(event.target.checked);
3630
+ }
3631
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("new.archiveOriginal") })]
2989
3632
  })]
2990
3633
  }),
2991
- freezeError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2992
- className: board_module_css_default.formError,
2993
- children: freezeError
2994
- }),
2995
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2996
- className: board_module_css_default.field,
2997
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2998
- className: board_module_css_default.fieldLabel,
2999
- children: t("new.handover")
3000
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
3001
- className: board_module_css_default.input,
3002
- rows: 3,
3003
- value: handoverText,
3004
- placeholder: t("new.handoverPlaceholder"),
3005
- spellCheck: false,
3006
- onChange: (event) => {
3007
- setHandoverText(event.target.value);
3008
- }
3009
- })]
3634
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsibleSection, {
3635
+ title: t("new.section.labels"),
3636
+ summary: labelsSummary,
3637
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskTagFields, {
3638
+ tags,
3639
+ knownTags: collectKnownTags(controller.getSnapshot().tasks),
3640
+ onChange: setTags
3641
+ })
3010
3642
  }),
3011
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3012
- className: board_module_css_default.field,
3013
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3014
- className: board_module_css_default.fieldLabel,
3015
- children: t("new.workspace")
3016
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3017
- className: board_module_css_default.select,
3018
- value: workspaceId,
3019
- onChange: (event) => {
3020
- setWorkspaceId(event.target.value);
3021
- },
3022
- children: [
3023
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3024
- value: "",
3025
- children: t("exec.workspace.recent")
3026
- }),
3027
- !workspaceKnown && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
3643
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CollapsibleSection, {
3644
+ title: t("new.section.execution"),
3645
+ summary: executionSummary,
3646
+ children: [
3647
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3648
+ className: board_module_css_default.field,
3649
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3650
+ className: board_module_css_default.fieldLabel,
3651
+ children: t("new.workspace")
3652
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3653
+ className: board_module_css_default.select,
3028
3654
  value: workspaceId,
3029
- children: [workspaceId, t("exec.mode.removed")]
3030
- }),
3031
- options.workspaces.map((workspace) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3032
- value: workspace.workspaceId,
3033
- children: workspace.title
3034
- }, workspace.workspaceId))
3035
- ]
3036
- })]
3037
- }),
3038
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3039
- className: board_module_css_default.field,
3040
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3041
- className: board_module_css_default.fieldLabel,
3042
- children: t("new.mode")
3043
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3044
- className: board_module_css_default.select,
3045
- value: mode,
3046
- onChange: (event) => {
3047
- setMode(event.target.value);
3048
- },
3049
- children: [
3050
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3051
- value: "",
3052
- children: t("exec.mode.default")
3053
- }),
3054
- !modeKnown && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
3655
+ onChange: (event) => {
3656
+ setWorkspaceId(event.target.value);
3657
+ },
3658
+ children: [
3659
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3660
+ value: "",
3661
+ children: t("exec.workspace.recent")
3662
+ }),
3663
+ !workspaceKnown && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
3664
+ value: workspaceId,
3665
+ children: [workspaceId, t("exec.mode.removed")]
3666
+ }),
3667
+ options.workspaces.map((workspace) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3668
+ value: workspace.workspaceId,
3669
+ children: workspace.title
3670
+ }, workspace.workspaceId))
3671
+ ]
3672
+ })]
3673
+ }),
3674
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3675
+ className: board_module_css_default.field,
3676
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3677
+ className: board_module_css_default.fieldLabel,
3678
+ children: t("new.agentPreset")
3679
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3680
+ className: board_module_css_default.select,
3055
3681
  value: mode,
3056
- children: [mode, t("exec.mode.removed")]
3057
- }),
3058
- options.presets.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
3059
- value: preset.id,
3060
- disabled: preset.broken !== void 0,
3682
+ onChange: (event) => {
3683
+ setMode(event.target.value);
3684
+ },
3061
3685
  children: [
3062
- preset.name ?? preset.id,
3063
- preset.isDefault ? t("exec.mode.defaultSuffix") : "",
3064
- preset.broken !== void 0 ? t("exec.mode.brokenSuffix") : ""
3686
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3687
+ value: "",
3688
+ children: inheritLabel
3689
+ }),
3690
+ !modeKnown && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
3691
+ value: mode,
3692
+ children: [mode, t("exec.mode.removed")]
3693
+ }),
3694
+ builtinPresets.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("optgroup", {
3695
+ label: t("exec.mode.builtinGroup"),
3696
+ children: builtinPresets.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
3697
+ value: preset.id,
3698
+ disabled: preset.broken !== void 0,
3699
+ children: [
3700
+ presetLabel(preset),
3701
+ preset.isDefault ? t("exec.mode.defaultSuffix") : "",
3702
+ preset.broken !== void 0 ? t("exec.mode.brokenSuffix") : ""
3703
+ ]
3704
+ }, preset.id))
3705
+ }),
3706
+ customPresets.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("optgroup", {
3707
+ label: t("exec.mode.customGroup"),
3708
+ children: customPresets.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
3709
+ value: preset.id,
3710
+ disabled: preset.broken !== void 0,
3711
+ children: [
3712
+ presetLabel(preset),
3713
+ preset.isDefault ? t("exec.mode.defaultSuffix") : "",
3714
+ preset.broken !== void 0 ? t("exec.mode.brokenSuffix") : ""
3715
+ ]
3716
+ }, preset.id))
3717
+ })
3065
3718
  ]
3066
- }, preset.id))
3067
- ]
3068
- })]
3069
- }),
3070
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3071
- className: board_module_css_default.field,
3072
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3073
- className: board_module_css_default.fieldLabel,
3074
- children: t("new.permission")
3075
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3076
- className: board_module_css_default.select,
3077
- value: permission,
3078
- onChange: (event) => {
3079
- setPermission(event.target.value);
3080
- },
3081
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3082
- value: "",
3083
- children: inheritedPermission === void 0 ? t("exec.permission.default") : t("exec.permission.inheritParent", { permission: t(`exec.permission.${inheritedPermission}`) })
3084
- }), TASK_PERMISSIONS.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3085
- value: id,
3086
- children: t(`exec.permission.${id}`)
3087
- }, id))]
3088
- })]
3089
- }),
3090
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3091
- className: board_module_css_default.field,
3092
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3093
- className: board_module_css_default.fieldLabel,
3094
- children: t("new.model")
3095
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3096
- className: board_module_css_default.select,
3097
- value: model,
3098
- onChange: (event) => {
3099
- setModel(event.target.value);
3100
- },
3101
- children: [
3102
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3103
- value: "",
3104
- children: t("exec.model.default")
3105
- }),
3106
- !modelKnown && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
3719
+ })]
3720
+ }),
3721
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3722
+ className: board_module_css_default.field,
3723
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3724
+ className: board_module_css_default.fieldLabel,
3725
+ children: t("new.permission")
3726
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3727
+ className: board_module_css_default.select,
3728
+ value: permission,
3729
+ onChange: (event) => {
3730
+ setPermission(event.target.value);
3731
+ },
3732
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3733
+ value: "",
3734
+ children: inheritedPermission === void 0 ? t("exec.permission.default") : t("exec.permission.inheritParent", { permission: t(`exec.permission.${inheritedPermission}`) })
3735
+ }), TASK_PERMISSIONS.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3736
+ value: id,
3737
+ children: t(`exec.permission.${id}`)
3738
+ }, id))]
3739
+ })]
3740
+ }),
3741
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3742
+ className: board_module_css_default.field,
3743
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3744
+ className: board_module_css_default.fieldLabel,
3745
+ children: t("new.model")
3746
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3747
+ className: board_module_css_default.select,
3107
3748
  value: model,
3108
- children: [model, t("exec.model.unknown")]
3109
- }),
3110
- options.models?.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3111
- value: item.id,
3112
- children: item.name ?? item.id
3113
- }, item.id))
3114
- ]
3115
- })]
3116
- }),
3117
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3118
- className: board_module_css_default.scheduleToggle,
3119
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3120
- type: "checkbox",
3121
- checked: reuseSession,
3122
- onChange: (event) => {
3123
- setReuseSession(event.target.checked);
3124
- }
3125
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("exec.reuseSession") })]
3126
- }),
3127
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3128
- className: board_module_css_default.detailText,
3129
- children: t("exec.reuseSessionHint")
3749
+ onChange: (event) => {
3750
+ setModel(event.target.value);
3751
+ },
3752
+ children: [
3753
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3754
+ value: "",
3755
+ children: t("exec.model.default")
3756
+ }),
3757
+ !modelKnown && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
3758
+ value: model,
3759
+ children: [model, t("exec.model.unknown")]
3760
+ }),
3761
+ options.models?.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3762
+ value: item.id,
3763
+ children: item.name ?? item.id
3764
+ }, item.id))
3765
+ ]
3766
+ })]
3767
+ })
3768
+ ]
3130
3769
  }),
3131
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3132
- className: board_module_css_default.detailSection,
3770
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CollapsibleSection, {
3771
+ title: t("new.section.run"),
3772
+ summary: runSummary,
3133
3773
  children: [
3134
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.schedule") }),
3135
3774
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3136
3775
  className: board_module_css_default.scheduleToggle,
3137
3776
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3138
3777
  type: "checkbox",
3139
- checked: scheduleEnabled,
3778
+ checked: reuseSession,
3140
3779
  onChange: (event) => {
3141
- setScheduleEnabled(event.target.checked);
3142
- if (!event.target.checked) setScheduleError(void 0);
3780
+ setReuseSession(event.target.checked);
3143
3781
  }
3144
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("detail.schedule.enable") })]
3782
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("exec.reuseSession") })]
3145
3783
  }),
3146
- scheduleEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3147
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3148
- className: board_module_css_default.scheduleRow,
3149
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3150
- className: `${board_module_css_default.input} ${board_module_css_default.scheduleInput}${scheduleError !== void 0 ? ` ${board_module_css_default.scheduleInputInvalid}` : ""}`,
3151
- value: scheduleCron,
3152
- placeholder: "0 9 * * *",
3153
- spellCheck: false,
3154
- "aria-label": t("detail.schedule.cron"),
3155
- onChange: (event) => {
3156
- setScheduleCron(event.target.value);
3157
- setScheduleError(void 0);
3158
- }
3159
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3160
- className: board_module_css_default.schedulePreset,
3161
- value: "",
3162
- "aria-label": t("detail.schedule.presets"),
3163
- onChange: (event) => {
3164
- if (event.target.value === "") return;
3165
- setScheduleCron(event.target.value);
3166
- setScheduleError(void 0);
3167
- },
3168
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
3169
- value: "",
3170
- children: [t("detail.schedule.presets"), "…"]
3171
- }), SCHEDULE_PRESETS.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3172
- value: preset.cron,
3173
- children: t(preset.label)
3174
- }, preset.cron))]
3175
- })]
3176
- }),
3177
- scheduleError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3178
- className: board_module_css_default.formError,
3179
- children: scheduleError
3180
- }),
3181
- scheduleError === void 0 && scheduleNextRun !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
3182
- className: board_module_css_default.scheduleMeta,
3183
- children: [
3184
- t("detail.schedule.nextRun"),
3185
- " ",
3186
- new Date(scheduleNextRun).toLocaleString()
3187
- ]
3188
- })
3189
- ] })
3784
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3785
+ className: board_module_css_default.detailText,
3786
+ children: t("exec.reuseSessionHint")
3787
+ }),
3788
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3789
+ className: board_module_css_default.scheduleToggle,
3790
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3791
+ type: "checkbox",
3792
+ checked: goalRun,
3793
+ onChange: (event) => {
3794
+ setGoalRun(event.target.checked);
3795
+ }
3796
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("exec.goalRun") })]
3797
+ }),
3798
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3799
+ className: board_module_css_default.detailText,
3800
+ children: t("exec.goalRunHint")
3801
+ })
3190
3802
  ]
3191
3803
  }),
3192
- isDuplicate && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3193
- className: board_module_css_default.checkboxLabel,
3194
- style: { marginTop: "12px" },
3195
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3196
- type: "checkbox",
3197
- checked: archiveOriginal,
3198
- onChange: (event) => {
3199
- setArchiveOriginal(event.target.checked);
3200
- }
3201
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("new.archiveOriginal") })]
3202
- })
3203
- ]
3204
- });
3205
- }
3206
- //#endregion
3207
- //#region src/client/board/status-key.ts
3208
- /** Task status → locale key (board column titles and the detail badge). */
3209
- const STATUS_KEY = {
3210
- backlog: "board.status.backlog",
3211
- todo: "board.status.todo",
3212
- running: "board.status.running",
3213
- done: "board.status.done",
3214
- failed: "board.status.failed"
3215
- };
3216
- //#endregion
3217
- //#region src/client/board/TaskCard.tsx
3218
- /**
3219
- * Task card: the board's column item. Clicking opens the task detail — it
3220
- * never executes anything directly (detail holds the Run button).
3221
- *
3222
- * Memoized: the card re-renders only when its own task record changes, so a
3223
- * status/filter update on one card (or scrolling) never re-renders every
3224
- * card on the board. The per-card onClick is built with a stable task reference
3225
- * by the board, so the memo boundary is effective.
3226
- */
3227
- /** Compact relative/absolute time label. */
3228
- function formatHostTimestamp(ms, timeZone) {
3229
- try {
3230
- return new Intl.DateTimeFormat(void 0, {
3231
- dateStyle: "medium",
3232
- timeStyle: "medium",
3233
- ...timeZone === void 0 ? {} : { timeZone }
3234
- }).format(new Date(ms));
3235
- } catch {
3236
- return new Date(ms).toISOString();
3237
- }
3238
- }
3239
- function formatTime(ms, timeZone) {
3240
- const date = new Date(ms);
3241
- const minutes = Math.floor((Date.now() - ms) / 6e4);
3242
- if (minutes < 1) return t("time.justNow");
3243
- if (minutes < 60) return `${minutes}m`;
3244
- if (minutes < 1440) return `${Math.floor(minutes / 60)}h`;
3245
- if (timeZone !== void 0) return formatHostTimestamp(ms, timeZone);
3246
- return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
3247
- }
3248
- function TaskCardInner({ task, pending, timeZone, onClick, subtaskCount = 0, isSubtask = false, subtasksDone = 0, subtasksRunning = 0, subtasksFailed = 0 }) {
3249
- const latest = task.executions[task.executions.length - 1];
3250
- const runs = task.executions.length;
3251
- const archived = task.archivedAt !== void 0;
3252
- const isDraggable = !archived && task.status !== "running" && !pending;
3253
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3254
- type: "button",
3255
- className: board_module_css_default.card,
3256
- "data-status": archived ? "archived" : task.status,
3257
- "data-dsh-part": "card",
3258
- "data-pending": pending || void 0,
3259
- draggable: isDraggable,
3260
- onDragStart: isDraggable ? (event) => {
3261
- event.dataTransfer.setData("text/plain", task.id);
3262
- event.dataTransfer.effectAllowed = "move";
3263
- } : void 0,
3264
- onClick,
3265
- title: task.description !== "" ? task.description : task.title,
3266
- children: [
3267
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3268
- className: board_module_css_default.cardTitle,
3269
- children: task.title
3270
- }),
3271
- task.tags !== void 0 && task.tags.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3272
- className: board_module_css_default.cardTags,
3273
- children: task.tags.map((tag) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3274
- className: board_module_css_default.cardTag,
3275
- "data-tag-tone": tagTone(tag.name),
3276
- "data-dsh-part": "tag-badge",
3277
- "data-tag-hint": tag.promptPrefix === void 0 ? void 0 : tag.promptPrefix,
3278
- title: tag.promptPrefix === void 0 ? tag.name : tag.promptPrefix,
3279
- children: tag.name
3280
- }, tag.name))
3281
- }),
3282
- task.description !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3283
- className: board_module_css_default.cardExcerpt,
3284
- children: task.description
3285
- }),
3286
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3287
- className: board_module_css_default.cardMeta,
3804
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CollapsibleSection, {
3805
+ title: t("new.section.handover"),
3806
+ summary: handoverSummary,
3807
+ forceOpen: freezeError !== void 0,
3288
3808
  children: [
3289
- isSubtask && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3290
- className: board_module_css_default.cardSubtask,
3291
- "data-dsh-part": "subtask-badge",
3292
- children: t("card.subtask")
3809
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3810
+ className: board_module_css_default.field,
3811
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3812
+ className: board_module_css_default.fieldLabel,
3813
+ children: t("new.freeze")
3814
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
3815
+ className: board_module_css_default.input,
3816
+ rows: 4,
3817
+ value: freezeText,
3818
+ placeholder: t("new.freezePlaceholder"),
3819
+ spellCheck: false,
3820
+ onChange: (event) => {
3821
+ setFreezeText(event.target.value);
3822
+ setFreezeError(void 0);
3823
+ }
3824
+ })]
3293
3825
  }),
3294
- subtaskCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3295
- className: board_module_css_default.cardSubtask,
3296
- "data-dsh-part": "subtask-count",
3297
- "data-tone": subtasksFailed > 0 ? "failed" : subtasksRunning > 0 ? "running" : subtasksDone === subtaskCount ? "done" : void 0,
3298
- title: t("card.subtasksBreakdown", {
3299
- total: String(subtaskCount),
3300
- done: String(subtasksDone),
3301
- running: String(subtasksRunning),
3302
- failed: String(subtasksFailed)
3303
- }),
3304
- children: [
3305
- t("card.subtasks", { count: String(subtaskCount) }),
3306
- subtasksFailed > 0 ? " · " + t("card.subtasksFailed", { count: String(subtasksFailed) }) : "",
3307
- subtasksFailed === 0 && subtasksRunning > 0 ? " · " + t("card.subtasksRunning", { count: String(subtasksRunning) }) : ""
3308
- ]
3826
+ freezeError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3827
+ className: board_module_css_default.formError,
3828
+ children: freezeError
3309
3829
  }),
3310
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3311
- className: board_module_css_default.cardTime,
3312
- children: [
3313
- t("board.updated"),
3314
- " ",
3315
- formatTime(task.updatedAt)
3316
- ]
3830
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3831
+ className: board_module_css_default.field,
3832
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3833
+ className: board_module_css_default.fieldLabel,
3834
+ children: t("new.handover")
3835
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
3836
+ className: board_module_css_default.input,
3837
+ rows: 3,
3838
+ value: handoverText,
3839
+ placeholder: t("new.handoverPlaceholder"),
3840
+ spellCheck: false,
3841
+ onChange: (event) => {
3842
+ setHandoverText(event.target.value);
3843
+ }
3844
+ })]
3845
+ })
3846
+ ]
3847
+ }),
3848
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CollapsibleSection, {
3849
+ title: t("new.section.schedule"),
3850
+ summary: scheduleSummary,
3851
+ forceOpen: scheduleError !== void 0,
3852
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3853
+ className: board_module_css_default.scheduleToggle,
3854
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3855
+ type: "checkbox",
3856
+ checked: scheduleEnabled,
3857
+ onChange: (event) => {
3858
+ setScheduleEnabled(event.target.checked);
3859
+ if (!event.target.checked) setScheduleError(void 0);
3860
+ }
3861
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("detail.schedule.enable") })]
3862
+ }), scheduleEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3863
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3864
+ className: board_module_css_default.scheduleRow,
3865
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3866
+ className: `${board_module_css_default.input} ${board_module_css_default.scheduleInput}${scheduleError !== void 0 ? ` ${board_module_css_default.scheduleInputInvalid}` : ""}`,
3867
+ value: scheduleCron,
3868
+ placeholder: "0 9 * * *",
3869
+ spellCheck: false,
3870
+ "aria-label": t("detail.schedule.cron"),
3871
+ onChange: (event) => {
3872
+ setScheduleCron(event.target.value);
3873
+ setScheduleError(void 0);
3874
+ }
3875
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3876
+ className: board_module_css_default.schedulePreset,
3877
+ value: "",
3878
+ "aria-label": t("detail.schedule.presets"),
3879
+ onChange: (event) => {
3880
+ if (event.target.value === "") return;
3881
+ setScheduleCron(event.target.value);
3882
+ setScheduleError(void 0);
3883
+ },
3884
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
3885
+ value: "",
3886
+ children: [t("detail.schedule.presets"), "…"]
3887
+ }), SCHEDULE_PRESETS.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3888
+ value: preset.cron,
3889
+ children: t(preset.label)
3890
+ }, preset.cron))]
3891
+ })]
3317
3892
  }),
3318
- task.freeze !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3319
- className: board_module_css_default.cardSchedule,
3320
- title: task.freeze.goal,
3321
- children: t("card.frozen")
3893
+ scheduleError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3894
+ className: board_module_css_default.formError,
3895
+ children: scheduleError
3322
3896
  }),
3323
- !archived && task.schedule?.enabled === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3324
- className: board_module_css_default.cardSchedule,
3325
- title: task.schedule.nextRunAt !== void 0 ? `${t("card.scheduled")} · ${formatHostTimestamp(task.schedule.nextRunAt, timeZone)}` : t("card.scheduled"),
3326
- children: t("card.scheduled")
3897
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3898
+ className: board_module_css_default.scheduleZone,
3899
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("detail.schedule.timeZone") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
3900
+ className: board_module_css_default.schedulePreset,
3901
+ value: scheduleZone,
3902
+ "aria-label": t("detail.schedule.timeZone"),
3903
+ title: t("detail.schedule.timeZoneHint"),
3904
+ onChange: (event) => {
3905
+ setScheduleZone(event.target.value);
3906
+ setScheduleError(void 0);
3907
+ },
3908
+ children: zoneChoices(hostTimeZone, initialTask?.schedule?.timeZone).map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3909
+ value: choice.id,
3910
+ children: choice.label
3911
+ }, choice.id === "" ? "__host" : choice.id))
3912
+ })]
3327
3913
  }),
3328
- latest !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3329
- className: board_module_css_default.cardRun,
3330
- "data-result": archived ? void 0 : latest.result,
3914
+ scheduleError === void 0 && scheduleNextRun !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
3915
+ className: board_module_css_default.scheduleMeta,
3331
3916
  children: [
3332
- runs,
3917
+ t("detail.schedule.nextRun"),
3333
3918
  " ",
3334
- t("board.runs")
3919
+ nextRunLabel(scheduleNextRun, scheduleTimeZone, formatHostTimestamp, Date.now())
3335
3920
  ]
3336
- }),
3337
- latest?.sessionId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3338
- className: board_module_css_default.cardSession,
3339
- title: latest.sessionId,
3340
- children: "⌁"
3341
- }),
3342
- !archived && (task.status === "running" || pending) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3343
- className: board_module_css_default.cardSpinner,
3344
- "aria-hidden": "true"
3345
3921
  })
3346
- ]
3347
- }),
3348
- !archived && pending && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3349
- className: board_module_css_default.cardRunningLabel,
3350
- children: [t("board.pending"), "…"]
3351
- }),
3352
- !archived && latest !== void 0 && executionLabel(latest) === "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3353
- className: board_module_css_default.cardRunningLabel,
3354
- children: [latest.ownResult === void 0 ? t("detail.result.running") : t("detail.subtasks.waiting"), "…"]
3922
+ ] })]
3355
3923
  })
3356
3924
  ]
3357
3925
  });
3358
3926
  }
3359
- /** Memoized card: re-renders only when the card's own task record changes. */
3360
- const TaskCard = (0, react.memo)(TaskCardInner);
3927
+ //#endregion
3928
+ //#region src/client/board/status-key.ts
3929
+ /** Task status → locale key (board column titles and the detail badge). */
3930
+ const STATUS_KEY = {
3931
+ backlog: "board.status.backlog",
3932
+ todo: "board.status.todo",
3933
+ running: "board.status.running",
3934
+ done: "board.status.done",
3935
+ failed: "board.status.failed"
3936
+ };
3361
3937
  //#endregion
3362
3938
  //#region src/client/board/ConfirmDialog.tsx
3363
3939
  /**
@@ -3512,7 +4088,15 @@ window.__ModuleLoader__.load({
3512
4088
  const [error, setError] = (0, react.useState)(void 0);
3513
4089
  const snapshot = controller.getSnapshot();
3514
4090
  const limit = snapshot.host?.maxSubtaskDepth ?? 1;
3515
- const candidates = snapshot.tasks.filter((task) => task.archivedAt === void 0 && task.status !== "running" && task.parentId === void 0 && task.id !== parent.id && checkParentLink(snapshot.tasks, task.id, parent.id, limit).ok);
4091
+ const tasks = snapshot.tasks;
4092
+ const candidates = (0, react.useMemo)(() => {
4093
+ const lineage = buildLineageIndex(tasks);
4094
+ return tasks.filter((task) => task.archivedAt === void 0 && task.status !== "running" && task.parentId === void 0 && task.id !== parent.id && checkParentLink(tasks, task.id, parent.id, limit, lineage).ok);
4095
+ }, [
4096
+ tasks,
4097
+ parent.id,
4098
+ limit
4099
+ ]);
3516
4100
  const link = async (taskId) => {
3517
4101
  setPending(true);
3518
4102
  setError(void 0);
@@ -3644,15 +4228,8 @@ window.__ModuleLoader__.load({
3644
4228
  });
3645
4229
  }
3646
4230
  /** The execution-target editor: workspace / mode / permission pickers. */
3647
- function ExecutionSettingsSection({ controller, task, pending }) {
3648
- const [options, setOptions] = (0, react.useState)(controller.getSnapshot().executionOptions);
3649
- const teamRunOffered = () => controller.getSnapshot().host?.teamRunAvailable === true;
3650
- const [teamRunAvailable, setTeamRunAvailable] = (0, react.useState)(teamRunOffered());
3651
- (0, react.useEffect)(() => controller.subscribe(() => {
3652
- const snapshot = controller.getSnapshot();
3653
- setOptions(snapshot.executionOptions);
3654
- setTeamRunAvailable(snapshot.host?.teamRunAvailable === true);
3655
- }), [controller]);
4231
+ function ExecutionSettingsSection({ controller, task, pending, executionOptions, teamRunAvailable }) {
4232
+ const options = executionOptions;
3656
4233
  const workspaceId = task.workspaceId ?? "";
3657
4234
  const mode = task.mode ?? "";
3658
4235
  const permission = task.permission ?? "";
@@ -3700,7 +4277,7 @@ window.__ModuleLoader__.load({
3700
4277
  className: board_module_css_default.field,
3701
4278
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3702
4279
  className: board_module_css_default.fieldLabel,
3703
- children: t("new.mode")
4280
+ children: t("new.agentPreset")
3704
4281
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3705
4282
  className: board_module_css_default.select,
3706
4283
  value: mode,
@@ -3711,7 +4288,7 @@ window.__ModuleLoader__.load({
3711
4288
  children: [
3712
4289
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3713
4290
  value: "",
3714
- children: t("exec.mode.default")
4291
+ children: inheritPresetLabel(options.presets)
3715
4292
  }),
3716
4293
  !modeKnown && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
3717
4294
  value: mode,
@@ -3721,7 +4298,7 @@ window.__ModuleLoader__.load({
3721
4298
  value: preset.id,
3722
4299
  disabled: preset.broken !== void 0,
3723
4300
  children: [
3724
- preset.name ?? preset.id,
4301
+ presetLabel(preset),
3725
4302
  preset.isDefault ? t("exec.mode.defaultSuffix") : "",
3726
4303
  preset.broken !== void 0 ? t("exec.mode.brokenSuffix") : ""
3727
4304
  ]
@@ -3793,6 +4370,21 @@ window.__ModuleLoader__.load({
3793
4370
  className: board_module_css_default.detailText,
3794
4371
  children: t("exec.reuseSessionHint")
3795
4372
  }),
4373
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4374
+ className: board_module_css_default.scheduleToggle,
4375
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4376
+ type: "checkbox",
4377
+ checked: task.goalRun !== false,
4378
+ disabled: pending,
4379
+ onChange: (event) => {
4380
+ controller.updateTask(task.id, { goalRun: event.target.checked });
4381
+ }
4382
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("exec.goalRun") })]
4383
+ }),
4384
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4385
+ className: board_module_css_default.detailText,
4386
+ children: t("exec.goalRunHint")
4387
+ }),
3796
4388
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3797
4389
  className: board_module_css_default.scheduleToggle,
3798
4390
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
@@ -3811,7 +4403,7 @@ window.__ModuleLoader__.load({
3811
4403
  ]
3812
4404
  });
3813
4405
  }
3814
- /** The scheduled-runs editor: enable toggle, cron input + presets, next-run info. */
4406
+ /** The scheduled-runs editor: enable toggle, cron input + presets, zone, next-run info. */
3815
4407
  function ScheduleSection({ controller, task, pending }) {
3816
4408
  const schedule = task.schedule;
3817
4409
  const [cron, setCron] = (0, react.useState)(schedule?.cron ?? "0 9 * * *");
@@ -3819,17 +4411,21 @@ window.__ModuleLoader__.load({
3819
4411
  const [nextRunAt, setNextRunAt] = (0, react.useState)(schedule?.nextRunAt);
3820
4412
  const [lastTriggeredAt, setLastTriggeredAt] = (0, react.useState)(schedule?.lastTriggeredAt);
3821
4413
  const [error, setError] = (0, react.useState)(void 0);
3822
- const timeZone = controller.getSnapshot().host?.scheduler.timeZone;
4414
+ const hostTimeZone = controller.getSnapshot().host?.scheduler.timeZone;
4415
+ const [zone, setZone] = (0, react.useState)(schedule?.timeZone ?? "");
4416
+ const now = Date.now();
3823
4417
  (0, react.useEffect)(() => {
3824
4418
  setCron(schedule?.cron ?? "0 9 * * *");
3825
4419
  setEnabled(schedule?.enabled ?? false);
3826
4420
  setNextRunAt(schedule?.nextRunAt);
3827
4421
  setLastTriggeredAt(schedule?.lastTriggeredAt);
4422
+ setZone(schedule?.timeZone ?? "");
3828
4423
  setError(void 0);
3829
4424
  }, [
3830
4425
  task.id,
3831
4426
  schedule?.enabled,
3832
4427
  schedule?.cron,
4428
+ schedule?.timeZone,
3833
4429
  schedule?.nextRunAt,
3834
4430
  schedule?.lastTriggeredAt
3835
4431
  ]);
@@ -3844,6 +4440,15 @@ window.__ModuleLoader__.load({
3844
4440
  setError(void 0);
3845
4441
  controller.setSchedule(task.id, { cron: trimmed });
3846
4442
  };
4443
+ /**
4444
+ * Change the rule's zone. `''` clears the stored zone, which is how a user
4445
+ * returns a rule to following the Host zone.
4446
+ */
4447
+ const changeZone = (value) => {
4448
+ setZone(value);
4449
+ setError(void 0);
4450
+ controller.setSchedule(task.id, { timeZone: value === "" ? null : value });
4451
+ };
3847
4452
  /** Arm/disarm the schedule (arming first persists the edited cron). */
3848
4453
  const toggleEnabled = (next) => {
3849
4454
  const trimmed = cron.trim();
@@ -3863,8 +4468,10 @@ window.__ModuleLoader__.load({
3863
4468
  setError(void 0);
3864
4469
  controller.setSchedule(task.id, { cron: preset });
3865
4470
  };
3866
- const nextLabel = !enabled || nextRunAt === void 0 ? t("detail.schedule.notScheduled") : nextRunAt <= Date.now() ? t("detail.schedule.dueSoon") : formatHostTimestamp(nextRunAt, timeZone);
3867
- const lastLabel = lastTriggeredAt === void 0 ? "—" : formatHostTimestamp(lastTriggeredAt, timeZone);
4471
+ const effectiveZone = zone === "" ? hostTimeZone : zone;
4472
+ const nextLabel = !enabled || nextRunAt === void 0 ? t("detail.schedule.notScheduled") : nextRunAt <= now ? t("detail.schedule.dueSoon") : nextRunLabel(nextRunAt, effectiveZone, formatHostTimestamp, now);
4473
+ const lastLabel = lastTriggeredAt === void 0 ? "—" : formatHostTimestamp(lastTriggeredAt, effectiveZone);
4474
+ const zones = zoneChoices(hostTimeZone, schedule?.timeZone);
3868
4475
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3869
4476
  className: board_module_css_default.detailSection,
3870
4477
  children: [
@@ -3916,6 +4523,23 @@ window.__ModuleLoader__.load({
3916
4523
  }, preset.cron))]
3917
4524
  })]
3918
4525
  }),
4526
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4527
+ className: board_module_css_default.scheduleZone,
4528
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("detail.schedule.timeZone") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
4529
+ className: board_module_css_default.schedulePreset,
4530
+ value: zone,
4531
+ disabled: pending,
4532
+ "aria-label": t("detail.schedule.timeZone"),
4533
+ title: t("detail.schedule.timeZoneHint"),
4534
+ onChange: (event) => {
4535
+ changeZone(event.target.value);
4536
+ },
4537
+ children: zones.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4538
+ value: choice.id,
4539
+ children: choice.label
4540
+ }, choice.id === "" ? "__host" : choice.id))
4541
+ })]
4542
+ }),
3919
4543
  error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3920
4544
  className: board_module_css_default.formError,
3921
4545
  children: error
@@ -3940,9 +4564,7 @@ window.__ModuleLoader__.load({
3940
4564
  * and the actions that grow or prune the tree. Every gate here mirrors the
3941
4565
  * Host lineage gate for affordance only; the Host re-checks the action.
3942
4566
  */
3943
- function SubtaskSection({ controller, task, pending, archived }) {
3944
- const [snapshot, setSnapshot] = (0, react.useState)(controller.getSnapshot());
3945
- (0, react.useEffect)(() => controller.subscribe(() => setSnapshot(controller.getSnapshot())), [controller]);
4567
+ function SubtaskSection({ controller, task, pending, archived, snapshot }) {
3946
4568
  const [showAdd, setShowAdd] = (0, react.useState)(false);
3947
4569
  const [showLink, setShowLink] = (0, react.useState)(false);
3948
4570
  const tasks = snapshot.tasks;
@@ -4141,7 +4763,8 @@ window.__ModuleLoader__.load({
4141
4763
  controller,
4142
4764
  task: current,
4143
4765
  pending,
4144
- archived
4766
+ archived,
4767
+ snapshot
4145
4768
  }),
4146
4769
  current.tags !== void 0 && current.tags.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4147
4770
  className: board_module_css_default.detailSection,
@@ -4213,9 +4836,9 @@ window.__ModuleLoader__.load({
4213
4836
  ": ",
4214
4837
  current.handover.workspaceId ?? t("exec.workspace.recent"),
4215
4838
  " · ",
4216
- t("new.mode"),
4839
+ t("new.agentPreset"),
4217
4840
  ": ",
4218
- current.handover.mode ?? t("exec.mode.default"),
4841
+ current.handover.mode ?? t("exec.mode.inherit"),
4219
4842
  " · ",
4220
4843
  t("new.permission"),
4221
4844
  ": ",
@@ -4269,7 +4892,9 @@ window.__ModuleLoader__.load({
4269
4892
  !archived && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExecutionSettingsSection, {
4270
4893
  controller,
4271
4894
  task: current,
4272
- pending
4895
+ pending,
4896
+ executionOptions: snapshot.executionOptions,
4897
+ teamRunAvailable: snapshot.host?.teamRunAvailable === true
4273
4898
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScheduleSection, {
4274
4899
  controller,
4275
4900
  task: current,
@@ -4842,390 +5467,89 @@ window.__ModuleLoader__.load({
4842
5467
  });
4843
5468
  }
4844
5469
  //#endregion
4845
- //#region src/client/body-mutations.ts
4846
- /** Cross-bundle registry key; `Symbol.for` so every module copy agrees. */
4847
- const HUB_KEY = Symbol.for("dsh-web.body-mutation-hub");
4848
- const INVALIDATION_ONLY = Symbol.for("dsh-web.body-mutation-invalidation");
4849
- function needsRecords(subscribers) {
4850
- for (const listener of subscribers) if (!listener[INVALIDATION_ONLY]) return true;
4851
- return false;
4852
- }
4853
- /**
4854
- * Subscribe to a coalesced DOM re-check without retaining mutation records.
4855
- * The marked wrapper also works with an older hub, which delivers records
4856
- * that it simply ignores until a page reload picks up the updated hub.
4857
- */
4858
- function subscribeBodyInvalidations(subscriber) {
4859
- const listener = () => {
4860
- subscriber();
4861
- };
4862
- listener[INVALIDATION_ONLY] = true;
4863
- return subscribeBodyMutations(listener);
4864
- }
4865
- /**
4866
- * Subscribe to body-level childList mutations.
4867
- * @param subscriber - called at most once per animation frame with the records
4868
- * collected since the previous flush; must be safe to run repeatedly.
4869
- * @returns the disposer removing this subscriber (and the observer when it was
4870
- * the last one).
4871
- */
4872
- function subscribeBodyMutations(subscriber) {
4873
- if (typeof globalThis === "undefined" || typeof document === "undefined") return () => {};
4874
- if (typeof MutationObserver !== "function") return () => {};
4875
- const registry = globalThis;
4876
- let hub = registry[HUB_KEY];
4877
- if (hub === void 0) {
4878
- const subscribers = /* @__PURE__ */ new Set();
4879
- const created = {
4880
- observer: void 0,
4881
- subscribers,
4882
- pending: [],
4883
- scheduled: false
4884
- };
4885
- const flush = () => {
4886
- created.frame = void 0;
4887
- created.scheduled = false;
4888
- const batch = created.pending;
4889
- created.pending = [];
4890
- for (const listener of [...subscribers]) {
4891
- if (!subscribers.has(listener)) continue;
4892
- try {
4893
- listener(batch);
4894
- } catch {}
4895
- }
4896
- };
4897
- const schedule = () => {
4898
- if (created.scheduled) return;
4899
- created.scheduled = true;
4900
- if (typeof requestAnimationFrame === "function") created.frame = requestAnimationFrame(flush);
4901
- else flush();
4902
- };
4903
- created.observer = new MutationObserver((records) => {
4904
- if (needsRecords(subscribers)) for (const record of records) created.pending.push(record);
4905
- schedule();
4906
- });
4907
- created.observer.observe(document.body ?? document.documentElement, {
4908
- childList: true,
4909
- subtree: true
4910
- });
4911
- registry[HUB_KEY] = created;
4912
- hub = created;
4913
- }
4914
- const active = hub;
4915
- active.subscribers.add(subscriber);
4916
- let subscribed = true;
4917
- return () => {
4918
- if (!subscribed) return;
4919
- subscribed = false;
4920
- active.subscribers.delete(subscriber);
4921
- if (!needsRecords(active.subscribers)) active.pending = [];
4922
- if (active.subscribers.size === 0 && registry[HUB_KEY] === active) {
4923
- active.observer.disconnect();
4924
- if (active.frame !== void 0 && typeof cancelAnimationFrame === "function") cancelAnimationFrame(active.frame);
4925
- active.frame = void 0;
4926
- active.pending = [];
4927
- active.scheduled = false;
4928
- delete registry[HUB_KEY];
4929
- }
4930
- };
4931
- }
4932
- //#endregion
4933
- //#region src/client/panel-mount-core.ts
5470
+ //#region src/client/native-panel.tsx
5471
+ /** Row order among the shell's global panel rows (Plugins is 0, Schedule 10). */
5472
+ const PANEL_ORDER = 20;
4934
5473
  /**
4935
- * Center-column panel takeover lifecycle.
4936
- *
4937
- * The `conversation` slot is single-occupant (ui-conversation) and external
4938
- * plugins cannot declare slots, so a family panel takes over the center
4939
- * column at the DOM level: a container is appended inside the center column
4940
- * (`[class*="centerCol"]`, the 0.1.0-rc.6+ AppFrame layout; previously
4941
- * `[data-pane="conversation"]` on older shells — the mount selector keeps
4942
- * both, ssh #243 / task-board #107) as an extra trailing child React never
4943
- * manages, and a stylesheet rule hides the conversation content while the
4944
- * panel is active. Toggling is a data attribute on <html> — no React
4945
- * involvement, so the conversation subtree underneath stays mounted and
4946
- * stateful.
5474
+ * The sidebar row glyph the shell asks for at its own size and active state.
5475
+ * The shell owns the button, label, tooltip and rail geometry; this component
5476
+ * draws only the glyph, like every other panel row.
4947
5477
  *
4948
- * Consuming plugins keep a thin wrapper that supplies the panel tree,
4949
- * container attribute names, and stylesheet class; those names are pinned by
4950
- * each package's CSS, skins, and the semantic-attributes contract. The
4951
- * sidebar row toggling the panel shares its core the same way
4952
- * (shared/client/sidebar-entry-core.ts, synced copy).
5478
+ * The glyph carries `data-dsh-panel-entry` because it is the only DOM the
5479
+ * panel's own code owns inside that shell-owned row: the L2 contract (skins)
5480
+ * resolves which row belongs to which plugin through it, since the shell
5481
+ * stamps no per-entry hook of its own (see contracts/semantic-attrs-v1.md).
5482
+ * @param props - the shell's icon share: square edge and selection state.
5483
+ * @returns the decorative board glyph.
4953
5484
  */
4954
- const CONVERSATION_COLUMN_SELECTOR = "[data-pane=\"conversation\"], [class*=\"centerCol\"]";
4955
- /** Cross-plugin activation event; detail is the activating panel name. */
4956
- const ACTIVATE_EVENT = "dsh-panel-activate";
4957
- const SIDEBAR_ROW_SELECTOR = "[class*=\"sessionRow\"], [class*=\"projectRow\"], [class*=\"searchResultRow\"], [class*=\"searchResultWorkspace\"], [class*=\"newSession\"]";
4958
- /** Find the center column, or undefined while the frame is not mounted. */
4959
- function conversationColumn() {
4960
- return document.querySelector(CONVERSATION_COLUMN_SELECTOR) ?? void 0;
4961
- }
4962
- /**
4963
- * Mount a family panel into the center column and bind its visibility to the
4964
- * owning controller's open state.
4965
- * @returns disposer unmounting the tree and restoring the column.
4966
- */
4967
- function mountCenterPanel(options) {
4968
- let root;
4969
- let container;
4970
- let unsubscribeLocale;
4971
- try {
4972
- unsubscribeLocale = options.locale?.subscribe(() => {
4973
- if (root !== void 0) options.render(root);
4974
- });
4975
- } catch {}
4976
- const ensure = () => {
4977
- if (container !== void 0 && !container.isConnected) {
4978
- root?.unmount();
4979
- root = void 0;
4980
- container.remove();
4981
- container = void 0;
4982
- }
4983
- if (container === void 0) {
4984
- const column = conversationColumn();
4985
- if (column === void 0) return;
4986
- container = document.createElement("div");
4987
- container.dataset[options.viewDatasetKey] = "";
4988
- container.dataset.dshPlugin = options.pluginName;
4989
- container.className = options.viewClassName;
4990
- column.appendChild(container);
4991
- }
4992
- if (root !== void 0 || !options.isOpen()) return;
4993
- root = (0, react_dom_client.createRoot)(container);
4994
- options.render(root);
4995
- };
4996
- const unsubscribeBody = subscribeBodyInvalidations(() => {
4997
- ensure();
5485
+ function TaskBoardPanelIcon({ size }) {
5486
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
5487
+ "data-dsh-panel-entry": TASK_BOARD_PANEL_ID,
5488
+ viewBox: "0 0 16 16",
5489
+ width: size,
5490
+ height: size,
5491
+ fill: "none",
5492
+ stroke: "currentColor",
5493
+ strokeWidth: "1.3",
5494
+ strokeLinecap: "round",
5495
+ strokeLinejoin: "round",
5496
+ "aria-hidden": "true",
5497
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
5498
+ x: "2",
5499
+ y: "2.5",
5500
+ width: "12",
5501
+ height: "11",
5502
+ rx: "1.5"
5503
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2 6.5h12M6.5 6.5v7" })]
4998
5504
  });
4999
- const applyActive = () => {
5000
- if (options.isOpen()) {
5001
- ensure();
5002
- document.documentElement.removeAttribute(options.siblingActiveAttribute);
5003
- document.documentElement.setAttribute(options.activeAttribute, "");
5004
- document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: options.panelName }));
5005
- } else document.documentElement.removeAttribute(options.activeAttribute);
5006
- };
5007
- const onOtherActivate = (event) => {
5008
- if (event.detail === options.siblingPanelName && options.isOpen()) options.close();
5009
- };
5010
- const onClickSidebarRow = (event) => {
5011
- if (!options.isOpen()) return;
5012
- const target = event.target;
5013
- if (target === null) return;
5014
- if (target.closest(SIDEBAR_ROW_SELECTOR) !== null) options.close();
5015
- };
5016
- document.addEventListener("click", onClickSidebarRow, true);
5017
- document.addEventListener(ACTIVATE_EVENT, onOtherActivate);
5018
- const unsubscribe = options.subscribe(applyActive);
5019
- applyActive();
5020
- ensure();
5021
- return () => {
5022
- document.removeEventListener("click", onClickSidebarRow, true);
5023
- document.removeEventListener(ACTIVATE_EVENT, onOtherActivate);
5024
- unsubscribeBody();
5025
- unsubscribe();
5026
- unsubscribeLocale?.();
5027
- document.documentElement.removeAttribute(options.activeAttribute);
5028
- root?.unmount();
5029
- root = void 0;
5030
- container?.remove();
5031
- container = void 0;
5032
- };
5033
5505
  }
5034
- //#endregion
5035
- //#region src/client/board-mount.tsx
5036
5506
  /**
5037
- * Mount the board React tree into the center column and bind its visibility
5038
- * to the controller's boardOpen state.
5039
- * @param controller - the board controller driving the view.
5040
- * @param locale - locale-change source; when given, re-renders a mounted board
5041
- * on a Language switch.
5042
- * @returns disposer unmounting the tree and restoring the column.
5507
+ * The main-slot page. The layout mounts it only while the board is the selected
5508
+ * panel, so the conversation keeps the center column untouched the rest of the
5509
+ * time; the wrapper carries the pinned `data-dsh-taskboard-view` semantic
5510
+ * anchor (L2 contract, skins) and the container query context the board's
5511
+ * responsive rules read.
5512
+ * @param props - the framework main-slot share plus this entry's injected face.
5513
+ * @returns the board page.
5043
5514
  */
5044
- function mountBoard(controller, locale) {
5045
- return mountCenterPanel({
5046
- render: (root) => root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskBoard, { controller })),
5047
- viewDatasetKey: "dshTaskboardView",
5048
- pluginName: "task-board",
5049
- viewClassName: board_module_css_default.boardView,
5050
- activeAttribute: "data-dsh-taskboard-active",
5051
- siblingActiveAttribute: "data-dsh-ssh-active",
5052
- panelName: "taskboard",
5053
- siblingPanelName: "ssh",
5054
- isOpen: () => controller.getSnapshot().boardOpen,
5055
- close: () => controller.closeBoard(),
5056
- subscribe: (listener) => controller.subscribe(listener),
5057
- locale
5515
+ function TaskBoardPanel({ controller }) {
5516
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5517
+ className: board_module_css_default.panel,
5518
+ "data-dsh-taskboard-view": "",
5519
+ "data-dsh-plugin": "task-board",
5520
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskBoard, { controller })
5058
5521
  });
5059
5522
  }
5060
- //#endregion
5061
- //#region src/client/sidebar-entry-core.ts
5062
5523
  /**
5063
- * Shared sidebar entry injection core.
5064
- *
5065
- * dsh's sidebar shell exposes no slot an external plugin can register into,
5066
- * so the entry row is injected between the shell's New Session button and the
5067
- * workspace browser. The injection self-heals: a MutationObserver watches the
5068
- * sidebar root and re-inserts the row whenever a React re-render displaces it
5069
- * (re-insertion happens in the same frame, before paint, so no flicker).
5070
- *
5071
- * The row is plain DOM (no React tree) so it can never disturb the shell's
5072
- * reconciliation; the view it toggles is a separate root owned by the caller.
5524
+ * Register the board's sidebar row and center-column page.
5073
5525
  *
5074
- * Packages receive this file as a generated copy via scripts/sync-shared.mjs;
5075
- * edit the shared source and re-run the sync instead of editing a copy.
5526
+ * Both seats are declared by shell plugins this package does not depend on at
5527
+ * runtime, so each registration is wrapped in `ctx.slots.inject`: the callback
5528
+ * runs only after the owning entry declares the seat, and a shell that never
5529
+ * declares it leaves the board simply absent instead of failing boot.
5530
+ * @param ctx - client root context (services: slots).
5531
+ * @param controller - the board controller the page and the row drive.
5532
+ * @returns disposer releasing both registrations.
5076
5533
  */
5077
- /** Find the sidebar shell root element, or undefined while not yet mounted. */
5078
- function sidebarRoot() {
5079
- const column = document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"]");
5080
- if (column === null) return void 0;
5081
- return column.querySelector("[class*=\"logoRow\"]")?.parentElement ?? column.firstElementChild;
5082
- }
5083
- /** The New Session button: nested in the logo row on current shells, a direct child on legacy shells. */
5084
- function newSessionButton(root) {
5085
- const nested = root.querySelector("button[class*=\"newSession\"]");
5086
- if (nested !== null) return nested;
5087
- for (const child of root.children) if (child.tagName === "BUTTON") return child;
5088
- }
5089
- /** Build the entry row (detached; inserted once the shell is up). */
5090
- function createEntry(options) {
5091
- const entry = document.createElement("button");
5092
- entry.type = "button";
5093
- entry.setAttribute(options.rowAttribute, "");
5094
- if (options.plugin !== void 0) {
5095
- entry.setAttribute("data-dsh-plugin", options.plugin);
5096
- entry.setAttribute("data-dsh-part", "sidebar-entry");
5097
- }
5098
- entry.className = options.css["entry"] ?? "";
5099
- const labelSpan = document.createElement("span");
5100
- labelSpan.className = options.css["entryLabel"] ?? "";
5101
- const iconSpan = document.createElement("span");
5102
- iconSpan.className = options.css["entryIcon"] ?? "";
5103
- iconSpan.innerHTML = options.icon;
5104
- entry.append(iconSpan, labelSpan);
5105
- const applyLabel = () => {
5106
- entry.setAttribute("aria-label", options.label());
5107
- if (options.tooltip !== void 0) entry.setAttribute("title", options.tooltip());
5108
- labelSpan.textContent = options.label();
5109
- };
5110
- applyLabel();
5111
- entry.addEventListener("click", options.onToggle);
5112
- return {
5113
- entry,
5114
- applyLabel
5115
- };
5116
- }
5117
- /** Re-insert the entry after the New Session row (before the browser region). */
5118
- function placeEntry(root, entry, options) {
5119
- const button = newSessionButton(root);
5120
- if (button === void 0) return false;
5121
- if (entry.parentElement !== root) {
5122
- const row = button.closest("[class*=\"logoRow\"]");
5123
- const base = row !== null && row.parentElement === root ? row : button;
5124
- const family = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches(options.familySelectors.join(", ")));
5125
- const anchor = options.position === "before" ? family.length > 0 ? family[0] : base.nextElementSibling : family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling;
5126
- root.insertBefore(entry, anchor);
5127
- }
5128
- return true;
5129
- }
5130
- /**
5131
- * Mount the sidebar entry, waiting for the shell to render and self-healing
5132
- * on later React re-renders.
5133
- * @param options - the row's attribute/icon/copy/action/ordering configuration.
5134
- * @returns disposer removing the entry and its observers.
5135
- */
5136
- function mountSidebarEntry$1(options) {
5137
- if (typeof document !== "undefined" && document.querySelector(options.rowSelector) !== null) return () => {};
5138
- const { entry, applyLabel } = createEntry(options);
5139
- let root;
5140
- let placed = false;
5141
- let unsubscribeRefresh;
5142
- if (options.refresh !== void 0) try {
5143
- unsubscribeRefresh = options.refresh.subscribe(applyLabel);
5144
- } catch {}
5145
- const tryPlace = () => {
5146
- if (root !== void 0 && !root.isConnected) {
5147
- rootObserver.disconnect();
5148
- root = void 0;
5149
- placed = false;
5150
- }
5151
- if (placed) {
5152
- if (document.body.contains(entry)) return;
5153
- rootObserver.disconnect();
5154
- root = void 0;
5155
- placed = false;
5156
- }
5157
- root ??= sidebarRoot();
5158
- if (root === void 0) return;
5159
- placed = placeEntry(root, entry, options);
5160
- if (placed) rootObserver.observe(root, {
5161
- childList: true,
5162
- subtree: true
5163
- });
5164
- };
5165
- const unsubscribeBody = subscribeBodyInvalidations(() => {
5166
- tryPlace();
5167
- });
5168
- const rootObserver = new MutationObserver(() => {
5169
- if (root === void 0 || !root.isConnected) {
5170
- placed = false;
5171
- tryPlace();
5172
- return;
5173
- }
5174
- if (!root.contains(entry)) placed = placeEntry(root, entry, options);
5175
- });
5176
- const unsubscribeActive = options.active === void 0 ? void 0 : (() => {
5177
- const syncActive = () => {
5178
- if (options.active.isOpen()) entry.dataset.active = "true";
5179
- else delete entry.dataset.active;
5180
- };
5181
- const unsubscribe = options.active.subscribe(syncActive);
5182
- syncActive();
5183
- return unsubscribe;
5184
- })();
5185
- tryPlace();
5534
+ function registerTaskBoardPanel(ctx, controller) {
5535
+ const slots = ctx.slots;
5536
+ const disposers = [];
5537
+ disposers.push(slots.inject("sidebar.panellist", () => slots.register({
5538
+ name: "sidebar.panellist",
5539
+ id: TASK_BOARD_PANEL_ID,
5540
+ order: PANEL_ORDER,
5541
+ label: () => t("entry.label")
5542
+ }, TaskBoardPanelIcon)));
5543
+ disposers.push(slots.inject("main", () => slots.register({
5544
+ name: "main",
5545
+ key: TASK_BOARD_PANEL_ID,
5546
+ inject: () => ({ controller })
5547
+ }, TaskBoardPanel)));
5186
5548
  return () => {
5187
- unsubscribeBody();
5188
- rootObserver.disconnect();
5189
- unsubscribeRefresh?.();
5190
- unsubscribeActive?.();
5191
- entry.remove();
5549
+ for (const dispose of disposers.splice(0)) dispose();
5192
5550
  };
5193
5551
  }
5194
5552
  //#endregion
5195
- //#region src/client/sidebar-entry.ts
5196
- /** Stable data attribute identifying the injected entry row. */
5197
- const ENTRY_SELECTOR = "[data-dsh-taskboard-entry]";
5198
- /** Inline icon normalized to the shell's 16px navigation glyph size. */
5199
- const ICON = "<svg viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" 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>";
5200
- /**
5201
- * Mount the sidebar entry, waiting for the shell to render and self-healing
5202
- * on later React re-renders.
5203
- * @param controller - the board controller the entry toggles.
5204
- * @param locale - locale-change source; when given, re-applies the label on
5205
- * a Language switch (the plain-DOM row otherwise keeps the mount-time copy).
5206
- * @returns disposer removing the entry and its observers.
5207
- */
5208
- function mountSidebarEntry(controller, locale) {
5209
- return mountSidebarEntry$1({
5210
- rowAttribute: "data-dsh-taskboard-entry",
5211
- rowSelector: ENTRY_SELECTOR,
5212
- plugin: "task-board",
5213
- icon: ICON,
5214
- css: board_module_css_default,
5215
- label: () => t("entry.label"),
5216
- refresh: locale === void 0 ? void 0 : { subscribe: (listener) => locale.subscribe(listener) },
5217
- onToggle: () => {
5218
- controller.toggleBoard();
5219
- },
5220
- position: "before",
5221
- familySelectors: ["[data-dsh-taskboard-entry]", "[data-dsh-ssh-entry]"],
5222
- active: {
5223
- subscribe: (listener) => controller.subscribe(listener),
5224
- isOpen: () => controller.getSnapshot().boardOpen
5225
- }
5226
- });
5227
- }
5228
- //#endregion
5229
5553
  //#region \0dsh-css:packages/dsh-task-board/src/client/settings-card.module.css.mjs
5230
5554
  const css = ".Jh0q7G_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.Jh0q7G_card:hover{border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_header{appearance:none;box-sizing:border-box;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.Jh0q7G_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.Jh0q7G_headerStatic{box-sizing:border-box;border-radius:12px;align-items:center;gap:12px;width:100%;padding:14px 16px;display:flex}.Jh0q7G_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.Jh0q7G_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.Jh0q7G_description{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.5}.Jh0q7G_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Jh0q7G_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.Jh0q7G_chevronOpen{transform:rotate(180deg)}.Jh0q7G_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.Jh0q7G_readOnly{color:var(--dsw-alias-label-secondary);margin:12px 0 0;font-size:12px;line-height:1.5}.Jh0q7G_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.Jh0q7G_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.Jh0q7G_failed{min-width:0;color:var(--dsw-alias-state-error-primary,#b42318);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:12px;line-height:1.5;overflow:hidden}.Jh0q7G_discard,.Jh0q7G_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.Jh0q7G_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.Jh0q7G_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.Jh0q7G_discard:disabled,.Jh0q7G_save:disabled{opacity:.4;cursor:default}.Jh0q7G_discard:focus-visible,.Jh0q7G_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.Jh0q7G_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.Jh0q7G_field+.Jh0q7G_field{border-top:1px solid var(--dsw-alias-border-l2)}.Jh0q7G_head{align-items:center;gap:8px;display:flex}.Jh0q7G_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.Jh0q7G_badges{align-items:center;gap:8px;display:inline-flex}.Jh0q7G_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Jh0q7G_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.Jh0q7G_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.Jh0q7G_reset:disabled{cursor:default}.Jh0q7G_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.Jh0q7G_input,.Jh0q7G_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Jh0q7G_input:focus-visible,.Jh0q7G_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.Jh0q7G_input:disabled,.Jh0q7G_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.Jh0q7G_inputInvalid{border:1px solid var(--dsw-alias-state-error-primary,#b42318);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Jh0q7G_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-state-error-primary,#b42318);outline-offset:1px;border-color:var(--dsw-alias-state-error-primary,#b42318)}.Jh0q7G_selectWrap{position:relative}.Jh0q7G_selectButton{appearance:none;text-align:left;cursor:pointer;justify-content:space-between;align-items:center;gap:8px;width:100%;display:flex}.Jh0q7G_selectLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.Jh0q7G_selectChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.Jh0q7G_selectChevronOpen{transform:rotate(180deg)}.Jh0q7G_selectPopup{z-index:40;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);max-height:240px;box-shadow:0 8px 24px var(--dsw-alias-bg-mask-2);opacity:0;border-radius:8px;flex-direction:column;padding:4px;transition:opacity .1s,transform .1s;display:flex;position:absolute;top:calc(100% + 4px);left:0;right:0;overflow-y:auto;transform:translateY(-4px)}.Jh0q7G_selectPopupOpen{opacity:1;transform:none}.Jh0q7G_selectPopupClose{opacity:0;pointer-events:none;transform:translateY(-4px)}.Jh0q7G_selectOption{color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;text-overflow:ellipsis;border-radius:6px;flex-shrink:0;padding:6px 10px;font-size:13px;line-height:1.5;overflow:hidden}.Jh0q7G_selectOption:hover,.Jh0q7G_selectOptionActive{background:var(--dsw-alias-interactive-bg-hover)}.Jh0q7G_selectOptionSelected{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary-new-colorprimary-new-color) 10%, transparent);font-weight:500}.Jh0q7G_invalid{color:var(--dsw-alias-state-error-primary,#b42318);margin:0;font-size:12px;line-height:1.5}.Jh0q7G_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.Jh0q7G_card,.Jh0q7G_header,.Jh0q7G_chevron,.Jh0q7G_chevronOpen,.Jh0q7G_discard,.Jh0q7G_save,.Jh0q7G_selectChevron,.Jh0q7G_selectChevronOpen,.Jh0q7G_selectPopup{transition:none}}";
5231
5555
  const tagId = "@linxin666/dsh-client-ui-task-board/packages/dsh-task-board/src/client/settings-card.module.css";
@@ -6144,6 +6468,71 @@ window.__ModuleLoader__.load({
6144
6468
  });
6145
6469
  }
6146
6470
  //#endregion
6471
+ //#region src/core/degraded-reason.ts
6472
+ /**
6473
+ * The one degraded record naming `plugin`, or undefined when that row is not
6474
+ * degraded (or the payload does not come from the shell's own shape). A
6475
+ * plugin may appear once: the shell keys the ledger by package name and
6476
+ * refreshes the record on every new failure.
6477
+ * @param body - parsed JSON body of the health route.
6478
+ * @param plugin - the real plugin package name to look for.
6479
+ * @returns the degraded row, or undefined when the ledger does not name it.
6480
+ */
6481
+ function degradedRowOf(body, plugin) {
6482
+ if (typeof body !== "object" || body === null) return void 0;
6483
+ const ledger = body;
6484
+ if (ledger.ok !== true || !Array.isArray(ledger.degraded)) return void 0;
6485
+ for (const entry of ledger.degraded) {
6486
+ if (typeof entry !== "object" || entry === null) continue;
6487
+ const record = entry;
6488
+ if (record.plugin !== plugin) continue;
6489
+ const stage = record.stage === "import" || record.stage === "shape" || record.stage === "start" ? record.stage : void 0;
6490
+ if (stage === void 0) continue;
6491
+ return {
6492
+ plugin,
6493
+ stage,
6494
+ reason: typeof record.reason === "string" ? record.reason : ""
6495
+ };
6496
+ }
6497
+ }
6498
+ /** The ledger owner pid a task-board lock reason names, when it names one. */
6499
+ const OWNED_BY_PROCESS_RE = /already owned by process\s+(\d+)/;
6500
+ /**
6501
+ * The marker the ledger appends when it could NOT confirm the recorded PID
6502
+ * belongs to the lock's real owner: the PID was reused by an unrelated process
6503
+ * after a crash, and the reason then carries the manual-removal step instead.
6504
+ * That reason must be rendered verbatim — telling the user to close "the other
6505
+ * DSH instance" would send them after a process that never touched the board.
6506
+ */
6507
+ const UNCONFIRMED_OWNER_MARKER = "reused after a crash";
6508
+ /**
6509
+ * Extract the pid of the LIVE process a ledger-lock failure blames, when the
6510
+ * reason identifies one. The task board's lock refusal is the one degraded
6511
+ * reason that carries an owner the user can act on, so the panel renders the
6512
+ * pid in its own sentence and adds the guidance that actually applies.
6513
+ * @param reason - the shell's one-line failure reason.
6514
+ * @returns the confirmed owner pid, or undefined when the reason names none or
6515
+ * warns that the PID may have been reused (its own text owns the recovery).
6516
+ */
6517
+ function ledgerOwnerPid(reason) {
6518
+ if (reason.includes(UNCONFIRMED_OWNER_MARKER)) return void 0;
6519
+ const match = OWNED_BY_PROCESS_RE.exec(reason);
6520
+ if (match === null) return void 0;
6521
+ const pid = Number(match[1]);
6522
+ return Number.isSafeInteger(pid) && pid > 0 ? pid : void 0;
6523
+ }
6524
+ //#endregion
6525
+ //#region src/core/package-name.ts
6526
+ /**
6527
+ * npm identity of this plugin. The dsh-web-all degraded ledger is keyed by the
6528
+ * real plugin package name of a family row, and this plugin's browser half
6529
+ * must name its OWN row there to learn why its Host API is missing (issue
6530
+ * #1730): the served bundle cannot derive the name, because a family row
6531
+ * mounts the package from a versioned profile path. The host half spells the
6532
+ * same name in its single-mount guard (src/index.ts); the two move together.
6533
+ */
6534
+ const TASK_BOARD_PACKAGE = "@linxin666/dsh-client-ui-task-board";
6535
+ //#endregion
6147
6536
  //#region src/protocol.ts
6148
6537
  const TASK_BOARD_API_PREFIX = "/api/task-board";
6149
6538
  /** Whether a decoded reply carries the three draft strings the form accepts. */
@@ -6182,6 +6571,90 @@ window.__ModuleLoader__.load({
6182
6571
  this.name = "HostApiError";
6183
6572
  }
6184
6573
  };
6574
+ /**
6575
+ * Same-origin family health route served by the dsh-web-all shell: the
6576
+ * degraded ledger, in which a family row that failed during apply appears WITH
6577
+ * its reason. DOCUMENT-RELATIVE for the same reason as the board's own routes
6578
+ * (issue #1707). A 404 here means the family shell is absent or older than the
6579
+ * route, which is not itself a failure of the board.
6580
+ */
6581
+ const FAMILY_DEGRADED_ROUTE = "api/dsh-web-all/degraded";
6582
+ /** Health-route ceiling: an unresponsive family shell must not hold the board's first paint. */
6583
+ const DEGRADED_LOOKUP_TIMEOUT_MS = 2e3;
6584
+ /**
6585
+ * The failure text for a row this browser knows failed during start.
6586
+ *
6587
+ * A ledger lock is the one class with an owner the user can act on, so its
6588
+ * reason is unpacked into the pid plus what to do about it; every other
6589
+ * degraded row is reported with the shell's own reason verbatim. The raw
6590
+ * reason rides along either way, because it is the only text that names the
6591
+ * lock file or the recovery step when the pid is not the whole story.
6592
+ * @param reason - the shell's one-line failure reason (never empty).
6593
+ */
6594
+ function degradedFailureMessage(reason) {
6595
+ const pid = ledgerOwnerPid(reason);
6596
+ if (pid === void 0) return {
6597
+ failure: "degraded",
6598
+ message: t("board.hostError.degraded", { detail: reason })
6599
+ };
6600
+ return {
6601
+ failure: "ledger-locked",
6602
+ message: t("board.hostError.ledgerLocked", {
6603
+ pid: String(pid),
6604
+ detail: reason
6605
+ })
6606
+ };
6607
+ }
6608
+ /**
6609
+ * The degraded record the family shell holds for THIS board, or undefined when
6610
+ * the row is healthy, the shell is absent, or the answer is unusable. The
6611
+ * lookup is best-effort by contract: any uncertainty keeps the caller's own
6612
+ * failure text instead of replacing it with a weaker one.
6613
+ * @param fetchImpl - fetch implementation (injected for tests).
6614
+ * @returns the ledger's reason for this row, or undefined when it names none.
6615
+ */
6616
+ async function fetchDegradedReason(fetchImpl) {
6617
+ const controller = new AbortController();
6618
+ const timer = globalThis.setTimeout(() => {
6619
+ controller.abort();
6620
+ }, DEGRADED_LOOKUP_TIMEOUT_MS);
6621
+ try {
6622
+ const response = await fetchImpl(FAMILY_DEGRADED_ROUTE, {
6623
+ signal: controller.signal,
6624
+ headers: { accept: "application/json" },
6625
+ cache: "no-store"
6626
+ });
6627
+ if (!response.ok) return void 0;
6628
+ return degradedRowOf(await response.json(), TASK_BOARD_PACKAGE)?.reason;
6629
+ } catch {
6630
+ return;
6631
+ } finally {
6632
+ globalThis.clearTimeout(timer);
6633
+ }
6634
+ }
6635
+ /**
6636
+ * Name the real reason this board's routes are missing (issue #1730).
6637
+ *
6638
+ * The core webserver answers an unmounted route with a bare 404, which cannot
6639
+ * distinguish "this deployment never loaded the plugin" from "this row
6640
+ * degraded during start" — a second DSH process holding `ledger-v2.lock` is
6641
+ * the case that matters, because the panel's advice ("restart the service")
6642
+ * is then false: the lock belongs to the other process, and restarting cannot
6643
+ * win it. The family health route knows the difference, so the 404 is
6644
+ * disambiguated there before anything is shown.
6645
+ * @param fallback - copy for the plain "routes are not there" failure, which
6646
+ * is route-specific (the board's state route and the AI-parse route phrase
6647
+ * their own absence); it is also what is shown when the ledger names nothing.
6648
+ * @param fetchImpl - fetch implementation (injected for tests).
6649
+ * @returns the HostApiError to throw for a missing board route.
6650
+ */
6651
+ async function missingRouteFailure(fallback = t("board.hostError.notMounted"), fetchImpl = fetch) {
6652
+ const absent = () => new HostApiError("not-mounted", fallback, 404);
6653
+ const reason = await fetchDegradedReason(fetchImpl);
6654
+ if (reason === void 0 || reason === "") return absent();
6655
+ const described = degradedFailureMessage(reason);
6656
+ return new HostApiError(described.failure, described.message, 404);
6657
+ }
6185
6658
  function uuid() {
6186
6659
  return globalThis.crypto?.randomUUID?.() ?? `browser-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
6187
6660
  }
@@ -6206,12 +6679,13 @@ window.__ModuleLoader__.load({
6206
6679
  return parsed;
6207
6680
  }
6208
6681
  if (hostError !== void 0) {
6209
- if (hostError === "forbidden") throw new HostApiError("unauthorized", t("board.hostError.unauthorized"), response.status);
6682
+ if (hostError === "forbidden") throw new HostApiError("forbidden", t("board.hostError.forbidden"), response.status);
6210
6683
  if (response.status === 503 || /lock/i.test(hostError)) throw new HostApiError("locked", t("board.hostError.locked", { detail: hostError }), response.status);
6211
6684
  throw new HostApiError("rejected", hostError, response.status);
6212
6685
  }
6213
- if (response.status === 404) throw new HostApiError("not-mounted", t("board.hostError.notMounted"), 404);
6214
- if (response.status === 401 || response.status === 403) throw new HostApiError("unauthorized", t("board.hostError.unauthorized"), response.status);
6686
+ if (response.status === 404) throw await missingRouteFailure();
6687
+ if (response.status === 403) throw new HostApiError("forbidden", t("board.hostError.forbidden"), 403);
6688
+ if (response.status === 401) throw new HostApiError("unauthorized", t("board.hostError.unauthorized"), 401);
6215
6689
  throw new HostApiError("unexpected", t("board.hostError.unexpected", { status: String(response.status) }), response.status);
6216
6690
  }
6217
6691
  var HttpTaskBoardHostTransport = class {
@@ -6310,8 +6784,9 @@ window.__ModuleLoader__.load({
6310
6784
  if (record !== void 0 && isTaskParseDraft(record.draft)) return record.draft;
6311
6785
  throw new HostApiError("unexpected", t("new.aiParseFailed", { error: t("board.hostError.unexpected", { status: String(response.status) }) }), response.status);
6312
6786
  }
6313
- if (response.status === 404) throw new HostApiError("not-mounted", t("new.aiParseUnavailable"), 404);
6314
- if (response.status === 401 || response.status === 403) throw new HostApiError("unauthorized", t("board.hostError.unauthorized"), response.status);
6787
+ if (response.status === 404) throw await missingRouteFailure(t("new.aiParseUnavailable"));
6788
+ if (response.status === 403) throw new HostApiError("forbidden", t("board.hostError.forbidden"), 403);
6789
+ if (response.status === 401) throw new HostApiError("unauthorized", t("board.hostError.unauthorized"), 401);
6315
6790
  const code = typeof record?.code === "string" ? record.code : void 0;
6316
6791
  if (code === "no-model") throw new HostApiError("rejected", t("new.aiParseNoModel"), response.status);
6317
6792
  if (code === "timeout") throw new HostApiError("timeout", t("new.aiParseTimeout", { seconds: String(TASK_PARSE_TIMEOUT_SECONDS) }), response.status);
@@ -6353,7 +6828,7 @@ window.__ModuleLoader__.load({
6353
6828
  /** The building package's version, when the bundle carries it. */
6354
6829
  function bakedVersion() {
6355
6830
  try {
6356
- return "0.4.2";
6831
+ return "0.4.4";
6357
6832
  } catch {
6358
6833
  return;
6359
6834
  }
@@ -6541,6 +7016,96 @@ window.__ModuleLoader__.load({
6541
7016
  reconcile();
6542
7017
  }
6543
7018
  //#endregion
7019
+ //#region src/client/settings-entry-form.ts
7020
+ /** The snapshot a form reports before the Host has answered with this entry. */
7021
+ function pendingSnapshot() {
7022
+ return {
7023
+ status: "loading",
7024
+ value: void 0,
7025
+ base: void 0,
7026
+ user: void 0,
7027
+ revision: void 0,
7028
+ writable: false,
7029
+ mode: "host"
7030
+ };
7031
+ }
7032
+ /**
7033
+ * Create a settings form bound to the profile entry id this Host serves, and
7034
+ * rebound whenever the shared describe mirror names a different one of this
7035
+ * package's rows.
7036
+ * @param options - the shared forms service and this package's candidate row ids.
7037
+ * @returns a form delegating to the currently resolved entry's own form.
7038
+ */
7039
+ function createServedEntryForm(options) {
7040
+ const { forms, entryIds } = options;
7041
+ const fallbackId = entryIds[0];
7042
+ const namespaceId = entryIds[entryIds.length - 1];
7043
+ const listeners = /* @__PURE__ */ new Set();
7044
+ let boundId;
7045
+ let bound;
7046
+ let offBound;
7047
+ let snapshot = pendingSnapshot();
7048
+ /** Republish: the delegated snapshot when one is bound, the pending one otherwise. */
7049
+ const publish = () => {
7050
+ if (bound !== void 0) snapshot = bound.getSnapshot();
7051
+ for (const listener of [...listeners]) listener();
7052
+ };
7053
+ /**
7054
+ * The entry id the mirror currently justifies. An unanswered or empty mirror
7055
+ * is not evidence of absence, so it keeps the first candidate; a mirror that
7056
+ * answers without any of this package's rows leaves the namespace itself.
7057
+ * @returns the entry id to bind.
7058
+ */
7059
+ const resolve = () => {
7060
+ let served;
7061
+ try {
7062
+ served = forms.describe().getSnapshot().view?.namespaces.map((row) => row.ns);
7063
+ } catch {
7064
+ served = void 0;
7065
+ }
7066
+ if (served === void 0 || served.length === 0) return fallbackId;
7067
+ return entryIds.find((id) => served.includes(id)) ?? namespaceId;
7068
+ };
7069
+ /** Bind (or rebind) the resolved entry's form; a no-op while it is unchanged. */
7070
+ const bind = () => {
7071
+ const target = resolve();
7072
+ if (target === boundId) return;
7073
+ offBound?.();
7074
+ offBound = void 0;
7075
+ let form;
7076
+ try {
7077
+ form = forms.get(target);
7078
+ } catch {
7079
+ boundId = void 0;
7080
+ return;
7081
+ }
7082
+ boundId = target;
7083
+ bound = form;
7084
+ offBound = form.subscribe(() => {
7085
+ publish();
7086
+ });
7087
+ publish();
7088
+ };
7089
+ try {
7090
+ forms.describe().subscribe(() => {
7091
+ bind();
7092
+ });
7093
+ } catch {}
7094
+ bind();
7095
+ return {
7096
+ getSnapshot: () => snapshot,
7097
+ subscribe: (listener) => {
7098
+ listeners.add(listener);
7099
+ return () => {
7100
+ listeners.delete(listener);
7101
+ };
7102
+ },
7103
+ set: (field, value) => bound?.set(field, value) ?? Promise.resolve(false),
7104
+ unset: (field) => bound?.unset(field) ?? Promise.resolve(false),
7105
+ mutate: (ops, expectedRevision) => bound?.mutate(ops, expectedRevision) ?? Promise.resolve(false)
7106
+ };
7107
+ }
7108
+ //#endregion
6544
7109
  //#region src/client/index.ts
6545
7110
  /** Locale namespace this plugin owns. */
6546
7111
  const NS = "task-board";
@@ -6581,9 +7146,48 @@ window.__ModuleLoader__.load({
6581
7146
  "locale",
6582
7147
  "remote",
6583
7148
  "remote.session",
6584
- "uiWorkspace"
7149
+ "uiWorkspace",
7150
+ "layout"
6585
7151
  ];
6586
7152
  /**
7153
+ * Read the agent-preset roster through whichever face the running host
7154
+ * serves: the generated api-remotes face (`remote.agentPresets`,
7155
+ * 0.1.2-alpha.2) or the connection RPC face
7156
+ * (`connection.api.agentPresets`, hosts below that cohort). Answers
7157
+ * undefined when the host serves neither, so the caller leaves the picker
7158
+ * options untouched instead of erroring.
7159
+ */
7160
+ async function readPresetRoster(ctx, remote) {
7161
+ let remotes;
7162
+ try {
7163
+ remotes = remote.agentPresets;
7164
+ } catch {
7165
+ remotes = void 0;
7166
+ }
7167
+ if (remotes !== void 0) {
7168
+ const response = await remotes.list();
7169
+ if (!response.ok) return {
7170
+ ok: false,
7171
+ presets: []
7172
+ };
7173
+ return {
7174
+ ok: true,
7175
+ presets: response.value.presets
7176
+ };
7177
+ }
7178
+ const legacy = ctx.get("connection").api?.agentPresets;
7179
+ if (legacy === void 0) return void 0;
7180
+ const response = await legacy.list({});
7181
+ if (!response.result.ok || response.result.value === void 0) return {
7182
+ ok: false,
7183
+ presets: []
7184
+ };
7185
+ return {
7186
+ ok: true,
7187
+ presets: response.result.value.presets ?? []
7188
+ };
7189
+ }
7190
+ /**
6587
7191
  * Mount the task board.
6588
7192
  * @param ctx - client root context (services: sessions, workspaces).
6589
7193
  */
@@ -6626,6 +7230,9 @@ window.__ModuleLoader__.load({
6626
7230
  const controller = new BoardController({
6627
7231
  store: new LocalStorageTaskStore(),
6628
7232
  transport: new HttpTaskBoardHostTransport(),
7233
+ panel: { select: (panelId) => {
7234
+ ctx.get("layout")?.selectPanel?.(panelId);
7235
+ } },
6629
7236
  sessions: {
6630
7237
  current: () => mainViewSessionId(sessions.list.getSnapshot().byId),
6631
7238
  open: (id) => ctx.uiWorkspace.openSession(id),
@@ -6646,6 +7253,21 @@ window.__ModuleLoader__.load({
6646
7253
  controller.setWorkspaceCreator(async (path) => {
6647
7254
  return { workspaceId: (await workspaces.create({ path })).workspaceId };
6648
7255
  });
7256
+ const pushPresetOptions = async () => {
7257
+ try {
7258
+ const roster = await readPresetRoster(ctx, remote);
7259
+ if (roster === void 0 || !roster.ok) return;
7260
+ controller.setExecutionOptions({ presets: roster.presets.map((preset) => ({
7261
+ id: preset.id,
7262
+ name: preset.name,
7263
+ description: preset.description,
7264
+ broken: preset.broken,
7265
+ isDefault: preset.isDefault
7266
+ })) });
7267
+ } catch (error) {
7268
+ console.error("[dsh-task-board] agent preset roster read failed", error);
7269
+ }
7270
+ };
6649
7271
  const pushModelOptions = async () => {
6650
7272
  try {
6651
7273
  let models = [];
@@ -6701,15 +7323,25 @@ window.__ModuleLoader__.load({
6701
7323
  console.error("[dsh-task-board] model options read failed", error);
6702
7324
  }
6703
7325
  };
7326
+ pushPresetOptions();
6704
7327
  pushModelOptions();
6705
7328
  disposers.push(ctx.on("connection/reset", () => {
7329
+ pushPresetOptions();
6706
7330
  pushModelOptions();
6707
7331
  }));
6708
7332
  try {
6709
- disposers.push(mountSidebarEntry(controller, ctx.locale));
6710
- disposers.push(mountBoard(controller, ctx.locale));
7333
+ disposers.push(registerTaskBoardPanel(ctx, controller));
7334
+ const layoutFace = ctx.get("layout");
7335
+ if (layoutFace?.panelInfo !== void 0) {
7336
+ const sync = () => {
7337
+ const active = layoutFace.panelInfo.getSnapshot().activePanelId;
7338
+ controller.syncPanelSelection(active === "task-board" ? TASK_BOARD_PANEL_ID : null);
7339
+ };
7340
+ sync();
7341
+ disposers.push(layoutFace.panelInfo.subscribe(sync));
7342
+ }
6711
7343
  } catch (error) {
6712
- console.error("[dsh-task-board] mount failed:", error);
7344
+ console.error("[dsh-task-board] panel registration failed:", error);
6713
7345
  }
6714
7346
  uiDisposer = () => {
6715
7347
  for (const dispose of disposers.splice(0)) dispose();
@@ -6744,19 +7376,23 @@ window.__ModuleLoader__.load({
6744
7376
  function bindSettingsForm(ctx) {
6745
7377
  const binder = ctx.get("webUiSettings");
6746
7378
  if (binder !== void 0 && typeof binder.bind === "function") return binder.bind({ namespace: TASK_BOARD_NS });
6747
- return ctx.configForms.get(servedEntryId(ctx.configForms));
7379
+ return createServedEntryForm({
7380
+ forms: ctx.configForms,
7381
+ entryIds: TASK_BOARD_ENTRY_IDS
7382
+ });
6748
7383
  }
6749
7384
  /**
6750
- * The profile entry id this package's own row carries.
7385
+ * The profile entry id this package's own row carries, for a page that serves
7386
+ * no family binder.
6751
7387
  *
6752
7388
  * The shared describe mirror is the only local evidence of which row id this
6753
- * profile actually serves, but it answers asynchronously: at plugin
6754
- * activation it usually holds nothing yet. An unanswered mirror therefore
6755
- * binds the aggregate row id rather than guessing among the candidates —
6756
- * the form is bound once for the session, so a wrong guess would leave the
6757
- * card reporting an unserved namespace even after the mirror settles.
7389
+ * profile actually serves, but it answers asynchronously: at plugin activation
7390
+ * it usually holds nothing yet. An unanswered mirror therefore binds the
7391
+ * aggregate row id rather than guessing among the candidates, and the binding
7392
+ * is re-resolved once the mirror answers — see
7393
+ * {@link createServedEntryForm}, which owns that rebinding.
6758
7394
  * @param forms - the shared configuration forms service.
6759
- * @returns the entry id to bind.
7395
+ * @returns the entry id to bind before the mirror answers.
6760
7396
  */
6761
7397
  function servedEntryId(forms) {
6762
7398
  let served;
@@ -6772,6 +7408,7 @@ window.__ModuleLoader__.load({
6772
7408
  exports.apply = apply;
6773
7409
  exports.bindSettingsForm = bindSettingsForm;
6774
7410
  exports.inject = inject;
7411
+ exports.servedEntryId = servedEntryId;
6775
7412
  return module.exports;
6776
7413
  }
6777
7414
  });