@linxin666/dsh-client-ui-task-board 0.4.5 → 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 +20 -12
- package/README.zh.md +20 -12
- package/lib/client.js +1327 -229
- package/lib/client.js.map +1 -1
- package/lib/index.js +1909 -348
- package/lib/types/client/TaskBoardSettingsCard.d.ts +12 -0
- package/lib/types/client/index.d.ts +4 -5
- package/lib/types/client/locales.d.ts +65 -0
- package/lib/types/client/plugin-card-seat.d.ts +5 -0
- package/lib/types/client/schedule-zone.d.ts +50 -0
- package/lib/types/core/controller.d.ts +10 -11
- package/lib/types/core/poll-cadence.d.ts +31 -0
- package/lib/types/core/tasks.d.ts +144 -14
- package/lib/types/core/use-cases/task-schedule.d.ts +68 -10
- package/lib/types/core/verification-budget.d.ts +52 -0
- package/lib/types/core/verification.d.ts +376 -9
- package/lib/types/host/verification-gate.d.ts +24 -0
- package/lib/types/host/verification-runner.d.ts +38 -7
- package/lib/types/host-ledger.d.ts +82 -7
- package/lib/types/host-runner.d.ts +32 -9
- package/lib/types/host-service.d.ts +117 -9
- package/lib/types/index.d.ts +54 -0
- package/lib/types/protocol.d.ts +50 -12
- package/package.json +2 -6
- package/src/client/TaskBoardSettingsCard.tsx +74 -1
- package/src/client/board/NewTaskModal.tsx +151 -20
- package/src/client/board/TaskCard.tsx +3 -3
- package/src/client/board/TaskDetail.tsx +219 -18
- package/src/client/board/VerificationReport.tsx +132 -13
- package/src/client/board/status-key.ts +3 -0
- package/src/client/index.ts +43 -50
- package/src/client/locales.ts +137 -4
- package/src/client/plugin-card-seat.ts +7 -0
- package/src/client/schedule-zone.ts +100 -2
- package/src/core/controller.ts +12 -8
- package/src/core/poll-cadence.ts +41 -0
- package/src/core/store.ts +64 -15
- package/src/core/tasks.ts +193 -39
- package/src/core/use-cases/task-create.ts +30 -9
- package/src/core/use-cases/task-schedule.ts +190 -25
- package/src/core/use-cases/task-update.ts +3 -3
- package/src/core/verification-budget.ts +115 -0
- package/src/core/verification.ts +785 -17
- package/src/host/agent-tools.ts +106 -21
- package/src/host/verification-gate.ts +172 -16
- package/src/host/verification-runner.ts +207 -40
- package/src/host-ledger.ts +328 -42
- package/src/host-runner.ts +52 -19
- package/src/host-service.ts +299 -36
- package/src/index.ts +101 -5
- package/src/protocol.ts +132 -28
- package/lib/invariant.js +0 -6
- package/lib/types/client/board-card-seat.d.ts +0 -34
- package/lib/types/invariant.d.ts +0 -3
- package/src/client/board-card-seat.ts +0 -102
- package/src/invariant.ts +0 -4
|
@@ -12,8 +12,9 @@ import type { TaskBoardPowerSnapshot, TaskBoardVerificationOptions } from '../pr
|
|
|
12
12
|
import type { TaskBoardExtensionDispatch } from '../core/extension.ts'
|
|
13
13
|
import { resolveContract, type ModelCatalogView, type VerificationSettings } from '../core/verification.ts'
|
|
14
14
|
import { parseModelRoute } from '../core/verification.ts'
|
|
15
|
-
import { PluginSettingsCard, BooleanField, ChoiceField } from './PluginSettingsCard.tsx'
|
|
15
|
+
import { PluginSettingsCard, BooleanField, ChoiceField, ValueField } from './PluginSettingsCard.tsx'
|
|
16
16
|
import { SUBTASK_DEPTH_MAX, SUBTASK_DEPTH_MIN } from '../core/subtask.ts'
|
|
17
|
+
import { SESSION_POLL_MAX_SECONDS, SESSION_POLL_MIN_SECONDS } from '../core/poll-cadence.ts'
|
|
17
18
|
import { CardForm, booleanField, type CardActions, type CardShell, type FieldSpec, type FieldState as CardFieldState } from './settings-form.ts'
|
|
18
19
|
import settingsCss from './board-settings.module.css'
|
|
19
20
|
|
|
@@ -23,6 +24,28 @@ const SUBTASK_DEPTH_CHOICES: readonly string[] = Array.from(
|
|
|
23
24
|
(_, index) => String(SUBTASK_DEPTH_MIN + index),
|
|
24
25
|
)
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* The roster-poll cadence field: a whole number of seconds inside the
|
|
29
|
+
* supported range, because the Host schema (`sessionPollSeconds`) is numeric.
|
|
30
|
+
* A draft outside the range blocks the save instead of staging a value the
|
|
31
|
+
* Host would clamp behind the user's back.
|
|
32
|
+
*/
|
|
33
|
+
function sessionPollField(): FieldSpec {
|
|
34
|
+
return {
|
|
35
|
+
field: 'sessionPollSeconds',
|
|
36
|
+
format: value => typeof value === 'number' && Number.isInteger(value) ? String(value) : '',
|
|
37
|
+
parse: (text) => {
|
|
38
|
+
const trimmed = text.trim()
|
|
39
|
+
if (trimmed === '') return { kind: 'clear' }
|
|
40
|
+
if (!/^\d+$/.test(trimmed)) return undefined
|
|
41
|
+
const seconds = Number(trimmed)
|
|
42
|
+
return seconds >= SESSION_POLL_MIN_SECONDS && seconds <= SESSION_POLL_MAX_SECONDS
|
|
43
|
+
? { kind: 'set', value: seconds }
|
|
44
|
+
: undefined
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
26
49
|
/**
|
|
27
50
|
* The depth field: a choice among the supported levels whose draft text is a
|
|
28
51
|
* number, because the Host schema (`maxSubtaskDepth`) is numeric. A draft
|
|
@@ -84,6 +107,14 @@ export interface TaskBoardSettings {
|
|
|
84
107
|
preventIdleSleep?: boolean
|
|
85
108
|
/** Subtask depth limit (1..3); 1 means a single level of subtasks. */
|
|
86
109
|
maxSubtaskDepth?: number
|
|
110
|
+
/** Roster-poll cadence in seconds while the board has work to reconcile. */
|
|
111
|
+
sessionPollSeconds?: number
|
|
112
|
+
/**
|
|
113
|
+
* GLOBAL native /goal switch (default off): the master control over whether
|
|
114
|
+
* this board starts a run with dsh's built-in /goal at all. Frozen per
|
|
115
|
+
* execution.
|
|
116
|
+
*/
|
|
117
|
+
goalRunEnabled?: boolean
|
|
87
118
|
/** Goal acceptance for this board's executions (default on). */
|
|
88
119
|
goalVerification?: boolean
|
|
89
120
|
/** Judge model route for goal acceptance; blank inherits the host default. */
|
|
@@ -102,6 +133,10 @@ export interface TaskBoardSettingsCardState extends CardShell {
|
|
|
102
133
|
preventIdleSleep: CardFieldState
|
|
103
134
|
/** Subtask depth limit field. */
|
|
104
135
|
maxSubtaskDepth: CardFieldState
|
|
136
|
+
/** Roster-poll cadence field. */
|
|
137
|
+
sessionPollSeconds: CardFieldState
|
|
138
|
+
/** GLOBAL native /goal switch. */
|
|
139
|
+
goalRunEnabled: CardFieldState
|
|
105
140
|
/** Goal-acceptance switch. */
|
|
106
141
|
goalVerification: CardFieldState
|
|
107
142
|
/** Judge model route field. */
|
|
@@ -132,6 +167,8 @@ export class TaskBoardSettingsCardController {
|
|
|
132
167
|
booleanField('announceToAgent'),
|
|
133
168
|
booleanField('preventIdleSleep'),
|
|
134
169
|
subtaskDepthField(),
|
|
170
|
+
sessionPollField(),
|
|
171
|
+
booleanField('goalRunEnabled'),
|
|
135
172
|
booleanField('goalVerification'),
|
|
136
173
|
judgeModelField(),
|
|
137
174
|
judgeEffortField(),
|
|
@@ -146,6 +183,8 @@ export class TaskBoardSettingsCardController {
|
|
|
146
183
|
announceToAgent: this.form.field('announceToAgent'),
|
|
147
184
|
preventIdleSleep: this.form.field('preventIdleSleep'),
|
|
148
185
|
maxSubtaskDepth: this.form.field('maxSubtaskDepth'),
|
|
186
|
+
sessionPollSeconds: this.form.field('sessionPollSeconds'),
|
|
187
|
+
goalRunEnabled: this.form.field('goalRunEnabled'),
|
|
149
188
|
goalVerification: this.form.field('goalVerification'),
|
|
150
189
|
goalVerificationModel: this.form.field('goalVerificationModel'),
|
|
151
190
|
goalVerificationReasoningEffort: this.form.field('goalVerificationReasoningEffort'),
|
|
@@ -342,6 +381,16 @@ export function TaskBoardSettingsCard(props: TaskBoardSettingsCardProps) {
|
|
|
342
381
|
onEdit={(text) => { props.edit('preventIdleSleep', text) }}
|
|
343
382
|
onReset={() => { props.resetField('preventIdleSleep') }}
|
|
344
383
|
/>
|
|
384
|
+
<ValueField
|
|
385
|
+
id="settings-task-board-session-poll"
|
|
386
|
+
label={t('settings.sessionPollSeconds')}
|
|
387
|
+
hint={t('settings.sessionPollSecondsHint')}
|
|
388
|
+
numeric
|
|
389
|
+
{...fieldProps}
|
|
390
|
+
{...state.sessionPollSeconds}
|
|
391
|
+
onEdit={(text) => { props.edit('sessionPollSeconds', text) }}
|
|
392
|
+
onReset={() => { props.resetField('sessionPollSeconds') }}
|
|
393
|
+
/>
|
|
345
394
|
<ChoiceField
|
|
346
395
|
id="settings-task-board-subtask-depth"
|
|
347
396
|
label={t('settings.maxSubtaskDepth')}
|
|
@@ -358,6 +407,30 @@ export function TaskBoardSettingsCard(props: TaskBoardSettingsCardProps) {
|
|
|
358
407
|
/>
|
|
359
408
|
</PluginSettingsCard>
|
|
360
409
|
|
|
410
|
+
<PluginSettingsCard
|
|
411
|
+
t={t}
|
|
412
|
+
titleKey="settings.runBehaviorTitle"
|
|
413
|
+
descriptionKey="settings.runBehaviorCardHint"
|
|
414
|
+
defaultOpen={false}
|
|
415
|
+
hideFooter
|
|
416
|
+
state={nestedShell}
|
|
417
|
+
onSave={props.save}
|
|
418
|
+
onDiscard={props.discard}
|
|
419
|
+
>
|
|
420
|
+
<BooleanField
|
|
421
|
+
id="settings-task-board-goal-run"
|
|
422
|
+
label={t('settings.goalRunGlobal')}
|
|
423
|
+
hint={t('settings.goalRunGlobalHint')}
|
|
424
|
+
inheritLabel={t('settings.inherit')}
|
|
425
|
+
onLabel={t('settings.on')}
|
|
426
|
+
offLabel={t('settings.off')}
|
|
427
|
+
{...fieldProps}
|
|
428
|
+
{...state.goalRunEnabled}
|
|
429
|
+
onEdit={(text) => { props.edit('goalRunEnabled', text) }}
|
|
430
|
+
onReset={() => { props.resetField('goalRunEnabled') }}
|
|
431
|
+
/>
|
|
432
|
+
</PluginSettingsCard>
|
|
433
|
+
|
|
361
434
|
<PluginSettingsCard
|
|
362
435
|
t={t}
|
|
363
436
|
titleKey="settings.goalVerificationTitle"
|
|
@@ -7,10 +7,10 @@ import type { BoardController } from '../../core/controller.ts'
|
|
|
7
7
|
import { isValidCron, nextRunAtMs } from '../../core/schedule.ts'
|
|
8
8
|
import { parseFreezeRequest } from '../../core/freeze-snapshot.ts'
|
|
9
9
|
import { effectiveTaskPermission } from '../../core/subtask.ts'
|
|
10
|
-
import { collectKnownTags, TASK_PERMISSIONS, type TaskPermission, type TaskRecord, type TaskTag } from '../../core/tasks.ts'
|
|
10
|
+
import { collectKnownTags, TASK_PERMISSIONS, type ScheduleMode, type TaskPermission, type TaskRecord, type TaskTag } from '../../core/tasks.ts'
|
|
11
11
|
import { t, type TaskBoardKey } from '../locales.ts'
|
|
12
12
|
import { SCHEDULE_PRESETS } from '../schedule-presets.ts'
|
|
13
|
-
import { nextRunLabel, zoneChoices } from '../schedule-zone.ts'
|
|
13
|
+
import { defaultZonedInputValue, nextRunLabel, parseZonedInput, runCapOption, zonedInputValue, zoneChoices } from '../schedule-zone.ts'
|
|
14
14
|
import { CollapsibleSection, ModalShell, TaskContentFields, TaskTagFields, cleanTags } from './TaskForm.tsx'
|
|
15
15
|
import type { OverlayPhase } from './overlay.tsx'
|
|
16
16
|
import { formatHostTimestamp } from './TaskCard.tsx'
|
|
@@ -57,12 +57,20 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
57
57
|
const [model, setModel] = useState(initialTask?.model ?? parentTask?.model ?? '')
|
|
58
58
|
const inheritedPermission = parentTask === undefined ? undefined : effectiveTaskPermission(parentTask)
|
|
59
59
|
const [reuseSession, setReuseSession] = useState(initialTask?.reuseSession ?? false)
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
const [goalRun, setGoalRun] = useState(initialTask?.goalRun ??
|
|
60
|
+
// Unchecked by default: a new task runs one plain turn unless the user opts
|
|
61
|
+
// into dsh's built-in /goal here. A duplicate keeps the original card's choice.
|
|
62
|
+
const [goalRun, setGoalRun] = useState(initialTask?.goalRun ?? false)
|
|
63
63
|
const [skipVerification, setSkipVerification] = useState(initialTask?.skipVerification ?? false)
|
|
64
64
|
const [scheduleEnabled, setScheduleEnabled] = useState(initialTask?.schedule?.enabled ?? false)
|
|
65
|
+
const [scheduleMode, setScheduleMode] = useState<ScheduleMode>(initialTask?.schedule?.mode ?? 'cron')
|
|
65
66
|
const [scheduleCron, setScheduleCron] = useState(initialTask?.schedule?.cron ?? '')
|
|
67
|
+
// A duplicate keeps the original card's plan; a fresh one-shot defaults to
|
|
68
|
+
// the next hour, so the field never opens on a past instant.
|
|
69
|
+
const [scheduleAt, setScheduleAt] = useState<string>(() => initialTask?.schedule?.mode === 'once' && initialTask.schedule.at !== undefined
|
|
70
|
+
? zonedInputValue(initialTask.schedule.at, initialTask.schedule.timeZone)
|
|
71
|
+
: defaultZonedInputValue(Date.now(), initialTask?.schedule?.timeZone))
|
|
72
|
+
const [scheduleMaxRunsChoice, setScheduleMaxRunsChoice] = useState<string>(() => runCapOption(initialTask?.schedule?.maxRuns))
|
|
73
|
+
const [scheduleMaxRunsText, setScheduleMaxRunsText] = useState<string>(initialTask?.schedule?.maxRuns === undefined ? '' : String(initialTask.schedule.maxRuns))
|
|
66
74
|
// '' means "follow the Host zone" (store no zone), which is the default a
|
|
67
75
|
// user gets unless they pick one explicitly.
|
|
68
76
|
const [scheduleZone, setScheduleZone] = useState(initialTask?.schedule?.timeZone ?? '')
|
|
@@ -77,6 +85,11 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
77
85
|
const [error, setError] = useState<string | undefined>(undefined)
|
|
78
86
|
const [pending, setPending] = useState(false)
|
|
79
87
|
const [options, setOptions] = useState(controller.getSnapshot().executionOptions)
|
|
88
|
+
// Live GLOBAL native-/goal switch. The new-task form never rewrites a stored
|
|
89
|
+
// preference from it: the checkbox stays operable and is explained while the
|
|
90
|
+
// master switch is off, and the user's own choice is what a later re-enable
|
|
91
|
+
// restores.
|
|
92
|
+
const [goalRunEnabled, setGoalRunEnabled] = useState(controller.getSnapshot().host?.goalRunEnabled === true)
|
|
80
93
|
// "Parse pasted text" (issue #1540) exists only when the deployment carries a
|
|
81
94
|
// parse face; the section stays hidden otherwise.
|
|
82
95
|
const [canParse] = useState(controller.getSnapshot().canParseTask === true)
|
|
@@ -99,7 +112,11 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
99
112
|
// The workspace list and preset roster arrive from the runtime after mount;
|
|
100
113
|
// follow them so the pickers never freeze on an empty snapshot.
|
|
101
114
|
useEffect(
|
|
102
|
-
() => controller.subscribe(() =>
|
|
115
|
+
() => controller.subscribe(() => {
|
|
116
|
+
const snapshot = controller.getSnapshot()
|
|
117
|
+
setOptions(snapshot.executionOptions)
|
|
118
|
+
setGoalRunEnabled(snapshot.host?.goalRunEnabled === true)
|
|
119
|
+
}),
|
|
103
120
|
[controller],
|
|
104
121
|
)
|
|
105
122
|
|
|
@@ -145,10 +162,25 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
145
162
|
*/
|
|
146
163
|
const submit = async (runAfterCreate: boolean): Promise<void> => {
|
|
147
164
|
if (scheduleEnabled) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
165
|
+
if (scheduleMode === 'once') {
|
|
166
|
+
const at = parseZonedInput(scheduleAt, scheduleZone === '' ? hostTimeZone : scheduleZone)
|
|
167
|
+
if (at === undefined || at <= Date.now()) {
|
|
168
|
+
setScheduleError(t('detail.schedule.atInvalid'))
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
const cron = scheduleCron.trim()
|
|
173
|
+
if (cron === '' || !isValidCron(cron)) {
|
|
174
|
+
setScheduleError(t('detail.schedule.invalid'))
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
if (scheduleMaxRunsChoice === 'custom') {
|
|
178
|
+
const cap = Number(scheduleMaxRunsText.trim())
|
|
179
|
+
if (scheduleMaxRunsText.trim() === '' || !Number.isInteger(cap) || cap < 1) {
|
|
180
|
+
setScheduleError(t('detail.schedule.maxRunsInvalid'))
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
}
|
|
152
184
|
}
|
|
153
185
|
}
|
|
154
186
|
// Optional continuation-card snapshot: parse the freeze block through the
|
|
@@ -188,11 +220,26 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
188
220
|
permission: permission === '' ? undefined : permission as TaskPermission,
|
|
189
221
|
model: model === '' ? undefined : model,
|
|
190
222
|
...(reuseSession ? { reuseSession: true } : {}),
|
|
191
|
-
...(goalRun ? {
|
|
223
|
+
...(goalRun ? { goalRun: true } : {}),
|
|
192
224
|
...(skipVerification ? { skipVerification: true } : {}),
|
|
193
225
|
...(tagList.length > 0 ? { tags: tagList } : {}),
|
|
194
226
|
schedule: scheduleEnabled
|
|
195
|
-
?
|
|
227
|
+
? scheduleMode === 'once'
|
|
228
|
+
? {
|
|
229
|
+
enabled: true,
|
|
230
|
+
mode: 'once' as const,
|
|
231
|
+
at: parseZonedInput(scheduleAt, scheduleZone === '' ? hostTimeZone : scheduleZone)!,
|
|
232
|
+
...(scheduleZone === '' ? {} : { timeZone: scheduleZone }),
|
|
233
|
+
}
|
|
234
|
+
: {
|
|
235
|
+
enabled: true,
|
|
236
|
+
mode: 'cron' as const,
|
|
237
|
+
cron: scheduleCron.trim(),
|
|
238
|
+
...(scheduleMaxRunsChoice === 'unlimited' ? {} : {
|
|
239
|
+
maxRuns: scheduleMaxRunsChoice === 'custom' ? Number(scheduleMaxRunsText.trim()) : Number(scheduleMaxRunsChoice),
|
|
240
|
+
}),
|
|
241
|
+
...(scheduleZone === '' ? {} : { timeZone: scheduleZone }),
|
|
242
|
+
}
|
|
196
243
|
: undefined,
|
|
197
244
|
})
|
|
198
245
|
if (task === undefined) {
|
|
@@ -218,13 +265,18 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
218
265
|
onClose()
|
|
219
266
|
}
|
|
220
267
|
|
|
221
|
-
// Next-run preview for
|
|
222
|
-
//
|
|
268
|
+
// Next-run preview for the armed plan (creation-time only), computed in the
|
|
269
|
+
// selected zone so it matches what the Host will arm. A one-shot's next run
|
|
270
|
+
// IS its planned instant; a recurring rule resolves its next occurrence.
|
|
223
271
|
const hostTimeZone = controller.getSnapshot().host?.scheduler.timeZone
|
|
224
272
|
const scheduleTimeZone = scheduleZone === '' ? hostTimeZone : scheduleZone
|
|
225
|
-
const scheduleNextRun = scheduleEnabled
|
|
226
|
-
?
|
|
227
|
-
:
|
|
273
|
+
const scheduleNextRun = !scheduleEnabled
|
|
274
|
+
? undefined
|
|
275
|
+
: scheduleMode === 'once'
|
|
276
|
+
? parseZonedInput(scheduleAt, scheduleTimeZone)
|
|
277
|
+
: scheduleCron.trim() !== '' && isValidCron(scheduleCron)
|
|
278
|
+
? nextRunAtMs(scheduleCron, Date.now(), scheduleTimeZone)
|
|
279
|
+
: undefined
|
|
228
280
|
|
|
229
281
|
const modalTitle = parentTask !== undefined
|
|
230
282
|
? t('new.subtaskTitle')
|
|
@@ -262,7 +314,9 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
262
314
|
const tagCount = cleanTags(tags).length
|
|
263
315
|
const labelsSummary = tagCount === 0 ? t('new.summary.none') : t('new.summary.labelCount', { count: String(tagCount) })
|
|
264
316
|
const runSummary = [
|
|
265
|
-
|
|
317
|
+
// A card whose goal option is on but whose master switch is off will run one
|
|
318
|
+
// plain turn: the collapsed line says the truth rather than the stored value.
|
|
319
|
+
goalRun && goalRunEnabled ? t('new.summary.multiRound') : t('new.summary.singleRound'),
|
|
266
320
|
...(reuseSession ? [t('exec.reuseSession')] : []),
|
|
267
321
|
].join(' · ')
|
|
268
322
|
const handoverSummary = freezeText.trim() !== '' || handoverText.trim() !== ''
|
|
@@ -270,7 +324,18 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
270
324
|
: t('new.summary.none')
|
|
271
325
|
const scheduleSummary = !scheduleEnabled
|
|
272
326
|
? t('new.summary.scheduleOff')
|
|
273
|
-
:
|
|
327
|
+
: scheduleMode === 'once'
|
|
328
|
+
? `${t('detail.schedule.mode.once')} · ${scheduleAt.replace('T', ' ')}`
|
|
329
|
+
: scheduleCron.trim() === ''
|
|
330
|
+
? t('new.summary.none')
|
|
331
|
+
: [
|
|
332
|
+
scheduleCron.trim(),
|
|
333
|
+
...(scheduleMaxRunsChoice === 'unlimited'
|
|
334
|
+
? []
|
|
335
|
+
: [t('detail.schedule.maxRunsValue', {
|
|
336
|
+
count: scheduleMaxRunsChoice === 'custom' ? scheduleMaxRunsText : scheduleMaxRunsChoice,
|
|
337
|
+
})]),
|
|
338
|
+
].join(' · ')
|
|
274
339
|
const parseSummary = parseText.trim() === '' ? t('new.summary.none') : t('new.summary.filled')
|
|
275
340
|
|
|
276
341
|
return (
|
|
@@ -467,11 +532,15 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
467
532
|
<input
|
|
468
533
|
type="checkbox"
|
|
469
534
|
checked={goalRun}
|
|
535
|
+
// Always operable: the global switch decides whether the choice
|
|
536
|
+
// takes effect, never whether it may be stated, and the state (and a
|
|
537
|
+
// duplicate's template) is never rewritten by it.
|
|
470
538
|
onChange={event => { setGoalRun(event.target.checked) }}
|
|
471
539
|
/>
|
|
472
540
|
<span>{t('exec.goalRun')}</span>
|
|
473
541
|
</label>
|
|
474
542
|
<p className={css.detailText}>{t('exec.goalRunHint')}</p>
|
|
543
|
+
{!goalRunEnabled && <p className={css.detailText}>{t('settings.goalRunGlobalDisabledTaskOption')}</p>}
|
|
475
544
|
|
|
476
545
|
<label className={css.scheduleToggle}>
|
|
477
546
|
<input
|
|
@@ -533,6 +602,32 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
533
602
|
</label>
|
|
534
603
|
{scheduleEnabled && (
|
|
535
604
|
<>
|
|
605
|
+
<label className={css.scheduleZone}>
|
|
606
|
+
<span>{t('detail.schedule.mode')}</span>
|
|
607
|
+
<select
|
|
608
|
+
className={css.schedulePreset}
|
|
609
|
+
value={scheduleMode}
|
|
610
|
+
aria-label={t('detail.schedule.mode')}
|
|
611
|
+
onChange={event => { setScheduleMode(event.target.value as ScheduleMode); setScheduleError(undefined) }}
|
|
612
|
+
>
|
|
613
|
+
<option value="cron">{t('detail.schedule.mode.cron')}</option>
|
|
614
|
+
<option value="once">{t('detail.schedule.mode.once')}</option>
|
|
615
|
+
</select>
|
|
616
|
+
</label>
|
|
617
|
+
{scheduleMode === 'once' ? (
|
|
618
|
+
<label className={css.scheduleZone}>
|
|
619
|
+
<span>{t('detail.schedule.at')}</span>
|
|
620
|
+
<input
|
|
621
|
+
className={css.input}
|
|
622
|
+
type="datetime-local"
|
|
623
|
+
value={scheduleAt}
|
|
624
|
+
aria-label={t('detail.schedule.at')}
|
|
625
|
+
title={t('detail.schedule.atHint')}
|
|
626
|
+
onChange={event => { setScheduleAt(event.target.value); setScheduleError(undefined) }}
|
|
627
|
+
/>
|
|
628
|
+
</label>
|
|
629
|
+
) : (
|
|
630
|
+
<>
|
|
536
631
|
<div className={css.scheduleRow}>
|
|
537
632
|
<input
|
|
538
633
|
className={`${css.input} ${css.scheduleInput}${scheduleError !== undefined ? ` ${css.scheduleInputInvalid}` : ''}`}
|
|
@@ -558,6 +653,42 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
558
653
|
))}
|
|
559
654
|
</select>
|
|
560
655
|
</div>
|
|
656
|
+
<label className={css.scheduleZone}>
|
|
657
|
+
<span>{t('detail.schedule.maxRuns')}</span>
|
|
658
|
+
<select
|
|
659
|
+
className={css.schedulePreset}
|
|
660
|
+
value={scheduleMaxRunsChoice}
|
|
661
|
+
aria-label={t('detail.schedule.maxRuns')}
|
|
662
|
+
onChange={event => {
|
|
663
|
+
setScheduleMaxRunsChoice(event.target.value)
|
|
664
|
+
setScheduleError(undefined)
|
|
665
|
+
if (event.target.value === 'custom') {
|
|
666
|
+
setScheduleMaxRunsText(initialTask?.schedule?.maxRuns === undefined ? '' : String(initialTask.schedule.maxRuns))
|
|
667
|
+
}
|
|
668
|
+
}}
|
|
669
|
+
>
|
|
670
|
+
<option value="unlimited">{t('detail.schedule.maxRunsUnlimited')}</option>
|
|
671
|
+
<option value="1">{t('detail.schedule.maxRunsValue', { count: '1' })}</option>
|
|
672
|
+
<option value="2">{t('detail.schedule.maxRunsValue', { count: '2' })}</option>
|
|
673
|
+
<option value="custom">{t('detail.schedule.maxRunsCustom')}</option>
|
|
674
|
+
</select>
|
|
675
|
+
</label>
|
|
676
|
+
{scheduleMaxRunsChoice === 'custom' && (
|
|
677
|
+
<label className={css.scheduleZone}>
|
|
678
|
+
<span>{t('detail.schedule.maxRuns')}</span>
|
|
679
|
+
<input
|
|
680
|
+
className={css.input}
|
|
681
|
+
type="number"
|
|
682
|
+
min={1}
|
|
683
|
+
step={1}
|
|
684
|
+
value={scheduleMaxRunsText}
|
|
685
|
+
aria-label={t('detail.schedule.maxRuns')}
|
|
686
|
+
onChange={event => { setScheduleMaxRunsText(event.target.value); setScheduleError(undefined) }}
|
|
687
|
+
/>
|
|
688
|
+
</label>
|
|
689
|
+
)}
|
|
690
|
+
</>
|
|
691
|
+
)}
|
|
561
692
|
{scheduleError !== undefined && <p className={css.formError}>{scheduleError}</p>}
|
|
562
693
|
<label className={css.scheduleZone}>
|
|
563
694
|
<span>{t('detail.schedule.timeZone')}</span>
|
|
@@ -565,7 +696,7 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
565
696
|
className={css.schedulePreset}
|
|
566
697
|
value={scheduleZone}
|
|
567
698
|
aria-label={t('detail.schedule.timeZone')}
|
|
568
|
-
title={t('detail.schedule.timeZoneHint')}
|
|
699
|
+
title={scheduleMode === 'once' ? t('detail.schedule.zoneOnceHint') : t('detail.schedule.timeZoneHint')}
|
|
569
700
|
onChange={event => { setScheduleZone(event.target.value); setScheduleError(undefined) }}
|
|
570
701
|
>
|
|
571
702
|
{zoneChoices(hostTimeZone, initialTask?.schedule?.timeZone).map(choice => (
|
|
@@ -274,11 +274,11 @@ function TaskCardInner({
|
|
|
274
274
|
<span
|
|
275
275
|
className={css.cardSchedule}
|
|
276
276
|
title={task.schedule.nextRunAt !== undefined
|
|
277
|
-
? `${t('card.scheduled')} · ${formatHostTimestamp(task.schedule.nextRunAt, timeZone)}`
|
|
278
|
-
: t('card.scheduled')}
|
|
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
279
|
>
|
|
280
280
|
<IconClock size={12} />
|
|
281
|
-
{t('card.scheduled')}
|
|
281
|
+
{t(task.schedule.mode === 'once' ? 'card.scheduledOnce' : 'card.scheduled')}
|
|
282
282
|
</span>
|
|
283
283
|
)}
|
|
284
284
|
{latest !== undefined && (
|