dsh-taskboard 0.2.2 → 0.4.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 +62 -6
- package/lib/client.js +1839 -74
- package/lib/host/execution.js +199 -54
- package/lib/host/execution.js.map +1 -1
- package/lib/host/git.js +327 -0
- package/lib/host/git.js.map +1 -0
- package/lib/host/protocol-text.js +5 -3
- package/lib/host/protocol-text.js.map +1 -1
- package/lib/host/routes.js +435 -5
- package/lib/host/routes.js.map +1 -1
- package/lib/host/store.js +12 -0
- package/lib/host/store.js.map +1 -1
- package/lib/host/templates.js +166 -0
- package/lib/host/templates.js.map +1 -0
- package/lib/host/tools.js +217 -3
- package/lib/host/tools.js.map +1 -1
- package/lib/index.js +23 -3
- package/lib/index.js.map +1 -1
- package/lib/shared/api.js.map +1 -1
- package/lib/shared/protocol.js +286 -1
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +74 -74
- package/src/client/api.ts +47 -3
- package/src/client/board/ImportModal.tsx +182 -0
- package/src/client/board/TaskBoard.tsx +118 -3
- package/src/client/board/TaskCard.tsx +9 -0
- package/src/client/board/TaskDetail.tsx +360 -4
- package/src/client/board/TaskFormModal.tsx +193 -12
- package/src/client/board/TemplateManager.tsx +121 -0
- package/src/client/controller.ts +238 -11
- package/src/client/index.ts +18 -1
- package/src/client/styles.ts +198 -0
- package/src/host/execution.ts +301 -67
- package/src/host/git.ts +370 -0
- package/src/host/protocol-text.ts +5 -3
- package/src/host/routes.ts +483 -5
- package/src/host/store.ts +13 -0
- package/src/host/templates.ts +143 -0
- package/src/host/tools.ts +215 -2
- package/src/index.ts +30 -1
- package/src/shared/api.ts +89 -3
- package/src/shared/protocol.ts +408 -0
- package/src/shared/version.ts +1 -1
package/lib/client.js
CHANGED
|
@@ -37,8 +37,29 @@ window.__ModuleLoader__.load({
|
|
|
37
37
|
reject: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/reject`, body),
|
|
38
38
|
comment: (id, bodyText) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/comment`, { body: bodyText }),
|
|
39
39
|
remove: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/delete`, body),
|
|
40
|
-
run: (id) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/run`, {}),
|
|
40
|
+
run: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/run`, body ?? {}),
|
|
41
41
|
cancel: (id) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/cancel`, {}),
|
|
42
|
+
mergeBranch: (id) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/merge`, {}),
|
|
43
|
+
worktreeRemove: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/worktree-remove`, body),
|
|
44
|
+
diagnostics: () => unwrap(fetch("/dsh-taskboard/diagnostics")),
|
|
45
|
+
worktreeCleanup: (workspaceId, taskId) => post("/dsh-taskboard/worktree-cleanup", {
|
|
46
|
+
workspaceId,
|
|
47
|
+
taskId
|
|
48
|
+
}),
|
|
49
|
+
diff: (taskId, query) => {
|
|
50
|
+
const params = new URLSearchParams({ execution: query.execution });
|
|
51
|
+
if (query.commit !== void 0) params.set("commit", query.commit);
|
|
52
|
+
if (query.path !== void 0) params.set("path", query.path);
|
|
53
|
+
return unwrap(fetch(`/dsh-taskboard/tasks/${encodeURIComponent(taskId)}/diff?${params.toString()}`));
|
|
54
|
+
},
|
|
55
|
+
importPreview: (file) => post("/dsh-taskboard/import/preview", file),
|
|
56
|
+
importCommit: (mode, ledger) => post("/dsh-taskboard/import", {
|
|
57
|
+
mode,
|
|
58
|
+
ledger
|
|
59
|
+
}),
|
|
60
|
+
templates: () => unwrap(fetch("/dsh-taskboard/templates")),
|
|
61
|
+
templateUpsert: (body) => post("/dsh-taskboard/templates", body),
|
|
62
|
+
templateDelete: (id) => post("/dsh-taskboard/templates/delete", { id }),
|
|
42
63
|
stream(onChange, onGap) {
|
|
43
64
|
const es = new EventSource("/dsh-taskboard/events");
|
|
44
65
|
let revision;
|
|
@@ -204,11 +225,35 @@ window.__ModuleLoader__.load({
|
|
|
204
225
|
tasks: []
|
|
205
226
|
};
|
|
206
227
|
}
|
|
228
|
+
/** Checklist progress: how many items are checked (absent checklist → 0/0). */
|
|
229
|
+
function checklistProgress(task) {
|
|
230
|
+
const items = task.checklist ?? [];
|
|
231
|
+
return {
|
|
232
|
+
done: items.filter((i) => i.checked).length,
|
|
233
|
+
total: items.length
|
|
234
|
+
};
|
|
235
|
+
}
|
|
207
236
|
|
|
208
237
|
//#endregion
|
|
209
238
|
//#region src/client/controller.ts
|
|
210
239
|
/** localStorage key for persisted view state (filters + sort). */
|
|
211
240
|
const VIEW_KEY = "dsh-taskboard-view-v1";
|
|
241
|
+
/** localStorage key for the remembered isolation toggle choice (0.3.0). */
|
|
242
|
+
const ISOLATION_KEY = "dsh-taskboard-isolation-v1";
|
|
243
|
+
/** Load the remembered default isolation (worktree unless explicitly turned off). */
|
|
244
|
+
function loadDefaultIsolation() {
|
|
245
|
+
try {
|
|
246
|
+
return localStorage.getItem(ISOLATION_KEY) === "none" ? "none" : "worktree";
|
|
247
|
+
} catch {
|
|
248
|
+
return "worktree";
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/** Remember the isolation toggle choice across forms (best effort). */
|
|
252
|
+
function saveDefaultIsolation(mode) {
|
|
253
|
+
try {
|
|
254
|
+
localStorage.setItem(ISOLATION_KEY, mode);
|
|
255
|
+
} catch {}
|
|
256
|
+
}
|
|
212
257
|
/** Load the persisted view state (never throws; fresh on any parse error). */
|
|
213
258
|
function loadView() {
|
|
214
259
|
try {
|
|
@@ -245,7 +290,11 @@ window.__ModuleLoader__.load({
|
|
|
245
290
|
search: "",
|
|
246
291
|
sortBy: view.sortBy,
|
|
247
292
|
composerOpen: false,
|
|
248
|
-
secondaryOpen: false
|
|
293
|
+
secondaryOpen: false,
|
|
294
|
+
diagOpen: false,
|
|
295
|
+
templates: [],
|
|
296
|
+
tplManagerOpen: false,
|
|
297
|
+
importOpen: false
|
|
249
298
|
};
|
|
250
299
|
}
|
|
251
300
|
/**
|
|
@@ -378,31 +427,47 @@ window.__ModuleLoader__.load({
|
|
|
378
427
|
select(id) {
|
|
379
428
|
this.setState({ selectedId: id });
|
|
380
429
|
}
|
|
381
|
-
/** Show/hide the task form (create mode when opening). */
|
|
430
|
+
/** Show/hide the task form (create mode when opening); always blank (no template prefill). */
|
|
382
431
|
setComposer(open) {
|
|
383
432
|
this.setState({
|
|
384
433
|
composerOpen: open,
|
|
385
|
-
editingId: void 0
|
|
434
|
+
editingId: void 0,
|
|
435
|
+
templatePrefill: void 0
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
/** Open the create form prefilled from a chosen template (0.4.0). */
|
|
439
|
+
newFromTemplate(spec) {
|
|
440
|
+
this.setState({
|
|
441
|
+
composerOpen: true,
|
|
442
|
+
editingId: void 0,
|
|
443
|
+
templatePrefill: spec
|
|
386
444
|
});
|
|
387
445
|
}
|
|
388
|
-
/** Open the form modal editing an existing task. */
|
|
446
|
+
/** Open the form modal editing an existing task (clears any template prefill). */
|
|
389
447
|
openEditor(id) {
|
|
390
448
|
this.setState({
|
|
391
449
|
composerOpen: true,
|
|
392
|
-
editingId: id
|
|
450
|
+
editingId: id,
|
|
451
|
+
templatePrefill: void 0
|
|
393
452
|
});
|
|
394
453
|
}
|
|
395
454
|
/** Close the form modal whatever its mode. */
|
|
396
455
|
closeForm() {
|
|
397
456
|
this.setState({
|
|
398
457
|
composerOpen: false,
|
|
399
|
-
editingId: void 0
|
|
458
|
+
editingId: void 0,
|
|
459
|
+
templatePrefill: void 0
|
|
400
460
|
});
|
|
401
461
|
}
|
|
402
462
|
/** Toggle the secondary tab. */
|
|
403
463
|
toggleSecondary() {
|
|
404
464
|
this.setState({ secondaryOpen: !this.state.secondaryOpen });
|
|
405
465
|
}
|
|
466
|
+
/** Whether a workspace passed git detection (form toggle enablement). */
|
|
467
|
+
gitAvailable(workspaceId) {
|
|
468
|
+
if (workspaceId === void 0) return true;
|
|
469
|
+
return this.state.workspaces.find((w) => w.id === workspaceId)?.gitAvailable === true;
|
|
470
|
+
}
|
|
406
471
|
/**
|
|
407
472
|
* Install the session-jump bridge (built from the runtime sessions service
|
|
408
473
|
* by the client entry). Without it openSession reports 'unavailable'.
|
|
@@ -508,6 +573,42 @@ window.__ModuleLoader__.load({
|
|
|
508
573
|
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
509
574
|
}
|
|
510
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* Toggle one checklist item as the USER (0.4.0): flips the item, records
|
|
578
|
+
* `checkedBy: 'user'`, keeps other items as they are (one update call).
|
|
579
|
+
*/
|
|
580
|
+
async toggleChecklistItem(task, itemId) {
|
|
581
|
+
const items = (task.checklist ?? []).map((item) => item.id === itemId ? item.checked ? {
|
|
582
|
+
id: item.id,
|
|
583
|
+
text: item.text,
|
|
584
|
+
checked: false
|
|
585
|
+
} : {
|
|
586
|
+
id: item.id,
|
|
587
|
+
text: item.text,
|
|
588
|
+
checked: true,
|
|
589
|
+
checkedBy: "user",
|
|
590
|
+
checkedAt: Date.now(),
|
|
591
|
+
...item.note !== void 0 ? { note: item.note } : {}
|
|
592
|
+
} : item);
|
|
593
|
+
try {
|
|
594
|
+
await this.client.update(task.id, {
|
|
595
|
+
ifVersion: task.version,
|
|
596
|
+
checklist: items
|
|
597
|
+
});
|
|
598
|
+
await this.refresh();
|
|
599
|
+
} catch (error) {
|
|
600
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
/** Diff view (0.4.0): one execution's commit or changed path; errors surface via throw. */
|
|
604
|
+
async fetchDiff(taskId, query) {
|
|
605
|
+
try {
|
|
606
|
+
return await this.client.diff(taskId, query);
|
|
607
|
+
} catch (error) {
|
|
608
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
511
612
|
/** Append a user comment. */
|
|
512
613
|
async comment(id, body) {
|
|
513
614
|
try {
|
|
@@ -517,10 +618,10 @@ window.__ModuleLoader__.load({
|
|
|
517
618
|
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
518
619
|
}
|
|
519
620
|
}
|
|
520
|
-
/** Trigger a manual run (fresh in-project session, pinned model)
|
|
521
|
-
async run(id) {
|
|
621
|
+
/** Trigger a manual run (fresh in-project session, pinned model); `reuse` = 续跑. */
|
|
622
|
+
async run(id, reuse = false) {
|
|
522
623
|
try {
|
|
523
|
-
await this.client.run(id);
|
|
624
|
+
await this.client.run(id, reuse ? { reuse: true } : {});
|
|
524
625
|
await this.refresh();
|
|
525
626
|
} catch (error) {
|
|
526
627
|
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
@@ -535,6 +636,66 @@ window.__ModuleLoader__.load({
|
|
|
535
636
|
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
536
637
|
}
|
|
537
638
|
}
|
|
639
|
+
/**
|
|
640
|
+
* ⇥ 合并 (detail page): merge the task branch into the main worktree.
|
|
641
|
+
* @returns the outcome; `noop` means the branch had no new commits (nothing merged).
|
|
642
|
+
*/
|
|
643
|
+
async mergeBranch(id) {
|
|
644
|
+
try {
|
|
645
|
+
const value = await this.client.mergeBranch(id);
|
|
646
|
+
await this.refresh();
|
|
647
|
+
return value.noop === true ? {
|
|
648
|
+
ok: true,
|
|
649
|
+
noop: true
|
|
650
|
+
} : { ok: true };
|
|
651
|
+
} catch (error) {
|
|
652
|
+
return {
|
|
653
|
+
ok: false,
|
|
654
|
+
error: error instanceof Error ? error.message : String(error)
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* 🗑 删除 worktree (detail page), optionally deleting the task branch too.
|
|
660
|
+
* @returns the outcome; failures carry the git message for an alert.
|
|
661
|
+
*/
|
|
662
|
+
async removeWorktree(id, deleteBranch) {
|
|
663
|
+
try {
|
|
664
|
+
const value = await this.client.worktreeRemove(id, { deleteBranch });
|
|
665
|
+
await this.refresh();
|
|
666
|
+
return value.branchError !== void 0 ? {
|
|
667
|
+
ok: true,
|
|
668
|
+
branchError: value.branchError
|
|
669
|
+
} : { ok: true };
|
|
670
|
+
} catch (error) {
|
|
671
|
+
return {
|
|
672
|
+
ok: false,
|
|
673
|
+
error: error instanceof Error ? error.message : String(error)
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
/** Open the ⚙ diagnostics panel and fetch a fresh snapshot. */
|
|
678
|
+
openDiagnostics() {
|
|
679
|
+
this.setState({ diagOpen: true });
|
|
680
|
+
this.client.diagnostics().then((diagnostics) => this.setState({ diagnostics })).catch((error) => this.setState({ error: error instanceof Error ? error.message : String(error) }));
|
|
681
|
+
}
|
|
682
|
+
/** Close the ⚙ diagnostics panel. */
|
|
683
|
+
closeDiagnostics() {
|
|
684
|
+
this.setState({ diagOpen: false });
|
|
685
|
+
}
|
|
686
|
+
/** Clean one orphan worktree (⚙ panel); refreshes the diagnostics payload. */
|
|
687
|
+
async cleanupOrphan(workspaceId, taskId) {
|
|
688
|
+
try {
|
|
689
|
+
await this.client.worktreeCleanup(workspaceId, taskId);
|
|
690
|
+
const diagnostics = await this.client.diagnostics();
|
|
691
|
+
this.setState({
|
|
692
|
+
diagnostics,
|
|
693
|
+
error: void 0
|
|
694
|
+
});
|
|
695
|
+
} catch (error) {
|
|
696
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
697
|
+
}
|
|
698
|
+
}
|
|
538
699
|
/** Soft-delete (agent parity) then optional purge. */
|
|
539
700
|
async remove(id, ifVersion, purge) {
|
|
540
701
|
try {
|
|
@@ -545,7 +706,7 @@ window.__ModuleLoader__.load({
|
|
|
545
706
|
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
546
707
|
}
|
|
547
708
|
}
|
|
548
|
-
/** Duplicate a task into a fresh todo card (same project/urgency/prompt/execution/model). */
|
|
709
|
+
/** Duplicate a task into a fresh todo card (same project/urgency/prompt/execution/model/isolation/checklist). */
|
|
549
710
|
async duplicate(task) {
|
|
550
711
|
try {
|
|
551
712
|
await this.client.create({
|
|
@@ -558,11 +719,109 @@ window.__ModuleLoader__.load({
|
|
|
558
719
|
mode: "scheduled",
|
|
559
720
|
cron: task.execution.cron
|
|
560
721
|
} : { mode: "claim" },
|
|
561
|
-
model: task.model
|
|
722
|
+
model: task.model,
|
|
723
|
+
isolation: task.isolation,
|
|
724
|
+
...task.presetId !== void 0 ? { presetId: task.presetId } : {},
|
|
725
|
+
...task.checklist !== void 0 && task.checklist.length > 0 ? { checklist: task.checklist.map((i) => i.text) } : {}
|
|
726
|
+
});
|
|
727
|
+
await this.refresh();
|
|
728
|
+
} catch (error) {
|
|
729
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/** Load the template list (best effort; errors surface). */
|
|
733
|
+
async loadTemplates() {
|
|
734
|
+
try {
|
|
735
|
+
const value = await this.client.templates();
|
|
736
|
+
this.setState({
|
|
737
|
+
templates: value.templates,
|
|
738
|
+
error: void 0
|
|
562
739
|
});
|
|
740
|
+
return value.templates;
|
|
741
|
+
} catch (error) {
|
|
742
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
743
|
+
return [];
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
/** Open the + 新建任务 dropdown's template list fresh (called on menu open). */
|
|
747
|
+
prepareTemplateMenu() {
|
|
748
|
+
if (this.state.templates.length === 0) this.loadTemplates();
|
|
749
|
+
}
|
|
750
|
+
/** Open the template manager modal. */
|
|
751
|
+
openTemplateManager() {
|
|
752
|
+
this.setState({ tplManagerOpen: true });
|
|
753
|
+
this.loadTemplates();
|
|
754
|
+
}
|
|
755
|
+
/** Close the template manager modal. */
|
|
756
|
+
closeTemplateManager() {
|
|
757
|
+
this.setState({ tplManagerOpen: false });
|
|
758
|
+
}
|
|
759
|
+
/** Create or replace a template; refreshes the list. */
|
|
760
|
+
async upsertTemplate(body) {
|
|
761
|
+
try {
|
|
762
|
+
await this.client.templateUpsert(body);
|
|
763
|
+
await this.loadTemplates();
|
|
764
|
+
return true;
|
|
765
|
+
} catch (error) {
|
|
766
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
/** Delete a template by id; refreshes the list. */
|
|
771
|
+
async deleteTemplate(id) {
|
|
772
|
+
try {
|
|
773
|
+
await this.client.templateDelete(id);
|
|
774
|
+
await this.loadTemplates();
|
|
775
|
+
} catch (error) {
|
|
776
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
/** 存为模板 from a task card: carries every configurable field incl. checklist texts. */
|
|
780
|
+
async saveAsTemplate(task) {
|
|
781
|
+
return this.upsertTemplate({
|
|
782
|
+
name: task.title.slice(0, 60),
|
|
783
|
+
task: {
|
|
784
|
+
title: task.title,
|
|
785
|
+
description: task.description.length > 0 ? task.description : void 0,
|
|
786
|
+
prompt: task.prompt.length > 0 ? task.prompt : void 0,
|
|
787
|
+
urgency: task.urgency,
|
|
788
|
+
execution: task.execution.mode === "scheduled" && task.execution.cron !== void 0 ? {
|
|
789
|
+
mode: "scheduled",
|
|
790
|
+
cron: task.execution.cron
|
|
791
|
+
} : { mode: "claim" },
|
|
792
|
+
model: task.model,
|
|
793
|
+
isolation: task.isolation,
|
|
794
|
+
...task.presetId !== void 0 ? { presetId: task.presetId } : {},
|
|
795
|
+
...task.checklist !== void 0 && task.checklist.length > 0 ? { checklist: task.checklist.map((i) => i.text) } : {}
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
/** Open the import modal. */
|
|
800
|
+
openImport() {
|
|
801
|
+
this.setState({ importOpen: true });
|
|
802
|
+
}
|
|
803
|
+
/** Close the import modal. */
|
|
804
|
+
closeImport() {
|
|
805
|
+
this.setState({ importOpen: false });
|
|
806
|
+
}
|
|
807
|
+
/** Dry-run an import file: classify its tasks against the live ledger. */
|
|
808
|
+
async importPreview(file) {
|
|
809
|
+
try {
|
|
810
|
+
return (await this.client.importPreview(file)).plan;
|
|
811
|
+
} catch (error) {
|
|
812
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
/** Commit an import; refreshes the ledger afterwards. */
|
|
817
|
+
async importCommit(mode, ledger) {
|
|
818
|
+
try {
|
|
819
|
+
const value = await this.client.importCommit(mode, ledger);
|
|
563
820
|
await this.refresh();
|
|
821
|
+
return value;
|
|
564
822
|
} catch (error) {
|
|
565
823
|
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
824
|
+
return;
|
|
566
825
|
}
|
|
567
826
|
}
|
|
568
827
|
/** Download the whole ledger as a JSON backup file. */
|
|
@@ -1105,6 +1364,204 @@ window.__ModuleLoader__.load({
|
|
|
1105
1364
|
color: var(--dsw-alias-label-primary, inherit);
|
|
1106
1365
|
}
|
|
1107
1366
|
.dsh-atb-alert .dsh-atb-btn { padding: 6px 28px; font-size: 13px; }
|
|
1367
|
+
|
|
1368
|
+
/* ---------- 0.3.0 isolation ---------- */
|
|
1369
|
+
.dsh-atb-isolation-note { display: block; margin-top: 6px; font-size: 11px; color: var(--dsw-alias-label-tertiary, gray); }
|
|
1370
|
+
.dsh-atb-mode-picker[data-disabled="true"] .dsh-atb-mode-opt { cursor: not-allowed; opacity: .55; }
|
|
1371
|
+
.dsh-atb-iso-none { font-size: 12.5px; color: var(--dsw-alias-label-secondary, inherit); }
|
|
1372
|
+
.dsh-atb-iso-facts { display: flex; flex-wrap: wrap; gap: 6px 12px; margin-bottom: 8px; }
|
|
1373
|
+
.dsh-atb-iso-fact { font-size: 11.5px; color: var(--dsw-alias-label-secondary, inherit); }
|
|
1374
|
+
.dsh-atb-iso-fact b { font-weight: 600; color: var(--dsw-alias-state-business-primary, #3e63dd); }
|
|
1375
|
+
.dsh-atb-iso-commits { display: flex; flex-direction: column; gap: 3px; margin-bottom: 8px; }
|
|
1376
|
+
.dsh-atb-iso-commit { display: flex; gap: 8px; font-size: 11.5px; align-items: baseline; }
|
|
1377
|
+
.dsh-atb-iso-commit code {
|
|
1378
|
+
font-family: ui-monospace, Consolas, monospace; font-size: 10.5px;
|
|
1379
|
+
color: var(--dsh-alias-state-business-primary, #3e63dd); flex-shrink: 0;
|
|
1380
|
+
}
|
|
1381
|
+
.dsh-atb-iso-commit span { word-break: break-all; color: var(--dsw-alias-label-secondary, inherit); }
|
|
1382
|
+
.dsh-atb-iso-more { font-size: 11px; color: var(--dsw-alias-label-tertiary, gray); }
|
|
1383
|
+
.dsh-atb-iso-nocommit { font-size: 11.5px; color: var(--dsw-alias-label-tertiary, gray); margin-bottom: 8px; }
|
|
1384
|
+
.dsh-atb-iso-dirty {
|
|
1385
|
+
font-size: 11.5px; color: var(--dsw-alias-state-error-primary, #e5484d);
|
|
1386
|
+
background: rgba(229,72,77,.09); border: 1px solid rgba(229,72,77,.35);
|
|
1387
|
+
border-radius: 8px; padding: 6px 10px; margin-bottom: 8px;
|
|
1388
|
+
}
|
|
1389
|
+
.dsh-atb-iso-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
1390
|
+
.dsh-atb-iso-hint { font-size: 11px; color: var(--dsw-alias-label-tertiary, gray); }
|
|
1391
|
+
|
|
1392
|
+
/* ---------- 0.3.0 diagnostics ---------- */
|
|
1393
|
+
.dsh-atb-diag { max-width: 520px; width: min(520px, 92vw); }
|
|
1394
|
+
.dsh-atb-diag-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 14px; }
|
|
1395
|
+
.dsh-atb-diag-item {
|
|
1396
|
+
display: flex; flex-direction: column; align-items: center; gap: 2px;
|
|
1397
|
+
padding: 10px 6px; border-radius: 10px;
|
|
1398
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
|
|
1399
|
+
background: var(--dsw-alias-bg-layer-1, rgba(128,128,128,.04));
|
|
1400
|
+
}
|
|
1401
|
+
.dsh-atb-diag-item b { font-size: 18px; font-weight: 700; }
|
|
1402
|
+
.dsh-atb-diag-item span { font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); }
|
|
1403
|
+
.dsh-atb-diag-item[data-bad="true"] b { color: var(--dsw-alias-state-error-primary, #e5484d); }
|
|
1404
|
+
.dsh-atb-diag-sec h4 { margin: 0 0 8px; font-size: 12.5px; }
|
|
1405
|
+
.dsh-atb-diag-orphans { display: flex; flex-direction: column; gap: 6px; }
|
|
1406
|
+
.dsh-atb-diag-orphan {
|
|
1407
|
+
display: flex; align-items: center; gap: 10px; justify-content: space-between;
|
|
1408
|
+
padding: 7px 10px; border-radius: 8px;
|
|
1409
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
|
|
1410
|
+
}
|
|
1411
|
+
.dsh-atb-diag-orphan-path { font-size: 11.5px; font-family: ui-monospace, Consolas, monospace; word-break: break-all; }
|
|
1412
|
+
|
|
1413
|
+
/* ---------- 0.4.0 checklist ---------- */
|
|
1414
|
+
.dsh-atb-cke { display: flex; flex-direction: column; gap: 6px; }
|
|
1415
|
+
.dsh-atb-cke-row { display: flex; align-items: center; gap: 8px; }
|
|
1416
|
+
.dsh-atb-cke-box { flex-shrink: 0; width: 15px; height: 15px; cursor: pointer; }
|
|
1417
|
+
.dsh-atb-cke-text {
|
|
1418
|
+
flex: 1; min-width: 0; font-size: 12.5px; padding: 6px 9px;
|
|
1419
|
+
border-radius: 8px; border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.3));
|
|
1420
|
+
background: var(--dsw-alias-bg-layer-1, transparent); color: inherit;
|
|
1421
|
+
}
|
|
1422
|
+
.dsh-atb-cke-del {
|
|
1423
|
+
flex-shrink: 0; border: none; background: none; cursor: pointer; padding: 4px;
|
|
1424
|
+
color: var(--dsw-alias-label-tertiary, gray); font-size: 12px; border-radius: 6px;
|
|
1425
|
+
}
|
|
1426
|
+
.dsh-atb-cke-del:hover { color: var(--dsw-alias-state-error-primary, #e5484d); background: rgba(229,72,77,.08); }
|
|
1427
|
+
.dsh-atb-cke-add {
|
|
1428
|
+
align-self: flex-start; border: 1px dashed var(--dsw-alias-border-l2, rgba(128,128,128,.4));
|
|
1429
|
+
background: none; color: var(--dsw-alias-label-secondary, inherit); cursor: pointer;
|
|
1430
|
+
font-size: 11.5px; padding: 5px 12px; border-radius: 8px;
|
|
1431
|
+
}
|
|
1432
|
+
.dsh-atb-cke-add:hover { color: var(--dsw-alias-state-business-primary, #3e63dd); border-color: var(--dsw-alias-state-business-primary, #3e63dd); }
|
|
1433
|
+
.dsh-atb-cke-hint { font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); }
|
|
1434
|
+
|
|
1435
|
+
.dsh-atb-cl-progress { margin-left: 8px; font-size: 11px; font-weight: 400; color: var(--dsw-alias-label-tertiary, gray); }
|
|
1436
|
+
.dsh-atb-cl-progress[data-tone="bad"] { color: var(--dsw-alias-state-error-primary, #e5484d); font-weight: 600; }
|
|
1437
|
+
.dsh-atb-cl-items { display: flex; flex-direction: column; gap: 5px; }
|
|
1438
|
+
.dsh-atb-cl-item {
|
|
1439
|
+
display: flex; align-items: baseline; gap: 9px; padding: 6px 9px; border-radius: 8px;
|
|
1440
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.22));
|
|
1441
|
+
background: var(--dsw-alias-bg-layer-1, rgba(128,128,128,.03)); cursor: pointer;
|
|
1442
|
+
}
|
|
1443
|
+
.dsh-atb-cl-item:hover { border-color: var(--dsw-alias-border-l2, rgba(128,128,128,.4)); }
|
|
1444
|
+
.dsh-atb-cl-item input { flex-shrink: 0; transform: translateY(1px); cursor: pointer; }
|
|
1445
|
+
.dsh-atb-cl-item[data-checked="true"] .dsh-atb-cl-text { text-decoration: line-through; color: var(--dsw-alias-label-tertiary, gray); }
|
|
1446
|
+
.dsh-atb-cl-item[data-alert="true"] {
|
|
1447
|
+
border-color: rgba(229,72,77,.45); background: rgba(229,72,77,.06);
|
|
1448
|
+
}
|
|
1449
|
+
.dsh-atb-cl-text { flex: 1; min-width: 0; font-size: 12.5px; word-break: break-word; }
|
|
1450
|
+
.dsh-atb-cl-meta { flex-shrink: 0; font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); display: flex; flex-direction: column; gap: 2px; align-items: flex-end; }
|
|
1451
|
+
.dsh-atb-cl-note { max-width: 280px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--dsw-alias-label-secondary, inherit); }
|
|
1452
|
+
|
|
1453
|
+
/* ---------- 0.4.0 report ---------- */
|
|
1454
|
+
.dsh-atb-rpt-summary { font-size: 12.5px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; margin-bottom: 8px; }
|
|
1455
|
+
.dsh-atb-rpt-sec { margin-bottom: 8px; }
|
|
1456
|
+
.dsh-atb-rpt-label { font-size: 11px; color: var(--dsw-alias-label-tertiary, gray); margin-bottom: 4px; }
|
|
1457
|
+
.dsh-atb-rpt-list { margin: 0; padding-left: 18px; font-size: 12px; line-height: 1.55; word-break: break-all; }
|
|
1458
|
+
.dsh-atb-rpt-risk {
|
|
1459
|
+
font-size: 12px; line-height: 1.55; white-space: pre-wrap; word-break: break-word;
|
|
1460
|
+
color: var(--dsw-alias-state-warn-primary, #f5a524);
|
|
1461
|
+
background: rgba(245,165,36,.08); border: 1px solid rgba(245,165,36,.3);
|
|
1462
|
+
border-radius: 8px; padding: 6px 10px;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/* ---------- 0.4.0 diff viewer ---------- */
|
|
1466
|
+
.dsh-atb-iso-commit { display: flex; flex-direction: column; gap: 3px; }
|
|
1467
|
+
.dsh-atb-iso-commit-btn {
|
|
1468
|
+
display: flex; gap: 8px; font-size: 11.5px; align-items: baseline; text-align: left;
|
|
1469
|
+
border: none; background: none; padding: 2px 4px; margin: 0 -4px; border-radius: 6px; cursor: pointer;
|
|
1470
|
+
color: inherit; width: fit-content; max-width: 100%;
|
|
1471
|
+
}
|
|
1472
|
+
.dsh-atb-iso-commit-btn:hover { background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.08)); }
|
|
1473
|
+
.dsh-atb-iso-commit-btn code {
|
|
1474
|
+
font-family: ui-monospace, Consolas, monospace; font-size: 10.5px;
|
|
1475
|
+
color: var(--dsw-alias-state-business-primary, #3e63dd); flex-shrink: 0;
|
|
1476
|
+
}
|
|
1477
|
+
.dsh-atb-iso-commit-btn span { word-break: break-all; color: var(--dsw-alias-label-secondary, inherit); }
|
|
1478
|
+
.dsh-atb-iso-commit[data-open="true"] > .dsh-atb-iso-commit-btn code { font-weight: 700; }
|
|
1479
|
+
.dsh-atb-iso-dirty { display: flex; flex-direction: column; gap: 6px;
|
|
1480
|
+
font-size: 11.5px; color: var(--dsw-alias-state-error-primary, #e5484d);
|
|
1481
|
+
background: rgba(229,72,77,.09); border: 1px solid rgba(229,72,77,.35);
|
|
1482
|
+
border-radius: 8px; padding: 6px 10px; margin-bottom: 8px;
|
|
1483
|
+
}
|
|
1484
|
+
.dsh-atb-iso-dirty-toggle { border: none; background: none; cursor: pointer; padding: 0; text-align: left; color: inherit; font-size: inherit; }
|
|
1485
|
+
.dsh-atb-iso-dirty-files { display: flex; flex-direction: column; gap: 2px; }
|
|
1486
|
+
.dsh-atb-iso-dirty-file {
|
|
1487
|
+
border: none; background: none; cursor: pointer; text-align: left; padding: 1px 2px;
|
|
1488
|
+
font-size: 11px; color: var(--dsw-alias-label-secondary, inherit); border-radius: 4px; word-break: break-all;
|
|
1489
|
+
}
|
|
1490
|
+
.dsh-atb-iso-dirty-file:hover { background: rgba(128,128,128,.1); color: var(--dsw-alias-state-business-primary, #3e63dd); }
|
|
1491
|
+
.dsh-atb-iso-dirty-file code { font-family: ui-monospace, Consolas, monospace; font-size: 10px; margin-right: 6px; }
|
|
1492
|
+
.dsh-atb-diffview { margin-top: 6px; border-radius: 8px; overflow: hidden;
|
|
1493
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25)); }
|
|
1494
|
+
.dsh-atb-diffview-head { display: flex; align-items: center; gap: 10px; padding: 5px 10px;
|
|
1495
|
+
background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.08)); }
|
|
1496
|
+
.dsh-atb-diffview-title { font-family: ui-monospace, Consolas, monospace; font-size: 10.5px;
|
|
1497
|
+
color: var(--dsw-alias-state-business-primary, #3e63dd); word-break: break-all; }
|
|
1498
|
+
.dsh-atb-diffview-hint { font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); }
|
|
1499
|
+
.dsh-atb-diffview-error { padding: 8px 10px; font-size: 11.5px; color: var(--dsw-alias-state-error-primary, #e5484d); }
|
|
1500
|
+
.dsh-atb-diffview-pre {
|
|
1501
|
+
margin: 0; padding: 8px 10px; max-height: 340px; overflow: auto;
|
|
1502
|
+
font-family: ui-monospace, Consolas, monospace; font-size: 10.5px; line-height: 1.5;
|
|
1503
|
+
white-space: pre; color: var(--dsw-alias-label-secondary, inherit);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
/* ---------- 0.4.0 new-task menu + template manager + import ---------- */
|
|
1507
|
+
.dsh-atb-newmenu { position: relative; display: inline-flex; }
|
|
1508
|
+
.dsh-atb-newmenu-backdrop { position: fixed; inset: 0; z-index: 40; }
|
|
1509
|
+
.dsh-atb-newmenu-list {
|
|
1510
|
+
position: absolute; top: calc(100% + 4px); left: 0; z-index: 41; min-width: 180px;
|
|
1511
|
+
display: flex; flex-direction: column; padding: 5px; border-radius: 10px;
|
|
1512
|
+
background: var(--dsw-alias-bg-overlay, #fff); color: var(--dsw-alias-label-primary, inherit);
|
|
1513
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
|
|
1514
|
+
box-shadow: var(--dsw-shadow-lv3, 0 12px 32px rgba(0,0,0,.18));
|
|
1515
|
+
}
|
|
1516
|
+
.dsh-atb-newmenu-opt {
|
|
1517
|
+
border: none; background: none; text-align: left; cursor: pointer; padding: 7px 10px;
|
|
1518
|
+
font-size: 12.5px; color: inherit; border-radius: 7px; white-space: nowrap;
|
|
1519
|
+
}
|
|
1520
|
+
.dsh-atb-newmenu-opt:hover { background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.1)); }
|
|
1521
|
+
.dsh-atb-newmenu-sep { height: 1px; margin: 4px 6px; background: var(--dsw-alias-border-l2, rgba(128,128,128,.25)); }
|
|
1522
|
+
|
|
1523
|
+
.dsh-atb-tplm { max-width: 560px; width: min(560px, 92vw); }
|
|
1524
|
+
.dsh-atb-tplm-list { display: flex; flex-direction: column; gap: 8px; }
|
|
1525
|
+
.dsh-atb-tplm-row {
|
|
1526
|
+
display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 8px;
|
|
1527
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
|
|
1528
|
+
}
|
|
1529
|
+
.dsh-atb-tplm-name {
|
|
1530
|
+
flex: 0 0 160px; font-size: 12.5px; padding: 5px 8px; border-radius: 7px;
|
|
1531
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.3));
|
|
1532
|
+
background: var(--dsw-alias-bg-layer-1, transparent); color: inherit;
|
|
1533
|
+
}
|
|
1534
|
+
.dsh-atb-tplm-meta { flex: 1; min-width: 0; font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); }
|
|
1535
|
+
.dsh-atb-tplm-btns { display: flex; gap: 6px; flex-shrink: 0; }
|
|
1536
|
+
|
|
1537
|
+
.dsh-atb-imp { max-width: 600px; width: min(600px, 92vw); }
|
|
1538
|
+
.dsh-atb-imp-picker { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
|
1539
|
+
.dsh-atb-imp-picker input[type="file"] { font-size: 12px; }
|
|
1540
|
+
.dsh-atb-imp-filename { font-size: 11.5px; color: var(--dsw-alias-state-business-primary, #3e63dd); word-break: break-all; }
|
|
1541
|
+
.dsh-atb-imp-note { font-size: 11px; color: var(--dsw-alias-label-tertiary, gray); margin-bottom: 10px; }
|
|
1542
|
+
.dsh-atb-imp-error { font-size: 12px; color: var(--dsw-alias-state-error-primary, #e5484d); margin-bottom: 8px; }
|
|
1543
|
+
.dsh-atb-imp-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 12px; }
|
|
1544
|
+
.dsh-atb-imp-stat {
|
|
1545
|
+
display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 9px 6px; border-radius: 9px;
|
|
1546
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
|
|
1547
|
+
}
|
|
1548
|
+
.dsh-atb-imp-stat b { font-size: 17px; font-weight: 700; }
|
|
1549
|
+
.dsh-atb-imp-stat span { font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); }
|
|
1550
|
+
.dsh-atb-imp-stat[data-tone="ok"] b { color: var(--dsw-alias-state-success-primary, #30a46c); }
|
|
1551
|
+
.dsh-atb-imp-stat[data-tone="warn"] b { color: var(--dsw-alias-state-warn-primary, #f5a524); }
|
|
1552
|
+
.dsh-atb-imp-stat[data-tone="bad"] b { color: var(--dsw-alias-state-error-primary, #e5484d); }
|
|
1553
|
+
.dsh-atb-imp-sec h4 { margin: 0 0 6px; font-size: 12px; }
|
|
1554
|
+
.dsh-atb-imp-sec { margin-bottom: 10px; }
|
|
1555
|
+
.dsh-atb-imp-list { display: flex; flex-direction: column; gap: 4px; max-height: 160px; overflow-y: auto; }
|
|
1556
|
+
.dsh-atb-imp-row {
|
|
1557
|
+
display: flex; align-items: center; gap: 10px; justify-content: space-between;
|
|
1558
|
+
padding: 5px 9px; border-radius: 7px; border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.2));
|
|
1559
|
+
}
|
|
1560
|
+
.dsh-atb-imp-row[data-tone="bad"] { border-color: rgba(229,72,77,.35); }
|
|
1561
|
+
.dsh-atb-imp-row-title { font-size: 12px; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1562
|
+
.dsh-atb-imp-row-status { font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); flex-shrink: 0; }
|
|
1563
|
+
.dsh-atb-imp-result { font-size: 12px; color: var(--dsw-alias-state-success-primary, #30a46c); margin-top: 10px; }
|
|
1564
|
+
.dsh-atb-badge[data-kind="checklist"] { color: var(--dsw-alias-label-secondary, inherit); }
|
|
1108
1565
|
`;
|
|
1109
1566
|
let injected = false;
|
|
1110
1567
|
/** Inject the stylesheet once (idempotent). */
|
|
@@ -1349,7 +1806,7 @@ window.__ModuleLoader__.load({
|
|
|
1349
1806
|
* @module dsh-taskboard/shared/version
|
|
1350
1807
|
*/
|
|
1351
1808
|
/** The package version (must equal package.json "version"). */
|
|
1352
|
-
const PLUGIN_VERSION = "0.
|
|
1809
|
+
const PLUGIN_VERSION = "0.4.0";
|
|
1353
1810
|
|
|
1354
1811
|
//#endregion
|
|
1355
1812
|
//#region src/client/board/TaskCard.tsx
|
|
@@ -1463,6 +1920,17 @@ window.__ModuleLoader__.load({
|
|
|
1463
1920
|
className: "dsh-atb-badge",
|
|
1464
1921
|
children: task.model.model
|
|
1465
1922
|
}),
|
|
1923
|
+
task.checklist !== void 0 && task.checklist.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1924
|
+
className: "dsh-atb-badge",
|
|
1925
|
+
"data-kind": task.status === "in_review" && task.checklist.some((i) => !i.checked) ? "blocked" : "checklist",
|
|
1926
|
+
title: task.status === "in_review" && task.checklist.some((i) => !i.checked) ? "待验收:清单未全部勾选" : "验收清单进度",
|
|
1927
|
+
children: [
|
|
1928
|
+
"☑ ",
|
|
1929
|
+
task.checklist.filter((i) => i.checked).length,
|
|
1930
|
+
"/",
|
|
1931
|
+
task.checklist.length
|
|
1932
|
+
]
|
|
1933
|
+
}),
|
|
1466
1934
|
task.status === "done" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1467
1935
|
className: "dsh-atb-badge",
|
|
1468
1936
|
"data-kind": "done",
|
|
@@ -1669,44 +2137,478 @@ window.__ModuleLoader__.load({
|
|
|
1669
2137
|
}), children]
|
|
1670
2138
|
});
|
|
1671
2139
|
}
|
|
2140
|
+
/** The most recent execution carrying isolation facts, newest first. */
|
|
2141
|
+
function latestIsolated(task) {
|
|
2142
|
+
return [...task.executions].reverse().find((e) => e.isolation !== void 0 || e.worktreePath !== void 0 || e.isolationNote !== void 0);
|
|
2143
|
+
}
|
|
2144
|
+
/** Short commit hash for display. */
|
|
2145
|
+
function shortHash(hash) {
|
|
2146
|
+
return hash === void 0 ? "" : hash.slice(0, 8);
|
|
2147
|
+
}
|
|
2148
|
+
/** Extract the path from one `git status --porcelain` line (rename-aware). */
|
|
2149
|
+
function porcelainPath(line) {
|
|
2150
|
+
let p = line.slice(3);
|
|
2151
|
+
const arrow = p.indexOf(" -> ");
|
|
2152
|
+
if (arrow >= 0) p = p.slice(arrow + 4);
|
|
2153
|
+
if (p.startsWith("\"") && p.endsWith("\"") && p.length > 1) p = p.slice(1, -1);
|
|
2154
|
+
return p;
|
|
2155
|
+
}
|
|
1672
2156
|
/**
|
|
1673
|
-
*
|
|
1674
|
-
* @param
|
|
1675
|
-
* @param controller - the controller.
|
|
1676
|
-
* @param now - current epoch ms (stale-claim highlight).
|
|
2157
|
+
* Lazy diff viewer (0.4.0): loads on mount, renders inside a capped <pre>.
|
|
2158
|
+
* @param spec - what to show: one commit hash, or one changed path.
|
|
1677
2159
|
*/
|
|
1678
|
-
function
|
|
1679
|
-
const [
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
2160
|
+
function DiffView({ controller, task, execution, commit, path }) {
|
|
2161
|
+
const [state, setState] = (0, react.useState)({ loading: true });
|
|
2162
|
+
(0, react.useEffect)(() => {
|
|
2163
|
+
let alive = true;
|
|
2164
|
+
setState({ loading: true });
|
|
2165
|
+
controller.fetchDiff(task.id, {
|
|
2166
|
+
execution: execution.id,
|
|
2167
|
+
...commit !== void 0 ? { commit } : { path: path ?? "" }
|
|
2168
|
+
}).then((result) => {
|
|
2169
|
+
if (!alive) return;
|
|
2170
|
+
if (result === void 0) setState({
|
|
2171
|
+
loading: false,
|
|
2172
|
+
failed: true
|
|
2173
|
+
});
|
|
2174
|
+
else setState({
|
|
2175
|
+
loading: false,
|
|
2176
|
+
diff: result.diff,
|
|
2177
|
+
truncated: result.truncated
|
|
2178
|
+
});
|
|
2179
|
+
});
|
|
2180
|
+
return () => {
|
|
2181
|
+
alive = false;
|
|
2182
|
+
};
|
|
2183
|
+
}, [
|
|
2184
|
+
controller,
|
|
2185
|
+
task.id,
|
|
2186
|
+
execution.id,
|
|
2187
|
+
commit,
|
|
2188
|
+
path
|
|
2189
|
+
]);
|
|
2190
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2191
|
+
className: "dsh-atb-diffview",
|
|
2192
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2193
|
+
className: "dsh-atb-diffview-head",
|
|
2194
|
+
children: [
|
|
2195
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2196
|
+
className: "dsh-atb-diffview-title",
|
|
2197
|
+
children: commit !== void 0 ? `提交 ${shortHash(commit)}` : `文件 ${path}`
|
|
2198
|
+
}),
|
|
2199
|
+
state.loading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2200
|
+
className: "dsh-atb-diffview-hint",
|
|
2201
|
+
children: "读取中…"
|
|
2202
|
+
}),
|
|
2203
|
+
state.truncated === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2204
|
+
className: "dsh-atb-diffview-hint",
|
|
2205
|
+
children: "⚠ 内容过长已截断"
|
|
2206
|
+
})
|
|
2207
|
+
]
|
|
2208
|
+
}), state.failed === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2209
|
+
className: "dsh-atb-diffview-error",
|
|
2210
|
+
children: "获取失败(原因见看板顶部错误条;对象可能已随 worktree 删除丢失)"
|
|
2211
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
2212
|
+
className: "dsh-atb-diffview-pre",
|
|
2213
|
+
children: state.diff ?? ""
|
|
2214
|
+
})]
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
2217
|
+
/**
|
|
2218
|
+
* The DoD checklist block (0.4.0): user-togglable items, checker + evidence
|
|
2219
|
+
* per row; unchecked items highlight while the task sits in in_review.
|
|
2220
|
+
*/
|
|
2221
|
+
function ChecklistBlock({ task, controller }) {
|
|
2222
|
+
const items = task.checklist ?? [];
|
|
2223
|
+
if (items.length === 0) return null;
|
|
2224
|
+
const { done, total } = checklistProgress(task);
|
|
2225
|
+
const unchecked = total - done;
|
|
2226
|
+
const reviewing = task.status === "in_review";
|
|
2227
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2228
|
+
className: "dsh-atb-fieldcard",
|
|
2229
|
+
"data-kind": "checklist",
|
|
2230
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2231
|
+
className: "dsh-atb-fieldcard-label",
|
|
2232
|
+
children: ["验收清单(DoD)", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2233
|
+
className: "dsh-atb-cl-progress",
|
|
2234
|
+
"data-tone": reviewing && unchecked > 0 ? "bad" : void 0,
|
|
2235
|
+
children: [
|
|
2236
|
+
"☑ ",
|
|
2237
|
+
done,
|
|
2238
|
+
"/",
|
|
2239
|
+
total,
|
|
2240
|
+
reviewing && unchecked > 0 ? ` · ${unchecked} 项未完成` : done === total ? " · 全部完成" : ""
|
|
2241
|
+
]
|
|
2242
|
+
})]
|
|
2243
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2244
|
+
className: "dsh-atb-cl-items",
|
|
2245
|
+
children: items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2246
|
+
className: "dsh-atb-cl-item",
|
|
2247
|
+
"data-checked": item.checked ? "true" : void 0,
|
|
2248
|
+
"data-alert": reviewing && !item.checked ? "true" : void 0,
|
|
2249
|
+
children: [
|
|
2250
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2251
|
+
type: "checkbox",
|
|
2252
|
+
checked: item.checked,
|
|
2253
|
+
onChange: () => void controller.toggleChecklistItem(task, item.id)
|
|
2254
|
+
}),
|
|
2255
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2256
|
+
className: "dsh-atb-cl-text",
|
|
2257
|
+
children: item.text
|
|
2258
|
+
}),
|
|
2259
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2260
|
+
className: "dsh-atb-cl-meta",
|
|
2261
|
+
children: [item.checked ? `${item.checkedBy === "user" ? "👤 用户" : `🤖 ${shortId(item.checkedBy)}`} · ${fmtTime(item.checkedAt)}` : "未完成", item.note !== void 0 && item.note.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2262
|
+
className: "dsh-atb-cl-note",
|
|
2263
|
+
title: item.note,
|
|
2264
|
+
children: ["证据:", item.note]
|
|
2265
|
+
})]
|
|
2266
|
+
})
|
|
2267
|
+
]
|
|
2268
|
+
}, item.id))
|
|
2269
|
+
})]
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
/**
|
|
2273
|
+
* The structured execution report block (0.4.0): the newest execution that
|
|
2274
|
+
* carries one, rendered section by section for the reviewer.
|
|
2275
|
+
*/
|
|
2276
|
+
function ReportBlock({ task }) {
|
|
2277
|
+
const execution = [...task.executions].reverse().find((e) => e.report !== void 0);
|
|
2278
|
+
const report = execution?.report;
|
|
2279
|
+
if (execution === void 0 || report === void 0) return null;
|
|
2280
|
+
const section = (label, rows) => rows !== void 0 && rows.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2281
|
+
className: "dsh-atb-rpt-sec",
|
|
2282
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2283
|
+
className: "dsh-atb-rpt-label",
|
|
2284
|
+
children: label
|
|
2285
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
2286
|
+
className: "dsh-atb-rpt-list",
|
|
2287
|
+
children: rows.map((row, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: row }, i))
|
|
2288
|
+
})]
|
|
2289
|
+
}) : null;
|
|
2290
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2291
|
+
className: "dsh-atb-fieldcard",
|
|
2292
|
+
"data-kind": "report",
|
|
2293
|
+
children: [
|
|
2294
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2295
|
+
className: "dsh-atb-fieldcard-label",
|
|
2296
|
+
children: ["执行报告", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2297
|
+
className: "dsh-atb-cl-progress",
|
|
2298
|
+
children: ["由执行会话提交 · ", fmtTime(execution.endedAt ?? execution.startedAt)]
|
|
2299
|
+
})]
|
|
2300
|
+
}),
|
|
2301
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2302
|
+
className: "dsh-atb-rpt-summary",
|
|
2303
|
+
children: report.summary
|
|
2304
|
+
}),
|
|
2305
|
+
section("改动文件", report.changedFiles),
|
|
2306
|
+
section("自验情况", report.checks),
|
|
2307
|
+
section("产物", report.artifacts),
|
|
2308
|
+
report.risk.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2309
|
+
className: "dsh-atb-rpt-sec",
|
|
2310
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2311
|
+
className: "dsh-atb-rpt-label",
|
|
2312
|
+
children: "剩余风险"
|
|
2313
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2314
|
+
className: "dsh-atb-rpt-risk",
|
|
2315
|
+
children: report.risk
|
|
2316
|
+
})]
|
|
2317
|
+
})
|
|
2318
|
+
]
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
/**
|
|
2322
|
+
* The 0.3.0 isolation block: branch / baseline→head commits / change stats /
|
|
2323
|
+
* uncommitted-changes warning, plus the user-only git actions (merge /
|
|
2324
|
+
* remove worktree — plan §3.3).
|
|
2325
|
+
*/
|
|
2326
|
+
function IsolationBlock({ task, controller }) {
|
|
1683
2327
|
const { alert: showAlert, el: alertEl } = useAlert();
|
|
1684
|
-
const
|
|
1685
|
-
const
|
|
1686
|
-
const
|
|
1687
|
-
const
|
|
1688
|
-
const
|
|
1689
|
-
|
|
1690
|
-
const
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
2328
|
+
const [confirmMerge, setConfirmMerge] = (0, react.useState)(false);
|
|
2329
|
+
const [confirmRemove, setConfirmRemove] = (0, react.useState)(null);
|
|
2330
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
2331
|
+
const [openDiff, setOpenDiff] = (0, react.useState)(null);
|
|
2332
|
+
const [dirtyOpen, setDirtyOpen] = (0, react.useState)(false);
|
|
2333
|
+
const execution = latestIsolated(task);
|
|
2334
|
+
const running = task.executions.some((e) => e.outcome === "running");
|
|
2335
|
+
if (execution === void 0) return null;
|
|
2336
|
+
const doMerge = () => {
|
|
2337
|
+
setBusy(true);
|
|
2338
|
+
controller.mergeBranch(task.id).then((result) => {
|
|
2339
|
+
setBusy(false);
|
|
2340
|
+
setConfirmMerge(false);
|
|
2341
|
+
if (!result.ok) showAlert(`合并失败:${result.error}`);
|
|
2342
|
+
else if (result.noop === true) showAlert("该分支没有领先主工作区的新提交,无需合并(可退回续跑或直接清理)");
|
|
1695
2343
|
});
|
|
1696
2344
|
};
|
|
2345
|
+
const doRemove = (deleteBranch) => {
|
|
2346
|
+
setBusy(true);
|
|
2347
|
+
controller.removeWorktree(task.id, deleteBranch).then((result) => {
|
|
2348
|
+
setBusy(false);
|
|
2349
|
+
setConfirmRemove(null);
|
|
2350
|
+
if (!result.ok) showAlert(`删除失败:${result.error}`);
|
|
2351
|
+
else if (result.branchError !== void 0) showAlert(`worktree 已删除,但分支删除失败:${result.branchError}`);
|
|
2352
|
+
});
|
|
2353
|
+
};
|
|
2354
|
+
if (execution.isolation !== "worktree" || execution.worktreePath === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2355
|
+
className: "dsh-atb-fieldcard",
|
|
2356
|
+
"data-kind": "isolation",
|
|
2357
|
+
children: [
|
|
2358
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2359
|
+
className: "dsh-atb-fieldcard-label",
|
|
2360
|
+
children: "执行隔离"
|
|
2361
|
+
}),
|
|
2362
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2363
|
+
className: "dsh-atb-iso-none",
|
|
2364
|
+
children: ["📁 原目录执行", execution.isolationNote !== void 0 ? ` · ${execution.isolationNote}` : ""]
|
|
2365
|
+
}),
|
|
2366
|
+
alertEl
|
|
2367
|
+
]
|
|
2368
|
+
});
|
|
2369
|
+
const commits = execution.commits ?? [];
|
|
2370
|
+
const commitTotal = execution.commitsTotal ?? commits.length;
|
|
2371
|
+
const dirty = execution.dirtyFiles ?? [];
|
|
2372
|
+
const dirtyTotal = execution.dirtyFilesTotal ?? dirty.length;
|
|
1697
2373
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1698
|
-
className: "dsh-atb-
|
|
1699
|
-
"data-
|
|
2374
|
+
className: "dsh-atb-fieldcard",
|
|
2375
|
+
"data-kind": "isolation",
|
|
1700
2376
|
children: [
|
|
2377
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2378
|
+
className: "dsh-atb-fieldcard-label",
|
|
2379
|
+
children: "执行隔离 · Worktree"
|
|
2380
|
+
}),
|
|
1701
2381
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1702
|
-
className: "dsh-atb-
|
|
1703
|
-
children: [
|
|
1704
|
-
|
|
2382
|
+
className: "dsh-atb-iso-facts",
|
|
2383
|
+
children: [
|
|
2384
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2385
|
+
className: "dsh-atb-iso-fact",
|
|
2386
|
+
title: execution.worktreePath,
|
|
2387
|
+
children: ["🌿 分支 ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: execution.branch ?? task.branch })]
|
|
2388
|
+
}),
|
|
2389
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2390
|
+
className: "dsh-atb-iso-fact",
|
|
2391
|
+
children: [
|
|
2392
|
+
"基线 ",
|
|
2393
|
+
shortHash(execution.baseCommit),
|
|
2394
|
+
" → ",
|
|
2395
|
+
shortHash(execution.headCommit)
|
|
2396
|
+
]
|
|
2397
|
+
}),
|
|
2398
|
+
execution.changedFiles !== void 0 && execution.changedFiles > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2399
|
+
className: "dsh-atb-iso-fact",
|
|
2400
|
+
children: [
|
|
2401
|
+
"改动 ",
|
|
2402
|
+
execution.changedFiles,
|
|
2403
|
+
" 个文件"
|
|
2404
|
+
]
|
|
2405
|
+
}),
|
|
2406
|
+
execution.diffStat !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2407
|
+
className: "dsh-atb-iso-fact",
|
|
2408
|
+
title: execution.diffStat,
|
|
2409
|
+
children: execution.diffStat
|
|
2410
|
+
})
|
|
2411
|
+
]
|
|
2412
|
+
}),
|
|
2413
|
+
commits.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2414
|
+
className: "dsh-atb-iso-commits",
|
|
2415
|
+
children: [commits.slice(0, 10).map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2416
|
+
className: "dsh-atb-iso-commit",
|
|
2417
|
+
"data-open": openDiff?.commit === c.hash ? "true" : void 0,
|
|
2418
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2419
|
+
type: "button",
|
|
2420
|
+
className: "dsh-atb-iso-commit-btn",
|
|
2421
|
+
title: "点击展开该提交的 diff",
|
|
2422
|
+
onClick: () => setOpenDiff(openDiff?.commit === c.hash ? null : { commit: c.hash }),
|
|
2423
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: shortHash(c.hash) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: c.subject })]
|
|
2424
|
+
}), openDiff?.commit === c.hash && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiffView, {
|
|
2425
|
+
controller,
|
|
2426
|
+
task,
|
|
2427
|
+
execution,
|
|
2428
|
+
commit: c.hash
|
|
2429
|
+
})]
|
|
2430
|
+
}, c.hash)), commitTotal > 10 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2431
|
+
className: "dsh-atb-iso-more",
|
|
1705
2432
|
children: [
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
2433
|
+
"… 共 ",
|
|
2434
|
+
commitTotal,
|
|
2435
|
+
" 个提交"
|
|
2436
|
+
]
|
|
2437
|
+
})]
|
|
2438
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2439
|
+
className: "dsh-atb-iso-nocommit",
|
|
2440
|
+
children: "该次执行没有产生提交(改动可能未提交,见下方警告)"
|
|
2441
|
+
}),
|
|
2442
|
+
dirtyTotal > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2443
|
+
className: "dsh-atb-iso-dirty",
|
|
2444
|
+
children: [
|
|
2445
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2446
|
+
type: "button",
|
|
2447
|
+
className: "dsh-atb-iso-dirty-toggle",
|
|
2448
|
+
onClick: () => setDirtyOpen(!dirtyOpen),
|
|
2449
|
+
children: [
|
|
2450
|
+
"⚠ 有 ",
|
|
2451
|
+
dirtyTotal,
|
|
2452
|
+
" 处未提交修改(合并前请让 agent 提交,或手动处理)",
|
|
2453
|
+
dirtyOpen ? " ▲" : " ▼ 查看文件"
|
|
2454
|
+
]
|
|
2455
|
+
}),
|
|
2456
|
+
dirtyOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2457
|
+
className: "dsh-atb-iso-dirty-files",
|
|
2458
|
+
children: [dirty.slice(0, 30).map((line, index) => {
|
|
2459
|
+
const filePath = porcelainPath(line);
|
|
2460
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2461
|
+
type: "button",
|
|
2462
|
+
className: "dsh-atb-iso-dirty-file",
|
|
2463
|
+
title: "点击查看该文件的未提交 diff",
|
|
2464
|
+
onClick: () => setOpenDiff(openDiff?.path === filePath ? null : { path: filePath }),
|
|
2465
|
+
children: [
|
|
2466
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: line.slice(0, 2) }),
|
|
2467
|
+
" ",
|
|
2468
|
+
filePath
|
|
2469
|
+
]
|
|
2470
|
+
}, `${line}-${index}`);
|
|
2471
|
+
}), dirtyTotal > 30 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2472
|
+
className: "dsh-atb-iso-more",
|
|
2473
|
+
children: [
|
|
2474
|
+
"… 共 ",
|
|
2475
|
+
dirtyTotal,
|
|
2476
|
+
" 处(完整列表见任务台账)"
|
|
2477
|
+
]
|
|
2478
|
+
})]
|
|
2479
|
+
}),
|
|
2480
|
+
openDiff?.path !== void 0 && dirtyOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiffView, {
|
|
2481
|
+
controller,
|
|
2482
|
+
task,
|
|
2483
|
+
execution,
|
|
2484
|
+
path: openDiff.path
|
|
2485
|
+
})
|
|
2486
|
+
]
|
|
2487
|
+
}),
|
|
2488
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2489
|
+
className: "dsh-atb-iso-actions",
|
|
2490
|
+
children: [
|
|
2491
|
+
running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2492
|
+
className: "dsh-atb-iso-hint",
|
|
2493
|
+
children: "执行中 — 结束后可合并或清理"
|
|
2494
|
+
}) : confirmMerge ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2495
|
+
className: "dsh-atb-confirm",
|
|
2496
|
+
children: [
|
|
2497
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2498
|
+
className: "dsh-atb-confirm-label",
|
|
2499
|
+
children: "将分支以 --no-ff 合并到主工作区?"
|
|
2500
|
+
}),
|
|
2501
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2502
|
+
type: "button",
|
|
2503
|
+
className: "dsh-atb-btn",
|
|
2504
|
+
"data-primary": "true",
|
|
2505
|
+
disabled: busy,
|
|
2506
|
+
onClick: doMerge,
|
|
2507
|
+
children: "确认合并"
|
|
2508
|
+
}),
|
|
2509
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2510
|
+
type: "button",
|
|
2511
|
+
className: "dsh-atb-btn",
|
|
2512
|
+
onClick: () => setConfirmMerge(false),
|
|
2513
|
+
children: "取消"
|
|
2514
|
+
})
|
|
2515
|
+
]
|
|
2516
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2517
|
+
type: "button",
|
|
2518
|
+
className: "dsh-atb-btn",
|
|
2519
|
+
disabled: busy,
|
|
2520
|
+
title: "在主工作区 git merge --no-ff 该任务分支(要求主区干净;冲突会原样报告)",
|
|
2521
|
+
onClick: () => setConfirmMerge(true),
|
|
2522
|
+
children: "⇥ 合并到主工作区"
|
|
2523
|
+
}),
|
|
2524
|
+
!running && (confirmRemove === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2525
|
+
type: "button",
|
|
2526
|
+
className: "dsh-atb-btn",
|
|
2527
|
+
"data-danger": "true",
|
|
2528
|
+
disabled: busy,
|
|
2529
|
+
title: "git worktree remove(有未提交修改时拒绝)",
|
|
2530
|
+
onClick: () => setConfirmRemove("wt"),
|
|
2531
|
+
children: "🗑 删除 worktree"
|
|
2532
|
+
}), task.branch !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2533
|
+
type: "button",
|
|
2534
|
+
className: "dsh-atb-btn",
|
|
2535
|
+
"data-danger": "true",
|
|
2536
|
+
disabled: busy,
|
|
2537
|
+
title: "删除 worktree 并删除任务分支(有未提交修改时拒绝)",
|
|
2538
|
+
onClick: () => setConfirmRemove("wtb"),
|
|
2539
|
+
children: "🗑 删 worktree + 分支"
|
|
2540
|
+
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2541
|
+
className: "dsh-atb-confirm",
|
|
2542
|
+
children: [
|
|
2543
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2544
|
+
className: "dsh-atb-confirm-label",
|
|
2545
|
+
children: confirmRemove === "wtb" ? "删除 worktree 并删除分支?" : "删除 worktree 目录?"
|
|
2546
|
+
}),
|
|
2547
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2548
|
+
type: "button",
|
|
2549
|
+
className: "dsh-atb-btn",
|
|
2550
|
+
"data-danger": "true",
|
|
2551
|
+
disabled: busy,
|
|
2552
|
+
onClick: () => doRemove(confirmRemove === "wtb"),
|
|
2553
|
+
children: "确认删除"
|
|
2554
|
+
}),
|
|
2555
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2556
|
+
type: "button",
|
|
2557
|
+
className: "dsh-atb-btn",
|
|
2558
|
+
onClick: () => setConfirmRemove(null),
|
|
2559
|
+
children: "取消"
|
|
2560
|
+
})
|
|
2561
|
+
]
|
|
2562
|
+
})),
|
|
2563
|
+
!running && confirmRemove === null && !confirmMerge && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2564
|
+
className: "dsh-atb-iso-hint",
|
|
2565
|
+
children: "分支与 worktree 保留中 — 可退回继续修改"
|
|
2566
|
+
})
|
|
2567
|
+
]
|
|
2568
|
+
}),
|
|
2569
|
+
alertEl
|
|
2570
|
+
]
|
|
2571
|
+
});
|
|
2572
|
+
}
|
|
2573
|
+
/**
|
|
2574
|
+
* The detail view.
|
|
2575
|
+
* @param task - the task record.
|
|
2576
|
+
* @param controller - the controller.
|
|
2577
|
+
* @param now - current epoch ms (stale-claim highlight).
|
|
2578
|
+
*/
|
|
2579
|
+
function TaskDetail({ task, controller, now }) {
|
|
2580
|
+
const [comment, setComment] = (0, react.useState)("");
|
|
2581
|
+
const [confirmDone, setConfirmDone] = (0, react.useState)(false);
|
|
2582
|
+
const [confirmPurge, setConfirmPurge] = (0, react.useState)(false);
|
|
2583
|
+
const [confirmCancel, setConfirmCancel] = (0, react.useState)(false);
|
|
2584
|
+
const { alert: showAlert, el: alertEl } = useAlert();
|
|
2585
|
+
const ws = controller.getSnapshot().workspaces.find((w) => w.id === task.workspaceId);
|
|
2586
|
+
const canRun = task.status !== "in_progress" && task.status !== "done" && task.status !== "archived";
|
|
2587
|
+
const runningExecution = task.executions.find((e) => e.outcome === "running");
|
|
2588
|
+
const holder = task.status === "in_progress" ? task.claimedBy : void 0;
|
|
2589
|
+
const stale = now !== void 0 && isStaleClaim(task, now);
|
|
2590
|
+
const unchecked = (task.checklist ?? []).filter((i) => !i.checked).length;
|
|
2591
|
+
/** Jump to an execution's session; prompt precisely when it cannot open. */
|
|
2592
|
+
const jumpToSession = (sessionId) => {
|
|
2593
|
+
controller.openSession(sessionId).then((result) => {
|
|
2594
|
+
if (result === "missing") showAlert(`该会话已被删除(${shortId(sessionId)}),无法打开`);
|
|
2595
|
+
else if (result === "archived") showAlert(`该会话已归档(${shortId(sessionId)}),已从会话列表隐藏`);
|
|
2596
|
+
else if (result === "unavailable") showAlert(`会话导航不可用,会话 ID:${sessionId}`);
|
|
2597
|
+
});
|
|
2598
|
+
};
|
|
2599
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2600
|
+
className: "dsh-atb-detail",
|
|
2601
|
+
"data-urgency": task.urgency,
|
|
2602
|
+
children: [
|
|
2603
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2604
|
+
className: "dsh-atb-detail-head",
|
|
2605
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2606
|
+
className: "dsh-atb-detail-titlewrap",
|
|
2607
|
+
children: [
|
|
2608
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2609
|
+
className: "dsh-atb-detail-titlebar",
|
|
2610
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: task.title }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2611
|
+
className: "dsh-atb-statuspill",
|
|
1710
2612
|
"data-status": task.status,
|
|
1711
2613
|
children: STATUS_LABEL[task.status] ?? task.status
|
|
1712
2614
|
})]
|
|
@@ -1726,6 +2628,10 @@ window.__ModuleLoader__.load({
|
|
|
1726
2628
|
icon: "✦",
|
|
1727
2629
|
children: task.model.model
|
|
1728
2630
|
}),
|
|
2631
|
+
task.presetId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chip, {
|
|
2632
|
+
icon: "🎛",
|
|
2633
|
+
children: task.presetId
|
|
2634
|
+
}),
|
|
1729
2635
|
task.execution.mode === "scheduled" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Chip, {
|
|
1730
2636
|
icon: "⏰",
|
|
1731
2637
|
children: [
|
|
@@ -1739,6 +2645,25 @@ window.__ModuleLoader__.load({
|
|
|
1739
2645
|
tone: "urgent",
|
|
1740
2646
|
children: "受阻"
|
|
1741
2647
|
}),
|
|
2648
|
+
task.checklist !== void 0 && task.checklist.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Chip, {
|
|
2649
|
+
icon: "☑",
|
|
2650
|
+
tone: task.status === "in_review" && task.checklist.some((i) => !i.checked) ? "urgent" : void 0,
|
|
2651
|
+
children: [
|
|
2652
|
+
"清单 ",
|
|
2653
|
+
checklistProgress(task).done,
|
|
2654
|
+
"/",
|
|
2655
|
+
task.checklist.length
|
|
2656
|
+
]
|
|
2657
|
+
}),
|
|
2658
|
+
task.branch !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Chip, {
|
|
2659
|
+
icon: "🌿",
|
|
2660
|
+
tone: void 0,
|
|
2661
|
+
children: ["Worktree · ", task.branch.length > 28 ? `${task.branch.slice(0, 28)}…` : task.branch]
|
|
2662
|
+
}),
|
|
2663
|
+
(task.isolation === void 0 || task.isolation === "worktree") && task.branch === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chip, {
|
|
2664
|
+
icon: "🌿",
|
|
2665
|
+
children: "Worktree 隔离"
|
|
2666
|
+
}),
|
|
1742
2667
|
holder !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Chip, {
|
|
1743
2668
|
icon: stale ? "⏱" : "🔑",
|
|
1744
2669
|
tone: stale ? "urgent" : void 0,
|
|
@@ -1782,6 +2707,24 @@ window.__ModuleLoader__.load({
|
|
|
1782
2707
|
onClick: () => void controller.duplicate(task),
|
|
1783
2708
|
children: "⧉ 复制"
|
|
1784
2709
|
}),
|
|
2710
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2711
|
+
type: "button",
|
|
2712
|
+
className: "dsh-atb-detail-edit",
|
|
2713
|
+
title: "把此任务的配置(含清单)保存为模板,新建任务时可用",
|
|
2714
|
+
onClick: () => {
|
|
2715
|
+
controller.saveAsTemplate(task).then((ok) => {
|
|
2716
|
+
if (ok) showAlert("已存为模板(新建任务 ▼ 下拉可用,可在模板管理中改名)");
|
|
2717
|
+
});
|
|
2718
|
+
},
|
|
2719
|
+
children: "⌗ 存为模板"
|
|
2720
|
+
}),
|
|
2721
|
+
canRun && task.branch !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2722
|
+
type: "button",
|
|
2723
|
+
className: "dsh-atb-detail-run",
|
|
2724
|
+
title: "续跑:保留现有 worktree 与分支(上次的改动和提交都在原处),在其上继续执行;默认「立即执行」会重置为全新基线",
|
|
2725
|
+
onClick: () => void controller.run(task.id, true),
|
|
2726
|
+
children: "↻ 续跑"
|
|
2727
|
+
}),
|
|
1785
2728
|
canRun && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1786
2729
|
type: "button",
|
|
1787
2730
|
className: "dsh-atb-detail-run",
|
|
@@ -1852,6 +2795,15 @@ window.__ModuleLoader__.load({
|
|
|
1852
2795
|
children: task.prompt
|
|
1853
2796
|
})]
|
|
1854
2797
|
}),
|
|
2798
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IsolationBlock, {
|
|
2799
|
+
task,
|
|
2800
|
+
controller
|
|
2801
|
+
}),
|
|
2802
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReportBlock, { task }),
|
|
2803
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChecklistBlock, {
|
|
2804
|
+
task,
|
|
2805
|
+
controller
|
|
2806
|
+
}),
|
|
1855
2807
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1856
2808
|
className: "dsh-atb-detail-actions",
|
|
1857
2809
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -1862,7 +2814,8 @@ window.__ModuleLoader__.load({
|
|
|
1862
2814
|
children: [
|
|
1863
2815
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1864
2816
|
className: "dsh-atb-confirm-label",
|
|
1865
|
-
|
|
2817
|
+
"data-tone": unchecked > 0 ? "bad" : void 0,
|
|
2818
|
+
children: unchecked > 0 ? `仍有 ${unchecked} 项清单未勾选,确认完成?` : "确认完成?"
|
|
1866
2819
|
}),
|
|
1867
2820
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1868
2821
|
type: "button",
|
|
@@ -2136,24 +3089,94 @@ window.__ModuleLoader__.load({
|
|
|
2136
3089
|
});
|
|
2137
3090
|
}
|
|
2138
3091
|
/**
|
|
2139
|
-
* The
|
|
2140
|
-
*
|
|
2141
|
-
*
|
|
3092
|
+
* The checklist (DoD) editor: toggle + text + remove per row, add button,
|
|
3093
|
+
* cap-enforced. Edit mode preserves checked state and notes (the GUI
|
|
3094
|
+
* replaces the whole list on save).
|
|
3095
|
+
*/
|
|
3096
|
+
function ChecklistEditor({ rows, onChange, editing }) {
|
|
3097
|
+
const setRow = (index, patch) => {
|
|
3098
|
+
onChange(rows.map((row, i) => i === index ? {
|
|
3099
|
+
...row,
|
|
3100
|
+
...patch
|
|
3101
|
+
} : row));
|
|
3102
|
+
};
|
|
3103
|
+
const checked = rows.filter((r) => r.checked).length;
|
|
3104
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3105
|
+
className: "dsh-atb-cke",
|
|
3106
|
+
children: [
|
|
3107
|
+
rows.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3108
|
+
className: "dsh-atb-cke-row",
|
|
3109
|
+
children: [
|
|
3110
|
+
editing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3111
|
+
type: "checkbox",
|
|
3112
|
+
className: "dsh-atb-cke-box",
|
|
3113
|
+
checked: row.checked,
|
|
3114
|
+
title: `勾选状态随保存保留(当前勾选人:${row.checkedBy ?? "未勾选"})`,
|
|
3115
|
+
onChange: (e) => setRow(index, { checked: e.target.checked })
|
|
3116
|
+
}),
|
|
3117
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3118
|
+
className: "dsh-atb-cke-text",
|
|
3119
|
+
value: row.text,
|
|
3120
|
+
maxLength: 200,
|
|
3121
|
+
placeholder: `验收项 ${index + 1}(完成标准)`,
|
|
3122
|
+
spellCheck: false,
|
|
3123
|
+
onChange: (e) => setRow(index, { text: e.target.value })
|
|
3124
|
+
}),
|
|
3125
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3126
|
+
type: "button",
|
|
3127
|
+
className: "dsh-atb-cke-del",
|
|
3128
|
+
title: "删除该验收项",
|
|
3129
|
+
onClick: () => onChange(rows.filter((_, i) => i !== index)),
|
|
3130
|
+
children: "✕"
|
|
3131
|
+
})
|
|
3132
|
+
]
|
|
3133
|
+
}, row.id ?? `new-${index}`)),
|
|
3134
|
+
rows.length < 30 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3135
|
+
type: "button",
|
|
3136
|
+
className: "dsh-atb-cke-add",
|
|
3137
|
+
onClick: () => onChange([...rows, {
|
|
3138
|
+
text: "",
|
|
3139
|
+
checked: false
|
|
3140
|
+
}]),
|
|
3141
|
+
children: "+ 添加验收项"
|
|
3142
|
+
}),
|
|
3143
|
+
rows.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3144
|
+
className: "dsh-atb-cke-hint",
|
|
3145
|
+
children: editing ? `已勾选 ${checked}/${rows.length}(保存将整体覆盖清单,勾选状态保留)` : `共 ${rows.length} 项,执行会话按清单干活并逐项勾选,未完成项验收时高亮`
|
|
3146
|
+
})
|
|
3147
|
+
]
|
|
3148
|
+
});
|
|
3149
|
+
}
|
|
3150
|
+
/**
|
|
3151
|
+
* The form modal. Without `task` it composes a new task (optionally
|
|
3152
|
+
* prefilled from a chosen template); with `task` it edits that record
|
|
3153
|
+
* (project, urgency, execution, model included — the GUI is the owner
|
|
3154
|
+
* surface).
|
|
2142
3155
|
* @param controller - the controller.
|
|
2143
3156
|
* @param task - the task being edited (create mode when absent).
|
|
2144
3157
|
*/
|
|
2145
3158
|
function TaskFormModal({ controller, task }) {
|
|
2146
3159
|
const state = controller.getSnapshot();
|
|
3160
|
+
const prefill = state.templatePrefill;
|
|
2147
3161
|
const editing = task !== void 0;
|
|
2148
|
-
const [title, setTitle] = (0, react.useState)(task?.title ?? "");
|
|
2149
|
-
const [description, setDescription] = (0, react.useState)(task?.description ?? "");
|
|
2150
|
-
const [prompt, setPrompt] = (0, react.useState)(task?.prompt ?? "");
|
|
3162
|
+
const [title, setTitle] = (0, react.useState)(task?.title ?? prefill?.title ?? "");
|
|
3163
|
+
const [description, setDescription] = (0, react.useState)(task?.description ?? prefill?.description ?? "");
|
|
3164
|
+
const [prompt, setPrompt] = (0, react.useState)(task?.prompt ?? prefill?.prompt ?? "");
|
|
2151
3165
|
const [workspaceId, setWorkspaceId] = (0, react.useState)(task?.workspaceId ?? state.filters.workspaceId ?? state.workspaces[0]?.id ?? "");
|
|
2152
|
-
const [urgency, setUrgency] = (0, react.useState)(task?.urgency ?? "normal");
|
|
2153
|
-
const [mode, setMode] = (0, react.useState)(task?.execution.mode === "scheduled" ? "scheduled" : "claim");
|
|
2154
|
-
const [cron, setCron] = (0, react.useState)(task?.execution.cron ?? "0 9 * * *");
|
|
3166
|
+
const [urgency, setUrgency] = (0, react.useState)(task?.urgency ?? (prefill?.urgency === "urgent" || prefill?.urgency === "relaxed" ? prefill.urgency : "normal"));
|
|
3167
|
+
const [mode, setMode] = (0, react.useState)(task?.execution.mode === "scheduled" || prefill?.execution?.mode === "scheduled" ? "scheduled" : "claim");
|
|
3168
|
+
const [cron, setCron] = (0, react.useState)(task?.execution.cron ?? prefill?.execution?.cron ?? "0 9 * * *");
|
|
2155
3169
|
const [catalog, setCatalog] = (0, react.useState)([]);
|
|
2156
|
-
const [model, setModel] = (0, react.useState)(task?.model !== void 0 ? JSON.stringify(task
|
|
3170
|
+
const [model, setModel] = (0, react.useState)(task?.model !== void 0 || prefill?.model !== void 0 ? JSON.stringify(task?.model ?? prefill?.model) : "");
|
|
3171
|
+
const initialPreset = task?.presetId ?? prefill?.presetId ?? "";
|
|
3172
|
+
const [presetId, setPresetId] = (0, react.useState)(initialPreset);
|
|
3173
|
+
const [presets, setPresets] = (0, react.useState)([]);
|
|
3174
|
+
const [presetDefault, setPresetDefault] = (0, react.useState)(void 0);
|
|
3175
|
+
const [isolation, setIsolation] = (0, react.useState)(task?.isolation ?? (prefill?.isolation === "none" ? "none" : prefill?.isolation === "worktree" ? "worktree" : loadDefaultIsolation()));
|
|
3176
|
+
const [checkRows, setCheckRows] = (0, react.useState)(task?.checklist !== void 0 && task.checklist.length > 0 ? task.checklist.map((i) => ({ ...i })) : (prefill?.checklist ?? []).map((text) => ({
|
|
3177
|
+
text,
|
|
3178
|
+
checked: false
|
|
3179
|
+
})));
|
|
2157
3180
|
const titleRef = (0, react.useRef)(null);
|
|
2158
3181
|
(0, react.useEffect)(() => {
|
|
2159
3182
|
titleRef.current?.focus();
|
|
@@ -2168,14 +3191,46 @@ window.__ModuleLoader__.load({
|
|
|
2168
3191
|
if (face === void 0) return;
|
|
2169
3192
|
face().then(setCatalog).catch(() => setCatalog([]));
|
|
2170
3193
|
}, [controller]);
|
|
3194
|
+
(0, react.useEffect)(() => {
|
|
3195
|
+
const face = controller.presetCatalog;
|
|
3196
|
+
if (face === void 0) return;
|
|
3197
|
+
face().then((roster) => {
|
|
3198
|
+
setPresets(roster.presets);
|
|
3199
|
+
setPresetDefault(roster.defaultId);
|
|
3200
|
+
if (task?.presetId === void 0 && initialPreset === "" && roster.defaultId !== void 0) setPresetId(roster.defaultId);
|
|
3201
|
+
}).catch(() => setPresets([]));
|
|
3202
|
+
}, [
|
|
3203
|
+
controller,
|
|
3204
|
+
task?.presetId,
|
|
3205
|
+
initialPreset
|
|
3206
|
+
]);
|
|
2171
3207
|
const cronMatch = mode === "scheduled" ? parseCron(cron.trim()) : null;
|
|
2172
3208
|
const nextRun = cronMatch !== null ? nextCronTime(cronMatch, Date.now()) : null;
|
|
2173
3209
|
const cronBad = mode === "scheduled" && (cronMatch === null || nextRun === null);
|
|
2174
3210
|
const valid = title.trim().length > 0 && workspaceId !== "" && !cronBad;
|
|
2175
3211
|
const runBlocked = editing && task.status === "in_progress";
|
|
3212
|
+
const isolationLocked = editing && ((task.executions?.length ?? 0) > 0 || task.status === "in_progress");
|
|
3213
|
+
const gitOk = controller.gitAvailable(workspaceId);
|
|
3214
|
+
const isolationDisabled = isolationLocked || !gitOk;
|
|
3215
|
+
/** Isolation payload for submit: undefined keeps the default (degrades naturally). */
|
|
3216
|
+
const isolationPayload = () => {
|
|
3217
|
+
if (!gitOk) return void 0;
|
|
3218
|
+
if (!editing) saveDefaultIsolation(isolation);
|
|
3219
|
+
return isolation;
|
|
3220
|
+
};
|
|
3221
|
+
/** Preset payload: '' = follow the deployment default (submit omits). */
|
|
3222
|
+
const presetPayload = () => presetId.trim().length > 0 ? presetId.trim() : void 0;
|
|
3223
|
+
/** Checklist rows with non-empty text (blank rows are dropped on submit). */
|
|
3224
|
+
const filledRows = () => checkRows.map((r) => ({
|
|
3225
|
+
...r,
|
|
3226
|
+
text: r.text.trim()
|
|
3227
|
+
})).filter((r) => r.text.length > 0);
|
|
2176
3228
|
const submit = () => {
|
|
2177
3229
|
if (!valid) return;
|
|
2178
3230
|
const picked = model !== "" ? JSON.parse(model) : void 0;
|
|
3231
|
+
const isolationOut = isolationPayload();
|
|
3232
|
+
const presetOut = presetPayload();
|
|
3233
|
+
const rows = filledRows();
|
|
2179
3234
|
if (editing) controller.update(task.id, task.version, {
|
|
2180
3235
|
title,
|
|
2181
3236
|
description,
|
|
@@ -2186,7 +3241,10 @@ window.__ModuleLoader__.load({
|
|
|
2186
3241
|
mode,
|
|
2187
3242
|
cron: cron.trim()
|
|
2188
3243
|
} : { mode },
|
|
2189
|
-
model: picked ?? null
|
|
3244
|
+
model: picked ?? null,
|
|
3245
|
+
...isolationOut !== void 0 && !isolationLocked ? { isolation: isolationOut } : {},
|
|
3246
|
+
presetId: presetOut ?? null,
|
|
3247
|
+
checklist: rows.length > 0 ? rows : null
|
|
2190
3248
|
});
|
|
2191
3249
|
else controller.create({
|
|
2192
3250
|
title,
|
|
@@ -2198,13 +3256,19 @@ window.__ModuleLoader__.load({
|
|
|
2198
3256
|
mode,
|
|
2199
3257
|
cron: cron.trim()
|
|
2200
3258
|
} : { mode },
|
|
2201
|
-
model: picked
|
|
3259
|
+
model: picked,
|
|
3260
|
+
...isolationOut !== void 0 ? { isolation: isolationOut } : {},
|
|
3261
|
+
...presetOut !== void 0 ? { presetId: presetOut } : {},
|
|
3262
|
+
...rows.length > 0 ? { checklist: rows.map((r) => r.text) } : {}
|
|
2202
3263
|
});
|
|
2203
3264
|
};
|
|
2204
3265
|
/** Save the form, then immediately trigger a manual run of the task. */
|
|
2205
3266
|
const submitAndRun = () => {
|
|
2206
3267
|
if (!valid || runBlocked) return;
|
|
2207
3268
|
const picked = model !== "" ? JSON.parse(model) : void 0;
|
|
3269
|
+
const isolationOut = isolationPayload();
|
|
3270
|
+
const presetOut = presetPayload();
|
|
3271
|
+
const rows = filledRows();
|
|
2208
3272
|
if (editing) (async () => {
|
|
2209
3273
|
if (await controller.update(task.id, task.version, {
|
|
2210
3274
|
title,
|
|
@@ -2216,7 +3280,10 @@ window.__ModuleLoader__.load({
|
|
|
2216
3280
|
mode,
|
|
2217
3281
|
cron: cron.trim()
|
|
2218
3282
|
} : { mode },
|
|
2219
|
-
model: picked ?? null
|
|
3283
|
+
model: picked ?? null,
|
|
3284
|
+
...isolationOut !== void 0 && !isolationLocked ? { isolation: isolationOut } : {},
|
|
3285
|
+
presetId: presetOut ?? null,
|
|
3286
|
+
checklist: rows.length > 0 ? rows : null
|
|
2220
3287
|
})) await controller.run(task.id);
|
|
2221
3288
|
})();
|
|
2222
3289
|
else (async () => {
|
|
@@ -2230,7 +3297,10 @@ window.__ModuleLoader__.load({
|
|
|
2230
3297
|
mode,
|
|
2231
3298
|
cron: cron.trim()
|
|
2232
3299
|
} : { mode },
|
|
2233
|
-
model: picked
|
|
3300
|
+
model: picked,
|
|
3301
|
+
...isolationOut !== void 0 ? { isolation: isolationOut } : {},
|
|
3302
|
+
...presetOut !== void 0 ? { presetId: presetOut } : {},
|
|
3303
|
+
...rows.length > 0 ? { checklist: rows.map((r) => r.text) } : {}
|
|
2234
3304
|
});
|
|
2235
3305
|
if (id !== void 0) await controller.run(id);
|
|
2236
3306
|
})();
|
|
@@ -2317,6 +3387,21 @@ window.__ModuleLoader__.load({
|
|
|
2317
3387
|
}, `${m.provider}/${m.model}`))]
|
|
2318
3388
|
})
|
|
2319
3389
|
}),
|
|
3390
|
+
presets.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
3391
|
+
label: "执行模式(preset)",
|
|
3392
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
3393
|
+
value: presetId,
|
|
3394
|
+
onChange: (e) => setPresetId(e.target.value),
|
|
3395
|
+
title: "执行会话按该 preset 组合(决定工具集与人设);默认 = 部署默认 preset",
|
|
3396
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
3397
|
+
value: "",
|
|
3398
|
+
children: ["跟随部署默认", presetDefault !== void 0 ? `(当前:${presets.find((p) => p.id === presetDefault)?.name ?? presetDefault})` : ""]
|
|
3399
|
+
}), presets.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
3400
|
+
value: p.id,
|
|
3401
|
+
children: [p.name ?? p.id, p.id === presetDefault ? "(部署默认)" : ""]
|
|
3402
|
+
}, p.id))]
|
|
3403
|
+
})
|
|
3404
|
+
}),
|
|
2320
3405
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2321
3406
|
label: "紧急度",
|
|
2322
3407
|
full: true,
|
|
@@ -2414,6 +3499,55 @@ window.__ModuleLoader__.load({
|
|
|
2414
3499
|
children: ["下次 ", fmtTime(nextRun)]
|
|
2415
3500
|
})]
|
|
2416
3501
|
})]
|
|
3502
|
+
}),
|
|
3503
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Field, {
|
|
3504
|
+
label: "执行隔离",
|
|
3505
|
+
full: true,
|
|
3506
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3507
|
+
className: "dsh-atb-mode-picker",
|
|
3508
|
+
"data-disabled": isolationDisabled ? "true" : void 0,
|
|
3509
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3510
|
+
type: "button",
|
|
3511
|
+
className: "dsh-atb-mode-opt",
|
|
3512
|
+
"data-on": isolation === "worktree",
|
|
3513
|
+
disabled: isolationDisabled,
|
|
3514
|
+
title: isolationLocked ? "任务已有执行记录,隔离方式已锁定" : !gitOk ? "当前项目非 git 仓库" : "每次执行在独立 worktree 分支上进行",
|
|
3515
|
+
onClick: () => setIsolation("worktree"),
|
|
3516
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3517
|
+
className: "dsh-atb-mode-name",
|
|
3518
|
+
children: "🌿 Worktree 隔离"
|
|
3519
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3520
|
+
className: "dsh-atb-mode-hint",
|
|
3521
|
+
children: isolationLocked ? "已锁定(执行开始后不可更改)" : !gitOk ? "当前项目非 git 仓库" : "独立分支 task/标题+ID,互不污染"
|
|
3522
|
+
})]
|
|
3523
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3524
|
+
type: "button",
|
|
3525
|
+
className: "dsh-atb-mode-opt",
|
|
3526
|
+
"data-on": isolation === "none",
|
|
3527
|
+
disabled: isolationDisabled,
|
|
3528
|
+
title: isolationLocked ? "任务已有执行记录,隔离方式已锁定" : "直接在项目目录执行(不使用 git)",
|
|
3529
|
+
onClick: () => setIsolation("none"),
|
|
3530
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3531
|
+
className: "dsh-atb-mode-name",
|
|
3532
|
+
children: "📁 原目录执行"
|
|
3533
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3534
|
+
className: "dsh-atb-mode-hint",
|
|
3535
|
+
children: isolationLocked ? "已锁定(执行开始后不可更改)" : !gitOk ? "当前项目非 git 仓库,将在原目录执行" : "不使用 git,直接在项目目录工作"
|
|
3536
|
+
})]
|
|
3537
|
+
})]
|
|
3538
|
+
}), !gitOk && !isolationLocked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3539
|
+
className: "dsh-atb-isolation-note",
|
|
3540
|
+
children: "当前项目非 git 仓库,将在原目录执行(任务仍按默认配置创建,运行时自动降级)"
|
|
3541
|
+
})]
|
|
3542
|
+
}),
|
|
3543
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
3544
|
+
label: editing ? "验收清单(DoD)" : "验收清单(DoD,可选)",
|
|
3545
|
+
full: true,
|
|
3546
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChecklistEditor, {
|
|
3547
|
+
rows: checkRows,
|
|
3548
|
+
onChange: setCheckRows,
|
|
3549
|
+
editing
|
|
3550
|
+
})
|
|
2417
3551
|
})
|
|
2418
3552
|
]
|
|
2419
3553
|
}),
|
|
@@ -2456,6 +3590,431 @@ window.__ModuleLoader__.load({
|
|
|
2456
3590
|
});
|
|
2457
3591
|
}
|
|
2458
3592
|
|
|
3593
|
+
//#endregion
|
|
3594
|
+
//#region src/client/board/ImportModal.tsx
|
|
3595
|
+
/**
|
|
3596
|
+
* The ledger-import modal (0.4.0): pick a JSON file → dry-run preview
|
|
3597
|
+
* (create / overwrite / invalid classification) → commit as merge or
|
|
3598
|
+
* replace. Replace swaps the WHOLE ledger after an automatic backup and a
|
|
3599
|
+
* double confirmation. Files exported by ⬇ JSON import as-is.
|
|
3600
|
+
*
|
|
3601
|
+
* @module dsh-taskboard/client/board/ImportModal
|
|
3602
|
+
*/
|
|
3603
|
+
/** One classified row (create / overwrite). */
|
|
3604
|
+
function PlanRow({ row }) {
|
|
3605
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3606
|
+
className: "dsh-atb-imp-row",
|
|
3607
|
+
title: row.id,
|
|
3608
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3609
|
+
className: "dsh-atb-imp-row-title",
|
|
3610
|
+
children: row.title
|
|
3611
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3612
|
+
className: "dsh-atb-imp-row-status",
|
|
3613
|
+
children: row.status
|
|
3614
|
+
})]
|
|
3615
|
+
});
|
|
3616
|
+
}
|
|
3617
|
+
/**
|
|
3618
|
+
* The import modal.
|
|
3619
|
+
* @param controller - the controller.
|
|
3620
|
+
*/
|
|
3621
|
+
function ImportModal({ controller }) {
|
|
3622
|
+
const [fileName, setFileName] = (0, react.useState)("");
|
|
3623
|
+
const [parsed, setParsed] = (0, react.useState)(null);
|
|
3624
|
+
const [parseError, setParseError] = (0, react.useState)(void 0);
|
|
3625
|
+
const [plan, setPlan] = (0, react.useState)(void 0);
|
|
3626
|
+
const [mode, setMode] = (0, react.useState)("merge");
|
|
3627
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
3628
|
+
const [result, setResult] = (0, react.useState)(void 0);
|
|
3629
|
+
const [confirmReplace, setConfirmReplace] = (0, react.useState)(false);
|
|
3630
|
+
const fileRef = (0, react.useRef)(null);
|
|
3631
|
+
const { alert: showAlert, el: alertEl } = useAlert();
|
|
3632
|
+
/** Read + parse the picked file, then dry-run the preview. */
|
|
3633
|
+
const onFile = (file) => {
|
|
3634
|
+
setPlan(void 0);
|
|
3635
|
+
setParseError(void 0);
|
|
3636
|
+
setResult(void 0);
|
|
3637
|
+
setConfirmReplace(false);
|
|
3638
|
+
setFileName("");
|
|
3639
|
+
setParsed(null);
|
|
3640
|
+
if (file === void 0) return;
|
|
3641
|
+
file.text().then((text) => {
|
|
3642
|
+
try {
|
|
3643
|
+
const value = JSON.parse(text);
|
|
3644
|
+
setParsed(value);
|
|
3645
|
+
setFileName(file.name);
|
|
3646
|
+
controller.importPreview(value).then((p) => {
|
|
3647
|
+
if (p !== void 0) setPlan(p);
|
|
3648
|
+
});
|
|
3649
|
+
} catch {
|
|
3650
|
+
setParseError("文件不是合法 JSON");
|
|
3651
|
+
}
|
|
3652
|
+
});
|
|
3653
|
+
};
|
|
3654
|
+
/** Commit the import (replace requires the inline double confirmation). */
|
|
3655
|
+
const commit = () => {
|
|
3656
|
+
if (parsed === null || plan === void 0 || busy) return;
|
|
3657
|
+
if (mode === "replace" && !confirmReplace) {
|
|
3658
|
+
setConfirmReplace(true);
|
|
3659
|
+
return;
|
|
3660
|
+
}
|
|
3661
|
+
setBusy(true);
|
|
3662
|
+
controller.importCommit(mode, parsed).then((r) => {
|
|
3663
|
+
setBusy(false);
|
|
3664
|
+
setConfirmReplace(false);
|
|
3665
|
+
if (r === void 0) return;
|
|
3666
|
+
setResult(r.mode === "replace" ? `整册替换完成:导入 ${r.created + r.overwritten} 张(原 ${r.replacedTotal} 张已整册备份)` : `合并完成:新增 ${r.created} 张、覆盖 ${r.overwritten} 张`);
|
|
3667
|
+
});
|
|
3668
|
+
};
|
|
3669
|
+
const close = () => controller.closeImport();
|
|
3670
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3671
|
+
className: "dsh-atb-modal-backdrop",
|
|
3672
|
+
onClick: (e) => {
|
|
3673
|
+
if (e.target === e.currentTarget) close();
|
|
3674
|
+
},
|
|
3675
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3676
|
+
className: "dsh-atb-modal dsh-atb-imp",
|
|
3677
|
+
role: "dialog",
|
|
3678
|
+
"aria-modal": "true",
|
|
3679
|
+
"aria-label": "导入台账",
|
|
3680
|
+
children: [
|
|
3681
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3682
|
+
className: "dsh-atb-modal-head",
|
|
3683
|
+
children: [
|
|
3684
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3685
|
+
className: "dsh-atb-modal-headicon",
|
|
3686
|
+
children: "⬆"
|
|
3687
|
+
}),
|
|
3688
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3689
|
+
className: "dsh-atb-modal-headtext",
|
|
3690
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "导入台账" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "选择导出的 JSON 备份文件:先预览、再合并或整册替换" })]
|
|
3691
|
+
}),
|
|
3692
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3693
|
+
type: "button",
|
|
3694
|
+
className: "dsh-atb-modal-close",
|
|
3695
|
+
"aria-label": "关闭",
|
|
3696
|
+
onClick: close,
|
|
3697
|
+
children: "✕"
|
|
3698
|
+
})
|
|
3699
|
+
]
|
|
3700
|
+
}),
|
|
3701
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3702
|
+
className: "dsh-atb-modal-body",
|
|
3703
|
+
children: [
|
|
3704
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3705
|
+
className: "dsh-atb-imp-picker",
|
|
3706
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3707
|
+
ref: fileRef,
|
|
3708
|
+
type: "file",
|
|
3709
|
+
accept: ".json,application/json",
|
|
3710
|
+
onChange: (e) => onFile(e.target.files?.[0])
|
|
3711
|
+
}), fileName.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3712
|
+
className: "dsh-atb-imp-filename",
|
|
3713
|
+
children: fileName
|
|
3714
|
+
})]
|
|
3715
|
+
}),
|
|
3716
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3717
|
+
className: "dsh-atb-imp-note",
|
|
3718
|
+
children: "⬇ JSON 导出的文件即为同格式备份,可直接导入恢复;导入文件的 schemaVersion 必须与当前版本一致。"
|
|
3719
|
+
}),
|
|
3720
|
+
parseError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3721
|
+
className: "dsh-atb-imp-error",
|
|
3722
|
+
children: parseError
|
|
3723
|
+
}),
|
|
3724
|
+
plan === void 0 && parseError === void 0 && fileName.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3725
|
+
className: "dsh-atb-empty2",
|
|
3726
|
+
children: "预览中…"
|
|
3727
|
+
}),
|
|
3728
|
+
plan !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
3729
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3730
|
+
className: "dsh-atb-imp-stats",
|
|
3731
|
+
children: [
|
|
3732
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3733
|
+
className: "dsh-atb-imp-stat",
|
|
3734
|
+
"data-tone": "ok",
|
|
3735
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: plan.create.length }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "新增" })]
|
|
3736
|
+
}),
|
|
3737
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3738
|
+
className: "dsh-atb-imp-stat",
|
|
3739
|
+
"data-tone": "warn",
|
|
3740
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: plan.overwrite.length }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "覆盖(同 id)" })]
|
|
3741
|
+
}),
|
|
3742
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3743
|
+
className: "dsh-atb-imp-stat",
|
|
3744
|
+
"data-tone": plan.invalid.length > 0 ? "bad" : void 0,
|
|
3745
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: plan.invalid.length }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "无效(跳过)" })]
|
|
3746
|
+
})
|
|
3747
|
+
]
|
|
3748
|
+
}),
|
|
3749
|
+
plan.create.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3750
|
+
className: "dsh-atb-imp-sec",
|
|
3751
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: "新增任务" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3752
|
+
className: "dsh-atb-imp-list",
|
|
3753
|
+
children: plan.create.map((r) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlanRow, { row: r }, r.id))
|
|
3754
|
+
})]
|
|
3755
|
+
}),
|
|
3756
|
+
plan.overwrite.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3757
|
+
className: "dsh-atb-imp-sec",
|
|
3758
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: "覆盖任务(整卡替换,含执行历史与评论)" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3759
|
+
className: "dsh-atb-imp-list",
|
|
3760
|
+
children: plan.overwrite.map((r) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlanRow, { row: r }, r.id))
|
|
3761
|
+
})]
|
|
3762
|
+
}),
|
|
3763
|
+
plan.invalid.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3764
|
+
className: "dsh-atb-imp-sec",
|
|
3765
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: "无效条目(不会导入)" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3766
|
+
className: "dsh-atb-imp-list",
|
|
3767
|
+
children: plan.invalid.map((r, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3768
|
+
className: "dsh-atb-imp-row",
|
|
3769
|
+
"data-tone": "bad",
|
|
3770
|
+
title: r.id ?? "",
|
|
3771
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3772
|
+
className: "dsh-atb-imp-row-title",
|
|
3773
|
+
children: r.id ?? "(无 id)"
|
|
3774
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3775
|
+
className: "dsh-atb-imp-row-status",
|
|
3776
|
+
children: r.reason
|
|
3777
|
+
})]
|
|
3778
|
+
}, r.id ?? `invalid-${i}`))
|
|
3779
|
+
})]
|
|
3780
|
+
}),
|
|
3781
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3782
|
+
className: "dsh-atb-mode-picker",
|
|
3783
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3784
|
+
type: "button",
|
|
3785
|
+
className: "dsh-atb-mode-opt",
|
|
3786
|
+
"data-on": mode === "merge",
|
|
3787
|
+
onClick: () => {
|
|
3788
|
+
setMode("merge");
|
|
3789
|
+
setConfirmReplace(false);
|
|
3790
|
+
},
|
|
3791
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3792
|
+
className: "dsh-atb-mode-name",
|
|
3793
|
+
children: "⊕ 合并"
|
|
3794
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3795
|
+
className: "dsh-atb-mode-hint",
|
|
3796
|
+
children: "新增 + 按 id 覆盖,其余不动"
|
|
3797
|
+
})]
|
|
3798
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3799
|
+
type: "button",
|
|
3800
|
+
className: "dsh-atb-mode-opt",
|
|
3801
|
+
"data-on": mode === "replace",
|
|
3802
|
+
onClick: () => setMode("replace"),
|
|
3803
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3804
|
+
className: "dsh-atb-mode-name",
|
|
3805
|
+
children: "💣 整册替换"
|
|
3806
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3807
|
+
className: "dsh-atb-mode-hint",
|
|
3808
|
+
children: "清空当前台账,以导入文件为准(先自动备份)"
|
|
3809
|
+
})]
|
|
3810
|
+
})]
|
|
3811
|
+
}),
|
|
3812
|
+
result !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3813
|
+
className: "dsh-atb-imp-result",
|
|
3814
|
+
children: result
|
|
3815
|
+
})
|
|
3816
|
+
] })
|
|
3817
|
+
]
|
|
3818
|
+
}),
|
|
3819
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3820
|
+
className: "dsh-atb-modal-foot",
|
|
3821
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3822
|
+
className: "dsh-atb-modal-hint",
|
|
3823
|
+
children: mode === "replace" ? confirmReplace ? "⚠ 再次点击确认执行整册替换(不可撤销,已自动备份)" : "整册替换需要二次确认" : "合并只写入预览中列出的任务"
|
|
3824
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3825
|
+
className: "dsh-atb-modal-footbtns",
|
|
3826
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3827
|
+
type: "button",
|
|
3828
|
+
className: "dsh-atb-btn",
|
|
3829
|
+
onClick: close,
|
|
3830
|
+
children: result !== void 0 ? "关闭" : "取消"
|
|
3831
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3832
|
+
type: "button",
|
|
3833
|
+
className: "dsh-atb-btn",
|
|
3834
|
+
"data-primary": "true",
|
|
3835
|
+
"data-danger": mode === "replace" && confirmReplace ? "true" : void 0,
|
|
3836
|
+
disabled: plan === void 0 || busy || result !== void 0 && false,
|
|
3837
|
+
onClick: commit,
|
|
3838
|
+
children: mode === "replace" && confirmReplace ? "确认整册替换" : "执行导入"
|
|
3839
|
+
})]
|
|
3840
|
+
})]
|
|
3841
|
+
})
|
|
3842
|
+
]
|
|
3843
|
+
}), alertEl]
|
|
3844
|
+
});
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
//#endregion
|
|
3848
|
+
//#region src/client/board/TemplateManager.tsx
|
|
3849
|
+
/**
|
|
3850
|
+
* The template-manager modal (0.4.0): rename / delete / use the stored task
|
|
3851
|
+
* templates. Templates live host-side (side file next to the ledger) and
|
|
3852
|
+
* prefill the create form from the + 新建任务 ▼ dropdown.
|
|
3853
|
+
*
|
|
3854
|
+
* @module dsh-taskboard/client/board/TemplateManager
|
|
3855
|
+
*/
|
|
3856
|
+
/**
|
|
3857
|
+
* The template manager modal.
|
|
3858
|
+
* @param controller - the controller.
|
|
3859
|
+
*/
|
|
3860
|
+
function TemplateManager({ controller }) {
|
|
3861
|
+
const state = controller.getSnapshot();
|
|
3862
|
+
const [edits, setEdits] = (0, react.useState)({});
|
|
3863
|
+
const [confirmId, setConfirmId] = (0, react.useState)(void 0);
|
|
3864
|
+
const { alert: showAlert, el: alertEl } = useAlert();
|
|
3865
|
+
const close = () => controller.closeTemplateManager();
|
|
3866
|
+
const nameOf = (id, fallback) => edits[id] ?? fallback;
|
|
3867
|
+
/** Save one template's rename. */
|
|
3868
|
+
const save = (id, name) => {
|
|
3869
|
+
const template = state.templates.find((t) => t.id === id);
|
|
3870
|
+
if (template === void 0 || name === template.name) return;
|
|
3871
|
+
controller.upsertTemplate({
|
|
3872
|
+
id,
|
|
3873
|
+
name,
|
|
3874
|
+
task: template.task
|
|
3875
|
+
}).then((ok) => {
|
|
3876
|
+
if (ok) {
|
|
3877
|
+
setEdits((prev) => {
|
|
3878
|
+
const next = { ...prev };
|
|
3879
|
+
delete next[id];
|
|
3880
|
+
return next;
|
|
3881
|
+
});
|
|
3882
|
+
showAlert("模板已改名");
|
|
3883
|
+
}
|
|
3884
|
+
});
|
|
3885
|
+
};
|
|
3886
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3887
|
+
className: "dsh-atb-modal-backdrop",
|
|
3888
|
+
onClick: (e) => {
|
|
3889
|
+
if (e.target === e.currentTarget) close();
|
|
3890
|
+
},
|
|
3891
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3892
|
+
className: "dsh-atb-modal dsh-atb-tplm",
|
|
3893
|
+
role: "dialog",
|
|
3894
|
+
"aria-modal": "true",
|
|
3895
|
+
"aria-label": "管理模板",
|
|
3896
|
+
children: [
|
|
3897
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3898
|
+
className: "dsh-atb-modal-head",
|
|
3899
|
+
children: [
|
|
3900
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3901
|
+
className: "dsh-atb-modal-headicon",
|
|
3902
|
+
children: "⌗"
|
|
3903
|
+
}),
|
|
3904
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3905
|
+
className: "dsh-atb-modal-headtext",
|
|
3906
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "任务模板" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "新建任务 ▼ 下拉的模板:改名 / 删除 / 直接使用;任务详情页「存为模板」可新增" })]
|
|
3907
|
+
}),
|
|
3908
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3909
|
+
type: "button",
|
|
3910
|
+
className: "dsh-atb-modal-close",
|
|
3911
|
+
"aria-label": "关闭",
|
|
3912
|
+
onClick: close,
|
|
3913
|
+
children: "✕"
|
|
3914
|
+
})
|
|
3915
|
+
]
|
|
3916
|
+
}),
|
|
3917
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3918
|
+
className: "dsh-atb-modal-body",
|
|
3919
|
+
children: state.templates.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3920
|
+
className: "dsh-atb-empty2",
|
|
3921
|
+
children: "暂无模板 — 在任务详情页点「存为模板」把常用配置沉淀下来"
|
|
3922
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3923
|
+
className: "dsh-atb-tplm-list",
|
|
3924
|
+
children: state.templates.map((t) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3925
|
+
className: "dsh-atb-tplm-row",
|
|
3926
|
+
children: [
|
|
3927
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3928
|
+
className: "dsh-atb-tplm-name",
|
|
3929
|
+
value: nameOf(t.id, t.name),
|
|
3930
|
+
maxLength: 60,
|
|
3931
|
+
spellCheck: false,
|
|
3932
|
+
"aria-label": `模板名 ${t.name}`,
|
|
3933
|
+
onChange: (e) => setEdits((prev) => ({
|
|
3934
|
+
...prev,
|
|
3935
|
+
[t.id]: e.target.value
|
|
3936
|
+
})),
|
|
3937
|
+
onKeyDown: (e) => {
|
|
3938
|
+
if (e.key === "Enter") save(t.id, nameOf(t.id, t.name));
|
|
3939
|
+
}
|
|
3940
|
+
}),
|
|
3941
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3942
|
+
className: "dsh-atb-tplm-meta",
|
|
3943
|
+
children: [
|
|
3944
|
+
t.builtin === true ? "内置" : "自建",
|
|
3945
|
+
t.task.checklist !== void 0 && t.task.checklist.length > 0 ? ` · 清单 ${t.task.checklist.length} 项` : "",
|
|
3946
|
+
t.task.urgency !== void 0 ? ` · ${t.task.urgency}` : ""
|
|
3947
|
+
]
|
|
3948
|
+
}),
|
|
3949
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3950
|
+
className: "dsh-atb-tplm-btns",
|
|
3951
|
+
children: [
|
|
3952
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3953
|
+
type: "button",
|
|
3954
|
+
className: "dsh-atb-btn",
|
|
3955
|
+
disabled: nameOf(t.id, t.name) === t.name || nameOf(t.id, t.name).trim().length === 0,
|
|
3956
|
+
title: "保存改名",
|
|
3957
|
+
onClick: () => save(t.id, nameOf(t.id, t.name)),
|
|
3958
|
+
children: "改名"
|
|
3959
|
+
}),
|
|
3960
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3961
|
+
type: "button",
|
|
3962
|
+
className: "dsh-atb-btn",
|
|
3963
|
+
title: "用此模板打开新建表单",
|
|
3964
|
+
onClick: () => {
|
|
3965
|
+
close();
|
|
3966
|
+
controller.newFromTemplate(t.task);
|
|
3967
|
+
},
|
|
3968
|
+
children: "用此新建"
|
|
3969
|
+
}),
|
|
3970
|
+
confirmId === t.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3971
|
+
type: "button",
|
|
3972
|
+
className: "dsh-atb-btn",
|
|
3973
|
+
"data-danger": "true",
|
|
3974
|
+
onClick: () => {
|
|
3975
|
+
controller.deleteTemplate(t.id);
|
|
3976
|
+
setConfirmId(void 0);
|
|
3977
|
+
},
|
|
3978
|
+
children: "确认删除"
|
|
3979
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3980
|
+
type: "button",
|
|
3981
|
+
className: "dsh-atb-btn",
|
|
3982
|
+
onClick: () => setConfirmId(void 0),
|
|
3983
|
+
children: "取消"
|
|
3984
|
+
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3985
|
+
type: "button",
|
|
3986
|
+
className: "dsh-atb-btn",
|
|
3987
|
+
"data-danger": "true",
|
|
3988
|
+
title: "删除该模板",
|
|
3989
|
+
onClick: () => setConfirmId(t.id),
|
|
3990
|
+
children: "🗑"
|
|
3991
|
+
})
|
|
3992
|
+
]
|
|
3993
|
+
})
|
|
3994
|
+
]
|
|
3995
|
+
}, t.id))
|
|
3996
|
+
})
|
|
3997
|
+
}),
|
|
3998
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3999
|
+
className: "dsh-atb-modal-foot",
|
|
4000
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4001
|
+
className: "dsh-atb-modal-hint",
|
|
4002
|
+
children: "模板随台账一同保存在 DSH 主目录,升级不丢"
|
|
4003
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4004
|
+
className: "dsh-atb-modal-footbtns",
|
|
4005
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4006
|
+
type: "button",
|
|
4007
|
+
className: "dsh-atb-btn",
|
|
4008
|
+
onClick: close,
|
|
4009
|
+
children: "关闭"
|
|
4010
|
+
})
|
|
4011
|
+
})]
|
|
4012
|
+
})
|
|
4013
|
+
]
|
|
4014
|
+
}), alertEl]
|
|
4015
|
+
});
|
|
4016
|
+
}
|
|
4017
|
+
|
|
2459
4018
|
//#endregion
|
|
2460
4019
|
//#region src/client/board/TaskBoard.tsx
|
|
2461
4020
|
/**
|
|
@@ -2524,6 +4083,8 @@ window.__ModuleLoader__.load({
|
|
|
2524
4083
|
const live = filterTasks(state, state.ledger.tasks.filter((t) => t.trashedAt === void 0));
|
|
2525
4084
|
const selected = state.selectedId === void 0 ? void 0 : state.ledger.tasks.find((t) => t.id === state.selectedId);
|
|
2526
4085
|
const { alert: showAlert, el: alertEl } = useAlert();
|
|
4086
|
+
const [newMenuOpen, setNewMenuOpen] = (0, react.useState)(false);
|
|
4087
|
+
const closeMenu = () => setNewMenuOpen(false);
|
|
2527
4088
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2528
4089
|
className: "dsh-atb-board",
|
|
2529
4090
|
children: [
|
|
@@ -2542,12 +4103,55 @@ window.__ModuleLoader__.load({
|
|
|
2542
4103
|
state.ledger.revision
|
|
2543
4104
|
]
|
|
2544
4105
|
}),
|
|
2545
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
4106
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4107
|
+
className: "dsh-atb-newmenu",
|
|
4108
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4109
|
+
type: "button",
|
|
4110
|
+
className: "dsh-atb-btn",
|
|
4111
|
+
"data-primary": "true",
|
|
4112
|
+
onClick: () => {
|
|
4113
|
+
const next = !newMenuOpen;
|
|
4114
|
+
setNewMenuOpen(next);
|
|
4115
|
+
if (next) controller.prepareTemplateMenu();
|
|
4116
|
+
},
|
|
4117
|
+
children: "+ 新建任务 ▼"
|
|
4118
|
+
}), newMenuOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4119
|
+
className: "dsh-atb-newmenu-backdrop",
|
|
4120
|
+
onClick: closeMenu
|
|
4121
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4122
|
+
className: "dsh-atb-newmenu-list",
|
|
4123
|
+
children: [
|
|
4124
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4125
|
+
type: "button",
|
|
4126
|
+
className: "dsh-atb-newmenu-opt",
|
|
4127
|
+
onClick: () => {
|
|
4128
|
+
closeMenu();
|
|
4129
|
+
controller.setComposer(true);
|
|
4130
|
+
},
|
|
4131
|
+
children: "空白任务"
|
|
4132
|
+
}),
|
|
4133
|
+
state.templates.map((t) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
4134
|
+
type: "button",
|
|
4135
|
+
className: "dsh-atb-newmenu-opt",
|
|
4136
|
+
title: t.task.description !== void 0 && t.task.description.length > 0 ? t.task.description.slice(0, 120) : t.name,
|
|
4137
|
+
onClick: () => {
|
|
4138
|
+
closeMenu();
|
|
4139
|
+
controller.newFromTemplate(t.task);
|
|
4140
|
+
},
|
|
4141
|
+
children: [t.name, t.builtin === true ? "" : ""]
|
|
4142
|
+
}, t.id)),
|
|
4143
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "dsh-atb-newmenu-sep" }),
|
|
4144
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4145
|
+
type: "button",
|
|
4146
|
+
className: "dsh-atb-newmenu-opt",
|
|
4147
|
+
onClick: () => {
|
|
4148
|
+
closeMenu();
|
|
4149
|
+
controller.openTemplateManager();
|
|
4150
|
+
},
|
|
4151
|
+
children: "⌗ 管理模板…"
|
|
4152
|
+
})
|
|
4153
|
+
]
|
|
4154
|
+
})] })]
|
|
2551
4155
|
}),
|
|
2552
4156
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "dsh-atb-spacer" }),
|
|
2553
4157
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
@@ -2614,6 +4218,20 @@ window.__ModuleLoader__.load({
|
|
|
2614
4218
|
onClick: () => controller.toggleSecondary(),
|
|
2615
4219
|
children: state.secondaryOpen ? "返回看板" : "其它任务"
|
|
2616
4220
|
}),
|
|
4221
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4222
|
+
type: "button",
|
|
4223
|
+
className: "dsh-atb-btn",
|
|
4224
|
+
title: "健康诊断:遗留 worktree、台账基本项",
|
|
4225
|
+
onClick: () => controller.openDiagnostics(),
|
|
4226
|
+
children: "⚙ 诊断"
|
|
4227
|
+
}),
|
|
4228
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4229
|
+
type: "button",
|
|
4230
|
+
className: "dsh-atb-btn",
|
|
4231
|
+
title: "从 JSON 备份文件导入台账(预览后合并或整册替换)",
|
|
4232
|
+
onClick: () => controller.openImport(),
|
|
4233
|
+
children: "⬆ 导入"
|
|
4234
|
+
}),
|
|
2617
4235
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2618
4236
|
type: "button",
|
|
2619
4237
|
className: "dsh-atb-btn",
|
|
@@ -2714,10 +4332,140 @@ window.__ModuleLoader__.load({
|
|
|
2714
4332
|
controller,
|
|
2715
4333
|
task: state.editingId === void 0 ? void 0 : state.ledger.tasks.find((t) => t.id === state.editingId)
|
|
2716
4334
|
}),
|
|
4335
|
+
state.diagOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiagnosticsPanel, { controller }),
|
|
4336
|
+
state.importOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImportModal, { controller }),
|
|
4337
|
+
state.tplManagerOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TemplateManager, { controller }),
|
|
2717
4338
|
alertEl
|
|
2718
4339
|
]
|
|
2719
4340
|
});
|
|
2720
4341
|
}
|
|
4342
|
+
/** ⚙ Health-diagnostics panel (plan §3.6): ledger basics + orphan worktrees + one-click cleanup. */
|
|
4343
|
+
function DiagnosticsPanel({ controller }) {
|
|
4344
|
+
const state = controller.getSnapshot();
|
|
4345
|
+
const diag = state.diagnostics;
|
|
4346
|
+
const wsName = (id) => {
|
|
4347
|
+
const ws = state.workspaces.find((w) => w.id === id);
|
|
4348
|
+
return ws?.title ?? ws?.path ?? id.slice(0, 8);
|
|
4349
|
+
};
|
|
4350
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4351
|
+
className: "dsh-atb-modal-backdrop",
|
|
4352
|
+
onClick: (e) => {
|
|
4353
|
+
if (e.target === e.currentTarget) controller.closeDiagnostics();
|
|
4354
|
+
},
|
|
4355
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4356
|
+
className: "dsh-atb-modal dsh-atb-diag",
|
|
4357
|
+
role: "dialog",
|
|
4358
|
+
"aria-modal": "true",
|
|
4359
|
+
"aria-label": "健康诊断",
|
|
4360
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4361
|
+
className: "dsh-atb-modal-head",
|
|
4362
|
+
children: [
|
|
4363
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4364
|
+
className: "dsh-atb-modal-headicon",
|
|
4365
|
+
children: "⚙"
|
|
4366
|
+
}),
|
|
4367
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4368
|
+
className: "dsh-atb-modal-headtext",
|
|
4369
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "健康诊断" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "台账基本项与 worktree 遗留清理" })]
|
|
4370
|
+
}),
|
|
4371
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4372
|
+
type: "button",
|
|
4373
|
+
className: "dsh-atb-modal-close",
|
|
4374
|
+
"aria-label": "关闭",
|
|
4375
|
+
onClick: () => controller.closeDiagnostics(),
|
|
4376
|
+
children: "✕"
|
|
4377
|
+
})
|
|
4378
|
+
]
|
|
4379
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4380
|
+
className: "dsh-atb-modal-body",
|
|
4381
|
+
children: diag === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4382
|
+
className: "dsh-atb-empty2",
|
|
4383
|
+
children: "读取中…"
|
|
4384
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
4385
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4386
|
+
className: "dsh-atb-diag-grid",
|
|
4387
|
+
children: [
|
|
4388
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4389
|
+
className: "dsh-atb-diag-item",
|
|
4390
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: diag.revision }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "台账修订号" })]
|
|
4391
|
+
}),
|
|
4392
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4393
|
+
className: "dsh-atb-diag-item",
|
|
4394
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: diag.tasks }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "任务总数" })]
|
|
4395
|
+
}),
|
|
4396
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4397
|
+
className: "dsh-atb-diag-item",
|
|
4398
|
+
"data-bad": diag.staleRunning > 0 ? "true" : void 0,
|
|
4399
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: diag.staleRunning }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "执行中" })]
|
|
4400
|
+
}),
|
|
4401
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4402
|
+
className: "dsh-atb-diag-item",
|
|
4403
|
+
"data-bad": diag.orphanWorktrees.length > 0 ? "true" : void 0,
|
|
4404
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: diag.orphanWorktrees.length }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "遗留 worktree" })]
|
|
4405
|
+
})
|
|
4406
|
+
]
|
|
4407
|
+
}),
|
|
4408
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4409
|
+
className: "dsh-atb-diag-sec",
|
|
4410
|
+
children: [
|
|
4411
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: "遗留 worktree(台账无主但目录存在)" }),
|
|
4412
|
+
diag.orphanWorktrees.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4413
|
+
className: "dsh-atb-empty2",
|
|
4414
|
+
children: "无遗留 — 各项目 .dsh-worktrees 目录干净"
|
|
4415
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4416
|
+
className: "dsh-atb-diag-orphans",
|
|
4417
|
+
children: diag.orphanWorktrees.map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4418
|
+
className: "dsh-atb-diag-orphan",
|
|
4419
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4420
|
+
className: "dsh-atb-diag-orphan-path",
|
|
4421
|
+
title: o.path,
|
|
4422
|
+
children: [
|
|
4423
|
+
wsName(o.workspaceId),
|
|
4424
|
+
" · ",
|
|
4425
|
+
o.taskId
|
|
4426
|
+
]
|
|
4427
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4428
|
+
type: "button",
|
|
4429
|
+
className: "dsh-atb-btn",
|
|
4430
|
+
"data-danger": "true",
|
|
4431
|
+
onClick: () => void controller.cleanupOrphan(o.workspaceId, o.taskId),
|
|
4432
|
+
children: "清理"
|
|
4433
|
+
})]
|
|
4434
|
+
}, o.path))
|
|
4435
|
+
}),
|
|
4436
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4437
|
+
className: "dsh-atb-empty2",
|
|
4438
|
+
children: "提示:有未提交修改的遗留目录会被拒绝清理,请先手动处理其内容。live 任务的 worktree 请在任务详情页删除。"
|
|
4439
|
+
})
|
|
4440
|
+
]
|
|
4441
|
+
}),
|
|
4442
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4443
|
+
className: "dsh-atb-diag-sec",
|
|
4444
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: "gitignore 建议" }), (diag.gitIgnoreSuggestions ?? []).length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4445
|
+
className: "dsh-atb-empty2",
|
|
4446
|
+
children: "无待办 — 各 git 项目已忽略 .dsh-worktrees 目录"
|
|
4447
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4448
|
+
className: "dsh-atb-diag-orphans",
|
|
4449
|
+
children: diag.gitIgnoreSuggestions.map((s) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4450
|
+
className: "dsh-atb-diag-orphan",
|
|
4451
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4452
|
+
className: "dsh-atb-diag-orphan-path",
|
|
4453
|
+
title: s.workspacePath,
|
|
4454
|
+
children: [
|
|
4455
|
+
wsName(s.workspaceId),
|
|
4456
|
+
" · 建议在 .gitignore 加入一行 ",
|
|
4457
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: ".dsh-worktrees/" }),
|
|
4458
|
+
"(不会自动修改)"
|
|
4459
|
+
]
|
|
4460
|
+
})
|
|
4461
|
+
}, s.workspaceId))
|
|
4462
|
+
})]
|
|
4463
|
+
})
|
|
4464
|
+
] })
|
|
4465
|
+
})]
|
|
4466
|
+
})
|
|
4467
|
+
});
|
|
4468
|
+
}
|
|
2721
4469
|
/** Secondary tab: tasks grouped into canceled / archived / trashed columns. */
|
|
2722
4470
|
function SecondaryTab({ controller, tasks }) {
|
|
2723
4471
|
const trashed = tasks.filter((t) => t.trashedAt !== void 0);
|
|
@@ -2920,17 +4668,34 @@ window.__ModuleLoader__.load({
|
|
|
2920
4668
|
injectStyles();
|
|
2921
4669
|
const controller = new BoardController(createClient());
|
|
2922
4670
|
const connection = ctx.get?.("connection");
|
|
2923
|
-
if (connection !== void 0)
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
4671
|
+
if (connection !== void 0) {
|
|
4672
|
+
controller.modelCatalog = async () => {
|
|
4673
|
+
const response = await connection.api.llm.models({});
|
|
4674
|
+
if (!response.result.ok) return [];
|
|
4675
|
+
const out = [];
|
|
4676
|
+
for (const group of response.result.value.groups) for (const model of group.models) out.push({
|
|
4677
|
+
provider: group.id,
|
|
4678
|
+
model: model.id,
|
|
4679
|
+
name: model.name
|
|
4680
|
+
});
|
|
4681
|
+
return out;
|
|
4682
|
+
};
|
|
4683
|
+
controller.presetCatalog = async () => {
|
|
4684
|
+
const list = connection.api.agentPresets;
|
|
4685
|
+
if (list === void 0) return { presets: [] };
|
|
4686
|
+
const response = await list.list({});
|
|
4687
|
+
if (!response.result.ok) return { presets: [] };
|
|
4688
|
+
const presets = response.result.value.presets.map((p) => ({
|
|
4689
|
+
id: p.id,
|
|
4690
|
+
name: p.name
|
|
4691
|
+
}));
|
|
4692
|
+
const def = response.result.value.presets.find((p) => p.isDefault);
|
|
4693
|
+
return {
|
|
4694
|
+
presets,
|
|
4695
|
+
...def !== void 0 ? { defaultId: def.id } : {}
|
|
4696
|
+
};
|
|
4697
|
+
};
|
|
4698
|
+
}
|
|
2934
4699
|
controller.installSessionJumper(createSessionJumper({
|
|
2935
4700
|
getSessions: () => ctx.get?.("sessions"),
|
|
2936
4701
|
getWorkspaces: () => ctx.get?.("workspaces")
|