dsh-taskboard 0.1.0

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 (49) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +169 -0
  3. package/cordis.patch.yml +12 -0
  4. package/lib/client.js +2085 -0
  5. package/lib/host/execution.js +189 -0
  6. package/lib/host/execution.js.map +1 -0
  7. package/lib/host/protocol-text.js +37 -0
  8. package/lib/host/protocol-text.js.map +1 -0
  9. package/lib/host/routes.js +369 -0
  10. package/lib/host/routes.js.map +1 -0
  11. package/lib/host/scheduler.js +91 -0
  12. package/lib/host/scheduler.js.map +1 -0
  13. package/lib/host/sdk.js +145 -0
  14. package/lib/host/sdk.js.map +1 -0
  15. package/lib/host/store.js +112 -0
  16. package/lib/host/store.js.map +1 -0
  17. package/lib/host/tools.js +620 -0
  18. package/lib/host/tools.js.map +1 -0
  19. package/lib/index.js +91 -0
  20. package/lib/index.js.map +1 -0
  21. package/lib/invariant.js +22 -0
  22. package/lib/invariant.js.map +1 -0
  23. package/lib/shared/api.js +9 -0
  24. package/lib/shared/api.js.map +1 -0
  25. package/lib/shared/protocol.js +279 -0
  26. package/lib/shared/protocol.js.map +1 -0
  27. package/package.json +74 -0
  28. package/src/client/api.ts +90 -0
  29. package/src/client/board/NewTaskModal.tsx +8 -0
  30. package/src/client/board/TaskBoard.tsx +184 -0
  31. package/src/client/board/TaskCard.tsx +61 -0
  32. package/src/client/board/TaskDetail.tsx +210 -0
  33. package/src/client/board/TaskFormModal.tsx +257 -0
  34. package/src/client/board-mount.tsx +92 -0
  35. package/src/client/controller.ts +241 -0
  36. package/src/client/index.ts +87 -0
  37. package/src/client/sidebar-entry.ts +165 -0
  38. package/src/client/styles.ts +391 -0
  39. package/src/host/execution.ts +244 -0
  40. package/src/host/protocol-text.ts +37 -0
  41. package/src/host/routes.ts +387 -0
  42. package/src/host/scheduler.ts +107 -0
  43. package/src/host/sdk.ts +200 -0
  44. package/src/host/store.ts +139 -0
  45. package/src/host/tools.ts +631 -0
  46. package/src/index.ts +124 -0
  47. package/src/invariant.ts +22 -0
  48. package/src/shared/api.ts +98 -0
  49. package/src/shared/protocol.ts +475 -0
