@linxin666/dsh-client-ui-task-board 0.4.6 → 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.
Files changed (58) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +14 -6
  3. package/README.zh.md +14 -6
  4. package/lib/client.js +1502 -820
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +674 -75
  7. package/lib/types/client/CardDefaultPresetField.d.ts +35 -0
  8. package/lib/types/client/TaskBoardSettingsCard.d.ts +49 -0
  9. package/lib/types/client/board/ReasoningEffortField.d.ts +25 -0
  10. package/lib/types/client/board/model-options.d.ts +67 -0
  11. package/lib/types/client/board/preset-label.d.ts +21 -0
  12. package/lib/types/client/board/preset-roster.d.ts +45 -0
  13. package/lib/types/client/board/team-run-promotion.d.ts +26 -0
  14. package/lib/types/client/locales.d.ts +16 -0
  15. package/lib/types/core/card-defaults.d.ts +78 -0
  16. package/lib/types/core/controller.d.ts +32 -7
  17. package/lib/types/core/launch-width.d.ts +31 -0
  18. package/lib/types/core/reasoning-effort.d.ts +57 -0
  19. package/lib/types/core/subtask.d.ts +30 -8
  20. package/lib/types/core/tasks.d.ts +19 -0
  21. package/lib/types/core/use-cases/task-create.d.ts +10 -5
  22. package/lib/types/core/use-cases/task-update.d.ts +1 -1
  23. package/lib/types/host/launch-admission.d.ts +72 -0
  24. package/lib/types/host-ledger.d.ts +9 -4
  25. package/lib/types/host-runner.d.ts +79 -4
  26. package/lib/types/host-service.d.ts +85 -2
  27. package/lib/types/index.d.ts +87 -0
  28. package/lib/types/protocol.d.ts +13 -0
  29. package/package.json +28 -28
  30. package/src/client/CardDefaultPresetField.tsx +101 -0
  31. package/src/client/TaskBoardSettingsCard.tsx +214 -16
  32. package/src/client/board/LinkSubtaskModal.tsx +8 -0
  33. package/src/client/board/NewTaskModal.tsx +86 -40
  34. package/src/client/board/ReasoningEffortField.tsx +50 -0
  35. package/src/client/board/TaskDetail.tsx +42 -6
  36. package/src/client/board/model-options.ts +178 -0
  37. package/src/client/board/preset-label.ts +32 -0
  38. package/src/client/board/preset-roster.ts +58 -0
  39. package/src/client/board/team-run-promotion.ts +39 -0
  40. package/src/client/index.ts +98 -94
  41. package/src/client/locales.ts +34 -2
  42. package/src/client/settings-entry-form.ts +0 -1
  43. package/src/core/card-defaults.ts +132 -0
  44. package/src/core/controller.ts +44 -9
  45. package/src/core/launch-width.ts +38 -0
  46. package/src/core/reasoning-effort.ts +92 -0
  47. package/src/core/store.ts +5 -0
  48. package/src/core/subtask.ts +48 -13
  49. package/src/core/tasks.ts +20 -0
  50. package/src/core/use-cases/task-create.ts +27 -16
  51. package/src/core/use-cases/task-update.ts +6 -1
  52. package/src/host/agent-tools.ts +21 -8
  53. package/src/host/launch-admission.ts +140 -0
  54. package/src/host-ledger.ts +18 -6
  55. package/src/host-runner.ts +184 -18
  56. package/src/host-service.ts +247 -23
  57. package/src/index.ts +149 -2
  58. package/src/protocol.ts +28 -4
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-client-ui-task-board",
3
3
  "description": "Host-authoritative task board for the DSH Web GUI with real session execution, Host cron scheduling, and optional cross-platform idle-sleep protection; mounted without DSH source changes.",
4
- "version": "0.4.6",
4
+ "version": "0.4.7",
5
5
  "icon": "icon.svg",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "dsh": {
22
22
  "engines": {
23
- "dsh": ">=0.2.0-rc.2"
23
+ "dsh": ">=0.2.1-rc.2"
24
24
  },
25
25
  "bundle": {
26
26
  "patch": "./cordis.patch.yml"
@@ -40,37 +40,37 @@
40
40
  }
41
41
  },
