@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.
Files changed (128) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +56 -18
  3. package/README.zh.md +56 -18
  4. package/lib/client.js +6242 -958
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +3475 -234
  7. package/lib/types/client/PluginSettingsCard.d.ts +7 -0
  8. package/lib/types/client/TaskBoardSettingsCard.d.ts +19 -3
  9. package/lib/types/client/board/ClampedMarkdown.d.ts +3 -0
  10. package/lib/types/client/board/ConfirmDialog.d.ts +4 -1
  11. package/lib/types/client/board/EditTaskModal.d.ts +5 -2
  12. package/lib/types/client/board/LinkSubtaskModal.d.ts +5 -2
  13. package/lib/types/client/board/NewTaskModal.d.ts +4 -1
  14. package/lib/types/client/board/TagManagerModal.d.ts +26 -0
  15. package/lib/types/client/board/TaskBoard.d.ts +10 -1
  16. package/lib/types/client/board/TaskCard.d.ts +14 -1
  17. package/lib/types/client/board/TaskDetail.d.ts +3 -1
  18. package/lib/types/client/board/TaskForm.d.ts +23 -2
  19. package/lib/types/client/board/VerificationReport.d.ts +23 -0
  20. package/lib/types/client/board/card-view.d.ts +113 -0
  21. package/lib/types/client/board/icons.d.ts +32 -0
  22. package/lib/types/client/board/overlay.d.ts +69 -0
  23. package/lib/types/client/board/preset-label.d.ts +22 -0
  24. package/lib/types/client/board/status-key.d.ts +15 -0
  25. package/lib/types/client/board/task-markdown.d.ts +19 -0
  26. package/lib/types/client/board-card-seat.d.ts +34 -0
  27. package/lib/types/client/host-api.d.ts +24 -1
  28. package/lib/types/client/index.d.ts +19 -0
  29. package/lib/types/client/locales.d.ts +141 -3
  30. package/lib/types/client/native-panel.d.ts +6 -20
  31. package/lib/types/client/schedule-zone.d.ts +47 -0
  32. package/lib/types/client/seats.d.ts +18 -0
  33. package/lib/types/client/service.d.ts +32 -0
  34. package/lib/types/client/settings-form.d.ts +16 -0
  35. package/lib/types/core/controller.d.ts +38 -1
  36. package/lib/types/core/degraded-reason.d.ts +46 -0
  37. package/lib/types/core/extension.d.ts +283 -0
  38. package/lib/types/core/handover.d.ts +13 -1
  39. package/lib/types/core/issue-form.d.ts +16 -0
  40. package/lib/types/core/package-name.d.ts +9 -0
  41. package/lib/types/core/schedule.d.ts +95 -20
  42. package/lib/types/core/subtask.d.ts +30 -11
  43. package/lib/types/core/tasks.d.ts +110 -5
  44. package/lib/types/core/use-cases/task-create.d.ts +2 -1
  45. package/lib/types/core/use-cases/task-schedule.d.ts +8 -4
  46. package/lib/types/core/use-cases/task-tag.d.ts +48 -0
  47. package/lib/types/core/use-cases/task-update.d.ts +5 -3
  48. package/lib/types/core/verification.d.ts +386 -0
  49. package/lib/types/core/workspace-target.d.ts +41 -0
  50. package/lib/types/host/agent-tools.d.ts +1 -1
  51. package/lib/types/host/extension-registry.d.ts +111 -0
  52. package/lib/types/host/llm-dispatch.d.ts +63 -0
  53. package/lib/types/host/verification-gate.d.ts +71 -0
  54. package/lib/types/host/verification-runner.d.ts +108 -0
  55. package/lib/types/host/workspace-evidence.d.ts +53 -0
  56. package/lib/types/host-ledger.d.ts +107 -11
  57. package/lib/types/host-runner.d.ts +99 -4
  58. package/lib/types/host-service.d.ts +114 -2
  59. package/lib/types/index.d.ts +80 -26
  60. package/lib/types/protocol.d.ts +53 -3
  61. package/lib/types/tool-surface.d.ts +71 -0
  62. package/package.json +27 -26
  63. package/src/client/PluginSettingsCard.tsx +9 -0
  64. package/src/client/TaskBoardSettingsCard.tsx +304 -65
  65. package/src/client/board/ClampedMarkdown.tsx +69 -0
  66. package/src/client/board/ConfirmDialog.tsx +7 -3
  67. package/src/client/board/EditTaskModal.tsx +5 -2
  68. package/src/client/board/LinkSubtaskModal.tsx +30 -15
  69. package/src/client/board/NewTaskModal.tsx +288 -151
  70. package/src/client/board/TagManagerModal.tsx +235 -0
  71. package/src/client/board/TaskBoard.tsx +259 -103
  72. package/src/client/board/TaskCard.tsx +238 -86
  73. package/src/client/board/TaskDetail.tsx +154 -59
  74. package/src/client/board/TaskForm.tsx +61 -6
  75. package/src/client/board/VerificationReport.tsx +203 -0
  76. package/src/client/board/card-view.ts +220 -0
  77. package/src/client/board/icons.tsx +89 -0
  78. package/src/client/board/overlay.tsx +189 -0
  79. package/src/client/board/preset-label.ts +44 -0
  80. package/src/client/board/status-key.ts +27 -0
  81. package/src/client/board/task-markdown.module.css +137 -0
  82. package/src/client/board/task-markdown.tsx +106 -0
  83. package/src/client/board-card-seat.ts +102 -0
  84. package/src/client/board-settings.module.css +66 -0
  85. package/src/client/board.module.css +602 -68
  86. package/src/client/host-api.ts +109 -6
  87. package/src/client/index.ts +48 -6
  88. package/src/client/locales.ts +290 -12
  89. package/src/client/native-panel.tsx +18 -2
  90. package/src/client/schedule-zone.ts +135 -0
  91. package/src/client/seats.tsx +58 -0
  92. package/src/client/service.ts +94 -0
  93. package/src/client/settings-card.module.css +31 -5
  94. package/src/client/settings-form.ts +27 -1
  95. package/src/core/controller.ts +101 -5
  96. package/src/core/degraded-reason.ts +81 -0
  97. package/src/core/extension.ts +376 -0
  98. package/src/core/freeze-snapshot.ts +2 -2
  99. package/src/core/handover.ts +16 -1
  100. package/src/core/issue-form.ts +19 -0
  101. package/src/core/package-name.ts +9 -0
  102. package/src/core/schedule.ts +211 -55
  103. package/src/core/store.ts +26 -1
  104. package/src/core/subtask.ts +77 -22
  105. package/src/core/tasks.ts +138 -10
  106. package/src/core/use-cases/task-create.ts +14 -2
  107. package/src/core/use-cases/task-parent.ts +3 -2
  108. package/src/core/use-cases/task-schedule.ts +17 -6
  109. package/src/core/use-cases/task-tag.ts +110 -0
  110. package/src/core/use-cases/task-update.ts +13 -4
  111. package/src/core/verification.ts +826 -0
  112. package/src/core/workspace-target.ts +59 -0
  113. package/src/host/agent-tools.ts +58 -24
  114. package/src/host/extension-registry.ts +509 -0
  115. package/src/host/llm-dispatch.ts +87 -0
  116. package/src/host/verification-gate.ts +331 -0
  117. package/src/host/verification-runner.ts +535 -0
  118. package/src/host/workspace-evidence.ts +176 -0
  119. package/src/host-ai.ts +15 -9
  120. package/src/host-ledger.ts +342 -51
  121. package/src/host-routes.ts +15 -2
  122. package/src/host-runner.ts +219 -20
  123. package/src/host-service.ts +346 -26
  124. package/src/index.ts +204 -16
  125. package/src/protocol.ts +122 -9
  126. package/src/tool-surface.ts +95 -0
  127. package/lib/types/host/run-guarded.d.ts +0 -39
  128. package/src/host/run-guarded.ts +0 -76
