@linxin666/dsh-client-ui-task-board 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +59 -20
- package/README.zh.md +59 -20
- package/lib/client.js +6208 -624
- package/lib/client.js.map +1 -1
- package/lib/index.js +4774 -493
- package/lib/types/client/PluginSettingsCard.d.ts +7 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +31 -3
- package/lib/types/client/board/ClampedMarkdown.d.ts +3 -0
- package/lib/types/client/board/ConfirmDialog.d.ts +4 -1
- package/lib/types/client/board/EditTaskModal.d.ts +5 -2
- package/lib/types/client/board/LinkSubtaskModal.d.ts +5 -2
- package/lib/types/client/board/NewTaskModal.d.ts +4 -1
- package/lib/types/client/board/TagManagerModal.d.ts +26 -0
- package/lib/types/client/board/TaskBoard.d.ts +10 -1
- package/lib/types/client/board/TaskCard.d.ts +14 -1
- package/lib/types/client/board/TaskDetail.d.ts +3 -1
- package/lib/types/client/board/TaskForm.d.ts +4 -1
- package/lib/types/client/board/VerificationReport.d.ts +23 -0
- package/lib/types/client/board/card-view.d.ts +113 -0
- package/lib/types/client/board/icons.d.ts +32 -0
- package/lib/types/client/board/overlay.d.ts +69 -0
- package/lib/types/client/board/status-key.d.ts +15 -0
- package/lib/types/client/board/task-markdown.d.ts +19 -0
- package/lib/types/client/index.d.ts +23 -5
- package/lib/types/client/locales.d.ts +168 -1
- package/lib/types/client/native-panel.d.ts +6 -20
- package/lib/types/client/plugin-card-seat.d.ts +5 -0
- package/lib/types/client/schedule-zone.d.ts +50 -0
- package/lib/types/client/seats.d.ts +18 -0
- package/lib/types/client/service.d.ts +32 -0
- package/lib/types/client/settings-form.d.ts +16 -0
- package/lib/types/core/controller.d.ts +46 -11
- package/lib/types/core/extension.d.ts +283 -0
- package/lib/types/core/handover.d.ts +13 -1
- package/lib/types/core/issue-form.d.ts +16 -0
- package/lib/types/core/poll-cadence.d.ts +31 -0
- package/lib/types/core/tasks.d.ts +228 -17
- package/lib/types/core/use-cases/task-schedule.d.ts +68 -10
- package/lib/types/core/use-cases/task-tag.d.ts +48 -0
- package/lib/types/core/use-cases/task-update.d.ts +5 -3
- package/lib/types/core/verification-budget.d.ts +52 -0
- package/lib/types/core/verification.d.ts +753 -0
- package/lib/types/core/workspace-target.d.ts +41 -0
- package/lib/types/host/agent-tools.d.ts +1 -1
- package/lib/types/host/extension-registry.d.ts +111 -0
- package/lib/types/host/llm-dispatch.d.ts +63 -0
- package/lib/types/host/verification-gate.d.ts +95 -0
- package/lib/types/host/verification-runner.d.ts +139 -0
- package/lib/types/host/workspace-evidence.d.ts +53 -0
- package/lib/types/host-ledger.d.ts +133 -14
- package/lib/types/host-runner.d.ts +55 -10
- package/lib/types/host-service.d.ts +205 -11
- package/lib/types/index.d.ts +134 -26
- package/lib/types/protocol.d.ts +92 -10
- package/lib/types/tool-surface.d.ts +71 -0
- package/package.json +28 -31
- package/src/client/PluginSettingsCard.tsx +9 -0
- package/src/client/TaskBoardSettingsCard.tsx +378 -66
- package/src/client/board/ClampedMarkdown.tsx +69 -0
- package/src/client/board/ConfirmDialog.tsx +7 -3
- package/src/client/board/EditTaskModal.tsx +5 -2
- package/src/client/board/LinkSubtaskModal.tsx +12 -7
- package/src/client/board/NewTaskModal.tsx +168 -21
- package/src/client/board/TagManagerModal.tsx +235 -0
- package/src/client/board/TaskBoard.tsx +259 -103
- package/src/client/board/TaskCard.tsx +214 -81
- package/src/client/board/TaskDetail.tsx +289 -43
- package/src/client/board/TaskForm.tsx +13 -3
- package/src/client/board/VerificationReport.tsx +322 -0
- package/src/client/board/card-view.ts +220 -0
- package/src/client/board/icons.tsx +89 -0
- package/src/client/board/overlay.tsx +189 -0
- package/src/client/board/status-key.ts +30 -0
- package/src/client/board/task-markdown.module.css +137 -0
- package/src/client/board/task-markdown.tsx +106 -0
- package/src/client/board-settings.module.css +66 -0
- package/src/client/board.module.css +488 -63
- package/src/client/index.ts +82 -51
- package/src/client/locales.ts +343 -6
- package/src/client/native-panel.tsx +18 -2
- package/src/client/plugin-card-seat.ts +7 -0
- package/src/client/schedule-zone.ts +100 -2
- package/src/client/seats.tsx +58 -0
- package/src/client/service.ts +94 -0
- package/src/client/settings-card.module.css +31 -5
- package/src/client/settings-form.ts +27 -1
- package/src/core/controller.ts +105 -10
- package/src/core/extension.ts +376 -0
- package/src/core/freeze-snapshot.ts +2 -2
- package/src/core/handover.ts +16 -1
- package/src/core/issue-form.ts +19 -0
- package/src/core/poll-cadence.ts +41 -0
- package/src/core/store.ts +80 -15
- package/src/core/tasks.ts +293 -45
- package/src/core/use-cases/task-create.ts +30 -9
- package/src/core/use-cases/task-parent.ts +3 -2
- package/src/core/use-cases/task-schedule.ts +190 -25
- package/src/core/use-cases/task-tag.ts +110 -0
- package/src/core/use-cases/task-update.ts +13 -7
- package/src/core/verification-budget.ts +115 -0
- package/src/core/verification.ts +1594 -0
- package/src/core/workspace-target.ts +59 -0
- package/src/host/agent-tools.ts +125 -38
- package/src/host/extension-registry.ts +509 -0
- package/src/host/llm-dispatch.ts +87 -0
- package/src/host/verification-gate.ts +487 -0
- package/src/host/verification-runner.ts +702 -0
- package/src/host/workspace-evidence.ts +176 -0
- package/src/host-ai.ts +15 -9
- package/src/host-ledger.ts +468 -71
- package/src/host-routes.ts +15 -2
- package/src/host-runner.ts +106 -24
- package/src/host-service.ts +546 -40
- package/src/index.ts +305 -21
- package/src/protocol.ts +226 -28
- package/src/tool-surface.ts +95 -0
- package/lib/invariant.js +0 -6
- package/lib/types/invariant.d.ts +0 -3
- package/src/invariant.ts +0 -4
|
@@ -4,14 +4,19 @@
|
|
|
4
4
|
* the Web UI plugin group renders, bound to the `task-board` namespace.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
7
|
+
import type { InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
8
8
|
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
|
9
9
|
import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
10
10
|
import { useEffect, useState } from 'react'
|
|
11
|
-
import type { TaskBoardPowerSnapshot } from '../protocol.ts'
|
|
12
|
-
import {
|
|
11
|
+
import type { TaskBoardPowerSnapshot, TaskBoardVerificationOptions } from '../protocol.ts'
|
|
12
|
+
import type { TaskBoardExtensionDispatch } from '../core/extension.ts'
|
|
13
|
+
import { resolveContract, type ModelCatalogView, type VerificationSettings } from '../core/verification.ts'
|
|
14
|
+
import { parseModelRoute } from '../core/verification.ts'
|
|
15
|
+
import { PluginSettingsCard, BooleanField, ChoiceField, ValueField } from './PluginSettingsCard.tsx'
|
|
13
16
|
import { SUBTASK_DEPTH_MAX, SUBTASK_DEPTH_MIN } from '../core/subtask.ts'
|
|
17
|
+
import { SESSION_POLL_MAX_SECONDS, SESSION_POLL_MIN_SECONDS } from '../core/poll-cadence.ts'
|
|
14
18
|
import { CardForm, booleanField, type CardActions, type CardShell, type FieldSpec, type FieldState as CardFieldState } from './settings-form.ts'
|
|
19
|
+
import settingsCss from './board-settings.module.css'
|
|
15
20
|
|
|
16
21
|
/** The depth choices the card offers, derived from the supported range. */
|
|
17
22
|
const SUBTASK_DEPTH_CHOICES: readonly string[] = Array.from(
|
|
@@ -19,6 +24,28 @@ const SUBTASK_DEPTH_CHOICES: readonly string[] = Array.from(
|
|
|
19
24
|
(_, index) => String(SUBTASK_DEPTH_MIN + index),
|
|
20
25
|
)
|
|
21
26
|
|
|
27
|
+
/**
|
|
28
|
+
* The roster-poll cadence field: a whole number of seconds inside the
|
|
29
|
+
* supported range, because the Host schema (`sessionPollSeconds`) is numeric.
|
|
30
|
+
* A draft outside the range blocks the save instead of staging a value the
|
|
31
|
+
* Host would clamp behind the user's back.
|
|
32
|
+
*/
|
|
33
|
+
function sessionPollField(): FieldSpec {
|
|
34
|
+
return {
|
|
35
|
+
field: 'sessionPollSeconds',
|
|
36
|
+
format: value => typeof value === 'number' && Number.isInteger(value) ? String(value) : '',
|
|
37
|
+
parse: (text) => {
|
|
38
|
+
const trimmed = text.trim()
|
|
39
|
+
if (trimmed === '') return { kind: 'clear' }
|
|
40
|
+
if (!/^\d+$/.test(trimmed)) return undefined
|
|
41
|
+
const seconds = Number(trimmed)
|
|
42
|
+
return seconds >= SESSION_POLL_MIN_SECONDS && seconds <= SESSION_POLL_MAX_SECONDS
|
|
43
|
+
? { kind: 'set', value: seconds }
|
|
44
|
+
: undefined
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
22
49
|
/**
|
|
23
50
|
* The depth field: a choice among the supported levels whose draft text is a
|
|
24
51
|
* number, because the Host schema (`maxSubtaskDepth`) is numeric. A draft
|
|
@@ -37,6 +64,39 @@ function subtaskDepthField(): FieldSpec {
|
|
|
37
64
|
}
|
|
38
65
|
}
|
|
39
66
|
|
|
67
|
+
/**
|
|
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.
|
|
71
|
+
*/
|
|
72
|
+
function judgeModelField(): FieldSpec {
|
|
73
|
+
return {
|
|
74
|
+
field: 'goalVerificationModel',
|
|
75
|
+
format: value => typeof value === 'string' ? value : '',
|
|
76
|
+
parse: (text) => {
|
|
77
|
+
const trimmed = text.trim()
|
|
78
|
+
if (trimmed === '') return { kind: 'clear' }
|
|
79
|
+
return parseModelRoute(trimmed) === undefined ? undefined : { kind: 'set', value: trimmed }
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
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.
|
|
88
|
+
*/
|
|
89
|
+
function judgeEffortField(): FieldSpec {
|
|
90
|
+
return {
|
|
91
|
+
field: 'goalVerificationReasoningEffort',
|
|
92
|
+
format: value => typeof value === 'string' ? value : '',
|
|
93
|
+
parse: (text) => {
|
|
94
|
+
const trimmed = text.trim()
|
|
95
|
+
return trimmed === '' ? { kind: 'clear' } : { kind: 'set', value: trimmed }
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
40
100
|
/** The task-board fields this card edits (the namespace's full schema). */
|
|
41
101
|
export interface TaskBoardSettings {
|
|
42
102
|
/** Master switch for the plugin. */
|
|
@@ -47,6 +107,20 @@ export interface TaskBoardSettings {
|
|
|
47
107
|
preventIdleSleep?: boolean
|
|
48
108
|
/** Subtask depth limit (1..3); 1 means a single level of subtasks. */
|
|
49
109
|
maxSubtaskDepth?: number
|
|
110
|
+
/** Roster-poll cadence in seconds while the board has work to reconcile. */
|
|
111
|
+
sessionPollSeconds?: number
|
|
112
|
+
/**
|
|
113
|
+
* GLOBAL native /goal switch (default off): the master control over whether
|
|
114
|
+
* this board starts a run with dsh's built-in /goal at all. Frozen per
|
|
115
|
+
* execution.
|
|
116
|
+
*/
|
|
117
|
+
goalRunEnabled?: boolean
|
|
118
|
+
/** Goal acceptance for this board's executions (default on). */
|
|
119
|
+
goalVerification?: boolean
|
|
120
|
+
/** Judge model route for goal acceptance; blank inherits the host default. */
|
|
121
|
+
goalVerificationModel?: string
|
|
122
|
+
/** Judge reasoning effort; blank inherits the host default level. */
|
|
123
|
+
goalVerificationReasoningEffort?: string
|
|
50
124
|
}
|
|
51
125
|
|
|
52
126
|
/** What the task-board card renders. */
|
|
@@ -59,6 +133,16 @@ export interface TaskBoardSettingsCardState extends CardShell {
|
|
|
59
133
|
preventIdleSleep: CardFieldState
|
|
60
134
|
/** Subtask depth limit field. */
|
|
61
135
|
maxSubtaskDepth: CardFieldState
|
|
136
|
+
/** Roster-poll cadence field. */
|
|
137
|
+
sessionPollSeconds: CardFieldState
|
|
138
|
+
/** GLOBAL native /goal switch. */
|
|
139
|
+
goalRunEnabled: CardFieldState
|
|
140
|
+
/** Goal-acceptance switch. */
|
|
141
|
+
goalVerification: CardFieldState
|
|
142
|
+
/** Judge model route field. */
|
|
143
|
+
goalVerificationModel: CardFieldState
|
|
144
|
+
/** Judge reasoning-effort field. */
|
|
145
|
+
goalVerificationReasoningEffort: CardFieldState
|
|
62
146
|
}
|
|
63
147
|
|
|
64
148
|
/** The registration-side face the card's slot entry injects. */
|
|
@@ -67,6 +151,8 @@ export interface TaskBoardSettingsCardFace extends CardActions {
|
|
|
67
151
|
/** Card snapshot bound by the renderer as useTaskBoardSettingsCard. */
|
|
68
152
|
taskBoardSettingsCard: SnapshotStore<TaskBoardSettingsCardState>
|
|
69
153
|
}
|
|
154
|
+
/** Deliver one provider action over the board's same-origin channel. */
|
|
155
|
+
dispatch: TaskBoardExtensionDispatch
|
|
70
156
|
}
|
|
71
157
|
|
|
72
158
|
/** Bridges the `task-board` settings form onto the card's staged form. */
|
|
@@ -75,12 +161,17 @@ export class TaskBoardSettingsCardController {
|
|
|
75
161
|
private readonly store: SnapshotStore<TaskBoardSettingsCardState>
|
|
76
162
|
|
|
77
163
|
/** @param scope - the bound configuration form for the `task-board` namespace. */
|
|
78
|
-
constructor(scope: ConfigForm<TaskBoardSettings
|
|
164
|
+
constructor(scope: ConfigForm<TaskBoardSettings>, private readonly dispatch: TaskBoardExtensionDispatch) {
|
|
79
165
|
this.form = new CardForm(scope, [
|
|
80
166
|
booleanField('enabled'),
|
|
81
167
|
booleanField('announceToAgent'),
|
|
82
168
|
booleanField('preventIdleSleep'),
|
|
83
169
|
subtaskDepthField(),
|
|
170
|
+
sessionPollField(),
|
|
171
|
+
booleanField('goalRunEnabled'),
|
|
172
|
+
booleanField('goalVerification'),
|
|
173
|
+
judgeModelField(),
|
|
174
|
+
judgeEffortField(),
|
|
84
175
|
])
|
|
85
176
|
this.store = this.form.bind(() => this.projection())
|
|
86
177
|
}
|
|
@@ -92,6 +183,11 @@ export class TaskBoardSettingsCardController {
|
|
|
92
183
|
announceToAgent: this.form.field('announceToAgent'),
|
|
93
184
|
preventIdleSleep: this.form.field('preventIdleSleep'),
|
|
94
185
|
maxSubtaskDepth: this.form.field('maxSubtaskDepth'),
|
|
186
|
+
sessionPollSeconds: this.form.field('sessionPollSeconds'),
|
|
187
|
+
goalRunEnabled: this.form.field('goalRunEnabled'),
|
|
188
|
+
goalVerification: this.form.field('goalVerification'),
|
|
189
|
+
goalVerificationModel: this.form.field('goalVerificationModel'),
|
|
190
|
+
goalVerificationReasoningEffort: this.form.field('goalVerificationReasoningEffort'),
|
|
95
191
|
}
|
|
96
192
|
}
|
|
97
193
|
|
|
@@ -100,7 +196,7 @@ export class TaskBoardSettingsCardController {
|
|
|
100
196
|
* @returns the card's snapshot and its form actions.
|
|
101
197
|
*/
|
|
102
198
|
inject(): TaskBoardSettingsCardFace {
|
|
103
|
-
return { hooks: { taskBoardSettingsCard: this.store }, ...this.form.actions() }
|
|
199
|
+
return { hooks: { taskBoardSettingsCard: this.store }, ...this.form.actions(), dispatch: this.dispatch }
|
|
104
200
|
}
|
|
105
201
|
|
|
106
202
|
/**
|
|
@@ -117,6 +213,18 @@ export type TaskBoardSettingsCardProps =
|
|
|
117
213
|
PropsRuntime<'web-ui.plugin.item'>
|
|
118
214
|
& PropsLocale<'task-board'>
|
|
119
215
|
& InjectFace<TaskBoardSettingsCardFace>
|
|
216
|
+
& PropsRenderSlots<'task-board.settings.section'>
|
|
217
|
+
|
|
218
|
+
/** Kanban mark for the card header: three columns of unequal depth. */
|
|
219
|
+
function BoardMark() {
|
|
220
|
+
return (
|
|
221
|
+
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
|
222
|
+
<rect x="2.2" y="2.6" width="3.9" height="12.8" rx="1.95" fill="currentColor" />
|
|
223
|
+
<rect x="7.05" y="2.6" width="3.9" height="7.6" rx="1.95" fill="currentColor" />
|
|
224
|
+
<rect x="11.9" y="2.6" width="3.9" height="10.2" rx="1.95" fill="currentColor" />
|
|
225
|
+
</svg>
|
|
226
|
+
)
|
|
227
|
+
}
|
|
120
228
|
|
|
121
229
|
/**
|
|
122
230
|
* Render the task-board card.
|
|
@@ -124,15 +232,22 @@ export type TaskBoardSettingsCardProps =
|
|
|
124
232
|
* @returns the card.
|
|
125
233
|
*/
|
|
126
234
|
export function TaskBoardSettingsCard(props: TaskBoardSettingsCardProps) {
|
|
127
|
-
const { t } = props
|
|
235
|
+
const { t, renderSlot, dispatch } = props
|
|
128
236
|
const state = props.useTaskBoardSettingsCard(snapshot => snapshot)
|
|
129
237
|
const disabled = !state.writable
|
|
130
238
|
const [power, setPower] = useState<TaskBoardPowerSnapshot | undefined>()
|
|
239
|
+
const [verification, setVerification] = useState<TaskBoardVerificationOptions | undefined>()
|
|
131
240
|
useEffect(() => {
|
|
132
241
|
// The SSE channel already carries power on every real change and pushes
|
|
133
242
|
// one frame on subscribe; polling the full /state snapshot every 5 s
|
|
134
243
|
// re-cloned and re-serialized the whole ledger server-side for one field.
|
|
135
244
|
let live = true
|
|
245
|
+
void fetch('api/task-board/verification')
|
|
246
|
+
.then(r => r.ok ? r.json() : undefined)
|
|
247
|
+
.then((data: TaskBoardVerificationOptions | undefined) => {
|
|
248
|
+
if (data?.catalog && live) setVerification(data)
|
|
249
|
+
})
|
|
250
|
+
.catch(() => {})
|
|
136
251
|
const events = new EventSource('api/task-board/events')
|
|
137
252
|
events.onmessage = (message: MessageEvent<string>): void => {
|
|
138
253
|
try {
|
|
@@ -144,17 +259,69 @@ export function TaskBoardSettingsCard(props: TaskBoardSettingsCardProps) {
|
|
|
144
259
|
}
|
|
145
260
|
return () => { live = false; events.close() }
|
|
146
261
|
}, [])
|
|
262
|
+
// The staged drafts drive the resolved preview, so an unsaved model or level
|
|
263
|
+
// change already shows what the next execution would freeze (including a
|
|
264
|
+
// reasoning-level fallback the target model forces).
|
|
265
|
+
const catalog: ModelCatalogView = verification?.catalog ?? { groups: [] }
|
|
266
|
+
const stagedSettings: VerificationSettings = {
|
|
267
|
+
enabled: state.goalVerification.text !== 'false',
|
|
268
|
+
model: state.goalVerificationModel.text,
|
|
269
|
+
reasoningEffort: state.goalVerificationReasoningEffort.text,
|
|
270
|
+
}
|
|
271
|
+
const preview = resolveContract(stagedSettings, catalog)
|
|
272
|
+
const inheritRoute = catalog.default
|
|
273
|
+
const modelChoices = [
|
|
274
|
+
{
|
|
275
|
+
value: '',
|
|
276
|
+
label: inheritRoute === undefined
|
|
277
|
+
? t('settings.goalVerificationModelInheritUnknown')
|
|
278
|
+
: t('settings.goalVerificationModelInherit', { model: inheritRoute.provider + '/' + inheritRoute.model }),
|
|
279
|
+
},
|
|
280
|
+
...catalog.groups.flatMap(group => group.models.map(model => ({
|
|
281
|
+
value: group.id + '/' + model.id,
|
|
282
|
+
label: (group.name ?? group.id) + ' · ' + (model.name ?? model.id),
|
|
283
|
+
}))),
|
|
284
|
+
]
|
|
285
|
+
const stagedRoute = parseModelRoute(stagedSettings.model) ?? (inheritRoute === undefined ? undefined : { provider: inheritRoute.provider, model: inheritRoute.model })
|
|
286
|
+
const stagedEfforts = stagedRoute === undefined
|
|
287
|
+
? []
|
|
288
|
+
: catalog.groups.find(group => group.id === stagedRoute.provider)?.models.find(model => model.id === stagedRoute.model)?.reasoning?.efforts ?? []
|
|
289
|
+
const effortChoices = [
|
|
290
|
+
{
|
|
291
|
+
value: '',
|
|
292
|
+
label: stagedRoute === undefined || stagedEfforts.length === 0
|
|
293
|
+
? t('settings.goalVerificationEffortInheritUnknown')
|
|
294
|
+
: t('settings.goalVerificationEffortInherit', {
|
|
295
|
+
effort: catalog.groups.find(group => group.id === stagedRoute.provider)?.models
|
|
296
|
+
.find(model => model.id === stagedRoute.model)?.reasoning?.defaultEffort ?? t('settings.goalVerificationEffortInheritUnknown'),
|
|
297
|
+
}),
|
|
298
|
+
},
|
|
299
|
+
...stagedEfforts.map(effort => ({ value: effort.id, label: effort.name ?? effort.id })),
|
|
300
|
+
]
|
|
147
301
|
const fieldProps = {
|
|
148
302
|
overriddenLabel: t('settings.overridden'),
|
|
149
303
|
resetLabel: t('settings.reset'),
|
|
150
304
|
invalidLabel: t('settings.invalidNumber'),
|
|
151
305
|
disabled,
|
|
152
306
|
}
|
|
307
|
+
// Chrome state of the nested disclosure cards below: they stage into this
|
|
308
|
+
// card's form and are written by its one save, so they carry no footer and no
|
|
309
|
+
// unsaved pill of their own (the outer header owns the pill).
|
|
310
|
+
const nestedShell: CardShell = {
|
|
311
|
+
available: true,
|
|
312
|
+
exposed: true,
|
|
313
|
+
writable: state.writable,
|
|
314
|
+
dirty: false,
|
|
315
|
+
invalid: state.invalid,
|
|
316
|
+
saving: state.saving,
|
|
317
|
+
failed: false,
|
|
318
|
+
}
|
|
153
319
|
return (
|
|
154
320
|
<PluginSettingsCard
|
|
155
321
|
t={t}
|
|
156
322
|
titleKey="settings.title"
|
|
157
323
|
descriptionKey="settings.description"
|
|
324
|
+
icon={<BoardMark />}
|
|
158
325
|
defaultOpen={false}
|
|
159
326
|
state={state}
|
|
160
327
|
renderChildrenWhenNotExposed
|
|
@@ -162,66 +329,211 @@ export function TaskBoardSettingsCard(props: TaskBoardSettingsCardProps) {
|
|
|
162
329
|
onSave={props.save}
|
|
163
330
|
onDiscard={props.discard}
|
|
164
331
|
>
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
332
|
+
{/* The board's own settings are nested disclosure cards, so an expanded
|
|
333
|
+
card reads as a short topic list: the board and its runtime behavior,
|
|
334
|
+
goal acceptance, and whatever a provider contributes (a provider
|
|
335
|
+
extension registers into the seat rendered last). Every one
|
|
336
|
+
of them starts collapsed and shares this card's single save. */}
|
|
337
|
+
<ul className={settingsCss.nestedCards}>
|
|
338
|
+
<PluginSettingsCard
|
|
339
|
+
t={t}
|
|
340
|
+
titleKey="settings.enabled"
|
|
341
|
+
descriptionKey="settings.enabledCardHint"
|
|
342
|
+
defaultOpen={false}
|
|
343
|
+
hideFooter
|
|
344
|
+
state={nestedShell}
|
|
345
|
+
onSave={props.save}
|
|
346
|
+
onDiscard={props.discard}
|
|
347
|
+
>
|
|
348
|
+
<BooleanField
|
|
349
|
+
id="settings-task-board-enabled"
|
|
350
|
+
label={t('settings.enabled')}
|
|
351
|
+
hint={t('settings.enabledHint')}
|
|
352
|
+
inheritLabel={t('settings.inherit')}
|
|
353
|
+
onLabel={t('settings.on')}
|
|
354
|
+
offLabel={t('settings.off')}
|
|
355
|
+
{...fieldProps}
|
|
356
|
+
{...state.enabled}
|
|
357
|
+
onEdit={(text) => { props.edit('enabled', text) }}
|
|
358
|
+
onReset={() => { props.resetField('enabled') }}
|
|
359
|
+
/>
|
|
360
|
+
<BooleanField
|
|
361
|
+
id="settings-task-board-announce"
|
|
362
|
+
label={t('settings.announceToAgent')}
|
|
363
|
+
hint={t('settings.announceToAgentHint')}
|
|
364
|
+
inheritLabel={t('settings.inherit')}
|
|
365
|
+
onLabel={t('settings.on')}
|
|
366
|
+
offLabel={t('settings.off')}
|
|
367
|
+
{...fieldProps}
|
|
368
|
+
{...state.announceToAgent}
|
|
369
|
+
onEdit={(text) => { props.edit('announceToAgent', text) }}
|
|
370
|
+
onReset={() => { props.resetField('announceToAgent') }}
|
|
371
|
+
/>
|
|
372
|
+
<BooleanField
|
|
373
|
+
id="settings-task-board-prevent-idle-sleep"
|
|
374
|
+
label={t('settings.preventIdleSleep')}
|
|
375
|
+
hint={t('settings.preventIdleSleepHint')}
|
|
376
|
+
inheritLabel={t('settings.inherit')}
|
|
377
|
+
onLabel={t('settings.on')}
|
|
378
|
+
offLabel={t('settings.off')}
|
|
379
|
+
{...fieldProps}
|
|
380
|
+
{...state.preventIdleSleep}
|
|
381
|
+
onEdit={(text) => { props.edit('preventIdleSleep', text) }}
|
|
382
|
+
onReset={() => { props.resetField('preventIdleSleep') }}
|
|
383
|
+
/>
|
|
384
|
+
<ValueField
|
|
385
|
+
id="settings-task-board-session-poll"
|
|
386
|
+
label={t('settings.sessionPollSeconds')}
|
|
387
|
+
hint={t('settings.sessionPollSecondsHint')}
|
|
388
|
+
numeric
|
|
389
|
+
{...fieldProps}
|
|
390
|
+
{...state.sessionPollSeconds}
|
|
391
|
+
onEdit={(text) => { props.edit('sessionPollSeconds', text) }}
|
|
392
|
+
onReset={() => { props.resetField('sessionPollSeconds') }}
|
|
393
|
+
/>
|
|
394
|
+
<ChoiceField
|
|
395
|
+
id="settings-task-board-subtask-depth"
|
|
396
|
+
label={t('settings.maxSubtaskDepth')}
|
|
397
|
+
hint={t('settings.maxSubtaskDepthHint')}
|
|
398
|
+
inheritLabel={t('settings.inherit')}
|
|
399
|
+
choices={SUBTASK_DEPTH_CHOICES.map(value => ({
|
|
400
|
+
value,
|
|
401
|
+
label: t('settings.maxSubtaskDepthOption', { depth: value }),
|
|
402
|
+
}))}
|
|
403
|
+
{...fieldProps}
|
|
404
|
+
{...state.maxSubtaskDepth}
|
|
405
|
+
onEdit={(text) => { props.edit('maxSubtaskDepth', text) }}
|
|
406
|
+
onReset={() => { props.resetField('maxSubtaskDepth') }}
|
|
407
|
+
/>
|
|
408
|
+
</PluginSettingsCard>
|
|
409
|
+
|
|
410
|
+
<PluginSettingsCard
|
|
411
|
+
t={t}
|
|
412
|
+
titleKey="settings.runBehaviorTitle"
|
|
413
|
+
descriptionKey="settings.runBehaviorCardHint"
|
|
414
|
+
defaultOpen={false}
|
|
415
|
+
hideFooter
|
|
416
|
+
state={nestedShell}
|
|
417
|
+
onSave={props.save}
|
|
418
|
+
onDiscard={props.discard}
|
|
419
|
+
>
|
|
420
|
+
<BooleanField
|
|
421
|
+
id="settings-task-board-goal-run"
|
|
422
|
+
label={t('settings.goalRunGlobal')}
|
|
423
|
+
hint={t('settings.goalRunGlobalHint')}
|
|
424
|
+
inheritLabel={t('settings.inherit')}
|
|
425
|
+
onLabel={t('settings.on')}
|
|
426
|
+
offLabel={t('settings.off')}
|
|
427
|
+
{...fieldProps}
|
|
428
|
+
{...state.goalRunEnabled}
|
|
429
|
+
onEdit={(text) => { props.edit('goalRunEnabled', text) }}
|
|
430
|
+
onReset={() => { props.resetField('goalRunEnabled') }}
|
|
431
|
+
/>
|
|
432
|
+
</PluginSettingsCard>
|
|
433
|
+
|
|
434
|
+
<PluginSettingsCard
|
|
435
|
+
t={t}
|
|
436
|
+
titleKey="settings.goalVerificationTitle"
|
|
437
|
+
descriptionKey="settings.goalVerificationCardHint"
|
|
438
|
+
defaultOpen={false}
|
|
439
|
+
hideFooter
|
|
440
|
+
state={nestedShell}
|
|
441
|
+
onSave={props.save}
|
|
442
|
+
onDiscard={props.discard}
|
|
443
|
+
>
|
|
444
|
+
<BooleanField
|
|
445
|
+
id="settings-task-board-goal-verification"
|
|
446
|
+
label={t('settings.goalVerification')}
|
|
447
|
+
hint={t('settings.goalVerificationHint')}
|
|
448
|
+
inheritLabel={t('settings.inherit')}
|
|
449
|
+
onLabel={t('settings.on')}
|
|
450
|
+
offLabel={t('settings.off')}
|
|
451
|
+
{...fieldProps}
|
|
452
|
+
{...state.goalVerification}
|
|
453
|
+
onEdit={(text) => { props.edit('goalVerification', text) }}
|
|
454
|
+
onReset={() => { props.resetField('goalVerification') }}
|
|
455
|
+
/>
|
|
456
|
+
<ChoiceField
|
|
457
|
+
id="settings-task-board-goal-verification-model"
|
|
458
|
+
label={t('settings.goalVerificationModel')}
|
|
459
|
+
hint={t('settings.goalVerificationModelHint')}
|
|
460
|
+
inheritLabel={t('settings.inherit')}
|
|
461
|
+
choices={modelChoices}
|
|
462
|
+
{...fieldProps}
|
|
463
|
+
{...state.goalVerificationModel}
|
|
464
|
+
onEdit={(text) => { props.edit('goalVerificationModel', text) }}
|
|
465
|
+
onReset={() => { props.resetField('goalVerificationModel') }}
|
|
466
|
+
/>
|
|
467
|
+
<ChoiceField
|
|
468
|
+
id="settings-task-board-goal-verification-effort"
|
|
469
|
+
label={t('settings.goalVerificationEffort')}
|
|
470
|
+
hint={t('settings.goalVerificationEffortHint')}
|
|
471
|
+
inheritLabel={t('settings.inherit')}
|
|
472
|
+
choices={effortChoices}
|
|
473
|
+
{...fieldProps}
|
|
474
|
+
{...state.goalVerificationReasoningEffort}
|
|
475
|
+
onEdit={(text) => { props.edit('goalVerificationReasoningEffort', text) }}
|
|
476
|
+
onReset={() => { props.resetField('goalVerificationReasoningEffort') }}
|
|
477
|
+
/>
|
|
478
|
+
<p className={settingsCss.note}>{t('settings.goalVerificationResolved')}</p>
|
|
479
|
+
{preview.route === undefined
|
|
480
|
+
? <p className={settingsCss.error}>{t('settings.goalVerificationRouteMissing')}</p>
|
|
481
|
+
: (
|
|
482
|
+
<ul className={settingsCss.resolvedList}>
|
|
483
|
+
<li>{t('settings.goalVerificationResolvedModel', { model: preview.route.provider + '/' + preview.route.model })}</li>
|
|
484
|
+
<li>
|
|
485
|
+
{preview.route.reasoningEffort === undefined
|
|
486
|
+
? t('settings.goalVerificationResolvedNoEffort')
|
|
487
|
+
: t('settings.goalVerificationResolvedEffort', { effort: preview.route.reasoningEffort })}
|
|
488
|
+
</li>
|
|
489
|
+
<li>
|
|
490
|
+
{t('settings.goalVerificationResolvedSource', {
|
|
491
|
+
source: preview.modelSource === 'inherit'
|
|
492
|
+
? t('settings.goalVerificationSourceInherit')
|
|
493
|
+
: t('settings.goalVerificationSourceExplicit'),
|
|
494
|
+
})}
|
|
495
|
+
</li>
|
|
496
|
+
<li>{t('settings.goalVerificationResolvedPreset', { threshold: String(preview.threshold) })}</li>
|
|
497
|
+
</ul>
|
|
498
|
+
)}
|
|
499
|
+
{preview.effortFallback !== undefined && (
|
|
500
|
+
<p className={settingsCss.note}>
|
|
501
|
+
{preview.effortFallback.resolved === undefined
|
|
502
|
+
? t('settings.goalVerificationEffortFallbackNone', { requested: preview.effortFallback.requested })
|
|
503
|
+
: t('settings.goalVerificationEffortFallback', {
|
|
504
|
+
requested: preview.effortFallback.requested,
|
|
505
|
+
resolved: preview.effortFallback.resolved,
|
|
506
|
+
})}
|
|
507
|
+
</p>
|
|
508
|
+
)}
|
|
509
|
+
{stagedSettings.model.trim() !== '' && parseModelRoute(stagedSettings.model) === undefined && (
|
|
510
|
+
<p className={settingsCss.note}>{t('settings.goalVerificationModelInvalid')}</p>
|
|
511
|
+
)}
|
|
512
|
+
</PluginSettingsCard>
|
|
513
|
+
|
|
514
|
+
{/* Provider sections: a family extension contributes its own nested
|
|
515
|
+
settings card into this list through the seat this card declares —
|
|
516
|
+
the family provider extension is the one that does. Each
|
|
517
|
+
contributed card keeps its own chrome and save row, and an empty
|
|
518
|
+
seat draws nothing. */}
|
|
519
|
+
{renderSlot('task-board.settings.section', { dispatch })}
|
|
520
|
+
</ul>
|
|
521
|
+
{/* Live power facts and the battery caveat: small print under the topic
|
|
522
|
+
cards, separated from them, instead of two body-size paragraphs. */}
|
|
523
|
+
<div className={settingsCss.power}>
|
|
524
|
+
<p className={settingsCss.powerLine}>
|
|
525
|
+
{t('settings.powerStatus', {
|
|
526
|
+
platform: power?.platform ?? t('settings.powerUnknown'),
|
|
527
|
+
phase: power?.phase ?? t('settings.powerUnknown'),
|
|
528
|
+
running: String(power?.runningSessions ?? 0),
|
|
529
|
+
schedules: String(power?.armedSchedules ?? 0),
|
|
530
|
+
})}
|
|
531
|
+
</p>
|
|
532
|
+
<p className={settingsCss.powerLine}>{t('settings.powerBoundary')}</p>
|
|
533
|
+
{power?.lastError !== undefined && (
|
|
534
|
+
<p className={settingsCss.powerError}>{t('settings.powerError', { error: power.lastError })}</p>
|
|
535
|
+
)}
|
|
536
|
+
</div>
|
|
225
537
|
</PluginSettingsCard>
|
|
226
538
|
)
|
|
227
539
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clamped markdown body for the detail overlay: a long description or prompt
|
|
3
|
+
* renders behind a max-height clamp with a fade into the dialog surface and
|
|
4
|
+
* an expand toggle, so it no longer pushes the execution settings and the
|
|
5
|
+
* execution history out of view. Content that fits the clamp renders
|
|
6
|
+
* untouched — no toggle, no fade, no extra markup behaviour.
|
|
7
|
+
*/
|
|
8
|
+
import { useEffect, useRef, useState } from 'react'
|
|
9
|
+
import css from '../board.module.css'
|
|
10
|
+
import { t } from '../locales.ts'
|
|
11
|
+
import { TaskMarkdown } from './task-markdown.tsx'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Clamp height used when the stylesheet is unavailable to the runtime (unit
|
|
15
|
+
* tests). The rendered value lives on `.clampBody[data-clamped='true']` in
|
|
16
|
+
* board.module.css and is read back through getComputedStyle, so the CSS
|
|
17
|
+
* stays the single source of the geometry.
|
|
18
|
+
*/
|
|
19
|
+
const CLAMP_FALLBACK_PX = 160
|
|
20
|
+
|
|
21
|
+
export function ClampedMarkdown({ source }: { source: string }) {
|
|
22
|
+
const [expanded, setExpanded] = useState(false)
|
|
23
|
+
const [overflowing, setOverflowing] = useState(false)
|
|
24
|
+
const bodyRef = useRef<HTMLDivElement | null>(null)
|
|
25
|
+
|
|
26
|
+
// A different text starts clamped again; the measure effect re-decides
|
|
27
|
+
// whether the toggle is needed at all.
|
|
28
|
+
useEffect(() => { setExpanded(false) }, [source])
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const node = bodyRef.current
|
|
32
|
+
if (node === null) return
|
|
33
|
+
const measure = (): void => {
|
|
34
|
+
// While clamped the computed max-height is the clamp; while expanded it
|
|
35
|
+
// is 'none' and the fallback keeps the toggle stable. scrollHeight is
|
|
36
|
+
// the full content height in both states (overflow:hidden does not
|
|
37
|
+
// truncate it), so one comparison covers both.
|
|
38
|
+
const declared = Number.parseFloat(getComputedStyle(node).maxHeight)
|
|
39
|
+
const clamp = Number.isFinite(declared) ? declared : CLAMP_FALLBACK_PX
|
|
40
|
+
setOverflowing(node.scrollHeight > clamp + 1)
|
|
41
|
+
}
|
|
42
|
+
measure()
|
|
43
|
+
// The overlay width follows the viewport, so a resize can cross the clamp
|
|
44
|
+
// threshold without the text itself changing.
|
|
45
|
+
if (typeof ResizeObserver === 'undefined') return
|
|
46
|
+
const observer = new ResizeObserver(measure)
|
|
47
|
+
observer.observe(node)
|
|
48
|
+
return () => { observer.disconnect() }
|
|
49
|
+
}, [source, expanded])
|
|
50
|
+
|
|
51
|
+
const clamped = overflowing && !expanded
|
|
52
|
+
return (
|
|
53
|
+
<>
|
|
54
|
+
<div ref={bodyRef} className={css.clampBody} data-clamped={clamped}>
|
|
55
|
+
<TaskMarkdown source={source} />
|
|
56
|
+
</div>
|
|
57
|
+
{overflowing && (
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
className={`${css.linkButton} ${css.clampToggle}`}
|
|
61
|
+
aria-expanded={expanded}
|
|
62
|
+
onClick={() => { setExpanded(!expanded) }}
|
|
63
|
+
>
|
|
64
|
+
{expanded ? t('detail.collapse') : t('detail.expand')}
|
|
65
|
+
</button>
|
|
66
|
+
)}
|
|
67
|
+
</>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { t } from '../locales.ts'
|
|
5
5
|
import css from '../board.module.css'
|
|
6
|
+
import { useDialog, type OverlayPhase } from './overlay.tsx'
|
|
6
7
|
|
|
7
8
|
/** Confirm overlay props. */
|
|
8
9
|
export interface ConfirmDialogProps {
|
|
@@ -11,15 +12,18 @@ export interface ConfirmDialogProps {
|
|
|
11
12
|
confirmLabel: string
|
|
12
13
|
/** Render the confirm button in the danger style. */
|
|
13
14
|
danger?: boolean
|
|
15
|
+
/** Which leg of the enter/exit motion pair the surface is on. */
|
|
16
|
+
phase?: OverlayPhase
|
|
14
17
|
onCancel: () => void
|
|
15
18
|
onConfirm: () => void
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
/** Small confirm overlay. */
|
|
19
|
-
export function ConfirmDialog({ title, message, confirmLabel, danger, onCancel, onConfirm }: ConfirmDialogProps) {
|
|
22
|
+
export function ConfirmDialog({ title, message, confirmLabel, danger, phase = 'open', onCancel, onConfirm }: ConfirmDialogProps) {
|
|
23
|
+
const dialog = useDialog<HTMLDivElement>(onCancel, phase)
|
|
20
24
|
return (
|
|
21
|
-
<div className={css.modalBackdrop}
|
|
22
|
-
<div className={css.modal} role="alertdialog" aria-label={title}>
|
|
25
|
+
<div className={css.modalBackdrop} data-state={phase} onMouseDown={dialog.onMouseDown}>
|
|
26
|
+
<div ref={dialog.attach} className={css.modal} role="alertdialog" aria-modal="true" aria-label={title} tabIndex={-1}>
|
|
23
27
|
<h2 className={css.modalTitle}>{title}</h2>
|
|
24
28
|
<p className={css.confirmMessage}>{message}</p>
|
|
25
29
|
<footer className={css.modalFooter}>
|