dsh-taskboard 0.1.1 → 0.2.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.
- package/README.md +47 -3
- package/lib/client.js +886 -139
- package/lib/host/execution.js +160 -33
- package/lib/host/execution.js.map +1 -1
- package/lib/host/routes.js +30 -3
- package/lib/host/routes.js.map +1 -1
- package/lib/host/scheduler.js +10 -1
- package/lib/host/scheduler.js.map +1 -1
- package/lib/host/store.js +31 -18
- package/lib/host/store.js.map +1 -1
- package/lib/host/tools.js +14 -4
- package/lib/host/tools.js.map +1 -1
- package/lib/index.js +27 -4
- package/lib/index.js.map +1 -1
- package/lib/shared/protocol.js +53 -2
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +1 -1
- package/src/client/api.ts +3 -0
- package/src/client/board/AlertModal.tsx +36 -0
- package/src/client/board/TaskBoard.tsx +126 -44
- package/src/client/board/TaskCard.tsx +16 -3
- package/src/client/board/TaskDetail.tsx +93 -14
- package/src/client/board/TaskFormModal.tsx +47 -1
- package/src/client/controller.ts +171 -8
- package/src/client/index.ts +10 -0
- package/src/client/session-jump.ts +93 -0
- package/src/client/sidebar-entry.ts +98 -1
- package/src/client/styles.ts +83 -6
- package/src/host/execution.ts +202 -18
- package/src/host/routes.ts +38 -11
- package/src/host/scheduler.ts +20 -4
- package/src/host/store.ts +34 -13
- package/src/host/tools.ts +30 -8
- package/src/index.ts +37 -3
- package/src/shared/protocol.ts +72 -3
- package/src/shared/version.ts +9 -0
package/lib/client.js
CHANGED
|
@@ -37,6 +37,7 @@ window.__ModuleLoader__.load({
|
|
|
37
37
|
comment: (id, bodyText) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/comment`, { body: bodyText }),
|
|
38
38
|
remove: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/delete`, body),
|
|
39
39
|
run: (id) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/run`, {}),
|
|
40
|
+
cancel: (id) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/cancel`, {}),
|
|
40
41
|
stream(onChange, onGap) {
|
|
41
42
|
const es = new EventSource("/dsh-taskboard/events");
|
|
42
43
|
let revision;
|
|
@@ -205,13 +206,43 @@ window.__ModuleLoader__.load({
|
|
|
205
206
|
|
|
206
207
|
//#endregion
|
|
207
208
|
//#region src/client/controller.ts
|
|
208
|
-
/**
|
|
209
|
+
/** localStorage key for persisted view state (filters + sort). */
|
|
210
|
+
const VIEW_KEY = "dsh-taskboard-view-v1";
|
|
211
|
+
/** Load the persisted view state (never throws; fresh on any parse error). */
|
|
212
|
+
function loadView() {
|
|
213
|
+
try {
|
|
214
|
+
const raw = localStorage.getItem(VIEW_KEY);
|
|
215
|
+
if (raw === null) return {
|
|
216
|
+
urgencies: [],
|
|
217
|
+
sortBy: "default"
|
|
218
|
+
};
|
|
219
|
+
const parsed = JSON.parse(raw);
|
|
220
|
+
const sortBy = parsed.sortBy === "updated" || parsed.sortBy === "urgency" || parsed.sortBy === "created" ? parsed.sortBy : "default";
|
|
221
|
+
return {
|
|
222
|
+
workspaceId: typeof parsed.workspaceId === "string" ? parsed.workspaceId : void 0,
|
|
223
|
+
urgencies: Array.isArray(parsed.urgencies) ? parsed.urgencies.filter((u) => u === "urgent" || u === "normal" || u === "relaxed") : [],
|
|
224
|
+
sortBy
|
|
225
|
+
};
|
|
226
|
+
} catch {
|
|
227
|
+
return {
|
|
228
|
+
urgencies: [],
|
|
229
|
+
sortBy: "default"
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/** Instantiate the default state (view state hydrated from localStorage). */
|
|
209
234
|
function initialState() {
|
|
235
|
+
const view = loadView();
|
|
210
236
|
return {
|
|
211
237
|
boardOpen: false,
|
|
212
238
|
ledger: emptyLedger(),
|
|
213
239
|
workspaces: [],
|
|
214
|
-
filters: {
|
|
240
|
+
filters: {
|
|
241
|
+
workspaceId: view.workspaceId,
|
|
242
|
+
urgencies: view.urgencies
|
|
243
|
+
},
|
|
244
|
+
search: "",
|
|
245
|
+
sortBy: view.sortBy,
|
|
215
246
|
composerOpen: false,
|
|
216
247
|
secondaryOpen: false
|
|
217
248
|
};
|
|
@@ -226,6 +257,7 @@ window.__ModuleLoader__.load({
|
|
|
226
257
|
disposed = false;
|
|
227
258
|
disposeStream;
|
|
228
259
|
refreshInFlight;
|
|
260
|
+
sessionJumper;
|
|
229
261
|
/** @param client - the route client. */
|
|
230
262
|
constructor(client) {
|
|
231
263
|
this.client = client;
|
|
@@ -303,14 +335,15 @@ window.__ModuleLoader__.load({
|
|
|
303
335
|
toggleBoard() {
|
|
304
336
|
this.setState({ boardOpen: !this.state.boardOpen });
|
|
305
337
|
}
|
|
306
|
-
/** Set the project filter. */
|
|
338
|
+
/** Set the project filter (persisted). */
|
|
307
339
|
setWorkspaceFilter(workspaceId) {
|
|
308
340
|
this.setState({ filters: {
|
|
309
341
|
...this.state.filters,
|
|
310
342
|
workspaceId
|
|
311
343
|
} });
|
|
344
|
+
this.persistView();
|
|
312
345
|
}
|
|
313
|
-
/** Toggle one urgency chip. */
|
|
346
|
+
/** Toggle one urgency chip (persisted). */
|
|
314
347
|
toggleUrgency(urgency) {
|
|
315
348
|
const set = new Set(this.state.filters.urgencies);
|
|
316
349
|
if (set.has(urgency)) set.delete(urgency);
|
|
@@ -319,6 +352,26 @@ window.__ModuleLoader__.load({
|
|
|
319
352
|
...this.state.filters,
|
|
320
353
|
urgencies: [...set]
|
|
321
354
|
} });
|
|
355
|
+
this.persistView();
|
|
356
|
+
}
|
|
357
|
+
/** Set the free-text search (transient — not persisted). */
|
|
358
|
+
setSearch(search) {
|
|
359
|
+
this.setState({ search });
|
|
360
|
+
}
|
|
361
|
+
/** Set the column sort order (persisted). */
|
|
362
|
+
setSortBy(sortBy) {
|
|
363
|
+
this.setState({ sortBy });
|
|
364
|
+
this.persistView();
|
|
365
|
+
}
|
|
366
|
+
/** Write the current view state to localStorage (best effort). */
|
|
367
|
+
persistView() {
|
|
368
|
+
try {
|
|
369
|
+
localStorage.setItem(VIEW_KEY, JSON.stringify({
|
|
370
|
+
workspaceId: this.state.filters.workspaceId,
|
|
371
|
+
urgencies: this.state.filters.urgencies,
|
|
372
|
+
sortBy: this.state.sortBy
|
|
373
|
+
}));
|
|
374
|
+
} catch {}
|
|
322
375
|
}
|
|
323
376
|
/** Select a task (open detail). */
|
|
324
377
|
select(id) {
|
|
@@ -349,17 +402,45 @@ window.__ModuleLoader__.load({
|
|
|
349
402
|
toggleSecondary() {
|
|
350
403
|
this.setState({ secondaryOpen: !this.state.secondaryOpen });
|
|
351
404
|
}
|
|
352
|
-
/**
|
|
405
|
+
/**
|
|
406
|
+
* Install the session-jump bridge (built from the runtime sessions service
|
|
407
|
+
* by the client entry). Without it openSession reports 'unavailable'.
|
|
408
|
+
* @param jumper - the jump function from createSessionJumper.
|
|
409
|
+
*/
|
|
410
|
+
installSessionJumper(jumper) {
|
|
411
|
+
this.sessionJumper = jumper;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Jump to an execution's session (open it in the GUI). On success the board
|
|
415
|
+
* closes so the conversation shows; a deleted-or-archived session reports
|
|
416
|
+
* 'missing' for the caller to prompt about.
|
|
417
|
+
* @param sessionId - the execution's session id.
|
|
418
|
+
* @returns the jump outcome.
|
|
419
|
+
*/
|
|
420
|
+
async openSession(sessionId) {
|
|
421
|
+
if (this.sessionJumper === void 0) return "unavailable";
|
|
422
|
+
let result;
|
|
423
|
+
try {
|
|
424
|
+
result = await this.sessionJumper(sessionId);
|
|
425
|
+
} catch {
|
|
426
|
+
return "unavailable";
|
|
427
|
+
}
|
|
428
|
+
if (result === "opened") this.closeBoard();
|
|
429
|
+
return result;
|
|
430
|
+
}
|
|
431
|
+
/** Create a task (composer submit); returns the new task id, undefined on failure. */
|
|
353
432
|
async create(body) {
|
|
354
433
|
try {
|
|
355
|
-
await this.client.create(body);
|
|
434
|
+
const summary = await this.client.create(body);
|
|
356
435
|
this.setState({
|
|
357
436
|
composerOpen: false,
|
|
358
437
|
error: void 0
|
|
359
438
|
});
|
|
360
439
|
await this.refresh();
|
|
440
|
+
return summary.id;
|
|
361
441
|
} catch (error) {
|
|
362
442
|
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
443
|
+
return;
|
|
363
444
|
}
|
|
364
445
|
}
|
|
365
446
|
/** Edit task fields (form modal submit; the GUI is the owner surface). */
|
|
@@ -375,8 +456,10 @@ window.__ModuleLoader__.load({
|
|
|
375
456
|
error: void 0
|
|
376
457
|
});
|
|
377
458
|
await this.refresh();
|
|
459
|
+
return true;
|
|
378
460
|
} catch (error) {
|
|
379
461
|
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
462
|
+
return false;
|
|
380
463
|
}
|
|
381
464
|
}
|
|
382
465
|
/** Move a task (user surface: done allowed). */
|
|
@@ -421,6 +504,15 @@ window.__ModuleLoader__.load({
|
|
|
421
504
|
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
422
505
|
}
|
|
423
506
|
}
|
|
507
|
+
/** Cancel the running execution (stops the agent session; task returns to todo). */
|
|
508
|
+
async cancel(id) {
|
|
509
|
+
try {
|
|
510
|
+
await this.client.cancel(id);
|
|
511
|
+
await this.refresh();
|
|
512
|
+
} catch (error) {
|
|
513
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
424
516
|
/** Soft-delete (agent parity) then optional purge. */
|
|
425
517
|
async remove(id, ifVersion, purge) {
|
|
426
518
|
try {
|
|
@@ -431,6 +523,93 @@ window.__ModuleLoader__.load({
|
|
|
431
523
|
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
432
524
|
}
|
|
433
525
|
}
|
|
526
|
+
/** Duplicate a task into a fresh todo card (same project/urgency/prompt/execution/model). */
|
|
527
|
+
async duplicate(task) {
|
|
528
|
+
try {
|
|
529
|
+
await this.client.create({
|
|
530
|
+
title: `${task.title}(副本)`,
|
|
531
|
+
workspaceId: task.workspaceId,
|
|
532
|
+
urgency: task.urgency,
|
|
533
|
+
description: task.description.length > 0 ? task.description : void 0,
|
|
534
|
+
prompt: task.prompt.length > 0 ? task.prompt : void 0,
|
|
535
|
+
execution: task.execution.mode === "scheduled" && task.execution.cron !== void 0 ? {
|
|
536
|
+
mode: "scheduled",
|
|
537
|
+
cron: task.execution.cron
|
|
538
|
+
} : { mode: "claim" },
|
|
539
|
+
model: task.model
|
|
540
|
+
});
|
|
541
|
+
await this.refresh();
|
|
542
|
+
} catch (error) {
|
|
543
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/** Download the whole ledger as a JSON backup file. */
|
|
547
|
+
exportJson() {
|
|
548
|
+
const stamp = /* @__PURE__ */ new Date();
|
|
549
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
550
|
+
const name = `dsh-taskboard-${stamp.getFullYear()}${pad(stamp.getMonth() + 1)}${pad(stamp.getDate())}-${pad(stamp.getHours())}${pad(stamp.getMinutes())}.json`;
|
|
551
|
+
const body = JSON.stringify(this.state.ledger, null, 2);
|
|
552
|
+
this.download(name, body, "application/json");
|
|
553
|
+
}
|
|
554
|
+
/** Download the task list as a CSV (BOM-prefixed for Excel + Chinese text). */
|
|
555
|
+
exportCsv() {
|
|
556
|
+
const esc = (v) => {
|
|
557
|
+
const s = String(v ?? "");
|
|
558
|
+
return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, "\"\"")}"` : s;
|
|
559
|
+
};
|
|
560
|
+
const header = [
|
|
561
|
+
"id",
|
|
562
|
+
"title",
|
|
563
|
+
"status",
|
|
564
|
+
"urgency",
|
|
565
|
+
"blocked",
|
|
566
|
+
"project",
|
|
567
|
+
"claimedBy",
|
|
568
|
+
"mode",
|
|
569
|
+
"cron",
|
|
570
|
+
"nextRunAt",
|
|
571
|
+
"model",
|
|
572
|
+
"createdAt",
|
|
573
|
+
"updatedAt",
|
|
574
|
+
"comments",
|
|
575
|
+
"executions"
|
|
576
|
+
];
|
|
577
|
+
const rows = this.state.ledger.tasks.map((t) => [
|
|
578
|
+
t.id,
|
|
579
|
+
t.title,
|
|
580
|
+
t.status,
|
|
581
|
+
t.urgency,
|
|
582
|
+
t.blocked ? "yes" : "no",
|
|
583
|
+
t.workspaceId,
|
|
584
|
+
t.claimedBy ?? "",
|
|
585
|
+
t.execution.mode,
|
|
586
|
+
t.execution.cron ?? "",
|
|
587
|
+
t.execution.nextRunAt !== void 0 ? new Date(t.execution.nextRunAt).toISOString() : "",
|
|
588
|
+
t.model !== void 0 ? `${t.model.provider}/${t.model.model}` : "",
|
|
589
|
+
new Date(t.createdAt).toISOString(),
|
|
590
|
+
new Date(t.updatedAt).toISOString(),
|
|
591
|
+
t.comments.length,
|
|
592
|
+
t.executions.length
|
|
593
|
+
].map(esc).join(","));
|
|
594
|
+
const stamp = /* @__PURE__ */ new Date();
|
|
595
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
596
|
+
const name = `dsh-taskboard-${stamp.getFullYear()}${pad(stamp.getMonth() + 1)}${pad(stamp.getDate())}.csv`;
|
|
597
|
+
this.download(name, `\uFEFF${[header.join(","), ...rows].join("\r\n")}`, "text/csv");
|
|
598
|
+
}
|
|
599
|
+
/** Trigger a browser download (no-op when the DOM is unavailable). */
|
|
600
|
+
download(filename, body, type) {
|
|
601
|
+
try {
|
|
602
|
+
const blob = new Blob([body], { type });
|
|
603
|
+
const url = URL.createObjectURL(blob);
|
|
604
|
+
const a = document.createElement("a");
|
|
605
|
+
a.href = url;
|
|
606
|
+
a.download = filename;
|
|
607
|
+
a.click();
|
|
608
|
+
setTimeout(() => URL.revokeObjectURL(url), 5e3);
|
|
609
|
+
} catch (error) {
|
|
610
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
611
|
+
}
|
|
612
|
+
}
|
|
434
613
|
};
|
|
435
614
|
|
|
436
615
|
//#endregion
|
|
@@ -446,7 +625,7 @@ window.__ModuleLoader__.load({
|
|
|
446
625
|
/** The stylesheet text. */
|
|
447
626
|
const STYLES = `
|
|
448
627
|
.dsh-atb-entry {
|
|
449
|
-
display: flex; align-items: center; gap: 8px;
|
|
628
|
+
display: flex; align-items: center; gap: 8px; position: relative;
|
|
450
629
|
width: calc(100% - 8px); margin: 2px 4px; padding: 6px 10px;
|
|
451
630
|
border: none; border-radius: 8px; background: transparent;
|
|
452
631
|
color: var(--dsw-text-secondary, inherit); font: inherit; font-size: 13px;
|
|
@@ -455,6 +634,35 @@ window.__ModuleLoader__.load({
|
|
|
455
634
|
.dsh-atb-entry:hover { background: var(--dsw-hover, rgba(128,128,128,.12)); color: var(--dsw-text-primary, inherit); }
|
|
456
635
|
.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
636
|
.dsh-atb-entry svg { flex: none; }
|
|
637
|
+
/* Status strip on the entry row's right: todo|in_progress|in_review counts. */
|
|
638
|
+
.dsh-atb-entry-stats {
|
|
639
|
+
margin-left: auto; display: inline-flex; align-items: center; gap: 3px;
|
|
640
|
+
font-size: 11px; line-height: 1; color: var(--dsw-text-secondary, gray);
|
|
641
|
+
font-variant-numeric: tabular-nums; white-space: nowrap; cursor: help;
|
|
642
|
+
}
|
|
643
|
+
.dsh-atb-entry-sep { opacity: .5; }
|
|
644
|
+
/* Each rolling count wears its status color (todo blue | in_progress orange |
|
|
645
|
+
in_review purple); the separators stay in the strip's neutral gray. */
|
|
646
|
+
.dsh-atb-roll[data-stat="todo"] { color: #3e63dd; }
|
|
647
|
+
.dsh-atb-roll[data-stat="in_progress"] { color: #d9822b; }
|
|
648
|
+
.dsh-atb-roll[data-stat="in_review"] { color: #8e4ec6; }
|
|
649
|
+
/* One rolling number: fixed one-line window, overflow hidden. */
|
|
650
|
+
.dsh-atb-roll {
|
|
651
|
+
position: relative; display: inline-block; overflow: hidden;
|
|
652
|
+
height: 12px; min-width: 1ch; text-align: center; vertical-align: middle;
|
|
653
|
+
}
|
|
654
|
+
.dsh-atb-rn { display: block; height: 12px; line-height: 12px; text-align: center; }
|
|
655
|
+
/* The incoming value sits just outside the window (below for up-scroll). */
|
|
656
|
+
.dsh-atb-rn-next { position: absolute; left: 0; right: 0; top: 100%; }
|
|
657
|
+
.dsh-atb-roll[data-dir="down"] .dsh-atb-rn-next { top: auto; bottom: 100%; }
|
|
658
|
+
.dsh-atb-roll .dsh-atb-rn { transition: transform .3s cubic-bezier(.25, .1, .25, 1); }
|
|
659
|
+
.dsh-atb-roll[data-anim="1"][data-dir="up"] .dsh-atb-rn { transform: translateY(-100%); }
|
|
660
|
+
.dsh-atb-roll[data-anim="1"][data-dir="down"] .dsh-atb-rn { transform: translateY(100%); }
|
|
661
|
+
@media (prefers-reduced-motion: reduce) {
|
|
662
|
+
.dsh-atb-roll .dsh-atb-rn { transition: none; }
|
|
663
|
+
}
|
|
664
|
+
.dsh-atb-search { width: 130px; }
|
|
665
|
+
.dsh-atb-badge[data-kind="stale"] { background: rgba(217,130,43,.15); color: #d9822b; }
|
|
458
666
|
|
|
459
667
|
html[data-dsh-atb-active] [data-pane="conversation"] > *:not([data-dsh-atb-view]) { display: none !important; }
|
|
460
668
|
.dsh-atb-view { display: none; }
|
|
@@ -464,6 +672,16 @@ window.__ModuleLoader__.load({
|
|
|
464
672
|
.dsh-atb-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
465
673
|
.dsh-atb-title { font-size: 15px; font-weight: 600; margin: 0; }
|
|
466
674
|
.dsh-atb-count { font-size: 12px; color: var(--dsw-text-secondary, gray); }
|
|
675
|
+
.dsh-atb-ver {
|
|
676
|
+
font-size: 11px; color: var(--dsw-text-secondary, gray);
|
|
677
|
+
font-variant-numeric: tabular-nums; white-space: nowrap; cursor: pointer;
|
|
678
|
+
text-decoration: none;
|
|
679
|
+
padding: 1px 9px; border-radius: 999px;
|
|
680
|
+
background: var(--dsw-bg-inset, rgba(128,128,128,.1));
|
|
681
|
+
border: 1px solid var(--dsw-border, rgba(128,128,128,.22));
|
|
682
|
+
transition: border-color .12s ease, color .12s ease;
|
|
683
|
+
}
|
|
684
|
+
.dsh-atb-ver:hover { border-color: var(--dsw-border-strong, rgba(128,128,128,.6)); color: inherit; }
|
|
467
685
|
.dsh-atb-spacer { flex: 1; }
|
|
468
686
|
.dsh-atb-select, .dsh-atb-input {
|
|
469
687
|
font: inherit; font-size: 12.5px; padding: 5px 8px; border-radius: 7px;
|
|
@@ -484,6 +702,16 @@ window.__ModuleLoader__.load({
|
|
|
484
702
|
.dsh-atb-dot[data-urgency="urgent"] { background: #e5484d; }
|
|
485
703
|
.dsh-atb-dot[data-urgency="normal"] { background: #8e4ec6; }
|
|
486
704
|
.dsh-atb-dot[data-urgency="relaxed"] { background: #3e63dd; }
|
|
705
|
+
/* Status dots (column heads): one fixed color per lifecycle status, matching
|
|
706
|
+
the detail pane's status pills. Canceled/archived share the resting gray;
|
|
707
|
+
trashed (pending purge) keeps the red of the 待清除 badge. */
|
|
708
|
+
.dsh-atb-dot[data-status="backlog"] { background: #8a8f98; }
|
|
709
|
+
.dsh-atb-dot[data-status="todo"] { background: #3e63dd; }
|
|
710
|
+
.dsh-atb-dot[data-status="in_progress"] { background: #d9822b; }
|
|
711
|
+
.dsh-atb-dot[data-status="in_review"] { background: #8e4ec6; }
|
|
712
|
+
.dsh-atb-dot[data-status="done"] { background: #2ea043; }
|
|
713
|
+
.dsh-atb-dot[data-status="canceled"], .dsh-atb-dot[data-status="archived"] { background: #8a8f98; }
|
|
714
|
+
.dsh-atb-dot[data-status="trashed"] { background: #e5484d; }
|
|
487
715
|
|
|
488
716
|
.dsh-atb-btn {
|
|
489
717
|
font: inherit; font-size: 12.5px; padding: 5px 11px; border-radius: 7px; cursor: pointer;
|
|
@@ -608,12 +836,13 @@ window.__ModuleLoader__.load({
|
|
|
608
836
|
.dsh-atb-desc { white-space: pre-wrap; word-break: break-word; font-size: 13px; line-height: 1.55; }
|
|
609
837
|
|
|
610
838
|
.dsh-atb-detail-actions { display: flex; flex-direction: column; gap: 8px; }
|
|
611
|
-
.dsh-atb-
|
|
612
|
-
font: inherit; font-size:
|
|
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);
|
|
839
|
+
.dsh-atb-detail-run {
|
|
840
|
+
font: inherit; font-size: 12px; font-weight: 600; padding: 4px 11px; border-radius: 7px; cursor: pointer;
|
|
841
|
+
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);
|
|
614
842
|
transition: filter .12s ease;
|
|
615
843
|
}
|
|
616
|
-
.dsh-atb-
|
|
844
|
+
.dsh-atb-detail-run:hover { filter: brightness(1.1); }
|
|
845
|
+
.dsh-atb-detail-run[data-danger="true"] { background: rgba(229,72,77,.92); }
|
|
617
846
|
.dsh-atb-movebtns { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
618
847
|
.dsh-atb-movebtn {
|
|
619
848
|
font: inherit; font-size: 12px; padding: 4px 11px; border-radius: 999px; cursor: pointer;
|
|
@@ -692,7 +921,11 @@ window.__ModuleLoader__.load({
|
|
|
692
921
|
.dsh-atb-exec-outcome[data-outcome="running"] { background: rgba(217,130,43,.15); color: #d9822b; }
|
|
693
922
|
.dsh-atb-exec-outcome[data-outcome="cancelled"] { background: rgba(128,128,128,.15); color: var(--dsw-text-secondary, gray); }
|
|
694
923
|
.dsh-atb-exec-time { font-size: 11px; color: var(--dsw-text-secondary, gray); }
|
|
695
|
-
.dsh-atb-exec-session {
|
|
924
|
+
.dsh-atb-exec-session {
|
|
925
|
+
font: inherit; font-size: 11px; color: var(--dsw-text-secondary, gray);
|
|
926
|
+
background: none; border: none; padding: 0; cursor: pointer;
|
|
927
|
+
}
|
|
928
|
+
.dsh-atb-exec-session:hover { color: var(--dsw-alias-brand-primary, inherit); text-decoration: underline dotted; }
|
|
696
929
|
.dsh-atb-exec-error { flex-basis: 100%; font-size: 11px; color: #e5484d; word-break: break-all; }
|
|
697
930
|
|
|
698
931
|
.dsh-atb-dangerzone {
|
|
@@ -812,6 +1045,29 @@ window.__ModuleLoader__.load({
|
|
|
812
1045
|
.dsh-atb-secondary { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; }
|
|
813
1046
|
.dsh-atb-link { color: var(--dsw-alias-state-business-primary, #3e63dd); cursor: pointer; text-decoration: none; }
|
|
814
1047
|
.dsh-atb-link:hover { text-decoration: underline; }
|
|
1048
|
+
|
|
1049
|
+
/* ---------- alert modal ---------- */
|
|
1050
|
+
.dsh-atb-alert-backdrop {
|
|
1051
|
+
position: fixed; inset: 0; z-index: 90;
|
|
1052
|
+
background: var(--dsw-alias-bg-mask-drop, rgba(28,30,36,.4)); backdrop-filter: var(--dsw-mask-blur, blur(2px));
|
|
1053
|
+
display: flex; align-items: center; justify-content: center;
|
|
1054
|
+
animation: dsh-atb-fade .12s ease;
|
|
1055
|
+
}
|
|
1056
|
+
.dsh-atb-alert {
|
|
1057
|
+
min-width: 280px; max-width: 380px; padding: 20px 24px; border-radius: 14px;
|
|
1058
|
+
background: var(--dsw-alias-bg-overlay, #fff); color: var(--dsw-alias-label-primary, inherit);
|
|
1059
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
|
|
1060
|
+
box-shadow: var(--dsw-shadow-lv3, 0 12px 32px rgba(0,0,0,.18));
|
|
1061
|
+
display: flex; flex-direction: column; align-items: center; gap: 14px;
|
|
1062
|
+
animation: dsh-atb-pop .14s ease;
|
|
1063
|
+
}
|
|
1064
|
+
.dsh-atb-alert-icon { font-size: 28px; line-height: 1; }
|
|
1065
|
+
.dsh-atb-alert-msg {
|
|
1066
|
+
font-size: 13.5px; line-height: 1.55; text-align: center;
|
|
1067
|
+
word-break: break-word; white-space: pre-wrap;
|
|
1068
|
+
color: var(--dsw-alias-label-primary, inherit);
|
|
1069
|
+
}
|
|
1070
|
+
.dsh-atb-alert .dsh-atb-btn { padding: 6px 28px; font-size: 13px; }
|
|
815
1071
|
`;
|
|
816
1072
|
let injected = false;
|
|
817
1073
|
/** Inject the stylesheet once (idempotent). */
|
|
@@ -857,12 +1113,108 @@ window.__ModuleLoader__.load({
|
|
|
857
1113
|
entry.dataset.dshAtbEntry = "";
|
|
858
1114
|
entry.className = "dsh-atb-entry";
|
|
859
1115
|
entry.setAttribute("aria-label", "Agent 任务看板");
|
|
860
|
-
entry.innerHTML = `<span class="dsh-atb-entry-icon">${ICON}</span><span class="dsh-atb-entry-label">任务看板</span>`;
|
|
1116
|
+
entry.innerHTML = `<span class="dsh-atb-entry-icon">${ICON}</span><span class="dsh-atb-entry-label">任务看板</span><span class="dsh-atb-entry-stats"></span>`;
|
|
861
1117
|
entry.addEventListener("click", () => {
|
|
862
1118
|
controller.toggleBoard();
|
|
863
1119
|
});
|
|
864
1120
|
return entry;
|
|
865
1121
|
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Live status counts shown at the right of the entry row:
|
|
1124
|
+
* `[todo, in_progress, in_review]` (trashed tasks excluded).
|
|
1125
|
+
*/
|
|
1126
|
+
function entryStats(controller) {
|
|
1127
|
+
let todo = 0;
|
|
1128
|
+
let inProgress = 0;
|
|
1129
|
+
let inReview = 0;
|
|
1130
|
+
for (const task of controller.getSnapshot().ledger.tasks) {
|
|
1131
|
+
if (task.trashedAt !== void 0) continue;
|
|
1132
|
+
if (task.status === "todo") todo++;
|
|
1133
|
+
else if (task.status === "in_progress") inProgress++;
|
|
1134
|
+
else if (task.status === "in_review") inReview++;
|
|
1135
|
+
}
|
|
1136
|
+
return [
|
|
1137
|
+
todo,
|
|
1138
|
+
inProgress,
|
|
1139
|
+
inReview
|
|
1140
|
+
];
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Set one rolling-number slot. Unchanged values no-op; changes animate the
|
|
1144
|
+
* old value out and the new value in with a vertical scroll (up when the
|
|
1145
|
+
* count grows, down when it shrinks). Plain DOM, no React.
|
|
1146
|
+
*/
|
|
1147
|
+
function setRollValue(slot, value) {
|
|
1148
|
+
const text = String(value);
|
|
1149
|
+
if (slot.dataset.value === text) return;
|
|
1150
|
+
const previous = slot.dataset.value;
|
|
1151
|
+
slot.dataset.value = text;
|
|
1152
|
+
slot.style.minWidth = `${text.length}ch`;
|
|
1153
|
+
if (previous === void 0) {
|
|
1154
|
+
slot.textContent = text;
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
if (slot.dataset.busy === "1") {
|
|
1158
|
+
slot.dataset.busy = "";
|
|
1159
|
+
slot.dataset.anim = "";
|
|
1160
|
+
}
|
|
1161
|
+
const oldEl = document.createElement("span");
|
|
1162
|
+
oldEl.className = "dsh-atb-rn";
|
|
1163
|
+
oldEl.textContent = previous;
|
|
1164
|
+
const newEl = document.createElement("span");
|
|
1165
|
+
newEl.className = "dsh-atb-rn dsh-atb-rn-next";
|
|
1166
|
+
newEl.textContent = text;
|
|
1167
|
+
slot.replaceChildren(oldEl, newEl);
|
|
1168
|
+
slot.dataset.dir = value > Number(previous) ? "up" : "down";
|
|
1169
|
+
slot.dataset.busy = "1";
|
|
1170
|
+
requestAnimationFrame(() => {
|
|
1171
|
+
slot.dataset.anim = "1";
|
|
1172
|
+
});
|
|
1173
|
+
const finish = () => {
|
|
1174
|
+
if (slot.dataset.busy !== "1") return;
|
|
1175
|
+
slot.dataset.busy = "";
|
|
1176
|
+
slot.dataset.anim = "";
|
|
1177
|
+
slot.textContent = slot.dataset.value ?? "";
|
|
1178
|
+
};
|
|
1179
|
+
slot.addEventListener("transitionend", finish, { once: true });
|
|
1180
|
+
setTimeout(finish, 400);
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Wire the stats strip into the entry: builds the three slots and keeps them
|
|
1184
|
+
* (plus the tooltip) in sync with every controller emit.
|
|
1185
|
+
* @returns the update function (also called once immediately).
|
|
1186
|
+
*/
|
|
1187
|
+
function wireStats(entry, controller) {
|
|
1188
|
+
const stats = entry.querySelector(".dsh-atb-entry-stats");
|
|
1189
|
+
if (stats === null) return () => {};
|
|
1190
|
+
const statKeys = [
|
|
1191
|
+
"todo",
|
|
1192
|
+
"in_progress",
|
|
1193
|
+
"in_review"
|
|
1194
|
+
];
|
|
1195
|
+
const slots = [];
|
|
1196
|
+
for (let i = 0; i < 3; i++) {
|
|
1197
|
+
if (i > 0) {
|
|
1198
|
+
const sep = document.createElement("span");
|
|
1199
|
+
sep.className = "dsh-atb-entry-sep";
|
|
1200
|
+
sep.textContent = "|";
|
|
1201
|
+
stats.append(sep);
|
|
1202
|
+
}
|
|
1203
|
+
const slot = document.createElement("span");
|
|
1204
|
+
slot.className = "dsh-atb-roll";
|
|
1205
|
+
slot.dataset.stat = statKeys[i];
|
|
1206
|
+
stats.append(slot);
|
|
1207
|
+
slots.push(slot);
|
|
1208
|
+
}
|
|
1209
|
+
const update = () => {
|
|
1210
|
+
const [todo, inProgress, inReview] = entryStats(controller);
|
|
1211
|
+
setRollValue(slots[0], todo);
|
|
1212
|
+
setRollValue(slots[1], inProgress);
|
|
1213
|
+
setRollValue(slots[2], inReview);
|
|
1214
|
+
stats.title = `待办 ${todo} | 进行中 ${inProgress} | 待验收 ${inReview}(待办|进行中|待验收)`;
|
|
1215
|
+
};
|
|
1216
|
+
return update;
|
|
1217
|
+
}
|
|
866
1218
|
/** Re-insert the entry after the New Session row (before the browser region). */
|
|
867
1219
|
function placeEntry(root, entry) {
|
|
868
1220
|
const button = newSessionButton(root);
|
|
@@ -933,9 +1285,11 @@ window.__ModuleLoader__.load({
|
|
|
933
1285
|
const retry = setInterval(() => {
|
|
934
1286
|
tryPlace();
|
|
935
1287
|
}, 2e3);
|
|
1288
|
+
const syncStats = wireStats(entry, controller);
|
|
936
1289
|
const syncActive = () => {
|
|
937
1290
|
if (controller.getSnapshot().boardOpen) entry.dataset.active = "true";
|
|
938
1291
|
else delete entry.dataset.active;
|
|
1292
|
+
syncStats();
|
|
939
1293
|
};
|
|
940
1294
|
const unsubscribe = controller.subscribe(syncActive);
|
|
941
1295
|
syncActive();
|
|
@@ -949,6 +1303,17 @@ window.__ModuleLoader__.load({
|
|
|
949
1303
|
};
|
|
950
1304
|
}
|
|
951
1305
|
|
|
1306
|
+
//#endregion
|
|
1307
|
+
//#region src/shared/version.ts
|
|
1308
|
+
/**
|
|
1309
|
+
* The plugin package version shown in the board UI. Kept in sync with
|
|
1310
|
+
* package.json by a regression test (tests lock drift).
|
|
1311
|
+
*
|
|
1312
|
+
* @module dsh-taskboard/shared/version
|
|
1313
|
+
*/
|
|
1314
|
+
/** The package version (must equal package.json "version"). */
|
|
1315
|
+
const PLUGIN_VERSION = "0.2.0";
|
|
1316
|
+
|
|
952
1317
|
//#endregion
|
|
953
1318
|
//#region src/client/board/TaskCard.tsx
|
|
954
1319
|
const URGENCY_LABEL$1 = {
|
|
@@ -968,16 +1333,27 @@ window.__ModuleLoader__.load({
|
|
|
968
1333
|
* The card view.
|
|
969
1334
|
* @param task - the task record.
|
|
970
1335
|
* @param controller - the controller.
|
|
971
|
-
* @param draggable - enable dragging
|
|
1336
|
+
* @param draggable - enable dragging.
|
|
1337
|
+
* @param now - current epoch ms (stale-claim highlight).
|
|
1338
|
+
* @param onAlert - show an alert message (replaces native alert).
|
|
972
1339
|
*/
|
|
973
|
-
function TaskCard({ task, controller, draggable = false }) {
|
|
1340
|
+
function TaskCard({ task, controller, draggable = false, now, onAlert }) {
|
|
974
1341
|
const last = task.executions.length > 0 ? task.executions[task.executions.length - 1] : void 0;
|
|
1342
|
+
const running = task.executions.find((ex) => ex.outcome === "running");
|
|
1343
|
+
const stale = now !== void 0 && isStaleClaim(task, now);
|
|
975
1344
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
976
1345
|
type: "button",
|
|
977
1346
|
className: "dsh-atb-card",
|
|
978
1347
|
"data-urgency": task.urgency,
|
|
979
1348
|
draggable,
|
|
980
1349
|
onDragStart: (e) => {
|
|
1350
|
+
if (running !== void 0) {
|
|
1351
|
+
e.preventDefault();
|
|
1352
|
+
const msg = `该任务正在由【${task.title}】会话执行,不能拖动`;
|
|
1353
|
+
if (onAlert !== void 0) onAlert(msg);
|
|
1354
|
+
else alert(msg);
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
981
1357
|
e.dataTransfer.setData(DRAG_TYPE, task.id);
|
|
982
1358
|
e.dataTransfer.effectAllowed = "move";
|
|
983
1359
|
e.currentTarget.dataset.dragging = "true";
|
|
@@ -1001,6 +1377,11 @@ window.__ModuleLoader__.load({
|
|
|
1001
1377
|
"data-kind": "blocked",
|
|
1002
1378
|
children: "受阻"
|
|
1003
1379
|
}),
|
|
1380
|
+
stale && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1381
|
+
className: "dsh-atb-badge",
|
|
1382
|
+
"data-kind": "stale",
|
|
1383
|
+
children: "⏱ 认领超时"
|
|
1384
|
+
}),
|
|
1004
1385
|
task.execution.mode === "scheduled" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1005
1386
|
className: "dsh-atb-badge",
|
|
1006
1387
|
"data-kind": "scheduled",
|
|
@@ -1035,6 +1416,62 @@ window.__ModuleLoader__.load({
|
|
|
1035
1416
|
});
|
|
1036
1417
|
}
|
|
1037
1418
|
|
|
1419
|
+
//#endregion
|
|
1420
|
+
//#region src/client/board/AlertModal.tsx
|
|
1421
|
+
/**
|
|
1422
|
+
* A lightweight alert modal — replaces native alert() with a themed overlay
|
|
1423
|
+
* that matches the shell design tokens.
|
|
1424
|
+
*
|
|
1425
|
+
* @module dsh-taskboard/client/board/AlertModal
|
|
1426
|
+
*/
|
|
1427
|
+
/** Show a non-blocking alert modal. Returns true when opened. */
|
|
1428
|
+
function useAlert() {
|
|
1429
|
+
const [msg, setMsg] = (0, react.useState)(null);
|
|
1430
|
+
const show = (m) => setMsg(m);
|
|
1431
|
+
const close = () => setMsg(null);
|
|
1432
|
+
return {
|
|
1433
|
+
alert: show,
|
|
1434
|
+
el: msg !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AlertModal, {
|
|
1435
|
+
message: msg,
|
|
1436
|
+
onClose: close
|
|
1437
|
+
}) : null
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
function AlertModal({ message, onClose }) {
|
|
1441
|
+
(0, react.useEffect)(() => {
|
|
1442
|
+
const handler = (e) => {
|
|
1443
|
+
if (e.key === "Escape") onClose();
|
|
1444
|
+
};
|
|
1445
|
+
window.addEventListener("keydown", handler);
|
|
1446
|
+
return () => window.removeEventListener("keydown", handler);
|
|
1447
|
+
}, [onClose]);
|
|
1448
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1449
|
+
className: "dsh-atb-alert-backdrop",
|
|
1450
|
+
onClick: onClose,
|
|
1451
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1452
|
+
className: "dsh-atb-alert",
|
|
1453
|
+
onClick: (e) => e.stopPropagation(),
|
|
1454
|
+
children: [
|
|
1455
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1456
|
+
className: "dsh-atb-alert-icon",
|
|
1457
|
+
children: "⛔"
|
|
1458
|
+
}),
|
|
1459
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1460
|
+
className: "dsh-atb-alert-msg",
|
|
1461
|
+
children: message
|
|
1462
|
+
}),
|
|
1463
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1464
|
+
type: "button",
|
|
1465
|
+
className: "dsh-atb-btn",
|
|
1466
|
+
"data-primary": "true",
|
|
1467
|
+
onClick: onClose,
|
|
1468
|
+
children: "知道了"
|
|
1469
|
+
})
|
|
1470
|
+
]
|
|
1471
|
+
})
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1038
1475
|
//#endregion
|
|
1039
1476
|
//#region src/client/board/TaskDetail.tsx
|
|
1040
1477
|
/**
|
|
@@ -1079,10 +1516,10 @@ window.__ModuleLoader__.load({
|
|
|
1079
1516
|
failed: "失败",
|
|
1080
1517
|
cancelled: "已取消"
|
|
1081
1518
|
};
|
|
1082
|
-
/** Compact session-id display. */
|
|
1519
|
+
/** Compact session-id display (execution sessions carry the taskboard infix). */
|
|
1083
1520
|
function shortId(id) {
|
|
1084
1521
|
if (id === void 0) return "";
|
|
1085
|
-
return id.replace(/^session
|
|
1522
|
+
return id.replace(/^session-(taskboard-)?/, "").slice(0, 8);
|
|
1086
1523
|
}
|
|
1087
1524
|
/** Execution duration between start and end. */
|
|
1088
1525
|
function duration(startedAt, endedAt) {
|
|
@@ -1107,13 +1544,27 @@ window.__ModuleLoader__.load({
|
|
|
1107
1544
|
* The detail view.
|
|
1108
1545
|
* @param task - the task record.
|
|
1109
1546
|
* @param controller - the controller.
|
|
1547
|
+
* @param now - current epoch ms (stale-claim highlight).
|
|
1110
1548
|
*/
|
|
1111
|
-
function TaskDetail({ task, controller }) {
|
|
1549
|
+
function TaskDetail({ task, controller, now }) {
|
|
1112
1550
|
const [comment, setComment] = (0, react.useState)("");
|
|
1113
1551
|
const [confirmDone, setConfirmDone] = (0, react.useState)(false);
|
|
1114
1552
|
const [confirmPurge, setConfirmPurge] = (0, react.useState)(false);
|
|
1553
|
+
const [confirmCancel, setConfirmCancel] = (0, react.useState)(false);
|
|
1554
|
+
const { alert: showAlert, el: alertEl } = useAlert();
|
|
1115
1555
|
const ws = controller.getSnapshot().workspaces.find((w) => w.id === task.workspaceId);
|
|
1116
1556
|
const canRun = task.status !== "in_progress" && task.status !== "done" && task.status !== "archived";
|
|
1557
|
+
const runningExecution = task.executions.find((e) => e.outcome === "running");
|
|
1558
|
+
const holder = task.status === "in_progress" ? task.claimedBy : void 0;
|
|
1559
|
+
const stale = now !== void 0 && isStaleClaim(task, now);
|
|
1560
|
+
/** Jump to an execution's session; prompt precisely when it cannot open. */
|
|
1561
|
+
const jumpToSession = (sessionId) => {
|
|
1562
|
+
controller.openSession(sessionId).then((result) => {
|
|
1563
|
+
if (result === "missing") showAlert(`该会话已被删除(${shortId(sessionId)}),无法打开`);
|
|
1564
|
+
else if (result === "archived") showAlert(`该会话已归档(${shortId(sessionId)}),已从会话列表隐藏`);
|
|
1565
|
+
else if (result === "unavailable") showAlert(`会话导航不可用,会话 ID:${sessionId}`);
|
|
1566
|
+
});
|
|
1567
|
+
};
|
|
1117
1568
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1118
1569
|
className: "dsh-atb-detail",
|
|
1119
1570
|
"data-urgency": task.urgency,
|
|
@@ -1159,6 +1610,15 @@ window.__ModuleLoader__.load({
|
|
|
1159
1610
|
tone: "urgent",
|
|
1160
1611
|
children: "受阻"
|
|
1161
1612
|
}),
|
|
1613
|
+
holder !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Chip, {
|
|
1614
|
+
icon: stale ? "⏱" : "🔑",
|
|
1615
|
+
tone: stale ? "urgent" : void 0,
|
|
1616
|
+
children: [
|
|
1617
|
+
stale ? "认领超时 · " : "由 ",
|
|
1618
|
+
shortId(holder),
|
|
1619
|
+
" 持有"
|
|
1620
|
+
]
|
|
1621
|
+
}),
|
|
1162
1622
|
task.trashedAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chip, {
|
|
1163
1623
|
icon: "🗑",
|
|
1164
1624
|
tone: "urgent",
|
|
@@ -1179,18 +1639,67 @@ window.__ModuleLoader__.load({
|
|
|
1179
1639
|
]
|
|
1180
1640
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1181
1641
|
className: "dsh-atb-detail-topbtns",
|
|
1182
|
-
children: [
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1642
|
+
children: [
|
|
1643
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1644
|
+
type: "button",
|
|
1645
|
+
className: "dsh-atb-detail-edit",
|
|
1646
|
+
onClick: () => controller.openEditor(task.id),
|
|
1647
|
+
children: "✎ 编辑"
|
|
1648
|
+
}),
|
|
1649
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1650
|
+
type: "button",
|
|
1651
|
+
className: "dsh-atb-detail-edit",
|
|
1652
|
+
title: "复制此任务的全部配置为一张新卡(待办列)",
|
|
1653
|
+
onClick: () => void controller.duplicate(task),
|
|
1654
|
+
children: "⧉ 复制"
|
|
1655
|
+
}),
|
|
1656
|
+
canRun && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1657
|
+
type: "button",
|
|
1658
|
+
className: "dsh-atb-detail-run",
|
|
1659
|
+
title: task.model !== void 0 ? `新会话执行(${task.model.model})` : "新会话执行(默认模型)",
|
|
1660
|
+
onClick: () => void controller.run(task.id),
|
|
1661
|
+
children: "▶ 立即执行"
|
|
1662
|
+
}),
|
|
1663
|
+
runningExecution !== void 0 && (confirmCancel ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1664
|
+
className: "dsh-atb-confirm",
|
|
1665
|
+
children: [
|
|
1666
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1667
|
+
className: "dsh-atb-confirm-label",
|
|
1668
|
+
children: "停止该执行会话?"
|
|
1669
|
+
}),
|
|
1670
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1671
|
+
type: "button",
|
|
1672
|
+
className: "dsh-atb-btn",
|
|
1673
|
+
"data-danger": "true",
|
|
1674
|
+
onClick: () => {
|
|
1675
|
+
controller.cancel(task.id);
|
|
1676
|
+
setConfirmCancel(false);
|
|
1677
|
+
},
|
|
1678
|
+
children: "停止"
|
|
1679
|
+
}),
|
|
1680
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1681
|
+
type: "button",
|
|
1682
|
+
className: "dsh-atb-btn",
|
|
1683
|
+
onClick: () => setConfirmCancel(false),
|
|
1684
|
+
children: "取消"
|
|
1685
|
+
})
|
|
1686
|
+
]
|
|
1687
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1688
|
+
type: "button",
|
|
1689
|
+
className: "dsh-atb-detail-run",
|
|
1690
|
+
"data-danger": "true",
|
|
1691
|
+
title: `停止执行会话 ${runningExecution.sessionId ?? ""}(任务回到待办)`,
|
|
1692
|
+
onClick: () => setConfirmCancel(true),
|
|
1693
|
+
children: "■ 停止执行"
|
|
1694
|
+
})),
|
|
1695
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1696
|
+
type: "button",
|
|
1697
|
+
className: "dsh-atb-detail-close",
|
|
1698
|
+
"aria-label": "关闭",
|
|
1699
|
+
onClick: () => controller.select(void 0),
|
|
1700
|
+
children: "✕"
|
|
1701
|
+
})
|
|
1702
|
+
]
|
|
1194
1703
|
})]
|
|
1195
1704
|
}),
|
|
1196
1705
|
task.description.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -1214,59 +1723,65 @@ window.__ModuleLoader__.load({
|
|
|
1214
1723
|
children: task.prompt
|
|
1215
1724
|
})]
|
|
1216
1725
|
}),
|
|
1217
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1726
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1218
1727
|
className: "dsh-atb-detail-actions",
|
|
1219
|
-
children:
|
|
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", {
|
|
1728
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1225
1729
|
className: "dsh-atb-movebtns",
|
|
1226
|
-
children: [
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1730
|
+
children: [
|
|
1731
|
+
moveTargets(task).map((to) => to === "done" ? confirmDone ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1732
|
+
className: "dsh-atb-confirm",
|
|
1733
|
+
children: [
|
|
1734
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1735
|
+
className: "dsh-atb-confirm-label",
|
|
1736
|
+
children: "确认完成?"
|
|
1737
|
+
}),
|
|
1738
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1739
|
+
type: "button",
|
|
1740
|
+
className: "dsh-atb-btn",
|
|
1741
|
+
"data-primary": "true",
|
|
1742
|
+
onClick: () => {
|
|
1743
|
+
controller.move(task.id, task.version, "done");
|
|
1744
|
+
setConfirmDone(false);
|
|
1745
|
+
},
|
|
1746
|
+
children: "确认"
|
|
1747
|
+
}),
|
|
1748
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1749
|
+
type: "button",
|
|
1750
|
+
className: "dsh-atb-btn",
|
|
1751
|
+
onClick: () => setConfirmDone(false),
|
|
1752
|
+
children: "取消"
|
|
1753
|
+
})
|
|
1754
|
+
]
|
|
1755
|
+
}, to) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1756
|
+
type: "button",
|
|
1757
|
+
className: "dsh-atb-movebtn",
|
|
1758
|
+
"data-to": to,
|
|
1759
|
+
onClick: () => setConfirmDone(true),
|
|
1760
|
+
children: ["移至→", MOVE_LABEL[to]]
|
|
1761
|
+
}, to) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1762
|
+
type: "button",
|
|
1763
|
+
className: "dsh-atb-movebtn",
|
|
1764
|
+
"data-to": to,
|
|
1765
|
+
onClick: () => void controller.move(task.id, task.version, to),
|
|
1766
|
+
children: ["移至→", MOVE_LABEL[to]]
|
|
1767
|
+
}, to)),
|
|
1768
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1769
|
+
type: "button",
|
|
1770
|
+
className: "dsh-atb-movebtn",
|
|
1771
|
+
"data-to": "blocked",
|
|
1772
|
+
onClick: () => void controller.toggleBlocked(task),
|
|
1773
|
+
children: task.blocked ? "✓ 解除受阻" : "⛔ 标记受阻"
|
|
1774
|
+
}),
|
|
1775
|
+
holder !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1776
|
+
type: "button",
|
|
1777
|
+
className: "dsh-atb-movebtn",
|
|
1778
|
+
"data-to": "release",
|
|
1779
|
+
title: `释放 ${holder} 的认领:任务回到待办(持有会话可能仍在工作,确认它已停止后再释放)`,
|
|
1780
|
+
onClick: () => void controller.move(task.id, task.version, "todo"),
|
|
1781
|
+
children: "🔓 释放认领"
|
|
1782
|
+
})
|
|
1783
|
+
]
|
|
1784
|
+
})
|
|
1270
1785
|
}),
|
|
1271
1786
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1272
1787
|
className: "dsh-atb-section",
|
|
@@ -1326,12 +1841,24 @@ window.__ModuleLoader__.load({
|
|
|
1326
1841
|
}),
|
|
1327
1842
|
task.executions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1328
1843
|
className: "dsh-atb-section",
|
|
1329
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h4", { children: [
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1844
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h4", { children: [
|
|
1845
|
+
"执行记录",
|
|
1846
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1847
|
+
className: "dsh-atb-count2",
|
|
1848
|
+
children: task.executions.length
|
|
1849
|
+
}),
|
|
1850
|
+
task.executionsPruned !== void 0 && task.executionsPruned > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1851
|
+
className: "dsh-atb-count2",
|
|
1852
|
+
title: `更早的 ${task.executionsPruned} 条执行记录已按保留上限清理`,
|
|
1853
|
+
children: [
|
|
1854
|
+
"+",
|
|
1855
|
+
task.executionsPruned,
|
|
1856
|
+
" 已清理"
|
|
1857
|
+
]
|
|
1858
|
+
})
|
|
1859
|
+
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1333
1860
|
className: "dsh-atb-execlist",
|
|
1334
|
-
children: task.executions.map((e) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1861
|
+
children: [...task.executions].reverse().map((e) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1335
1862
|
className: "dsh-atb-exec-row",
|
|
1336
1863
|
children: [
|
|
1337
1864
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -1351,10 +1878,16 @@ window.__ModuleLoader__.load({
|
|
|
1351
1878
|
className: "dsh-atb-exec-time",
|
|
1352
1879
|
children: [fmtTime(e.startedAt), e.endedAt !== void 0 && ` · ${duration(e.startedAt, e.endedAt)}`]
|
|
1353
1880
|
}),
|
|
1354
|
-
e.sessionId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
1881
|
+
e.sessionId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1882
|
+
type: "button",
|
|
1355
1883
|
className: "dsh-atb-exec-session",
|
|
1356
|
-
title: e.sessionId
|
|
1357
|
-
|
|
1884
|
+
title: `点击打开该执行会话:${e.sessionId}`,
|
|
1885
|
+
onClick: () => jumpToSession(e.sessionId),
|
|
1886
|
+
children: [
|
|
1887
|
+
"🤖 ",
|
|
1888
|
+
shortId(e.sessionId),
|
|
1889
|
+
" ↗"
|
|
1890
|
+
]
|
|
1358
1891
|
}),
|
|
1359
1892
|
e.error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1360
1893
|
className: "dsh-atb-exec-error",
|
|
@@ -1404,7 +1937,8 @@ window.__ModuleLoader__.load({
|
|
|
1404
1937
|
onClick: () => setConfirmPurge(true),
|
|
1405
1938
|
children: "🔥 物理清除(需确认)"
|
|
1406
1939
|
})
|
|
1407
|
-
})
|
|
1940
|
+
}),
|
|
1941
|
+
alertEl
|
|
1408
1942
|
]
|
|
1409
1943
|
});
|
|
1410
1944
|
}
|
|
@@ -1509,6 +2043,7 @@ window.__ModuleLoader__.load({
|
|
|
1509
2043
|
const nextRun = cronMatch !== null ? nextCronTime(cronMatch, Date.now()) : null;
|
|
1510
2044
|
const cronBad = mode === "scheduled" && (cronMatch === null || nextRun === null);
|
|
1511
2045
|
const valid = title.trim().length > 0 && workspaceId !== "" && !cronBad;
|
|
2046
|
+
const runBlocked = editing && task.status === "in_progress";
|
|
1512
2047
|
const submit = () => {
|
|
1513
2048
|
if (!valid) return;
|
|
1514
2049
|
const picked = model !== "" ? JSON.parse(model) : void 0;
|
|
@@ -1537,6 +2072,40 @@ window.__ModuleLoader__.load({
|
|
|
1537
2072
|
model: picked
|
|
1538
2073
|
});
|
|
1539
2074
|
};
|
|
2075
|
+
/** Save the form, then immediately trigger a manual run of the task. */
|
|
2076
|
+
const submitAndRun = () => {
|
|
2077
|
+
if (!valid || runBlocked) return;
|
|
2078
|
+
const picked = model !== "" ? JSON.parse(model) : void 0;
|
|
2079
|
+
if (editing) (async () => {
|
|
2080
|
+
if (await controller.update(task.id, task.version, {
|
|
2081
|
+
title,
|
|
2082
|
+
description,
|
|
2083
|
+
prompt,
|
|
2084
|
+
urgency,
|
|
2085
|
+
workspaceId,
|
|
2086
|
+
execution: mode === "scheduled" ? {
|
|
2087
|
+
mode,
|
|
2088
|
+
cron: cron.trim()
|
|
2089
|
+
} : { mode },
|
|
2090
|
+
model: picked ?? null
|
|
2091
|
+
})) await controller.run(task.id);
|
|
2092
|
+
})();
|
|
2093
|
+
else (async () => {
|
|
2094
|
+
const id = await controller.create({
|
|
2095
|
+
title,
|
|
2096
|
+
workspaceId,
|
|
2097
|
+
urgency,
|
|
2098
|
+
description: description.length > 0 ? description : void 0,
|
|
2099
|
+
prompt: prompt.length > 0 ? prompt : void 0,
|
|
2100
|
+
execution: mode === "scheduled" ? {
|
|
2101
|
+
mode,
|
|
2102
|
+
cron: cron.trim()
|
|
2103
|
+
} : { mode },
|
|
2104
|
+
model: picked
|
|
2105
|
+
});
|
|
2106
|
+
if (id !== void 0) await controller.run(id);
|
|
2107
|
+
})();
|
|
2108
|
+
};
|
|
1540
2109
|
const hint = !valid ? title.trim().length === 0 ? "请填写标题" : workspaceId === "" ? "请选择项目" : "Cron 表达式无效(分 时 日 月 周)" : mode === "scheduled" && nextRun !== null ? `下次运行 ${fmtTime(nextRun)}` : editing ? `保存后版本 v${task.version} → v${task.version + 1}` : "创建后项目内会话可认领执行";
|
|
1541
2110
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1542
2111
|
className: "dsh-atb-modal-backdrop",
|
|
@@ -1658,7 +2227,7 @@ window.__ModuleLoader__.load({
|
|
|
1658
2227
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
1659
2228
|
value: prompt,
|
|
1660
2229
|
onChange: (e) => setPrompt(e.target.value),
|
|
1661
|
-
placeholder: "
|
|
2230
|
+
placeholder: "发给执行会话的完整指令。支持模板变量:{{lastExecution}}(上次执行结果)、{{lastComments}}(最近 3 条评论)"
|
|
1662
2231
|
})
|
|
1663
2232
|
}),
|
|
1664
2233
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
@@ -1727,19 +2296,30 @@ window.__ModuleLoader__.load({
|
|
|
1727
2296
|
children: hint
|
|
1728
2297
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1729
2298
|
className: "dsh-atb-modal-footbtns",
|
|
1730
|
-
children: [
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
2299
|
+
children: [
|
|
2300
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2301
|
+
type: "button",
|
|
2302
|
+
className: "dsh-atb-btn",
|
|
2303
|
+
onClick: () => controller.closeForm(),
|
|
2304
|
+
children: "取消"
|
|
2305
|
+
}),
|
|
2306
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2307
|
+
type: "button",
|
|
2308
|
+
className: "dsh-atb-btn",
|
|
2309
|
+
disabled: !valid || runBlocked,
|
|
2310
|
+
title: runBlocked ? "任务正在执行中,不能重复发起" : "保存后立即发起执行(新会话)",
|
|
2311
|
+
onClick: submitAndRun,
|
|
2312
|
+
children: "⚡ 立即执行"
|
|
2313
|
+
}),
|
|
2314
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2315
|
+
type: "button",
|
|
2316
|
+
className: "dsh-atb-btn",
|
|
2317
|
+
"data-primary": "true",
|
|
2318
|
+
disabled: !valid,
|
|
2319
|
+
onClick: submit,
|
|
2320
|
+
children: editing ? "保存修改" : "创建任务"
|
|
2321
|
+
})
|
|
2322
|
+
]
|
|
1743
2323
|
})]
|
|
1744
2324
|
})
|
|
1745
2325
|
]
|
|
@@ -1765,8 +2345,6 @@ window.__ModuleLoader__.load({
|
|
|
1765
2345
|
canceled: "已取消",
|
|
1766
2346
|
archived: "已归档"
|
|
1767
2347
|
};
|
|
1768
|
-
/** The two columns between which cards may be dragged both ways. */
|
|
1769
|
-
const DRAGGABLE_STATUSES = /* @__PURE__ */ new Set(["backlog", "todo"]);
|
|
1770
2348
|
/** Urgency chip labels. */
|
|
1771
2349
|
const URGENCY_LABELS = {
|
|
1772
2350
|
urgent: "紧急",
|
|
@@ -1780,9 +2358,28 @@ window.__ModuleLoader__.load({
|
|
|
1780
2358
|
const pad = (n) => String(n).padStart(2, "0");
|
|
1781
2359
|
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
1782
2360
|
}
|
|
1783
|
-
/**
|
|
2361
|
+
/** A claim idle for longer than this is highlighted as stale (ms). */
|
|
2362
|
+
const STALE_CLAIM_MS = 30 * 6e4;
|
|
2363
|
+
/** Whether the task's claim is stale (in_progress, held, idle too long). */
|
|
2364
|
+
function isStaleClaim(task, now) {
|
|
2365
|
+
return task.status === "in_progress" && task.claimedAt !== void 0 && now - task.claimedAt > 18e5;
|
|
2366
|
+
}
|
|
2367
|
+
/** Urgency sort rank (urgent first). */
|
|
2368
|
+
const URGENCY_RANK = {
|
|
2369
|
+
urgent: 0,
|
|
2370
|
+
normal: 1,
|
|
2371
|
+
relaxed: 2
|
|
2372
|
+
};
|
|
2373
|
+
/** Apply the active filters + search + sort to a task list. */
|
|
1784
2374
|
function filterTasks(state, tasks) {
|
|
1785
|
-
|
|
2375
|
+
const q = state.search.trim().toLowerCase();
|
|
2376
|
+
const filtered = tasks.filter((t) => (state.filters.workspaceId === void 0 || t.workspaceId === state.filters.workspaceId) && (state.filters.urgencies.length === 0 || state.filters.urgencies.includes(t.urgency)) && (q.length === 0 || t.title.toLowerCase().includes(q) || t.id.toLowerCase().includes(q)));
|
|
2377
|
+
if (state.sortBy === "default") return filtered;
|
|
2378
|
+
const sorted = [...filtered];
|
|
2379
|
+
if (state.sortBy === "updated") sorted.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
2380
|
+
else if (state.sortBy === "created") sorted.sort((a, b) => b.createdAt - a.createdAt);
|
|
2381
|
+
else if (state.sortBy === "urgency") sorted.sort((a, b) => URGENCY_RANK[a.urgency] - URGENCY_RANK[b.urgency] || b.updatedAt - a.updatedAt);
|
|
2382
|
+
return sorted;
|
|
1786
2383
|
}
|
|
1787
2384
|
/**
|
|
1788
2385
|
* The board view root.
|
|
@@ -1790,8 +2387,14 @@ window.__ModuleLoader__.load({
|
|
|
1790
2387
|
*/
|
|
1791
2388
|
function TaskBoard({ controller }) {
|
|
1792
2389
|
const state = (0, react.useSyncExternalStore)((cb) => controller.subscribe(cb), () => controller.getSnapshot());
|
|
2390
|
+
const [now, setNow] = (0, react.useState)(() => Date.now());
|
|
2391
|
+
(0, react.useEffect)(() => {
|
|
2392
|
+
const timer = setInterval(() => setNow(Date.now()), 6e4);
|
|
2393
|
+
return () => clearInterval(timer);
|
|
2394
|
+
}, []);
|
|
1793
2395
|
const live = filterTasks(state, state.ledger.tasks.filter((t) => t.trashedAt === void 0));
|
|
1794
2396
|
const selected = state.selectedId === void 0 ? void 0 : state.ledger.tasks.find((t) => t.id === state.selectedId);
|
|
2397
|
+
const { alert: showAlert, el: alertEl } = useAlert();
|
|
1795
2398
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1796
2399
|
className: "dsh-atb-board",
|
|
1797
2400
|
children: [
|
|
@@ -1810,7 +2413,21 @@ window.__ModuleLoader__.load({
|
|
|
1810
2413
|
state.ledger.revision
|
|
1811
2414
|
]
|
|
1812
2415
|
}),
|
|
2416
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2417
|
+
type: "button",
|
|
2418
|
+
className: "dsh-atb-btn",
|
|
2419
|
+
"data-primary": "true",
|
|
2420
|
+
onClick: () => controller.setComposer(true),
|
|
2421
|
+
children: "+ 新建任务"
|
|
2422
|
+
}),
|
|
1813
2423
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "dsh-atb-spacer" }),
|
|
2424
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2425
|
+
className: "dsh-atb-input dsh-atb-search",
|
|
2426
|
+
value: state.search,
|
|
2427
|
+
placeholder: "搜索标题 / ID…",
|
|
2428
|
+
spellCheck: false,
|
|
2429
|
+
onChange: (e) => controller.setSearch(e.target.value)
|
|
2430
|
+
}),
|
|
1814
2431
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1815
2432
|
className: "dsh-atb-select",
|
|
1816
2433
|
value: state.filters.workspaceId ?? "",
|
|
@@ -1823,6 +2440,30 @@ window.__ModuleLoader__.load({
|
|
|
1823
2440
|
children: ws.title || ws.path
|
|
1824
2441
|
}, ws.id))]
|
|
1825
2442
|
}),
|
|
2443
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
2444
|
+
className: "dsh-atb-select",
|
|
2445
|
+
value: state.sortBy,
|
|
2446
|
+
title: "列内排序",
|
|
2447
|
+
onChange: (e) => controller.setSortBy(e.target.value),
|
|
2448
|
+
children: [
|
|
2449
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2450
|
+
value: "default",
|
|
2451
|
+
children: "默认排序"
|
|
2452
|
+
}),
|
|
2453
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2454
|
+
value: "updated",
|
|
2455
|
+
children: "最近更新"
|
|
2456
|
+
}),
|
|
2457
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2458
|
+
value: "urgency",
|
|
2459
|
+
children: "按紧急度"
|
|
2460
|
+
}),
|
|
2461
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2462
|
+
value: "created",
|
|
2463
|
+
children: "创建时间"
|
|
2464
|
+
})
|
|
2465
|
+
]
|
|
2466
|
+
}),
|
|
1826
2467
|
[
|
|
1827
2468
|
"urgent",
|
|
1828
2469
|
"normal",
|
|
@@ -1847,9 +2488,23 @@ window.__ModuleLoader__.load({
|
|
|
1847
2488
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1848
2489
|
type: "button",
|
|
1849
2490
|
className: "dsh-atb-btn",
|
|
1850
|
-
|
|
1851
|
-
onClick: () => controller.
|
|
1852
|
-
children: "
|
|
2491
|
+
title: "下载完整台账备份(JSON)",
|
|
2492
|
+
onClick: () => controller.exportJson(),
|
|
2493
|
+
children: "⬇ JSON"
|
|
2494
|
+
}),
|
|
2495
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2496
|
+
type: "button",
|
|
2497
|
+
className: "dsh-atb-btn",
|
|
2498
|
+
title: "下载任务清单(CSV)",
|
|
2499
|
+
onClick: () => controller.exportCsv(),
|
|
2500
|
+
children: "⬇ CSV"
|
|
2501
|
+
}),
|
|
2502
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
|
|
2503
|
+
className: "dsh-atb-ver",
|
|
2504
|
+
href: "https://github.com/cloader/dsh-taskboard",
|
|
2505
|
+
target: "_blank",
|
|
2506
|
+
rel: "noopener noreferrer",
|
|
2507
|
+
children: ["V", PLUGIN_VERSION]
|
|
1853
2508
|
})
|
|
1854
2509
|
]
|
|
1855
2510
|
}),
|
|
@@ -1864,40 +2519,52 @@ window.__ModuleLoader__.load({
|
|
|
1864
2519
|
className: "dsh-atb-columns",
|
|
1865
2520
|
children: MAIN_STATUSES.map((status) => {
|
|
1866
2521
|
const columnTasks = live.filter((t) => t.status === status);
|
|
1867
|
-
const dropTarget = DRAGGABLE_STATUSES.has(status);
|
|
1868
2522
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1869
2523
|
className: "dsh-atb-column",
|
|
1870
|
-
onDragOver:
|
|
2524
|
+
onDragOver: (e) => {
|
|
1871
2525
|
if (e.dataTransfer.types.includes("application/x-dsh-atb-task")) {
|
|
1872
2526
|
e.preventDefault();
|
|
1873
2527
|
e.dataTransfer.dropEffect = "move";
|
|
1874
2528
|
e.currentTarget.dataset.dragover = "true";
|
|
1875
2529
|
}
|
|
1876
|
-
}
|
|
1877
|
-
onDragLeave:
|
|
2530
|
+
},
|
|
2531
|
+
onDragLeave: (e) => {
|
|
1878
2532
|
delete e.currentTarget.dataset.dragover;
|
|
1879
|
-
}
|
|
1880
|
-
onDrop:
|
|
2533
|
+
},
|
|
2534
|
+
onDrop: (e) => {
|
|
1881
2535
|
e.preventDefault();
|
|
1882
2536
|
delete e.currentTarget.dataset.dragover;
|
|
1883
2537
|
const id = e.dataTransfer.getData(DRAG_TYPE);
|
|
1884
2538
|
if (id.length === 0) return;
|
|
1885
2539
|
const task = state.ledger.tasks.find((t) => t.id === id);
|
|
1886
2540
|
if (task === void 0 || task.status === status) return;
|
|
2541
|
+
if (!canTransition(task.status, status)) {
|
|
2542
|
+
showAlert(`无法从「${COLUMN_LABELS[task.status]}」拖至「${COLUMN_LABELS[status]}」`);
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
1887
2545
|
controller.move(id, task.version, status);
|
|
1888
|
-
}
|
|
2546
|
+
},
|
|
1889
2547
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1890
2548
|
className: "dsh-atb-colhead",
|
|
1891
|
-
children: [
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
2549
|
+
children: [
|
|
2550
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2551
|
+
className: "dsh-atb-dot",
|
|
2552
|
+
"data-status": status
|
|
2553
|
+
}),
|
|
2554
|
+
COLUMN_LABELS[status],
|
|
2555
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2556
|
+
className: "dsh-atb-colcount",
|
|
2557
|
+
children: columnTasks.length
|
|
2558
|
+
})
|
|
2559
|
+
]
|
|
1895
2560
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1896
2561
|
className: "dsh-atb-cards",
|
|
1897
2562
|
children: [columnTasks.map((task) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskCard, {
|
|
1898
2563
|
task,
|
|
1899
2564
|
controller,
|
|
1900
|
-
draggable:
|
|
2565
|
+
draggable: true,
|
|
2566
|
+
now,
|
|
2567
|
+
onAlert: showAlert
|
|
1901
2568
|
}, task.id)), columnTasks.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1902
2569
|
className: "dsh-atb-empty",
|
|
1903
2570
|
children: "无任务"
|
|
@@ -1910,32 +2577,75 @@ window.__ModuleLoader__.load({
|
|
|
1910
2577
|
className: "dsh-atb-detailpanel",
|
|
1911
2578
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskDetail, {
|
|
1912
2579
|
task: selected,
|
|
1913
|
-
controller
|
|
2580
|
+
controller,
|
|
2581
|
+
now
|
|
1914
2582
|
})
|
|
1915
2583
|
}),
|
|
1916
2584
|
state.composerOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskFormModal, {
|
|
1917
2585
|
controller,
|
|
1918
2586
|
task: state.editingId === void 0 ? void 0 : state.ledger.tasks.find((t) => t.id === state.editingId)
|
|
1919
|
-
})
|
|
2587
|
+
}),
|
|
2588
|
+
alertEl
|
|
1920
2589
|
]
|
|
1921
2590
|
});
|
|
1922
2591
|
}
|
|
1923
|
-
/** Secondary tab: canceled/archived/trashed
|
|
2592
|
+
/** Secondary tab: tasks grouped into canceled / archived / trashed columns. */
|
|
1924
2593
|
function SecondaryTab({ controller, tasks }) {
|
|
1925
|
-
const
|
|
1926
|
-
|
|
2594
|
+
const trashed = tasks.filter((t) => t.trashedAt !== void 0);
|
|
2595
|
+
const archived = tasks.filter((t) => t.trashedAt === void 0 && t.status === "archived");
|
|
2596
|
+
const canceled = tasks.filter((t) => t.trashedAt === void 0 && t.status === "canceled");
|
|
2597
|
+
const groups = [
|
|
2598
|
+
{
|
|
2599
|
+
label: "已取消",
|
|
2600
|
+
dot: "canceled",
|
|
2601
|
+
rows: canceled
|
|
2602
|
+
},
|
|
2603
|
+
{
|
|
2604
|
+
label: "已归档",
|
|
2605
|
+
dot: "archived",
|
|
2606
|
+
rows: archived
|
|
2607
|
+
},
|
|
2608
|
+
{
|
|
2609
|
+
label: "已删除",
|
|
2610
|
+
dot: "trashed",
|
|
2611
|
+
rows: trashed
|
|
2612
|
+
}
|
|
2613
|
+
];
|
|
2614
|
+
if (trashed.length + archived.length + canceled.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1927
2615
|
className: "dsh-atb-secondary",
|
|
1928
|
-
children:
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
2616
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2617
|
+
className: "dsh-atb-empty",
|
|
2618
|
+
children: "无已取消 / 已归档 / 已删除任务"
|
|
2619
|
+
})
|
|
2620
|
+
});
|
|
2621
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2622
|
+
className: "dsh-atb-columns",
|
|
2623
|
+
children: groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2624
|
+
className: "dsh-atb-column",
|
|
2625
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2626
|
+
className: "dsh-atb-colhead",
|
|
2627
|
+
children: [
|
|
2628
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2629
|
+
className: "dsh-atb-dot",
|
|
2630
|
+
"data-status": group.dot
|
|
2631
|
+
}),
|
|
2632
|
+
group.label,
|
|
2633
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2634
|
+
className: "dsh-atb-colcount",
|
|
2635
|
+
children: group.rows.length
|
|
2636
|
+
})
|
|
2637
|
+
]
|
|
2638
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2639
|
+
className: "dsh-atb-cards",
|
|
2640
|
+
children: [group.rows.map((task) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskCard, {
|
|
2641
|
+
task,
|
|
2642
|
+
controller
|
|
2643
|
+
}, task.id)), group.rows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2644
|
+
className: "dsh-atb-empty",
|
|
2645
|
+
children: "无任务"
|
|
2646
|
+
})]
|
|
2647
|
+
})]
|
|
2648
|
+
}, group.label))
|
|
1939
2649
|
});
|
|
1940
2650
|
}
|
|
1941
2651
|
|
|
@@ -2020,6 +2730,39 @@ window.__ModuleLoader__.load({
|
|
|
2020
2730
|
};
|
|
2021
2731
|
}
|
|
2022
2732
|
|
|
2733
|
+
//#endregion
|
|
2734
|
+
//#region src/client/session-jump.ts
|
|
2735
|
+
/**
|
|
2736
|
+
* Build the jump function the controller installs.
|
|
2737
|
+
* @param access - lazy service accessors, consulted on every jump.
|
|
2738
|
+
* @returns the jump function: `(sessionId) => Promise<SessionJumpResult>`.
|
|
2739
|
+
*/
|
|
2740
|
+
function createSessionJumper(access) {
|
|
2741
|
+
const lookup = (sessions, workspaces, sessionId) => {
|
|
2742
|
+
if (sessions.list.getSnapshot().byId[sessionId] === void 0) return "absent";
|
|
2743
|
+
return workspaces?.list.getSnapshot().archivedSessionIds.includes(sessionId) ?? false ? "archived" : "openable";
|
|
2744
|
+
};
|
|
2745
|
+
return async (sessionId) => {
|
|
2746
|
+
const sessions = access.getSessions();
|
|
2747
|
+
if (sessions === void 0) return "unavailable";
|
|
2748
|
+
try {
|
|
2749
|
+
let state = lookup(sessions, access.getWorkspaces(), sessionId);
|
|
2750
|
+
if (state === "absent") {
|
|
2751
|
+
try {
|
|
2752
|
+
await sessions.refresh();
|
|
2753
|
+
} catch {}
|
|
2754
|
+
state = lookup(sessions, access.getWorkspaces(), sessionId);
|
|
2755
|
+
}
|
|
2756
|
+
if (state === "archived") return "archived";
|
|
2757
|
+
if (state === "absent") return "missing";
|
|
2758
|
+
sessions.open(sessionId);
|
|
2759
|
+
return "opened";
|
|
2760
|
+
} catch {
|
|
2761
|
+
return "unavailable";
|
|
2762
|
+
}
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2023
2766
|
//#endregion
|
|
2024
2767
|
//#region src/client/index.ts
|
|
2025
2768
|
/**
|
|
@@ -2059,6 +2802,10 @@ window.__ModuleLoader__.load({
|
|
|
2059
2802
|
});
|
|
2060
2803
|
return out;
|
|
2061
2804
|
};
|
|
2805
|
+
controller.installSessionJumper(createSessionJumper({
|
|
2806
|
+
getSessions: () => ctx.get?.("sessions"),
|
|
2807
|
+
getWorkspaces: () => ctx.get?.("workspaces")
|
|
2808
|
+
}));
|
|
2062
2809
|
controller.start();
|
|
2063
2810
|
const disposers = [];
|
|
2064
2811
|
try {
|