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/package.json
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-taskboard",
|
|
3
|
-
"description": "Agent-first task board for the DSH web GUI: host-authoritative task ledger with taskboard_* agent tools, project (= workspace) claim boundaries, per-task model execution in fresh sessions, host-side cron scheduling, and a live SSE kanban view. Mounts via the official dsh plugin system — no DSH source changes.",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "lib/index.js",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": "./lib/index.js",
|
|
9
|
-
"./invariant": "./lib/invariant.js",
|
|
10
|
-
"./client": "./lib/client.js",
|
|
11
|
-
"./package.json": "./package.json"
|
|
12
|
-
},
|
|
13
|
-
"dsh": {
|
|
14
|
-
"bundle": {
|
|
15
|
-
"patch": "./cordis.patch.yml"
|
|
16
|
-
},
|
|
17
|
-
"client": {
|
|
18
|
-
"inject": [],
|
|
19
|
-
"platform": "web"
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
|
-
"files": [
|
|
23
|
-
"lib/**/*.js",
|
|
24
|
-
"lib/**/*.js.map",
|
|
25
|
-
"src",
|
|
26
|
-
"cordis.patch.yml",
|
|
27
|
-
"LICENSE"
|
|
28
|
-
],
|
|
29
|
-
"license": "Apache-2.0",
|
|
30
|
-
"author": "cloader",
|
|
31
|
-
"repository": {
|
|
32
|
-
"type": "git",
|
|
33
|
-
"url": "git+https://github.com/cloader/dsh-taskboard.git"
|
|
34
|
-
},
|
|
35
|
-
"bugs": {
|
|
36
|
-
"url": "https://github.com/cloader/dsh-taskboard/issues"
|
|
37
|
-
},
|
|
38
|
-
"homepage": "https://github.com/cloader/dsh-taskboard#readme",
|
|
39
|
-
"keywords": [
|
|
40
|
-
"dsh",
|
|
41
|
-
"deepseek-harness",
|
|
42
|
-
"dsh-plugin",
|
|
43
|
-
"taskboard",
|
|
44
|
-
"kanban",
|
|
45
|
-
"agent"
|
|
46
|
-
],
|
|
47
|
-
"scripts": {
|
|
48
|
-
"build": "npm run build:host && npm run build:client",
|
|
49
|
-
"build:host": "tsdown -c tsdown.host.config.ts",
|
|
50
|
-
"build:client": "tsdown -c tsdown.client.config.ts && node scripts/wrap-client.mjs",
|
|
51
|
-
"watch": "tsdown -c tsdown.host.config.ts --watch",
|
|
52
|
-
"typecheck": "tsc --noEmit",
|
|
53
|
-
"test": "vitest run"
|
|
54
|
-
},
|
|
55
|
-
"devDependencies": {
|
|
56
|
-
"@deepseek-ai/cordis": "^4.0.1",
|
|
57
|
-
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
|
|
58
|
-
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6",
|
|
59
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
60
|
-
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
|
|
61
|
-
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
62
|
-
"@deepseek-ai/dsh-workspace": "^0.1.0-rc.6",
|
|
63
|
-
"@deepseek-ai/schemastery": "^3.18.1",
|
|
64
|
-
"@types/node": "^22.20.1",
|
|
65
|
-
"@types/react": "~18.3.1",
|
|
66
|
-
"@types/react-dom": "^18.3.7",
|
|
67
|
-
"jsdom": "^25.0.1",
|
|
68
|
-
"react": "^18.3.1",
|
|
69
|
-
"react-dom": "^18.3.1",
|
|
70
|
-
"tsdown": "0.22.2",
|
|
71
|
-
"typescript": "~5.7.2",
|
|
72
|
-
"vitest": "^3.0.0"
|
|
73
|
-
}
|
|
74
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-taskboard",
|
|
3
|
+
"description": "Agent-first task board for the DSH web GUI: host-authoritative task ledger with taskboard_* agent tools, project (= workspace) claim boundaries, per-task model execution in fresh sessions, optional per-task git-worktree isolation (dedicated task branches, commit evidence, one-click merge), host-side cron scheduling, and a live SSE kanban view. Mounts via the official dsh plugin system — no DSH source changes.",
|
|
4
|
+
"version": "0.4.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./invariant": "./lib/invariant.js",
|
|
10
|
+
"./client": "./lib/client.js",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"dsh": {
|
|
14
|
+
"bundle": {
|
|
15
|
+
"patch": "./cordis.patch.yml"
|
|
16
|
+
},
|
|
17
|
+
"client": {
|
|
18
|
+
"inject": [],
|
|
19
|
+
"platform": "web"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"lib/**/*.js",
|
|
24
|
+
"lib/**/*.js.map",
|
|
25
|
+
"src",
|
|
26
|
+
"cordis.patch.yml",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"license": "Apache-2.0",
|
|
30
|
+
"author": "cloader",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/cloader/dsh-taskboard.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/cloader/dsh-taskboard/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/cloader/dsh-taskboard#readme",
|
|
39
|
+
"keywords": [
|
|
40
|
+
"dsh",
|
|
41
|
+
"deepseek-harness",
|
|
42
|
+
"dsh-plugin",
|
|
43
|
+
"taskboard",
|
|
44
|
+
"kanban",
|
|
45
|
+
"agent"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "npm run build:host && npm run build:client",
|
|
49
|
+
"build:host": "tsdown -c tsdown.host.config.ts",
|
|
50
|
+
"build:client": "tsdown -c tsdown.client.config.ts && node scripts/wrap-client.mjs",
|
|
51
|
+
"watch": "tsdown -c tsdown.host.config.ts --watch",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"test": "vitest run"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
57
|
+
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
|
|
58
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6",
|
|
59
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
60
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
|
|
61
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
62
|
+
"@deepseek-ai/dsh-workspace": "^0.1.0-rc.6",
|
|
63
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
64
|
+
"@types/node": "^22.20.1",
|
|
65
|
+
"@types/react": "~18.3.1",
|
|
66
|
+
"@types/react-dom": "^18.3.7",
|
|
67
|
+
"jsdom": "^25.0.1",
|
|
68
|
+
"react": "^18.3.1",
|
|
69
|
+
"react-dom": "^18.3.1",
|
|
70
|
+
"tsdown": "0.22.2",
|
|
71
|
+
"typescript": "~5.7.2",
|
|
72
|
+
"vitest": "^3.0.0"
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/client/api.ts
CHANGED
|
@@ -10,11 +10,20 @@ import type {
|
|
|
10
10
|
ChangeEvent,
|
|
11
11
|
CreateTaskBody,
|
|
12
12
|
DeleteTaskBody,
|
|
13
|
+
DiagnosticsResponse,
|
|
14
|
+
DiffResponse,
|
|
15
|
+
ImportCommitResponse,
|
|
16
|
+
ImportPreviewResponse,
|
|
17
|
+
MergeBranchResponse,
|
|
13
18
|
MoveTaskBody,
|
|
14
19
|
RejectTaskBody,
|
|
20
|
+
RunTaskBody,
|
|
15
21
|
StateResponse,
|
|
16
22
|
TaskRecord,
|
|
23
|
+
TaskTemplate,
|
|
24
|
+
TemplatesResponse,
|
|
17
25
|
UpdateTaskBody,
|
|
26
|
+
WorktreeRemoveBody,
|
|
18
27
|
WorkspaceView,
|
|
19
28
|
} from '../shared/api.ts'
|
|
20
29
|
import type { CommentRecord, TaskSummary } from '../shared/protocol.ts'
|
|
@@ -49,10 +58,30 @@ export interface TaskboardClient {
|
|
|
49
58
|
reject(id: string, body: RejectTaskBody): Promise<TaskSummary>
|
|
50
59
|
comment(id: string, bodyText: string): Promise<CommentRecord>
|
|
51
60
|
remove(id: string, body: DeleteTaskBody): Promise<{ trashed?: boolean; purged?: boolean }>
|
|
52
|
-
/** Trigger a manual run (fresh in-project session)
|
|
53
|
-
run(id: string): Promise<{ executionId: string; sessionId: string }>
|
|
61
|
+
/** Trigger a manual run (fresh in-project session); `reuse: true` = 续跑. */
|
|
62
|
+
run(id: string, body?: RunTaskBody): Promise<{ executionId: string; sessionId: string }>
|
|
54
63
|
/** Cancel the running execution (stops the agent session; task returns to todo). */
|
|
55
64
|
cancel(id: string): Promise<{ cancelled: true; executionId: string }>
|
|
65
|
+
/** Merge the task branch into the main worktree (--no-ff, user-only). */
|
|
66
|
+
mergeBranch(id: string): Promise<MergeBranchResponse>
|
|
67
|
+
/** Remove the task's worktree; optionally delete its branch. */
|
|
68
|
+
worktreeRemove(id: string, body: WorktreeRemoveBody): Promise<{ removed: true; branchDeleted: boolean; branchError?: string }>
|
|
69
|
+
/** Health diagnostics (⚙ panel). */
|
|
70
|
+
diagnostics(): Promise<DiagnosticsResponse>
|
|
71
|
+
/** Clean up one orphan worktree directory (task no longer in the ledger). */
|
|
72
|
+
worktreeCleanup(workspaceId: string, taskId: string): Promise<{ cleaned: true; path: string }>
|
|
73
|
+
/** Diff view: one execution's commit or changed path (read-only, capped). */
|
|
74
|
+
diff(taskId: string, query: { execution: string; commit?: string; path?: string }): Promise<DiffResponse>
|
|
75
|
+
/** Import dry-run: classify the uploaded ledger against the live one. */
|
|
76
|
+
importPreview(file: unknown): Promise<ImportPreviewResponse>
|
|
77
|
+
/** Commit an import (merge upserts; replace swaps the whole ledger, backing it up first). */
|
|
78
|
+
importCommit(mode: 'merge' | 'replace', ledger: unknown): Promise<ImportCommitResponse>
|
|
79
|
+
/** List task templates. */
|
|
80
|
+
templates(): Promise<TemplatesResponse>
|
|
81
|
+
/** Create or replace a template. */
|
|
82
|
+
templateUpsert(body: { id?: string; name: string; task: TaskTemplate['task'] }): Promise<TaskTemplate>
|
|
83
|
+
/** Delete a template by id. */
|
|
84
|
+
templateDelete(id: string): Promise<{ deleted: boolean }>
|
|
56
85
|
/** Subscribe to change frames; the disposer stops the stream. */
|
|
57
86
|
stream(onChange: (event: ChangeEvent) => void, onGap: () => void): () => void
|
|
58
87
|
}
|
|
@@ -69,8 +98,23 @@ export function createClient(): TaskboardClient {
|
|
|
69
98
|
reject: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/reject`, body),
|
|
70
99
|
comment: (id, bodyText) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/comment`, { body: bodyText }),
|
|
71
100
|
remove: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/delete`, body),
|
|
72
|
-
run: id => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/run`, {}),
|
|
101
|
+
run: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/run`, body ?? {}),
|
|
73
102
|
cancel: id => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/cancel`, {}),
|
|
103
|
+
mergeBranch: id => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/merge`, {}),
|
|
104
|
+
worktreeRemove: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/worktree-remove`, body),
|
|
105
|
+
diagnostics: () => unwrap<DiagnosticsResponse>(fetch('/dsh-taskboard/diagnostics')),
|
|
106
|
+
worktreeCleanup: (workspaceId, taskId) => post('/dsh-taskboard/worktree-cleanup', { workspaceId, taskId }),
|
|
107
|
+
diff: (taskId, query) => {
|
|
108
|
+
const params = new URLSearchParams({ execution: query.execution })
|
|
109
|
+
if (query.commit !== undefined) params.set('commit', query.commit)
|
|
110
|
+
if (query.path !== undefined) params.set('path', query.path)
|
|
111
|
+
return unwrap<DiffResponse>(fetch(`/dsh-taskboard/tasks/${encodeURIComponent(taskId)}/diff?${params.toString()}`))
|
|
112
|
+
},
|
|
113
|
+
importPreview: file => post('/dsh-taskboard/import/preview', file),
|
|
114
|
+
importCommit: (mode, ledger) => post('/dsh-taskboard/import', { mode, ledger }),
|
|
115
|
+
templates: () => unwrap<TemplatesResponse>(fetch('/dsh-taskboard/templates')),
|
|
116
|
+
templateUpsert: body => post('/dsh-taskboard/templates', body),
|
|
117
|
+
templateDelete: id => post('/dsh-taskboard/templates/delete', { id }),
|
|
74
118
|
stream(onChange, onGap) {
|
|
75
119
|
const es = new EventSource('/dsh-taskboard/events')
|
|
76
120
|
let revision: number | undefined
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ledger-import modal (0.4.0): pick a JSON file → dry-run preview
|
|
3
|
+
* (create / overwrite / invalid classification) → commit as merge or
|
|
4
|
+
* replace. Replace swaps the WHOLE ledger after an automatic backup and a
|
|
5
|
+
* double confirmation. Files exported by ⬇ JSON import as-is.
|
|
6
|
+
*
|
|
7
|
+
* @module dsh-taskboard/client/board/ImportModal
|
|
8
|
+
*/
|
|
9
|
+
import { useRef, useState } from 'react'
|
|
10
|
+
import type { BoardController } from '../controller.ts'
|
|
11
|
+
import type { ImportPreviewResponse } from '../../shared/api.ts'
|
|
12
|
+
import { useAlert } from './AlertModal.tsx'
|
|
13
|
+
|
|
14
|
+
/** One classified row (create / overwrite). */
|
|
15
|
+
function PlanRow({ row }: { row: { id: string; title: string; status: string } }) {
|
|
16
|
+
return (
|
|
17
|
+
<div className="dsh-atb-imp-row" title={row.id}>
|
|
18
|
+
<span className="dsh-atb-imp-row-title">{row.title}</span>
|
|
19
|
+
<span className="dsh-atb-imp-row-status">{row.status}</span>
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The import modal.
|
|
26
|
+
* @param controller - the controller.
|
|
27
|
+
*/
|
|
28
|
+
export function ImportModal({ controller }: { controller: BoardController }) {
|
|
29
|
+
const [fileName, setFileName] = useState('')
|
|
30
|
+
const [parsed, setParsed] = useState<unknown>(null)
|
|
31
|
+
const [parseError, setParseError] = useState<string | undefined>(undefined)
|
|
32
|
+
const [plan, setPlan] = useState<ImportPreviewResponse['plan'] | undefined>(undefined)
|
|
33
|
+
const [mode, setMode] = useState<'merge' | 'replace'>('merge')
|
|
34
|
+
const [busy, setBusy] = useState(false)
|
|
35
|
+
const [result, setResult] = useState<string | undefined>(undefined)
|
|
36
|
+
const [confirmReplace, setConfirmReplace] = useState(false)
|
|
37
|
+
const fileRef = useRef<HTMLInputElement>(null)
|
|
38
|
+
const { alert: showAlert, el: alertEl } = useAlert()
|
|
39
|
+
|
|
40
|
+
/** Read + parse the picked file, then dry-run the preview. */
|
|
41
|
+
const onFile = (file: File | undefined): void => {
|
|
42
|
+
setPlan(undefined)
|
|
43
|
+
setParseError(undefined)
|
|
44
|
+
setResult(undefined)
|
|
45
|
+
setConfirmReplace(false)
|
|
46
|
+
setFileName('')
|
|
47
|
+
setParsed(null)
|
|
48
|
+
if (file === undefined) return
|
|
49
|
+
void file.text().then(text => {
|
|
50
|
+
try {
|
|
51
|
+
const value: unknown = JSON.parse(text)
|
|
52
|
+
setParsed(value)
|
|
53
|
+
setFileName(file.name)
|
|
54
|
+
void controller.importPreview(value).then(p => {
|
|
55
|
+
if (p !== undefined) setPlan(p)
|
|
56
|
+
})
|
|
57
|
+
} catch {
|
|
58
|
+
setParseError('文件不是合法 JSON')
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Commit the import (replace requires the inline double confirmation). */
|
|
64
|
+
const commit = (): void => {
|
|
65
|
+
if (parsed === null || plan === undefined || busy) return
|
|
66
|
+
if (mode === 'replace' && !confirmReplace) {
|
|
67
|
+
setConfirmReplace(true)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
setBusy(true)
|
|
71
|
+
void controller.importCommit(mode, parsed).then(r => {
|
|
72
|
+
setBusy(false)
|
|
73
|
+
setConfirmReplace(false)
|
|
74
|
+
if (r === undefined) return
|
|
75
|
+
setResult(r.mode === 'replace'
|
|
76
|
+
? `整册替换完成:导入 ${r.created + r.overwritten} 张(原 ${r.replacedTotal} 张已整册备份)`
|
|
77
|
+
: `合并完成:新增 ${r.created} 张、覆盖 ${r.overwritten} 张`)
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const close = (): void => controller.closeImport()
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className="dsh-atb-modal-backdrop" onClick={e => { if (e.target === e.currentTarget) close() }}>
|
|
85
|
+
<div className="dsh-atb-modal dsh-atb-imp" role="dialog" aria-modal="true" aria-label="导入台账">
|
|
86
|
+
<div className="dsh-atb-modal-head">
|
|
87
|
+
<span className="dsh-atb-modal-headicon">⬆</span>
|
|
88
|
+
<div className="dsh-atb-modal-headtext">
|
|
89
|
+
<h3>导入台账</h3>
|
|
90
|
+
<p>选择导出的 JSON 备份文件:先预览、再合并或整册替换</p>
|
|
91
|
+
</div>
|
|
92
|
+
<button type="button" className="dsh-atb-modal-close" aria-label="关闭" onClick={close}>✕</button>
|
|
93
|
+
</div>
|
|
94
|
+
<div className="dsh-atb-modal-body">
|
|
95
|
+
<div className="dsh-atb-imp-picker">
|
|
96
|
+
<input
|
|
97
|
+
ref={fileRef}
|
|
98
|
+
type="file"
|
|
99
|
+
accept=".json,application/json"
|
|
100
|
+
onChange={e => onFile(e.target.files?.[0])}
|
|
101
|
+
/>
|
|
102
|
+
{fileName.length > 0 && <span className="dsh-atb-imp-filename">{fileName}</span>}
|
|
103
|
+
</div>
|
|
104
|
+
<div className="dsh-atb-imp-note">⬇ JSON 导出的文件即为同格式备份,可直接导入恢复;导入文件的 schemaVersion 必须与当前版本一致。</div>
|
|
105
|
+
|
|
106
|
+
{parseError !== undefined && <div className="dsh-atb-imp-error">{parseError}</div>}
|
|
107
|
+
{plan === undefined && parseError === undefined && fileName.length > 0 && <div className="dsh-atb-empty2">预览中…</div>}
|
|
108
|
+
|
|
109
|
+
{plan !== undefined && (
|
|
110
|
+
<>
|
|
111
|
+
<div className="dsh-atb-imp-stats">
|
|
112
|
+
<div className="dsh-atb-imp-stat" data-tone="ok"><b>{plan.create.length}</b><span>新增</span></div>
|
|
113
|
+
<div className="dsh-atb-imp-stat" data-tone="warn"><b>{plan.overwrite.length}</b><span>覆盖(同 id)</span></div>
|
|
114
|
+
<div className="dsh-atb-imp-stat" data-tone={plan.invalid.length > 0 ? 'bad' : undefined}><b>{plan.invalid.length}</b><span>无效(跳过)</span></div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{plan.create.length > 0 && (
|
|
118
|
+
<div className="dsh-atb-imp-sec">
|
|
119
|
+
<h4>新增任务</h4>
|
|
120
|
+
<div className="dsh-atb-imp-list">{plan.create.map(r => <PlanRow key={r.id} row={r} />)}</div>
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
{plan.overwrite.length > 0 && (
|
|
124
|
+
<div className="dsh-atb-imp-sec">
|
|
125
|
+
<h4>覆盖任务(整卡替换,含执行历史与评论)</h4>
|
|
126
|
+
<div className="dsh-atb-imp-list">{plan.overwrite.map(r => <PlanRow key={r.id} row={r} />)}</div>
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
{plan.invalid.length > 0 && (
|
|
130
|
+
<div className="dsh-atb-imp-sec">
|
|
131
|
+
<h4>无效条目(不会导入)</h4>
|
|
132
|
+
<div className="dsh-atb-imp-list">
|
|
133
|
+
{plan.invalid.map((r, i) => (
|
|
134
|
+
<div key={r.id ?? `invalid-${i}`} className="dsh-atb-imp-row" data-tone="bad" title={r.id ?? ''}>
|
|
135
|
+
<span className="dsh-atb-imp-row-title">{r.id ?? '(无 id)'}</span>
|
|
136
|
+
<span className="dsh-atb-imp-row-status">{r.reason}</span>
|
|
137
|
+
</div>
|
|
138
|
+
))}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
|
|
143
|
+
<div className="dsh-atb-mode-picker">
|
|
144
|
+
<button type="button" className="dsh-atb-mode-opt" data-on={mode === 'merge'} onClick={() => { setMode('merge'); setConfirmReplace(false) }}>
|
|
145
|
+
<span className="dsh-atb-mode-name">⊕ 合并</span>
|
|
146
|
+
<span className="dsh-atb-mode-hint">新增 + 按 id 覆盖,其余不动</span>
|
|
147
|
+
</button>
|
|
148
|
+
<button type="button" className="dsh-atb-mode-opt" data-on={mode === 'replace'} onClick={() => setMode('replace')}>
|
|
149
|
+
<span className="dsh-atb-mode-name">💣 整册替换</span>
|
|
150
|
+
<span className="dsh-atb-mode-hint">清空当前台账,以导入文件为准(先自动备份)</span>
|
|
151
|
+
</button>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
{result !== undefined && <div className="dsh-atb-imp-result">{result}</div>}
|
|
155
|
+
</>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
<div className="dsh-atb-modal-foot">
|
|
159
|
+
<span className="dsh-atb-modal-hint">
|
|
160
|
+
{mode === 'replace'
|
|
161
|
+
? confirmReplace ? '⚠ 再次点击确认执行整册替换(不可撤销,已自动备份)' : '整册替换需要二次确认'
|
|
162
|
+
: '合并只写入预览中列出的任务'}
|
|
163
|
+
</span>
|
|
164
|
+
<span className="dsh-atb-modal-footbtns">
|
|
165
|
+
<button type="button" className="dsh-atb-btn" onClick={close}>{result !== undefined ? '关闭' : '取消'}</button>
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
className="dsh-atb-btn"
|
|
169
|
+
data-primary="true"
|
|
170
|
+
data-danger={mode === 'replace' && confirmReplace ? 'true' : undefined}
|
|
171
|
+
disabled={plan === undefined || busy || (result !== undefined && false)}
|
|
172
|
+
onClick={commit}
|
|
173
|
+
>
|
|
174
|
+
{mode === 'replace' && confirmReplace ? '确认整册替换' : '执行导入'}
|
|
175
|
+
</button>
|
|
176
|
+
</span>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
{alertEl}
|
|
180
|
+
</div>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
@@ -12,6 +12,8 @@ import { PLUGIN_VERSION } from '../../shared/version.ts'
|
|
|
12
12
|
import { DRAG_TYPE, TaskCard } from './TaskCard.tsx'
|
|
13
13
|
import { TaskDetail } from './TaskDetail.tsx'
|
|
14
14
|
import { TaskFormModal } from './TaskFormModal.tsx'
|
|
15
|
+
import { ImportModal } from './ImportModal.tsx'
|
|
16
|
+
import { TemplateManager } from './TemplateManager.tsx'
|
|
15
17
|
import { useAlert } from './AlertModal.tsx'
|
|
16
18
|
|
|
17
19
|
/** Column labels. */
|
|
@@ -85,15 +87,50 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
|
|
|
85
87
|
const live = filterTasks(state, state.ledger.tasks.filter(t => t.trashedAt === undefined))
|
|
86
88
|
const selected = state.selectedId === undefined ? undefined : state.ledger.tasks.find(t => t.id === state.selectedId)
|
|
87
89
|
const { alert: showAlert, el: alertEl } = useAlert()
|
|
90
|
+
// + 新建任务 ▼ dropdown (0.4.0): blank / templates / manage / import.
|
|
91
|
+
const [newMenuOpen, setNewMenuOpen] = useState(false)
|
|
92
|
+
const closeMenu = (): void => setNewMenuOpen(false)
|
|
88
93
|
|
|
89
94
|
return (
|
|
90
95
|
<div className="dsh-atb-board">
|
|
91
96
|
<div className="dsh-atb-toolbar">
|
|
92
97
|
<h2 className="dsh-atb-title">Agent 任务看板</h2>
|
|
93
98
|
<span className="dsh-atb-count">{live.length} 任务 · rev {state.ledger.revision}</span>
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
<div className="dsh-atb-newmenu">
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
className="dsh-atb-btn"
|
|
103
|
+
data-primary="true"
|
|
104
|
+
onClick={() => {
|
|
105
|
+
const next = !newMenuOpen
|
|
106
|
+
setNewMenuOpen(next)
|
|
107
|
+
if (next) controller.prepareTemplateMenu()
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
+ 新建任务 ▼
|
|
111
|
+
</button>
|
|
112
|
+
{newMenuOpen && (
|
|
113
|
+
<>
|
|
114
|
+
<div className="dsh-atb-newmenu-backdrop" onClick={closeMenu} />
|
|
115
|
+
<div className="dsh-atb-newmenu-list">
|
|
116
|
+
<button type="button" className="dsh-atb-newmenu-opt" onClick={() => { closeMenu(); controller.setComposer(true) }}>空白任务</button>
|
|
117
|
+
{state.templates.map(t => (
|
|
118
|
+
<button
|
|
119
|
+
key={t.id}
|
|
120
|
+
type="button"
|
|
121
|
+
className="dsh-atb-newmenu-opt"
|
|
122
|
+
title={t.task.description !== undefined && t.task.description.length > 0 ? t.task.description.slice(0, 120) : t.name}
|
|
123
|
+
onClick={() => { closeMenu(); controller.newFromTemplate(t.task) }}
|
|
124
|
+
>
|
|
125
|
+
{t.name}{t.builtin === true ? '' : ''}
|
|
126
|
+
</button>
|
|
127
|
+
))}
|
|
128
|
+
<div className="dsh-atb-newmenu-sep" />
|
|
129
|
+
<button type="button" className="dsh-atb-newmenu-opt" onClick={() => { closeMenu(); controller.openTemplateManager() }}>⌗ 管理模板…</button>
|
|
130
|
+
</div>
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
97
134
|
<div className="dsh-atb-spacer" />
|
|
98
135
|
<input
|
|
99
136
|
className="dsh-atb-input dsh-atb-search"
|
|
@@ -137,6 +174,8 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
|
|
|
137
174
|
<button type="button" className="dsh-atb-btn" onClick={() => controller.toggleSecondary()}>
|
|
138
175
|
{state.secondaryOpen ? '返回看板' : '其它任务'}
|
|
139
176
|
</button>
|
|
177
|
+
<button type="button" className="dsh-atb-btn" title="健康诊断:遗留 worktree、台账基本项" onClick={() => controller.openDiagnostics()}>⚙ 诊断</button>
|
|
178
|
+
<button type="button" className="dsh-atb-btn" title="从 JSON 备份文件导入台账(预览后合并或整册替换)" onClick={() => controller.openImport()}>⬆ 导入</button>
|
|
140
179
|
<button type="button" className="dsh-atb-btn" title="下载完整台账备份(JSON)" onClick={() => controller.exportJson()}>⬇ JSON</button>
|
|
141
180
|
<button type="button" className="dsh-atb-btn" title="下载任务清单(CSV)" onClick={() => controller.exportCsv()}>⬇ CSV</button>
|
|
142
181
|
<a
|
|
@@ -220,11 +259,87 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
|
|
|
220
259
|
/>
|
|
221
260
|
)}
|
|
222
261
|
|
|
262
|
+
{state.diagOpen && <DiagnosticsPanel controller={controller} />}
|
|
263
|
+
|
|
264
|
+
{state.importOpen && <ImportModal controller={controller} />}
|
|
265
|
+
|
|
266
|
+
{state.tplManagerOpen && <TemplateManager controller={controller} />}
|
|
267
|
+
|
|
223
268
|
{alertEl}
|
|
224
269
|
</div>
|
|
225
270
|
)
|
|
226
271
|
}
|
|
227
272
|
|
|
273
|
+
/** ⚙ Health-diagnostics panel (plan §3.6): ledger basics + orphan worktrees + one-click cleanup. */
|
|
274
|
+
function DiagnosticsPanel({ controller }: { controller: BoardController }) {
|
|
275
|
+
const state = controller.getSnapshot()
|
|
276
|
+
const diag = state.diagnostics
|
|
277
|
+
const wsName = (id: string): string => {
|
|
278
|
+
const ws = state.workspaces.find(w => w.id === id)
|
|
279
|
+
return ws?.title ?? ws?.path ?? id.slice(0, 8)
|
|
280
|
+
}
|
|
281
|
+
return (
|
|
282
|
+
<div className="dsh-atb-modal-backdrop" onClick={e => { if (e.target === e.currentTarget) controller.closeDiagnostics() }}>
|
|
283
|
+
<div className="dsh-atb-modal dsh-atb-diag" role="dialog" aria-modal="true" aria-label="健康诊断">
|
|
284
|
+
<div className="dsh-atb-modal-head">
|
|
285
|
+
<span className="dsh-atb-modal-headicon">⚙</span>
|
|
286
|
+
<div className="dsh-atb-modal-headtext">
|
|
287
|
+
<h3>健康诊断</h3>
|
|
288
|
+
<p>台账基本项与 worktree 遗留清理</p>
|
|
289
|
+
</div>
|
|
290
|
+
<button type="button" className="dsh-atb-modal-close" aria-label="关闭" onClick={() => controller.closeDiagnostics()}>✕</button>
|
|
291
|
+
</div>
|
|
292
|
+
<div className="dsh-atb-modal-body">
|
|
293
|
+
{diag === undefined
|
|
294
|
+
? <div className="dsh-atb-empty2">读取中…</div>
|
|
295
|
+
: (
|
|
296
|
+
<>
|
|
297
|
+
<div className="dsh-atb-diag-grid">
|
|
298
|
+
<div className="dsh-atb-diag-item"><b>{diag.revision}</b><span>台账修订号</span></div>
|
|
299
|
+
<div className="dsh-atb-diag-item"><b>{diag.tasks}</b><span>任务总数</span></div>
|
|
300
|
+
<div className="dsh-atb-diag-item" data-bad={diag.staleRunning > 0 ? 'true' : undefined}><b>{diag.staleRunning}</b><span>执行中</span></div>
|
|
301
|
+
<div className="dsh-atb-diag-item" data-bad={diag.orphanWorktrees.length > 0 ? 'true' : undefined}><b>{diag.orphanWorktrees.length}</b><span>遗留 worktree</span></div>
|
|
302
|
+
</div>
|
|
303
|
+
<div className="dsh-atb-diag-sec">
|
|
304
|
+
<h4>遗留 worktree(台账无主但目录存在)</h4>
|
|
305
|
+
{diag.orphanWorktrees.length === 0
|
|
306
|
+
? <div className="dsh-atb-empty2">无遗留 — 各项目 .dsh-worktrees 目录干净</div>
|
|
307
|
+
: (
|
|
308
|
+
<div className="dsh-atb-diag-orphans">
|
|
309
|
+
{diag.orphanWorktrees.map(o => (
|
|
310
|
+
<div key={o.path} className="dsh-atb-diag-orphan">
|
|
311
|
+
<span className="dsh-atb-diag-orphan-path" title={o.path}>{wsName(o.workspaceId)} · {o.taskId}</span>
|
|
312
|
+
<button type="button" className="dsh-atb-btn" data-danger="true" onClick={() => void controller.cleanupOrphan(o.workspaceId, o.taskId)}>清理</button>
|
|
313
|
+
</div>
|
|
314
|
+
))}
|
|
315
|
+
</div>
|
|
316
|
+
)}
|
|
317
|
+
<div className="dsh-atb-empty2">提示:有未提交修改的遗留目录会被拒绝清理,请先手动处理其内容。live 任务的 worktree 请在任务详情页删除。</div>
|
|
318
|
+
</div>
|
|
319
|
+
<div className="dsh-atb-diag-sec">
|
|
320
|
+
<h4>gitignore 建议</h4>
|
|
321
|
+
{(diag.gitIgnoreSuggestions ?? []).length === 0
|
|
322
|
+
? <div className="dsh-atb-empty2">无待办 — 各 git 项目已忽略 .dsh-worktrees 目录</div>
|
|
323
|
+
: (
|
|
324
|
+
<div className="dsh-atb-diag-orphans">
|
|
325
|
+
{diag.gitIgnoreSuggestions.map(s => (
|
|
326
|
+
<div key={s.workspaceId} className="dsh-atb-diag-orphan">
|
|
327
|
+
<span className="dsh-atb-diag-orphan-path" title={s.workspacePath}>
|
|
328
|
+
{wsName(s.workspaceId)} · 建议在 .gitignore 加入一行 <code>.dsh-worktrees/</code>(不会自动修改)
|
|
329
|
+
</span>
|
|
330
|
+
</div>
|
|
331
|
+
))}
|
|
332
|
+
</div>
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
</>
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
|
|
228
343
|
/** Secondary tab: tasks grouped into canceled / archived / trashed columns. */
|
|
229
344
|
function SecondaryTab({ controller, tasks }: { controller: BoardController; tasks: TaskRecord[] }) {
|
|
230
345
|
// Trashed takes precedence (a trashed task still carries its old status,
|
|
@@ -86,6 +86,15 @@ export function TaskCard({ task, controller, draggable = false, now, onAlert }:
|
|
|
86
86
|
<span className="dsh-atb-badge" data-kind="scheduled">⏰ {fmtTime(task.execution.nextRunAt)}</span>
|
|
87
87
|
)}
|
|
88
88
|
{task.model !== undefined && <span className="dsh-atb-badge">{task.model.model}</span>}
|
|
89
|
+
{task.checklist !== undefined && task.checklist.length > 0 && (
|
|
90
|
+
<span
|
|
91
|
+
className="dsh-atb-badge"
|
|
92
|
+
data-kind={task.status === 'in_review' && task.checklist.some(i => !i.checked) ? 'blocked' : 'checklist'}
|
|
93
|
+
title={task.status === 'in_review' && task.checklist.some(i => !i.checked) ? '待验收:清单未全部勾选' : '验收清单进度'}
|
|
94
|
+
>
|
|
95
|
+
☑ {task.checklist.filter(i => i.checked).length}/{task.checklist.length}
|
|
96
|
+
</span>
|
|
97
|
+
)}
|
|
89
98
|
{task.status === 'done' && <span className="dsh-atb-badge" data-kind="done">完成</span>}
|
|
90
99
|
{last !== undefined && (
|
|
91
100
|
<span className="dsh-atb-badge" data-kind={last.outcome === 'running' ? 'running' : last.outcome}>
|