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
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module dsh-taskboard/client/board/TaskDetail
|
|
9
9
|
*/
|
|
10
|
-
import { useState, type ReactNode } from 'react'
|
|
10
|
+
import { useEffect, useState, type ReactNode } from 'react'
|
|
11
11
|
import type { BoardController } from '../controller.ts'
|
|
12
|
-
import type { TaskRecord } from '../../shared/protocol.ts'
|
|
13
|
-
import { canTransition } from '../../shared/protocol.ts'
|
|
12
|
+
import type { ExecutionRecord, TaskRecord } from '../../shared/protocol.ts'
|
|
13
|
+
import { canTransition, checklistProgress } from '../../shared/protocol.ts'
|
|
14
14
|
import { useAlert } from './AlertModal.tsx'
|
|
15
15
|
import { fmtTime, isStaleClaim } from './TaskBoard.tsx'
|
|
16
16
|
|
|
@@ -48,6 +48,321 @@ function Chip({ icon, children, tone }: { icon?: string; children: ReactNode; to
|
|
|
48
48
|
return <span className="dsh-atb-chip2" data-tone={tone}>{icon !== undefined && <span className="dsh-atb-chip2-icon">{icon}</span>}{children}</span>
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** The most recent execution carrying isolation facts, newest first. */
|
|
52
|
+
function latestIsolated(task: TaskRecord): ExecutionRecord | undefined {
|
|
53
|
+
return [...task.executions].reverse().find(e => e.isolation !== undefined || e.worktreePath !== undefined || e.isolationNote !== undefined)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Short commit hash for display. */
|
|
57
|
+
function shortHash(hash: string | undefined): string {
|
|
58
|
+
return hash === undefined ? '' : hash.slice(0, 8)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Extract the path from one `git status --porcelain` line (rename-aware). */
|
|
62
|
+
function porcelainPath(line: string): string {
|
|
63
|
+
let p = line.slice(3)
|
|
64
|
+
const arrow = p.indexOf(' -> ')
|
|
65
|
+
if (arrow >= 0) p = p.slice(arrow + 4)
|
|
66
|
+
if (p.startsWith('"') && p.endsWith('"') && p.length > 1) p = p.slice(1, -1)
|
|
67
|
+
return p
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Lazy diff viewer (0.4.0): loads on mount, renders inside a capped <pre>.
|
|
72
|
+
* @param spec - what to show: one commit hash, or one changed path.
|
|
73
|
+
*/
|
|
74
|
+
function DiffView({ controller, task, execution, commit, path }: {
|
|
75
|
+
controller: BoardController
|
|
76
|
+
task: TaskRecord
|
|
77
|
+
execution: ExecutionRecord
|
|
78
|
+
commit?: string
|
|
79
|
+
path?: string
|
|
80
|
+
}) {
|
|
81
|
+
const [state, setState] = useState<{ loading: boolean; diff?: string; truncated?: boolean; failed?: boolean }>({ loading: true })
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
let alive = true
|
|
84
|
+
setState({ loading: true })
|
|
85
|
+
void controller.fetchDiff(task.id, { execution: execution.id, ...(commit !== undefined ? { commit } : { path: path ?? '' }) }).then(result => {
|
|
86
|
+
if (!alive) return
|
|
87
|
+
if (result === undefined) setState({ loading: false, failed: true })
|
|
88
|
+
else setState({ loading: false, diff: result.diff, truncated: result.truncated })
|
|
89
|
+
})
|
|
90
|
+
return () => { alive = false }
|
|
91
|
+
}, [controller, task.id, execution.id, commit, path])
|
|
92
|
+
return (
|
|
93
|
+
<div className="dsh-atb-diffview">
|
|
94
|
+
<div className="dsh-atb-diffview-head">
|
|
95
|
+
<span className="dsh-atb-diffview-title">{commit !== undefined ? `提交 ${shortHash(commit)}` : `文件 ${path}`}</span>
|
|
96
|
+
{state.loading && <span className="dsh-atb-diffview-hint">读取中…</span>}
|
|
97
|
+
{state.truncated === true && <span className="dsh-atb-diffview-hint">⚠ 内容过长已截断</span>}
|
|
98
|
+
</div>
|
|
99
|
+
{state.failed === true
|
|
100
|
+
? <div className="dsh-atb-diffview-error">获取失败(原因见看板顶部错误条;对象可能已随 worktree 删除丢失)</div>
|
|
101
|
+
: <pre className="dsh-atb-diffview-pre">{state.diff ?? ''}</pre>}
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The DoD checklist block (0.4.0): user-togglable items, checker + evidence
|
|
108
|
+
* per row; unchecked items highlight while the task sits in in_review.
|
|
109
|
+
*/
|
|
110
|
+
function ChecklistBlock({ task, controller }: { task: TaskRecord; controller: BoardController }) {
|
|
111
|
+
const items = task.checklist ?? []
|
|
112
|
+
if (items.length === 0) return null
|
|
113
|
+
const { done, total } = checklistProgress(task)
|
|
114
|
+
const unchecked = total - done
|
|
115
|
+
const reviewing = task.status === 'in_review'
|
|
116
|
+
return (
|
|
117
|
+
<div className="dsh-atb-fieldcard" data-kind="checklist">
|
|
118
|
+
<div className="dsh-atb-fieldcard-label">
|
|
119
|
+
验收清单(DoD)
|
|
120
|
+
<span className="dsh-atb-cl-progress" data-tone={reviewing && unchecked > 0 ? 'bad' : undefined}>
|
|
121
|
+
☑ {done}/{total}{reviewing && unchecked > 0 ? ` · ${unchecked} 项未完成` : done === total ? ' · 全部完成' : ''}
|
|
122
|
+
</span>
|
|
123
|
+
</div>
|
|
124
|
+
<div className="dsh-atb-cl-items">
|
|
125
|
+
{items.map(item => (
|
|
126
|
+
<label
|
|
127
|
+
key={item.id}
|
|
128
|
+
className="dsh-atb-cl-item"
|
|
129
|
+
data-checked={item.checked ? 'true' : undefined}
|
|
130
|
+
data-alert={reviewing && !item.checked ? 'true' : undefined}
|
|
131
|
+
>
|
|
132
|
+
<input
|
|
133
|
+
type="checkbox"
|
|
134
|
+
checked={item.checked}
|
|
135
|
+
onChange={() => void controller.toggleChecklistItem(task, item.id)}
|
|
136
|
+
/>
|
|
137
|
+
<span className="dsh-atb-cl-text">{item.text}</span>
|
|
138
|
+
<span className="dsh-atb-cl-meta">
|
|
139
|
+
{item.checked
|
|
140
|
+
? `${item.checkedBy === 'user' ? '👤 用户' : `🤖 ${shortId(item.checkedBy)}`} · ${fmtTime(item.checkedAt)}`
|
|
141
|
+
: '未完成'}
|
|
142
|
+
{item.note !== undefined && item.note.length > 0 && <span className="dsh-atb-cl-note" title={item.note}>证据:{item.note}</span>}
|
|
143
|
+
</span>
|
|
144
|
+
</label>
|
|
145
|
+
))}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The structured execution report block (0.4.0): the newest execution that
|
|
153
|
+
* carries one, rendered section by section for the reviewer.
|
|
154
|
+
*/
|
|
155
|
+
function ReportBlock({ task }: { task: TaskRecord }) {
|
|
156
|
+
const execution = [...task.executions].reverse().find(e => e.report !== undefined)
|
|
157
|
+
const report = execution?.report
|
|
158
|
+
if (execution === undefined || report === undefined) return null
|
|
159
|
+
const section = (label: string, rows: string[] | undefined): ReactNode => rows !== undefined && rows.length > 0
|
|
160
|
+
? (
|
|
161
|
+
<div className="dsh-atb-rpt-sec">
|
|
162
|
+
<div className="dsh-atb-rpt-label">{label}</div>
|
|
163
|
+
<ul className="dsh-atb-rpt-list">{rows.map((row, i) => <li key={i}>{row}</li>)}</ul>
|
|
164
|
+
</div>
|
|
165
|
+
)
|
|
166
|
+
: null
|
|
167
|
+
return (
|
|
168
|
+
<div className="dsh-atb-fieldcard" data-kind="report">
|
|
169
|
+
<div className="dsh-atb-fieldcard-label">执行报告<span className="dsh-atb-cl-progress">由执行会话提交 · {fmtTime(execution.endedAt ?? execution.startedAt)}</span></div>
|
|
170
|
+
<div className="dsh-atb-rpt-summary">{report.summary}</div>
|
|
171
|
+
{section('改动文件', report.changedFiles)}
|
|
172
|
+
{section('自验情况', report.checks)}
|
|
173
|
+
{section('产物', report.artifacts)}
|
|
174
|
+
{report.risk.length > 0 && (
|
|
175
|
+
<div className="dsh-atb-rpt-sec">
|
|
176
|
+
<div className="dsh-atb-rpt-label">剩余风险</div>
|
|
177
|
+
<div className="dsh-atb-rpt-risk">{report.risk}</div>
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The 0.3.0 isolation block: branch / baseline→head commits / change stats /
|
|
186
|
+
* uncommitted-changes warning, plus the user-only git actions (merge /
|
|
187
|
+
* remove worktree — plan §3.3).
|
|
188
|
+
*/
|
|
189
|
+
function IsolationBlock({ task, controller }: { task: TaskRecord; controller: BoardController }) {
|
|
190
|
+
const { alert: showAlert, el: alertEl } = useAlert()
|
|
191
|
+
const [confirmMerge, setConfirmMerge] = useState(false)
|
|
192
|
+
const [confirmRemove, setConfirmRemove] = useState<'wt' | 'wtb' | null>(null)
|
|
193
|
+
const [busy, setBusy] = useState(false)
|
|
194
|
+
// Diff viewer (0.4.0): which commit / changed path is expanded.
|
|
195
|
+
const [openDiff, setOpenDiff] = useState<{ commit?: string; path?: string } | null>(null)
|
|
196
|
+
const [dirtyOpen, setDirtyOpen] = useState(false)
|
|
197
|
+
const execution = latestIsolated(task)
|
|
198
|
+
const running = task.executions.some(e => e.outcome === 'running')
|
|
199
|
+
if (execution === undefined) return null
|
|
200
|
+
|
|
201
|
+
const doMerge = (): void => {
|
|
202
|
+
setBusy(true)
|
|
203
|
+
void controller.mergeBranch(task.id).then(result => {
|
|
204
|
+
setBusy(false)
|
|
205
|
+
setConfirmMerge(false)
|
|
206
|
+
if (!result.ok) showAlert(`合并失败:${result.error}`)
|
|
207
|
+
else if (result.noop === true) showAlert('该分支没有领先主工作区的新提交,无需合并(可退回续跑或直接清理)')
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const doRemove = (deleteBranch: boolean): void => {
|
|
212
|
+
setBusy(true)
|
|
213
|
+
void controller.removeWorktree(task.id, deleteBranch).then(result => {
|
|
214
|
+
setBusy(false)
|
|
215
|
+
setConfirmRemove(null)
|
|
216
|
+
if (!result.ok) showAlert(`删除失败:${result.error}`)
|
|
217
|
+
else if (result.branchError !== undefined) showAlert(`worktree 已删除,但分支删除失败:${result.branchError}`)
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Degraded / off isolation: one quiet line explaining why.
|
|
222
|
+
if (execution.isolation !== 'worktree' || execution.worktreePath === undefined) {
|
|
223
|
+
return (
|
|
224
|
+
<div className="dsh-atb-fieldcard" data-kind="isolation">
|
|
225
|
+
<div className="dsh-atb-fieldcard-label">执行隔离</div>
|
|
226
|
+
<div className="dsh-atb-iso-none">📁 原目录执行{execution.isolationNote !== undefined ? ` · ${execution.isolationNote}` : ''}</div>
|
|
227
|
+
{alertEl}
|
|
228
|
+
</div>
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const commits = execution.commits ?? []
|
|
233
|
+
const commitTotal = execution.commitsTotal ?? commits.length
|
|
234
|
+
const dirty = execution.dirtyFiles ?? []
|
|
235
|
+
const dirtyTotal = execution.dirtyFilesTotal ?? dirty.length
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div className="dsh-atb-fieldcard" data-kind="isolation">
|
|
239
|
+
<div className="dsh-atb-fieldcard-label">执行隔离 · Worktree</div>
|
|
240
|
+
<div className="dsh-atb-iso-facts">
|
|
241
|
+
<span className="dsh-atb-iso-fact" title={execution.worktreePath}>🌿 分支 <b>{execution.branch ?? task.branch}</b></span>
|
|
242
|
+
<span className="dsh-atb-iso-fact">基线 {shortHash(execution.baseCommit)} → {shortHash(execution.headCommit)}</span>
|
|
243
|
+
{execution.changedFiles !== undefined && execution.changedFiles > 0 && (
|
|
244
|
+
<span className="dsh-atb-iso-fact">改动 {execution.changedFiles} 个文件</span>
|
|
245
|
+
)}
|
|
246
|
+
{execution.diffStat !== undefined && <span className="dsh-atb-iso-fact" title={execution.diffStat}>{execution.diffStat}</span>}
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
{commits.length > 0
|
|
250
|
+
? (
|
|
251
|
+
<div className="dsh-atb-iso-commits">
|
|
252
|
+
{commits.slice(0, 10).map(c => (
|
|
253
|
+
<div key={c.hash} className="dsh-atb-iso-commit" data-open={openDiff?.commit === c.hash ? 'true' : undefined}>
|
|
254
|
+
<button
|
|
255
|
+
type="button"
|
|
256
|
+
className="dsh-atb-iso-commit-btn"
|
|
257
|
+
title="点击展开该提交的 diff"
|
|
258
|
+
onClick={() => setOpenDiff(openDiff?.commit === c.hash ? null : { commit: c.hash })}
|
|
259
|
+
>
|
|
260
|
+
<code>{shortHash(c.hash)}</code>
|
|
261
|
+
<span>{c.subject}</span>
|
|
262
|
+
</button>
|
|
263
|
+
{openDiff?.commit === c.hash && (
|
|
264
|
+
<DiffView controller={controller} task={task} execution={execution} commit={c.hash} />
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
))}
|
|
268
|
+
{commitTotal > 10 && <div className="dsh-atb-iso-more">… 共 {commitTotal} 个提交</div>}
|
|
269
|
+
</div>
|
|
270
|
+
)
|
|
271
|
+
: <div className="dsh-atb-iso-nocommit">该次执行没有产生提交(改动可能未提交,见下方警告)</div>}
|
|
272
|
+
|
|
273
|
+
{dirtyTotal > 0 && (
|
|
274
|
+
<div className="dsh-atb-iso-dirty">
|
|
275
|
+
<button type="button" className="dsh-atb-iso-dirty-toggle" onClick={() => setDirtyOpen(!dirtyOpen)}>
|
|
276
|
+
⚠ 有 {dirtyTotal} 处未提交修改(合并前请让 agent 提交,或手动处理){dirtyOpen ? ' ▲' : ' ▼ 查看文件'}
|
|
277
|
+
</button>
|
|
278
|
+
{dirtyOpen && (
|
|
279
|
+
<div className="dsh-atb-iso-dirty-files">
|
|
280
|
+
{dirty.slice(0, 30).map((line, index) => {
|
|
281
|
+
const filePath = porcelainPath(line)
|
|
282
|
+
return (
|
|
283
|
+
<button
|
|
284
|
+
key={`${line}-${index}`}
|
|
285
|
+
type="button"
|
|
286
|
+
className="dsh-atb-iso-dirty-file"
|
|
287
|
+
title="点击查看该文件的未提交 diff"
|
|
288
|
+
onClick={() => setOpenDiff(openDiff?.path === filePath ? null : { path: filePath })}
|
|
289
|
+
>
|
|
290
|
+
<code>{line.slice(0, 2)}</code> {filePath}
|
|
291
|
+
</button>
|
|
292
|
+
)
|
|
293
|
+
})}
|
|
294
|
+
{dirtyTotal > 30 && <div className="dsh-atb-iso-more">… 共 {dirtyTotal} 处(完整列表见任务台账)</div>}
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
297
|
+
{openDiff?.path !== undefined && dirtyOpen && (
|
|
298
|
+
<DiffView controller={controller} task={task} execution={execution} path={openDiff.path} />
|
|
299
|
+
)}
|
|
300
|
+
</div>
|
|
301
|
+
)}
|
|
302
|
+
|
|
303
|
+
<div className="dsh-atb-iso-actions">
|
|
304
|
+
{running
|
|
305
|
+
? <span className="dsh-atb-iso-hint">执行中 — 结束后可合并或清理</span>
|
|
306
|
+
: confirmMerge
|
|
307
|
+
? (
|
|
308
|
+
<span className="dsh-atb-confirm">
|
|
309
|
+
<span className="dsh-atb-confirm-label">将分支以 --no-ff 合并到主工作区?</span>
|
|
310
|
+
<button type="button" className="dsh-atb-btn" data-primary="true" disabled={busy} onClick={doMerge}>确认合并</button>
|
|
311
|
+
<button type="button" className="dsh-atb-btn" onClick={() => setConfirmMerge(false)}>取消</button>
|
|
312
|
+
</span>
|
|
313
|
+
)
|
|
314
|
+
: (
|
|
315
|
+
<button
|
|
316
|
+
type="button"
|
|
317
|
+
className="dsh-atb-btn"
|
|
318
|
+
disabled={busy}
|
|
319
|
+
title="在主工作区 git merge --no-ff 该任务分支(要求主区干净;冲突会原样报告)"
|
|
320
|
+
onClick={() => setConfirmMerge(true)}
|
|
321
|
+
>
|
|
322
|
+
⇥ 合并到主工作区
|
|
323
|
+
</button>
|
|
324
|
+
)}
|
|
325
|
+
{!running && (confirmRemove === null
|
|
326
|
+
? (
|
|
327
|
+
<>
|
|
328
|
+
<button
|
|
329
|
+
type="button"
|
|
330
|
+
className="dsh-atb-btn"
|
|
331
|
+
data-danger="true"
|
|
332
|
+
disabled={busy}
|
|
333
|
+
title="git worktree remove(有未提交修改时拒绝)"
|
|
334
|
+
onClick={() => setConfirmRemove('wt')}
|
|
335
|
+
>
|
|
336
|
+
🗑 删除 worktree
|
|
337
|
+
</button>
|
|
338
|
+
{task.branch !== undefined && (
|
|
339
|
+
<button
|
|
340
|
+
type="button"
|
|
341
|
+
className="dsh-atb-btn"
|
|
342
|
+
data-danger="true"
|
|
343
|
+
disabled={busy}
|
|
344
|
+
title="删除 worktree 并删除任务分支(有未提交修改时拒绝)"
|
|
345
|
+
onClick={() => setConfirmRemove('wtb')}
|
|
346
|
+
>
|
|
347
|
+
🗑 删 worktree + 分支
|
|
348
|
+
</button>
|
|
349
|
+
)}
|
|
350
|
+
</>
|
|
351
|
+
)
|
|
352
|
+
: (
|
|
353
|
+
<span className="dsh-atb-confirm">
|
|
354
|
+
<span className="dsh-atb-confirm-label">{confirmRemove === 'wtb' ? '删除 worktree 并删除分支?' : '删除 worktree 目录?'}</span>
|
|
355
|
+
<button type="button" className="dsh-atb-btn" data-danger="true" disabled={busy} onClick={() => doRemove(confirmRemove === 'wtb')}>确认删除</button>
|
|
356
|
+
<button type="button" className="dsh-atb-btn" onClick={() => setConfirmRemove(null)}>取消</button>
|
|
357
|
+
</span>
|
|
358
|
+
))}
|
|
359
|
+
{!running && confirmRemove === null && !confirmMerge && <span className="dsh-atb-iso-hint">分支与 worktree 保留中 — 可退回继续修改</span>}
|
|
360
|
+
</div>
|
|
361
|
+
{alertEl}
|
|
362
|
+
</div>
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
51
366
|
/**
|
|
52
367
|
* The detail view.
|
|
53
368
|
* @param task - the task record.
|
|
@@ -65,6 +380,7 @@ export function TaskDetail({ task, controller, now }: { task: TaskRecord; contro
|
|
|
65
380
|
const runningExecution = task.executions.find(e => e.outcome === 'running')
|
|
66
381
|
const holder = task.status === 'in_progress' ? task.claimedBy : undefined
|
|
67
382
|
const stale = now !== undefined && isStaleClaim(task, now)
|
|
383
|
+
const unchecked = (task.checklist ?? []).filter(i => !i.checked).length
|
|
68
384
|
|
|
69
385
|
/** Jump to an execution's session; prompt precisely when it cannot open. */
|
|
70
386
|
const jumpToSession = (sessionId: string): void => {
|
|
@@ -87,10 +403,20 @@ export function TaskDetail({ task, controller, now }: { task: TaskRecord; contro
|
|
|
87
403
|
<Chip tone={task.urgency}>● {URGENCY_LABEL[task.urgency] ?? task.urgency}</Chip>
|
|
88
404
|
<Chip icon="📁">{ws?.title ?? shortId(task.workspaceId)}</Chip>
|
|
89
405
|
{task.model !== undefined && <Chip icon="✦">{task.model.model}</Chip>}
|
|
406
|
+
{task.presetId !== undefined && <Chip icon="🎛" >{task.presetId}</Chip>}
|
|
90
407
|
{task.execution.mode === 'scheduled' && (
|
|
91
408
|
<Chip icon="⏰">{task.execution.cron} · 下次 {fmtTime(task.execution.nextRunAt)}</Chip>
|
|
92
409
|
)}
|
|
93
410
|
{task.blocked && <Chip icon="⛔" tone="urgent">受阻</Chip>}
|
|
411
|
+
{task.checklist !== undefined && task.checklist.length > 0 && (
|
|
412
|
+
<Chip icon="☑" tone={task.status === 'in_review' && task.checklist.some(i => !i.checked) ? 'urgent' : undefined}>
|
|
413
|
+
清单 {checklistProgress(task).done}/{task.checklist.length}
|
|
414
|
+
</Chip>
|
|
415
|
+
)}
|
|
416
|
+
{task.branch !== undefined && (
|
|
417
|
+
<Chip icon="🌿" tone={undefined}>Worktree · {task.branch.length > 28 ? `${task.branch.slice(0, 28)}…` : task.branch}</Chip>
|
|
418
|
+
)}
|
|
419
|
+
{(task.isolation === undefined || task.isolation === 'worktree') && task.branch === undefined && <Chip icon="🌿">Worktree 隔离</Chip>}
|
|
94
420
|
{holder !== undefined && (
|
|
95
421
|
<Chip icon={stale ? '⏱' : '🔑'} tone={stale ? 'urgent' : undefined}>
|
|
96
422
|
{stale ? '认领超时 · ' : '由 '}{shortId(holder)} 持有
|
|
@@ -113,6 +439,28 @@ export function TaskDetail({ task, controller, now }: { task: TaskRecord; contro
|
|
|
113
439
|
>
|
|
114
440
|
⧉ 复制
|
|
115
441
|
</button>
|
|
442
|
+
<button
|
|
443
|
+
type="button"
|
|
444
|
+
className="dsh-atb-detail-edit"
|
|
445
|
+
title="把此任务的配置(含清单)保存为模板,新建任务时可用"
|
|
446
|
+
onClick={() => {
|
|
447
|
+
void controller.saveAsTemplate(task).then(ok => {
|
|
448
|
+
if (ok) showAlert('已存为模板(新建任务 ▼ 下拉可用,可在模板管理中改名)')
|
|
449
|
+
})
|
|
450
|
+
}}
|
|
451
|
+
>
|
|
452
|
+
⌗ 存为模板
|
|
453
|
+
</button>
|
|
454
|
+
{canRun && task.branch !== undefined && (
|
|
455
|
+
<button
|
|
456
|
+
type="button"
|
|
457
|
+
className="dsh-atb-detail-run"
|
|
458
|
+
title="续跑:保留现有 worktree 与分支(上次的改动和提交都在原处),在其上继续执行;默认「立即执行」会重置为全新基线"
|
|
459
|
+
onClick={() => void controller.run(task.id, true)}
|
|
460
|
+
>
|
|
461
|
+
↻ 续跑
|
|
462
|
+
</button>
|
|
463
|
+
)}
|
|
116
464
|
{canRun && (
|
|
117
465
|
<button
|
|
118
466
|
type="button"
|
|
@@ -160,13 +508,21 @@ export function TaskDetail({ task, controller, now }: { task: TaskRecord; contro
|
|
|
160
508
|
</div>
|
|
161
509
|
)}
|
|
162
510
|
|
|
511
|
+
<IsolationBlock task={task} controller={controller} />
|
|
512
|
+
|
|
513
|
+
<ReportBlock task={task} />
|
|
514
|
+
|
|
515
|
+
<ChecklistBlock task={task} controller={controller} />
|
|
516
|
+
|
|
163
517
|
<div className="dsh-atb-detail-actions">
|
|
164
518
|
<div className="dsh-atb-movebtns">
|
|
165
519
|
{moveTargets(task).map(to => to === 'done'
|
|
166
520
|
? (confirmDone
|
|
167
521
|
? (
|
|
168
522
|
<span key={to} className="dsh-atb-confirm">
|
|
169
|
-
<span className="dsh-atb-confirm-label"
|
|
523
|
+
<span className="dsh-atb-confirm-label" data-tone={unchecked > 0 ? 'bad' : undefined}>
|
|
524
|
+
{unchecked > 0 ? `仍有 ${unchecked} 项清单未勾选,确认完成?` : '确认完成?'}
|
|
525
|
+
</span>
|
|
170
526
|
<button type="button" className="dsh-atb-btn" data-primary="true" onClick={() => { void controller.move(task.id, task.version, 'done'); setConfirmDone(false) }}>确认</button>
|
|
171
527
|
<button type="button" className="dsh-atb-btn" onClick={() => setConfirmDone(false)}>取消</button>
|
|
172
528
|
</span>
|