@@ -4,12 +4,13 @@
4
4
  * a link the Host would refuse is not offered in the first place; the Host
5
5
  * still re-checks the action at submit time.
6
6
  */
7
- import { useState } from 'react'
7
+ import { useMemo, useState } from 'react'
8
8
  import type { BoardController } from '../../core/controller.ts'
9
- import { DEFAULT_SUBTASK_DEPTH, checkParentLink } from '../../core/subtask.ts'
10
- import type { TaskRecord } from '../../core/tasks.ts'
9
+ import { DEFAULT_SUBTASK_DEPTH, buildLineageIndex, checkParentLink } from '../../core/subtask.ts'
10
+ import { hasOpenExecution, type TaskRecord } from '../../core/tasks.ts'
11
11
  import { t } from '../locales.ts'
12
12
  import css from '../board.module.css'
13
+ import { useDialog, type OverlayPhase } from './overlay.tsx'
13
14
  import { STATUS_KEY } from './status-key.ts'
14
15
 
15
16
  /** Link-subtask overlay props. */
@@ -18,25 +19,39 @@ export interface LinkSubtaskModalProps {
18
19
  /** The task the picked card becomes a subtask of. */
19
20
  parent: TaskRecord
20
21
  onClose: () => void
22
+ /** Which leg of the enter/exit motion pair the surface is on. */
23
+ phase?: OverlayPhase
21
24
  }
22
25
 
23
26
  /** Link-subtask overlay. */
