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.
- package/README.md +27 -160
- package/lib/client.js +2564 -678
- package/lib/host/execution.js +3 -0
- package/lib/host/execution.js.map +1 -1
- package/lib/host/routes.js +31 -1
- package/lib/host/routes.js.map +1 -1
- package/lib/host/session-sync.js +449 -0
- package/lib/host/session-sync.js.map +1 -0
- package/lib/host/store.js +9 -2
- package/lib/host/store.js.map +1 -1
- package/lib/index.js +109 -2
- package/lib/index.js.map +1 -1
- package/lib/shared/api.js.map +1 -1
- package/lib/shared/protocol.js +27 -1
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +75 -75
- package/src/client/api.ts +8 -0
- package/src/client/board/AlertModal.tsx +3 -1
- package/src/client/board/ImportModal.tsx +26 -24
- package/src/client/board/SettingsModal.tsx +98 -22
- package/src/client/board/SlashPromptInput.tsx +272 -0
- package/src/client/board/TaskBoard.tsx +53 -49
- package/src/client/board/TaskCard.tsx +33 -21
- package/src/client/board/TaskDetail.tsx +169 -104
- package/src/client/board/TaskFormModal.tsx +254 -202
- package/src/client/board/TemplateManager.tsx +32 -29
- package/src/client/board/labels.ts +36 -27
- package/src/client/controller.ts +62 -2
- package/src/client/i18n/en.ts +455 -0
- package/src/client/i18n/runtime.ts +155 -0
- package/src/client/i18n/zh.ts +460 -0
- package/src/client/index.ts +182 -42
- package/src/client/sidebar-entry.ts +13 -3
- package/src/client/styles.ts +131 -0
- package/src/host/execution.ts +14 -1
- package/src/host/routes.ts +49 -1
- package/src/host/session-sync.ts +650 -0
- package/src/host/store.ts +15 -1
- package/src/index.ts +125 -1
- package/src/shared/api.ts +49 -0
- package/src/shared/protocol.ts +54 -0
- 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 {
|
|
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">
|
|
70
|
-
<span className="dsh-atb-count">{live.length
|
|
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) }}
|
|
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() }}
|
|
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=
|
|
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=""
|
|
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"
|
|
129
|
-
<option value="updated"
|
|
130
|
-
<option value="urgency"
|
|
131
|
-
<option value="created"
|
|
132
|
-
<option value="title"
|
|
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
|
-
{
|
|
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=
|
|
151
|
-
<button type="button" className="dsh-atb-btn" title=
|
|
152
|
-
<button type="button" className="dsh-atb-btn" title=
|
|
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=
|
|
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
|
-
|
|
174
|
+
{t('board.export.json')}
|
|
173
175
|
</button>
|
|
174
176
|
<button
|
|
175
177
|
type="button"
|
|
176
178
|
className="dsh-atb-newmenu-opt"
|
|
177
|
-
title=
|
|
179
|
+
title={t('board.export.csvTitle')}
|
|
178
180
|
onClick={() => { closeExport(); controller.exportCsv() }}
|
|
179
181
|
>
|
|
180
|
-
|
|
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(
|
|
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
|
-
{
|
|
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"
|
|
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
|
|
297
|
-
<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=
|
|
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"
|
|
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
|
|
308
|
-
<div className="dsh-atb-diag-item"><b>{diag.tasks}</b><span
|
|
309
|
-
<div className="dsh-atb-diag-item" data-bad={diag.staleRunning > 0 ? 'true' : undefined}><b>{diag.staleRunning}</b><span
|
|
310
|
-
<div className="dsh-atb-diag-item" data-bad={diag.orphanWorktrees.length > 0 ? 'true' : undefined}><b>{diag.orphanWorktrees.length}</b><span
|
|
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
|
|
316
|
+
<h4>{t('diag.orphans.heading')}</h4>
|
|
314
317
|
{diag.orphanWorktrees.length === 0
|
|
315
|
-
? <div className="dsh-atb-empty2"
|
|
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)}
|
|
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"
|
|
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
|
|
332
|
+
<h4>{t('diag.gitignore.heading')}</h4>
|
|
330
333
|
{(diag.gitIgnoreSuggestions ?? []).length === 0
|
|
331
|
-
? <div className="dsh-atb-empty2"
|
|
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)} ·
|
|
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: '
|
|
361
|
-
{ label: '
|
|
362
|
-
{ label: '
|
|
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"
|
|
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"
|
|
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 {
|
|
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 =
|
|
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">{
|
|
89
|
-
{task.blocked && <span className="dsh-atb-badge" data-kind="blocked"
|
|
90
|
-
{stale && <span className="dsh-atb-badge" data-kind="stale"
|
|
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={
|
|
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"
|
|
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
|
-
{
|
|
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={
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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"
|
|
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=
|
|
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}
|
|
164
|
-
<button type="button" className="dsh-atb-quickbtn" data-act="reject-cancel" onClick={() => { setRejectOpen(false); setNote('') }}
|
|
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
|
))}
|