dsh-taskboard 0.5.4 → 0.6.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.
Files changed (42) hide show
  1. package/README.md +27 -160
  2. package/lib/client.js +2564 -678
  3. package/lib/host/execution.js +3 -0
  4. package/lib/host/execution.js.map +1 -1
  5. package/lib/host/routes.js +31 -1
  6. package/lib/host/routes.js.map +1 -1
  7. package/lib/host/session-sync.js +449 -0
  8. package/lib/host/session-sync.js.map +1 -0
  9. package/lib/host/store.js +9 -2
  10. package/lib/host/store.js.map +1 -1
  11. package/lib/index.js +109 -2
  12. package/lib/index.js.map +1 -1
  13. package/lib/shared/api.js.map +1 -1
  14. package/lib/shared/protocol.js +27 -1
  15. package/lib/shared/protocol.js.map +1 -1
  16. package/package.json +75 -75
  17. package/src/client/api.ts +8 -0
  18. package/src/client/board/AlertModal.tsx +3 -1
  19. package/src/client/board/ImportModal.tsx +26 -24
  20. package/src/client/board/SettingsModal.tsx +98 -22
  21. package/src/client/board/SlashPromptInput.tsx +272 -0
  22. package/src/client/board/TaskBoard.tsx +53 -49
  23. package/src/client/board/TaskCard.tsx +33 -21
  24. package/src/client/board/TaskDetail.tsx +169 -104
  25. package/src/client/board/TaskFormModal.tsx +254 -202
  26. package/src/client/board/TemplateManager.tsx +32 -29
  27. package/src/client/board/labels.ts +36 -27
  28. package/src/client/controller.ts +62 -2
  29. package/src/client/i18n/en.ts +455 -0
  30. package/src/client/i18n/runtime.ts +155 -0
  31. package/src/client/i18n/zh.ts +460 -0
  32. package/src/client/index.ts +182 -42
  33. package/src/client/sidebar-entry.ts +13 -3
  34. package/src/client/styles.ts +131 -0
  35. package/src/host/execution.ts +14 -1
  36. package/src/host/routes.ts +49 -1
  37. package/src/host/session-sync.ts +650 -0
  38. package/src/host/store.ts +15 -1
  39. package/src/index.ts +125 -1
  40. package/src/shared/api.ts +49 -0
  41. package/src/shared/protocol.ts +54 -0
  42. package/src/shared/version.ts +1 -1
@@ -9,7 +9,8 @@ import type { BoardController, ControllerState } from '../controller.ts'
9
9
  import type { TaskRecord, TaskStatus, Urgency } from '../../shared/protocol.ts'
10
10
  import { MAIN_STATUSES, canTransition } from '../../shared/protocol.ts'
11
11
  import { PLUGIN_VERSION } from '../../shared/version.ts'
12
- import { COLUMN_LABELS, URGENCY_LABEL } from './labels.ts'
12
+ import { COLUMN_KEYS, URGENCY_KEYS } from './labels.ts'
13
+ import { useT } from '../i18n/runtime.ts'
13
14
  import { fmtTime, isStaleClaim } from './format.ts'
14
15
  import { DRAG_TYPE, TaskCard } from './TaskCard.tsx'
15
16
  import { TaskDetail } from './TaskDetail.tsx'
@@ -43,6 +44,7 @@ export function filterTasks(state: ControllerState, tasks: TaskRecord[]): TaskRe
43
44
  * @param controller - the controller.
44
45
  */