42
42
  "peerDependencies": {
43
- "@deepseek-ai/dsh": ">=0.2.0-rc.2",
43
+ "@deepseek-ai/dsh": ">=0.2.1-rc.2",
44
44
  "react": "^18.2.0"
45
45
  },
46
46
  "dependencies": {
47
- "@deepseek-ai/schemastery": "^3.18.4",
47
+ "@deepseek-ai/schemastery": "^3.18.5",
48
48
  "marked": "^18.0.14"
49
49
  },
50
50
  "devDependencies": {
51
- "@deepseek-ai/cordis": "^4.0.4",
52
- "@deepseek-ai/dsh-agent": "^0.2.0-rc.2",
53
- "@deepseek-ai/dsh-agent-preset-registry": "^0.2.0-rc.2",
54
- "@deepseek-ai/dsh-api-gateway": "^0.2.0-rc.2",
55
- "@deepseek-ai/dsh-api-remotes": "^0.2.0-rc.2",
56
- "@deepseek-ai/dsh-api-session-controller": "^0.2.0-rc.2",
57
- "@deepseek-ai/dsh-api-workspace-controller": "^0.2.0-rc.2",
58
- "@deepseek-ai/dsh-client-connection": "^0.2.0-rc.2",
59
- "@deepseek-ai/dsh-client-locale": "^0.2.0-rc.2",
60
- "@deepseek-ai/dsh-client-store": "^0.2.0-rc.2",
61
- "@deepseek-ai/dsh-client-ui-layout": "^0.2.0-rc.2",
62
- "@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.2",
63
- "@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.2",
64
- "@deepseek-ai/dsh-client-ui-sidebar": "^0.2.0-rc.2",
65
- "@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.2",
66
- "@deepseek-ai/dsh-client-ui-workspace": "^0.2.0-rc.2",
67
- "@deepseek-ai/dsh-commands": "^0.2.0-rc.2",
68
- "@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.2",
69
- "@deepseek-ai/dsh-llm": "^0.2.0-rc.2",
70
- "@deepseek-ai/dsh-settings": "^0.2.0-rc.2",
71
- "@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.2",
72
- "@deepseek-ai/dsh-tools": "^0.2.0-rc.2",
73
- "@deepseek-ai/dsh-workspace": "^0.2.0-rc.2",
51
+ "@deepseek-ai/cordis": "^4.0.5",
52
+ "@deepseek-ai/dsh-agent": "^0.2.1-rc.2",
53
+ "@deepseek-ai/dsh-agent-preset-registry": "^0.2.1-rc.2",
54
+ "@deepseek-ai/dsh-api-gateway": "^0.2.1-rc.2",
55
+ "@deepseek-ai/dsh-api-remotes": "^0.2.1-rc.2",
56
+ "@deepseek-ai/dsh-api-session-controller": "^0.2.1-rc.2",
57
+ "@deepseek-ai/dsh-api-workspace-controller": "^0.2.1-rc.2",
58
+ "@deepseek-ai/dsh-client-connection": "^0.2.1-rc.2",
59
+ "@deepseek-ai/dsh-client-locale": "^0.2.1-rc.2",
60
+ "@deepseek-ai/dsh-client-store": "^0.2.1-rc.2",
61
+ "@deepseek-ai/dsh-client-ui-layout": "^0.2.1-rc.2",
62
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.2.1-rc.2",
63
+ "@deepseek-ai/dsh-client-ui-settings": "^0.2.1-rc.2",
64
+ "@deepseek-ai/dsh-client-ui-sidebar": "^0.2.1-rc.2",
65
+ "@deepseek-ai/dsh-client-ui-slots": "^0.2.1-rc.2",
66
+ "@deepseek-ai/dsh-client-ui-workspace": "^0.2.1-rc.2",
67
+ "@deepseek-ai/dsh-commands": "^0.2.1-rc.2",
68
+ "@deepseek-ai/dsh-host-webserver": "^0.2.1-rc.2",
69
+ "@deepseek-ai/dsh-llm": "^0.2.1-rc.2",
70
+ "@deepseek-ai/dsh-settings": "^0.2.1-rc.2",
71
+ "@deepseek-ai/dsh-system-prompt": "^0.2.1-rc.2",
72
+ "@deepseek-ai/dsh-tools": "^0.2.1-rc.2",
73
+ "@deepseek-ai/dsh-workspace": "^0.2.1-rc.2",
74
74
  "@types/node": "^22.20.0",
75
75
  "@types/react": "~18.3.1",
76
76
  "@types/react-dom": "^18.3.5",
@@ -100,7 +100,7 @@
100
100
  "build": "tsc -b tsconfig.build.json && tsdown",
101
101
  "bundle": "tsdown",
102
102
  "watch": "tsdown --watch",
103
- "typecheck": "tsc --noEmit",
103
+ "typecheck": "tsgo --noEmit",
104
104
  "test": "vitest run",
105
105
  "test:watch": "vitest"
106
106
  }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The default-agent-preset picker of the settings card's new-card-defaults
