dsh-taskboard 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -4
- package/lib/client.js +743 -142
- package/lib/host/execution.js +179 -35
- 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 +19 -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/TaskBoard.tsx +95 -13
- package/src/client/board/TaskCard.tsx +6 -3
- package/src/client/board/TaskDetail.tsx +81 -7
- package/src/client/board/TaskFormModal.tsx +1 -1
- package/src/client/controller.ts +163 -4
- 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 +56 -2
- package/src/host/execution.ts +223 -29
- 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 +27 -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,6 +402,32 @@ window.__ModuleLoader__.load({
|
|
|
349
402
|
toggleSecondary() {
|
|
350
403
|
this.setState({ secondaryOpen: !this.state.secondaryOpen });
|
|
351
404
|
}
|
|
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
|
+
}
|
|
352
431
|
/** Create a task (composer submit); returns the new task id, undefined on failure. */
|
|
353
432
|
async create(body) {
|
|
354
433
|
try {
|
|
@@ -425,6 +504,15 @@ window.__ModuleLoader__.load({
|
|
|
425
504
|
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
426
505
|
}
|
|
427
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
|
+
}
|
|
428
516
|
/** Soft-delete (agent parity) then optional purge. */
|
|
429
517
|
async remove(id, ifVersion, purge) {
|
|
430
518
|
try {
|
|
@@ -435,6 +523,93 @@ window.__ModuleLoader__.load({
|
|
|
435
523
|
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
436
524
|
}
|
|
437
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
|
+
}
|
|
438
613
|
};
|
|
439
614
|
|
|
440
615
|
//#endregion
|
|
@@ -450,7 +625,7 @@ window.__ModuleLoader__.load({
|
|
|
450
625
|
/** The stylesheet text. */
|
|
451
626
|
const STYLES = `
|
|
452
627
|
.dsh-atb-entry {
|
|
453
|
-
display: flex; align-items: center; gap: 8px;
|
|
628
|
+
display: flex; align-items: center; gap: 8px; position: relative;
|
|
454
629
|
width: calc(100% - 8px); margin: 2px 4px; padding: 6px 10px;
|
|
455
630
|
border: none; border-radius: 8px; background: transparent;
|
|
456
631
|
color: var(--dsw-text-secondary, inherit); font: inherit; font-size: 13px;
|
|
@@ -459,6 +634,35 @@ window.__ModuleLoader__.load({
|
|
|
459
634
|
.dsh-atb-entry:hover { background: var(--dsw-hover, rgba(128,128,128,.12)); color: var(--dsw-text-primary, inherit); }
|
|
460
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; }
|
|
461
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; }
|
|
462
666
|
|
|
463
667
|
html[data-dsh-atb-active] [data-pane="conversation"] > *:not([data-dsh-atb-view]) { display: none !important; }
|
|
464
668
|
.dsh-atb-view { display: none; }
|
|
@@ -468,6 +672,16 @@ window.__ModuleLoader__.load({
|
|
|
468
672
|
.dsh-atb-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
469
673
|
.dsh-atb-title { font-size: 15px; font-weight: 600; margin: 0; }
|
|
470
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; }
|
|
471
685
|
.dsh-atb-spacer { flex: 1; }
|
|
472
686
|
.dsh-atb-select, .dsh-atb-input {
|
|
473
687
|
font: inherit; font-size: 12.5px; padding: 5px 8px; border-radius: 7px;
|
|
@@ -488,6 +702,16 @@ window.__ModuleLoader__.load({
|
|
|
488
702
|
.dsh-atb-dot[data-urgency="urgent"] { background: #e5484d; }
|
|
489
703
|
.dsh-atb-dot[data-urgency="normal"] { background: #8e4ec6; }
|
|
490
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; }
|
|
491
715
|
|
|
492
716
|
.dsh-atb-btn {
|
|
493
717
|
font: inherit; font-size: 12.5px; padding: 5px 11px; border-radius: 7px; cursor: pointer;
|
|
@@ -618,6 +842,7 @@ window.__ModuleLoader__.load({
|
|
|
618
842
|
transition: filter .12s ease;
|
|
619
843
|
}
|
|
620
844
|
.dsh-atb-detail-run:hover { filter: brightness(1.1); }
|
|
845
|
+
.dsh-atb-detail-run[data-danger="true"] { background: rgba(229,72,77,.92); }
|
|
621
846
|
.dsh-atb-movebtns { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
622
847
|
.dsh-atb-movebtn {
|
|
623
848
|
font: inherit; font-size: 12px; padding: 4px 11px; border-radius: 999px; cursor: pointer;
|
|
@@ -696,7 +921,11 @@ window.__ModuleLoader__.load({
|
|
|
696
921
|
.dsh-atb-exec-outcome[data-outcome="running"] { background: rgba(217,130,43,.15); color: #d9822b; }
|
|
697
922
|
.dsh-atb-exec-outcome[data-outcome="cancelled"] { background: rgba(128,128,128,.15); color: var(--dsw-text-secondary, gray); }
|
|
698
923
|
.dsh-atb-exec-time { font-size: 11px; color: var(--dsw-text-secondary, gray); }
|
|
699
|
-
.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; }
|
|
700
929
|
.dsh-atb-exec-error { flex-basis: 100%; font-size: 11px; color: #e5484d; word-break: break-all; }
|
|
701
930
|
|
|
702
931
|
.dsh-atb-dangerzone {
|
|
@@ -884,12 +1113,108 @@ window.__ModuleLoader__.load({
|
|
|
884
1113
|
entry.dataset.dshAtbEntry = "";
|
|
885
1114
|
entry.className = "dsh-atb-entry";
|
|
886
1115
|
entry.setAttribute("aria-label", "Agent 任务看板");
|
|
887
|
-
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>`;
|
|
888
1117
|
entry.addEventListener("click", () => {
|
|
889
1118
|
controller.toggleBoard();
|
|
890
1119
|
});
|
|
891
1120
|
return entry;
|
|
892
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
|
+
}
|
|
893
1218
|
/** Re-insert the entry after the New Session row (before the browser region). */
|
|
894
1219
|
function placeEntry(root, entry) {
|
|
895
1220
|
const button = newSessionButton(root);
|
|
@@ -960,9 +1285,11 @@ window.__ModuleLoader__.load({
|
|
|
960
1285
|
const retry = setInterval(() => {
|
|
961
1286
|
tryPlace();
|
|
962
1287
|
}, 2e3);
|
|
1288
|
+
const syncStats = wireStats(entry, controller);
|
|
963
1289
|
const syncActive = () => {
|
|
964
1290
|
if (controller.getSnapshot().boardOpen) entry.dataset.active = "true";
|
|
965
1291
|
else delete entry.dataset.active;
|
|
1292
|
+
syncStats();
|
|
966
1293
|
};
|
|
967
1294
|
const unsubscribe = controller.subscribe(syncActive);
|
|
968
1295
|
syncActive();
|
|
@@ -976,6 +1303,17 @@ window.__ModuleLoader__.load({
|
|
|
976
1303
|
};
|
|
977
1304
|
}
|
|
978
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.1";
|
|
1316
|
+
|
|
979
1317
|
//#endregion
|
|
980
1318
|
//#region src/client/board/TaskCard.tsx
|
|
981
1319
|
const URGENCY_LABEL$1 = {
|
|
@@ -996,17 +1334,20 @@ window.__ModuleLoader__.load({
|
|
|
996
1334
|
* @param task - the task record.
|
|
997
1335
|
* @param controller - the controller.
|
|
998
1336
|
* @param draggable - enable dragging.
|
|
1337
|
+
* @param now - current epoch ms (stale-claim highlight).
|
|
999
1338
|
* @param onAlert - show an alert message (replaces native alert).
|
|
1000
1339
|
*/
|
|
1001
|
-
function TaskCard({ task, controller, draggable = false, onAlert }) {
|
|
1340
|
+
function TaskCard({ task, controller, draggable = false, now, onAlert }) {
|
|
1002
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);
|
|
1003
1344
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1004
1345
|
type: "button",
|
|
1005
1346
|
className: "dsh-atb-card",
|
|
1006
1347
|
"data-urgency": task.urgency,
|
|
1007
1348
|
draggable,
|
|
1008
1349
|
onDragStart: (e) => {
|
|
1009
|
-
if (
|
|
1350
|
+
if (running !== void 0) {
|
|
1010
1351
|
e.preventDefault();
|
|
1011
1352
|
const msg = `该任务正在由【${task.title}】会话执行,不能拖动`;
|
|
1012
1353
|
if (onAlert !== void 0) onAlert(msg);
|
|
@@ -1036,6 +1377,11 @@ window.__ModuleLoader__.load({
|
|
|
1036
1377
|
"data-kind": "blocked",
|
|
1037
1378
|
children: "受阻"
|
|
1038
1379
|
}),
|
|
1380
|
+
stale && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1381
|
+
className: "dsh-atb-badge",
|
|
1382
|
+
"data-kind": "stale",
|
|
1383
|
+
children: "⏱ 认领超时"
|
|
1384
|
+
}),
|
|
1039
1385
|
task.execution.mode === "scheduled" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1040
1386
|
className: "dsh-atb-badge",
|
|
1041
1387
|
"data-kind": "scheduled",
|
|
@@ -1070,6 +1416,62 @@ window.__ModuleLoader__.load({
|
|
|
1070
1416
|
});
|
|
1071
1417
|
}
|
|
1072
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
|
+
|
|
1073
1475
|
//#endregion
|
|
1074
1476
|
//#region src/client/board/TaskDetail.tsx
|
|
1075
1477
|
/**
|
|
@@ -1114,10 +1516,10 @@ window.__ModuleLoader__.load({
|
|
|
1114
1516
|
failed: "失败",
|
|
1115
1517
|
cancelled: "已取消"
|
|
1116
1518
|
};
|
|
1117
|
-
/** Compact session-id display. */
|
|
1519
|
+
/** Compact session-id display (execution sessions carry the taskboard infix). */
|
|
1118
1520
|
function shortId(id) {
|
|
1119
1521
|
if (id === void 0) return "";
|
|
1120
|
-
return id.replace(/^session
|
|
1522
|
+
return id.replace(/^session-(taskboard-)?/, "").slice(0, 8);
|
|
1121
1523
|
}
|
|
1122
1524
|
/** Execution duration between start and end. */
|
|
1123
1525
|
function duration(startedAt, endedAt) {
|
|
@@ -1142,13 +1544,27 @@ window.__ModuleLoader__.load({
|
|
|
1142
1544
|
* The detail view.
|
|
1143
1545
|
* @param task - the task record.
|
|
1144
1546
|
* @param controller - the controller.
|
|
1547
|
+
* @param now - current epoch ms (stale-claim highlight).
|
|
1145
1548
|
*/
|
|
1146
|
-
function TaskDetail({ task, controller }) {
|
|
1549
|
+
function TaskDetail({ task, controller, now }) {
|
|
1147
1550
|
const [comment, setComment] = (0, react.useState)("");
|
|
1148
1551
|
const [confirmDone, setConfirmDone] = (0, react.useState)(false);
|
|
1149
1552
|
const [confirmPurge, setConfirmPurge] = (0, react.useState)(false);
|
|
1553
|
+
const [confirmCancel, setConfirmCancel] = (0, react.useState)(false);
|
|
1554
|
+
const { alert: showAlert, el: alertEl } = useAlert();
|
|
1150
1555
|
const ws = controller.getSnapshot().workspaces.find((w) => w.id === task.workspaceId);
|
|
1151
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
|
+
};
|
|
1152
1568
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1153
1569
|
className: "dsh-atb-detail",
|
|
1154
1570
|
"data-urgency": task.urgency,
|
|
@@ -1194,6 +1610,15 @@ window.__ModuleLoader__.load({
|
|
|
1194
1610
|
tone: "urgent",
|
|
1195
1611
|
children: "受阻"
|
|
1196
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
|
+
}),
|
|
1197
1622
|
task.trashedAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chip, {
|
|
1198
1623
|
icon: "🗑",
|
|
1199
1624
|
tone: "urgent",
|
|
@@ -1221,6 +1646,13 @@ window.__ModuleLoader__.load({
|
|
|
1221
1646
|
onClick: () => controller.openEditor(task.id),
|
|
1222
1647
|
children: "✎ 编辑"
|
|
1223
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
|
+
}),
|
|
1224
1656
|
canRun && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1225
1657
|
type: "button",
|
|
1226
1658
|
className: "dsh-atb-detail-run",
|
|
@@ -1228,6 +1660,38 @@ window.__ModuleLoader__.load({
|
|
|
1228
1660
|
onClick: () => void controller.run(task.id),
|
|
1229
1661
|
children: "▶ 立即执行"
|
|
1230
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
|
+
})),
|
|
1231
1695
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1232
1696
|
type: "button",
|
|
1233
1697
|
className: "dsh-atb-detail-close",
|
|
@@ -1263,49 +1727,60 @@ window.__ModuleLoader__.load({
|
|
|
1263
1727
|
className: "dsh-atb-detail-actions",
|
|
1264
1728
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1265
1729
|
className: "dsh-atb-movebtns",
|
|
1266
|
-
children: [
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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
|
+
]
|
|
1309
1784
|
})
|
|
1310
1785
|
}),
|
|
1311
1786
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -1366,12 +1841,24 @@ window.__ModuleLoader__.load({
|
|
|
1366
1841
|
}),
|
|
1367
1842
|
task.executions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1368
1843
|
className: "dsh-atb-section",
|
|
1369
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h4", { children: [
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
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", {
|
|
1373
1860
|
className: "dsh-atb-execlist",
|
|
1374
|
-
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", {
|
|
1375
1862
|
className: "dsh-atb-exec-row",
|
|
1376
1863
|
children: [
|
|
1377
1864
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -1391,10 +1878,16 @@ window.__ModuleLoader__.load({
|
|
|
1391
1878
|
className: "dsh-atb-exec-time",
|
|
1392
1879
|
children: [fmtTime(e.startedAt), e.endedAt !== void 0 && ` · ${duration(e.startedAt, e.endedAt)}`]
|
|
1393
1880
|
}),
|
|
1394
|
-
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",
|
|
1395
1883
|
className: "dsh-atb-exec-session",
|
|
1396
|
-
title: e.sessionId
|
|
1397
|
-
|
|
1884
|
+
title: `点击打开该执行会话:${e.sessionId}`,
|
|
1885
|
+
onClick: () => jumpToSession(e.sessionId),
|
|
1886
|
+
children: [
|
|
1887
|
+
"🤖 ",
|
|
1888
|
+
shortId(e.sessionId),
|
|
1889
|
+
" ↗"
|
|
1890
|
+
]
|
|
1398
1891
|
}),
|
|
1399
1892
|
e.error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1400
1893
|
className: "dsh-atb-exec-error",
|
|
@@ -1444,7 +1937,8 @@ window.__ModuleLoader__.load({
|
|
|
1444
1937
|
onClick: () => setConfirmPurge(true),
|
|
1445
1938
|
children: "🔥 物理清除(需确认)"
|
|
1446
1939
|
})
|
|
1447
|
-
})
|
|
1940
|
+
}),
|
|
1941
|
+
alertEl
|
|
1448
1942
|
]
|
|
1449
1943
|
});
|
|
1450
1944
|
}
|
|
@@ -1733,7 +2227,7 @@ window.__ModuleLoader__.load({
|
|
|
1733
2227
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
1734
2228
|
value: prompt,
|
|
1735
2229
|
onChange: (e) => setPrompt(e.target.value),
|
|
1736
|
-
placeholder: "
|
|
2230
|
+
placeholder: "发给执行会话的完整指令。支持模板变量:{{lastExecution}}(上次执行结果)、{{lastComments}}(最近 3 条评论)"
|
|
1737
2231
|
})
|
|
1738
2232
|
}),
|
|
1739
2233
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
@@ -1833,62 +2327,6 @@ window.__ModuleLoader__.load({
|
|
|
1833
2327
|
});
|
|
1834
2328
|
}
|
|
1835
2329
|
|
|
1836
|
-
//#endregion
|
|
1837
|
-
//#region src/client/board/AlertModal.tsx
|
|
1838
|
-
/**
|
|
1839
|
-
* A lightweight alert modal — replaces native alert() with a themed overlay
|
|
1840
|
-
* that matches the shell design tokens.
|
|
1841
|
-
*
|
|
1842
|
-
* @module dsh-taskboard/client/board/AlertModal
|
|
1843
|
-
*/
|
|
1844
|
-
/** Show a non-blocking alert modal. Returns true when opened. */
|
|
1845
|
-
function useAlert() {
|
|
1846
|
-
const [msg, setMsg] = (0, react.useState)(null);
|
|
1847
|
-
const show = (m) => setMsg(m);
|
|
1848
|
-
const close = () => setMsg(null);
|
|
1849
|
-
return {
|
|
1850
|
-
alert: show,
|
|
1851
|
-
el: msg !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AlertModal, {
|
|
1852
|
-
message: msg,
|
|
1853
|
-
onClose: close
|
|
1854
|
-
}) : null
|
|
1855
|
-
};
|
|
1856
|
-
}
|
|
1857
|
-
function AlertModal({ message, onClose }) {
|
|
1858
|
-
(0, react.useEffect)(() => {
|
|
1859
|
-
const handler = (e) => {
|
|
1860
|
-
if (e.key === "Escape") onClose();
|
|
1861
|
-
};
|
|
1862
|
-
window.addEventListener("keydown", handler);
|
|
1863
|
-
return () => window.removeEventListener("keydown", handler);
|
|
1864
|
-
}, [onClose]);
|
|
1865
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1866
|
-
className: "dsh-atb-alert-backdrop",
|
|
1867
|
-
onClick: onClose,
|
|
1868
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1869
|
-
className: "dsh-atb-alert",
|
|
1870
|
-
onClick: (e) => e.stopPropagation(),
|
|
1871
|
-
children: [
|
|
1872
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1873
|
-
className: "dsh-atb-alert-icon",
|
|
1874
|
-
children: "⛔"
|
|
1875
|
-
}),
|
|
1876
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1877
|
-
className: "dsh-atb-alert-msg",
|
|
1878
|
-
children: message
|
|
1879
|
-
}),
|
|
1880
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1881
|
-
type: "button",
|
|
1882
|
-
className: "dsh-atb-btn",
|
|
1883
|
-
"data-primary": "true",
|
|
1884
|
-
onClick: onClose,
|
|
1885
|
-
children: "知道了"
|
|
1886
|
-
})
|
|
1887
|
-
]
|
|
1888
|
-
})
|
|
1889
|
-
});
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
2330
|
//#endregion
|
|
1893
2331
|
//#region src/client/board/TaskBoard.tsx
|
|
1894
2332
|
/**
|
|
@@ -1920,9 +2358,28 @@ window.__ModuleLoader__.load({
|
|
|
1920
2358
|
const pad = (n) => String(n).padStart(2, "0");
|
|
1921
2359
|
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
1922
2360
|
}
|
|
1923
|
-
/**
|
|
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. */
|
|
1924
2374
|
function filterTasks(state, tasks) {
|
|
1925
|
-
|
|
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;
|
|
1926
2383
|
}
|
|
1927
2384
|
/**
|
|
1928
2385
|
* The board view root.
|
|
@@ -1930,6 +2387,11 @@ window.__ModuleLoader__.load({
|
|
|
1930
2387
|
*/
|
|
1931
2388
|
function TaskBoard({ controller }) {
|
|
1932
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
|
+
}, []);
|
|
1933
2395
|
const live = filterTasks(state, state.ledger.tasks.filter((t) => t.trashedAt === void 0));
|
|
1934
2396
|
const selected = state.selectedId === void 0 ? void 0 : state.ledger.tasks.find((t) => t.id === state.selectedId);
|
|
1935
2397
|
const { alert: showAlert, el: alertEl } = useAlert();
|
|
@@ -1959,6 +2421,13 @@ window.__ModuleLoader__.load({
|
|
|
1959
2421
|
children: "+ 新建任务"
|
|
1960
2422
|
}),
|
|
1961
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
|
+
}),
|
|
1962
2431
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1963
2432
|
className: "dsh-atb-select",
|
|
1964
2433
|
value: state.filters.workspaceId ?? "",
|
|
@@ -1971,6 +2440,30 @@ window.__ModuleLoader__.load({
|
|
|
1971
2440
|
children: ws.title || ws.path
|
|
1972
2441
|
}, ws.id))]
|
|
1973
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
|
+
}),
|
|
1974
2467
|
[
|
|
1975
2468
|
"urgent",
|
|
1976
2469
|
"normal",
|
|
@@ -1991,6 +2484,27 @@ window.__ModuleLoader__.load({
|
|
|
1991
2484
|
className: "dsh-atb-btn",
|
|
1992
2485
|
onClick: () => controller.toggleSecondary(),
|
|
1993
2486
|
children: state.secondaryOpen ? "返回看板" : "其它任务"
|
|
2487
|
+
}),
|
|
2488
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2489
|
+
type: "button",
|
|
2490
|
+
className: "dsh-atb-btn",
|
|
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]
|
|
1994
2508
|
})
|
|
1995
2509
|
]
|
|
1996
2510
|
}),
|
|
@@ -2032,16 +2546,24 @@ window.__ModuleLoader__.load({
|
|
|
2032
2546
|
},
|
|
2033
2547
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2034
2548
|
className: "dsh-atb-colhead",
|
|
2035
|
-
children: [
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
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
|
+
]
|
|
2039
2560
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2040
2561
|
className: "dsh-atb-cards",
|
|
2041
2562
|
children: [columnTasks.map((task) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskCard, {
|
|
2042
2563
|
task,
|
|
2043
2564
|
controller,
|
|
2044
2565
|
draggable: true,
|
|
2566
|
+
now,
|
|
2045
2567
|
onAlert: showAlert
|
|
2046
2568
|
}, task.id)), columnTasks.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2047
2569
|
className: "dsh-atb-empty",
|
|
@@ -2055,7 +2577,8 @@ window.__ModuleLoader__.load({
|
|
|
2055
2577
|
className: "dsh-atb-detailpanel",
|
|
2056
2578
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskDetail, {
|
|
2057
2579
|
task: selected,
|
|
2058
|
-
controller
|
|
2580
|
+
controller,
|
|
2581
|
+
now
|
|
2059
2582
|
})
|
|
2060
2583
|
}),
|
|
2061
2584
|
state.composerOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskFormModal, {
|
|
@@ -2066,22 +2589,63 @@ window.__ModuleLoader__.load({
|
|
|
2066
2589
|
]
|
|
2067
2590
|
});
|
|
2068
2591
|
}
|
|
2069
|
-
/** Secondary tab: canceled/archived/trashed
|
|
2592
|
+
/** Secondary tab: tasks grouped into canceled / archived / trashed columns. */
|
|
2070
2593
|
function SecondaryTab({ controller, tasks }) {
|
|
2071
|
-
const
|
|
2072
|
-
|
|
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", {
|
|
2073
2615
|
className: "dsh-atb-secondary",
|
|
2074
|
-
children:
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
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))
|
|
2085
2649
|
});
|
|
2086
2650
|
}
|
|
2087
2651
|
|
|
@@ -2166,6 +2730,39 @@ window.__ModuleLoader__.load({
|
|
|
2166
2730
|
};
|
|
2167
2731
|
}
|
|
2168
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
|
+
|
|
2169
2766
|
//#endregion
|
|
2170
2767
|
//#region src/client/index.ts
|
|
2171
2768
|
/**
|
|
@@ -2205,6 +2802,10 @@ window.__ModuleLoader__.load({
|
|
|
2205
2802
|
});
|
|
2206
2803
|
return out;
|
|
2207
2804
|
};
|
|
2805
|
+
controller.installSessionJumper(createSessionJumper({
|
|
2806
|
+
getSessions: () => ctx.get?.("sessions"),
|
|
2807
|
+
getWorkspaces: () => ctx.get?.("workspaces")
|
|
2808
|
+
}));
|
|
2208
2809
|
controller.start();
|
|
2209
2810
|
const disposers = [];
|
|
2210
2811
|
try {
|