@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.
Files changed (83) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +34 -18
  3. package/README.zh.md +34 -18
  4. package/lib/client.js +2804 -1024
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +2576 -416
  7. package/lib/types/client/CardDefaultPresetField.d.ts +35 -0
  8. package/lib/types/client/TaskBoardSettingsCard.d.ts +61 -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/index.d.ts +4 -5
  15. package/lib/types/client/locales.d.ts +81 -0
  16. package/lib/types/client/plugin-card-seat.d.ts +5 -0
  17. package/lib/types/client/schedule-zone.d.ts +50 -0
  18. package/lib/types/core/card-defaults.d.ts +78 -0
  19. package/lib/types/core/controller.d.ts +41 -17
  20. package/lib/types/core/launch-width.d.ts +31 -0
  21. package/lib/types/core/poll-cadence.d.ts +31 -0
  22. package/lib/types/core/reasoning-effort.d.ts +57 -0
  23. package/lib/types/core/subtask.d.ts +30 -8
  24. package/lib/types/core/tasks.d.ts +163 -14
  25. package/lib/types/core/use-cases/task-create.d.ts +10 -5
  26. package/lib/types/core/use-cases/task-schedule.d.ts +68 -10
  27. package/lib/types/core/use-cases/task-update.d.ts +1 -1
  28. package/lib/types/core/verification-budget.d.ts +52 -0
  29. package/lib/types/core/verification.d.ts +376 -9
  30. package/lib/types/host/launch-admission.d.ts +72 -0
  31. package/lib/types/host/verification-gate.d.ts +24 -0
  32. package/lib/types/host/verification-runner.d.ts +38 -7
  33. package/lib/types/host-ledger.d.ts +91 -11
  34. package/lib/types/host-runner.d.ts +111 -13
  35. package/lib/types/host-service.d.ts +199 -8
  36. package/lib/types/index.d.ts +141 -0
  37. package/lib/types/protocol.d.ts +63 -12
  38. package/package.json +29 -33
  39. package/src/client/CardDefaultPresetField.tsx +101 -0
  40. package/src/client/TaskBoardSettingsCard.tsx +288 -17
  41. package/src/client/board/LinkSubtaskModal.tsx +8 -0
  42. package/src/client/board/NewTaskModal.tsx +237 -60
  43. package/src/client/board/ReasoningEffortField.tsx +50 -0
  44. package/src/client/board/TaskCard.tsx +3 -3
  45. package/src/client/board/TaskDetail.tsx +261 -24
  46. package/src/client/board/VerificationReport.tsx +132 -13
  47. package/src/client/board/model-options.ts +178 -0
  48. package/src/client/board/preset-label.ts +32 -0
  49. package/src/client/board/preset-roster.ts +58 -0
  50. package/src/client/board/status-key.ts +3 -0
  51. package/src/client/board/team-run-promotion.ts +39 -0
  52. package/src/client/index.ts +105 -108
  53. package/src/client/locales.ts +171 -6
  54. package/src/client/plugin-card-seat.ts +7 -0
  55. package/src/client/schedule-zone.ts +100 -2
  56. package/src/client/settings-entry-form.ts +0 -1
  57. package/src/core/card-defaults.ts +132 -0
  58. package/src/core/controller.ts +55 -16
  59. package/src/core/launch-width.ts +38 -0
  60. package/src/core/poll-cadence.ts +41 -0
  61. package/src/core/reasoning-effort.ts +92 -0
  62. package/src/core/store.ts +69 -15
  63. package/src/core/subtask.ts +48 -13
  64. package/src/core/tasks.ts +213 -39
  65. package/src/core/use-cases/task-create.ts +57 -25
  66. package/src/core/use-cases/task-schedule.ts +190 -25
  67. package/src/core/use-cases/task-update.ts +9 -4
  68. package/src/core/verification-budget.ts +115 -0
  69. package/src/core/verification.ts +785 -17
  70. package/src/host/agent-tools.ts +127 -29
  71. package/src/host/launch-admission.ts +140 -0
  72. package/src/host/verification-gate.ts +172 -16
  73. package/src/host/verification-runner.ts +207 -40
  74. package/src/host-ledger.ts +346 -48
  75. package/src/host-runner.ts +234 -35
  76. package/src/host-service.ts +539 -52
  77. package/src/index.ts +250 -7
  78. package/src/protocol.ts +160 -32
  79. package/lib/invariant.js +0 -6
  80. package/lib/types/client/board-card-seat.d.ts +0 -34
  81. package/lib/types/invariant.d.ts +0 -3
  82. package/src/client/board-card-seat.ts +0 -102
  83. package/src/invariant.ts +0 -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.5",
