@linxin666/dsh-client-ui-task-board 0.4.4 → 0.4.6
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.i18n.yaml +2 -2
- package/README.md +59 -20
- package/README.zh.md +59 -20
- package/lib/client.js +6208 -624
- package/lib/client.js.map +1 -1
- package/lib/index.js +4774 -493
- package/lib/types/client/PluginSettingsCard.d.ts +7 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +31 -3
- package/lib/types/client/board/ClampedMarkdown.d.ts +3 -0
- package/lib/types/client/board/ConfirmDialog.d.ts +4 -1
- package/lib/types/client/board/EditTaskModal.d.ts +5 -2
- package/lib/types/client/board/LinkSubtaskModal.d.ts +5 -2
- package/lib/types/client/board/NewTaskModal.d.ts +4 -1
- package/lib/types/client/board/TagManagerModal.d.ts +26 -0
- package/lib/types/client/board/TaskBoard.d.ts +10 -1
- package/lib/types/client/board/TaskCard.d.ts +14 -1
- package/lib/types/client/board/TaskDetail.d.ts +3 -1
- package/lib/types/client/board/TaskForm.d.ts +4 -1
- package/lib/types/client/board/VerificationReport.d.ts +23 -0
- package/lib/types/client/board/card-view.d.ts +113 -0
- package/lib/types/client/board/icons.d.ts +32 -0
- package/lib/types/client/board/overlay.d.ts +69 -0
- package/lib/types/client/board/status-key.d.ts +15 -0
- package/lib/types/client/board/task-markdown.d.ts +19 -0
- package/lib/types/client/index.d.ts +23 -5
- package/lib/types/client/locales.d.ts +168 -1
- package/lib/types/client/native-panel.d.ts +6 -20
- package/lib/types/client/plugin-card-seat.d.ts +5 -0
- package/lib/types/client/schedule-zone.d.ts +50 -0
- package/lib/types/client/seats.d.ts +18 -0
- package/lib/types/client/service.d.ts +32 -0
- package/lib/types/client/settings-form.d.ts +16 -0
- package/lib/types/core/controller.d.ts +46 -11
- package/lib/types/core/extension.d.ts +283 -0
- package/lib/types/core/handover.d.ts +13 -1
- package/lib/types/core/issue-form.d.ts +16 -0
- package/lib/types/core/poll-cadence.d.ts +31 -0
- package/lib/types/core/tasks.d.ts +228 -17
- package/lib/types/core/use-cases/task-schedule.d.ts +68 -10
- package/lib/types/core/use-cases/task-tag.d.ts +48 -0
- package/lib/types/core/use-cases/task-update.d.ts +5 -3
- package/lib/types/core/verification-budget.d.ts +52 -0
- package/lib/types/core/verification.d.ts +753 -0
- package/lib/types/core/workspace-target.d.ts +41 -0
- package/lib/types/host/agent-tools.d.ts +1 -1
- package/lib/types/host/extension-registry.d.ts +111 -0
- package/lib/types/host/llm-dispatch.d.ts +63 -0
- package/lib/types/host/verification-gate.d.ts +95 -0
- package/lib/types/host/verification-runner.d.ts +139 -0
- package/lib/types/host/workspace-evidence.d.ts +53 -0
- package/lib/types/host-ledger.d.ts +133 -14
- package/lib/types/host-runner.d.ts +55 -10
- package/lib/types/host-service.d.ts +205 -11
- package/lib/types/index.d.ts +134 -26
- package/lib/types/protocol.d.ts +92 -10
- package/lib/types/tool-surface.d.ts +71 -0
- package/package.json +28 -31
- package/src/client/PluginSettingsCard.tsx +9 -0
- package/src/client/TaskBoardSettingsCard.tsx +378 -66
- package/src/client/board/ClampedMarkdown.tsx +69 -0
- package/src/client/board/ConfirmDialog.tsx +7 -3
- package/src/client/board/EditTaskModal.tsx +5 -2
- package/src/client/board/LinkSubtaskModal.tsx +12 -7
- package/src/client/board/NewTaskModal.tsx +168 -21
- package/src/client/board/TagManagerModal.tsx +235 -0
- package/src/client/board/TaskBoard.tsx +259 -103
- package/src/client/board/TaskCard.tsx +214 -81
- package/src/client/board/TaskDetail.tsx +289 -43
- package/src/client/board/TaskForm.tsx +13 -3
- package/src/client/board/VerificationReport.tsx +322 -0
- package/src/client/board/card-view.ts +220 -0
- package/src/client/board/icons.tsx +89 -0
- package/src/client/board/overlay.tsx +189 -0
- package/src/client/board/status-key.ts +30 -0
- package/src/client/board/task-markdown.module.css +137 -0
- package/src/client/board/task-markdown.tsx +106 -0
- package/src/client/board-settings.module.css +66 -0
- package/src/client/board.module.css +488 -63
- package/src/client/index.ts +82 -51
- package/src/client/locales.ts +343 -6
- package/src/client/native-panel.tsx +18 -2
- package/src/client/plugin-card-seat.ts +7 -0
- package/src/client/schedule-zone.ts +100 -2
- package/src/client/seats.tsx +58 -0
- package/src/client/service.ts +94 -0
- package/src/client/settings-card.module.css +31 -5
- package/src/client/settings-form.ts +27 -1
- package/src/core/controller.ts +105 -10
- package/src/core/extension.ts +376 -0
- package/src/core/freeze-snapshot.ts +2 -2
- package/src/core/handover.ts +16 -1
- package/src/core/issue-form.ts +19 -0
- package/src/core/poll-cadence.ts +41 -0
- package/src/core/store.ts +80 -15
- package/src/core/tasks.ts +293 -45
- package/src/core/use-cases/task-create.ts +30 -9
- package/src/core/use-cases/task-parent.ts +3 -2
- package/src/core/use-cases/task-schedule.ts +190 -25
- package/src/core/use-cases/task-tag.ts +110 -0
- package/src/core/use-cases/task-update.ts +13 -7
- package/src/core/verification-budget.ts +115 -0
- package/src/core/verification.ts +1594 -0
- package/src/core/workspace-target.ts +59 -0
- package/src/host/agent-tools.ts +125 -38
- package/src/host/extension-registry.ts +509 -0
- package/src/host/llm-dispatch.ts +87 -0
- package/src/host/verification-gate.ts +487 -0
- package/src/host/verification-runner.ts +702 -0
- package/src/host/workspace-evidence.ts +176 -0
- package/src/host-ai.ts +15 -9
- package/src/host-ledger.ts +468 -71
- package/src/host-routes.ts +15 -2
- package/src/host-runner.ts +106 -24
- package/src/host-service.ts +546 -40
- package/src/index.ts +305 -21
- package/src/protocol.ts +226 -28
- package/src/tool-surface.ts +95 -0
- package/lib/invariant.js +0 -6
- package/lib/types/invariant.d.ts +0 -3
- package/src/invariant.ts +0 -4
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Task card: the board's column item.
|
|
3
|
-
* never executes anything directly (detail holds the Run
|
|
2
|
+
* Task card: the board's column item. The stretched open control opens the
|
|
3
|
+
* task detail — it never executes anything directly (detail holds the Run
|
|
4
|
+
* button).
|
|
5
|
+
*
|
|
6
|
+
* The card is an `article` with one stretched `button` rather than a button
|
|
7
|
+
* itself: interactive content inside a button is invalid HTML, and the card
|
|
8
|
+
* carries its own quick actions (open the execution session, a provider's
|
|
9
|
+
* link such as the tracker issue). The article owns the pointer click and
|
|
10
|
+
* ignores clicks that land on a quick action; the stretched open button is the
|
|
11
|
+
* keyboard and accessibility target, and its click bubbles into the same
|
|
12
|
+
* handler, so the detail opens exactly once either way.
|
|
4
13
|
*
|
|
5
14
|
* Memoized: the card re-renders only when its own task record changes, so a
|
|
6
15
|
* status/filter update on one card (or scrolling) never re-renders every
|
|
7
|
-
* card on the board. The per-card
|
|
8
|
-
* by the board, so the memo boundary is effective.
|
|
16
|
+
* card on the board. The per-card callbacks are built with a stable task
|
|
17
|
+
* reference by the board, so the memo boundary is effective.
|
|
9
18
|
*/
|
|
10
|
-
import { memo } from 'react'
|
|
19
|
+
import { memo, useMemo } from 'react'
|
|
11
20
|
import type { TaskRecord } from '../../core/tasks.ts'
|
|
12
|
-
import { executionLabel, tagTone } from '../../core/tasks.ts'
|
|
21
|
+
import { executionLabel, hasOpenExecution, tagTone } from '../../core/tasks.ts'
|
|
13
22
|
import { t } from '../locales.ts'
|
|
23
|
+
import { useTaskBoardSeats } from '../seats.tsx'
|
|
24
|
+
import { cardOutcome, cardTimeline, RESULT_KEY, splitTitleKind } from './card-view.ts'
|
|
25
|
+
import { IconClock, IconSession } from './icons.tsx'
|
|
26
|
+
import { verificationRunningKey } from './status-key.ts'
|
|
14
27
|
import css from '../board.module.css'
|
|
28
|
+
import { markdownExcerpt, markdownToPlainText } from './task-markdown.tsx'
|
|
15
29
|
|
|
16
30
|
/**
|
|
17
31
|
* Built formatters, keyed by time zone ('' = the browser's own). Constructing
|
|
@@ -56,11 +70,43 @@ export function formatTime(ms: number, timeZone?: string): string {
|
|
|
56
70
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
|
57
71
|
}
|
|
58
72
|
|
|
73
|
+
/** Cached YYYY-MM-DD formatters, keyed by time zone ('' = the browser's own). */
|
|
74
|
+
const CARD_DATE_FORMATS = new Map<string, Intl.DateTimeFormat>()
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Card time label: relative within a day, otherwise the calendar date read in
|
|
78
|
+
* the Host time zone (the same zone the tooltip and the detail view use), so a
|
|
79
|
+
* card never shows a browser-local date beside a Host-zone tooltip.
|
|
80
|
+
* @param ms - the instant.
|
|
81
|
+
* @param timeZone - Host IANA zone; absent uses the browser zone.
|
|
82
|
+
* @param now - the current instant.
|
|
83
|
+
*/
|
|
84
|
+
export function formatCardTime(ms: number, timeZone?: string, now: number = Date.now()): string {
|
|
85
|
+
const minutes = Math.floor((now - ms) / 60000)
|
|
86
|
+
if (minutes < 1) return t('time.justNow')
|
|
87
|
+
if (minutes < 60) return `${minutes}m`
|
|
88
|
+
if (minutes < 60 * 24) return `${Math.floor(minutes / 60)}h`
|
|
89
|
+
const key = timeZone ?? ''
|
|
90
|
+
try {
|
|
91
|
+
let format = CARD_DATE_FORMATS.get(key)
|
|
92
|
+
if (format === undefined) {
|
|
93
|
+
format = new Intl.DateTimeFormat('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit', ...(timeZone === undefined ? {} : { timeZone }) })
|
|
94
|
+
CARD_DATE_FORMATS.set(key, format)
|
|
95
|
+
}
|
|
96
|
+
return format.format(new Date(ms))
|
|
97
|
+
} catch {
|
|
98
|
+
// An unsupported zone id: never cached; fall back to the UTC calendar date.
|
|
99
|
+
return new Date(ms).toISOString().slice(0, 10)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
59
103
|
function TaskCardInner({
|
|
60
104
|
task,
|
|
61
105
|
pending,
|
|
62
106
|
timeZone,
|
|
63
107
|
onClick,
|
|
108
|
+
onOpenSession,
|
|
109
|
+
compact = false,
|
|
64
110
|
subtaskCount = 0,
|
|
65
111
|
isSubtask = false,
|
|
66
112
|
subtasksDone = 0,
|
|
@@ -71,6 +117,10 @@ function TaskCardInner({
|
|
|
71
117
|
pending: boolean
|
|
72
118
|
timeZone?: string
|
|
73
119
|
onClick: () => void
|
|
120
|
+
/** Open an execution session; absent renders the session mark inert. */
|
|
121
|
+
onOpenSession?: (sessionId: string) => void
|
|
122
|
+
/** Single-line density used by long settled columns. */
|
|
123
|
+
compact?: boolean
|
|
74
124
|
/** Direct subtasks of this card, shown as a badge (0 hides it). */
|
|
75
125
|
subtaskCount?: number
|
|
76
126
|
/** Whether this card is itself a subtask. */
|
|
@@ -82,16 +132,70 @@ function TaskCardInner({
|
|
|
82
132
|
/** Direct subtasks that failed, for the roll-up badge. */
|
|
83
133
|
subtasksFailed?: number
|
|
84
134
|
}) {
|
|
85
|
-
const
|
|
135
|
+
const { cardDecoration } = useTaskBoardSeats()
|
|
86
136
|
const runs = task.executions.length
|
|
137
|
+
const summary = useMemo(() => markdownToPlainText(task.title) || task.title, [task.title])
|
|
138
|
+
const title = useMemo(() => splitTitleKind(summary), [summary])
|
|
139
|
+
const excerpt = useMemo(() => (compact ? '' : markdownExcerpt(task.description)), [task.description, compact])
|
|
87
140
|
const archived = task.archivedAt !== undefined
|
|
88
|
-
|
|
141
|
+
// The lock is an open execution, not the column: a card parked in 'running'
|
|
142
|
+
// by hand stays draggable, and an executing card never is.
|
|
143
|
+
const busy = hasOpenExecution(task)
|
|
144
|
+
const isDraggable = !archived && !busy && !pending
|
|
145
|
+
const outcome = cardOutcome(task, archived)
|
|
146
|
+
const latest = outcome.latest
|
|
147
|
+
// Forced acceptance owns the running label.
|
|
148
|
+
const runningKey = verificationRunningKey(latest?.verification)
|
|
149
|
+
const timeline = cardTimeline(task, archived)
|
|
150
|
+
const resultText = latest?.result !== undefined ? t(RESULT_KEY[latest.result]) : ''
|
|
151
|
+
const sessionId = latest?.sessionId
|
|
152
|
+
const tagNames = (task.tags ?? []).map(tag => tag.name)
|
|
89
153
|
|
|
90
|
-
|
|
154
|
+
const ariaLabel = [
|
|
155
|
+
title.kind === undefined ? summary : `${title.kind}: ${title.text}`,
|
|
156
|
+
!archived && pending ? t('board.pending') : '',
|
|
157
|
+
!archived && busy ? t(runningKey ?? 'detail.result.running') : '',
|
|
158
|
+
!archived && !busy && resultText !== '' ? resultText : '',
|
|
159
|
+
outcome.declared ? t('card.declared') : '',
|
|
160
|
+
subtasksFailed > 0 ? t('card.subtasksFailed', { count: String(subtasksFailed) }) : '',
|
|
161
|
+
tagNames.length > 0 ? t('card.tagsLabel', { tags: tagNames.join(', ') }) : '',
|
|
162
|
+
].filter(Boolean).join(' · ')
|
|
163
|
+
// The tooltip names what the clamped surface cannot show — the full title and
|
|
164
|
+
// the absolute instant — not the whole excerpt again.
|
|
165
|
+
const tooltip = [
|
|
166
|
+
summary,
|
|
167
|
+
t(timeline.key, { time: formatHostTimestamp(timeline.at, timeZone) }),
|
|
168
|
+
outcome.declared ? t('card.declaredHint') : '',
|
|
169
|
+
].filter(Boolean).join('\n')
|
|
170
|
+
|
|
171
|
+
const sessionMark = sessionId !== undefined && (onOpenSession !== undefined ? (
|
|
91
172
|
<button
|
|
92
173
|
type="button"
|
|
174
|
+
className={`${css.cardSession} ${css.cardAction}`}
|
|
175
|
+
data-dsh-part="card-session"
|
|
176
|
+
title={t('card.openSession')}
|
|
177
|
+
aria-label={t('card.openSession')}
|
|
178
|
+
onClick={() => { onOpenSession(sessionId) }}
|
|
179
|
+
>
|
|
180
|
+
<IconSession size={14} />
|
|
181
|
+
</button>
|
|
182
|
+
) : (
|
|
183
|
+
<span className={css.cardSession} title={sessionId}>
|
|
184
|
+
<IconSession size={12} />
|
|
185
|
+
</span>
|
|
186
|
+
))
|
|
187
|
+
const decoration = <span className={css.cardDecoration}>{cardDecoration({ task })}</span>
|
|
188
|
+
const timeLabel = <span className={css.cardTime} data-dsh-part="card-time">{t(timeline.key, { time: formatCardTime(timeline.at, timeZone) })}</span>
|
|
189
|
+
const kindBadge = title.kind !== undefined && (
|
|
190
|
+
<span className={css.cardKind} data-dsh-part="card-kind">{title.kind}</span>
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<article
|
|
93
195
|
className={css.card}
|
|
94
196
|
data-status={archived ? 'archived' : task.status}
|
|
197
|
+
data-tone={outcome.tone}
|
|
198
|
+
data-compact={compact || undefined}
|
|
95
199
|
data-dsh-part="card"
|
|
96
200
|
data-pending={pending || undefined}
|
|
97
201
|
draggable={isDraggable}
|
|
@@ -99,82 +203,111 @@ function TaskCardInner({
|
|
|
99
203
|
event.dataTransfer.setData('text/plain', task.id)
|
|
100
204
|
event.dataTransfer.effectAllowed = 'move'
|
|
101
205
|
} : undefined}
|
|
102
|
-
|
|
103
|
-
|
|
206
|
+
title={tooltip}
|
|
207
|
+
onClick={(event) => {
|
|
208
|
+
// A quick action (session, provider link) handles its own click.
|
|
209
|
+
const target = event.target as Element
|
|
210
|
+
if (target.closest('a, button:not([data-dsh-part="card-open"])') !== null) return
|
|
211
|
+
onClick()
|
|
212
|
+
}}
|
|
104
213
|
>
|
|
105
|
-
<
|
|
106
|
-
{
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<span
|
|
110
|
-
key={tag.name}
|
|
111
|
-
className={css.cardTag}
|
|
112
|
-
data-tag-tone={tagTone(tag.name)}
|
|
113
|
-
data-dsh-part="tag-badge"
|
|
114
|
-
data-tag-hint={tag.promptPrefix === undefined ? undefined : tag.promptPrefix}
|
|
115
|
-
title={tag.promptPrefix === undefined ? tag.name : tag.promptPrefix}
|
|
116
|
-
>
|
|
117
|
-
{tag.name}
|
|
118
|
-
</span>
|
|
119
|
-
))}
|
|
120
|
-
</span>
|
|
121
|
-
)}
|
|
122
|
-
{task.description !== '' && <span className={css.cardExcerpt}>{task.description}</span>}
|
|
123
|
-
<span className={css.cardMeta}>
|
|
124
|
-
{isSubtask && (
|
|
125
|
-
<span className={css.cardSubtask} data-dsh-part="subtask-badge">{t('card.subtask')}</span>
|
|
126
|
-
)}
|
|
127
|
-
{subtaskCount > 0 && (
|
|
128
|
-
// A board that hides subtask cards still has to report them: the badge
|
|
129
|
-
// rolls up the direct children and takes the tone of the worst state,
|
|
130
|
-
// so a hidden failing tree cannot look green.
|
|
131
|
-
<span
|
|
132
|
-
className={css.cardSubtask}
|
|
133
|
-
data-dsh-part="subtask-count"
|
|
134
|
-
data-tone={subtasksFailed > 0 ? 'failed' : subtasksRunning > 0 ? 'running' : subtasksDone === subtaskCount ? 'done' : undefined}
|
|
135
|
-
title={t('card.subtasksBreakdown', {
|
|
136
|
-
total: String(subtaskCount),
|
|
137
|
-
done: String(subtasksDone),
|
|
138
|
-
running: String(subtasksRunning),
|
|
139
|
-
failed: String(subtasksFailed),
|
|
140
|
-
})}
|
|
141
|
-
>
|
|
142
|
-
{t('card.subtasks', { count: String(subtaskCount) })}
|
|
143
|
-
{subtasksFailed > 0 ? ' · ' + t('card.subtasksFailed', { count: String(subtasksFailed) }) : ''}
|
|
144
|
-
{subtasksFailed === 0 && subtasksRunning > 0 ? ' · ' + t('card.subtasksRunning', { count: String(subtasksRunning) }) : ''}
|
|
145
|
-
</span>
|
|
146
|
-
)}
|
|
147
|
-
<span className={css.cardTime}>{t('board.updated')} {formatTime(task.updatedAt)}</span>
|
|
148
|
-
{task.freeze !== undefined && (
|
|
149
|
-
<span className={css.cardSchedule} title={task.freeze.goal}>{t('card.frozen')}</span>
|
|
150
|
-
)}
|
|
151
|
-
{!archived && task.schedule?.enabled === true && (
|
|
152
|
-
<span
|
|
153
|
-
className={css.cardSchedule}
|
|
154
|
-
title={task.schedule.nextRunAt !== undefined
|
|
155
|
-
? `${t('card.scheduled')} · ${formatHostTimestamp(task.schedule.nextRunAt, timeZone)}`
|
|
156
|
-
: t('card.scheduled')}
|
|
157
|
-
>
|
|
158
|
-
{t('card.scheduled')}
|
|
159
|
-
</span>
|
|
160
|
-
)}
|
|
161
|
-
{latest !== undefined && (
|
|
162
|
-
<span className={css.cardRun} data-result={archived ? undefined : latest.result}>
|
|
163
|
-
{runs} {t('board.runs')}
|
|
164
|
-
</span>
|
|
165
|
-
)}
|
|
166
|
-
{latest?.sessionId !== undefined && (
|
|
167
|
-
<span className={css.cardSession} title={latest.sessionId}>⌁</span>
|
|
168
|
-
)}
|
|
169
|
-
{!archived && (task.status === 'running' || pending) && <span className={css.cardSpinner} aria-hidden="true" />}
|
|
214
|
+
<button type="button" className={css.cardOpen} data-dsh-part="card-open" aria-label={ariaLabel} />
|
|
215
|
+
<span className={css.cardTitleRow}>
|
|
216
|
+
{kindBadge}
|
|
217
|
+
<span className={css.cardTitle} data-dsh-part="card-title">{title.text}</span>
|
|
170
218
|
</span>
|
|
171
|
-
{
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
{
|
|
219
|
+
{compact ? (
|
|
220
|
+
<span className={css.cardMeta}>
|
|
221
|
+
{decoration}
|
|
222
|
+
{timeLabel}
|
|
223
|
+
{!archived && (busy || pending) && <span className={css.cardSpinner} aria-hidden="true" />}
|
|
175
224
|
</span>
|
|
225
|
+
) : (
|
|
226
|
+
<>
|
|
227
|
+
{task.tags !== undefined && task.tags.length > 0 && (
|
|
228
|
+
<span className={css.cardTags}>
|
|
229
|
+
{task.tags.map(tag => (
|
|
230
|
+
<span
|
|
231
|
+
key={tag.name}
|
|
232
|
+
className={css.cardTag}
|
|
233
|
+
data-tag-tone={tagTone(tag.name)}
|
|
234
|
+
data-dsh-part="tag-badge"
|
|
235
|
+
data-tag-hint={tag.promptPrefix === undefined ? undefined : tag.promptPrefix}
|
|
236
|
+
title={tag.promptPrefix === undefined ? tag.name : tag.promptPrefix}
|
|
237
|
+
>
|
|
238
|
+
{tag.name}
|
|
239
|
+
</span>
|
|
240
|
+
))}
|
|
241
|
+
</span>
|
|
242
|
+
)}
|
|
243
|
+
{excerpt !== '' && <span className={css.cardExcerpt} data-dsh-part="card-excerpt">{excerpt}</span>}
|
|
244
|
+
<span className={css.cardMeta}>
|
|
245
|
+
{decoration}
|
|
246
|
+
{isSubtask && (
|
|
247
|
+
<span className={css.cardSubtask} data-dsh-part="subtask-badge">{t('card.subtask')}</span>
|
|
248
|
+
)}
|
|
249
|
+
{subtaskCount > 0 && (
|
|
250
|
+
// A board that hides subtask cards still has to report them: the badge
|
|
251
|
+
// rolls up the direct children and takes the tone of the worst state,
|
|
252
|
+
// so a hidden failing tree cannot look green.
|
|
253
|
+
<span
|
|
254
|
+
className={css.cardSubtask}
|
|
255
|
+
data-dsh-part="subtask-count"
|
|
256
|
+
data-tone={subtasksFailed > 0 ? 'failed' : subtasksRunning > 0 ? 'running' : subtasksDone === subtaskCount ? 'done' : undefined}
|
|
257
|
+
title={t('card.subtasksBreakdown', {
|
|
258
|
+
total: String(subtaskCount),
|
|
259
|
+
done: String(subtasksDone),
|
|
260
|
+
running: String(subtasksRunning),
|
|
261
|
+
failed: String(subtasksFailed),
|
|
262
|
+
})}
|
|
263
|
+
>
|
|
264
|
+
{t('card.subtasks', { count: String(subtaskCount) })}
|
|
265
|
+
{subtasksFailed > 0 ? ' · ' + t('card.subtasksFailed', { count: String(subtasksFailed) }) : ''}
|
|
266
|
+
{subtasksFailed === 0 && subtasksRunning > 0 ? ' · ' + t('card.subtasksRunning', { count: String(subtasksRunning) }) : ''}
|
|
267
|
+
</span>
|
|
268
|
+
)}
|
|
269
|
+
{timeLabel}
|
|
270
|
+
{task.freeze !== undefined && (
|
|
271
|
+
<span className={css.cardSchedule} title={task.freeze.goal}>{t('card.frozen')}</span>
|
|
272
|
+
)}
|
|
273
|
+
{!archived && task.schedule?.enabled === true && (
|
|
274
|
+
<span
|
|
275
|
+
className={css.cardSchedule}
|
|
276
|
+
title={task.schedule.nextRunAt !== undefined
|
|
277
|
+
? `${t(task.schedule.mode === 'once' ? 'card.scheduledOnce' : 'card.scheduled')} · ${formatHostTimestamp(task.schedule.nextRunAt, timeZone)}`
|
|
278
|
+
: t(task.schedule.mode === 'once' ? 'card.scheduledOnce' : 'card.scheduled')}
|
|
279
|
+
>
|
|
280
|
+
<IconClock size={12} />
|
|
281
|
+
{t(task.schedule.mode === 'once' ? 'card.scheduledOnce' : 'card.scheduled')}
|
|
282
|
+
</span>
|
|
283
|
+
)}
|
|
284
|
+
{latest !== undefined && (
|
|
285
|
+
<span
|
|
286
|
+
className={css.cardRun}
|
|
287
|
+
data-dsh-part="card-run"
|
|
288
|
+
data-result={archived || latest.result === undefined ? undefined : latest.result}
|
|
289
|
+
data-declared={outcome.declared || undefined}
|
|
290
|
+
title={outcome.declared ? t('card.declaredHint') : undefined}
|
|
291
|
+
>
|
|
292
|
+
{!busy && resultText !== '' ? `${resultText} · ` : ''}
|
|
293
|
+
{runs} {t('board.runs')}
|
|
294
|
+
{outcome.declared ? ` · ${t('card.declared')}` : ''}
|
|
295
|
+
</span>
|
|
296
|
+
)}
|
|
297
|
+
{sessionMark}
|
|
298
|
+
{!archived && (busy || pending) && <span className={css.cardSpinner} aria-hidden="true" />}
|
|
299
|
+
</span>
|
|
300
|
+
{!archived && pending && <span className={css.cardRunningLabel}>{t('board.pending')}…</span>}
|
|
301
|
+
{!archived && latest !== undefined && executionLabel(latest) === 'running' && (
|
|
302
|
+
<span className={css.cardRunningLabel}>
|
|
303
|
+
{runningKey !== undefined
|
|
304
|
+
? t(runningKey)
|
|
305
|
+
: latest.ownResult === undefined ? t('detail.result.running') : t('detail.subtasks.waiting')}…
|
|
306
|
+
</span>
|
|
307
|
+
)}
|
|
308
|
+
</>
|
|
176
309
|
)}
|
|
177
|
-
</
|
|
310
|
+
</article>
|
|
178
311
|
)
|
|
179
312
|
}
|
|
180
313
|
|