@linxin666/dsh-client-ui-task-board 0.4.3 → 0.4.5
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 +56 -18
- package/README.zh.md +56 -18
- package/lib/client.js +6242 -958
- package/lib/client.js.map +1 -1
- package/lib/index.js +3475 -234
- package/lib/types/client/PluginSettingsCard.d.ts +7 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +19 -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 +23 -2
- 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/preset-label.d.ts +22 -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/board-card-seat.d.ts +34 -0
- package/lib/types/client/host-api.d.ts +24 -1
- package/lib/types/client/index.d.ts +19 -0
- package/lib/types/client/locales.d.ts +141 -3
- package/lib/types/client/native-panel.d.ts +6 -20
- package/lib/types/client/schedule-zone.d.ts +47 -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 +38 -1
- package/lib/types/core/degraded-reason.d.ts +46 -0
- 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/package-name.d.ts +9 -0
- package/lib/types/core/schedule.d.ts +95 -20
- package/lib/types/core/subtask.d.ts +30 -11
- package/lib/types/core/tasks.d.ts +110 -5
- package/lib/types/core/use-cases/task-create.d.ts +2 -1
- package/lib/types/core/use-cases/task-schedule.d.ts +8 -4
- 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.d.ts +386 -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 +71 -0
- package/lib/types/host/verification-runner.d.ts +108 -0
- package/lib/types/host/workspace-evidence.d.ts +53 -0
- package/lib/types/host-ledger.d.ts +107 -11
- package/lib/types/host-runner.d.ts +99 -4
- package/lib/types/host-service.d.ts +114 -2
- package/lib/types/index.d.ts +80 -26
- package/lib/types/protocol.d.ts +53 -3
- package/lib/types/tool-surface.d.ts +71 -0
- package/package.json +27 -26
- package/src/client/PluginSettingsCard.tsx +9 -0
- package/src/client/TaskBoardSettingsCard.tsx +304 -65
- 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 +30 -15
- package/src/client/board/NewTaskModal.tsx +288 -151
- package/src/client/board/TagManagerModal.tsx +235 -0
- package/src/client/board/TaskBoard.tsx +259 -103
- package/src/client/board/TaskCard.tsx +238 -86
- package/src/client/board/TaskDetail.tsx +154 -59
- package/src/client/board/TaskForm.tsx +61 -6
- package/src/client/board/VerificationReport.tsx +203 -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/preset-label.ts +44 -0
- package/src/client/board/status-key.ts +27 -0
- package/src/client/board/task-markdown.module.css +137 -0
- package/src/client/board/task-markdown.tsx +106 -0
- package/src/client/board-card-seat.ts +102 -0
- package/src/client/board-settings.module.css +66 -0
- package/src/client/board.module.css +602 -68
- package/src/client/host-api.ts +109 -6
- package/src/client/index.ts +48 -6
- package/src/client/locales.ts +290 -12
- package/src/client/native-panel.tsx +18 -2
- package/src/client/schedule-zone.ts +135 -0
- 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 +101 -5
- package/src/core/degraded-reason.ts +81 -0
- 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/package-name.ts +9 -0
- package/src/core/schedule.ts +211 -55
- package/src/core/store.ts +26 -1
- package/src/core/subtask.ts +77 -22
- package/src/core/tasks.ts +138 -10
- package/src/core/use-cases/task-create.ts +14 -2
- package/src/core/use-cases/task-parent.ts +3 -2
- package/src/core/use-cases/task-schedule.ts +17 -6
- package/src/core/use-cases/task-tag.ts +110 -0
- package/src/core/use-cases/task-update.ts +13 -4
- package/src/core/verification.ts +826 -0
- package/src/core/workspace-target.ts +59 -0
- package/src/host/agent-tools.ts +58 -24
- package/src/host/extension-registry.ts +509 -0
- package/src/host/llm-dispatch.ts +87 -0
- package/src/host/verification-gate.ts +331 -0
- package/src/host/verification-runner.ts +535 -0
- package/src/host/workspace-evidence.ts +176 -0
- package/src/host-ai.ts +15 -9
- package/src/host-ledger.ts +342 -51
- package/src/host-routes.ts +15 -2
- package/src/host-runner.ts +219 -20
- package/src/host-service.ts +346 -26
- package/src/index.ts +204 -16
- package/src/protocol.ts +122 -9
- package/src/tool-surface.ts +95 -0
- package/lib/types/host/run-guarded.d.ts +0 -39
- package/src/host/run-guarded.ts +0 -76
|
@@ -4,22 +4,30 @@
|
|
|
4
4
|
* delete (with confirmation), manual status moves, and a jump to the
|
|
5
5
|
* execution's session transcript.
|
|
6
6
|
*/
|
|
7
|
-
import { useEffect, useState } from 'react'
|
|
8
|
-
import type { BoardController } from '../../core/controller.ts'
|
|
7
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
8
|
+
import type { BoardController, ControllerSnapshot } from '../../core/controller.ts'
|
|
9
|
+
import type { TaskBoardExtensionActionRequest, TaskBoardExtensionDispatch } from '../../core/extension.ts'
|
|
10
|
+
import { useTaskBoardSeats } from '../seats.tsx'
|
|
9
11
|
import { isValidCron } from '../../core/schedule.ts'
|
|
10
|
-
import { MANUAL_STATUSES, TASK_PERMISSIONS, tagTone, type ExecutionRecord, type TaskPermission, type TaskRecord } from '../../core/tasks.ts'
|
|
12
|
+
import { MANUAL_STATUSES, TASK_PERMISSIONS, canMoveTask, hasOpenExecution, tagTone, type ExecutionRecord, type TaskPermission, type TaskRecord } from '../../core/tasks.ts'
|
|
11
13
|
import { canEditTaskContent } from '../../core/use-cases/task-update.ts'
|
|
12
14
|
import { requiresPermissionConfirmation } from '../../core/handover.ts'
|
|
13
15
|
import { DEFAULT_SUBTASK_DEPTH, directSubtasks, taskDepth } from '../../core/subtask.ts'
|
|
14
16
|
import { t, type TaskBoardKey } from '../locales.ts'
|
|
15
17
|
import { SCHEDULE_PRESETS } from '../schedule-presets.ts'
|
|
18
|
+
import { nextRunLabel, zoneChoices } from '../schedule-zone.ts'
|
|
16
19
|
import css from '../board.module.css'
|
|
17
20
|
import { ConfirmDialog } from './ConfirmDialog.tsx'
|
|
18
21
|
import { EditTaskModal, EditTagsModal } from './EditTaskModal.tsx'
|
|
22
|
+
import { IconClose, IconPlus, IconSession } from './icons.tsx'
|
|
19
23
|
import { LinkSubtaskModal } from './LinkSubtaskModal.tsx'
|
|
20
24
|
import { NewTaskModal } from './NewTaskModal.tsx'
|
|
25
|
+
import { useDialog, usePresence, type OverlayPhase } from './overlay.tsx'
|
|
26
|
+
import { inheritPresetLabel, presetLabel } from './preset-label.ts'
|
|
21
27
|
import { formatHostTimestamp, formatTime } from './TaskCard.tsx'
|
|
22
28
|
import { STATUS_KEY } from './status-key.ts'
|
|
29
|
+
import { VerificationReport } from './VerificationReport.tsx'
|
|
30
|
+
import { ClampedMarkdown } from './ClampedMarkdown.tsx'
|
|
23
31
|
|
|
24
32
|
/** Execution outcome → locale key. */
|
|
25
33
|
const RESULT_KEY: Record<NonNullable<ExecutionRecord['result']>, TaskBoardKey> = {
|
|
@@ -45,6 +53,7 @@ function ExecutionRow({ execution, timeZone, onOpen }: { execution: ExecutionRec
|
|
|
45
53
|
{t('detail.execution.initiator', { session: execution.initiatedBy })}
|
|
46
54
|
</span>
|
|
47
55
|
)}
|
|
56
|
+
{execution.verification !== undefined && <VerificationReport verification={execution.verification} />}
|
|
48
57
|
{execution.sessionId !== undefined && (
|
|
49
58
|
<button
|
|
50
59
|
type="button"
|
|
@@ -52,7 +61,8 @@ function ExecutionRow({ execution, timeZone, onOpen }: { execution: ExecutionRec
|
|
|
52
61
|
onClick={() => { onOpen(execution.sessionId as string) }}
|
|
53
62
|
title={execution.sessionId}
|
|
54
63
|
>
|
|
55
|
-
{t('detail.viewSession')}
|
|
64
|
+
{t('detail.viewSession')}
|
|
65
|
+
<IconSession size={13} />
|
|
56
66
|
</button>
|
|
57
67
|
)}
|
|
58
68
|
{execution.error !== undefined && execution.error !== '' && (
|
|
@@ -63,20 +73,16 @@ function ExecutionRow({ execution, timeZone, onOpen }: { execution: ExecutionRec
|
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
/** The execution-target editor: workspace / mode / permission pickers. */
|
|
66
|
-
function ExecutionSettingsSection({ controller, task, pending
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
setTeamRunAvailable(snapshot.host?.teamRunAvailable === true)
|
|
77
|
-
}),
|
|
78
|
-
[controller],
|
|
79
|
-
)
|
|
76
|
+
function ExecutionSettingsSection({ controller, task, pending, executionOptions, teamRunAvailable }: {
|
|
77
|
+
controller: BoardController
|
|
78
|
+
task: TaskRecord
|
|
79
|
+
pending: boolean
|
|
80
|
+
/** Picker option sets, owned by the detail overlay's snapshot. */
|
|
81
|
+
executionOptions: ControllerSnapshot['executionOptions']
|
|
82
|
+
/** Whether this deployment serves the Agent Teams service. */
|
|
83
|
+
teamRunAvailable: boolean
|
|
84
|
+
}) {
|
|
85
|
+
const options = executionOptions
|
|
80
86
|
const workspaceId = task.workspaceId ?? ''
|
|
81
87
|
const mode = task.mode ?? ''
|
|
82
88
|
const permission = task.permission ?? ''
|
|
@@ -107,18 +113,18 @@ function ExecutionSettingsSection({ controller, task, pending }: { controller: B
|
|
|
107
113
|
</select>
|
|
108
114
|
</label>
|
|
109
115
|
<label className={css.field}>
|
|
110
|
-
<span className={css.fieldLabel}>{t('new.
|
|
116
|
+
<span className={css.fieldLabel}>{t('new.agentPreset')}</span>
|
|
111
117
|
<select
|
|
112
118
|
className={css.select}
|
|
113
119
|
value={mode}
|
|
114
120
|
disabled={pending}
|
|
115
121
|
onChange={event => { controller.updateTask(task.id, { mode: event.target.value }) }}
|
|
116
122
|
>
|
|
117
|
-
<option value="">{
|
|
123
|
+
<option value="">{inheritPresetLabel(options.presets)}</option>
|
|
118
124
|
{!modeKnown && <option value={mode}>{mode}{t('exec.mode.removed')}</option>}
|
|
119
125
|
{options.presets.map(preset => (
|
|
120
126
|
<option key={preset.id} value={preset.id} disabled={preset.broken !== undefined}>
|
|
121
|
-
{preset
|
|
127
|
+
{presetLabel(preset)}
|
|
122
128
|
{preset.isDefault ? t('exec.mode.defaultSuffix') : ''}
|
|
123
129
|
{preset.broken !== undefined ? t('exec.mode.brokenSuffix') : ''}
|
|
124
130
|
</option>
|
|
@@ -164,6 +170,26 @@ function ExecutionSettingsSection({ controller, task, pending }: { controller: B
|
|
|
164
170
|
<span>{t('exec.reuseSession')}</span>
|
|
165
171
|
</label>
|
|
166
172
|
<p className={css.detailText}>{t('exec.reuseSessionHint')}</p>
|
|
173
|
+
<label className={css.scheduleToggle}>
|
|
174
|
+
<input
|
|
175
|
+
type="checkbox"
|
|
176
|
+
checked={task.goalRun !== false}
|
|
177
|
+
disabled={pending}
|
|
178
|
+
onChange={event => { controller.updateTask(task.id, { goalRun: event.target.checked }) }}
|
|
179
|
+
/>
|
|
180
|
+
<span>{t('exec.goalRun')}</span>
|
|
181
|
+
</label>
|
|
182
|
+
<p className={css.detailText}>{t('exec.goalRunHint')}</p>
|
|
183
|
+
<label className={css.scheduleToggle}>
|
|
184
|
+
<input
|
|
185
|
+
type="checkbox"
|
|
186
|
+
checked={task.skipVerification === true}
|
|
187
|
+
disabled={pending}
|
|
188
|
+
onChange={event => { controller.updateTask(task.id, { skipVerification: event.target.checked }) }}
|
|
189
|
+
/>
|
|
190
|
+
<span>{t('exec.skipVerification')}</span>
|
|
191
|
+
</label>
|
|
192
|
+
<p className={css.detailText}>{t('exec.skipVerificationHint')}</p>
|
|
167
193
|
<label className={css.scheduleToggle}>
|
|
168
194
|
<input
|
|
169
195
|
type="checkbox"
|
|
@@ -178,7 +204,7 @@ function ExecutionSettingsSection({ controller, task, pending }: { controller: B
|
|
|
178
204
|
)
|
|
179
205
|
}
|
|
180
206
|
|
|
181
|
-
/** The scheduled-runs editor: enable toggle, cron input + presets, next-run info. */
|
|
207
|
+
/** The scheduled-runs editor: enable toggle, cron input + presets, zone, next-run info. */
|
|
182
208
|
function ScheduleSection({ controller, task, pending }: { controller: BoardController; task: TaskRecord; pending: boolean }) {
|
|
183
209
|
const schedule = task.schedule
|
|
184
210
|
const [cron, setCron] = useState(schedule?.cron ?? '0 9 * * *')
|
|
@@ -186,7 +212,12 @@ function ScheduleSection({ controller, task, pending }: { controller: BoardContr
|
|
|
186
212
|
const [nextRunAt, setNextRunAt] = useState<number | undefined>(schedule?.nextRunAt)
|
|
187
213
|
const [lastTriggeredAt, setLastTriggeredAt] = useState<number | undefined>(schedule?.lastTriggeredAt)
|
|
188
214
|
const [error, setError] = useState<string | undefined>(undefined)
|
|
189
|
-
const
|
|
215
|
+
const hostTimeZone = controller.getSnapshot().host?.scheduler.timeZone
|
|
216
|
+
// The rule's own zone; '' means "follow the Host zone" (no stored zone).
|
|
217
|
+
const [zone, setZone] = useState<string>(schedule?.timeZone ?? '')
|
|
218
|
+
// One clock reading per render pass, so the absolute and relative halves of
|
|
219
|
+
// the label always describe the same instant.
|
|
220
|
+
const now = Date.now()
|
|
190
221
|
|
|
191
222
|
// Keep the editor in sync when the task record changes underneath (the
|
|
192
223
|
// schedule rolls forward as runs trigger).
|
|
@@ -195,8 +226,9 @@ function ScheduleSection({ controller, task, pending }: { controller: BoardContr
|
|
|
195
226
|
setEnabled(schedule?.enabled ?? false)
|
|
196
227
|
setNextRunAt(schedule?.nextRunAt)
|
|
197
228
|
setLastTriggeredAt(schedule?.lastTriggeredAt)
|
|
229
|
+
setZone(schedule?.timeZone ?? '')
|
|
198
230
|
setError(undefined)
|
|
199
|
-
}, [task.id, schedule?.enabled, schedule?.cron, schedule?.nextRunAt, schedule?.lastTriggeredAt])
|
|
231
|
+
}, [task.id, schedule?.enabled, schedule?.cron, schedule?.timeZone, schedule?.nextRunAt, schedule?.lastTriggeredAt])
|
|
200
232
|
|
|
201
233
|
/** Validate + persist the current cron text (Enter or blur). */
|
|
202
234
|
const saveCron = (value: string): void => {
|
|
@@ -210,6 +242,16 @@ function ScheduleSection({ controller, task, pending }: { controller: BoardContr
|
|
|
210
242
|
controller.setSchedule(task.id, { cron: trimmed })
|
|
211
243
|
}
|
|
212
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Change the rule's zone. `''` clears the stored zone, which is how a user
|
|
247
|
+
* returns a rule to following the Host zone.
|
|
248
|
+
*/
|
|
249
|
+
const changeZone = (value: string): void => {
|
|
250
|
+
setZone(value)
|
|
251
|
+
setError(undefined)
|
|
252
|
+
controller.setSchedule(task.id, { timeZone: value === '' ? null : value })
|
|
253
|
+
}
|
|
254
|
+
|
|
213
255
|
/** Arm/disarm the schedule (arming first persists the edited cron). */
|
|
214
256
|
const toggleEnabled = (next: boolean): void => {
|
|
215
257
|
const trimmed = cron.trim()
|
|
@@ -232,12 +274,18 @@ function ScheduleSection({ controller, task, pending }: { controller: BoardContr
|
|
|
232
274
|
controller.setSchedule(task.id, { cron: preset })
|
|
233
275
|
}
|
|
234
276
|
|
|
277
|
+
// The zone the rule's wall clock is actually read in: its own when set,
|
|
278
|
+
// otherwise the Host zone. The preview must use the same one the Host will.
|
|
279
|
+
const effectiveZone = zone === '' ? hostTimeZone : zone
|
|
235
280
|
const nextLabel = !enabled || nextRunAt === undefined
|
|
236
281
|
? t('detail.schedule.notScheduled')
|
|
237
|
-
: nextRunAt <=
|
|
282
|
+
: nextRunAt <= now
|
|
238
283
|
? t('detail.schedule.dueSoon')
|
|
239
|
-
:
|
|
240
|
-
const lastLabel = lastTriggeredAt === undefined ? '—' : formatHostTimestamp(lastTriggeredAt,
|
|
284
|
+
: nextRunLabel(nextRunAt, effectiveZone, formatHostTimestamp, now)
|
|
285
|
+
const lastLabel = lastTriggeredAt === undefined ? '—' : formatHostTimestamp(lastTriggeredAt, effectiveZone)
|
|
286
|
+
// The rule's own stored zone rides the list even when this runtime's
|
|
287
|
+
// inventory does not carry it, so opening the editor cannot rewrite it.
|
|
288
|
+
const zones = zoneChoices(hostTimeZone, schedule?.timeZone)
|
|
241
289
|
|
|
242
290
|
return (
|
|
243
291
|
<section className={css.detailSection}>
|
|
@@ -276,6 +324,23 @@ function ScheduleSection({ controller, task, pending }: { controller: BoardContr
|
|
|
276
324
|
))}
|
|
277
325
|
</select>
|
|
278
326
|
</div>
|
|
327
|
+
{/* A list is used instead of a free-text field so only zones the Host can
|
|
328
|
+
also resolve are offered; the Host zone entry clears the stored value. */}
|
|
329
|
+
<label className={css.scheduleZone}>
|
|
330
|
+
<span>{t('detail.schedule.timeZone')}</span>
|
|
331
|
+
<select
|
|
332
|
+
className={css.schedulePreset}
|
|
333
|
+
value={zone}
|
|
334
|
+
disabled={pending}
|
|
335
|
+
aria-label={t('detail.schedule.timeZone')}
|
|
336
|
+
title={t('detail.schedule.timeZoneHint')}
|
|
337
|
+
onChange={event => { changeZone(event.target.value) }}
|
|
338
|
+
>
|
|
339
|
+
{zones.map(choice => (
|
|
340
|
+
<option key={choice.id === '' ? '__host' : choice.id} value={choice.id}>{choice.label}</option>
|
|
341
|
+
))}
|
|
342
|
+
</select>
|
|
343
|
+
</label>
|
|
279
344
|
{error !== undefined && <p className={css.formError}>{error}</p>}
|
|
280
345
|
<p className={css.scheduleMeta}>
|
|
281
346
|
{t('detail.schedule.nextRun')} {nextLabel}
|
|
@@ -290,19 +355,18 @@ function ScheduleSection({ controller, task, pending }: { controller: BoardContr
|
|
|
290
355
|
* and the actions that grow or prune the tree. Every gate here mirrors the
|
|
291
356
|
* Host lineage gate for affordance only; the Host re-checks the action.
|
|
292
357
|
*/
|
|
293
|
-
function SubtaskSection({ controller, task, pending, archived }: {
|
|
358
|
+
function SubtaskSection({ controller, task, pending, archived, snapshot }: {
|
|
294
359
|
controller: BoardController
|
|
295
360
|
task: TaskRecord
|
|
296
361
|
pending: boolean
|
|
297
362
|
archived: boolean
|
|
363
|
+
/** Detail-overlay snapshot; the overlay already re-renders on every notify. */
|
|
364
|
+
snapshot: ControllerSnapshot
|
|
298
365
|
}) {
|
|
299
|
-
const [snapshot, setSnapshot] = useState(controller.getSnapshot())
|
|
300
|
-
useEffect(
|
|
301
|
-
() => controller.subscribe(() => setSnapshot(controller.getSnapshot())),
|
|
302
|
-
[controller],
|
|
303
|
-
)
|
|
304
366
|
const [showAdd, setShowAdd] = useState(false)
|
|
305
367
|
const [showLink, setShowLink] = useState(false)
|
|
368
|
+
const addPresence = usePresence(showAdd)
|
|
369
|
+
const linkPresence = usePresence(showLink)
|
|
306
370
|
const tasks = snapshot.tasks
|
|
307
371
|
const parent = task.parentId === undefined ? undefined : tasks.find(candidate => candidate.id === task.parentId)
|
|
308
372
|
const children = directSubtasks(tasks, task.id)
|
|
@@ -344,8 +408,8 @@ function SubtaskSection({ controller, task, pending, archived }: {
|
|
|
344
408
|
<button
|
|
345
409
|
type="button"
|
|
346
410
|
className={css.linkButton}
|
|
347
|
-
disabled={pending || child
|
|
348
|
-
title={child
|
|
411
|
+
disabled={pending || hasOpenExecution(child)}
|
|
412
|
+
title={hasOpenExecution(child) ? t('detail.subtasks.runningLock') : undefined}
|
|
349
413
|
onClick={() => { void controller.setParent(child.id, null) }}
|
|
350
414
|
>
|
|
351
415
|
{t('detail.subtasks.detach')}
|
|
@@ -363,7 +427,8 @@ function SubtaskSection({ controller, task, pending, archived }: {
|
|
|
363
427
|
? (
|
|
364
428
|
<div className={css.subtaskAddRow}>
|
|
365
429
|
<button type="button" className={css.ghostButton} disabled={!editable} onClick={() => { setShowAdd(true) }}>
|
|
366
|
-
|
|
430
|
+
<IconPlus size={14} />
|
|
431
|
+
{t('detail.subtasks.add')}
|
|
367
432
|
</button>
|
|
368
433
|
<button type="button" className={css.ghostButton} disabled={!editable} onClick={() => { setShowLink(true) }}>
|
|
369
434
|
{t('detail.subtasks.link')}
|
|
@@ -372,18 +437,19 @@ function SubtaskSection({ controller, task, pending, archived }: {
|
|
|
372
437
|
)
|
|
373
438
|
: <p className={css.detailMeta}>{t('detail.subtasks.hostOnly')}</p>)
|
|
374
439
|
: <p className={css.detailMeta}>{t('detail.subtasks.depthLimit', { depth: String(limit) })}</p>)}
|
|
375
|
-
{
|
|
376
|
-
<NewTaskModal controller={controller} parentTask={task} onClose={() => { setShowAdd(false) }} />
|
|
440
|
+
{addPresence.mounted && (
|
|
441
|
+
<NewTaskModal controller={controller} parentTask={task} phase={addPresence.phase} onClose={() => { setShowAdd(false) }} />
|
|
377
442
|
)}
|
|
378
|
-
{
|
|
379
|
-
<LinkSubtaskModal controller={controller} parent={task} onClose={() => { setShowLink(false) }} />
|
|
443
|
+
{linkPresence.mounted && (
|
|
444
|
+
<LinkSubtaskModal controller={controller} parent={task} phase={linkPresence.phase} onClose={() => { setShowLink(false) }} />
|
|
380
445
|
)}
|
|
381
446
|
</section>
|
|
382
447
|
)
|
|
383
448
|
}
|
|
384
449
|
|
|
450
|
+
|
|
385
451
|
/** Task detail overlay. */
|
|
386
|
-
export function TaskDetail({ controller, task }: { controller: BoardController; task: TaskRecord }) {
|
|
452
|
+
export function TaskDetail({ controller, task, phase = 'open' }: { controller: BoardController; task: TaskRecord; phase?: OverlayPhase }) {
|
|
387
453
|
const [confirmDelete, setConfirmDelete] = useState(false)
|
|
388
454
|
const [showEdit, setShowEdit] = useState(false)
|
|
389
455
|
const [showEditTags, setShowEditTags] = useState(false)
|
|
@@ -399,18 +465,36 @@ export function TaskDetail({ controller, task }: { controller: BoardController;
|
|
|
399
465
|
setShowDuplicate(false)
|
|
400
466
|
}, [task.id])
|
|
401
467
|
const current = latest
|
|
468
|
+
const seats = useTaskBoardSeats()
|
|
469
|
+
const dispatch = useCallback(
|
|
470
|
+
(request: TaskBoardExtensionActionRequest): Promise<boolean> => controller.dispatchExtension(request),
|
|
471
|
+
[controller],
|
|
472
|
+
) as TaskBoardExtensionDispatch
|
|
402
473
|
const snapshot = controller.getSnapshot()
|
|
403
|
-
|
|
474
|
+
// Busy means the runner is executing this card, which is an execution the
|
|
475
|
+
// detail view can see; the column alone never locks a card.
|
|
476
|
+
const busy = hasOpenExecution(current)
|
|
404
477
|
const archived = current.archivedAt !== undefined
|
|
405
478
|
const pending = snapshot.pendingTaskIds.includes(current.id)
|
|
406
479
|
const transportError = snapshot.transportError
|
|
407
480
|
const timeZone = snapshot.host?.scheduler.timeZone
|
|
408
481
|
const permissionPending = requiresPermissionConfirmation(current, snapshot.host?.sessionDefaultPermission)
|
|
409
482
|
const subtaskChildren = directSubtasks(snapshot.tasks, current.id)
|
|
483
|
+
const dialog = useDialog<HTMLDivElement>(() => { controller.closeTask() }, phase)
|
|
484
|
+
// One presence per nested overlay: the detail view sits under up to four of
|
|
485
|
+
// them, and each has to run its own exit leg instead of the surface being
|
|
486
|
+
// pulled out of the DOM the moment its flag flips.
|
|
487
|
+
const editOpen = showEdit && !archived && canEditTaskContent(current)
|
|
488
|
+
const tagsOpen = showEditTags && !archived && !busy
|
|
489
|
+
const duplicateOpen = showDuplicate && !archived
|
|
490
|
+
const confirmPresence = usePresence(confirmDelete)
|
|
491
|
+
const editPresence = usePresence(editOpen)
|
|
492
|
+
const tagsPresence = usePresence(tagsOpen)
|
|
493
|
+
const duplicatePresence = usePresence(duplicateOpen)
|
|
410
494
|
|
|
411
495
|
return (
|
|
412
|
-
<div className={css.modalBackdrop}
|
|
413
|
-
<div className={css.detail} role="dialog" aria-label={t('detail.title')}>
|
|
496
|
+
<div className={css.modalBackdrop} data-state={phase} onMouseDown={dialog.onMouseDown}>
|
|
497
|
+
<div ref={dialog.attach} className={css.detail} role="dialog" aria-modal="true" aria-label={t('detail.title')} tabIndex={-1}>
|
|
414
498
|
<header className={css.detailHeader}>
|
|
415
499
|
<h2 className={css.detailTitle}>{current.title}</h2>
|
|
416
500
|
<span className={css.statusBadge} data-status={archived ? 'archived' : current.status}>
|
|
@@ -420,9 +504,10 @@ export function TaskDetail({ controller, task }: { controller: BoardController;
|
|
|
420
504
|
type="button"
|
|
421
505
|
className={css.iconButton}
|
|
422
506
|
aria-label={t('detail.close')}
|
|
507
|
+
title={t('detail.close')}
|
|
423
508
|
onClick={() => { controller.closeTask() }}
|
|
424
509
|
>
|
|
425
|
-
|
|
510
|
+
<IconClose size={16} />
|
|
426
511
|
</button>
|
|
427
512
|
</header>
|
|
428
513
|
|
|
@@ -437,10 +522,10 @@ export function TaskDetail({ controller, task }: { controller: BoardController;
|
|
|
437
522
|
)}
|
|
438
523
|
<section className={css.detailSection}>
|
|
439
524
|
<h4>{t('detail.description')}</h4>
|
|
440
|
-
<
|
|
525
|
+
<ClampedMarkdown source={current.description !== '' ? current.description : '—'} />
|
|
441
526
|
</section>
|
|
442
527
|
|
|
443
|
-
<SubtaskSection controller={controller} task={current} pending={pending} archived={archived} />
|
|
528
|
+
<SubtaskSection controller={controller} task={current} pending={pending} archived={archived} snapshot={snapshot} />
|
|
444
529
|
|
|
445
530
|
{current.tags !== undefined && current.tags.length > 0 && (
|
|
446
531
|
<section className={css.detailSection} data-dsh-part="tags">
|
|
@@ -484,7 +569,7 @@ export function TaskDetail({ controller, task }: { controller: BoardController;
|
|
|
484
569
|
<h4>{t('detail.handover')}</h4>
|
|
485
570
|
<p className={css.detailText}>
|
|
486
571
|
{t('new.workspace')}: {current.handover.workspaceId ?? t('exec.workspace.recent')}
|
|
487
|
-
{' · '}{t('new.
|
|
572
|
+
{' · '}{t('new.agentPreset')}: {current.handover.mode ?? t('exec.mode.inherit')}
|
|
488
573
|
{' · '}{t('new.permission')}: {current.handover.permission === undefined ? t('exec.permission.default') : t(`exec.permission.${current.handover.permission}` as TaskBoardKey)}
|
|
489
574
|
</p>
|
|
490
575
|
<p className={css.detailText}><strong>{t('detail.handover.references')}</strong></p>
|
|
@@ -511,14 +596,22 @@ export function TaskDetail({ controller, task }: { controller: BoardController;
|
|
|
511
596
|
<p className={css.detailMeta}>{t('detail.permissionConfirmed', { time: formatHostTimestamp(current.permissionConfirmedAt, timeZone) })}</p>
|
|
512
597
|
)}
|
|
513
598
|
|
|
599
|
+
{seats.detailSection({ task: current, dispatch })}
|
|
600
|
+
|
|
514
601
|
<section className={css.detailSection}>
|
|
515
602
|
<h4>{t('detail.prompt')}</h4>
|
|
516
|
-
<
|
|
603
|
+
<ClampedMarkdown source={current.prompt !== '' ? current.prompt : current.title} />
|
|
517
604
|
</section>
|
|
518
605
|
|
|
519
606
|
{!archived && (
|
|
520
607
|
<>
|
|
521
|
-
<ExecutionSettingsSection
|
|
608
|
+
<ExecutionSettingsSection
|
|
609
|
+
controller={controller}
|
|
610
|
+
task={current}
|
|
611
|
+
pending={pending}
|
|
612
|
+
executionOptions={snapshot.executionOptions}
|
|
613
|
+
teamRunAvailable={snapshot.host?.teamRunAvailable === true}
|
|
614
|
+
/>
|
|
522
615
|
<ScheduleSection controller={controller} task={current} pending={pending} />
|
|
523
616
|
</>
|
|
524
617
|
)}
|
|
@@ -550,7 +643,7 @@ export function TaskDetail({ controller, task }: { controller: BoardController;
|
|
|
550
643
|
key={status}
|
|
551
644
|
type="button"
|
|
552
645
|
className={css.ghostButton}
|
|
553
|
-
disabled={
|
|
646
|
+
disabled={pending || !canMoveTask(current, status)}
|
|
554
647
|
onClick={() => { controller.moveTask(current.id, status) }}
|
|
555
648
|
>
|
|
556
649
|
{t(`status.move.${status}` as TaskBoardKey)}
|
|
@@ -573,7 +666,7 @@ export function TaskDetail({ controller, task }: { controller: BoardController;
|
|
|
573
666
|
{t('detail.edit')}
|
|
574
667
|
</button>
|
|
575
668
|
)}
|
|
576
|
-
{!archived && !canEditTaskContent(current) &&
|
|
669
|
+
{!archived && !canEditTaskContent(current) && !busy && (
|
|
577
670
|
<button
|
|
578
671
|
type="button"
|
|
579
672
|
className={css.ghostButton}
|
|
@@ -598,7 +691,7 @@ export function TaskDetail({ controller, task }: { controller: BoardController;
|
|
|
598
691
|
<button
|
|
599
692
|
type="button"
|
|
600
693
|
className={css.primaryButton}
|
|
601
|
-
disabled={
|
|
694
|
+
disabled={busy || pending}
|
|
602
695
|
title={subtaskChildren.length > 0
|
|
603
696
|
? t('detail.subtasks.runHint', { count: String(subtaskChildren.length) })
|
|
604
697
|
: undefined}
|
|
@@ -651,12 +744,13 @@ export function TaskDetail({ controller, task }: { controller: BoardController;
|
|
|
651
744
|
</footer>
|
|
652
745
|
</div>
|
|
653
746
|
|
|
654
|
-
{
|
|
747
|
+
{confirmPresence.mounted && (
|
|
655
748
|
<ConfirmDialog
|
|
656
749
|
title={t('delete.title')}
|
|
657
750
|
message={t('delete.confirm', { name: current.title })}
|
|
658
751
|
confirmLabel={t('delete.ok')}
|
|
659
752
|
danger
|
|
753
|
+
phase={confirmPresence.phase}
|
|
660
754
|
onCancel={() => { setConfirmDelete(false) }}
|
|
661
755
|
onConfirm={() => {
|
|
662
756
|
setConfirmDelete(false)
|
|
@@ -665,18 +759,19 @@ export function TaskDetail({ controller, task }: { controller: BoardController;
|
|
|
665
759
|
/>
|
|
666
760
|
)}
|
|
667
761
|
|
|
668
|
-
{
|
|
669
|
-
<EditTaskModal controller={controller} task={current} onClose={() => { setShowEdit(false) }} />
|
|
762
|
+
{editPresence.mounted && (
|
|
763
|
+
<EditTaskModal controller={controller} task={current} phase={editPresence.phase} onClose={() => { setShowEdit(false) }} />
|
|
670
764
|
)}
|
|
671
765
|
|
|
672
|
-
{
|
|
673
|
-
<EditTagsModal controller={controller} task={current} onClose={() => { setShowEditTags(false) }} />
|
|
766
|
+
{tagsPresence.mounted && (
|
|
767
|
+
<EditTagsModal controller={controller} task={current} phase={tagsPresence.phase} onClose={() => { setShowEditTags(false) }} />
|
|
674
768
|
)}
|
|
675
769
|
|
|
676
|
-
{
|
|
770
|
+
{duplicatePresence.mounted && (
|
|
677
771
|
<NewTaskModal
|
|
678
772
|
controller={controller}
|
|
679
773
|
initialTask={current}
|
|
774
|
+
phase={duplicatePresence.phase}
|
|
680
775
|
onClose={() => { setShowDuplicate(false) }}
|
|
681
776
|
onDuplicateSuccess={async (sourceId) => {
|
|
682
777
|
await controller.archiveTask(sourceId)
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* NewTaskModal and the EditTaskModal. State stays in the owning modal; these
|
|
5
5
|
* are controlled components.
|
|
6
6
|
*/
|
|
7
|
-
import type
|
|
7
|
+
import { useState, type ReactNode } from 'react'
|
|
8
8
|
import { TAG_NAME_MAX_LENGTH, TAG_PROMPT_MAX_LENGTH, TASK_TAG_LIMIT, normalizeTags, type TaskTag } from '../../core/tasks.ts'
|
|
9
9
|
import { t } from '../locales.ts'
|
|
10
10
|
import css from '../board.module.css'
|
|
11
|
+
import { IconClose, IconPlus } from './icons.tsx'
|
|
12
|
+
import { useDialog, type OverlayPhase } from './overlay.tsx'
|
|
11
13
|
|
|
12
14
|
/** DOM id shared by the tag-name inputs and their datalist (one board at a time). */
|
|
13
15
|
const TAG_NAME_LIST_ID = 'dsh-task-board-tag-names'
|
|
@@ -22,6 +24,7 @@ export function ModalShell({
|
|
|
22
24
|
onSubmit,
|
|
23
25
|
onClose,
|
|
24
26
|
secondaryAction,
|
|
27
|
+
phase = 'open',
|
|
25
28
|
children,
|
|
26
29
|
}: {
|
|
27
30
|
ariaLabel: string
|
|
@@ -33,19 +36,25 @@ export function ModalShell({
|
|
|
33
36
|
onClose: () => void
|
|
34
37
|
/** Optional second action beside the primary submit (e.g. create and run). */
|
|
35
38
|
secondaryAction?: { label: string; onSubmit: () => void }
|
|
39
|
+
/** Which leg of the enter/exit motion pair the surface is on. */
|
|
40
|
+
phase?: OverlayPhase
|
|
36
41
|
children: ReactNode
|
|
37
42
|
}) {
|
|
43
|
+
const dialog = useDialog<HTMLFormElement>(onClose, phase)
|
|
38
44
|
return (
|
|
39
|
-
<div className={css.modalBackdrop}
|
|
45
|
+
<div className={css.modalBackdrop} data-state={phase} onMouseDown={dialog.onMouseDown}>
|
|
40
46
|
<form
|
|
47
|
+
ref={dialog.attach}
|
|
41
48
|
className={css.modal}
|
|
42
49
|
role="dialog"
|
|
50
|
+
aria-modal="true"
|
|
43
51
|
aria-label={ariaLabel}
|
|
52
|
+
tabIndex={-1}
|
|
44
53
|
onSubmit={event => { event.preventDefault(); onSubmit() }}
|
|
45
54
|
>
|
|
46
55
|
<h2 className={css.modalTitle}>{title}</h2>
|
|
47
56
|
|
|
48
|
-
{children}
|
|
57
|
+
<div className={css.modalBody}>{children}</div>
|
|
49
58
|
|
|
50
59
|
{error !== undefined && <p className={css.formError}>{error}</p>}
|
|
51
60
|
|
|
@@ -72,6 +81,51 @@ export function ModalShell({
|
|
|
72
81
|
)
|
|
73
82
|
}
|
|
74
83
|
|
|
84
|
+
/**
|
|
85
|
+
* One collapsible region of a task form (create/edit dialog UX): the form
|
|
86
|
+
* groups its configuration into a few regions instead of stacking every field
|
|
87
|
+
* in one long column, so a normal-sized dialog needs no scrollbar. A collapsed
|
|
88
|
+
* region keeps a one-line summary of the values it holds, so nothing is hidden
|
|
89
|
+
* without a trace; a region holding a blocking error is forced open by its
|
|
90
|
+
* owner (forceOpen) rather than reporting the error out of sight.
|
|
91
|
+
*/
|
|
92
|
+
export function CollapsibleSection({
|
|
93
|
+
title,
|
|
94
|
+
summary,
|
|
95
|
+
defaultOpen = false,
|
|
96
|
+
forceOpen = false,
|
|
97
|
+
children,
|
|
98
|
+
}: {
|
|
99
|
+
title: string
|
|
100
|
+
/** One-line value preview rendered only while the region is collapsed. */
|
|
101
|
+
summary?: string | undefined
|
|
102
|
+
/** Whether the region starts expanded (the owner's default layout). */
|
|
103
|
+
defaultOpen?: boolean
|
|
104
|
+
/** Open regardless of the stored state: an error inside must stay visible. */
|
|
105
|
+
forceOpen?: boolean
|
|
106
|
+
children: ReactNode
|
|
107
|
+
}) {
|
|
108
|
+
const [open, setOpen] = useState(defaultOpen)
|
|
109
|
+
const expanded = forceOpen || open
|
|
110
|
+
return (
|
|
111
|
+
<section className={css.formSection} data-dsh-part="form-section" data-open={expanded ? 'true' : undefined}>
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
className={css.formSectionHeader}
|
|
115
|
+
aria-expanded={expanded}
|
|
116
|
+
onClick={() => { setOpen(value => !value) }}
|
|
117
|
+
>
|
|
118
|
+
<span className={css.formSectionChevron} aria-hidden="true" />
|
|
119
|
+
<span className={css.formSectionTitle}>{title}</span>
|
|
120
|
+
{!expanded && summary !== undefined && summary !== '' && (
|
|
121
|
+
<span className={css.formSectionSummary}>{summary}</span>
|
|
122
|
+
)}
|
|
123
|
+
</button>
|
|
124
|
+
{expanded && <div className={css.formSectionBody}>{children}</div>}
|
|
125
|
+
</section>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
75
129
|
/** Title + description + prompt fields shared by the new and edit task forms. */
|
|
76
130
|
export function TaskContentFields({
|
|
77
131
|
title,
|
|
@@ -106,7 +160,7 @@ export function TaskContentFields({
|
|
|
106
160
|
<span className={css.fieldLabel}>{t('new.description')}</span>
|
|
107
161
|
<textarea
|
|
108
162
|
className={css.input}
|
|
109
|
-
rows={
|
|
163
|
+
rows={2}
|
|
110
164
|
value={description}
|
|
111
165
|
placeholder={t('new.descriptionPlaceholder')}
|
|
112
166
|
onChange={event => onDescriptionChange(event.target.value)}
|
|
@@ -181,7 +235,7 @@ export function TaskTagFields({
|
|
|
181
235
|
aria-label={t('new.tagRemove', { name: tag.name })}
|
|
182
236
|
onClick={() => { onChange(tags.filter((_, position) => position !== index)) }}
|
|
183
237
|
>
|
|
184
|
-
|
|
238
|
+
<IconClose size={14} />
|
|
185
239
|
</button>
|
|
186
240
|
</div>
|
|
187
241
|
))}
|
|
@@ -194,7 +248,8 @@ export function TaskTagFields({
|
|
|
194
248
|
disabled={tags.length >= TASK_TAG_LIMIT}
|
|
195
249
|
onClick={() => { onChange([...tags, { name: '' }]) }}
|
|
196
250
|
>
|
|
197
|
-
|
|
251
|
+
<IconPlus size={14} />
|
|
252
|
+
{t('new.tagAdd')}
|
|
198
253
|
</button>
|
|
199
254
|
</div>
|
|
200
255
|
)
|