package/lib/client.js ADDED
@@ -0,0 +1,2085 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-taskboard",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
8
+ let react_dom_client = require("react-dom/client");
9
+ let react = require("react");
10
+ let react_jsx_runtime = require("react/jsx-runtime");
11
+
12
+ //#region src/client/api.ts
13
+ /** Unwrap the envelope or throw a readable error. */
14
+ async function unwrap(pending) {
15
+ const res = await pending;
16
+ const body = await res.json().catch(() => null);
17
+ if (body === null) throw new Error(`taskboard: HTTP ${res.status}`);
18
+ if (!body.ok) throw new Error(`taskboard: ${body.error.code}: ${body.error.message}`);
19
+ return body.value;
20
+ }
21
+ async function post(path, body) {
22
+ return unwrap(await fetch(path, {
23
+ method: "POST",
24
+ headers: { "content-type": "application/json" },
25
+ body: JSON.stringify(body)
26
+ }));
27
+ }
28
+ /** Build the client over fetch + EventSource. */
29
+ function createClient() {
30
+ return {
31
+ state: () => unwrap(fetch("/dsh-taskboard/state")),
32
+ workspaces: () => unwrap(fetch("/dsh-taskboard/workspaces")),
33
+ create: (body) => post("/dsh-taskboard/tasks", body),
34
+ get: (id) => unwrap(fetch(`/dsh-taskboard/tasks/${encodeURIComponent(id)}`)),
35
+ update: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/update`, body),
36
+ move: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/move`, body),
37
+ comment: (id, bodyText) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/comment`, { body: bodyText }),
38
+ remove: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/delete`, body),
39
+ run: (id) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/run`, {}),
40
+ stream(onChange, onGap) {
41
+ const es = new EventSource("/dsh-taskboard/events");
42
+ let revision;
43
+ const hello = (event) => {
44
+ const payload = JSON.parse(event.data);
45
+ if (revision !== void 0 && payload.revision !== revision) onGap();
46
+ revision = payload.revision;
47
+ };
48
+ const change = (event) => {
49
+ const payload = JSON.parse(event.data);
50
+ if (revision !== void 0 && payload.revision !== revision + 1) onGap();
51
+ revision = payload.revision;
52
+ onChange(payload);
53
+ };
54
+ es.addEventListener("hello", hello);
55
+ es.addEventListener("change", change);
56
+ es.onerror = () => {};
57
+ return () => {
58
+ es.close();
59
+ };
60
+ }
61
+ };
62
+ }
63
+
64
+ //#endregion
65
+ //#region src/shared/protocol.ts
66
+ /** Statuses shown as the five main board columns, in order. */
67
+ const MAIN_STATUSES = [
68
+ "backlog",
69
+ "todo",
70
+ "in_progress",
71
+ "in_review",
72
+ "done"
73
+ ];
74
+ /** Statuses collected under the secondary tab. */
75
+ const SECONDARY_STATUSES = ["canceled", "archived"];
76
+ /** Every valid status, main first. */
77
+ const ALL_STATUSES = [...MAIN_STATUSES, ...SECONDARY_STATUSES];
78
+ /**
79
+ * Legal forward/sideways transitions. Anything not listed is rejected with
80
+ * `invalid_transition`. `archived` is terminal.
81
+ */
82
+ const TRANSITIONS = {
83
+ backlog: ["todo", "canceled"],
84
+ todo: [
85
+ "in_progress",
86
+ "backlog",
87
+ "canceled"
88
+ ],
89
+ in_progress: [
90
+ "in_review",
91
+ "todo",
92
+ "canceled"
93
+ ],
94
+ in_review: [
95
+ "in_progress",
96
+ "todo",
97
+ "done",
98
+ "canceled"
99
+ ],
100
+ done: ["archived"],
101
+ canceled: ["archived", "todo"],
102
+ archived: []
103
+ };
104
+ /**
105
+ * Whether a status move is legal per the state machine.
106
+ * @param from - current status.
107
+ * @param to - requested status.
108
+ * @returns true when the transition is allowed.
109
+ */
110
+ function canTransition(from, to) {
111
+ return TRANSITIONS[from].includes(to);
112
+ }
113
+ /**
114
+ * Parse a five-field cron expression. Supported field syntax: star, star/step
115
+ * (`* / n` without spaces), a single number, an `a-b` range, and comma lists
116
+ * of those. Day-of-week accepts both 0 and 7 as Sunday (normalized to 0).
117
+ *
118
+ * @param expr - the expression to parse.
119
+ * @returns the match sets per field, or null when invalid.
120
+ */
121
+ function parseCron(expr) {
122
+ const fields = expr.trim().split(/\s+/);
123
+ if (fields.length !== 5) return null;
124
+ const ranges = [
125
+ [0, 59],
126
+ [0, 23],
127
+ [1, 31],
128
+ [1, 12],
129
+ [0, 7]
130
+ ];
131
+ const sets = [];
132
+ for (let i = 0; i < 5; i++) {
133
+ const [min, max] = ranges[i];
134
+ const set = /* @__PURE__ */ new Set();
135
+ if (!parseCronField(fields[i], min, max, set)) return null;
136
+ sets.push(set);
137
+ }
138
+ const weekdays = /* @__PURE__ */ new Set();
139
+ for (const day of sets[4]) weekdays.add(day === 7 ? 0 : day);
140
+ return {
141
+ minutes: sets[0],
142
+ hours: sets[1],
143
+ days: sets[2],
144
+ months: sets[3],
145
+ weekdays
146
+ };
147
+ }
148
+ /** Parse one cron field into a match set; false on any syntax error. */
149
+ function parseCronField(field, min, max, out) {
150
+ for (const part of field.split(",")) {
151
+ const [range, stepRaw] = part.split("/");
152
+ const step = stepRaw === void 0 ? 1 : Number.parseInt(stepRaw, 10);
153
+ if (!Number.isInteger(step) || step < 1) return false;
154
+ let lo;
155
+ let hi;
156
+ if (range === void 0 || range === "") return false;
157
+ if (range === "*") {
158
+ lo = min;
159
+ hi = max;
160
+ } else if (range.includes("-")) {
161
+ const [a, b] = range.split("-");
162
+ lo = Number.parseInt(a ?? "", 10);
163
+ hi = Number.parseInt(b ?? "", 10);
164
+ if (!Number.isInteger(lo) || !Number.isInteger(hi)) return false;
165
+ } else {
166
+ lo = Number.parseInt(range, 10);
167
+ if (!Number.isInteger(lo)) return false;
168
+ hi = stepRaw === void 0 ? lo : max;
169
+ }
170
+ if (lo < min || hi > max || lo > hi) return false;
171
+ for (let v = lo; v <= hi; v += step) out.add(v);
172
+ }
173
+ return out.size > 0;
174
+ }
175
+ /**
176
+ * The next time at or after `from` matching the cron sets (local time),
177
+ * or null when no match exists within four years (e.g. Feb 30).
178
+ * @param match - parsed cron sets.
179
+ * @param from - epoch ms start point (inclusive match candidate).
180
+ * @returns the next match's epoch ms, or null.
181
+ */
182
+ function nextCronTime(match, from) {
183
+ const start = new Date(from);
184
+ start.setSeconds(0, 0);
185
+ start.setMinutes(start.getMinutes() + 1);
186
+ const cap = from + 4 * 366 * 24 * 60 * 60 * 1e3;
187
+ let t = start.getTime();
188
+ while (t <= cap) {
189
+ const d = new Date(t);
190
+ if (match.months.has(d.getMonth() + 1) && match.days.has(d.getDate()) && match.weekdays.has(d.getDay()) && match.hours.has(d.getHours()) && match.minutes.has(d.getMinutes())) return t;
191
+ t += 6e4;
192
+ }
193
+ return null;
194
+ }
195
+ /** Current ledger format version. */
196
+ const LEDGER_SCHEMA_VERSION = 1;
197
+ /** An empty ledger. */
198
+ function emptyLedger() {
199
+ return {
200
+ schemaVersion: 1,
201
+ revision: 0,
202
+ tasks: []
203
+ };
204
+ }
205
+
206
+ //#endregion
207
+ //#region src/client/controller.ts
208
+ /** Instantiate the default state. */
209
+ function initialState() {
210
+ return {
211
+ boardOpen: false,
212
+ ledger: emptyLedger(),
213
+ workspaces: [],
214
+ filters: { urgencies: [] },
215
+ composerOpen: false,
216
+ secondaryOpen: false
217
+ };
218
+ }
219
+ /**
220
+ * The board controller.
221
+ */
222
+ var BoardController = class {
223
+ client;
224
+ state = initialState();
225
+ subscribers = /* @__PURE__ */ new Set();
226
+ disposed = false;
227
+ disposeStream;
228
+ refreshInFlight;
229
+ /** @param client - the route client. */
230
+ constructor(client) {
231
+ this.client = client;
232
+ }
233
+ /** Current snapshot (render input). */
234
+ getSnapshot() {
235
+ return this.state;
236
+ }
237
+ /** Subscribe; returns unsubscribe. */
238
+ subscribe(fn) {
239
+ this.subscribers.add(fn);
240
+ return () => this.subscribers.delete(fn);
241
+ }
242
+ emit() {
243
+ if (this.disposed) return;
244
+ for (const fn of this.subscribers) fn();
245
+ }
246
+ setState(patch) {
247
+ this.state = {
248
+ ...this.state,
249
+ ...patch
250
+ };
251
+ this.emit();
252
+ }
253
+ /** Start subscriptions; call once after construction. */
254
+ start() {
255
+ this.refresh();
256
+ this.disposeStream = this.client.stream((change) => {
257
+ this.setState({ ledger: {
258
+ ...this.state.ledger,
259
+ revision: change.revision
260
+ } });
261
+ this.refresh();
262
+ }, () => {
263
+ this.refresh();
264
+ });
265
+ }
266
+ /** Full refetch (state + workspaces + open detail). */
267
+ async refresh() {
268
+ if (this.refreshInFlight !== void 0) return this.refreshInFlight;
269
+ this.refreshInFlight = (async () => {
270
+ try {
271
+ const [ledger, workspaces] = await Promise.all([this.client.state(), this.client.workspaces()]);
272
+ let selected;
273
+ if (this.state.selectedId !== void 0) selected = ledger.tasks.find((t) => t.id === this.state.selectedId);
274
+ this.setState({
275
+ ledger,
276
+ workspaces,
277
+ error: void 0,
278
+ selectedId: selected === void 0 ? void 0 : this.state.selectedId
279
+ });
280
+ } catch (error) {
281
+ this.setState({ error: error instanceof Error ? error.message : String(error) });
282
+ } finally {
283
+ this.refreshInFlight = void 0;
284
+ }
285
+ })();
286
+ return this.refreshInFlight;
287
+ }
288
+ /** Stop everything. */
289
+ dispose() {
290
+ this.disposed = true;
291
+ this.disposeStream?.();
292
+ this.subscribers.clear();
293
+ }
294
+ /** Open the board (sidebar entry). */
295
+ openBoard() {
296
+ this.setState({ boardOpen: true });
297
+ }
298
+ /** Close the board. */
299
+ closeBoard() {
300
+ this.setState({ boardOpen: false });
301
+ }
302
+ /** Toggle the board. */
303
+ toggleBoard() {
304
+ this.setState({ boardOpen: !this.state.boardOpen });
305
+ }
306
+ /** Set the project filter. */
307
+ setWorkspaceFilter(workspaceId) {
308
+ this.setState({ filters: {
309
+ ...this.state.filters,
310
+ workspaceId
311
+ } });
312
+ }
313
+ /** Toggle one urgency chip. */
314
+ toggleUrgency(urgency) {
315
+ const set = new Set(this.state.filters.urgencies);
316
+ if (set.has(urgency)) set.delete(urgency);
317
+ else set.add(urgency);
318
+ this.setState({ filters: {
319
+ ...this.state.filters,
320
+ urgencies: [...set]
321
+ } });
322
+ }
323
+ /** Select a task (open detail). */
324
+ select(id) {
325
+ this.setState({ selectedId: id });
326
+ }
327
+ /** Show/hide the task form (create mode when opening). */
328
+ setComposer(open) {
329
+ this.setState({
330
+ composerOpen: open,
331
+ editingId: void 0
332
+ });
333
+ }
334
+ /** Open the form modal editing an existing task. */
335
+ openEditor(id) {
336
+ this.setState({
337
+ composerOpen: true,
338
+ editingId: id
339
+ });
340
+ }
341
+ /** Close the form modal whatever its mode. */
342
+ closeForm() {
343
+ this.setState({
344
+ composerOpen: false,
345
+ editingId: void 0
346
+ });
347
+ }
348
+ /** Toggle the secondary tab. */
349
+ toggleSecondary() {
350
+ this.setState({ secondaryOpen: !this.state.secondaryOpen });
351
+ }
352
+ /** Create a task (composer submit). */
353
+ async create(body) {
354
+ try {
355
+ await this.client.create(body);
356
+ this.setState({
357
+ composerOpen: false,
358
+ error: void 0
359
+ });
360
+ await this.refresh();
361
+ } catch (error) {
362
+ this.setState({ error: error instanceof Error ? error.message : String(error) });
363
+ }
364
+ }
365
+ /** Edit task fields (form modal submit; the GUI is the owner surface). */
366
+ async update(id, ifVersion, body) {
367
+ try {
368
+ await this.client.update(id, {
369
+ ifVersion,
370
+ ...body
371
+ });
372
+ this.setState({
373
+ composerOpen: false,
374
+ editingId: void 0,
375
+ error: void 0
376
+ });
377
+ await this.refresh();
378
+ } catch (error) {
379
+ this.setState({ error: error instanceof Error ? error.message : String(error) });
380
+ }
381
+ }
382
+ /** Move a task (user surface: done allowed). */
383
+ async move(id, ifVersion, status) {
384
+ try {
385
+ await this.client.move(id, {
386
+ ifVersion,
387
+ status
388
+ });
389
+ await this.refresh();
390
+ } catch (error) {
391
+ this.setState({ error: error instanceof Error ? error.message : String(error) });
392
+ }
393
+ }
394
+ /** Toggle the blocked marker. */
395
+ async toggleBlocked(task) {
396
+ try {
397
+ await this.client.update(task.id, {
398
+ ifVersion: task.version,
399
+ blocked: !task.blocked
400
+ });
401
+ await this.refresh();
402
+ } catch (error) {
403
+ this.setState({ error: error instanceof Error ? error.message : String(error) });
404
+ }
405
+ }
406
+ /** Append a user comment. */
407
+ async comment(id, body) {
408
+ try {
409
+ await this.client.comment(id, body);
410
+ await this.refresh();
411
+ } catch (error) {
412
+ this.setState({ error: error instanceof Error ? error.message : String(error) });
413
+ }
414
+ }
415
+ /** Trigger a manual run (fresh in-project session, pinned model). */
416
+ async run(id) {
417
+ try {
418
+ await this.client.run(id);
419
+ await this.refresh();
420
+ } catch (error) {
421
+ this.setState({ error: error instanceof Error ? error.message : String(error) });
422
+ }
423
+ }
424
+ /** Soft-delete (agent parity) then optional purge. */
425
+ async remove(id, ifVersion, purge) {
426
+ try {
427
+ await this.client.remove(id, purge ? { purge: true } : { ifVersion });
428
+ if (purge) this.setState({ selectedId: void 0 });
429
+ await this.refresh();
430
+ } catch (error) {
431
+ this.setState({ error: error instanceof Error ? error.message : String(error) });
432
+ }
433
+ }
434
+ };
435
+
436
+ //#endregion
437
+ //#region src/client/styles.ts
438
+ /**
439
+ * Board styles, injected as one global stylesheet with dsh-atb- prefixed
440
+ * classes. Colors ride the shell's --dsw-* design tokens where available so
441
+ * the board follows the active theme/skin; urgency accents are the fixed
442
+ * red/purple/blue of the protocol.
443
+ *
444
+ * @module dsh-taskboard/client/styles
445
+ */
446
+ /** The stylesheet text. */
447
+ const STYLES = `
448
+ .dsh-atb-entry {
449
+ display: flex; align-items: center; gap: 8px;
450
+ width: calc(100% - 8px); margin: 2px 4px; padding: 6px 10px;
451
+ border: none; border-radius: 8px; background: transparent;
452
+ color: var(--dsw-text-secondary, inherit); font: inherit; font-size: 13px;
453
+ cursor: pointer; text-align: left;
454
+ }
455
+ .dsh-atb-entry:hover { background: var(--dsw-hover, rgba(128,128,128,.12)); color: var(--dsw-text-primary, inherit); }
456
+ .dsh-atb-entry[data-active="true"] { background: var(--dsw-active, rgba(128,128,128,.18)); color: var(--dsw-text-primary, inherit); font-weight: 500; }
457
+ .dsh-atb-entry svg { flex: none; }
458
+
459
+ html[data-dsh-atb-active] [data-pane="conversation"] > *:not([data-dsh-atb-view]) { display: none !important; }
460
+ .dsh-atb-view { display: none; }
461
+ html[data-dsh-atb-active] .dsh-atb-view { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
462
+
463
+ .dsh-atb-board { display: flex; flex-direction: column; height: 100%; min-height: 0; padding: 12px 16px; gap: 10px; box-sizing: border-box; }
464
+ .dsh-atb-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
465
+ .dsh-atb-title { font-size: 15px; font-weight: 600; margin: 0; }
466
+ .dsh-atb-count { font-size: 12px; color: var(--dsw-text-secondary, gray); }
467
+ .dsh-atb-spacer { flex: 1; }
468
+ .dsh-atb-select, .dsh-atb-input {
469
+ font: inherit; font-size: 12.5px; padding: 5px 8px; border-radius: 7px;
470
+ border: 1px solid var(--dsw-border, rgba(128,128,128,.35));
471
+ background: var(--dsw-bg, transparent); color: inherit;
472
+ }
473
+ .dsh-atb-chip {
474
+ display: inline-flex; align-items: center; gap: 5px;
475
+ font-size: 12px; padding: 3px 9px; border-radius: 999px; cursor: pointer;
476
+ border: 1px solid var(--dsw-border, rgba(128,128,128,.35));
477
+ background: transparent; color: var(--dsw-text-secondary, inherit);
478
+ }
479
+ .dsh-atb-chip[data-on="true"] { color: #fff; border-color: transparent; }
480
+ .dsh-atb-chip[data-urgency="urgent"][data-on="true"] { background: #e5484d; }
481
+ .dsh-atb-chip[data-urgency="normal"][data-on="true"] { background: #8e4ec6; }
482
+ .dsh-atb-chip[data-urgency="relaxed"][data-on="true"] { background: #3e63dd; }
483
+ .dsh-atb-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
484
+ .dsh-atb-dot[data-urgency="urgent"] { background: #e5484d; }
485
+ .dsh-atb-dot[data-urgency="normal"] { background: #8e4ec6; }
486
+ .dsh-atb-dot[data-urgency="relaxed"] { background: #3e63dd; }
487
+
488
+ .dsh-atb-btn {
489
+ font: inherit; font-size: 12.5px; padding: 5px 11px; border-radius: 7px; cursor: pointer;
490
+ border: 1px solid var(--dsw-border, rgba(128,128,128,.35));
491
+ background: var(--dsw-bg-elevated, rgba(128,128,128,.08)); color: inherit;
492
+ }
493
+ .dsh-atb-btn:hover { background: var(--dsw-hover, rgba(128,128,128,.18)); }
494
+ .dsh-atb-btn:disabled { opacity: .45; cursor: default; }
495
+ .dsh-atb-btn[data-primary="true"] { background: var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary, #1f2328)); border-color: transparent; color: var(--dsw-alias-label-primary-foreground, #fff); }
496
+ .dsh-atb-btn[data-danger="true"] { color: var(--dsw-alias-state-error-primary, #e5484d); border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary, #e5484d) 45%, transparent); }
497
+
498
+ .dsh-atb-columns { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 10px; flex: 1; min-height: 0; overflow-x: auto; }
499
+
500
+ .dsh-atb-detailpanel {
501
+ display: flex; flex-direction: column;
502
+ flex: none; max-height: 55%; min-height: 180px; overflow: hidden;
503
+ border: 1px solid var(--dsw-border, rgba(128,128,128,.25)); border-radius: 12px;
504
+ background: var(--dsw-bg-panel, var(--dsw-bg-elevated, rgba(128,128,128,.05)));
505
+ padding: 10px 12px; box-shadow: 0 -4px 18px rgba(0,0,0,.12);
506
+ }
507
+ .dsh-atb-detailpanel .dsh-atb-detail { flex: 1; min-height: 0; }
508
+ .dsh-atb-column { display: flex; flex-direction: column; min-width: 200px; min-height: 0; border-radius: 10px; background: var(--dsw-bg-inset, rgba(128,128,128,.07)); padding: 8px; gap: 8px; }
509
+ .dsh-atb-colhead { display: flex; align-items: center; gap: 6px; font-size: 12.5px; font-weight: 600; padding: 2px 4px; }
510
+ .dsh-atb-colcount { font-size: 11px; font-weight: 400; color: var(--dsw-text-secondary, gray); }
511
+ .dsh-atb-cards { display: flex; flex-direction: column; gap: 8px; overflow-y: auto; min-height: 0; flex: 1; padding: 2px; }
512
+
513
+ .dsh-atb-card {
514
+ position: relative; border-radius: 9px; padding: 8px 10px 8px 13px; cursor: pointer;
515
+ background: var(--dsw-bg-elevated, rgba(128,128,128,.1));
516
+ border: 1px solid var(--dsw-border, rgba(128,128,128,.25));
517
+ font-size: 13px; text-align: left; color: inherit; width: 100%; box-sizing: border-box;
518
+ }
519
+ .dsh-atb-card:hover { border-color: var(--dsw-border-strong, rgba(128,128,128,.55)); }
520
+ .dsh-atb-card[draggable="true"] { cursor: grab; }
521
+ .dsh-atb-card[data-dragging] { opacity: .45; }
522
+ .dsh-atb-column[data-dragover] { outline: 2px dashed var(--dsw-border-strong, rgba(128,128,128,.55)); outline-offset: -2px; background: var(--dsw-bg-hover, rgba(128,128,128,.12)); }
523
+ .dsh-atb-card::before {
524
+ content: ""; position: absolute; left: 4px; top: 6px; bottom: 6px; width: 3.5px; border-radius: 3px;
525
+ }
526
+ .dsh-atb-card[data-urgency="urgent"]::before { background: #e5484d; }
527
+ .dsh-atb-card[data-urgency="normal"]::before { background: #8e4ec6; }
528
+ .dsh-atb-card[data-urgency="relaxed"]::before { background: #3e63dd; }
529
+ .dsh-atb-card-title { font-weight: 550; line-height: 1.35; word-break: break-word; }
530
+ .dsh-atb-card-meta { display: flex; align-items: center; gap: 6px; margin-top: 5px; font-size: 11px; color: var(--dsw-text-secondary, gray); flex-wrap: wrap; }
531
+ .dsh-atb-badge { font-size: 10.5px; padding: 1px 6px; border-radius: 5px; background: rgba(128,128,128,.18); }
532
+ .dsh-atb-badge[data-kind="blocked"] { background: rgba(229,72,77,.18); color: #e5484d; }
533
+ .dsh-atb-badge[data-kind="scheduled"] { background: rgba(62,99,221,.16); color: #3e63dd; }
534
+ .dsh-atb-badge[data-kind="trashed"] { background: rgba(229,72,77,.14); color: #e5484d; text-decoration: line-through; }
535
+ .dsh-atb-badge[data-kind="done"] { background: rgba(46,160,67,.16); color: #2ea043; }
536
+ .dsh-atb-badge[data-kind="running"] { background: rgba(229,152,42,.16); color: #e69842; }
537
+
538
+ .dsh-atb-error { font-size: 12px; color: #e5484d; padding: 4px 8px; border-radius: 6px; background: rgba(229,72,77,.1); }
539
+ .dsh-atb-empty { font-size: 12px; color: var(--dsw-text-secondary, gray); padding: 10px 4px; }
540
+
541
+ /* ---------- detail pane (polished) ---------- */
542
+ .dsh-atb-detail {
543
+ display: flex; flex-direction: column; gap: 12px; overflow-y: auto; min-height: 0; flex: 1;
544
+ padding: 2px; position: relative;
545
+ }
546
+ .dsh-atb-detail::before {
547
+ content: ""; position: sticky; top: 0; height: 3px; border-radius: 3px; flex: none;
548
+ }
549
+ .dsh-atb-detail[data-urgency="urgent"]::before { background: linear-gradient(90deg, #e5484d, rgba(229,72,77,.15)); }
550
+ .dsh-atb-detail[data-urgency="normal"]::before { background: linear-gradient(90deg, #8e4ec6, rgba(142,78,198,.15)); }
551
+ .dsh-atb-detail[data-urgency="relaxed"]::before { background: linear-gradient(90deg, #3e63dd, rgba(62,99,221,.15)); }
552
+
553
+ .dsh-atb-detail-head { display: flex; align-items: flex-start; gap: 10px; }
554
+ .dsh-atb-detail-titlewrap { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
555
+ .dsh-atb-detail-titlebar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
556
+ .dsh-atb-detail-titlebar h3 { margin: 0; font-size: 15.5px; line-height: 1.35; word-break: break-word; }
557
+ .dsh-atb-detail-close {
558
+ flex: none; width: 26px; height: 26px; border-radius: 7px; border: none; cursor: pointer;
559
+ background: transparent; color: var(--dsw-text-secondary, gray); font-size: 13px; line-height: 1;
560
+ }
561
+ .dsh-atb-detail-close:hover { background: var(--dsw-hover, rgba(128,128,128,.18)); color: inherit; }
562
+ .dsh-atb-detail-topbtns { display: flex; align-items: center; gap: 6px; flex: none; }
563
+ .dsh-atb-detail-edit {
564
+ font: inherit; font-size: 12px; padding: 4px 10px; border-radius: 7px; cursor: pointer;
565
+ border: 1px solid var(--dsw-border, rgba(128,128,128,.32));
566
+ background: var(--dsw-bg-elevated, rgba(128,128,128,.07)); color: var(--dsw-text-secondary, inherit);
567
+ }
568
+ .dsh-atb-detail-edit:hover { border-color: var(--dsw-alias-brand-primary, #1f2328); color: var(--dsw-alias-label-primary, inherit); }
569
+
570
+ .dsh-atb-statuspill {
571
+ flex: none; font-size: 11px; font-weight: 600; padding: 2px 9px; border-radius: 999px; letter-spacing: .02em;
572
+ }
573
+ .dsh-atb-statuspill[data-status="backlog"] { background: rgba(128,128,128,.18); color: var(--dsw-text-secondary, #888); }
574
+ .dsh-atb-statuspill[data-status="todo"] { background: rgba(62,99,221,.15); color: #3e63dd; }
575
+ .dsh-atb-statuspill[data-status="in_progress"] { background: rgba(230,152,66,.16); color: #d9822b; }
576
+ .dsh-atb-statuspill[data-status="in_review"] { background: rgba(142,78,198,.16); color: #8e4ec6; }
577
+ .dsh-atb-statuspill[data-status="done"] { background: rgba(46,160,67,.16); color: #2ea043; }
578
+ .dsh-atb-statuspill[data-status="canceled"], .dsh-atb-statuspill[data-status="archived"] { background: rgba(128,128,128,.14); color: var(--dsw-text-secondary, #888); }
579
+
580
+ .dsh-atb-detail-chips { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
581
+ .dsh-atb-chip2 {
582
+ display: inline-flex; align-items: center; gap: 4px; font-size: 11px; line-height: 1;
583
+ padding: 3px 8px; border-radius: 6px;
584
+ background: var(--dsw-bg-inset, rgba(128,128,128,.09)); color: var(--dsw-text-secondary, #999);
585
+ }
586
+ .dsh-atb-chip2-icon { font-size: 10.5px; opacity: .85; }
587
+ .dsh-atb-chip2[data-tone="urgent"] { background: rgba(229,72,77,.15); color: #e5484d; }
588
+ .dsh-atb-chip2[data-tone="normal"] { background: rgba(142,78,198,.14); color: #a06ce0; }
589
+ .dsh-atb-chip2[data-tone="relaxed"] { background: rgba(62,99,221,.13); color: #6d92e8; }
590
+ .dsh-atb-detail-sub { font-size: 11.5px; color: var(--dsw-text-secondary, gray); }
591
+
592
+ .dsh-atb-fieldcard {
593
+ border: 1px solid var(--dsw-border, rgba(128,128,128,.22)); border-radius: 10px;
594
+ padding: 9px 11px; display: flex; flex-direction: column; gap: 5px;
595
+ background: var(--dsw-bg-elevated, rgba(128,128,128,.06));
596
+ }
597
+ .dsh-atb-fieldcard-label {
598
+ font-size: 10.5px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
599
+ color: var(--dsw-text-secondary, gray);
600
+ }
601
+ .dsh-atb-fieldcard[data-kind="prompt"] .dsh-atb-fieldcard-label { color: #8e63c8; }
602
+ .dsh-atb-promptbox {
603
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
604
+ font-size: 12px; line-height: 1.55; white-space: pre-wrap; word-break: break-word;
605
+ background: var(--dsw-bg-inset, rgba(128,128,128,.08)); border-radius: 7px; padding: 8px 10px;
606
+ border: 1px dashed var(--dsw-border, rgba(128,128,128,.25));
607
+ }
608
+ .dsh-atb-desc { white-space: pre-wrap; word-break: break-word; font-size: 13px; line-height: 1.55; }
609
+
610
+ .dsh-atb-detail-actions { display: flex; flex-direction: column; gap: 8px; }
611
+ .dsh-atb-runbtn {
612
+ font: inherit; font-size: 13px; font-weight: 600; padding: 8px 14px; border-radius: 9px; cursor: pointer;
613
+ border: 1px solid transparent; background: var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary, #1f2328)); color: var(--dsw-alias-label-primary-foreground, #fff); text-align: center;
614
+ transition: filter .12s ease;
615
+ }
616
+ .dsh-atb-runbtn:hover { filter: brightness(1.1); }
617
+ .dsh-atb-movebtns { display: flex; gap: 6px; flex-wrap: wrap; }
618
+ .dsh-atb-movebtn {
619
+ font: inherit; font-size: 12px; padding: 4px 11px; border-radius: 999px; cursor: pointer;
620
+ border: 1px solid var(--dsw-border, rgba(128,128,128,.32));
621
+ background: var(--dsw-bg-elevated, rgba(128,128,128,.07)); color: var(--dsw-text-secondary, inherit);
622
+ transition: border-color .12s ease, color .12s ease;
623
+ }
624
+ .dsh-atb-movebtn:hover { border-color: var(--dsw-border-strong, rgba(128,128,128,.6)); color: inherit; }
625
+ .dsh-atb-movebtn[data-to="done"] { border-color: rgba(46,160,67,.55); color: #2ea043; }
626
+ .dsh-atb-movebtn[data-to="done"]:hover { background: rgba(46,160,67,.12); }
627
+ .dsh-atb-movebtn[data-to="canceled"], .dsh-atb-movebtn[data-to="archived"] { opacity: .75; }
628
+ .dsh-atb-movebtn[data-to="blocked"] { border-color: rgba(229,72,77,.45); }
629
+ .dsh-atb-movebtn[data-to="blocked"]:hover { background: rgba(229,72,77,.1); }
630
+ .dsh-atb-confirm { display: inline-flex; align-items: center; gap: 6px; }
631
+ .dsh-atb-confirm-label { font-size: 11.5px; color: var(--dsw-text-secondary, gray); }
632
+
633
+ .dsh-atb-section { font-size: 13px; display: flex; flex-direction: column; gap: 7px; }
634
+ .dsh-atb-section h4 {
635
+ margin: 0; font-size: 11px; color: var(--dsw-text-secondary, gray);
636
+ text-transform: uppercase; letter-spacing: .05em; display: flex; align-items: center; gap: 6px;
637
+ }
638
+ .dsh-atb-count2 {
639
+ font-size: 10px; font-weight: 600; padding: 0 6px; border-radius: 999px; line-height: 16px;
640
+ background: var(--dsw-bg-inset, rgba(128,128,128,.14)); color: var(--dsw-text-secondary, gray);
641
+ }
642
+ .dsh-atb-empty2 { font-size: 12px; color: var(--dsw-text-secondary, gray); padding: 8px 0; }
643
+
644
+ .dsh-atb-commentlist { display: flex; flex-direction: column; gap: 8px; }
645
+ .dsh-atb-bubble { display: flex; gap: 8px; }
646
+ .dsh-atb-bubble-avatar {
647
+ flex: none; width: 26px; height: 26px; border-radius: 8px; display: flex; align-items: center; justify-content: center;
648
+ font-size: 13px; background: var(--dsw-bg-inset, rgba(128,128,128,.12));
649
+ }
650
+ .dsh-atb-bubble[data-from="agent"] .dsh-atb-bubble-avatar { background: rgba(142,78,198,.15); }
651
+ .dsh-atb-bubble-main {
652
+ flex: 1; min-width: 0; border-radius: 10px; padding: 6px 10px;
653
+ background: var(--dsw-bg-elevated, rgba(128,128,128,.08));
654
+ border: 1px solid var(--dsw-border, rgba(128,128,128,.18));
655
+ }
656
+ .dsh-atb-bubble[data-from="agent"] .dsh-atb-bubble-main { border-color: rgba(142,78,198,.3); background: rgba(142,78,198,.07); }
657
+ .dsh-atb-bubble-meta { display: flex; align-items: baseline; gap: 8px; margin-bottom: 3px; }
658
+ .dsh-atb-bubble-meta b { font-size: 11.5px; font-weight: 600; color: var(--dsw-text-primary, inherit); }
659
+ .dsh-atb-bubble[data-from="agent"] .dsh-atb-bubble-meta b { color: #a06ce0; }
660
+ .dsh-atb-bubble-meta span { font-size: 10.5px; color: var(--dsw-text-secondary, gray); }
661
+ .dsh-atb-bubble-body { font-size: 12.5px; line-height: 1.55; white-space: pre-wrap; word-break: break-word; }
662
+
663
+ .dsh-atb-composer { display: flex; gap: 7px; align-items: flex-end; margin-top: 2px; }
664
+ .dsh-atb-composer-input {
665
+ flex: 1; font: inherit; font-size: 12.5px; line-height: 1.5; padding: 7px 10px; border-radius: 9px;
666
+ border: 1px solid var(--dsw-border, rgba(128,128,128,.32));
667
+ background: var(--dsw-bg, transparent); color: inherit; resize: vertical; min-height: 38px;
668
+ }
669
+ .dsh-atb-composer-input:focus { outline: none; border-color: var(--dsw-alias-brand-primary, #1f2328); }
670
+ .dsh-atb-composer-send {
671
+ flex: none; font: inherit; font-size: 12.5px; padding: 7px 14px; border-radius: 9px; cursor: pointer;
672
+ border: 1px solid transparent; background: var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary, #1f2328)); color: var(--dsw-alias-label-primary-foreground, #fff);
673
+ }
674
+ .dsh-atb-composer-send:disabled { opacity: .4; cursor: default; }
675
+
676
+ .dsh-atb-execlist { display: flex; flex-direction: column; gap: 5px; }
677
+ .dsh-atb-exec-row {
678
+ display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 12px;
679
+ padding: 5px 9px; border-radius: 8px;
680
+ background: var(--dsw-bg-elevated, rgba(128,128,128,.07));
681
+ border: 1px solid var(--dsw-border, rgba(128,128,128,.16));
682
+ }
683
+ .dsh-atb-exec-dot { flex: none; width: 7px; height: 7px; border-radius: 50%; background: rgba(128,128,128,.5); }
684
+ .dsh-atb-exec-dot[data-outcome="succeeded"] { background: #2ea043; box-shadow: 0 0 0 3px rgba(46,160,67,.15); }
685
+ .dsh-atb-exec-dot[data-outcome="failed"] { background: #e5484d; box-shadow: 0 0 0 3px rgba(229,72,77,.15); }
686
+ .dsh-atb-exec-dot[data-outcome="running"] { background: #d9822b; box-shadow: 0 0 0 3px rgba(217,130,43,.18); animation: dsh-atb-pulse 1.6s ease-in-out infinite; }
687
+ @keyframes dsh-atb-pulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } }
688
+ .dsh-atb-exec-trigger { font-size: 11px; color: var(--dsw-text-secondary, gray); }
689
+ .dsh-atb-exec-outcome { font-size: 11px; font-weight: 600; padding: 1px 7px; border-radius: 5px; }
690
+ .dsh-atb-exec-outcome[data-outcome="succeeded"] { background: rgba(46,160,67,.15); color: #2ea043; }
691
+ .dsh-atb-exec-outcome[data-outcome="failed"] { background: rgba(229,72,77,.14); color: #e5484d; }
692
+ .dsh-atb-exec-outcome[data-outcome="running"] { background: rgba(217,130,43,.15); color: #d9822b; }
693
+ .dsh-atb-exec-outcome[data-outcome="cancelled"] { background: rgba(128,128,128,.15); color: var(--dsw-text-secondary, gray); }
694
+ .dsh-atb-exec-time { font-size: 11px; color: var(--dsw-text-secondary, gray); }
695
+ .dsh-atb-exec-session { font-size: 11px; color: var(--dsw-text-secondary, gray); }
696
+ .dsh-atb-exec-error { flex-basis: 100%; font-size: 11px; color: #e5484d; word-break: break-all; }
697
+
698
+ .dsh-atb-dangerzone {
699
+ display: flex; align-items: center; gap: 8px; margin-top: auto; padding-top: 8px;
700
+ border-top: 1px dashed var(--dsw-border, rgba(128,128,128,.25));
701
+ }
702
+
703
+ /* ---------- task form modal (create + edit, polished) ---------- */
704
+ .dsh-atb-modal-backdrop {
705
+ position: fixed; inset: 0; z-index: 80;
706
+ background: var(--dsw-alias-bg-mask-drop, rgba(28,30,36,.4)); backdrop-filter: var(--dsw-mask-blur, blur(2px));
707
+ display: flex; align-items: center; justify-content: center;
708
+ animation: dsh-atb-fade .14s ease;
709
+ }
710
+ @keyframes dsh-atb-fade { from { opacity: 0; } }
711
+ .dsh-atb-modal {
712
+ width: min(560px, calc(100vw - 48px)); max-height: calc(100vh - 80px);
713
+ display: flex; flex-direction: column; overflow: hidden; border-radius: 14px;
714
+ background: var(--dsw-alias-bg-overlay, #fff); color: var(--dsw-alias-label-primary, inherit);
715
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
716
+ box-shadow: var(--dsw-shadow-lv3, 0 12px 32px rgba(0,0,0,.18));
717
+ animation: dsh-atb-pop .16s ease;
718
+ }
719
+ @keyframes dsh-atb-pop { from { opacity: 0; transform: translateY(8px) scale(.98); } }
720
+ .dsh-atb-modal-head {
721
+ display: flex; align-items: center; gap: 10px;
722
+ padding: 13px 16px 11px; border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.18));
723
+ }
724
+ .dsh-atb-modal-headicon {
725
+ flex: none; width: 30px; height: 30px; border-radius: 9px;
726
+ display: flex; align-items: center; justify-content: center;
727
+ font-size: 14px; background: var(--dsw-alias-brand-primary, #1f2328); color: var(--dsw-alias-label-primary-foreground, #fff);
728
+ }
729
+ .dsh-atb-modal-headtext { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
730
+ .dsh-atb-modal-headtext h3 { margin: 0; font-size: 15px; line-height: 1.3; }
731
+ .dsh-atb-modal-headtext p { margin: 0; font-size: 11.5px; color: var(--dsw-alias-label-secondary, gray); }
732
+ .dsh-atb-modal-close {
733
+ flex: none; width: 26px; height: 26px; border-radius: 7px; border: none; cursor: pointer;
734
+ background: transparent; color: var(--dsw-alias-label-tertiary, gray); font-size: 13px; line-height: 1;
735
+ }
736
+ .dsh-atb-modal-close:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.18)); color: var(--dsw-alias-label-primary, inherit); }
737
+
738
+ .dsh-atb-modal-body {
739
+ padding: 13px 16px; overflow-y: auto;
740
+ display: grid; grid-template-columns: 1fr 1fr; gap: 11px 10px;
741
+ }
742
+ .dsh-atb-field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
743
+ .dsh-atb-field[data-span="full"] { grid-column: 1 / -1; }
744
+ .dsh-atb-field-label {
745
+ display: flex; align-items: center; gap: 3px;
746
+ font-size: 11px; font-weight: 600; letter-spacing: .03em;
747
+ color: var(--dsw-alias-label-secondary, gray);
748
+ }
749
+ .dsh-atb-req { color: var(--dsw-alias-state-error-primary, #e5484d); font-style: normal; }
750
+ .dsh-atb-modal-body input, .dsh-atb-modal-body textarea, .dsh-atb-modal-body select {
751
+ font: inherit; font-size: 13px; padding: 7px 10px; border-radius: 8px;
752
+ width: 100%; box-sizing: border-box;
753
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.35));
754
+ background: var(--dsw-specific-input-major, transparent); color: var(--dsw-alias-label-primary, inherit);
755
+ transition: border-color .12s ease, box-shadow .12s ease;
756
+ }
757
+ .dsh-atb-modal-body textarea { min-height: 64px; resize: vertical; }
758
+ .dsh-atb-modal-body input:focus, .dsh-atb-modal-body textarea:focus, .dsh-atb-modal-body select:focus {
759
+ outline: none; border-color: var(--dsw-alias-brand-primary, #1f2328);
760
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary, #1f2328) 18%, transparent);
761
+ }
762
+ .dsh-atb-modal-body .dsh-atb-input-bad { border-color: var(--dsw-alias-state-error-primary, #e5484d); }
763
+ .dsh-atb-modal-body .dsh-atb-input-bad:focus { box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-state-error-primary, #e5484d) 20%, transparent); }
764
+
765
+ .dsh-atb-urgency-picker { display: grid; grid-template-columns: repeat(3, 1fr); gap: 7px; }
766
+ .dsh-atb-urgency-opt {
767
+ display: flex; flex-direction: column; align-items: flex-start; gap: 3px;
768
+ padding: 8px 10px; border-radius: 9px; cursor: pointer;
769
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.35));
770
+ background: transparent; color: inherit;
771
+ transition: border-color .12s ease, background .12s ease;
772
+ }
773
+ .dsh-atb-urgency-name { display: flex; align-items: center; gap: 6px; font-size: 12.5px; font-weight: 600; }
774
+ .dsh-atb-urgency-hint { font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); }
775
+ .dsh-atb-urgency-opt:hover { border-color: var(--dsw-alias-label-tertiary, rgba(128,128,128,.6)); }
776
+ .dsh-atb-urgency-opt[data-on="true"][data-urgency="urgent"] { border-color: rgba(229,72,77,.65); background: rgba(229,72,77,.1); }
777
+ .dsh-atb-urgency-opt[data-on="true"][data-urgency="normal"] { border-color: rgba(142,78,198,.65); background: rgba(142,78,198,.1); }
778
+ .dsh-atb-urgency-opt[data-on="true"][data-urgency="relaxed"] { border-color: rgba(62,99,221,.65); background: rgba(62,99,221,.1); }
779
+
780
+ .dsh-atb-mode-picker { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; }
781
+ .dsh-atb-mode-opt {
782
+ display: flex; flex-direction: column; align-items: flex-start; gap: 3px;
783
+ padding: 8px 10px; border-radius: 9px; cursor: pointer;
784
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.35));
785
+ background: transparent; color: inherit;
786
+ transition: border-color .12s ease, background .12s ease;
787
+ }
788
+ .dsh-atb-mode-name { font-size: 12.5px; font-weight: 600; }
789
+ .dsh-atb-mode-hint { font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); }
790
+ .dsh-atb-mode-opt:hover { border-color: var(--dsw-alias-label-tertiary, rgba(128,128,128,.6)); }
791
+ .dsh-atb-mode-opt[data-on="true"] { border-color: var(--dsw-alias-brand-primary, #1f2328); background: color-mix(in srgb, var(--dsw-alias-brand-primary, #1f2328) 8%, transparent); }
792
+
793
+ .dsh-atb-cron-presets { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
794
+ .dsh-atb-cron-preset {
795
+ font: inherit; font-size: 11px; padding: 2px 9px; border-radius: 999px; cursor: pointer;
796
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.35));
797
+ background: transparent; color: var(--dsw-alias-label-secondary, inherit);
798
+ }
799
+ .dsh-atb-cron-preset:hover { border-color: var(--dsw-alias-label-tertiary, rgba(128,128,128,.6)); color: inherit; }
800
+ .dsh-atb-cron-preset[data-on="true"] { border-color: transparent; background: var(--dsw-alias-brand-primary, #1f2328); color: var(--dsw-alias-label-primary-foreground, #fff); }
801
+ .dsh-atb-cron-next { margin-left: auto; font-size: 11px; color: var(--dsw-alias-label-tertiary, gray); }
802
+
803
+ .dsh-atb-modal-foot {
804
+ display: flex; align-items: center; gap: 10px;
805
+ padding: 11px 16px; border-top: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.18));
806
+ background: var(--dsw-alias-bg-layer-1, rgba(128,128,128,.04));
807
+ }
808
+ .dsh-atb-modal-hint { flex: 1; min-width: 0; font-size: 11.5px; color: var(--dsw-alias-label-tertiary, gray); }
809
+ .dsh-atb-modal-hint[data-tone="bad"] { color: var(--dsw-alias-state-error-primary, #e5484d); }
810
+ .dsh-atb-modal-footbtns { display: flex; gap: 8px; }
811
+
812
+ .dsh-atb-secondary { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; }
813
+ .dsh-atb-link { color: var(--dsw-alias-state-business-primary, #3e63dd); cursor: pointer; text-decoration: none; }
814
+ .dsh-atb-link:hover { text-decoration: underline; }
815
+ `;
816
+ let injected = false;
817
+ /** Inject the stylesheet once (idempotent). */
818
+ function injectStyles() {
819
+ if (injected || typeof document === "undefined") return;
820
+ const style = document.createElement("style");
821
+ style.id = "dsh-taskboard-styles";
822
+ style.textContent = STYLES;
823
+ document.head.append(style);
824
+ injected = true;
825
+ }
826
+
827
+ //#endregion
828
+ //#region src/client/sidebar-entry.ts
829
+ /** Inline icon (16px nav-icon look). */
830
+ const ICON = "<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"2\" y=\"2.5\" width=\"12\" height=\"11\" rx=\"1.5\"/><path d=\"M2 6.5h12M6.5 6.5v7\"/></svg>";
831
+ /**
832
+ * Find the sidebar shell root element, or undefined while not yet mounted.
833
+ * (Same as the working family plugins: sidebarCol pane → logoRow owner.)
834
+ */
835
+ function sidebarRoot() {
836
+ const column = document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"]");
837
+ if (column === null) return void 0;
838
+ return column.querySelector("[class*=\"logoRow\"]")?.parentElement ?? column.firstElementChild;
839
+ }
840
+ /**
841
+ * The New Session button: nested in the logo row on current shells, a direct
842
+ * child BUTTON on the real shell (the family plugins' fallback), with
843
+ * aria-label/text fallbacks for other shells.
844
+ */
845
+ function newSessionButton(root) {
846
+ const nested = root.querySelector("button[class*=\"newSession\"]");
847
+ if (nested !== null) return nested;
848
+ for (const child of root.children) if (child instanceof HTMLButtonElement) return child;
849
+ const byAria = root.querySelector("button[aria-label=\"新建会话\"], button[aria-label=\"New Session\"], button[aria-label*=\"新会话\"], button[aria-label*=\"new session\" i]");
850
+ if (byAria !== null) return byAria;
851
+ return Array.from(root.querySelectorAll("button")).find((button) => /新会话|新建会话|new session/i.test(button.textContent ?? ""));
852
+ }
853
+ /** Build the entry row (a detached button; insert once the shell is up). */
854
+ function createEntry(controller) {
855
+ const entry = document.createElement("button");
856
+ entry.type = "button";
857
+ entry.dataset.dshAtbEntry = "";
858
+ entry.className = "dsh-atb-entry";
859
+ entry.setAttribute("aria-label", "Agent 任务看板");
860
+ entry.innerHTML = `<span class="dsh-atb-entry-icon">${ICON}</span><span class="dsh-atb-entry-label">任务看板</span>`;
861
+ entry.addEventListener("click", () => {
862
+ controller.toggleBoard();
863
+ });
864
+ return entry;
865
+ }
866
+ /** Re-insert the entry after the New Session row (before the browser region). */
867
+ function placeEntry(root, entry) {
868
+ const button = newSessionButton(root);
869
+ if (button === void 0) return false;
870
+ if (entry.parentElement !== root) {
871
+ const row = button.closest("[class*=\"logoRow\"]");
872
+ const base = row !== null && row.parentElement === root ? row : button;
873
+ const family = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches("[data-dsh-atb-entry], [data-dsh-taskboard-entry], [data-dsh-ssh-entry]"));
874
+ const anchor = family.length > 0 ? family[0] ?? null : base.nextElementSibling ?? null;
875
+ root.insertBefore(entry, anchor);
876
+ }
877
+ return true;
878
+ }
879
+ /**
880
+ * Mount the sidebar entry, waiting for the shell to render and self-healing
881
+ * on later React re-renders.
882
+ * @param controller - the board controller the entry toggles.
883
+ * @returns disposer removing the entry and its observers.
884
+ */
885
+ function mountSidebarEntry(controller) {
886
+ const entry = createEntry(controller);
887
+ const debug = {
888
+ attempts: 0,
889
+ found: false,
890
+ placed: false
891
+ };
892
+ window.__atbDebug = debug;
893
+ let root;
894
+ let placed = false;
895
+ const tryPlace = () => {
896
+ debug.attempts++;
897
+ if (root !== void 0 && !root.isConnected) {
898
+ rootObserver.disconnect();
899
+ root = void 0;
900
+ placed = false;
901
+ }
902
+ if (placed) {
903
+ if (document.body.contains(entry)) return;
904
+ rootObserver.disconnect();
905
+ root = void 0;
906
+ placed = false;
907
+ }
908
+ root ??= sidebarRoot();
909
+ if (root === void 0) return;
910
+ debug.found = newSessionButton(root) !== void 0;
911
+ placed = placeEntry(root, entry);
912
+ debug.placed = placed;
913
+ if (placed) rootObserver.observe(root, {
914
+ childList: true,
915
+ subtree: true
916
+ });
917
+ };
918
+ const waitObserver = new MutationObserver(() => {
919
+ tryPlace();
920
+ });
921
+ waitObserver.observe(document.body, {
922
+ childList: true,
923
+ subtree: true
924
+ });
925
+ const rootObserver = new MutationObserver(() => {
926
+ if (root === void 0 || !root.isConnected) {
927
+ placed = false;
928
+ tryPlace();
929
+ return;
930
+ }
931
+ if (!root.contains(entry)) placed = placeEntry(root, entry);
932
+ });
933
+ const retry = setInterval(() => {
934
+ tryPlace();
935
+ }, 2e3);
936
+ const syncActive = () => {
937
+ if (controller.getSnapshot().boardOpen) entry.dataset.active = "true";
938
+ else delete entry.dataset.active;
939
+ };
940
+ const unsubscribe = controller.subscribe(syncActive);
941
+ syncActive();
942
+ tryPlace();
943
+ return () => {
944
+ clearInterval(retry);
945
+ waitObserver.disconnect();
946
+ rootObserver.disconnect();
947
+ unsubscribe();
948
+ entry.remove();
949
+ };
950
+ }
951
+
952
+ //#endregion
953
+ //#region src/client/board/TaskCard.tsx
954
+ const URGENCY_LABEL$1 = {
955
+ urgent: "紧急",
956
+ normal: "一般",
957
+ relaxed: "不急"
958
+ };
959
+ const OUTCOME_LABEL$1 = {
960
+ running: "执行中",
961
+ succeeded: "成功",
962
+ failed: "失败",
963
+ cancelled: "已取消"
964
+ };
965
+ /** dataTransfer type carrying the dragged task id. */
966
+ const DRAG_TYPE = "application/x-dsh-atb-task";
967
+ /**
968
+ * The card view.
969
+ * @param task - the task record.
970
+ * @param controller - the controller.
971
+ * @param draggable - enable dragging (backlog/todo columns only).
972
+ */
973
+ function TaskCard({ task, controller, draggable = false }) {
974
+ const last = task.executions.length > 0 ? task.executions[task.executions.length - 1] : void 0;
975
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
976
+ type: "button",
977
+ className: "dsh-atb-card",
978
+ "data-urgency": task.urgency,
979
+ draggable,
980
+ onDragStart: (e) => {
981
+ e.dataTransfer.setData(DRAG_TYPE, task.id);
982
+ e.dataTransfer.effectAllowed = "move";
983
+ e.currentTarget.dataset.dragging = "true";
984
+ },
985
+ onDragEnd: (e) => {
986
+ delete e.currentTarget.dataset.dragging;
987
+ },
988
+ onClick: () => controller.select(task.id),
989
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
990
+ className: "dsh-atb-card-title",
991
+ children: task.title
992
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
993
+ className: "dsh-atb-card-meta",
994
+ children: [
995
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
996
+ className: "dsh-atb-badge",
997
+ children: URGENCY_LABEL$1[task.urgency]
998
+ }),
999
+ task.blocked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1000
+ className: "dsh-atb-badge",
1001
+ "data-kind": "blocked",
1002
+ children: "受阻"
1003
+ }),
1004
+ task.execution.mode === "scheduled" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1005
+ className: "dsh-atb-badge",
1006
+ "data-kind": "scheduled",
1007
+ children: ["⏰ ", fmtTime(task.execution.nextRunAt)]
1008
+ }),
1009
+ task.model !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1010
+ className: "dsh-atb-badge",
1011
+ children: task.model.model
1012
+ }),
1013
+ task.status === "done" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1014
+ className: "dsh-atb-badge",
1015
+ "data-kind": "done",
1016
+ children: "完成"
1017
+ }),
1018
+ last !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1019
+ className: "dsh-atb-badge",
1020
+ "data-kind": last.outcome === "running" ? "running" : last.outcome,
1021
+ children: OUTCOME_LABEL$1[last.outcome] ?? last.outcome
1022
+ }),
1023
+ task.comments.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["💬 ", task.comments.length] }),
1024
+ task.trashedAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1025
+ className: "dsh-atb-badge",
1026
+ "data-kind": "trashed",
1027
+ children: "待清除"
1028
+ }),
1029
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1030
+ style: { marginLeft: "auto" },
1031
+ children: fmtTime(task.updatedAt)
1032
+ })
1033
+ ]
1034
+ })]
1035
+ });
1036
+ }
1037
+
1038
+ //#endregion
1039
+ //#region src/client/board/TaskDetail.tsx
1040
+ /**
1041
+ * The task detail pane — visually polished: urgency accent header with
1042
+ * status pill and meta chips, card-wrapped description/prompt, chat-style
1043
+ * comment bubbles distinguishing user vs agent authors, a timeline of
1044
+ * executions with outcome pills, grouped actions (run / transitions /
1045
+ * danger zone), and the user comment composer.
1046
+ *
1047
+ * @module dsh-taskboard/client/board/TaskDetail
1048
+ */
1049
+ /** Statuses a user may move this task to, per the state machine. */
1050
+ function moveTargets(task) {
1051
+ return [
1052
+ "backlog",
1053
+ "todo",
1054
+ "in_progress",
1055
+ "in_review",
1056
+ "done",
1057
+ "canceled",
1058
+ "archived"
1059
+ ].filter((to) => canTransition(task.status, to));
1060
+ }
1061
+ const MOVE_LABEL = {
1062
+ backlog: "待规划",
1063
+ todo: "待办",
1064
+ in_progress: "进行中",
1065
+ in_review: "待验收",
1066
+ done: "完成",
1067
+ canceled: "取消",
1068
+ archived: "归档"
1069
+ };
1070
+ const STATUS_LABEL = { ...MOVE_LABEL };
1071
+ const URGENCY_LABEL = {
1072
+ urgent: "紧急",
1073
+ normal: "一般",
1074
+ relaxed: "不急"
1075
+ };
1076
+ const OUTCOME_LABEL = {
1077
+ running: "执行中",
1078
+ succeeded: "成功",
1079
+ failed: "失败",
1080
+ cancelled: "已取消"
1081
+ };
1082
+ /** Compact session-id display. */
1083
+ function shortId(id) {
1084
+ if (id === void 0) return "";
1085
+ return id.replace(/^session-/, "").slice(0, 8);
1086
+ }
1087
+ /** Execution duration between start and end. */
1088
+ function duration(startedAt, endedAt) {
1089
+ if (startedAt === void 0 || endedAt === void 0) return "";
1090
+ const s = Math.max(0, Math.round((endedAt - startedAt) / 1e3));
1091
+ if (s < 60) return `${s}s`;
1092
+ if (s < 3600) return `${Math.floor(s / 60)}m${s % 60}s`;
1093
+ return `${Math.floor(s / 3600)}h${Math.floor(s % 3600 / 60)}m`;
1094
+ }
1095
+ /** Small labelled meta chip. */
1096
+ function Chip({ icon, children, tone }) {
1097
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1098
+ className: "dsh-atb-chip2",
1099
+ "data-tone": tone,
1100
+ children: [icon !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1101
+ className: "dsh-atb-chip2-icon",
1102
+ children: icon
1103
+ }), children]
1104
+ });
1105
+ }
1106
+ /**
1107
+ * The detail view.
1108
+ * @param task - the task record.
1109
+ * @param controller - the controller.
1110
+ */
1111
+ function TaskDetail({ task, controller }) {
1112
+ const [comment, setComment] = (0, react.useState)("");
1113
+ const [confirmDone, setConfirmDone] = (0, react.useState)(false);
1114
+ const [confirmPurge, setConfirmPurge] = (0, react.useState)(false);
1115
+ const ws = controller.getSnapshot().workspaces.find((w) => w.id === task.workspaceId);
1116
+ const canRun = task.status !== "in_progress" && task.status !== "done" && task.status !== "archived";
1117
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1118
+ className: "dsh-atb-detail",
1119
+ "data-urgency": task.urgency,
1120
+ children: [
1121
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1122
+ className: "dsh-atb-detail-head",
1123
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1124
+ className: "dsh-atb-detail-titlewrap",
1125
+ children: [
1126
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1127
+ className: "dsh-atb-detail-titlebar",
1128
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: task.title }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1129
+ className: "dsh-atb-statuspill",
1130
+ "data-status": task.status,
1131
+ children: STATUS_LABEL[task.status] ?? task.status
1132
+ })]
1133
+ }),
1134
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1135
+ className: "dsh-atb-detail-chips",
1136
+ children: [
1137
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Chip, {
1138
+ tone: task.urgency,
1139
+ children: ["● ", URGENCY_LABEL[task.urgency] ?? task.urgency]
1140
+ }),
1141
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chip, {
1142
+ icon: "📁",
1143
+ children: ws?.title ?? shortId(task.workspaceId)
1144
+ }),
1145
+ task.model !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chip, {
1146
+ icon: "✦",
1147
+ children: task.model.model
1148
+ }),
1149
+ task.execution.mode === "scheduled" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Chip, {
1150
+ icon: "⏰",
1151
+ children: [
1152
+ task.execution.cron,
1153
+ " · 下次 ",
1154
+ fmtTime(task.execution.nextRunAt)
1155
+ ]
1156
+ }),
1157
+ task.blocked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chip, {
1158
+ icon: "⛔",
1159
+ tone: "urgent",
1160
+ children: "受阻"
1161
+ }),
1162
+ task.trashedAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chip, {
1163
+ icon: "🗑",
1164
+ tone: "urgent",
1165
+ children: "已删除待清除"
1166
+ }),
1167
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Chip, { children: ["v", task.version] })
1168
+ ]
1169
+ }),
1170
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1171
+ className: "dsh-atb-detail-sub",
1172
+ children: [
1173
+ "更新 ",
1174
+ fmtTime(task.updatedAt),
1175
+ " · 最近操作 ",
1176
+ task.updatedBy.kind === "agent" ? `🤖 ${shortId(task.updatedBy.sessionId)}` : "👤 用户"
1177
+ ]
1178
+ })
1179
+ ]
1180
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1181
+ className: "dsh-atb-detail-topbtns",
1182
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1183
+ type: "button",
1184
+ className: "dsh-atb-detail-edit",
1185
+ onClick: () => controller.openEditor(task.id),
1186
+ children: "✎ 编辑"
1187
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1188
+ type: "button",
1189
+ className: "dsh-atb-detail-close",
1190
+ "aria-label": "关闭",
1191
+ onClick: () => controller.select(void 0),
1192
+ children: "✕"
1193
+ })]
1194
+ })]
1195
+ }),
1196
+ task.description.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1197
+ className: "dsh-atb-fieldcard",
1198
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1199
+ className: "dsh-atb-fieldcard-label",
1200
+ children: "描述"
1201
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1202
+ className: "dsh-atb-desc",
1203
+ children: task.description
1204
+ })]
1205
+ }),
1206
+ task.prompt.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1207
+ className: "dsh-atb-fieldcard",
1208
+ "data-kind": "prompt",
1209
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1210
+ className: "dsh-atb-fieldcard-label",
1211
+ children: "执行 Prompt"
1212
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1213
+ className: "dsh-atb-promptbox",
1214
+ children: task.prompt
1215
+ })]
1216
+ }),
1217
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1218
+ className: "dsh-atb-detail-actions",
1219
+ children: [canRun && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1220
+ type: "button",
1221
+ className: "dsh-atb-runbtn",
1222
+ onClick: () => void controller.run(task.id),
1223
+ children: ["▶ 执行 · 新会话", task.model !== void 0 ? `(${task.model.model})` : "(默认模型)"]
1224
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1225
+ className: "dsh-atb-movebtns",
1226
+ children: [moveTargets(task).map((to) => to === "done" ? confirmDone ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1227
+ className: "dsh-atb-confirm",
1228
+ children: [
1229
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1230
+ className: "dsh-atb-confirm-label",
1231
+ children: "确认完成?"
1232
+ }),
1233
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1234
+ type: "button",
1235
+ className: "dsh-atb-btn",
1236
+ "data-primary": "true",
1237
+ onClick: () => {
1238
+ controller.move(task.id, task.version, "done");
1239
+ setConfirmDone(false);
1240
+ },
1241
+ children: "确认"
1242
+ }),
1243
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1244
+ type: "button",
1245
+ className: "dsh-atb-btn",
1246
+ onClick: () => setConfirmDone(false),
1247
+ children: "取消"
1248
+ })
1249
+ ]
1250
+ }, to) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1251
+ type: "button",
1252
+ className: "dsh-atb-movebtn",
1253
+ "data-to": to,
1254
+ onClick: () => setConfirmDone(true),
1255
+ children: ["✓ ", MOVE_LABEL[to]]
1256
+ }, to) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1257
+ type: "button",
1258
+ className: "dsh-atb-movebtn",
1259
+ "data-to": to,
1260
+ onClick: () => void controller.move(task.id, task.version, to),
1261
+ children: MOVE_LABEL[to]
1262
+ }, to)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1263
+ type: "button",
1264
+ className: "dsh-atb-movebtn",
1265
+ "data-to": "blocked",
1266
+ onClick: () => void controller.toggleBlocked(task),
1267
+ children: task.blocked ? "✓ 解除受阻" : "⛔ 标记受阻"
1268
+ })]
1269
+ })]
1270
+ }),
1271
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1272
+ className: "dsh-atb-section",
1273
+ children: [
1274
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h4", { children: ["评论", task.comments.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1275
+ className: "dsh-atb-count2",
1276
+ children: task.comments.length
1277
+ })] }),
1278
+ task.comments.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1279
+ className: "dsh-atb-empty2",
1280
+ children: "暂无评论 — agent 交接时会在这里汇报改动与验证结果"
1281
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1282
+ className: "dsh-atb-commentlist",
1283
+ children: task.comments.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1284
+ className: "dsh-atb-bubble",
1285
+ "data-from": c.threadId !== void 0 ? "agent" : "user",
1286
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1287
+ className: "dsh-atb-bubble-avatar",
1288
+ children: c.threadId !== void 0 ? "🤖" : "👤"
1289
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1290
+ className: "dsh-atb-bubble-main",
1291
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1292
+ className: "dsh-atb-bubble-meta",
1293
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: c.threadId !== void 0 ? `agent ${shortId(c.threadId)}` : "用户" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: fmtTime(c.createdAt) })]
1294
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1295
+ className: "dsh-atb-bubble-body",
1296
+ children: c.body
1297
+ })]
1298
+ })]
1299
+ }, c.id))
1300
+ }),
1301
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1302
+ className: "dsh-atb-composer",
1303
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1304
+ className: "dsh-atb-composer-input",
1305
+ value: comment,
1306
+ placeholder: "以用户身份留言(agent 开工前会读)…",
1307
+ onChange: (e) => setComment(e.target.value),
1308
+ onKeyDown: (e) => {
1309
+ if ((e.ctrlKey || e.metaKey) && e.key === "Enter" && comment.trim().length > 0) {
1310
+ controller.comment(task.id, comment);
1311
+ setComment("");
1312
+ }
1313
+ }
1314
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1315
+ type: "button",
1316
+ className: "dsh-atb-composer-send",
1317
+ disabled: comment.trim().length === 0,
1318
+ onClick: () => {
1319
+ controller.comment(task.id, comment);
1320
+ setComment("");
1321
+ },
1322
+ children: "发表"
1323
+ })]
1324
+ })
1325
+ ]
1326
+ }),
1327
+ task.executions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1328
+ className: "dsh-atb-section",
1329
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h4", { children: ["执行记录", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1330
+ className: "dsh-atb-count2",
1331
+ children: task.executions.length
1332
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1333
+ className: "dsh-atb-execlist",
1334
+ children: task.executions.map((e) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1335
+ className: "dsh-atb-exec-row",
1336
+ children: [
1337
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1338
+ className: "dsh-atb-exec-dot",
1339
+ "data-outcome": e.outcome
1340
+ }),
1341
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1342
+ className: "dsh-atb-exec-trigger",
1343
+ children: e.trigger === "manual" ? "手动" : "定时"
1344
+ }),
1345
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1346
+ className: "dsh-atb-exec-outcome",
1347
+ "data-outcome": e.outcome,
1348
+ children: OUTCOME_LABEL[e.outcome] ?? e.outcome
1349
+ }),
1350
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1351
+ className: "dsh-atb-exec-time",
1352
+ children: [fmtTime(e.startedAt), e.endedAt !== void 0 && ` · ${duration(e.startedAt, e.endedAt)}`]
1353
+ }),
1354
+ e.sessionId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1355
+ className: "dsh-atb-exec-session",
1356
+ title: e.sessionId,
1357
+ children: ["🤖 ", shortId(e.sessionId)]
1358
+ }),
1359
+ e.error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1360
+ className: "dsh-atb-exec-error",
1361
+ title: e.error,
1362
+ children: [e.error.slice(0, 80), e.error.length > 80 ? "…" : ""]
1363
+ })
1364
+ ]
1365
+ }, e.id))
1366
+ })]
1367
+ }),
1368
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1369
+ className: "dsh-atb-dangerzone",
1370
+ children: task.trashedAt === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1371
+ type: "button",
1372
+ className: "dsh-atb-btn",
1373
+ "data-danger": "true",
1374
+ onClick: () => void controller.remove(task.id, task.version, false),
1375
+ children: "🗑 删除(标记待清除)"
1376
+ }) : confirmPurge ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1377
+ className: "dsh-atb-confirm",
1378
+ children: [
1379
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1380
+ className: "dsh-atb-confirm-label",
1381
+ children: "物理清除不可恢复"
1382
+ }),
1383
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1384
+ type: "button",
1385
+ className: "dsh-atb-btn",
1386
+ "data-danger": "true",
1387
+ onClick: () => {
1388
+ controller.remove(task.id, task.version, true);
1389
+ setConfirmPurge(false);
1390
+ },
1391
+ children: "确认清除"
1392
+ }),
1393
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1394
+ type: "button",
1395
+ className: "dsh-atb-btn",
1396
+ onClick: () => setConfirmPurge(false),
1397
+ children: "取消"
1398
+ })
1399
+ ]
1400
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1401
+ type: "button",
1402
+ className: "dsh-atb-btn",
1403
+ "data-danger": "true",
1404
+ onClick: () => setConfirmPurge(true),
1405
+ children: "🔥 物理清除(需确认)"
1406
+ })
1407
+ })
1408
+ ]
1409
+ });
1410
+ }
1411
+
1412
+ //#endregion
1413
+ //#region src/client/board/TaskFormModal.tsx
1414
+ /**
1415
+ * The task form modal — create and edit in one polished dialog: header with
1416
+ * icon / subtitle / close, a sectioned field grid (title, project, model,
1417
+ * urgency tri-picker with hints, description, prompt, execution-mode
1418
+ * segmented picker, cron with presets and a live next-run preview), and a
1419
+ * footer bar carrying the validation hint and the actions. Esc closes;
1420
+ * the title input is focused on open.
1421
+ *
1422
+ * @module dsh-taskboard/client/board/TaskFormModal
1423
+ */
1424
+ /** Urgency segmented options with a one-line hint each. */
1425
+ const URGENCY_OPTIONS = [
1426
+ {
1427
+ value: "urgent",
1428
+ label: "紧急",
1429
+ hint: "优先处理"
1430
+ },
1431
+ {
1432
+ value: "normal",
1433
+ label: "一般",
1434
+ hint: "正常排期"
1435
+ },
1436
+ {
1437
+ value: "relaxed",
1438
+ label: "不急",
1439
+ hint: "有空再做"
1440
+ }
1441
+ ];
1442
+ /** Cron presets offered in the scheduled mode. */
1443
+ const CRON_PRESETS = [
1444
+ {
1445
+ label: "每天 09:00",
1446
+ cron: "0 9 * * *"
1447
+ },
1448
+ {
1449
+ label: "每小时",
1450
+ cron: "0 * * * *"
1451
+ },
1452
+ {
1453
+ label: "每 10 分钟",
1454
+ cron: "*/10 * * * *"
1455
+ },
1456
+ {
1457
+ label: "每周一 09:00",
1458
+ cron: "0 9 * * 1"
1459
+ }
1460
+ ];
1461
+ /** Field shell: label + control, optionally spanning the full grid row. */
1462
+ function Field({ label, required = false, full = false, children }) {
1463
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1464
+ className: "dsh-atb-field",
1465
+ "data-span": full ? "full" : void 0,
1466
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1467
+ className: "dsh-atb-field-label",
1468
+ children: [label, required && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("em", {
1469
+ className: "dsh-atb-req",
1470
+ children: "*"
1471
+ })]
1472
+ }), children]
1473
+ });
1474
+ }
1475
+ /**
1476
+ * The form modal. Without `task` it composes a new task; with `task` it
1477
+ * edits that record (project, urgency, execution, model included — the GUI
1478
+ * is the owner surface).
1479
+ * @param controller - the controller.
1480
+ * @param task - the task being edited (create mode when absent).
1481
+ */
1482
+ function TaskFormModal({ controller, task }) {
1483
+ const state = controller.getSnapshot();
1484
+ const editing = task !== void 0;
1485
+ const [title, setTitle] = (0, react.useState)(task?.title ?? "");
1486
+ const [description, setDescription] = (0, react.useState)(task?.description ?? "");
1487
+ const [prompt, setPrompt] = (0, react.useState)(task?.prompt ?? "");
1488
+ const [workspaceId, setWorkspaceId] = (0, react.useState)(task?.workspaceId ?? state.filters.workspaceId ?? state.workspaces[0]?.id ?? "");
1489
+ const [urgency, setUrgency] = (0, react.useState)(task?.urgency ?? "normal");
1490
+ const [mode, setMode] = (0, react.useState)(task?.execution.mode === "scheduled" ? "scheduled" : "claim");
1491
+ const [cron, setCron] = (0, react.useState)(task?.execution.cron ?? "0 9 * * *");
1492
+ const [catalog, setCatalog] = (0, react.useState)([]);
1493
+ const [model, setModel] = (0, react.useState)(task?.model !== void 0 ? JSON.stringify(task.model) : "");
1494
+ const titleRef = (0, react.useRef)(null);
1495
+ (0, react.useEffect)(() => {
1496
+ titleRef.current?.focus();
1497
+ const onKey = (e) => {
1498
+ if (e.key === "Escape") controller.closeForm();
1499
+ };
1500
+ document.addEventListener("keydown", onKey);
1501
+ return () => document.removeEventListener("keydown", onKey);
1502
+ }, [controller]);
1503
+ (0, react.useEffect)(() => {
1504
+ const face = controller.modelCatalog;
1505
+ if (face === void 0) return;
1506
+ face().then(setCatalog).catch(() => setCatalog([]));
1507
+ }, [controller]);
1508
+ const cronMatch = mode === "scheduled" ? parseCron(cron.trim()) : null;
1509
+ const nextRun = cronMatch !== null ? nextCronTime(cronMatch, Date.now()) : null;
1510
+ const cronBad = mode === "scheduled" && (cronMatch === null || nextRun === null);
1511
+ const valid = title.trim().length > 0 && workspaceId !== "" && !cronBad;
1512
+ const submit = () => {
1513
+ if (!valid) return;
1514
+ const picked = model !== "" ? JSON.parse(model) : void 0;
1515
+ if (editing) controller.update(task.id, task.version, {
1516
+ title,
1517
+ description,
1518
+ prompt,
1519
+ urgency,
1520
+ workspaceId,
1521
+ execution: mode === "scheduled" ? {
1522
+ mode,
1523
+ cron: cron.trim()
1524
+ } : { mode },
1525
+ model: picked ?? null
1526
+ });
1527
+ else controller.create({
1528
+ title,
1529
+ workspaceId,
1530
+ urgency,
1531
+ description: description.length > 0 ? description : void 0,
1532
+ prompt: prompt.length > 0 ? prompt : void 0,
1533
+ execution: mode === "scheduled" ? {
1534
+ mode,
1535
+ cron: cron.trim()
1536
+ } : { mode },
1537
+ model: picked
1538
+ });
1539
+ };
1540
+ const hint = !valid ? title.trim().length === 0 ? "请填写标题" : workspaceId === "" ? "请选择项目" : "Cron 表达式无效(分 时 日 月 周)" : mode === "scheduled" && nextRun !== null ? `下次运行 ${fmtTime(nextRun)}` : editing ? `保存后版本 v${task.version} → v${task.version + 1}` : "创建后项目内会话可认领执行";
1541
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1542
+ className: "dsh-atb-modal-backdrop",
1543
+ onClick: (e) => {
1544
+ if (e.target === e.currentTarget) controller.closeForm();
1545
+ },
1546
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1547
+ className: "dsh-atb-modal",
1548
+ "data-mode": editing ? "edit" : "create",
1549
+ role: "dialog",
1550
+ "aria-modal": "true",
1551
+ "aria-label": editing ? "编辑任务" : "新建任务",
1552
+ children: [
1553
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1554
+ className: "dsh-atb-modal-head",
1555
+ children: [
1556
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1557
+ className: "dsh-atb-modal-headicon",
1558
+ children: editing ? "✎" : "✚"
1559
+ }),
1560
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1561
+ className: "dsh-atb-modal-headtext",
1562
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: editing ? "编辑任务" : "新建任务" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: editing ? "调整任务内容与执行配置" : "推入看板,项目内会话可认领执行" })]
1563
+ }),
1564
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1565
+ type: "button",
1566
+ className: "dsh-atb-modal-close",
1567
+ "aria-label": "关闭",
1568
+ onClick: () => controller.closeForm(),
1569
+ children: "✕"
1570
+ })
1571
+ ]
1572
+ }),
1573
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1574
+ className: "dsh-atb-modal-body",
1575
+ children: [
1576
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1577
+ label: "标题",
1578
+ required: true,
1579
+ full: true,
1580
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1581
+ ref: titleRef,
1582
+ value: title,
1583
+ onChange: (e) => setTitle(e.target.value),
1584
+ placeholder: "一句话说清要做什么",
1585
+ maxLength: 200
1586
+ })
1587
+ }),
1588
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1589
+ label: "项目",
1590
+ required: true,
1591
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
1592
+ value: workspaceId,
1593
+ onChange: (e) => setWorkspaceId(e.target.value),
1594
+ children: state.workspaces.map((ws) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1595
+ value: ws.id,
1596
+ children: ws.title || ws.path
1597
+ }, ws.id))
1598
+ })
1599
+ }),
1600
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1601
+ label: "模型(默认 = 会话默认模型)",
1602
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1603
+ value: model,
1604
+ onChange: (e) => setModel(e.target.value),
1605
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1606
+ value: "",
1607
+ children: "默认模型"
1608
+ }), catalog.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1609
+ value: JSON.stringify({
1610
+ provider: m.provider,
1611
+ model: m.model
1612
+ }),
1613
+ children: [
1614
+ m.name ?? m.model,
1615
+ "(",
1616
+ m.provider,
1617
+ ")"
1618
+ ]
1619
+ }, `${m.provider}/${m.model}`))]
1620
+ })
1621
+ }),
1622
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1623
+ label: "紧急度",
1624
+ full: true,
1625
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1626
+ className: "dsh-atb-urgency-picker",
1627
+ children: URGENCY_OPTIONS.map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1628
+ type: "button",
1629
+ className: "dsh-atb-urgency-opt",
1630
+ "data-urgency": o.value,
1631
+ "data-on": urgency === o.value,
1632
+ onClick: () => setUrgency(o.value),
1633
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1634
+ className: "dsh-atb-urgency-name",
1635
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1636
+ className: "dsh-atb-dot",
1637
+ "data-urgency": o.value
1638
+ }), o.label]
1639
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1640
+ className: "dsh-atb-urgency-hint",
1641
+ children: o.hint
1642
+ })]
1643
+ }, o.value))
1644
+ })
1645
+ }),
1646
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1647
+ label: editing ? "描述" : "描述(可选)",
1648
+ full: true,
1649
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1650
+ value: description,
1651
+ onChange: (e) => setDescription(e.target.value),
1652
+ placeholder: "需求细节、验收标准…"
1653
+ })
1654
+ }),
1655
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1656
+ label: editing ? "执行 Prompt" : "执行 Prompt(可选,默认 = 标题+描述)",
1657
+ full: true,
1658
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1659
+ value: prompt,
1660
+ onChange: (e) => setPrompt(e.target.value),
1661
+ placeholder: "发给执行会话的完整指令"
1662
+ })
1663
+ }),
1664
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1665
+ label: "执行方式",
1666
+ full: true,
1667
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1668
+ className: "dsh-atb-mode-picker",
1669
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1670
+ type: "button",
1671
+ className: "dsh-atb-mode-opt",
1672
+ "data-on": mode === "claim",
1673
+ onClick: () => setMode("claim"),
1674
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1675
+ className: "dsh-atb-mode-name",
1676
+ children: "🤝 认领制"
1677
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1678
+ className: "dsh-atb-mode-hint",
1679
+ children: "项目内会话认领"
1680
+ })]
1681
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1682
+ type: "button",
1683
+ className: "dsh-atb-mode-opt",
1684
+ "data-on": mode === "scheduled",
1685
+ onClick: () => setMode("scheduled"),
1686
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1687
+ className: "dsh-atb-mode-name",
1688
+ children: "⏰ 定时执行"
1689
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1690
+ className: "dsh-atb-mode-hint",
1691
+ children: "到点自动开跑"
1692
+ })]
1693
+ })]
1694
+ })
1695
+ }),
1696
+ mode === "scheduled" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Field, {
1697
+ label: "Cron 表达式",
1698
+ required: true,
1699
+ full: true,
1700
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1701
+ className: cronBad ? "dsh-atb-input-bad" : void 0,
1702
+ value: cron,
1703
+ onChange: (e) => setCron(e.target.value),
1704
+ placeholder: "分 时 日 月 周",
1705
+ spellCheck: false
1706
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1707
+ className: "dsh-atb-cron-presets",
1708
+ children: [CRON_PRESETS.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1709
+ type: "button",
1710
+ className: "dsh-atb-cron-preset",
1711
+ "data-on": cron.trim() === p.cron,
1712
+ onClick: () => setCron(p.cron),
1713
+ children: p.label
1714
+ }, p.cron)), !cronBad && nextRun !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1715
+ className: "dsh-atb-cron-next",
1716
+ children: ["下次 ", fmtTime(nextRun)]
1717
+ })]
1718
+ })]
1719
+ })
1720
+ ]
1721
+ }),
1722
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1723
+ className: "dsh-atb-modal-foot",
1724
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1725
+ className: "dsh-atb-modal-hint",
1726
+ "data-tone": valid ? void 0 : "bad",
1727
+ children: hint
1728
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1729
+ className: "dsh-atb-modal-footbtns",
1730
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1731
+ type: "button",
1732
+ className: "dsh-atb-btn",
1733
+ onClick: () => controller.closeForm(),
1734
+ children: "取消"
1735
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1736
+ type: "button",
1737
+ className: "dsh-atb-btn",
1738
+ "data-primary": "true",
1739
+ disabled: !valid,
1740
+ onClick: submit,
1741
+ children: editing ? "保存修改" : "创建任务"
1742
+ })]
1743
+ })]
1744
+ })
1745
+ ]
1746
+ })
1747
+ });
1748
+ }
1749
+
1750
+ //#endregion
1751
+ //#region src/client/board/TaskBoard.tsx
1752
+ /**
1753
+ * The main board view: toolbar (project filter, urgency chips, secondary tab,
1754
+ * composer), five status columns, the detail pane, and the new-task modal.
1755
+ *
1756
+ * @module dsh-taskboard/client/board/TaskBoard
1757
+ */
1758
+ /** Column labels. */
1759
+ const COLUMN_LABELS = {
1760
+ backlog: "待规划",
1761
+ todo: "待办",
1762
+ in_progress: "进行中",
1763
+ in_review: "待验收",
1764
+ done: "已完成",
1765
+ canceled: "已取消",
1766
+ archived: "已归档"
1767
+ };
1768
+ /** The two columns between which cards may be dragged both ways. */
1769
+ const DRAGGABLE_STATUSES = /* @__PURE__ */ new Set(["backlog", "todo"]);
1770
+ /** Urgency chip labels. */
1771
+ const URGENCY_LABELS = {
1772
+ urgent: "紧急",
1773
+ normal: "一般",
1774
+ relaxed: "不急"
1775
+ };
1776
+ /** Format an epoch ms as a short local stamp. */
1777
+ function fmtTime(ms) {
1778
+ if (ms === void 0) return "";
1779
+ const d = new Date(ms);
1780
+ const pad = (n) => String(n).padStart(2, "0");
1781
+ return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
1782
+ }
1783
+ /** Apply the active filters to a task list. */
1784
+ function filterTasks(state, tasks) {
1785
+ return tasks.filter((t) => (state.filters.workspaceId === void 0 || t.workspaceId === state.filters.workspaceId) && (state.filters.urgencies.length === 0 || state.filters.urgencies.includes(t.urgency)));
1786
+ }
1787
+ /**
1788
+ * The board view root.
1789
+ * @param controller - the controller.
1790
+ */
1791
+ function TaskBoard({ controller }) {
1792
+ const state = (0, react.useSyncExternalStore)((cb) => controller.subscribe(cb), () => controller.getSnapshot());
1793
+ const live = filterTasks(state, state.ledger.tasks.filter((t) => t.trashedAt === void 0));
1794
+ const selected = state.selectedId === void 0 ? void 0 : state.ledger.tasks.find((t) => t.id === state.selectedId);
1795
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1796
+ className: "dsh-atb-board",
1797
+ children: [
1798
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1799
+ className: "dsh-atb-toolbar",
1800
+ children: [
1801
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1802
+ className: "dsh-atb-title",
1803
+ children: "Agent 任务看板"
1804
+ }),
1805
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1806
+ className: "dsh-atb-count",
1807
+ children: [
1808
+ live.length,
1809
+ " 任务 · rev ",
1810
+ state.ledger.revision
1811
+ ]
1812
+ }),
1813
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "dsh-atb-spacer" }),
1814
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1815
+ className: "dsh-atb-select",
1816
+ value: state.filters.workspaceId ?? "",
1817
+ onChange: (e) => controller.setWorkspaceFilter(e.target.value === "" ? void 0 : e.target.value),
1818
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1819
+ value: "",
1820
+ children: "全部项目"
1821
+ }), state.workspaces.map((ws) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1822
+ value: ws.id,
1823
+ children: ws.title || ws.path
1824
+ }, ws.id))]
1825
+ }),
1826
+ [
1827
+ "urgent",
1828
+ "normal",
1829
+ "relaxed"
1830
+ ].map((u) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1831
+ type: "button",
1832
+ className: "dsh-atb-chip",
1833
+ "data-urgency": u,
1834
+ "data-on": state.filters.urgencies.includes(u),
1835
+ onClick: () => controller.toggleUrgency(u),
1836
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1837
+ className: "dsh-atb-dot",
1838
+ "data-urgency": u
1839
+ }), URGENCY_LABELS[u]]
1840
+ }, u)),
1841
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1842
+ type: "button",
1843
+ className: "dsh-atb-btn",
1844
+ onClick: () => controller.toggleSecondary(),
1845
+ children: state.secondaryOpen ? "返回看板" : "其它任务"
1846
+ }),
1847
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1848
+ type: "button",
1849
+ className: "dsh-atb-btn",
1850
+ "data-primary": "true",
1851
+ onClick: () => controller.setComposer(true),
1852
+ children: "+ 新建任务"
1853
+ })
1854
+ ]
1855
+ }),
1856
+ state.error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1857
+ className: "dsh-atb-error",
1858
+ children: state.error
1859
+ }),
1860
+ state.secondaryOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SecondaryTab, {
1861
+ controller,
1862
+ tasks: filterTasks(state, state.ledger.tasks)
1863
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1864
+ className: "dsh-atb-columns",
1865
+ children: MAIN_STATUSES.map((status) => {
1866
+ const columnTasks = live.filter((t) => t.status === status);
1867
+ const dropTarget = DRAGGABLE_STATUSES.has(status);
1868
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1869
+ className: "dsh-atb-column",
1870
+ onDragOver: dropTarget ? (e) => {
1871
+ if (e.dataTransfer.types.includes("application/x-dsh-atb-task")) {
1872
+ e.preventDefault();
1873
+ e.dataTransfer.dropEffect = "move";
1874
+ e.currentTarget.dataset.dragover = "true";
1875
+ }
1876
+ } : void 0,
1877
+ onDragLeave: dropTarget ? (e) => {
1878
+ delete e.currentTarget.dataset.dragover;
1879
+ } : void 0,
1880
+ onDrop: dropTarget ? (e) => {
1881
+ e.preventDefault();
1882
+ delete e.currentTarget.dataset.dragover;
1883
+ const id = e.dataTransfer.getData(DRAG_TYPE);
1884
+ if (id.length === 0) return;
1885
+ const task = state.ledger.tasks.find((t) => t.id === id);
1886
+ if (task === void 0 || task.status === status) return;
1887
+ controller.move(id, task.version, status);
1888
+ } : void 0,
1889
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1890
+ className: "dsh-atb-colhead",
1891
+ children: [COLUMN_LABELS[status], /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1892
+ className: "dsh-atb-colcount",
1893
+ children: columnTasks.length
1894
+ })]
1895
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1896
+ className: "dsh-atb-cards",
1897
+ children: [columnTasks.map((task) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskCard, {
1898
+ task,
1899
+ controller,
1900
+ draggable: dropTarget
1901
+ }, task.id)), columnTasks.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1902
+ className: "dsh-atb-empty",
1903
+ children: "无任务"
1904
+ })]
1905
+ })]
1906
+ }, status);
1907
+ })
1908
+ }),
1909
+ selected !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1910
+ className: "dsh-atb-detailpanel",
1911
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskDetail, {
1912
+ task: selected,
1913
+ controller
1914
+ })
1915
+ }),
1916
+ state.composerOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskFormModal, {
1917
+ controller,
1918
+ task: state.editingId === void 0 ? void 0 : state.ledger.tasks.find((t) => t.id === state.editingId)
1919
+ })
1920
+ ]
1921
+ });
1922
+ }
1923
+ /** Secondary tab: canceled/archived/trashed rows. */
1924
+ function SecondaryTab({ controller, tasks }) {
1925
+ const rows = tasks.filter((t) => t.status === "canceled" || t.status === "archived" || t.trashedAt !== void 0);
1926
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1927
+ className: "dsh-atb-secondary",
1928
+ children: [
1929
+ rows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1930
+ className: "dsh-atb-empty",
1931
+ children: "无已取消 / 已归档 / 已删除任务"
1932
+ }),
1933
+ rows.map((task) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskCard, {
1934
+ task,
1935
+ controller
1936
+ }, task.id)),
1937
+ void 0
1938
+ ]
1939
+ });
1940
+ }
1941
+
1942
+ //#endregion
1943
+ //#region src/client/board-mount.tsx
1944
+ /**
1945
+ * Board view mounting: a container appended inside the `[data-pane=
1946
+ * "conversation"]` grid item (a trailing child React never manages), with a
1947
+ * stylesheet rule hiding the conversation content while the board is active.
1948
+ * Toggling rides a data attribute on <html> — no React involvement in the
1949
+ * shell.
1950
+ *
1951
+ * @module dsh-taskboard/client/board-mount
1952
+ */
1953
+ const CONVERSATION_COLUMN_SELECTOR = "[data-pane=\"conversation\"]";
1954
+ const ACTIVE_ATTR = "data-dsh-atb-active";
1955
+ /** Sibling panels' activation attributes, evicted when this board opens. */
1956
+ const OTHER_ACTIVE_ATTRS = ["data-dsh-taskboard-active", "data-dsh-ssh-active"];
1957
+ /** Cross-plugin activation event; detail is the activating panel name. */
1958
+ const ACTIVATE_EVENT = "dsh-panel-activate";
1959
+ const PANEL_NAME = "dsh-taskboard";
1960
+ /** Find the center column. */
1961
+ function conversationColumn() {
1962
+ return document.querySelector(CONVERSATION_COLUMN_SELECTOR) ?? void 0;
1963
+ }
1964
+ /**
1965
+ * Mount the board React tree and bind visibility to the controller.
1966
+ * @param controller - the controller.
1967
+ * @returns disposer.
1968
+ */
1969
+ function mountBoard(controller) {
1970
+ let root;
1971
+ let container;
1972
+ const ensure = () => {
1973
+ if (container !== void 0) return;
1974
+ const column = conversationColumn();
1975
+ if (column === void 0) return;
1976
+ container = document.createElement("div");
1977
+ container.dataset.dshAtbView = "";
1978
+ container.className = "dsh-atb-view";
1979
+ column.appendChild(container);
1980
+ root = (0, react_dom_client.createRoot)(container);
1981
+ root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskBoard, { controller }));
1982
+ };
1983
+ const waitObserver = new MutationObserver(() => {
1984
+ ensure();
1985
+ });
1986
+ waitObserver.observe(document.body, {
1987
+ childList: true,
1988
+ subtree: true
1989
+ });
1990
+ const applyActive = () => {
1991
+ if (controller.getSnapshot().boardOpen) {
1992
+ for (const attr of OTHER_ACTIVE_ATTRS) document.documentElement.removeAttribute(attr);
1993
+ document.documentElement.setAttribute(ACTIVE_ATTR, "");
1994
+ document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: PANEL_NAME }));
1995
+ } else document.documentElement.removeAttribute(ACTIVE_ATTR);
1996
+ };
1997
+ const onOtherActivate = (event) => {
1998
+ if (event.detail !== PANEL_NAME && controller.getSnapshot().boardOpen) controller.closeBoard();
1999
+ };
2000
+ const SIDEBAR_ROW_SELECTOR = "[class*=\"sessionRow\"], [class*=\"projectRow\"], [class*=\"searchResultRow\"], [class*=\"searchResultWorkspace\"], [class*=\"newSession\"]";
2001
+ const onClickSidebarRow = (event) => {
2002
+ if (!controller.getSnapshot().boardOpen) return;
2003
+ const target = event.target;
2004
+ if (target === null) return;
2005
+ if (target.closest(SIDEBAR_ROW_SELECTOR) !== null) controller.closeBoard();
2006
+ };
2007
+ document.addEventListener("click", onClickSidebarRow, true);
2008
+ document.addEventListener(ACTIVATE_EVENT, onOtherActivate);
2009
+ const unsubscribe = controller.subscribe(applyActive);
2010
+ applyActive();
2011
+ ensure();
2012
+ return () => {
2013
+ document.removeEventListener("click", onClickSidebarRow, true);
2014
+ document.removeEventListener(ACTIVATE_EVENT, onOtherActivate);
2015
+ waitObserver.disconnect();
2016
+ unsubscribe();
2017
+ document.documentElement.removeAttribute(ACTIVE_ATTR);
2018
+ root?.unmount();
2019
+ container?.remove();
2020
+ };
2021
+ }
2022
+
2023
+ //#endregion
2024
+ //#region src/client/index.ts
2025
+ /**
2026
+ * Browser half entry for dsh-taskboard: wires the route client and the
2027
+ * board controller, exposes the model catalog (via the runtime's llm.models
2028
+ * RPC when the connection service is present), mounts the sidebar entry and
2029
+ * the board view.
2030
+ *
2031
+ * Failure policy: DOM mounting problems are logged, never thrown — the web
2032
+ * shell fails the whole boot when a plugin apply throws.
2033
+ *
2034
+ * Export shape: `name` / `inject` / `apply`, no default.
2035
+ *
2036
+ * @module dsh-taskboard/client
2037
+ */
2038
+ /** Client plugin name. */
2039
+ const name = "dsh-taskboard/client";
2040
+ /** Required client services (fiber inject waiting). */
2041
+ const inject = ["connection"];
2042
+ /**
2043
+ * Mount the client half.
2044
+ * @param ctx - the client context (connection injected).
2045
+ */
2046
+ function apply(ctx) {
2047
+ try {
2048
+ injectStyles();
2049
+ const controller = new BoardController(createClient());
2050
+ const connection = ctx.get?.("connection");
2051
+ if (connection !== void 0) controller.modelCatalog = async () => {
2052
+ const response = await connection.api.llm.models({});
2053
+ if (!response.result.ok) return [];
2054
+ const out = [];
2055
+ for (const group of response.result.value.groups) for (const model of group.models) out.push({
2056
+ provider: group.id,
2057
+ model: model.id,
2058
+ name: model.name
2059
+ });
2060
+ return out;
2061
+ };
2062
+ controller.start();
2063
+ const disposers = [];
2064
+ try {
2065
+ disposers.push(mountSidebarEntry(controller));
2066
+ disposers.push(mountBoard(controller));
2067
+ } catch (error) {
2068
+ console.error("[dsh-taskboard] mount failed:", error);
2069
+ }
2070
+ ctx.effect?.(() => () => {
2071
+ for (const d of disposers.splice(0)) d();
2072
+ controller.dispose();
2073
+ }, "dsh-taskboard: client mount");
2074
+ } catch (error) {
2075
+ console.error("[dsh-taskboard] client half failed to start:", error);
2076
+ }
2077
+ }
2078
+
2079
+ //#endregion
2080
+ exports.apply = apply;
2081
+ exports.inject = inject;
2082
+ exports.name = name;
2083
+ return module.exports;
2084
+ }
2085
+ });