4
+ "version": "0.4.7",
5
5
  "icon": "icon.svg",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",
@@ -11,10 +11,6 @@
11
11
  "types": "./lib/types/index.d.ts",
12
12
  "default": "./lib/index.js"
13
13
  },
14
- "./invariant": {
15
- "types": "./lib/types/invariant.d.ts",
16
- "default": "./lib/invariant.js"
17
- },
18
14
  "./client": {
19
15
  "types": "./lib/types/client/index.d.ts",
20
16
  "default": "./lib/client.js"
@@ -24,7 +20,7 @@
24
20
  },
25
21
  "dsh": {
26
22
  "engines": {
27
- "dsh": ">=0.2.0-rc.2"
23
+ "dsh": ">=0.2.1-rc.2"
28
24
  },
29
25
  "bundle": {
30
26
  "patch": "./cordis.patch.yml"
@@ -44,37 +40,37 @@
44
40
  }
45
41
  },
46
42
  "peerDependencies": {
47
- "@deepseek-ai/dsh": ">=0.2.0-rc.2",
43
+ "@deepseek-ai/dsh": ">=0.2.1-rc.2",
48
44
  "react": "^18.2.0"
49
45
  },
50
46
  "dependencies": {
51
- "@deepseek-ai/schemastery": "^3.18.4",
47
+ "@deepseek-ai/schemastery": "^3.18.5",
52
48
  "marked": "^18.0.14"
53
49
  },
54
50
  "devDependencies": {
55
- "@deepseek-ai/cordis": "^4.0.4",
56
- "@deepseek-ai/dsh-agent": "^0.2.0-rc.2",
57
- "@deepseek-ai/dsh-agent-preset-registry": "^0.2.0-rc.2",
58
- "@deepseek-ai/dsh-api-gateway": "^0.2.0-rc.2",
59
- "@deepseek-ai/dsh-api-remotes": "^0.2.0-rc.2",
60
- "@deepseek-ai/dsh-api-session-controller": "^0.2.0-rc.2",
61
- "@deepseek-ai/dsh-api-workspace-controller": "^0.2.0-rc.2",
62
- "@deepseek-ai/dsh-client-connection": "^0.2.0-rc.2",
63
- "@deepseek-ai/dsh-client-locale": "^0.2.0-rc.2",
64
- "@deepseek-ai/dsh-client-store": "^0.2.0-rc.2",
65
- "@deepseek-ai/dsh-client-ui-layout": "^0.2.0-rc.2",
66
- "@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.2",
67
- "@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.2",
68
- "@deepseek-ai/dsh-client-ui-sidebar": "^0.2.0-rc.2",
69
- "@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.2",
70
- "@deepseek-ai/dsh-client-ui-workspace": "^0.2.0-rc.2",
71
- "@deepseek-ai/dsh-commands": "^0.2.0-rc.2",
72
- "@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.2",
73
- "@deepseek-ai/dsh-llm": "^0.2.0-rc.2",
74
- "@deepseek-ai/dsh-settings": "^0.2.0-rc.2",
75
- "@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.2",
76
- "@deepseek-ai/dsh-tools": "^0.2.0-rc.2",
77
- "@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",
78
74
  "@types/node": "^22.20.0",
79
75
  "@types/react": "~18.3.1",
80
76
  "@types/react-dom": "^18.3.5",
@@ -101,10 +97,10 @@
101
97
  "url": "https://github.com/zhu1090093659/dsh-web.git"
102
98
  },