45
46
  export function TaskBoard({ controller }: { controller: BoardController }) {
47
+ const t = useT()
46
48
  const state = useSyncExternalStore(
47
49
  cb => controller.subscribe(cb),
48
50
  () => controller.getSnapshot(),
@@ -66,8 +68,8 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
66
68
  return (
67
69
  <div className="dsh-atb-board">
68
70
  <div className="dsh-atb-toolbar">
69
- <h2 className="dsh-atb-title">Agent 任务看板</h2>
70
- <span className="dsh-atb-count">{live.length} 任务 · rev {state.ledger.revision}</span>
71
+ <h2 className="dsh-atb-title">{t('board.title')}</h2>
72
+ <span className="dsh-atb-count">{t('board.count.tasks', { n: live.length, rev: state.ledger.revision })}</span>
71
73
  <div className="dsh-atb-newmenu">
72
74
  <button
73
75
  type="button"
@@ -79,13 +81,13 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
79
81
  if (next) controller.prepareTemplateMenu()
80
82
  }}
81
83
  >
82
- + 新建任务 ▼
84
+ {t('board.action.newTask')}
83
85
  </button>
84
86
  {newMenuOpen && (
85
87
  <>
86
88
  <div className="dsh-atb-newmenu-backdrop" onClick={closeMenu} />
87
89
  <div className="dsh-atb-newmenu-list">
88
- <button type="button" className="dsh-atb-newmenu-opt" onClick={() => { closeMenu(); controller.setComposer(true) }}>空白任务</button>
90
+ <button type="button" className="dsh-atb-newmenu-opt" onClick={() => { closeMenu(); controller.setComposer(true) }}>{t('board.action.blankTask')}</button>
89
91
  {state.templates.map(t => (
90
92
  <button
91
93
  key={t.id}
@@ -98,7 +100,7 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
98
100
  </button>
99
101
  ))}
100
102
  <div className="dsh-atb-newmenu-sep" />
101
- <button type="button" className="dsh-atb-newmenu-opt" onClick={() => { closeMenu(); controller.openTemplateManager() }}>⌗ 管理模板…</button>
103
+ <button type="button" className="dsh-atb-newmenu-opt" onClick={() => { closeMenu(); controller.openTemplateManager() }}>{t('board.action.manageTemplates')}</button>
102
104
  </div>
103
105
  </>
104
106
  )}
@@ -107,7 +109,7 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
107
109
  <input
108
110
  className="dsh-atb-input dsh-atb-search"
109
111
  value={state.search}
110
- placeholder="搜索标题 / ID…"
112
+ placeholder={t('board.search.placeholder')}
111
113
  spellCheck={false}
112
114
  onChange={e => controller.setSearch(e.target.value)}
113
115
  />
@@ -116,20 +118,20 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
116
118
  value={state.filters.workspaceId ?? ''}
117
119
  onChange={e => controller.setWorkspaceFilter(e.target.value === '' ? undefined : e.target.value)}
118
120
  >
119
- <option value="">全部项目</option>
121
+ <option value="">{t('board.filter.allProjects')}</option>
120
122
  {state.workspaces.map(ws => <option key={ws.id} value={ws.id}>{ws.title || ws.path}</option>)}
121
123
  </select>
122
124
  <select
123
125
  className="dsh-atb-select"
124
126
  value={state.sortBy}
125
- title="列内排序"
127
+ title={t('board.sort.title')}
126
128
  onChange={e => controller.setSortBy(e.target.value as typeof state.sortBy)}
127
129
  >
128
- <option value="default">默认排序</option>
129
- <option value="updated">最近更新</option>
130
- <option value="urgency">按紧急度</option>
131
- <option value="created">创建时间</option>
132
- <option value="title">按标题</option>
130
+ <option value="default">{t('board.sort.default')}</option>
131
+ <option value="updated">{t('board.sort.updated')}</option>
132
+ <option value="urgency">{t('board.sort.urgency')}</option>
133
+ <option value="created">{t('board.sort.created')}</option>
134
+ <option value="title">{t('board.sort.byTitle')}</option>
133
135
  </select>
134
136
  {(['urgent', 'normal', 'relaxed'] as const).map(u => (
135
137
  <button
@@ -141,23 +143,23 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
141
143
  onClick={() => controller.toggleUrgency(u)}
142
144
  >
143
145
  <span className="dsh-atb-dot" data-urgency={u} />
144
- {URGENCY_LABEL[u]}
146
+ {t(URGENCY_KEYS[u])}
145
147
  </button>
146
148
  ))}
147
149
  <button type="button" className="dsh-atb-btn" onClick={() => controller.toggleSecondary()}>
148
- {state.secondaryOpen ? '返回看板' : '其它任务'}
150
+ {state.secondaryOpen ? t('board.action.backToBoard') : t('board.action.otherTasks')}
149
151
  </button>
150
- <button type="button" className="dsh-atb-btn" title="看板设置:新建任务的默认执行隔离等" onClick={() => controller.openSettings()}>🛠 设置</button>
151
- <button type="button" className="dsh-atb-btn" title="健康诊断:遗留 worktree、台账基本项" onClick={() => controller.openDiagnostics()}>⚙ 诊断</button>
152
- <button type="button" className="dsh-atb-btn" title="从 JSON 备份文件导入台账(预览后合并或整册替换)" onClick={() => controller.openImport()}>⬆ 导入</button>
152
+ <button type="button" className="dsh-atb-btn" title={t('board.action.settingsTitle')} onClick={() => controller.openSettings()}>{t('board.action.settings')}</button>
153
+ <button type="button" className="dsh-atb-btn" title={t('board.action.diagTitle')} onClick={() => controller.openDiagnostics()}>{t('board.action.diag')}</button>
154
+ <button type="button" className="dsh-atb-btn" title={t('board.action.importTitle')} onClick={() => controller.openImport()}>{t('board.action.import')}</button>
153
155
  <div className="dsh-atb-newmenu">
154
156
  <button
155
157
  type="button"
156
158
  className="dsh-atb-btn"
157
- title="导出台账:完整 JSON 备份或任务清单 CSV"
159
+ title={t('board.action.exportTitle')}
158
160
  onClick={() => setExportOpen(!exportOpen)}
159
161
  >
160
- ⬇ 导出 ▼
162
+ {t('board.action.export')}
161
163
  </button>
162
164
  {exportOpen && (
163
165
  <>
@@ -166,18 +168,18 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
166
168
  <button
167
169
  type="button"
168
170
  className="dsh-atb-newmenu-opt"
169
- title="完整台账备份(含执行历史与看板设置),可用于导入恢复"
171
+ title={t('board.export.jsonTitle')}
170
172
  onClick={() => { closeExport(); controller.exportJson() }}
171
173
  >
172
- 完整台账(JSON)
174
+ {t('board.export.json')}
173
175
  </button>
174
176
  <button
175
177
  type="button"
176
178
  className="dsh-atb-newmenu-opt"
177
- title="任务清单表格(Excel 可直接打开,中文已加 BOM)"
179
+ title={t('board.export.csvTitle')}
178
180
  onClick={() => { closeExport(); controller.exportCsv() }}
179
181
  >
180
- 任务清单(CSV)
182
+ {t('board.export.csv')}
181
183
  </button>
182
184
  </div>
183
185
  </>
@@ -221,7 +223,7 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
221
223
  const task = state.ledger.tasks.find(t => t.id === id)
222
224
  if (task === undefined || task.status === status) return
223
225
  if (!canTransition(task.status, status)) {
224
- showAlert(`无法从「${COLUMN_LABELS[task.status]}」拖至「${COLUMN_LABELS[status]}」`)
226
+ showAlert(t('board.drag.forbidden', { from: t(COLUMN_KEYS[task.status]), to: t(COLUMN_KEYS[status]) }))
225
227
  return
226
228
  }
227
229
  void controller.move(id, task.version, status)
@@ -229,7 +231,7 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
229
231
  >
230
232
  <div className="dsh-atb-colhead">
231
233
  <span className="dsh-atb-dot" data-status={status} />
232
- {COLUMN_LABELS[status]}
234
+ {t(COLUMN_KEYS[status])}
233
235
  <span className="dsh-atb-colcount">{columnTasks.length}</span>
234
236
  </div>
235
237
  <div className="dsh-atb-cards">
@@ -243,7 +245,7 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
243
245
  onAlert={showAlert}
244
246
  />
245
247
  ))}
