@linxin666/dsh-client-ui-task-board 0.4.5 → 0.4.7
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 +34 -18
- package/README.zh.md +34 -18
- package/lib/client.js +2804 -1024
- package/lib/client.js.map +1 -1
- package/lib/index.js +2576 -416
- package/lib/types/client/CardDefaultPresetField.d.ts +35 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +61 -0
- package/lib/types/client/board/ReasoningEffortField.d.ts +25 -0
- package/lib/types/client/board/model-options.d.ts +67 -0
- package/lib/types/client/board/preset-label.d.ts +21 -0
- package/lib/types/client/board/preset-roster.d.ts +45 -0
- package/lib/types/client/board/team-run-promotion.d.ts +26 -0
- package/lib/types/client/index.d.ts +4 -5
- package/lib/types/client/locales.d.ts +81 -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/card-defaults.d.ts +78 -0
- package/lib/types/core/controller.d.ts +41 -17
- package/lib/types/core/launch-width.d.ts +31 -0
- package/lib/types/core/poll-cadence.d.ts +31 -0
- package/lib/types/core/reasoning-effort.d.ts +57 -0
- package/lib/types/core/subtask.d.ts +30 -8
- package/lib/types/core/tasks.d.ts +163 -14
- package/lib/types/core/use-cases/task-create.d.ts +10 -5
- package/lib/types/core/use-cases/task-schedule.d.ts +68 -10
- package/lib/types/core/use-cases/task-update.d.ts +1 -1
- package/lib/types/core/verification-budget.d.ts +52 -0
- package/lib/types/core/verification.d.ts +376 -9
- package/lib/types/host/launch-admission.d.ts +72 -0
- 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 +91 -11
- package/lib/types/host-runner.d.ts +111 -13
- package/lib/types/host-service.d.ts +199 -8
- package/lib/types/index.d.ts +141 -0
- package/lib/types/protocol.d.ts +63 -12
- package/package.json +29 -33
- package/src/client/CardDefaultPresetField.tsx +101 -0
- package/src/client/TaskBoardSettingsCard.tsx +288 -17
- package/src/client/board/LinkSubtaskModal.tsx +8 -0
- package/src/client/board/NewTaskModal.tsx +237 -60
- package/src/client/board/ReasoningEffortField.tsx +50 -0
- package/src/client/board/TaskCard.tsx +3 -3
- package/src/client/board/TaskDetail.tsx +261 -24
- package/src/client/board/VerificationReport.tsx +132 -13
- package/src/client/board/model-options.ts +178 -0
- package/src/client/board/preset-label.ts +32 -0
- package/src/client/board/preset-roster.ts +58 -0
- package/src/client/board/status-key.ts +3 -0
- package/src/client/board/team-run-promotion.ts +39 -0
- package/src/client/index.ts +105 -108
- package/src/client/locales.ts +171 -6
- package/src/client/plugin-card-seat.ts +7 -0
- package/src/client/schedule-zone.ts +100 -2
- package/src/client/settings-entry-form.ts +0 -1
- package/src/core/card-defaults.ts +132 -0
- package/src/core/controller.ts +55 -16
- package/src/core/launch-width.ts +38 -0
- package/src/core/poll-cadence.ts +41 -0
- package/src/core/reasoning-effort.ts +92 -0
- package/src/core/store.ts +69 -15
- package/src/core/subtask.ts +48 -13
- package/src/core/tasks.ts +213 -39
- package/src/core/use-cases/task-create.ts +57 -25
- package/src/core/use-cases/task-schedule.ts +190 -25
- package/src/core/use-cases/task-update.ts +9 -4
- package/src/core/verification-budget.ts +115 -0
- package/src/core/verification.ts +785 -17
- package/src/host/agent-tools.ts +127 -29
- package/src/host/launch-admission.ts +140 -0
- package/src/host/verification-gate.ts +172 -16
- package/src/host/verification-runner.ts +207 -40
- package/src/host-ledger.ts +346 -48
- package/src/host-runner.ts +234 -35
- package/src/host-service.ts +539 -52
- package/src/index.ts +250 -7
- package/src/protocol.ts +160 -32
- 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
|
@@ -4,18 +4,22 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { useEffect, useRef, useState } from 'react'
|
|
6
6
|
import type { BoardController } from '../../core/controller.ts'
|
|
7
|
+
import { resolveCardTargets, type CardTargetSource } from '../../core/card-defaults.ts'
|
|
7
8
|
import { isValidCron, nextRunAtMs } from '../../core/schedule.ts'
|
|
8
9
|
import { parseFreezeRequest } from '../../core/freeze-snapshot.ts'
|
|
9
10
|
import { effectiveTaskPermission } from '../../core/subtask.ts'
|
|
10
|
-
import { collectKnownTags, TASK_PERMISSIONS, type TaskPermission, type TaskRecord, type TaskTag } from '../../core/tasks.ts'
|
|
11
|
+
import { collectKnownTags, TASK_PERMISSIONS, type ScheduleMode, type TaskPermission, type TaskRecord, type TaskTag } from '../../core/tasks.ts'
|
|
11
12
|
import { t, type TaskBoardKey } from '../locales.ts'
|
|
12
13
|
import { SCHEDULE_PRESETS } from '../schedule-presets.ts'
|
|
13
|
-
import { nextRunLabel, zoneChoices } from '../schedule-zone.ts'
|
|
14
|
+
import { defaultZonedInputValue, nextRunLabel, parseZonedInput, runCapOption, zonedInputValue, zoneChoices } from '../schedule-zone.ts'
|
|
14
15
|
import { CollapsibleSection, ModalShell, TaskContentFields, TaskTagFields, cleanTags } from './TaskForm.tsx'
|
|
15
16
|
import type { OverlayPhase } from './overlay.tsx'
|
|
16
17
|
import { formatHostTimestamp } from './TaskCard.tsx'
|
|
17
18
|
import { readParseModelPreference, writeParseModelPreference } from './parse-model-pref.ts'
|
|
18
|
-
import { inheritPresetLabel,
|
|
19
|
+
import { inheritPresetLabel, presetChoiceRows, presetLabel } from './preset-label.ts'
|
|
20
|
+
import { ReasoningEffortField } from './ReasoningEffortField.tsx'
|
|
21
|
+
import { reconcileReasoningEffort } from './model-options.ts'
|
|
22
|
+
import { parentAwaitsTeamRunPromotion, promoteParentForTeamRun } from './team-run-promotion.ts'
|
|
19
23
|
import css from '../board.module.css'
|
|
20
24
|
|
|
21
25
|
export interface NewTaskModalProps {
|
|
@@ -46,23 +50,48 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
46
50
|
const [title, setTitle] = useState(initialTask?.title ?? '')
|
|
47
51
|
const [description, setDescription] = useState(initialTask?.description ?? '')
|
|
48
52
|
const [prompt, setPrompt] = useState(initialTask?.prompt ?? '')
|
|
53
|
+
// The four execution targets this dialog opens on, resolved by the one rule
|
|
54
|
+
// the Host create path also uses (core/card-defaults.ts): what the card is
|
|
55
|
+
// created from (a duplicate's source, a subtask's parent) first, the board's
|
|
56
|
+
// new-card defaults second, blank when neither supplies a value. Resolving it
|
|
57
|
+
// from the SAME function the ledger uses is what keeps the dialog from
|
|
58
|
+
// drifting from what a create without an explicit choice stores.
|
|
59
|
+
const [seed] = useState(() => {
|
|
60
|
+
const source: CardTargetSource | undefined = parentTask !== undefined
|
|
61
|
+
? { kind: 'parent', task: parentTask }
|
|
62
|
+
: initialTask !== undefined ? { kind: 'duplicate', task: initialTask } : undefined
|
|
63
|
+
return resolveCardTargets(controller.getSnapshot().host?.cardDefaults, source)
|
|
64
|
+
})
|
|
49
65
|
// A subtask starts from its parent's execution contract (issue: subtask
|
|
50
66
|
// inheritance); each picker below still overrides it for this card. The
|
|
51
|
-
// permission stays UNSET
|
|
52
|
-
// inherit the parent's binding together with its human
|
|
53
|
-
// picking a value pins this card's own binding and
|
|
67
|
+
// permission stays UNSET unless a default supplies one: leaving it empty is
|
|
68
|
+
// what makes the Host inherit the parent's binding together with its human
|
|
69
|
+
// confirmation, while picking a value pins this card's own binding and
|
|
70
|
+
// re-arms the gate. A parent's permission never travels here at all.
|
|
54
71
|
const [workspaceId, setWorkspaceId] = useState(initialTask?.workspaceId ?? parentTask?.workspaceId ?? defaultWorkspaceId ?? '')
|
|
55
|
-
const [mode, setMode] = useState(
|
|
56
|
-
const [permission, setPermission] = useState(
|
|
57
|
-
const [model, setModel] = useState(
|
|
72
|
+
const [mode, setMode] = useState(seed.mode)
|
|
73
|
+
const [permission, setPermission] = useState(seed.permission)
|
|
74
|
+
const [model, setModel] = useState(seed.model)
|
|
75
|
+
// The effort belongs to the model pin: the resolved seed carries the source
|
|
76
|
+
// card's pair and applies the configured default level only while the final
|
|
77
|
+
// model IS the configured default model; '' follows the model/Host default.
|
|
78
|
+
const [reasoningEffort, setReasoningEffort] = useState(seed.reasoningEffort)
|
|
58
79
|
const inheritedPermission = parentTask === undefined ? undefined : effectiveTaskPermission(parentTask)
|
|
59
80
|
const [reuseSession, setReuseSession] = useState(initialTask?.reuseSession ?? false)
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
const [goalRun, setGoalRun] = useState(initialTask?.goalRun ??
|
|
81
|
+
// Unchecked by default: a new task runs one plain turn unless the user opts
|
|
82
|
+
// into dsh's built-in /goal here. A duplicate keeps the original card's choice.
|
|
83
|
+
const [goalRun, setGoalRun] = useState(initialTask?.goalRun ?? false)
|
|
63
84
|
const [skipVerification, setSkipVerification] = useState(initialTask?.skipVerification ?? false)
|
|
64
85
|
const [scheduleEnabled, setScheduleEnabled] = useState(initialTask?.schedule?.enabled ?? false)
|
|
86
|
+
const [scheduleMode, setScheduleMode] = useState<ScheduleMode>(initialTask?.schedule?.mode ?? 'cron')
|
|
65
87
|
const [scheduleCron, setScheduleCron] = useState(initialTask?.schedule?.cron ?? '')
|
|
88
|
+
// A duplicate keeps the original card's plan; a fresh one-shot defaults to
|
|
89
|
+
// the next hour, so the field never opens on a past instant.
|
|
90
|
+
const [scheduleAt, setScheduleAt] = useState<string>(() => initialTask?.schedule?.mode === 'once' && initialTask.schedule.at !== undefined
|
|
91
|
+
? zonedInputValue(initialTask.schedule.at, initialTask.schedule.timeZone)
|
|
92
|
+
: defaultZonedInputValue(Date.now(), initialTask?.schedule?.timeZone))
|
|
93
|
+
const [scheduleMaxRunsChoice, setScheduleMaxRunsChoice] = useState<string>(() => runCapOption(initialTask?.schedule?.maxRuns))
|
|
94
|
+
const [scheduleMaxRunsText, setScheduleMaxRunsText] = useState<string>(initialTask?.schedule?.maxRuns === undefined ? '' : String(initialTask.schedule.maxRuns))
|
|
66
95
|
// '' means "follow the Host zone" (store no zone), which is the default a
|
|
67
96
|
// user gets unless they pick one explicitly.
|
|
68
97
|
const [scheduleZone, setScheduleZone] = useState(initialTask?.schedule?.timeZone ?? '')
|
|
@@ -77,6 +106,11 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
77
106
|
const [error, setError] = useState<string | undefined>(undefined)
|
|
78
107
|
const [pending, setPending] = useState(false)
|
|
79
108
|
const [options, setOptions] = useState(controller.getSnapshot().executionOptions)
|
|
109
|
+
// Live GLOBAL native-/goal switch. The new-task form never rewrites a stored
|
|
110
|
+
// preference from it: the checkbox stays operable and is explained while the
|
|
111
|
+
// master switch is off, and the user's own choice is what a later re-enable
|
|
112
|
+
// restores.
|
|
113
|
+
const [goalRunEnabled, setGoalRunEnabled] = useState(controller.getSnapshot().host?.goalRunEnabled === true)
|
|
80
114
|
// "Parse pasted text" (issue #1540) exists only when the deployment carries a
|
|
81
115
|
// parse face; the section stays hidden otherwise.
|
|
82
116
|
const [canParse] = useState(controller.getSnapshot().canParseTask === true)
|
|
@@ -87,19 +121,23 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
87
121
|
const [parsePending, setParsePending] = useState(false)
|
|
88
122
|
const [parseError, setParseError] = useState<string | undefined>(undefined)
|
|
89
123
|
const parseAbort = useRef<AbortController | undefined>(undefined)
|
|
90
|
-
const
|
|
124
|
+
const models = options.models ?? []
|
|
91
125
|
// A pinned target may have disappeared from the runtime (workspace deleted,
|
|
92
126
|
// preset removed) — especially when the form starts from a subtask's parent.
|
|
93
127
|
// Keep it selectable as a stale row, so the field shows what the create
|
|
94
128
|
// action will actually send instead of silently displaying the default.
|
|
95
129
|
const workspaceKnown = workspaceId === '' || options.workspaces.some(item => item.workspaceId === workspaceId)
|
|
96
130
|
const modeKnown = mode === '' || options.presets.some(item => item.id === mode)
|
|
97
|
-
const modelKnown = model === '' ||
|
|
131
|
+
const modelKnown = model === '' || models.some(item => item.id === model)
|
|
98
132
|
|
|
99
133
|
// The workspace list and preset roster arrive from the runtime after mount;
|
|
100
134
|
// follow them so the pickers never freeze on an empty snapshot.
|
|
101
135
|
useEffect(
|
|
102
|
-
() => controller.subscribe(() =>
|
|
136
|
+
() => controller.subscribe(() => {
|
|
137
|
+
const snapshot = controller.getSnapshot()
|
|
138
|
+
setOptions(snapshot.executionOptions)
|
|
139
|
+
setGoalRunEnabled(snapshot.host?.goalRunEnabled === true)
|
|
140
|
+
}),
|
|
103
141
|
[controller],
|
|
104
142
|
)
|
|
105
143
|
|
|
@@ -107,8 +145,8 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
107
145
|
// no longer offers falls back to the Host default; an empty value is the
|
|
108
146
|
// Host-default choice and is never overwritten by the roster (issue #1621).
|
|
109
147
|
useEffect(() => {
|
|
110
|
-
if (parseModel === '' ||
|
|
111
|
-
if (
|
|
148
|
+
if (parseModel === '' || models.length === 0) return
|
|
149
|
+
if (models.some(option => option.id === parseModel)) return
|
|
112
150
|
setParseModel('')
|
|
113
151
|
writeParseModelPreference('')
|
|
114
152
|
}, [parseModel, options.models])
|
|
@@ -145,10 +183,25 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
145
183
|
*/
|
|
146
184
|
const submit = async (runAfterCreate: boolean): Promise<void> => {
|
|
147
185
|
if (scheduleEnabled) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
186
|
+
if (scheduleMode === 'once') {
|
|
187
|
+
const at = parseZonedInput(scheduleAt, scheduleZone === '' ? hostTimeZone : scheduleZone)
|
|
188
|
+
if (at === undefined || at <= Date.now()) {
|
|
189
|
+
setScheduleError(t('detail.schedule.atInvalid'))
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
const cron = scheduleCron.trim()
|
|
194
|
+
if (cron === '' || !isValidCron(cron)) {
|
|
195
|
+
setScheduleError(t('detail.schedule.invalid'))
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
if (scheduleMaxRunsChoice === 'custom') {
|
|
199
|
+
const cap = Number(scheduleMaxRunsText.trim())
|
|
200
|
+
if (scheduleMaxRunsText.trim() === '' || !Number.isInteger(cap) || cap < 1) {
|
|
201
|
+
setScheduleError(t('detail.schedule.maxRunsInvalid'))
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
}
|
|
152
205
|
}
|
|
153
206
|
}
|
|
154
207
|
// Optional continuation-card snapshot: parse the freeze block through the
|
|
@@ -183,16 +236,36 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
183
236
|
...(parentTask === undefined ? {} : { parentId: parentTask.id }),
|
|
184
237
|
freeze,
|
|
185
238
|
handover,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
239
|
+
// A cleared picker is OMITTED, never sent as an empty value: absence is
|
|
240
|
+
// how "no pin, resolved at run time" is stored, so blanking a field the
|
|
241
|
+
// dialog opened prefilled really unpins it. This dialog never sends
|
|
242
|
+
// `applyCardDefaults` — the prefill above is the whole feature here.
|
|
243
|
+
...(workspaceId === '' ? {} : { workspaceId }),
|
|
244
|
+
...(mode === '' ? {} : { mode }),
|
|
245
|
+
...(permission === '' ? {} : { permission: permission as TaskPermission }),
|
|
246
|
+
...(model === '' ? {} : { model }),
|
|
247
|
+
...(reasoningEffort === '' ? {} : { reasoningEffort }),
|
|
190
248
|
...(reuseSession ? { reuseSession: true } : {}),
|
|
191
|
-
...(goalRun ? {
|
|
249
|
+
...(goalRun ? { goalRun: true } : {}),
|
|
192
250
|
...(skipVerification ? { skipVerification: true } : {}),
|
|
193
251
|
...(tagList.length > 0 ? { tags: tagList } : {}),
|
|
194
252
|
schedule: scheduleEnabled
|
|
195
|
-
?
|
|
253
|
+
? scheduleMode === 'once'
|
|
254
|
+
? {
|
|
255
|
+
enabled: true,
|
|
256
|
+
mode: 'once' as const,
|
|
257
|
+
at: parseZonedInput(scheduleAt, scheduleZone === '' ? hostTimeZone : scheduleZone)!,
|
|
258
|
+
...(scheduleZone === '' ? {} : { timeZone: scheduleZone }),
|
|
259
|
+
}
|
|
260
|
+
: {
|
|
261
|
+
enabled: true,
|
|
262
|
+
mode: 'cron' as const,
|
|
263
|
+
cron: scheduleCron.trim(),
|
|
264
|
+
...(scheduleMaxRunsChoice === 'unlimited' ? {} : {
|
|
265
|
+
maxRuns: scheduleMaxRunsChoice === 'custom' ? Number(scheduleMaxRunsText.trim()) : Number(scheduleMaxRunsChoice),
|
|
266
|
+
}),
|
|
267
|
+
...(scheduleZone === '' ? {} : { timeZone: scheduleZone }),
|
|
268
|
+
}
|
|
196
269
|
: undefined,
|
|
197
270
|
})
|
|
198
271
|
if (task === undefined) {
|
|
@@ -200,6 +273,11 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
200
273
|
setError(controller.getSnapshot().transportError ?? t('new.required'))
|
|
201
274
|
return
|
|
202
275
|
}
|
|
276
|
+
// The join is confirmed, so a parent that just gained a subtask is opted in
|
|
277
|
+
// where Agent Teams are served. The write is a convenience, not part of the
|
|
278
|
+
// creation: a refused one (the card was archived meanwhile) already
|
|
279
|
+
// surfaced through the snapshot and never fails the created task.
|
|
280
|
+
await promoteParentForTeamRun(controller, parentTask)
|
|
203
281
|
if (isDuplicate && archiveOriginal && initialTask !== undefined) {
|
|
204
282
|
if (onDuplicateSuccess !== undefined) {
|
|
205
283
|
await onDuplicateSuccess(initialTask.id)
|
|
@@ -218,23 +296,29 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
218
296
|
onClose()
|
|
219
297
|
}
|
|
220
298
|
|
|
221
|
-
// Next-run preview for
|
|
222
|
-
//
|
|
299
|
+
// Next-run preview for the armed plan (creation-time only), computed in the
|
|
300
|
+
// selected zone so it matches what the Host will arm. A one-shot's next run
|
|
301
|
+
// IS its planned instant; a recurring rule resolves its next occurrence.
|
|
223
302
|
const hostTimeZone = controller.getSnapshot().host?.scheduler.timeZone
|
|
224
303
|
const scheduleTimeZone = scheduleZone === '' ? hostTimeZone : scheduleZone
|
|
225
|
-
const scheduleNextRun = scheduleEnabled
|
|
226
|
-
?
|
|
227
|
-
:
|
|
304
|
+
const scheduleNextRun = !scheduleEnabled
|
|
305
|
+
? undefined
|
|
306
|
+
: scheduleMode === 'once'
|
|
307
|
+
? parseZonedInput(scheduleAt, scheduleTimeZone)
|
|
308
|
+
: scheduleCron.trim() !== '' && isValidCron(scheduleCron)
|
|
309
|
+
? nextRunAtMs(scheduleCron, Date.now(), scheduleTimeZone)
|
|
310
|
+
: undefined
|
|
228
311
|
|
|
229
312
|
const modalTitle = parentTask !== undefined
|
|
230
313
|
? t('new.subtaskTitle')
|
|
231
314
|
: isDuplicate ? t('new.duplicateTitle') : t('board.new')
|
|
232
315
|
|
|
233
316
|
// Agent-preset picker rows: the four built-in presets first, then every other
|
|
234
|
-
// roster row (user presets)
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
317
|
+
// roster row (user presets), each labelled with its default/unavailable
|
|
318
|
+
// suffixes by the one row builder the settings card's default-preset field
|
|
319
|
+
// also renders through. The inherit option names the preset a run actually
|
|
320
|
+
// lands on when the card pins nothing.
|
|
321
|
+
const presetRows = presetChoiceRows(options.presets)
|
|
238
322
|
const inheritLabel = inheritPresetLabel(options.presets)
|
|
239
323
|
const selectedPreset = options.presets.find(preset => preset.id === mode)
|
|
240
324
|
const modeLabel = mode === '' ? inheritLabel : selectedPreset === undefined ? mode : presetLabel(selectedPreset)
|
|
@@ -249,6 +333,9 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
249
333
|
const modelLabel = model === ''
|
|
250
334
|
? t('exec.model.default')
|
|
251
335
|
: (options.models ?? []).find(item => item.id === model)?.name ?? model
|
|
336
|
+
// The effort is part of the model pin, so a pinned one rides the model's own
|
|
337
|
+
// summary entry instead of standing as a region-level value of its own.
|
|
338
|
+
const modelSummary = reasoningEffort === '' ? modelLabel : `${modelLabel} · ${reasoningEffort}`
|
|
252
339
|
// Collapsed-region summaries: what each closed region currently holds, so a
|
|
253
340
|
// collapse never hides the configuration without a trace.
|
|
254
341
|
// A host-default model adds only the long parenthetical name to the
|
|
@@ -257,12 +344,14 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
257
344
|
workspaceLabel,
|
|
258
345
|
modeLabel,
|
|
259
346
|
permissionLabel,
|
|
260
|
-
...(model === '' ? [] : [
|
|
347
|
+
...(model === '' ? [] : [modelSummary]),
|
|
261
348
|
].join(' · ')
|
|
262
349
|
const tagCount = cleanTags(tags).length
|
|
263
350
|
const labelsSummary = tagCount === 0 ? t('new.summary.none') : t('new.summary.labelCount', { count: String(tagCount) })
|
|
264
351
|
const runSummary = [
|
|
265
|
-
|
|
352
|
+
// A card whose goal option is on but whose master switch is off will run one
|
|
353
|
+
// plain turn: the collapsed line says the truth rather than the stored value.
|
|
354
|
+
goalRun && goalRunEnabled ? t('new.summary.multiRound') : t('new.summary.singleRound'),
|
|
266
355
|
...(reuseSession ? [t('exec.reuseSession')] : []),
|
|
267
356
|
].join(' · ')
|
|
268
357
|
const handoverSummary = freezeText.trim() !== '' || handoverText.trim() !== ''
|
|
@@ -270,7 +359,18 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
270
359
|
: t('new.summary.none')
|
|
271
360
|
const scheduleSummary = !scheduleEnabled
|
|
272
361
|
? t('new.summary.scheduleOff')
|
|
273
|
-
:
|
|
362
|
+
: scheduleMode === 'once'
|
|
363
|
+
? `${t('detail.schedule.mode.once')} · ${scheduleAt.replace('T', ' ')}`
|
|
364
|
+
: scheduleCron.trim() === ''
|
|
365
|
+
? t('new.summary.none')
|
|
366
|
+
: [
|
|
367
|
+
scheduleCron.trim(),
|
|
368
|
+
...(scheduleMaxRunsChoice === 'unlimited'
|
|
369
|
+
? []
|
|
370
|
+
: [t('detail.schedule.maxRunsValue', {
|
|
371
|
+
count: scheduleMaxRunsChoice === 'custom' ? scheduleMaxRunsText : scheduleMaxRunsChoice,
|
|
372
|
+
})]),
|
|
373
|
+
].join(' · ')
|
|
274
374
|
const parseSummary = parseText.trim() === '' ? t('new.summary.none') : t('new.summary.filled')
|
|
275
375
|
|
|
276
376
|
return (
|
|
@@ -313,7 +413,7 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
313
413
|
}}
|
|
314
414
|
>
|
|
315
415
|
<option value="">{t('exec.model.default')}</option>
|
|
316
|
-
{
|
|
416
|
+
{models.map(option => (
|
|
317
417
|
<option key={option.id} value={option.id}>{option.name ?? option.id}</option>
|
|
318
418
|
))}
|
|
319
419
|
</select>
|
|
@@ -340,9 +440,14 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
340
440
|
)}
|
|
341
441
|
|
|
342
442
|
{parentTask !== undefined && (
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
443
|
+
<>
|
|
444
|
+
<p className={css.detailText} data-dsh-part="subtask-inherit">
|
|
445
|
+
{t('detail.parent')}: {parentTask.title} · {t('new.subtaskInherit')}
|
|
446
|
+
</p>
|
|
447
|
+
{parentAwaitsTeamRunPromotion(controller, parentTask) && (
|
|
448
|
+
<p className={css.detailText}>{t('exec.teamRunAutoHint')}</p>
|
|
449
|
+
)}
|
|
450
|
+
</>
|
|
346
451
|
)}
|
|
347
452
|
|
|
348
453
|
<CollapsibleSection title={t('new.section.content')} defaultOpen>
|
|
@@ -395,25 +500,17 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
395
500
|
>
|
|
396
501
|
<option value="">{inheritLabel}</option>
|
|
397
502
|
{!modeKnown && <option value={mode}>{mode}{t('exec.mode.removed')}</option>}
|
|
398
|
-
{
|
|
503
|
+
{presetRows.builtin.length > 0 && (
|
|
399
504
|
<optgroup label={t('exec.mode.builtinGroup')}>
|
|
400
|
-
{
|
|
401
|
-
<option key={
|
|
402
|
-
{presetLabel(preset)}
|
|
403
|
-
{preset.isDefault ? t('exec.mode.defaultSuffix') : ''}
|
|
404
|
-
{preset.broken !== undefined ? t('exec.mode.brokenSuffix') : ''}
|
|
405
|
-
</option>
|
|
505
|
+
{presetRows.builtin.map(row => (
|
|
506
|
+
<option key={row.id} value={row.id} disabled={row.disabled}>{row.label}</option>
|
|
406
507
|
))}
|
|
407
508
|
</optgroup>
|
|
408
509
|
)}
|
|
409
|
-
{
|
|
510
|
+
{presetRows.custom.length > 0 && (
|
|
410
511
|
<optgroup label={t('exec.mode.customGroup')}>
|
|
411
|
-
{
|
|
412
|
-
<option key={
|
|
413
|
-
{presetLabel(preset)}
|
|
414
|
-
{preset.isDefault ? t('exec.mode.defaultSuffix') : ''}
|
|
415
|
-
{preset.broken !== undefined ? t('exec.mode.brokenSuffix') : ''}
|
|
416
|
-
</option>
|
|
512
|
+
{presetRows.custom.map(row => (
|
|
513
|
+
<option key={row.id} value={row.id} disabled={row.disabled}>{row.label}</option>
|
|
417
514
|
))}
|
|
418
515
|
</optgroup>
|
|
419
516
|
)}
|
|
@@ -441,15 +538,29 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
441
538
|
<select
|
|
442
539
|
className={css.select}
|
|
443
540
|
value={model}
|
|
444
|
-
onChange={event => {
|
|
541
|
+
onChange={event => {
|
|
542
|
+
// The effort belongs to the model: switching to a model that
|
|
543
|
+
// cannot honor the picked level drops it instead of carrying it.
|
|
544
|
+
const next = event.target.value
|
|
545
|
+
setModel(next)
|
|
546
|
+
setReasoningEffort(current => reconcileReasoningEffort(models, next, current))
|
|
547
|
+
}}
|
|
445
548
|
>
|
|
446
549
|
<option value="">{t('exec.model.default')}</option>
|
|
447
550
|
{!modelKnown && <option value={model}>{model}{t('exec.model.unknown')}</option>}
|
|
448
|
-
{
|
|
551
|
+
{models.map(item => (
|
|
449
552
|
<option key={item.id} value={item.id}>{item.name ?? item.id}</option>
|
|
450
553
|
))}
|
|
451
554
|
</select>
|
|
452
555
|
</label>
|
|
556
|
+
|
|
557
|
+
<ReasoningEffortField
|
|
558
|
+
models={models}
|
|
559
|
+
modelId={model}
|
|
560
|
+
value={reasoningEffort}
|
|
561
|
+
pending={pending}
|
|
562
|
+
onChange={setReasoningEffort}
|
|
563
|
+
/>
|
|
453
564
|
</CollapsibleSection>
|
|
454
565
|
|
|
455
566
|
<CollapsibleSection title={t('new.section.run')} summary={runSummary}>
|
|
@@ -467,11 +578,15 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
467
578
|
<input
|
|
468
579
|
type="checkbox"
|
|
469
580
|
checked={goalRun}
|
|
581
|
+
// Always operable: the global switch decides whether the choice
|
|
582
|
+
// takes effect, never whether it may be stated, and the state (and a
|
|
583
|
+
// duplicate's template) is never rewritten by it.
|
|
470
584
|
onChange={event => { setGoalRun(event.target.checked) }}
|
|
471
585
|
/>
|
|
472
586
|
<span>{t('exec.goalRun')}</span>
|
|
473
587
|
</label>
|
|
474
588
|
<p className={css.detailText}>{t('exec.goalRunHint')}</p>
|
|
589
|
+
{!goalRunEnabled && <p className={css.detailText}>{t('settings.goalRunGlobalDisabledTaskOption')}</p>}
|
|
475
590
|
|
|
476
591
|
<label className={css.scheduleToggle}>
|
|
477
592
|
<input
|
|
@@ -533,6 +648,32 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
533
648
|
</label>
|
|
534
649
|
{scheduleEnabled && (
|
|
535
650
|
<>
|
|
651
|
+
<label className={css.scheduleZone}>
|
|
652
|
+
<span>{t('detail.schedule.mode')}</span>
|
|
653
|
+
<select
|
|
654
|
+
className={css.schedulePreset}
|
|
655
|
+
value={scheduleMode}
|
|
656
|
+
aria-label={t('detail.schedule.mode')}
|
|
657
|
+
onChange={event => { setScheduleMode(event.target.value as ScheduleMode); setScheduleError(undefined) }}
|
|
658
|
+
>
|
|
659
|
+
<option value="cron">{t('detail.schedule.mode.cron')}</option>
|
|
660
|
+
<option value="once">{t('detail.schedule.mode.once')}</option>
|
|
661
|
+
</select>
|
|
662
|
+
</label>
|
|
663
|
+
{scheduleMode === 'once' ? (
|
|
664
|
+
<label className={css.scheduleZone}>
|
|
665
|
+
<span>{t('detail.schedule.at')}</span>
|
|
666
|
+
<input
|
|
667
|
+
className={css.input}
|
|
668
|
+
type="datetime-local"
|
|
669
|
+
value={scheduleAt}
|
|
670
|
+
aria-label={t('detail.schedule.at')}
|
|
671
|
+
title={t('detail.schedule.atHint')}
|
|
672
|
+
onChange={event => { setScheduleAt(event.target.value); setScheduleError(undefined) }}
|
|
673
|
+
/>
|
|
674
|
+
</label>
|
|
675
|
+
) : (
|
|
676
|
+
<>
|
|
536
677
|
<div className={css.scheduleRow}>
|
|
537
678
|
<input
|
|
538
679
|
className={`${css.input} ${css.scheduleInput}${scheduleError !== undefined ? ` ${css.scheduleInputInvalid}` : ''}`}
|
|
@@ -558,6 +699,42 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
558
699
|
))}
|
|
559
700
|
</select>
|
|
560
701
|
</div>
|
|
702
|
+
<label className={css.scheduleZone}>
|
|
703
|
+
<span>{t('detail.schedule.maxRuns')}</span>
|
|
704
|
+
<select
|
|
705
|
+
className={css.schedulePreset}
|
|
706
|
+
value={scheduleMaxRunsChoice}
|
|
707
|
+
aria-label={t('detail.schedule.maxRuns')}
|
|
708
|
+
onChange={event => {
|
|
709
|
+
setScheduleMaxRunsChoice(event.target.value)
|
|
710
|
+
setScheduleError(undefined)
|
|
711
|
+
if (event.target.value === 'custom') {
|
|
712
|
+
setScheduleMaxRunsText(initialTask?.schedule?.maxRuns === undefined ? '' : String(initialTask.schedule.maxRuns))
|
|
713
|
+
}
|
|
714
|
+
}}
|
|
715
|
+
>
|
|
716
|
+
<option value="unlimited">{t('detail.schedule.maxRunsUnlimited')}</option>
|
|
717
|
+
<option value="1">{t('detail.schedule.maxRunsValue', { count: '1' })}</option>
|
|
718
|
+
<option value="2">{t('detail.schedule.maxRunsValue', { count: '2' })}</option>
|
|
719
|
+
<option value="custom">{t('detail.schedule.maxRunsCustom')}</option>
|
|
720
|
+
</select>
|
|
721
|
+
</label>
|
|
722
|
+
{scheduleMaxRunsChoice === 'custom' && (
|
|
723
|
+
<label className={css.scheduleZone}>
|
|
724
|
+
<span>{t('detail.schedule.maxRuns')}</span>
|
|
725
|
+
<input
|
|
726
|
+
className={css.input}
|
|
727
|
+
type="number"
|
|
728
|
+
min={1}
|
|
729
|
+
step={1}
|
|
730
|
+
value={scheduleMaxRunsText}
|
|
731
|
+
aria-label={t('detail.schedule.maxRuns')}
|
|
732
|
+
onChange={event => { setScheduleMaxRunsText(event.target.value); setScheduleError(undefined) }}
|
|
733
|
+
/>
|
|
734
|
+
</label>
|
|
735
|
+
)}
|
|
736
|
+
</>
|
|
737
|
+
)}
|
|
561
738
|
{scheduleError !== undefined && <p className={css.formError}>{scheduleError}</p>}
|
|
562
739
|
<label className={css.scheduleZone}>
|
|
563
740
|
<span>{t('detail.schedule.timeZone')}</span>
|
|
@@ -565,7 +742,7 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
565
742
|
className={css.schedulePreset}
|
|
566
743
|
value={scheduleZone}
|
|
567
744
|
aria-label={t('detail.schedule.timeZone')}
|
|
568
|
-
title={t('detail.schedule.timeZoneHint')}
|
|
745
|
+
title={scheduleMode === 'once' ? t('detail.schedule.zoneOnceHint') : t('detail.schedule.timeZoneHint')}
|
|
569
746
|
onChange={event => { setScheduleZone(event.target.value); setScheduleError(undefined) }}
|
|
570
747
|
>
|
|
571
748
|
{zoneChoices(hostTimeZone, initialTask?.schedule?.timeZone).map(choice => (
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reasoning-effort field: one picker shared by both execution-target editors
|
|
3
|
+
* (the create dialog and the detail page), so the two never describe the same
|
|
4
|
+
* capability differently. The levels are the ones the picked model's catalog
|
|
5
|
+
* entry declares; an empty value is a real choice — "follow the model or Host
|
|
6
|
+
* default" — and is the default state.
|
|
7
|
+
*
|
|
8
|
+
* A stored level the roster no longer lists (the model left the roster, or its
|
|
9
|
+
* adapter changed) stays shown and labelled, but is not offered again, so the
|
|
10
|
+
* field never silently shows a different value than the one the card will ask
|
|
11
|
+
* for while the choosable set stays exactly the levels the model declares.
|
|
12
|
+
*/
|
|
13
|
+
import type { ExecutionModelOption } from '../../core/controller.ts'
|
|
14
|
+
import { t } from '../locales.ts'
|
|
15
|
+
import css from '../board.module.css'
|
|
16
|
+
import { reasoningEffortsFor, reasoningHintKey, reasoningPickerEnabled } from './model-options.ts'
|
|
17
|
+
|
|
18
|
+
/** The controlled reasoning-effort picker. */
|
|
19
|
+
export function ReasoningEffortField({ models, modelId, value, pending, onChange }: {
|
|
20
|
+
/** The roster the picked model is looked up in. */
|
|
21
|
+
models: readonly ExecutionModelOption[]
|
|
22
|
+
/** The model this card pins; '' means "no pin". */
|
|
23
|
+
modelId: string
|
|
24
|
+
/** The selected effort; '' means "follow the model or Host default". */
|
|
25
|
+
value: string
|
|
26
|
+
/** Whether a mutation is in flight (the field locks with the rest of the form). */
|
|
27
|
+
pending: boolean
|
|
28
|
+
onChange: (next: string) => void
|
|
29
|
+
}) {
|
|
30
|
+
const efforts = reasoningEffortsFor(models, modelId)
|
|
31
|
+
const unlisted = value !== '' && !efforts.includes(value)
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
<label className={css.field}>
|
|
35
|
+
<span className={css.fieldLabel}>{t('exec.reasoning')}</span>
|
|
36
|
+
<select
|
|
37
|
+
className={css.select}
|
|
38
|
+
value={value}
|
|
39
|
+
disabled={pending || !reasoningPickerEnabled(models, modelId)}
|
|
40
|
+
onChange={event => { onChange(event.target.value) }}
|
|
41
|
+
>
|
|
42
|
+
<option value="">{t('exec.reasoning.inherit')}</option>
|
|
43
|
+
{unlisted && <option value={value} disabled>{value}{t('exec.reasoning.unknown')}</option>}
|
|
44
|
+
{efforts.map(effort => <option key={effort} value={effort}>{effort}</option>)}
|
|
45
|
+
</select>
|
|
46
|
+
</label>
|
|
47
|
+
<p className={css.detailText}>{t(reasoningHintKey(models, modelId))}</p>
|
|
48
|
+
</>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -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 && (
|