@linxin666/dsh-client-ui-task-board 0.4.2 → 0.4.4
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 +21 -13
- package/README.zh.md +21 -13
- package/lib/client.js +1566 -929
- package/lib/client.js.map +1 -1
- package/lib/index.js +934 -176
- package/lib/types/client/board/TaskForm.d.ts +19 -1
- package/lib/types/client/board/preset-label.d.ts +22 -0
- package/lib/types/client/host-api.d.ts +24 -1
- package/lib/types/client/index.d.ts +15 -1
- package/lib/types/client/locales.d.ts +38 -2
- package/lib/types/client/native-panel.d.ts +62 -0
- package/lib/types/client/schedule-zone.d.ts +47 -0
- package/lib/types/client/settings-entry-form.d.ts +49 -0
- package/lib/types/core/controller.d.ts +42 -2
- package/lib/types/core/degraded-reason.d.ts +46 -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 +26 -2
- 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-update.d.ts +1 -1
- package/lib/types/host-ledger.d.ts +65 -4
- package/lib/types/host-runner.d.ts +76 -3
- package/lib/types/host-service.d.ts +77 -5
- package/lib/types/index.d.ts +11 -1
- package/lib/types/mount-once.d.ts +38 -7
- package/lib/types/protocol.d.ts +9 -3
- package/package.json +26 -23
- package/src/client/board/LinkSubtaskModal.tsx +21 -11
- package/src/client/board/NewTaskModal.tsx +271 -150
- package/src/client/board/TaskCard.tsx +24 -5
- package/src/client/board/TaskDetail.tsx +83 -33
- package/src/client/board/TaskForm.tsx +48 -3
- package/src/client/board/preset-label.ts +44 -0
- package/src/client/board.module.css +130 -136
- package/src/client/host-api.ts +109 -6
- package/src/client/index.ts +50 -18
- package/src/client/locales.ts +82 -8
- package/src/client/native-panel.tsx +124 -0
- package/src/client/schedule-zone.ts +135 -0
- package/src/client/settings-entry-form.ts +142 -0
- package/src/core/controller.ts +66 -4
- package/src/core/degraded-reason.ts +81 -0
- package/src/core/package-name.ts +9 -0
- package/src/core/schedule.ts +211 -55
- package/src/core/store.ts +10 -1
- package/src/core/subtask.ts +77 -22
- package/src/core/tasks.ts +38 -4
- package/src/core/use-cases/task-create.ts +14 -2
- package/src/core/use-cases/task-schedule.ts +17 -6
- package/src/core/use-cases/task-update.ts +4 -1
- package/src/host/agent-tools.ts +40 -8
- package/src/host-ledger.ts +223 -23
- package/src/host-routes.ts +37 -1
- package/src/host-runner.ts +164 -14
- package/src/host-service.ts +232 -39
- package/src/index.ts +23 -1
- package/src/mount-once.ts +107 -13
- package/src/protocol.ts +27 -8
- package/lib/types/client/board-mount.d.ts +0 -23
- package/lib/types/client/body-mutations.d.ts +0 -45
- package/lib/types/client/panel-mount-core.d.ts +0 -56
- package/lib/types/client/sidebar-entry-core.d.ts +0 -55
- package/lib/types/client/sidebar-entry.d.ts +0 -29
- package/lib/types/host/run-guarded.d.ts +0 -39
- package/src/client/board-mount.tsx +0 -43
- package/src/client/body-mutations.ts +0 -133
- package/src/client/panel-mount-core.ts +0 -155
- package/src/client/sidebar-entry-core.ts +0 -237
- package/src/client/sidebar-entry.ts +0 -53
- package/src/host/run-guarded.ts +0 -76
|
@@ -13,15 +13,34 @@ import { executionLabel, tagTone } from '../../core/tasks.ts'
|
|
|
13
13
|
import { t } from '../locales.ts'
|
|
14
14
|
import css from '../board.module.css'
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Built formatters, keyed by time zone ('' = the browser's own). Constructing
|
|
18
|
+
* an Intl.DateTimeFormat is orders of magnitude more expensive than formatting
|
|
19
|
+
* with one, and the board renders dozens of timestamps per SSE frame; the
|
|
20
|
+
* cached formatter keeps that cost off the render path.
|
|
21
|
+
*/
|
|
22
|
+
const HOST_TIMESTAMP_FORMATS = new Map<string, Intl.DateTimeFormat>()
|
|
23
|
+
|
|
24
|
+
function hostTimestampFormat(timeZone: string | undefined): Intl.DateTimeFormat {
|
|
25
|
+
const key = timeZone ?? ''
|
|
26
|
+
const cached = HOST_TIMESTAMP_FORMATS.get(key)
|
|
27
|
+
if (cached !== undefined) return cached
|
|
28
|
+
const format = new Intl.DateTimeFormat(undefined, {
|
|
29
|
+
dateStyle: 'medium',
|
|
30
|
+
timeStyle: 'medium',
|
|
31
|
+
...(timeZone === undefined ? {} : { timeZone }),
|
|
32
|
+
})
|
|
33
|
+
HOST_TIMESTAMP_FORMATS.set(key, format)
|
|
34
|
+
return format
|
|
35
|
+
}
|
|
36
|
+
|
|
16
37
|
/** Compact relative/absolute time label. */
|
|
17
38
|
export function formatHostTimestamp(ms: number, timeZone?: string): string {
|
|
18
39
|
try {
|
|
19
|
-
return new
|
|
20
|
-
dateStyle: 'medium',
|
|
21
|
-
timeStyle: 'medium',
|
|
22
|
-
...(timeZone === undefined ? {} : { timeZone }),
|
|
23
|
-
}).format(new Date(ms))
|
|
40
|
+
return hostTimestampFormat(timeZone).format(new Date(ms))
|
|
24
41
|
} catch {
|
|
42
|
+
// An unsupported time-zone id throws on construction: never cache it, and
|
|
43
|
+
// fall back to the ISO instant for this call.
|
|
25
44
|
return new Date(ms).toISOString()
|
|
26
45
|
}
|
|
27
46
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* execution's session transcript.
|
|
6
6
|
*/
|
|
7
7
|
import { useEffect, useState } from 'react'
|
|
8
|
-
import type { BoardController } from '../../core/controller.ts'
|
|
8
|
+
import type { BoardController, ControllerSnapshot } from '../../core/controller.ts'
|
|
9
9
|
import { isValidCron } from '../../core/schedule.ts'
|
|
10
10
|
import { MANUAL_STATUSES, TASK_PERMISSIONS, tagTone, type ExecutionRecord, type TaskPermission, type TaskRecord } from '../../core/tasks.ts'
|
|
11
11
|
import { canEditTaskContent } from '../../core/use-cases/task-update.ts'
|
|
@@ -13,11 +13,13 @@ import { requiresPermissionConfirmation } from '../../core/handover.ts'
|
|
|
13
13
|
import { DEFAULT_SUBTASK_DEPTH, directSubtasks, taskDepth } from '../../core/subtask.ts'
|
|
14
14
|
import { t, type TaskBoardKey } from '../locales.ts'
|
|
15
15
|
import { SCHEDULE_PRESETS } from '../schedule-presets.ts'
|
|
16
|
+
import { nextRunLabel, zoneChoices } from '../schedule-zone.ts'
|
|
16
17
|
import css from '../board.module.css'
|
|
17
18
|
import { ConfirmDialog } from './ConfirmDialog.tsx'
|
|
18
19
|
import { EditTaskModal, EditTagsModal } from './EditTaskModal.tsx'
|
|
19
20
|
import { LinkSubtaskModal } from './LinkSubtaskModal.tsx'
|
|
20
21
|
import { NewTaskModal } from './NewTaskModal.tsx'
|
|
22
|
+
import { inheritPresetLabel, presetLabel } from './preset-label.ts'
|
|
21
23
|
import { formatHostTimestamp, formatTime } from './TaskCard.tsx'
|
|
22
24
|
import { STATUS_KEY } from './status-key.ts'
|
|
23
25
|
|
|
@@ -63,20 +65,16 @@ function ExecutionRow({ execution, timeZone, onOpen }: { execution: ExecutionRec
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
/** 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
|
-
)
|
|
68
|
+
function ExecutionSettingsSection({ controller, task, pending, executionOptions, teamRunAvailable }: {
|
|
69
|
+
controller: BoardController
|
|
70
|
+
task: TaskRecord
|
|
71
|
+
pending: boolean
|
|
72
|
+
/** Picker option sets, owned by the detail overlay's snapshot. */
|
|
73
|
+
executionOptions: ControllerSnapshot['executionOptions']
|
|
74
|
+
/** Whether this deployment serves the Agent Teams service. */
|
|
75
|
+
teamRunAvailable: boolean
|
|
76
|
+
}) {
|
|
77
|
+
const options = executionOptions
|
|
80
78
|
const workspaceId = task.workspaceId ?? ''
|
|
81
79
|
const mode = task.mode ?? ''
|
|
82
80
|
const permission = task.permission ?? ''
|
|
@@ -107,18 +105,18 @@ function ExecutionSettingsSection({ controller, task, pending }: { controller: B
|
|
|
107
105
|
</select>
|
|
108
106
|
</label>
|
|
109
107
|
<label className={css.field}>
|
|
110
|
-
<span className={css.fieldLabel}>{t('new.
|
|
108
|
+
<span className={css.fieldLabel}>{t('new.agentPreset')}</span>
|
|
111
109
|
<select
|
|
112
110
|
className={css.select}
|
|
113
111
|
value={mode}
|
|
114
112
|
disabled={pending}
|
|
115
113
|
onChange={event => { controller.updateTask(task.id, { mode: event.target.value }) }}
|
|
116
114
|
>
|
|
117
|
-
<option value="">{
|
|
115
|
+
<option value="">{inheritPresetLabel(options.presets)}</option>
|
|
118
116
|
{!modeKnown && <option value={mode}>{mode}{t('exec.mode.removed')}</option>}
|
|
119
117
|
{options.presets.map(preset => (
|
|
120
118
|
<option key={preset.id} value={preset.id} disabled={preset.broken !== undefined}>
|
|
121
|
-
{preset
|
|
119
|
+
{presetLabel(preset)}
|
|
122
120
|
{preset.isDefault ? t('exec.mode.defaultSuffix') : ''}
|
|
123
121
|
{preset.broken !== undefined ? t('exec.mode.brokenSuffix') : ''}
|
|
124
122
|
</option>
|
|
@@ -164,6 +162,16 @@ function ExecutionSettingsSection({ controller, task, pending }: { controller: B
|
|
|
164
162
|
<span>{t('exec.reuseSession')}</span>
|
|
165
163
|
</label>
|
|
166
164
|
<p className={css.detailText}>{t('exec.reuseSessionHint')}</p>
|
|
165
|
+
<label className={css.scheduleToggle}>
|
|
166
|
+
<input
|
|
167
|
+
type="checkbox"
|
|
168
|
+
checked={task.goalRun !== false}
|
|
169
|
+
disabled={pending}
|
|
170
|
+
onChange={event => { controller.updateTask(task.id, { goalRun: event.target.checked }) }}
|
|
171
|
+
/>
|
|
172
|
+
<span>{t('exec.goalRun')}</span>
|
|
173
|
+
</label>
|
|
174
|
+
<p className={css.detailText}>{t('exec.goalRunHint')}</p>
|
|
167
175
|
<label className={css.scheduleToggle}>
|
|
168
176
|
<input
|
|
169
177
|
type="checkbox"
|
|
@@ -178,7 +186,7 @@ function ExecutionSettingsSection({ controller, task, pending }: { controller: B
|
|
|
178
186
|
)
|
|
179
187
|
}
|
|
180
188
|
|
|
181
|
-
/** The scheduled-runs editor: enable toggle, cron input + presets, next-run info. */
|
|
189
|
+
/** The scheduled-runs editor: enable toggle, cron input + presets, zone, next-run info. */
|
|
182
190
|
function ScheduleSection({ controller, task, pending }: { controller: BoardController; task: TaskRecord; pending: boolean }) {
|
|
183
191
|
const schedule = task.schedule
|
|
184
192
|
const [cron, setCron] = useState(schedule?.cron ?? '0 9 * * *')
|
|
@@ -186,7 +194,12 @@ function ScheduleSection({ controller, task, pending }: { controller: BoardContr
|
|
|
186
194
|
const [nextRunAt, setNextRunAt] = useState<number | undefined>(schedule?.nextRunAt)
|
|
187
195
|
const [lastTriggeredAt, setLastTriggeredAt] = useState<number | undefined>(schedule?.lastTriggeredAt)
|
|
188
196
|
const [error, setError] = useState<string | undefined>(undefined)
|
|
189
|
-
const
|
|
197
|
+
const hostTimeZone = controller.getSnapshot().host?.scheduler.timeZone
|
|
198
|
+
// The rule's own zone; '' means "follow the Host zone" (no stored zone).
|
|
199
|
+
const [zone, setZone] = useState<string>(schedule?.timeZone ?? '')
|
|
200
|
+
// One clock reading per render pass, so the absolute and relative halves of
|
|
201
|
+
// the label always describe the same instant.
|
|
202
|
+
const now = Date.now()
|
|
190
203
|
|
|
191
204
|
// Keep the editor in sync when the task record changes underneath (the
|
|
192
205
|
// schedule rolls forward as runs trigger).
|
|
@@ -195,8 +208,9 @@ function ScheduleSection({ controller, task, pending }: { controller: BoardContr
|
|
|
195
208
|
setEnabled(schedule?.enabled ?? false)
|
|
196
209
|
setNextRunAt(schedule?.nextRunAt)
|
|
197
210
|
setLastTriggeredAt(schedule?.lastTriggeredAt)
|
|
211
|
+
setZone(schedule?.timeZone ?? '')
|
|
198
212
|
setError(undefined)
|
|
199
|
-
}, [task.id, schedule?.enabled, schedule?.cron, schedule?.nextRunAt, schedule?.lastTriggeredAt])
|
|
213
|
+
}, [task.id, schedule?.enabled, schedule?.cron, schedule?.timeZone, schedule?.nextRunAt, schedule?.lastTriggeredAt])
|
|
200
214
|
|
|
201
215
|
/** Validate + persist the current cron text (Enter or blur). */
|
|
202
216
|
const saveCron = (value: string): void => {
|
|
@@ -210,6 +224,16 @@ function ScheduleSection({ controller, task, pending }: { controller: BoardContr
|
|
|
210
224
|
controller.setSchedule(task.id, { cron: trimmed })
|
|
211
225
|
}
|
|
212
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Change the rule's zone. `''` clears the stored zone, which is how a user
|
|
229
|
+
* returns a rule to following the Host zone.
|
|
230
|
+
*/
|
|
231
|
+
const changeZone = (value: string): void => {
|
|
232
|
+
setZone(value)
|
|
233
|
+
setError(undefined)
|
|
234
|
+
controller.setSchedule(task.id, { timeZone: value === '' ? null : value })
|
|
235
|
+
}
|
|
236
|
+
|
|
213
237
|
/** Arm/disarm the schedule (arming first persists the edited cron). */
|
|
214
238
|
const toggleEnabled = (next: boolean): void => {
|
|
215
239
|
const trimmed = cron.trim()
|
|
@@ -232,12 +256,18 @@ function ScheduleSection({ controller, task, pending }: { controller: BoardContr
|
|
|
232
256
|
controller.setSchedule(task.id, { cron: preset })
|
|
233
257
|
}
|
|
234
258
|
|
|
259
|
+
// The zone the rule's wall clock is actually read in: its own when set,
|
|
260
|
+
// otherwise the Host zone. The preview must use the same one the Host will.
|
|
261
|
+
const effectiveZone = zone === '' ? hostTimeZone : zone
|
|
235
262
|
const nextLabel = !enabled || nextRunAt === undefined
|
|
236
263
|
? t('detail.schedule.notScheduled')
|
|
237
|
-
: nextRunAt <=
|
|
264
|
+
: nextRunAt <= now
|
|
238
265
|
? t('detail.schedule.dueSoon')
|
|
239
|
-
:
|
|
240
|
-
const lastLabel = lastTriggeredAt === undefined ? '—' : formatHostTimestamp(lastTriggeredAt,
|
|
266
|
+
: nextRunLabel(nextRunAt, effectiveZone, formatHostTimestamp, now)
|
|
267
|
+
const lastLabel = lastTriggeredAt === undefined ? '—' : formatHostTimestamp(lastTriggeredAt, effectiveZone)
|
|
268
|
+
// The rule's own stored zone rides the list even when this runtime's
|
|
269
|
+
// inventory does not carry it, so opening the editor cannot rewrite it.
|
|
270
|
+
const zones = zoneChoices(hostTimeZone, schedule?.timeZone)
|
|
241
271
|
|
|
242
272
|
return (
|
|
243
273
|
<section className={css.detailSection}>
|
|
@@ -276,6 +306,23 @@ function ScheduleSection({ controller, task, pending }: { controller: BoardContr
|
|
|
276
306
|
))}
|
|
277
307
|
</select>
|
|
278
308
|
</div>
|
|
309
|
+
{/* A list is used instead of a free-text field so only zones the Host can
|
|
310
|
+
also resolve are offered; the Host zone entry clears the stored value. */}
|
|
311
|
+
<label className={css.scheduleZone}>
|
|
312
|
+
<span>{t('detail.schedule.timeZone')}</span>
|
|
313
|
+
<select
|
|
314
|
+
className={css.schedulePreset}
|
|
315
|
+
value={zone}
|
|
316
|
+
disabled={pending}
|
|
317
|
+
aria-label={t('detail.schedule.timeZone')}
|
|
318
|
+
title={t('detail.schedule.timeZoneHint')}
|
|
319
|
+
onChange={event => { changeZone(event.target.value) }}
|
|
320
|
+
>
|
|
321
|
+
{zones.map(choice => (
|
|
322
|
+
<option key={choice.id === '' ? '__host' : choice.id} value={choice.id}>{choice.label}</option>
|
|
323
|
+
))}
|
|
324
|
+
</select>
|
|
325
|
+
</label>
|
|
279
326
|
{error !== undefined && <p className={css.formError}>{error}</p>}
|
|
280
327
|
<p className={css.scheduleMeta}>
|
|
281
328
|
{t('detail.schedule.nextRun')} {nextLabel}
|
|
@@ -290,17 +337,14 @@ function ScheduleSection({ controller, task, pending }: { controller: BoardContr
|
|
|
290
337
|
* and the actions that grow or prune the tree. Every gate here mirrors the
|
|
291
338
|
* Host lineage gate for affordance only; the Host re-checks the action.
|
|
292
339
|
*/
|
|
293
|
-
function SubtaskSection({ controller, task, pending, archived }: {
|
|
340
|
+
function SubtaskSection({ controller, task, pending, archived, snapshot }: {
|
|
294
341
|
controller: BoardController
|
|
295
342
|
task: TaskRecord
|
|
296
343
|
pending: boolean
|
|
297
344
|
archived: boolean
|
|
345
|
+
/** Detail-overlay snapshot; the overlay already re-renders on every notify. */
|
|
346
|
+
snapshot: ControllerSnapshot
|
|
298
347
|
}) {
|
|
299
|
-
const [snapshot, setSnapshot] = useState(controller.getSnapshot())
|
|
300
|
-
useEffect(
|
|
301
|
-
() => controller.subscribe(() => setSnapshot(controller.getSnapshot())),
|
|
302
|
-
[controller],
|
|
303
|
-
)
|
|
304
348
|
const [showAdd, setShowAdd] = useState(false)
|
|
305
349
|
const [showLink, setShowLink] = useState(false)
|
|
306
350
|
const tasks = snapshot.tasks
|
|
@@ -440,7 +484,7 @@ export function TaskDetail({ controller, task }: { controller: BoardController;
|
|
|
440
484
|
<p className={css.detailText}>{current.description !== '' ? current.description : '—'}</p>
|
|
441
485
|
</section>
|
|
442
486
|
|
|
443
|
-
<SubtaskSection controller={controller} task={current} pending={pending} archived={archived} />
|
|
487
|
+
<SubtaskSection controller={controller} task={current} pending={pending} archived={archived} snapshot={snapshot} />
|
|
444
488
|
|
|
445
489
|
{current.tags !== undefined && current.tags.length > 0 && (
|
|
446
490
|
<section className={css.detailSection} data-dsh-part="tags">
|
|
@@ -484,7 +528,7 @@ export function TaskDetail({ controller, task }: { controller: BoardController;
|
|
|
484
528
|
<h4>{t('detail.handover')}</h4>
|
|
485
529
|
<p className={css.detailText}>
|
|
486
530
|
{t('new.workspace')}: {current.handover.workspaceId ?? t('exec.workspace.recent')}
|
|
487
|
-
{' · '}{t('new.
|
|
531
|
+
{' · '}{t('new.agentPreset')}: {current.handover.mode ?? t('exec.mode.inherit')}
|
|
488
532
|
{' · '}{t('new.permission')}: {current.handover.permission === undefined ? t('exec.permission.default') : t(`exec.permission.${current.handover.permission}` as TaskBoardKey)}
|
|
489
533
|
</p>
|
|
490
534
|
<p className={css.detailText}><strong>{t('detail.handover.references')}</strong></p>
|
|
@@ -518,7 +562,13 @@ export function TaskDetail({ controller, task }: { controller: BoardController;
|
|
|
518
562
|
|
|
519
563
|
{!archived && (
|
|
520
564
|
<>
|
|
521
|
-
<ExecutionSettingsSection
|
|
565
|
+
<ExecutionSettingsSection
|
|
566
|
+
controller={controller}
|
|
567
|
+
task={current}
|
|
568
|
+
pending={pending}
|
|
569
|
+
executionOptions={snapshot.executionOptions}
|
|
570
|
+
teamRunAvailable={snapshot.host?.teamRunAvailable === true}
|
|
571
|
+
/>
|
|
522
572
|
<ScheduleSection controller={controller} task={current} pending={pending} />
|
|
523
573
|
</>
|
|
524
574
|
)}
|
|
@@ -4,7 +4,7 @@
|
|
|
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'
|
|
@@ -45,7 +45,7 @@ export function ModalShell({
|
|
|
45
45
|
>
|
|
46
46
|
<h2 className={css.modalTitle}>{title}</h2>
|
|
47
47
|
|
|
48
|
-
{children}
|
|
48
|
+
<div className={css.modalBody}>{children}</div>
|
|
49
49
|
|
|
50
50
|
{error !== undefined && <p className={css.formError}>{error}</p>}
|
|
51
51
|
|
|
@@ -72,6 +72,51 @@ export function ModalShell({
|
|
|
72
72
|
)
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* One collapsible region of a task form (create/edit dialog UX): the form
|
|
77
|
+
* groups its configuration into a few regions instead of stacking every field
|
|
78
|
+
* in one long column, so a normal-sized dialog needs no scrollbar. A collapsed
|
|
79
|
+
* region keeps a one-line summary of the values it holds, so nothing is hidden
|
|
80
|
+
* without a trace; a region holding a blocking error is forced open by its
|
|
81
|
+
* owner (forceOpen) rather than reporting the error out of sight.
|
|
82
|
+
*/
|
|
83
|
+
export function CollapsibleSection({
|
|
84
|
+
title,
|
|
85
|
+
summary,
|
|
86
|
+
defaultOpen = false,
|
|
87
|
+
forceOpen = false,
|
|
88
|
+
children,
|
|
89
|
+
}: {
|
|
90
|
+
title: string
|
|
91
|
+
/** One-line value preview rendered only while the region is collapsed. */
|
|
92
|
+
summary?: string | undefined
|
|
93
|
+
/** Whether the region starts expanded (the owner's default layout). */
|
|
94
|
+
defaultOpen?: boolean
|
|
95
|
+
/** Open regardless of the stored state: an error inside must stay visible. */
|
|
96
|
+
forceOpen?: boolean
|
|
97
|
+
children: ReactNode
|
|
98
|
+
}) {
|
|
99
|
+
const [open, setOpen] = useState(defaultOpen)
|
|
100
|
+
const expanded = forceOpen || open
|
|
101
|
+
return (
|
|
102
|
+
<section className={css.formSection} data-dsh-part="form-section" data-open={expanded ? 'true' : undefined}>
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
className={css.formSectionHeader}
|
|
106
|
+
aria-expanded={expanded}
|
|
107
|
+
onClick={() => { setOpen(value => !value) }}
|
|
108
|
+
>
|
|
109
|
+
<span className={css.formSectionChevron} aria-hidden="true" />
|
|
110
|
+
<span className={css.formSectionTitle}>{title}</span>
|
|
111
|
+
{!expanded && summary !== undefined && summary !== '' && (
|
|
112
|
+
<span className={css.formSectionSummary}>{summary}</span>
|
|
113
|
+
)}
|
|
114
|
+
</button>
|
|
115
|
+
{expanded && <div className={css.formSectionBody}>{children}</div>}
|
|
116
|
+
</section>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
75
120
|
/** Title + description + prompt fields shared by the new and edit task forms. */
|
|
76
121
|
export function TaskContentFields({
|
|
77
122
|
title,
|
|
@@ -106,7 +151,7 @@ export function TaskContentFields({
|
|
|
106
151
|
<span className={css.fieldLabel}>{t('new.description')}</span>
|
|
107
152
|
<textarea
|
|
108
153
|
className={css.input}
|
|
109
|
-
rows={
|
|
154
|
+
rows={2}
|
|
110
155
|
value={description}
|
|
111
156
|
placeholder={t('new.descriptionPlaceholder')}
|
|
112
157
|
onChange={event => onDescriptionChange(event.target.value)}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display-only labels for the agent-preset roster, shared by the task form and
|
|
3
|
+
* the detail view.
|
|
4
|
+
*
|
|
5
|
+
* The deployment's four built-in presets (the `standard` / `ptc` / `minimal` /
|
|
6
|
+
* `cordis` rows the shipped web-app bundle declares) carry no display name in
|
|
7
|
+
* the registry roster, so a picker would list bare ids beside the user presets.
|
|
8
|
+
* The map below only supplies names for those four; any other id — a renamed
|
|
9
|
+
* built-in, a community preset, a user preset — falls back to the roster name
|
|
10
|
+
* and then to the id, so this table can never hide or mislabel a preset.
|
|
11
|
+
*/
|
|
12
|
+
import type { ExecutionPresetOption } from '../../core/controller.ts'
|
|
13
|
+
import { t, type TaskBoardKey } from '../locales.ts'
|
|
14
|
+
|
|
15
|
+
/** Built-in preset id -> display-name key (display sugar only). */
|
|
16
|
+
const BUILTIN_PRESET_LABELS: Record<string, TaskBoardKey> = {
|
|
17
|
+
standard: 'preset.builtin.standard',
|
|
18
|
+
ptc: 'preset.builtin.ptc',
|
|
19
|
+
minimal: 'preset.builtin.minimal',
|
|
20
|
+
cordis: 'preset.builtin.cordis',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Whether one roster row is a shipped built-in preset. */
|
|
24
|
+
export function isBuiltinPreset(id: string): boolean {
|
|
25
|
+
return BUILTIN_PRESET_LABELS[id] !== undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Display label of one preset row: built-in label, roster name, or raw id. */
|
|
29
|
+
export function presetLabel(preset: Pick<ExecutionPresetOption, 'id' | 'name'>): string {
|
|
30
|
+
const builtin = BUILTIN_PRESET_LABELS[preset.id]
|
|
31
|
+
if (builtin !== undefined && (preset.name ?? '').trim() === '') return t(builtin)
|
|
32
|
+
return preset.name ?? preset.id
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Label of the empty ("inherit") choice: names the preset a run actually lands
|
|
37
|
+
* on when the card pins nothing, which is the deployment default the roster
|
|
38
|
+
* marks with `isDefault`.
|
|
39
|
+
*/
|
|
40
|
+
export function inheritPresetLabel(presets: readonly ExecutionPresetOption[]): string {
|
|
41
|
+
const fallback = presets.find(preset => preset.isDefault)
|
|
42
|
+
if (fallback === undefined) return t('exec.mode.inherit')
|
|
43
|
+
return t('exec.mode.inheritWith', { preset: presetLabel(fallback) })
|
|
44
|
+
}
|