3
+ * topic.
4
+ *
5
+ * It renders the rows the create dialog's own mode picker renders — built-in
6
+ * presets first, then user presets, each labelled with its default and
7
+ * unavailable suffixes (board/preset-label.ts owns both) — and it renders a
8
+ * preset that cannot compose a session as a row that is visible but NOT
9
+ * selectable, so the board never stores a default no session could be composed
10
+ * from.
11
+ *
12
+ * The shared ChoiceField cannot express that: its choices carry no disabled
13
+ * state, and a stored pin the roster no longer serves would render as an empty
14
+ * trigger rather than as the unavailable preset it is. Both need a native
15
+ * `<select>` (the fallback the shared SelectField itself takes under an
16
+ * appearance skin), styled with the settings card's own field classes so the
17
+ * control stays a sibling of the fields beside it.
18
+ */
19
+ import type { ExecutionPresetOption } from '../core/controller.ts'
20
+ import { inheritPresetLabel, presetChoiceRows } from './board/preset-label.ts'
21
+ import { t } from './locales.ts'
22
+ import type { FieldProps } from './PluginSettingsCard.tsx'
23
+ import css from './settings-card.module.css'
24
+
25
+ /** Props of the default-preset field: the shared field contract plus the roster. */
26
+ export interface CardDefaultPresetFieldProps extends FieldProps {
27
+ /**
28
+ * The deployment's agent-preset roster, or undefined while its runtime read
29
+ * has not answered: an unread roster offers the inherit choice alone instead
30
+ * of calling every stored pin "removed".
31
+ */
32
+ presets: readonly ExecutionPresetOption[] | undefined
33
+ }
34
+
35
+ /**
36
+ * Render the default-preset select.
37
+ * @param props - the field contract and the roster to list.
38
+ * @returns the field.
39
+ */
40
+ export function CardDefaultPresetField(props: CardDefaultPresetFieldProps) {
41
+ const presets = props.presets
42
+ const rows = presetChoiceRows(presets ?? [])
43
+ // A stored pin the roster no longer serves stays visible and pickable: it is
44
+ // what the board currently holds, so the control must show it rather than
45
+ // silently reading as the inherit choice.
46
+ const unlisted = props.text !== '' && presets !== undefined && !presets.some(preset => preset.id === props.text)
47
+ return (
48
+ <div className={css.field}>
49
+ <div className={css.head}>
50
+ <label className={css.label} htmlFor={props.id}>{props.label}</label>
51
+ {props.overridden
52
+ ? (
53
+ <span className={css.badges}>
54
+ <span className={css.badge}>{props.overriddenLabel}</span>
55
+ <button
56
+ type="button"
57
+ className={css.reset}
58
+ disabled={props.disabled}
59
+ onClick={props.onReset}
60
+ >
61
+ {props.resetLabel}
62
+ </button>
63
+ </span>
64
+ )
65
+ : null}
66
+ </div>
67
+ <select
68
+ id={props.id}
69
+ className={css.select}
70
+ value={props.text}
71
+ disabled={props.disabled}
72
+ aria-invalid={props.invalid || undefined}
73
+ onChange={(event) => { props.onEdit(event.target.value) }}
74
+ >
75
+ <option value="">{inheritPresetLabel(presets ?? [])}</option>
76
+ {unlisted ? <option value={props.text}>{props.text}{t('exec.mode.removed')}</option> : null}
77
+ {rows.builtin.length > 0
78
+ ? (
79
+ <optgroup label={t('exec.mode.builtinGroup')}>
80
+ {rows.builtin.map(row => (
81
+ <option key={row.id} value={row.id} disabled={row.disabled}>{row.label}</option>
82
+ ))}
83
+ </optgroup>
84
+ )
85
+ : null}
86
+ {rows.custom.length > 0
87
+ ? (
88
+ <optgroup label={t('exec.mode.customGroup')}>
89
+ {rows.custom.map(row => (
90
+ <option key={row.id} value={row.id} disabled={row.disabled}>{row.label}</option>
91
+ ))}
92
+ </optgroup>
93
+ )
94
+ : null}
95
+ </select>
96
+ <p className={props.invalid ? css.invalid : css.hint}>
97
+ {props.invalid ? props.invalidLabel : props.hint}
98
+ </p>
99
+ </div>
100
+ )
101
+ }
@@ -10,12 +10,34 @@ import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client'
10
10
  import { useEffect, useState } from 'react'