246
- {columnTasks.length === 0 && <div className="dsh-atb-empty">无任务</div>}
248
+ {columnTasks.length === 0 && <div className="dsh-atb-empty">{t('board.empty')}</div>}
247
249
  </div>
248
250
  </div>
249
251
  )
@@ -281,6 +283,7 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
281
283
 
282
284
  /** ⚙ Health-diagnostics panel (plan §3.6): ledger basics + orphan worktrees + one-click cleanup. */
283
285
  function DiagnosticsPanel({ controller }: { controller: BoardController }) {
286
+ const t = useT()
284
287
  const state = controller.getSnapshot()
285
288
  const diag = state.diagnostics
286
289
  const wsName = (id: string): string => {
@@ -289,52 +292,52 @@ function DiagnosticsPanel({ controller }: { controller: BoardController }) {
289
292
  }
290
293
  return (
291
294
  <div className="dsh-atb-modal-backdrop" onClick={e => { if (e.target === e.currentTarget) controller.closeDiagnostics() }}>
292
- <div className="dsh-atb-modal dsh-atb-diag" role="dialog" aria-modal="true" aria-label="健康诊断">
295
+ <div className="dsh-atb-modal dsh-atb-diag" role="dialog" aria-modal="true" aria-label={t('diag.title')}>
293
296
  <div className="dsh-atb-modal-head">
294
297
  <span className="dsh-atb-modal-headicon">⚙</span>
295
298
  <div className="dsh-atb-modal-headtext">
296
- <h3>健康诊断</h3>
297
- <p>台账基本项与 worktree 遗留清理</p>
299
+ <h3>{t('diag.title')}</h3>
300
+ <p>{t('diag.subtitle')}</p>
298
301
  </div>
299
- <button type="button" className="dsh-atb-modal-close" aria-label="关闭" onClick={() => controller.closeDiagnostics()}>✕</button>
302
+ <button type="button" className="dsh-atb-modal-close" aria-label={t('shared.close')} onClick={() => controller.closeDiagnostics()}>✕</button>
300
303
  </div>
301
304
  <div className="dsh-atb-modal-body">
302
305
  {diag === undefined
303
- ? <div className="dsh-atb-empty2">读取中…</div>
306
+ ? <div className="dsh-atb-empty2">{t('shared.loading')}</div>
304
307
  : (
305
308
  <>
306
309
  <div className="dsh-atb-diag-grid">
307
- <div className="dsh-atb-diag-item"><b>{diag.revision}</b><span>台账修订号</span></div>
308
- <div className="dsh-atb-diag-item"><b>{diag.tasks}</b><span>任务总数</span></div>
309
- <div className="dsh-atb-diag-item" data-bad={diag.staleRunning > 0 ? 'true' : undefined}><b>{diag.staleRunning}</b><span>执行中</span></div>
310
- <div className="dsh-atb-diag-item" data-bad={diag.orphanWorktrees.length > 0 ? 'true' : undefined}><b>{diag.orphanWorktrees.length}</b><span>遗留 worktree</span></div>
310
+ <div className="dsh-atb-diag-item"><b>{diag.revision}</b><span>{t('diag.revision')}</span></div>
311
+ <div className="dsh-atb-diag-item"><b>{diag.tasks}</b><span>{t('diag.tasks')}</span></div>
312
+ <div className="dsh-atb-diag-item" data-bad={diag.staleRunning > 0 ? 'true' : undefined}><b>{diag.staleRunning}</b><span>{t('diag.running')}</span></div>
313
+ <div className="dsh-atb-diag-item" data-bad={diag.orphanWorktrees.length > 0 ? 'true' : undefined}><b>{diag.orphanWorktrees.length}</b><span>{t('diag.orphans')}</span></div>
311
314
  </div>
312
315
  <div className="dsh-atb-diag-sec">
313
- <h4>遗留 worktree(台账无主但目录存在)</h4>
316
+ <h4>{t('diag.orphans.heading')}</h4>
314
317
  {diag.orphanWorktrees.length === 0
315
- ? <div className="dsh-atb-empty2">无遗留 — 各项目 .dsh-worktrees 目录干净</div>
318
+ ? <div className="dsh-atb-empty2">{t('diag.orphans.none')}</div>
316
319
  : (
317
320
  <div className="dsh-atb-diag-orphans">
318
321
  {diag.orphanWorktrees.map(o => (
319
322
  <div key={o.path} className="dsh-atb-diag-orphan">
320
323
  <span className="dsh-atb-diag-orphan-path" title={o.path}>{wsName(o.workspaceId)} · {o.taskId}</span>
321
- <button type="button" className="dsh-atb-btn" data-danger="true" onClick={() => void controller.cleanupOrphan(o.workspaceId, o.taskId)}>清理</button>
324
+ <button type="button" className="dsh-atb-btn" data-danger="true" onClick={() => void controller.cleanupOrphan(o.workspaceId, o.taskId)}>{t('diag.orphans.cleanup')}</button>
322
325
  </div>
323
326
  ))}
324
327
  </div>
325
328
  )}
326
- <div className="dsh-atb-empty2">提示:有未提交修改的遗留目录会被拒绝清理,请先手动处理其内容。live 任务的 worktree 请在任务详情页删除。</div>
329
+ <div className="dsh-atb-empty2">{t('diag.orphans.hint')}</div>
327
330
  </div>
328
331
  <div className="dsh-atb-diag-sec">
329
- <h4>gitignore 建议</h4>
332
+ <h4>{t('diag.gitignore.heading')}</h4>
330
333
  {(diag.gitIgnoreSuggestions ?? []).length === 0
331
- ? <div className="dsh-atb-empty2">无待办 — 各 git 项目已忽略 .dsh-worktrees 目录</div>
334
+ ? <div className="dsh-atb-empty2">{t('diag.gitignore.none')}</div>
332
335
  : (
333
336
  <div className="dsh-atb-diag-orphans">
334
337
  {diag.gitIgnoreSuggestions.map(s => (
335
338
  <div key={s.workspaceId} className="dsh-atb-diag-orphan">
336
339
  <span className="dsh-atb-diag-orphan-path" title={s.workspacePath}>
337
- {wsName(s.workspaceId)} · 建议在 .gitignore 加入一行 <code>.dsh-worktrees/</code>(不会自动修改)
340
+ {wsName(s.workspaceId)} · {t('diag.gitignore.suggestA')} <code>.dsh-worktrees/</code>{t('diag.gitignore.suggestB')}
338
341
  </span>
339
342
  </div>
340
343
  ))}
@@ -351,20 +354,21 @@ function DiagnosticsPanel({ controller }: { controller: BoardController }) {
351
354
 
352
355
  /** Secondary tab: tasks grouped into canceled / archived / trashed columns. */
353
356
  function SecondaryTab({ controller, tasks }: { controller: BoardController; tasks: TaskRecord[] }) {
357
+ const t = useT()
354
358
  // Trashed takes precedence (a trashed task still carries its old status,
355
359
  // but what matters to the user is the pending purge).
356
360
  const trashed = tasks.filter(t => t.trashedAt !== undefined)
357
361
  const archived = tasks.filter(t => t.trashedAt === undefined && t.status === 'archived')
358
362
  const canceled = tasks.filter(t => t.trashedAt === undefined && t.status === 'canceled')
359
363
  const groups = [
360
- { label: '已取消', dot: 'canceled', rows: canceled },
361
- { label: '已归档', dot: 'archived', rows: archived },
362
- { label: '已删除', dot: 'trashed', rows: trashed },
364
+ { label: t('status.column.canceled'), dot: 'canceled', rows: canceled },
365
+ { label: t('status.column.archived'), dot: 'archived', rows: archived },
366
+ { label: t('board.group.trashed'), dot: 'trashed', rows: trashed },
363
367
  ]
364
368
  if (trashed.length + archived.length + canceled.length === 0) {
365
369
  return (
366
370
  <div className="dsh-atb-secondary">
367
- <div className="dsh-atb-empty">无已取消 / 已归档 / 已删除任务</div>
371
+ <div className="dsh-atb-empty">{t('board.secondary.empty')}</div>
368
372
  </div>
369
373
  )
370
374
  }
@@ -381,7 +385,7 @@ function SecondaryTab({ controller, tasks }: { controller: BoardController; task
381
385
  {group.rows.map(task => (
382
386
  <TaskCard key={task.id} task={task} controller={controller} />
383
387
  ))}
384
- {group.rows.length === 0 && <div className="dsh-atb-empty">无任务</div>}
388
+ {group.rows.length === 0 && <div className="dsh-atb-empty">{t('board.empty')}</div>}
385
389
  </div>
386
390
  </div>
387
391
  ))}
@@ -15,7 +15,8 @@ import { useState } from 'react'
15
15
  import type { BoardController } from '../controller.ts'
16
16
  import type { TaskRecord } from '../../shared/protocol.ts'
17
17
  import { fmtTime, isStaleClaim } from './format.ts'
18
- import { OUTCOME_LABEL, URGENCY_LABEL } from './labels.ts'
18
+ import { OUTCOME_KEYS, URGENCY_KEYS } from './labels.ts'
19
+ import { useT } from '../i18n/runtime.ts'
19
20
 
20
21
  /** dataTransfer type carrying the dragged task id. */
21
22
  export const DRAG_TYPE = 'application/x-dsh-atb-task'
@@ -35,6 +36,7 @@ function shortId(id: string | undefined): string {
35
36
  * @param onAlert - show an alert message (replaces native alert).
36
37
  */
37
38
  export function TaskCard({ task, controller, draggable = false, now, onAlert }: { task: TaskRecord; controller: BoardController; draggable?: boolean; now?: number; onAlert?: (msg: string) => void }) {
39
+ const t = useT()
38
40
  const [rejectOpen, setRejectOpen] = useState(false)
39
41
  const [note, setNote] = useState('')
40
42
  const last = task.executions.length > 0 ? task.executions[task.executions.length - 1] : undefined
@@ -63,7 +65,7 @@ export function TaskCard({ task, controller, draggable = false, now, onAlert }:
63
65
  // Block drag if a session is still executing this task
64
66
  if (running !== undefined) {
65
67
  e.preventDefault()
66
- const msg = `该任务正由会话执行中(${task.title}),不能拖动`
68
+ const msg = t('card.drag.running', { title: task.title })
67
69
  if (onAlert !== undefined) onAlert(msg)
68
70
  else alert(msg)
69
71
  return
@@ -85,53 +87,63 @@ export function TaskCard({ task, controller, draggable = false, now, onAlert }:
85
87
  >
86
88
  <div className="dsh-atb-card-title">{task.title}</div>
87
89
  <div className="dsh-atb-card-meta">
88
- <span className="dsh-atb-badge">{URGENCY_LABEL[task.urgency]}</span>
89
- {task.blocked && <span className="dsh-atb-badge" data-kind="blocked">受阻</span>}
90
- {stale && <span className="dsh-atb-badge" data-kind="stale">⏱ 认领超时</span>}
90
+ <span className="dsh-atb-badge">{t(URGENCY_KEYS[task.urgency])}</span>
91
+ {task.blocked && <span className="dsh-atb-badge" data-kind="blocked">{t('shared.blocked')}</span>}
92
+ {stale && <span className="dsh-atb-badge" data-kind="stale">{t('card.badge.stale')}</span>}
91
93
  {task.execution.mode === 'scheduled' && (
92
94
  <span className="dsh-atb-badge" data-kind="scheduled">⏰ {fmtTime(task.execution.nextRunAt)}</span>
93
95
  )}
94
96
  {task.model !== undefined && (
95
97
  <span
96
98
  className="dsh-atb-badge"
97
- title={`固定模型: ${task.model.provider}/${task.model.model}${task.model.reasoningEffort !== undefined ? ` · 思考强度: ${task.model.reasoningEffort}` : ''}`}
99
+ title={t('card.badge.modelTitle', { model: task.model.provider + '/' + task.model.model }) + (task.model.reasoningEffort !== undefined ? t('card.badge.modelEffort', { effort: task.model.reasoningEffort }) : '')}
98
100
  >
99
101
  {task.model.model}{task.model.reasoningEffort !== undefined ? ` (${task.model.reasoningEffort})` : ''}
100
102
  </span>
101
103
  )}
104
+ {task.permission === 'read-only' && (
105
+ <span className="dsh-atb-badge" data-kind="blocked" title={t('card.badge.permReadOnlyTitle')}>
106
+ {t('card.badge.permReadOnly')}
107
+ </span>
108
+ )}
109
+ {task.permission === 'danger-full-access' && (
110
+ <span className="dsh-atb-badge" data-kind="urgent" title={t('card.badge.permFullTitle')}>
111
+ {t('card.badge.permFull')}
112
+ </span>
113
+ )}
102
114
  {task.checklist !== undefined && task.checklist.length > 0 && (
103
115
  <span
104
116
  className="dsh-atb-badge"
105
117
  data-kind={task.status === 'in_review' && task.checklist.some(i => !i.checked) ? 'blocked' : 'checklist'}
106
- title={task.status === 'in_review' && task.checklist.some(i => !i.checked) ? '待验收:清单未全部勾选' : '验收清单进度'}
118
+ title={task.status === 'in_review' && task.checklist.some(i => !i.checked) ? t('card.badge.checklistReview') : t('card.badge.checklist')}
107
119
  >
108
120
  ☑ {task.checklist.filter(i => i.checked).length}/{task.checklist.length}
109
121
  </span>
110
122
  )}
111
- {task.status === 'done' && <span className="dsh-atb-badge" data-kind="done">完成</span>}
123
+ {task.status === 'done' && <span className="dsh-atb-badge" data-kind="done">{t('status.pill.done')}</span>}
112
124
  {last !== undefined && (
113
125
  <span className="dsh-atb-badge" data-kind={last.outcome === 'running' ? 'running' : last.outcome}>
114
- {OUTCOME_LABEL[last.outcome] ?? last.outcome}
126
+ {t(OUTCOME_KEYS[last.outcome] ?? last.outcome)}
115
127
  </span>
116
128
  )}
117
129
  {targetSessionId !== undefined && (
118
130
  <button
119
131
  type="button"
120
132
  className="dsh-atb-card-session"
121
- title={`点击一键跳转到会话:${targetSessionId}`}
133
+ title={t('card.session.jumpTitle', { id: targetSessionId })}
122
134
  onClick={(e) => {
123
135
  e.stopPropagation()
124
136
  void controller.openSession(targetSessionId).then(result => {
125
137
  if (result === 'missing') {
126
- const msg = `该会话已被删除(${shortId(targetSessionId)}),无法打开`
138
+ const msg = t('card.session.missing', { id: shortId(targetSessionId) })
127
139
  if (onAlert !== undefined) onAlert(msg)
128
140
  else alert(msg)
129
141
  } else if (result === 'archived') {
130
- const msg = `该会话已归档(${shortId(targetSessionId)}),已从会话列表隐藏`
142
+ const msg = t('card.session.archived', { id: shortId(targetSessionId) })
131
143
  if (onAlert !== undefined) onAlert(msg)
132
144
  else alert(msg)
133
145
  } else if (result === 'unavailable') {
134
- const msg = `会话导航不可用,会话 ID:${targetSessionId}`
146
+ const msg = t('card.session.unavailable', { id: targetSessionId })
135
147
  if (onAlert !== undefined) onAlert(msg)
136
148
  else alert(msg)
137
149
  }
@@ -142,7 +154,7 @@ export function TaskCard({ task, controller, draggable = false, now, onAlert }:
142
154
  </button>
143
155
  )}
144
156
  {task.comments.length > 0 && <span>💬 {task.comments.length}</span>}
145
- {task.trashedAt !== undefined && <span className="dsh-atb-badge" data-kind="trashed">待清除</span>}
157
+ {task.trashedAt !== undefined && <span className="dsh-atb-badge" data-kind="trashed">{t('card.badge.pendingPurge')}</span>}
146
158
  <span style={{ marginLeft: 'auto' }}>{fmtTime(task.updatedAt)}</span>
147
159
  </div>
148
160
  {reviewing && (rejectOpen
@@ -151,7 +163,7 @@ export function TaskCard({ task, controller, draggable = false, now, onAlert }:
151
163
  <input
152
164
  className="dsh-atb-input dsh-atb-quick-note"
153
165
  value={note}
154
- placeholder="退回原因(可选,agent 开工前会读)…"
166
+ placeholder={t('card.reject.placeholder')}
155
167
  autoFocus
156
168
  spellCheck={false}
157
169
  onChange={e => setNote(e.target.value)}
@@ -160,8 +172,8 @@ export function TaskCard({ task, controller, draggable = false, now, onAlert }:
160
172
  else if (e.key === 'Escape') { setRejectOpen(false); setNote('') }
161
173
  }}
162
174
  />
163
- <button type="button" className="dsh-atb-quickbtn" data-act="reject-confirm" onClick={submitReject}>退回待办</button>
164
- <button type="button" className="dsh-atb-quickbtn" data-act="reject-cancel" onClick={() => { setRejectOpen(false); setNote('') }}>取消</button>
175
+ <button type="button" className="dsh-atb-quickbtn" data-act="reject-confirm" onClick={submitReject}>{t('card.reject.confirm')}</button>
176
+ <button type="button" className="dsh-atb-quickbtn" data-act="reject-cancel" onClick={() => { setRejectOpen(false); setNote('') }}>{t('shared.cancel')}</button>
165
177
  </div>
166
178
  )
167
179
  : (
@@ -170,19 +182,19 @@ export function TaskCard({ task, controller, draggable = false, now, onAlert }:
170
182
  type="button"
171
183
  className="dsh-atb-quickbtn"
172
184
  data-act="done"
173
- title="验收完成:移至已完成"
185
+ title={t('card.action.doneTitle')}
174
186
  onClick={() => void controller.move(task.id, task.version, 'done')}
175
187
  >
176
- ✓ 完成
188
+ {t('card.action.done')}
177
189
  </button>
178
190
  <button
179
191
  type="button"
180
192
  className="dsh-atb-quickbtn"
181
193
  data-act="reject"
182
- title="退回待办,可附退回原因"
194
+ title={t('card.action.rejectTitle')}
183
195
  onClick={() => setRejectOpen(true)}
184
196
  >
185
- ✗ 退回
197
+ {t('card.action.reject')}
186
198
  </button>
187
199
  </div>
188
200
  ))}