103
99
  "scripts": {
104
- "build": "tsc -p tsconfig.build.json && tsdown",
100
+ "build": "tsc -b tsconfig.build.json && tsdown",
105
101
  "bundle": "tsdown",
106
102
  "watch": "tsdown --watch",
107
- "typecheck": "tsc --noEmit",
103
+ "typecheck": "tsgo --noEmit",
108
104
  "test": "vitest run",
109
105
  "test:watch": "vitest"
110
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,11 +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'
15
- import { PluginSettingsCard, BooleanField, ChoiceField } from './PluginSettingsCard.tsx'
17
+ import { CardDefaultPresetField } from './CardDefaultPresetField.tsx'
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
- import { CardForm, booleanField, type CardActions, type CardShell, type FieldSpec, type FieldState as CardFieldState } from './settings-form.ts'
28
+ import { SESSION_POLL_MAX_SECONDS, SESSION_POLL_MIN_SECONDS } from '../core/poll-cadence.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'
18
41
  import settingsCss from './board-settings.module.css'
19
42
 
20
43
  /** The depth choices the card offers, derived from the supported range. */
@@ -23,6 +46,28 @@ const SUBTASK_DEPTH_CHOICES: readonly string[] = Array.from(
23
46
  (_, index) => String(SUBTASK_DEPTH_MIN + index),
24
47
  )
25
48
 
49
+ /**
50
+ * The roster-poll cadence field: a whole number of seconds inside the
51
+ * supported range, because the Host schema (`sessionPollSeconds`) is numeric.
52
+ * A draft outside the range blocks the save instead of staging a value the
53
+ * Host would clamp behind the user's back.
54
+ */
55
+ function sessionPollField(): FieldSpec {
56
+ return {
57
+ field: 'sessionPollSeconds',
58
+ format: value => typeof value === 'number' && Number.isInteger(value) ? String(value) : '',
59
+ parse: (text) => {
60
+ const trimmed = text.trim()
61
+ if (trimmed === '') return { kind: 'clear' }
62
+ if (!/^\d+$/.test(trimmed)) return undefined
63
+ const seconds = Number(trimmed)
64
+ return seconds >= SESSION_POLL_MIN_SECONDS && seconds <= SESSION_POLL_MAX_SECONDS
65
+ ? { kind: 'set', value: seconds }
66
+ : undefined
67
+ },
68
+ }
69
+ }
70
+
26
71
  /**
27
72
  * The depth field: a choice among the supported levels whose draft text is a
28
73
  * number, because the Host schema (`maxSubtaskDepth`) is numeric. A draft
@@ -42,34 +87,42 @@ function subtaskDepthField(): FieldSpec {
42
87
  }
43
88
 
44
89
  /**
45
- * The judge-model field: blank inherits the host model catalog default, and a
46
- * non-blank draft must be a qualified provider/model route. An unparseable
47
- * 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.
48
94
  */
49
- function judgeModelField(): FieldSpec {
95
+ function runLaunchWidthField(): FieldSpec {
50
96
  return {
51
- field: 'goalVerificationModel',
52
- format: value => typeof value === 'string' ? value : '',
97
+ field: 'runLaunchWidth',
98
+ format: value => typeof value === 'number' && Number.isInteger(value) ? String(value) : '',
53
99
  parse: (text) => {
54
100
  const trimmed = text.trim()
55
101
  if (trimmed === '') return { kind: 'clear' }
56
- 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
57
107
  },
58
108
  }
59
109
  }
60
110
 
61
111
  /**
62
- * The reasoning-effort field. Any non-blank id is staged: which levels a model
63
- * accepts is decided by the host catalog, and the settings card shows the
64
- * 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.
65
117
  */
66
- function judgeEffortField(): FieldSpec {
118
+ function modelRouteField(field: string): FieldSpec {
67
119
  return {
68
- field: 'goalVerificationReasoningEffort',
120
+ field,
69
121
  format: value => typeof value === 'string' ? value : '',
70
122
  parse: (text) => {
71
123
  const trimmed = text.trim()
72
- return trimmed === '' ? { kind: 'clear' } : { kind: 'set', value: trimmed }
124
+ if (trimmed === '') return { kind: 'clear' }
125
+ return parseModelRoute(trimmed) === undefined ? undefined : { kind: 'set', value: trimmed }
73
126
  },
74
127
  }
75
128
  }
@@ -84,12 +137,40 @@ export interface TaskBoardSettings {
84
137
  preventIdleSleep?: boolean
85
138
  /** Subtask depth limit (1..3); 1 means a single level of subtasks. */
86
139
  maxSubtaskDepth?: number
140
+ /** Roster-poll cadence in seconds while the board has work to reconcile. */
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
148
+ /**
149
+ * GLOBAL native /goal switch (default off): the master control over whether
150
+ * this board starts a run with dsh's built-in /goal at all. Frozen per
151
+ * execution.
152
+ */
153
+ goalRunEnabled?: boolean
87
154
  /** Goal acceptance for this board's executions (default on). */
88
155
  goalVerification?: boolean
89
156
  /** Judge model route for goal acceptance; blank inherits the host default. */
90
157
  goalVerificationModel?: string
91
158
  /** Judge reasoning effort; blank inherits the host default level. */
92
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
93
174
  }
94
175
 
95
176
  /** What the task-board card renders. */
@@ -102,12 +183,32 @@ export interface TaskBoardSettingsCardState extends CardShell {
102
183
  preventIdleSleep: CardFieldState
103
184
  /** Subtask depth limit field. */
104
185
  maxSubtaskDepth: CardFieldState
186
+ /** Roster-poll cadence field. */
187
+ sessionPollSeconds: CardFieldState
188
+ /** Run-launch admission width field. */
189
+ runLaunchWidth: CardFieldState
190
+ /** GLOBAL native /goal switch. */
191
+ goalRunEnabled: CardFieldState
105
192
  /** Goal-acceptance switch. */
106
193
  goalVerification: CardFieldState
107
194
  /** Judge model route field. */
108
195
  goalVerificationModel: CardFieldState
109
196
  /** Judge reasoning-effort field. */
110
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[]
111
212
  }
112
213
 
113
214
  /** The registration-side face the card's slot entry injects. */
@@ -124,6 +225,8 @@ export interface TaskBoardSettingsCardFace extends CardActions {
124
225
  export class TaskBoardSettingsCardController {
125
226
  private readonly form: CardForm<TaskBoardSettings>
126
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
127
230
 
128
231
  /** @param scope - the bound configuration form for the `task-board` namespace. */
129
232
  constructor(scope: ConfigForm<TaskBoardSettings>, private readonly dispatch: TaskBoardExtensionDispatch) {
@@ -132,9 +235,19 @@ export class TaskBoardSettingsCardController {
132
235
  booleanField('announceToAgent'),
133
236
  booleanField('preventIdleSleep'),
134
237
  subtaskDepthField(),
238
+ sessionPollField(),
239
+ runLaunchWidthField(),
240
+ booleanField('goalRunEnabled'),
135
241
  booleanField('goalVerification'),
136
- judgeModelField(),
137
- 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'),
138
251
  ])
139
252
  this.store = this.form.bind(() => this.projection())
140
253
  }
@@ -146,12 +259,34 @@ export class TaskBoardSettingsCardController {
146
259
  announceToAgent: this.form.field('announceToAgent'),
147
260
  preventIdleSleep: this.form.field('preventIdleSleep'),
148
261
  maxSubtaskDepth: this.form.field('maxSubtaskDepth'),
262
+ sessionPollSeconds: this.form.field('sessionPollSeconds'),
263
+ runLaunchWidth: this.form.field('runLaunchWidth'),
264
+ goalRunEnabled: this.form.field('goalRunEnabled'),
149
265
  goalVerification: this.form.field('goalVerification'),
150
266
  goalVerificationModel: this.form.field('goalVerificationModel'),
151
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 }),
152
273
  }
153
274
  }
154
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
+
155
290
  /**
156
291
  * Build the face the card's slot registration injects.
157
292
  * @returns the card's snapshot and its form actions.
@@ -265,6 +400,25 @@ export function TaskBoardSettingsCard(props: TaskBoardSettingsCardProps) {
265
400
  invalidLabel: t('settings.invalidNumber'),
266
401
  disabled,
267
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
+ }
268
422
  // Chrome state of the nested disclosure cards below: they stage into this
269
423
  // card's form and are written by its one save, so they carry no footer and no
270
424
  // unsaved pill of their own (the outer header owns the pill).
@@ -342,6 +496,16 @@ export function TaskBoardSettingsCard(props: TaskBoardSettingsCardProps) {
342
496
  onEdit={(text) => { props.edit('preventIdleSleep', text) }}
343
497
  onReset={() => { props.resetField('preventIdleSleep') }}
344
498
  />
499
+ <ValueField
500
+ id="settings-task-board-session-poll"
501
+ label={t('settings.sessionPollSeconds')}
502
+ hint={t('settings.sessionPollSecondsHint')}
503
+ numeric
504
+ {...fieldProps}
505
+ {...state.sessionPollSeconds}
506
+ onEdit={(text) => { props.edit('sessionPollSeconds', text) }}
507
+ onReset={() => { props.resetField('sessionPollSeconds') }}
508
+ />
345
509
  <ChoiceField
346
510
  id="settings-task-board-subtask-depth"
347
511
  label={t('settings.maxSubtaskDepth')}
@@ -356,6 +520,40 @@ export function TaskBoardSettingsCard(props: TaskBoardSettingsCardProps) {
356
520
  onEdit={(text) => { props.edit('maxSubtaskDepth', text) }}
357
521
  onReset={() => { props.resetField('maxSubtaskDepth') }}
358
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
+ />
533
+ </PluginSettingsCard>
534
+
535
+ <PluginSettingsCard
536
+ t={t}
537
+ titleKey="settings.runBehaviorTitle"
538
+ descriptionKey="settings.runBehaviorCardHint"
539
+ defaultOpen={false}
540
+ hideFooter
541
+ state={nestedShell}
542
+ onSave={props.save}
543
+ onDiscard={props.discard}
544
+ >
545
+ <BooleanField
546
+ id="settings-task-board-goal-run"
547
+ label={t('settings.goalRunGlobal')}
548
+ hint={t('settings.goalRunGlobalHint')}
549
+ inheritLabel={t('settings.inherit')}
550
+ onLabel={t('settings.on')}
551
+ offLabel={t('settings.off')}
552
+ {...fieldProps}
553
+ {...state.goalRunEnabled}
554
+ onEdit={(text) => { props.edit('goalRunEnabled', text) }}
555
+ onReset={() => { props.resetField('goalRunEnabled') }}
556
+ />
359
557
  </PluginSettingsCard>
360
558
 
361
559
  <PluginSettingsCard
@@ -438,6 +636,79 @@ export function TaskBoardSettingsCard(props: TaskBoardSettingsCardProps) {
438
636
  )}
439
637
  </PluginSettingsCard>
440
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
+
441
712
  {/* Provider sections: a family extension contributes its own nested
442
713
  settings card into this list through the seat this card declares —
443
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
  : (