11
11
  import type { TaskBoardPowerSnapshot, TaskBoardVerificationOptions } from '../protocol.ts'
12
12
  import type { TaskBoardExtensionDispatch } from '../core/extension.ts'
13
+ import type { ExecutionPresetOption } from '../core/controller.ts'
14
+ import { TASK_PERMISSIONS } from '../core/tasks.ts'
13
15
  import { resolveContract, type ModelCatalogView, type VerificationSettings } from '../core/verification.ts'
14
16
  import { parseModelRoute } from '../core/verification.ts'
17
+ import { CardDefaultPresetField } from './CardDefaultPresetField.tsx'
15
18
  import { PluginSettingsCard, BooleanField, ChoiceField, ValueField } from './PluginSettingsCard.tsx'
19
+ import {
20
+ modelChoiceRows,
21
+ modelOptionsFromCatalog,
22
+ reasoningEffortsFor,
23
+ reasoningHintKey,
24
+ reasoningPickerEnabled,
25
+ reconcileReasoningEffort,
26
+ } from './board/model-options.ts'
16
27
  import { SUBTASK_DEPTH_MAX, SUBTASK_DEPTH_MIN } from '../core/subtask.ts'
17
28
  import { SESSION_POLL_MAX_SECONDS, SESSION_POLL_MIN_SECONDS } from '../core/poll-cadence.ts'
18
- import { CardForm, booleanField, type CardActions, type CardShell, type FieldSpec, type FieldState as CardFieldState } from './settings-form.ts'
29
+ import { RUN_LAUNCH_WIDTH_MAX, RUN_LAUNCH_WIDTH_MIN } from '../core/launch-width.ts'
30
+ import {
31
+ CardForm,
32
+ booleanField,
33
+ choiceField,
34
+ textField,
35
+ type CardActions,
36
+ type CardShell,
37
+ type FieldSpec,
38
+ type FieldState as CardFieldState,
39
+ } from './settings-form.ts'
40
+ import type { TaskBoardKey } from './locales.ts'
19
41
  import settingsCss from './board-settings.module.css'
20
42
 
21
43
  /** The depth choices the card offers, derived from the supported range. */
@@ -65,34 +87,42 @@ function subtaskDepthField(): FieldSpec {
65
87
  }
66
88
 
67
89
  /**
68
- * The judge-model field: blank inherits the host model catalog default, and a
69
- * non-blank draft must be a qualified provider/model route. An unparseable
70
- * draft blocks the save instead of storing a route nothing can resolve.
90
+ * The run-launch width field: a whole number of concurrent launches inside the
91
+ * supported range, because the Host schema (`runLaunchWidth`) is numeric. A
92
+ * draft outside the range blocks the save instead of staging a value the Host
93
+ * would clamp behind the user's back.
71
94
  */