24
- export function LinkSubtaskModal({ controller, parent, onClose }: LinkSubtaskModalProps) {
27
+ export function LinkSubtaskModal({ controller, parent, onClose, phase = 'open' }: LinkSubtaskModalProps) {
28
+ const dialog = useDialog<HTMLDivElement>(onClose, phase)
25
29
  const [pending, setPending] = useState(false)
26
30
  const [error, setError] = useState<string | undefined>(undefined)
27
31
  const snapshot = controller.getSnapshot()
28
32
  const limit = snapshot.host?.maxSubtaskDepth ?? DEFAULT_SUBTASK_DEPTH
29
33
  // Only root, on-board tasks can move under another parent; the lineage gate
30
34
  // decides the rest (depth, cycle, archived parent).
31
- const candidates = snapshot.tasks.filter(task =>
32
- task.archivedAt === undefined
33
- // A running task may already be a participant of another run group; the
34
- // Host refuses the move, so it is not offered here.
35
- && task.status !== 'running'
36
- && task.parentId === undefined
37
- && task.id !== parent.id
38
- && checkParentLink(snapshot.tasks, task.id, parent.id, limit).ok,
39
- )
35
+ //
36
+ // The gate is run once per candidate task, so it is given ONE lineage index
37
+ // for the whole pass: without it every call rebuilds the id/child maps and
38
+ // rescans the ledger, turning a render into quadratic work. The derivation
39
+ // also runs on every controller notification (the SSE heartbeat included),
40
+ // so it is memoized on the inputs it actually reads.
41
+ const tasks = snapshot.tasks
42
+ const candidates = useMemo(() => {
43
+ const lineage = buildLineageIndex(tasks)
44
+ return tasks.filter(task =>
45
+ task.archivedAt === undefined
46
+ // An executing task may already be a participant of another run group;
47
+ // the Host refuses the move, so it is not offered here. A card parked in
48
+ // the running column by hand carries no execution and stays linkable.
49
+ && !hasOpenExecution(task)
50
+ && task.parentId === undefined
51
+ && task.id !== parent.id
52
+ && checkParentLink(tasks, task.id, parent.id, limit, lineage).ok,
53
+ )
54
+ }, [tasks, parent.id, limit])
40
55
  const link = async (taskId: string): Promise<void> => {
41
56
  setPending(true)
42
57
  setError(undefined)
@@ -48,8 +63,8 @@ export function LinkSubtaskModal({ controller, parent, onClose }: LinkSubtaskMod
48
63
  setError(controller.getSnapshot().transportError ?? t('new.required'))
49
64
  }
50
65
  return (
51
- <div className={css.modalBackdrop} onMouseDown={event => { if (event.target === event.currentTarget) onClose() }}>
52
- <div className={css.modal} role="dialog" aria-label={t('link.subtask.title')}>
66
+ <div className={css.modalBackdrop} data-state={phase} onMouseDown={dialog.onMouseDown}>
67
+ <div ref={dialog.attach} className={css.modal} role="dialog" aria-modal="true" aria-label={t('link.subtask.title')} tabIndex={-1}>
53
68
  <h2 className={css.modalTitle}>{t('link.subtask.title')}</h2>
54
69
  <p className={css.fieldHint}>{t('link.subtask.hint')}</p>
55
70
  {candidates.length === 0
@@ -10,8 +10,12 @@ import { effectiveTaskPermission } from '../../core/subtask.ts'
10
10
  import { collectKnownTags, TASK_PERMISSIONS, 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 { ModalShell, TaskContentFields, TaskTagFields, cleanTags } from './TaskForm.tsx'
13
+ import { nextRunLabel, zoneChoices } from '../schedule-zone.ts'
14
+ import { CollapsibleSection, ModalShell, TaskContentFields, TaskTagFields, cleanTags } from './TaskForm.tsx'
15
+ import type { OverlayPhase } from './overlay.tsx'
16
+ import { formatHostTimestamp } from './TaskCard.tsx'
14
17
  import { readParseModelPreference, writeParseModelPreference } from './parse-model-pref.ts'
18
+ import { inheritPresetLabel, isBuiltinPreset, presetLabel } from './preset-label.ts'
15
19
  import css from '../board.module.css'
16
20
 
17
21
  export interface NewTaskModalProps {
@@ -32,10 +36,12 @@ export interface NewTaskModalProps {
32
36
  * carries the parent link; unset targets inherit again at run time.
33
37
  */
34
38
  parentTask?: TaskRecord
39
+ /** Which leg of the enter/exit motion pair the surface is on. */
40
+ phase?: OverlayPhase
35
41
  }
36
42
 
37
43
  /** New-task form overlay. */
38
- export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspaceId, onDuplicateSuccess, parentTask }: NewTaskModalProps) {
44
+ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspaceId, onDuplicateSuccess, parentTask, phase }: NewTaskModalProps) {
39
45
  const isDuplicate = initialTask !== undefined
40
46
  const [title, setTitle] = useState(initialTask?.title ?? '')
41
47
  const [description, setDescription] = useState(initialTask?.description ?? '')
@@ -51,8 +57,15 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
51
57
  const [model, setModel] = useState(initialTask?.model ?? parentTask?.model ?? '')
52
58
  const inheritedPermission = parentTask === undefined ? undefined : effectiveTaskPermission(parentTask)
53
59
  const [reuseSession, setReuseSession] = useState(initialTask?.reuseSession ?? false)
60
+ // Checked by default: a new task starts its runs with dsh's built-in /goal
61
+ // unless the user opts out here. A duplicate keeps the original card's choice.
62
+ const [goalRun, setGoalRun] = useState(initialTask?.goalRun ?? true)
63
+ const [skipVerification, setSkipVerification] = useState(initialTask?.skipVerification ?? false)
54
64
  const [scheduleEnabled, setScheduleEnabled] = useState(initialTask?.schedule?.enabled ?? false)
55
65
  const [scheduleCron, setScheduleCron] = useState(initialTask?.schedule?.cron ?? '')
66
+ // '' means "follow the Host zone" (store no zone), which is the default a
67
+ // user gets unless they pick one explicitly.
68
+ const [scheduleZone, setScheduleZone] = useState(initialTask?.schedule?.timeZone ?? '')
56
69
  const [scheduleError, setScheduleError] = useState<string | undefined>(undefined)
57
70
  const [freezeText, setFreezeText] = useState('')
58
71
  const [freezeError, setFreezeError] = useState<string | undefined>(undefined)
@@ -175,8 +188,12 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
175
188
  permission: permission === '' ? undefined : permission as TaskPermission,
176
189
  model: model === '' ? undefined : model,
177
190
  ...(reuseSession ? { reuseSession: true } : {}),
191
+ ...(goalRun ? {} : { goalRun: false }),
192
+ ...(skipVerification ? { skipVerification: true } : {}),
178
193
  ...(tagList.length > 0 ? { tags: tagList } : {}),
179
- schedule: scheduleEnabled ? { enabled: true, cron: scheduleCron.trim() } : undefined,
194
+ schedule: scheduleEnabled
195
+ ? { enabled: true, cron: scheduleCron.trim(), ...(scheduleZone === '' ? {} : { timeZone: scheduleZone }) }
196
+ : undefined,
180
197
  })
181
198
  if (task === undefined) {
182
199
  setPending(false)
@@ -201,15 +218,61 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
201
218
  onClose()
202
219
  }
203
220
 
204
- /** Next-run preview for a valid armed cron (creation-time only). */
221
+ // Next-run preview for a valid armed cron (creation-time only), computed in
222
+ // the selected zone so it matches what the Host will arm.
223
+ const hostTimeZone = controller.getSnapshot().host?.scheduler.timeZone
224
+ const scheduleTimeZone = scheduleZone === '' ? hostTimeZone : scheduleZone
205
225
  const scheduleNextRun = scheduleEnabled && scheduleCron.trim() !== '' && isValidCron(scheduleCron)
206
- ? nextRunAtMs(scheduleCron, Date.now())
226
+ ? nextRunAtMs(scheduleCron, Date.now(), scheduleTimeZone)
207
227
  : undefined
208
228
 
209
229
  const modalTitle = parentTask !== undefined
210
230
  ? t('new.subtaskTitle')
211
231
  : isDuplicate ? t('new.duplicateTitle') : t('board.new')
212
232
 
233
+ // Agent-preset picker rows: the four built-in presets first, then every other
234
+ // roster row (user presets). The inherit option names the preset a run
235
+ // actually lands on when the card pins nothing.
236
+ const builtinPresets = options.presets.filter(preset => isBuiltinPreset(preset.id))
237
+ const customPresets = options.presets.filter(preset => !isBuiltinPreset(preset.id))
238
+ const inheritLabel = inheritPresetLabel(options.presets)
239
+ const selectedPreset = options.presets.find(preset => preset.id === mode)
240
+ const modeLabel = mode === '' ? inheritLabel : selectedPreset === undefined ? mode : presetLabel(selectedPreset)
241
+ const workspaceLabel = workspaceId === ''
242
+ ? t('exec.workspace.recent')
243
+ : options.workspaces.find(item => item.workspaceId === workspaceId)?.title ?? workspaceId
244
+ const permissionLabel = permission === ''
245
+ ? inheritedPermission === undefined
246
+ ? t('exec.permission.default')
247
+ : t('exec.permission.inheritParent', { permission: t(`exec.permission.${inheritedPermission}` as TaskBoardKey) })
248
+ : t(`exec.permission.${permission}` as TaskBoardKey)
249
+ const modelLabel = model === ''
250
+ ? t('exec.model.default')
251
+ : (options.models ?? []).find(item => item.id === model)?.name ?? model
252
+ // Collapsed-region summaries: what each closed region currently holds, so a
253
+ // collapse never hides the configuration without a trace.
254
+ // A host-default model adds only the long parenthetical name to the
255
+ // collapsed line, so it is summarized only once it is actually pinned.
256
+ const executionSummary = [
257
+ workspaceLabel,
258
+ modeLabel,
259
+ permissionLabel,
260
+ ...(model === '' ? [] : [modelLabel]),
261
+ ].join(' · ')
262
+ const tagCount = cleanTags(tags).length
263
+ const labelsSummary = tagCount === 0 ? t('new.summary.none') : t('new.summary.labelCount', { count: String(tagCount) })
264
+ const runSummary = [
265
+ goalRun ? t('new.summary.multiRound') : t('new.summary.singleRound'),
266
+ ...(reuseSession ? [t('exec.reuseSession')] : []),
267
+ ].join(' · ')
268
+ const handoverSummary = freezeText.trim() !== '' || handoverText.trim() !== ''
269
+ ? t('new.summary.filled')
270
+ : t('new.summary.none')
271
+ const scheduleSummary = !scheduleEnabled
272
+ ? t('new.summary.scheduleOff')
273
+ : scheduleCron.trim() === '' ? t('new.summary.none') : scheduleCron.trim()
274
+ const parseSummary = parseText.trim() === '' ? t('new.summary.none') : t('new.summary.filled')
275
+
213
276
  return (
214
277
  <ModalShell
215
278
  ariaLabel={modalTitle}
@@ -219,54 +282,61 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
219
282
  submitLabel={t('new.submit')}
220
283
  onSubmit={() => { void submit(false) }}
221
284
  onClose={onClose}
285
+ phase={phase}
222
286
  secondaryAction={{ label: t('new.createAndRun'), onSubmit: () => { void submit(true) } }}
223
287
  >
224
288
  {canParse && (
225
- <section className={css.aiParse} data-dsh-part="ai-parse">
226
- <span className={css.fieldLabel}>{t('new.aiParse')}</span>
227
- <p className={css.fieldHint}>{t('new.aiParseHint')}</p>
228
- <textarea
229
- className={css.input}
230
- rows={3}
231
- value={parseText}
232
- placeholder={t('new.aiParsePlaceholder')}
233
- spellCheck={false}
234
- onChange={event => { setParseText(event.target.value); setParseError(undefined) }}
235
- />
236
- <div className={css.aiParseRow}>
237
- <select
238
- className={css.select}
239
- value={parseModel}
240
- aria-label={t('new.aiParseModel')}
241
- onChange={event => {
242
- setParseModel(event.target.value)
243
- writeParseModelPreference(event.target.value)
244
- }}
245
- >
246
- <option value="">{t('exec.model.default')}</option>
247
- {parseModels.map(option => (
248
- <option key={option.id} value={option.id}>{option.name ?? option.id}</option>
249
- ))}
250
- </select>
251
- {parsePending
252
- ? (
253
- <button type="button" className={css.ghostButton} onClick={() => { parseAbort.current?.abort() }}>
254
- {t('new.aiParseCancel')}
255
- </button>
256
- )
257
- : (
258
- <button
259
- type="button"
260
- className={css.primaryButton}
261
- disabled={parseText.trim() === ''}
262
- onClick={() => { void runParse() }}
263
- >
264
- {t('new.aiParseRun')}
265
- </button>
266
- )}
267
- </div>
268
- {parseError !== undefined && <p className={css.formError}>{parseError}</p>}
269
- </section>
289
+ <CollapsibleSection
290
+ title={t('new.section.parse')}
291
+ summary={parseSummary}
292
+ forceOpen={parseError !== undefined}
293
+ >
294
+ <section className={css.aiParse} data-dsh-part="ai-parse">
295
+ <span className={css.fieldLabel}>{t('new.aiParse')}</span>
296
+ <p className={css.fieldHint}>{t('new.aiParseHint')}</p>
297
+ <textarea
298
+ className={css.input}
299
+ rows={3}
300
+ value={parseText}
301
+ placeholder={t('new.aiParsePlaceholder')}
302
+ spellCheck={false}
303
+ onChange={event => { setParseText(event.target.value); setParseError(undefined) }}
304
+ />
305
+ <div className={css.aiParseRow}>
306
+ <select
307
+ className={css.select}
308
+ value={parseModel}
309
+ aria-label={t('new.aiParseModel')}
310
+ onChange={event => {
311
+ setParseModel(event.target.value)
312
+ writeParseModelPreference(event.target.value)
313
+ }}
314
+ >
315
+ <option value="">{t('exec.model.default')}</option>
316
+ {parseModels.map(option => (
317
+ <option key={option.id} value={option.id}>{option.name ?? option.id}</option>
318
+ ))}
319
+ </select>
320
+ {parsePending
321
+ ? (
322
+ <button type="button" className={css.ghostButton} onClick={() => { parseAbort.current?.abort() }}>
323
+ {t('new.aiParseCancel')}
324
+ </button>
325
+ )
326
+ : (
327
+ <button
328
+ type="button"
329
+ className={css.primaryButton}
330
+ disabled={parseText.trim() === ''}
331
+ onClick={() => { void runParse() }}
332
+ >
333
+ {t('new.aiParseRun')}
334
+ </button>
335
+ )}
336
+ </div>
337
+ {parseError !== undefined && <p className={css.formError}>{parseError}</p>}
338
+ </section>
339
+ </CollapsibleSection>
270
340
  )}
271
341
 
272
342
  {parentTask !== undefined && (
@@ -275,42 +345,32 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
275
345
  </p>
276
346
  )}
277
347
 
278
- <TaskContentFields
279
- title={title}
280
- description={description}
281
- prompt={prompt}
282
- onTitleChange={value => { setTitle(value); setError(undefined) }}
283
- onDescriptionChange={setDescription}
284
- onPromptChange={setPrompt}
285
- />
286
-
287
- <TaskTagFields tags={tags} knownTags={collectKnownTags(controller.getSnapshot().tasks)} onChange={setTags} />
288
-
289
- <label className={css.field}>
290
- <span className={css.fieldLabel}>{t('new.freeze')}</span>
291
- <textarea
292
- className={css.input}
293
- rows={4}
294
- value={freezeText}
295
- placeholder={t('new.freezePlaceholder')}
296
- spellCheck={false}
297
- onChange={event => { setFreezeText(event.target.value); setFreezeError(undefined) }}
298
- />
299
- </label>
300
- {freezeError !== undefined && <p className={css.formError}>{freezeError}</p>}
348
+ <CollapsibleSection title={t('new.section.content')} defaultOpen>
349
+ <TaskContentFields
350
+ title={title}
351
+ description={description}
352
+ prompt={prompt}
353
+ onTitleChange={value => { setTitle(value); setError(undefined) }}
354
+ onDescriptionChange={setDescription}
355
+ onPromptChange={setPrompt}
356
+ />
357
+ {isDuplicate && (
358
+ <label className={css.checkboxLabel}>
359
+ <input
360
+ type="checkbox"
361
+ checked={archiveOriginal}
362
+ onChange={event => { setArchiveOriginal(event.target.checked) }}
363
+ />
364
+ <span>{t('new.archiveOriginal')}</span>
365
+ </label>
366
+ )}
367
+ </CollapsibleSection>
301
368
 
302
- <label className={css.field}>
303
- <span className={css.fieldLabel}>{t('new.handover')}</span>
304
- <textarea
305
- className={css.input}
306
- rows={3}
307
- value={handoverText}
308
- placeholder={t('new.handoverPlaceholder')}
309
- spellCheck={false}
310
- onChange={event => { setHandoverText(event.target.value) }}
311
- />
312
- </label>
369
+ <CollapsibleSection title={t('new.section.labels')} summary={labelsSummary}>
370
+ <TaskTagFields tags={tags} knownTags={collectKnownTags(controller.getSnapshot().tasks)} onChange={setTags} />
371
+ </CollapsibleSection>
313
372
 
373
+ <CollapsibleSection title={t('new.section.execution')} summary={executionSummary}>
314
374
  <label className={css.field}>
315
375
  <span className={css.fieldLabel}>{t('new.workspace')}</span>
316
376
  <select
@@ -327,21 +387,36 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
327
387
  </label>
328
388
 
329
389
  <label className={css.field}>
330
- <span className={css.fieldLabel}>{t('new.mode')}</span>
390
+ <span className={css.fieldLabel}>{t('new.agentPreset')}</span>
331
391
  <select
332
392
  className={css.select}
333
393
  value={mode}
334
394
  onChange={event => { setMode(event.target.value) }}
335
395
  >
336
- <option value="">{t('exec.mode.default')}</option>
396
+ <option value="">{inheritLabel}</option>
337
397
  {!modeKnown && <option value={mode}>{mode}{t('exec.mode.removed')}</option>}
338
- {options.presets.map(preset => (
339
- <option key={preset.id} value={preset.id} disabled={preset.broken !== undefined}>
340
- {preset.name ?? preset.id}
341
- {preset.isDefault ? t('exec.mode.defaultSuffix') : ''}
342
- {preset.broken !== undefined ? t('exec.mode.brokenSuffix') : ''}
343
- </option>
344
- ))}
398
+ {builtinPresets.length > 0 && (
399
+ <optgroup label={t('exec.mode.builtinGroup')}>
400
+ {builtinPresets.map(preset => (
401
+ <option key={preset.id} value={preset.id} disabled={preset.broken !== undefined}>
402
+ {presetLabel(preset)}
403
+ {preset.isDefault ? t('exec.mode.defaultSuffix') : ''}
404
+ {preset.broken !== undefined ? t('exec.mode.brokenSuffix') : ''}
405
+ </option>
406
+ ))}
407
+ </optgroup>
408
+ )}
409
+ {customPresets.length > 0 && (
410
+ <optgroup label={t('exec.mode.customGroup')}>
411
+ {customPresets.map(preset => (
412
+ <option key={preset.id} value={preset.id} disabled={preset.broken !== undefined}>
413
+ {presetLabel(preset)}
414
+ {preset.isDefault ? t('exec.mode.defaultSuffix') : ''}
415
+ {preset.broken !== undefined ? t('exec.mode.brokenSuffix') : ''}
416
+ </option>
417
+ ))}
418
+ </optgroup>
419
+ )}
345
420
  </select>
346
421
  </label>
347
422
 
@@ -375,7 +450,9 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
375
450
  ))}
376
451
  </select>
377
452
  </label>
453
+ </CollapsibleSection>
378
454
 
455
+ <CollapsibleSection title={t('new.section.run')} summary={runSummary}>
379
456
  <label className={css.scheduleToggle}>
380
457
  <input
381
458
  type="checkbox"
@@ -386,65 +463,125 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
386
463
  </label>
387
464
  <p className={css.detailText}>{t('exec.reuseSessionHint')}</p>
388
465
 
389
- <section className={css.detailSection}>
390
- <h4>{t('detail.schedule')}</h4>
391
- <label className={css.scheduleToggle}>
392
- <input
393
- type="checkbox"
394
- checked={scheduleEnabled}
395
- onChange={event => {
396
- setScheduleEnabled(event.target.checked)
397
- if (!event.target.checked) setScheduleError(undefined)
398
- }}
399
- />
400
- <span>{t('detail.schedule.enable')}</span>
401
- </label>
402
- {scheduleEnabled && (
403
- <>
404
- <div className={css.scheduleRow}>
405
- <input
406
- className={`${css.input} ${css.scheduleInput}${scheduleError !== undefined ? ` ${css.scheduleInputInvalid}` : ''}`}
407
- value={scheduleCron}
408
- placeholder="0 9 * * *"
409
- spellCheck={false}
410
- aria-label={t('detail.schedule.cron')}
411
- onChange={event => { setScheduleCron(event.target.value); setScheduleError(undefined) }}
412
- />
413
- <select
414
- className={css.schedulePreset}
415
- value=""
416
- aria-label={t('detail.schedule.presets')}
417
- onChange={event => {
418
- if (event.target.value === '') return
419
- setScheduleCron(event.target.value)
420
- setScheduleError(undefined)
421
- }}
422
- >
423
- <option value="">{t('detail.schedule.presets')}…</option>
424
- {SCHEDULE_PRESETS.map(preset => (
425
- <option key={preset.cron} value={preset.cron}>{t(preset.label)}</option>
426
- ))}
427
- </select>
428
- </div>
429
- {scheduleError !== undefined && <p className={css.formError}>{scheduleError}</p>}
430
- {scheduleError === undefined && scheduleNextRun !== undefined && (
431
- <p className={css.scheduleMeta}>
432
- {t('detail.schedule.nextRun')} {new Date(scheduleNextRun).toLocaleString()}
433
- </p>
434
- )}
435
- </>
436
- )}
437
- </section>
438
- {isDuplicate && (
439
- <label className={css.checkboxLabel} style={{ marginTop: '12px' }}>
440
- <input
441
- type="checkbox"
442
- checked={archiveOriginal}
443
- onChange={event => { setArchiveOriginal(event.target.checked) }}
444
- />
445
- <span>{t('new.archiveOriginal')}</span>
446
- </label>
466
+ <label className={css.scheduleToggle}>
467
+ <input
468
+ type="checkbox"
469
+ checked={goalRun}
470
+ onChange={event => { setGoalRun(event.target.checked) }}
471
+ />
472
+ <span>{t('exec.goalRun')}</span>
473
+ </label>
474
+ <p className={css.detailText}>{t('exec.goalRunHint')}</p>
475
+
476
+ <label className={css.scheduleToggle}>
477
+ <input
478
+ type="checkbox"
479
+ checked={skipVerification}
480
+ onChange={event => { setSkipVerification(event.target.checked) }}
481
+ />
482
+ <span>{t('exec.skipVerification')}</span>
483
+ </label>
484
+ <p className={css.detailText}>{t('exec.skipVerificationHint')}</p>
485
+ </CollapsibleSection>
486
+
487
+ <CollapsibleSection
488
+ title={t('new.section.handover')}
489
+ summary={handoverSummary}
490
+ forceOpen={freezeError !== undefined}
491
+ >
492
+ <label className={css.field}>
493
+ <span className={css.fieldLabel}>{t('new.freeze')}</span>
494
+ <textarea
495
+ className={css.input}
496
+ rows={4}
497
+ value={freezeText}
498
+ placeholder={t('new.freezePlaceholder')}
499
+ spellCheck={false}
500
+ onChange={event => { setFreezeText(event.target.value); setFreezeError(undefined) }}
501
+ />
502
+ </label>
503
+ {freezeError !== undefined && <p className={css.formError}>{freezeError}</p>}
504
+
505
+ <label className={css.field}>
506
+ <span className={css.fieldLabel}>{t('new.handover')}</span>
507
+ <textarea
508
+ className={css.input}
509
+ rows={3}
510
+ value={handoverText}
511
+ placeholder={t('new.handoverPlaceholder')}
512
+ spellCheck={false}
513
+ onChange={event => { setHandoverText(event.target.value) }}
514
+ />
515
+ </label>
516
+ </CollapsibleSection>
517
+
518
+ <CollapsibleSection
519
+ title={t('new.section.schedule')}
520
+ summary={scheduleSummary}
521
+ forceOpen={scheduleError !== undefined}
522
+ >
523
+ <label className={css.scheduleToggle}>
524
+ <input
525
+ type="checkbox"
526
+ checked={scheduleEnabled}
527
+ onChange={event => {
528
+ setScheduleEnabled(event.target.checked)
529
+ if (!event.target.checked) setScheduleError(undefined)
530
+ }}
531
+ />
532
+ <span>{t('detail.schedule.enable')}</span>
533
+ </label>
534
+ {scheduleEnabled && (
535
+ <>
536
+ <div className={css.scheduleRow}>
537
+ <input
538
+ className={`${css.input} ${css.scheduleInput}${scheduleError !== undefined ? ` ${css.scheduleInputInvalid}` : ''}`}
539
+ value={scheduleCron}
540
+ placeholder="0 9 * * *"
541
+ spellCheck={false}
542
+ aria-label={t('detail.schedule.cron')}
543
+ onChange={event => { setScheduleCron(event.target.value); setScheduleError(undefined) }}
544
+ />
545
+ <select
546
+ className={css.schedulePreset}
547
+ value=""
548
+ aria-label={t('detail.schedule.presets')}
549
+ onChange={event => {
550
+ if (event.target.value === '') return
551
+ setScheduleCron(event.target.value)
552
+ setScheduleError(undefined)
553
+ }}
554
+ >
555
+ <option value="">{t('detail.schedule.presets')}…</option>
556
+ {SCHEDULE_PRESETS.map(preset => (
557
+ <option key={preset.cron} value={preset.cron}>{t(preset.label)}</option>
558
+ ))}
559
+ </select>
560
+ </div>
561
+ {scheduleError !== undefined && <p className={css.formError}>{scheduleError}</p>}
562
+ <label className={css.scheduleZone}>
563
+ <span>{t('detail.schedule.timeZone')}</span>
564
+ <select
565
+ className={css.schedulePreset}
566
+ value={scheduleZone}
567
+ aria-label={t('detail.schedule.timeZone')}
568
+ title={t('detail.schedule.timeZoneHint')}
569
+ onChange={event => { setScheduleZone(event.target.value); setScheduleError(undefined) }}
570
+ >
571
+ {zoneChoices(hostTimeZone, initialTask?.schedule?.timeZone).map(choice => (
572
+ <option key={choice.id === '' ? '__host' : choice.id} value={choice.id}>{choice.label}</option>
573
+ ))}
574
+ </select>
575
+ </label>
576
+ {scheduleError === undefined && scheduleNextRun !== undefined && (
577
+ <p className={css.scheduleMeta}>
578
+ {t('detail.schedule.nextRun')}{' '}
579
+ {nextRunLabel(scheduleNextRun, scheduleTimeZone, formatHostTimestamp, Date.now())}
580
+ </p>
581
+ )}
582
+ </>
447
583
  )}
584
+ </CollapsibleSection>
448
585
  </ModalShell>
449
586
  )
450
587
  }