72
- function judgeModelField(): FieldSpec {
95
+ function runLaunchWidthField(): FieldSpec {
73
96
  return {
74
- field: 'goalVerificationModel',
75
- format: value => typeof value === 'string' ? value : '',
97
+ field: 'runLaunchWidth',
98
+ format: value => typeof value === 'number' && Number.isInteger(value) ? String(value) : '',
76
99
  parse: (text) => {
77
100
  const trimmed = text.trim()
78
101
  if (trimmed === '') return { kind: 'clear' }
79
- return parseModelRoute(trimmed) === undefined ? undefined : { kind: 'set', value: trimmed }
102
+ if (!/^\d+$/.test(trimmed)) return undefined
103
+ const width = Number(trimmed)
104
+ return width >= RUN_LAUNCH_WIDTH_MIN && width <= RUN_LAUNCH_WIDTH_MAX
105
+ ? { kind: 'set', value: width }
106
+ : undefined
80
107
  },
81
108
  }
82
109
  }
83
110
 
84
111
  /**
85
- * The reasoning-effort field. Any non-blank id is staged: which levels a model
86
- * accepts is decided by the host catalog, and the settings card shows the
87
- * resolved level (including a fallback) rather than guessing here.
112
+ * The judge-model field: blank inherits the host model catalog default, and a
113
+ * non-blank draft must be a qualified provider/model route. An unparseable
114
+ * draft blocks the save instead of storing a route nothing can resolve.
115
+ * The new-card default model uses the same gate, so both fields accept exactly
116
+ * the routes an execution can pin.
88
117
  */
89
- function judgeEffortField(): FieldSpec {
118
+ function modelRouteField(field: string): FieldSpec {
90
119
  return {
91
- field: 'goalVerificationReasoningEffort',
120
+ field,
92
121
  format: value => typeof value === 'string' ? value : '',
93
122
  parse: (text) => {
94
123
  const trimmed = text.trim()
95
- return trimmed === '' ? { kind: 'clear' } : { kind: 'set', value: trimmed }
124
+ if (trimmed === '') return { kind: 'clear' }
125
+ return parseModelRoute(trimmed) === undefined ? undefined : { kind: 'set', value: trimmed }
96
126
  },
97
127
  }
98
128
  }
@@ -109,6 +139,12 @@ export interface TaskBoardSettings {
109
139
  maxSubtaskDepth?: number
110
140
  /** Roster-poll cadence in seconds while the board has work to reconcile. */
111
141
  sessionPollSeconds?: number
142
+ /**
143
+ * How many run launches may be in flight at once (1..16, default 3). One run
144
+ * opens its whole subtree, so this bounds the burst of sessions a single run
145
+ * action creates; members enter the running column in batches, none is dropped.
146
+ */
147
+ runLaunchWidth?: number
112
148
  /**
113
149
  * GLOBAL native /goal switch (default off): the master control over whether
114
150
  * this board starts a run with dsh's built-in /goal at all. Frozen per
@@ -121,6 +157,20 @@ export interface TaskBoardSettings {
121
157
  goalVerificationModel?: string
122
158
  /** Judge reasoning effort; blank inherits the host default level. */
123
159
  goalVerificationReasoningEffort?: string
160
+ /**
161
+ * Agent preset the create dialog of this board opens with. Blank pins
162
+ * nothing, so a new card follows the deployment default as before.
163
+ */
164
+ cardDefaultMode?: string
165
+ /** Permission the create dialog opens with; blank applies no default. */
166
+ cardDefaultPermission?: string
167
+ /** Execution model route the create dialog opens with; blank applies none. */
168
+ cardDefaultModel?: string
169
+ /**
170
+ * Reasoning level bound to `cardDefaultModel`; it applies only while a
171
+ * card's final model IS that configured route, and is inert without one.
172
+ */
173
+ cardDefaultReasoningEffort?: string
124
174
  }
125
175
 
126
176
  /** What the task-board card renders. */
@@ -135,6 +185,8 @@ export interface TaskBoardSettingsCardState extends CardShell {
135
185
  maxSubtaskDepth: CardFieldState
136
186
  /** Roster-poll cadence field. */
137
187
  sessionPollSeconds: CardFieldState
188
+ /** Run-launch admission width field. */
189
+ runLaunchWidth: CardFieldState
138
190
  /** GLOBAL native /goal switch. */
139
191
  goalRunEnabled: CardFieldState
140
192
  /** Goal-acceptance switch. */
@@ -143,6 +195,20 @@ export interface TaskBoardSettingsCardState extends CardShell {
143
195
  goalVerificationModel: CardFieldState
144
196
  /** Judge reasoning-effort field. */
145
197
  goalVerificationReasoningEffort: CardFieldState
198
+ /** New-card default agent preset. */
199
+ cardDefaultMode: CardFieldState
200
+ /** New-card default permission. */
201
+ cardDefaultPermission: CardFieldState
202
+ /** New-card default model route. */
203
+ cardDefaultModel: CardFieldState
204
+ /** New-card default reasoning level. */
205
+ cardDefaultReasoningEffort: CardFieldState
206
+ /**
207
+ * The deployment's agent-preset roster, published by the runtime read the
208
+ * plugin wires; undefined until it answers, which leaves the default-preset
209
+ * field offering the inherit choice alone.
210
+ */
211
+ presets?: readonly ExecutionPresetOption[]
146
212
  }
147
213
 
148
214
  /** The registration-side face the card's slot entry injects. */
@@ -159,6 +225,8 @@ export interface TaskBoardSettingsCardFace extends CardActions {
159
225
  export class TaskBoardSettingsCardController {
160
226
  private readonly form: CardForm<TaskBoardSettings>
161
227
  private readonly store: SnapshotStore<TaskBoardSettingsCardState>
228
+ /** The deployment's agent-preset roster, once its runtime read has answered. */
229
+ private presets: readonly ExecutionPresetOption[] | undefined
162
230
 
163
231
  /** @param scope - the bound configuration form for the `task-board` namespace. */
164
232
  constructor(scope: ConfigForm<TaskBoardSettings>, private readonly dispatch: TaskBoardExtensionDispatch) {
@@ -168,10 +236,18 @@ export class TaskBoardSettingsCardController {
168
236
  booleanField('preventIdleSleep'),
169
237
  subtaskDepthField(),
170
238
  sessionPollField(),
239
+ runLaunchWidthField(),
171
240
  booleanField('goalRunEnabled'),
172
241
  booleanField('goalVerification'),
173
- judgeModelField(),
174
- judgeEffortField(),
242
+ modelRouteField('goalVerificationModel'),
243
+ textField('goalVerificationReasoningEffort'),
244
+ // The new-card defaults: blank is a real value every one of them keeps
245
+ // ("apply no default"), which is why they are staged like any other field
246
+ // and only their parse rules differ.
247
+ textField('cardDefaultMode'),
248
+ choiceField('cardDefaultPermission', TASK_PERMISSIONS),
249
+ modelRouteField('cardDefaultModel'),
250
+ textField('cardDefaultReasoningEffort'),
175
251
  ])
176
252
  this.store = this.form.bind(() => this.projection())
177
253
  }
@@ -184,13 +260,33 @@ export class TaskBoardSettingsCardController {
184
260
  preventIdleSleep: this.form.field('preventIdleSleep'),
185
261
  maxSubtaskDepth: this.form.field('maxSubtaskDepth'),
186
262
  sessionPollSeconds: this.form.field('sessionPollSeconds'),
263
+ runLaunchWidth: this.form.field('runLaunchWidth'),
187
264
  goalRunEnabled: this.form.field('goalRunEnabled'),
188
265
  goalVerification: this.form.field('goalVerification'),
189
266
  goalVerificationModel: this.form.field('goalVerificationModel'),
190
267
  goalVerificationReasoningEffort: this.form.field('goalVerificationReasoningEffort'),
268
+ cardDefaultMode: this.form.field('cardDefaultMode'),
269
+ cardDefaultPermission: this.form.field('cardDefaultPermission'),
270
+ cardDefaultModel: this.form.field('cardDefaultModel'),
271
+ cardDefaultReasoningEffort: this.form.field('cardDefaultReasoningEffort'),
272
+ ...(this.presets === undefined ? {} : { presets: this.presets }),
191
273
  }
192
274
  }
193
275
 
276
+ /**
277
+ * Publish the deployment's agent-preset roster to the default-preset field.
278
+ * The roster is a runtime fact read outside this card (src/client/index.ts
279
+ * wires it once for both the board's pickers and this topic); it can arrive
280
+ * late or not at all, and until it does the field lists the inherit choice
281
+ * alone. This never blocks a save: a deployment that serves no roster still
282
+ * has a writable settings document.
283
+ * @param presets - the roster rows.
284
+ */
285
+ setPresetRoster(presets: readonly ExecutionPresetOption[]): void {
286
+ this.presets = presets
287
+ this.store.set(this.projection())
288
+ }
289
+
194
290
  /**
195
291
  * Build the face the card's slot registration injects.
196
292
  * @returns the card's snapshot and its form actions.
@@ -304,6 +400,25 @@ export function TaskBoardSettingsCard(props: TaskBoardSettingsCardProps) {
304
400
  invalidLabel: t('settings.invalidNumber'),
305
401
  disabled,
306
402
  }
403
+ // The new-card-defaults topic reads the SAME catalog the acceptance fields
404
+ // and the create dialog read (session/modelCatalog, served here through the
405
+ // verification payload), normalized once into the roster the reasoning
406
+ // helpers below consume, so the default level can only offer levels the
407
+ // chosen default model really declares.
408
+ const defaultModels = modelOptionsFromCatalog(catalog)
409
+ const defaultModel = state.cardDefaultModel.text
410
+ const defaultEffortChoices = reasoningEffortsFor(defaultModels, defaultModel)
411
+ .map(effort => ({ value: effort, label: effort }))
412
+ /**
413
+ * Drop a staged default level the newly picked default model cannot honour,
414
+ * exactly as the create dialog's own model picker does (one shared rule):
415
+ * the level belongs to the route it was chosen for.
416
+ */
417
+ const dropUnhonouredEffort = (nextModel: string): void => {
418
+ const current = state.cardDefaultReasoningEffort.text
419
+ const kept = reconcileReasoningEffort(defaultModels, nextModel, current)
420
+ if (kept !== current) props.edit('cardDefaultReasoningEffort', kept)
421
+ }
307
422
  // Chrome state of the nested disclosure cards below: they stage into this
308
423
  // card's form and are written by its one save, so they carry no footer and no
309
424
  // unsaved pill of their own (the outer header owns the pill).
@@ -405,6 +520,16 @@ export function TaskBoardSettingsCard(props: TaskBoardSettingsCardProps) {
405
520
  onEdit={(text) => { props.edit('maxSubtaskDepth', text) }}
406
521
  onReset={() => { props.resetField('maxSubtaskDepth') }}
407
522
  />
523
+ <ValueField
524
+ id="settings-task-board-run-launch-width"
525
+ label={t('settings.runLaunchWidth')}
526
+ hint={t('settings.runLaunchWidthHint')}
527
+ numeric
528
+ {...fieldProps}
529
+ {...state.runLaunchWidth}
530
+ onEdit={(text) => { props.edit('runLaunchWidth', text) }}
531
+ onReset={() => { props.resetField('runLaunchWidth') }}
532
+ />
408
533
  </PluginSettingsCard>
409
534
 
410
535
  <PluginSettingsCard
@@ -511,6 +636,79 @@ export function TaskBoardSettingsCard(props: TaskBoardSettingsCardProps) {
511
636
  )}
512
637
  </PluginSettingsCard>
513
638
 
639
+ {/* New-card defaults: what the create dialog opens with, and what the
640
+ Host create path fills an omitted target from when the caller opts
641
+ in. Every field's blank choice is a real value — "apply no default"
642
+ — so a deployment that configures none of them creates cards
643
+ exactly as it did before this topic existed. */}
644
+ <PluginSettingsCard
645
+ t={t}
646
+ titleKey="settings.cardDefaultsTitle"
647
+ descriptionKey="settings.cardDefaultsCardHint"
648
+ defaultOpen={false}
649
+ hideFooter
650
+ state={nestedShell}
651
+ onSave={props.save}
652
+ onDiscard={props.discard}
653
+ >
654
+ <CardDefaultPresetField
655
+ id="settings-task-board-card-default-mode"
656
+ label={t('new.agentPreset')}
657
+ hint={t('settings.cardDefaultModeHint')}
658
+ presets={state.presets}
659
+ {...fieldProps}
660
+ {...state.cardDefaultMode}
661
+ onEdit={(text) => { props.edit('cardDefaultMode', text) }}
662
+ onReset={() => { props.resetField('cardDefaultMode') }}
663
+ />
664
+ <ChoiceField
665
+ id="settings-task-board-card-default-permission"
666
+ label={t('new.permission')}
667
+ hint={t('settings.cardDefaultPermissionHint')}
668
+ inheritLabel={t('exec.permission.default')}
669
+ choices={TASK_PERMISSIONS.map(id => ({ value: id, label: t(`exec.permission.${id}` as TaskBoardKey) }))}
670
+ {...fieldProps}
671
+ {...state.cardDefaultPermission}
672
+ onEdit={(text) => { props.edit('cardDefaultPermission', text) }}
673
+ onReset={() => { props.resetField('cardDefaultPermission') }}
674
+ />
675
+ <ChoiceField
676
+ id="settings-task-board-card-default-model"
677
+ label={t('new.model')}
678
+ hint={t('settings.cardDefaultModelHint')}
679
+ inheritLabel={t('exec.model.default')}
680
+ choices={modelChoiceRows(defaultModels)}
681
+ {...fieldProps}
682
+ {...state.cardDefaultModel}
683
+ onEdit={(text) => {
684
+ props.edit('cardDefaultModel', text)
685
+ dropUnhonouredEffort(text)
686
+ }}
687
+ onReset={() => {
688
+ props.resetField('cardDefaultModel')
689
+ // A blank default model carries no route for a level to bind to,
690
+ // so the staged default level goes with it.
691
+ dropUnhonouredEffort('')
692
+ }}
693
+ />
694
+ <ChoiceField
695
+ id="settings-task-board-card-default-effort"
696
+ label={t('exec.reasoning')}
697
+ hint={t(reasoningHintKey(defaultModels, defaultModel))}
698
+ inheritLabel={t('exec.reasoning.inherit')}
699
+ choices={defaultEffortChoices}
700
+ {...fieldProps}
701
+ {...state.cardDefaultReasoningEffort}
702
+ // The levels on offer are the declared ones of the picked default
703
+ // model, so without a model that declares any there is nothing to
704
+ // choose: the selector locks instead of offering a level no model
705
+ // would accept.
706
+ disabled={disabled || !reasoningPickerEnabled(defaultModels, defaultModel)}
707
+ onEdit={(text) => { props.edit('cardDefaultReasoningEffort', text) }}
708
+ onReset={() => { props.resetField('cardDefaultReasoningEffort') }}
709
+ />
710
+ </PluginSettingsCard>
711
+
514
712
  {/* Provider sections: a family extension contributes its own nested
515
713
  settings card into this list through the seat this card declares —
516
714
  the family provider extension is the one that does. Each
@@ -12,6 +12,7 @@ import { t } from '../locales.ts'
12
12
  import css from '../board.module.css'
13
13
  import { useDialog, type OverlayPhase } from './overlay.tsx'
14
14
  import { STATUS_KEY } from './status-key.ts'
15
+ import { parentAwaitsTeamRunPromotion, promoteParentForTeamRun } from './team-run-promotion.ts'
15
16
 
16
17
  /** Link-subtask overlay props. */
17
18
  export interface LinkSubtaskModalProps {
@@ -56,6 +57,10 @@ export function LinkSubtaskModal({ controller, parent, onClose, phase = 'open' }
56
57
  setPending(true)
57
58
  setError(undefined)
58
59
  if (await controller.setParent(taskId, parent.id)) {
60
+ // The link is confirmed, so a parent that just gained a subtask is opted
61
+ // in where Agent Teams are served; a refused opt-in never undoes a link
62
+ // the Host already committed.
63
+ await promoteParentForTeamRun(controller, parent)
59
64
  onClose()
60
65
  return
61
66
  }
@@ -67,6 +72,9 @@ export function LinkSubtaskModal({ controller, parent, onClose, phase = 'open' }
67
72
  <div ref={dialog.attach} className={css.modal} role="dialog" aria-modal="true" aria-label={t('link.subtask.title')} tabIndex={-1}>
68
73
  <h2 className={css.modalTitle}>{t('link.subtask.title')}</h2>
69
74
  <p className={css.fieldHint}>{t('link.subtask.hint')}</p>
75
+ {parentAwaitsTeamRunPromotion(controller, parent) && (
76
+ <p className={css.fieldHint}>{t('exec.teamRunAutoHint')}</p>
77
+ )}
70
78
  {candidates.length === 0
71
79
  ? <p className={css.detailText}>{t('link.subtask.empty')}</p>
72
80
  : (