@linxin666/dsh-client-ui-task-board 0.4.4 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +42 -11
- package/README.zh.md +42 -11
- package/lib/client.js +4958 -472
- package/lib/client.js.map +1 -1
- package/lib/index.js +2841 -121
- package/lib/types/client/PluginSettingsCard.d.ts +7 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +19 -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/board-card-seat.d.ts +34 -0
- package/lib/types/client/index.d.ts +19 -0
- package/lib/types/client/locales.d.ts +103 -1
- package/lib/types/client/native-panel.d.ts +6 -20
- 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 +37 -1
- 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/tasks.d.ts +84 -3
- 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.d.ts +386 -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 +71 -0
- package/lib/types/host/verification-runner.d.ts +108 -0
- package/lib/types/host/workspace-evidence.d.ts +53 -0
- package/lib/types/host-ledger.d.ts +52 -8
- package/lib/types/host-runner.d.ts +23 -1
- package/lib/types/host-service.d.ts +88 -2
- package/lib/types/index.d.ts +80 -26
- package/lib/types/protocol.d.ts +49 -5
- package/lib/types/tool-surface.d.ts +71 -0
- package/package.json +27 -26
- package/src/client/PluginSettingsCard.tsx +9 -0
- package/src/client/TaskBoardSettingsCard.tsx +304 -65
- 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 +17 -1
- 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 +71 -26
- package/src/client/board/TaskForm.tsx +13 -3
- package/src/client/board/VerificationReport.tsx +203 -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 +27 -0
- package/src/client/board/task-markdown.module.css +137 -0
- package/src/client/board/task-markdown.tsx +106 -0
- package/src/client/board-card-seat.ts +102 -0
- package/src/client/board-settings.module.css +66 -0
- package/src/client/board.module.css +488 -63
- package/src/client/index.ts +43 -5
- package/src/client/locales.ts +208 -4
- package/src/client/native-panel.tsx +18 -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 +94 -3
- 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/store.ts +16 -0
- package/src/core/tasks.ts +100 -6
- package/src/core/use-cases/task-parent.ts +3 -2
- package/src/core/use-cases/task-tag.ts +110 -0
- package/src/core/use-cases/task-update.ts +10 -4
- package/src/core/verification.ts +826 -0
- package/src/core/workspace-target.ts +59 -0
- package/src/host/agent-tools.ts +20 -18
- package/src/host/extension-registry.ts +509 -0
- package/src/host/llm-dispatch.ts +87 -0
- package/src/host/verification-gate.ts +331 -0
- package/src/host/verification-runner.ts +535 -0
- package/src/host/workspace-evidence.ts +176 -0
- package/src/host-ai.ts +15 -9
- package/src/host-ledger.ts +143 -32
- package/src/host-routes.ts +15 -2
- package/src/host-runner.ts +60 -11
- package/src/host-service.ts +253 -10
- package/src/index.ts +204 -16
- package/src/protocol.ts +102 -8
- package/src/tool-surface.ts +95 -0
|
@@ -4,14 +4,18 @@
|
|
|
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'
|
|
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'
|
|
12
15
|
import { PluginSettingsCard, BooleanField, ChoiceField } from './PluginSettingsCard.tsx'
|
|
13
16
|
import { SUBTASK_DEPTH_MAX, SUBTASK_DEPTH_MIN } from '../core/subtask.ts'
|
|
14
17
|
import { CardForm, booleanField, type CardActions, type CardShell, type FieldSpec, type FieldState as CardFieldState } from './settings-form.ts'
|
|
18
|
+
import settingsCss from './board-settings.module.css'
|
|
15
19
|
|
|
16
20
|
/** The depth choices the card offers, derived from the supported range. */
|
|
17
21
|
const SUBTASK_DEPTH_CHOICES: readonly string[] = Array.from(
|
|
@@ -37,6 +41,39 @@ function subtaskDepthField(): FieldSpec {
|
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
43
|
|
|
44
|
+
/**
|
|
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.
|
|
48
|
+
*/
|
|
49
|
+
function judgeModelField(): FieldSpec {
|
|
50
|
+
return {
|
|
51
|
+
field: 'goalVerificationModel',
|
|
52
|
+
format: value => typeof value === 'string' ? value : '',
|
|
53
|
+
parse: (text) => {
|
|
54
|
+
const trimmed = text.trim()
|
|
55
|
+
if (trimmed === '') return { kind: 'clear' }
|
|
56
|
+
return parseModelRoute(trimmed) === undefined ? undefined : { kind: 'set', value: trimmed }
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
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.
|
|
65
|
+
*/
|
|
66
|
+
function judgeEffortField(): FieldSpec {
|
|
67
|
+
return {
|
|
68
|
+
field: 'goalVerificationReasoningEffort',
|
|
69
|
+
format: value => typeof value === 'string' ? value : '',
|
|
70
|
+
parse: (text) => {
|
|
71
|
+
const trimmed = text.trim()
|
|
72
|
+
return trimmed === '' ? { kind: 'clear' } : { kind: 'set', value: trimmed }
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
40
77
|
/** The task-board fields this card edits (the namespace's full schema). */
|
|
41
78
|
export interface TaskBoardSettings {
|
|
42
79
|
/** Master switch for the plugin. */
|
|
@@ -47,6 +84,12 @@ export interface TaskBoardSettings {
|
|
|
47
84
|
preventIdleSleep?: boolean
|
|
48
85
|
/** Subtask depth limit (1..3); 1 means a single level of subtasks. */
|
|
49
86
|
maxSubtaskDepth?: number
|
|
87
|
+
/** Goal acceptance for this board's executions (default on). */
|
|
88
|
+
goalVerification?: boolean
|
|
89
|
+
/** Judge model route for goal acceptance; blank inherits the host default. */
|
|
90
|
+
goalVerificationModel?: string
|
|
91
|
+
/** Judge reasoning effort; blank inherits the host default level. */
|
|
92
|
+
goalVerificationReasoningEffort?: string
|
|
50
93
|
}
|
|
51
94
|
|
|
52
95
|
/** What the task-board card renders. */
|
|
@@ -59,6 +102,12 @@ export interface TaskBoardSettingsCardState extends CardShell {
|
|
|
59
102
|
preventIdleSleep: CardFieldState
|
|
60
103
|
/** Subtask depth limit field. */
|
|
61
104
|
maxSubtaskDepth: CardFieldState
|
|
105
|
+
/** Goal-acceptance switch. */
|
|
106
|
+
goalVerification: CardFieldState
|
|
107
|
+
/** Judge model route field. */
|
|
108
|
+
goalVerificationModel: CardFieldState
|
|
109
|
+
/** Judge reasoning-effort field. */
|
|
110
|
+
goalVerificationReasoningEffort: CardFieldState
|
|
62
111
|
}
|
|
63
112
|
|
|
64
113
|
/** The registration-side face the card's slot entry injects. */
|
|
@@ -67,6 +116,8 @@ export interface TaskBoardSettingsCardFace extends CardActions {
|
|
|
67
116
|
/** Card snapshot bound by the renderer as useTaskBoardSettingsCard. */
|
|
68
117
|
taskBoardSettingsCard: SnapshotStore<TaskBoardSettingsCardState>
|
|
69
118
|
}
|
|
119
|
+
/** Deliver one provider action over the board's same-origin channel. */
|
|
120
|
+
dispatch: TaskBoardExtensionDispatch
|
|
70
121
|
}
|
|
71
122
|
|
|
72
123
|
/** Bridges the `task-board` settings form onto the card's staged form. */
|
|
@@ -75,12 +126,15 @@ export class TaskBoardSettingsCardController {
|
|
|
75
126
|
private readonly store: SnapshotStore<TaskBoardSettingsCardState>
|
|
76
127
|
|
|
77
128
|
/** @param scope - the bound configuration form for the `task-board` namespace. */
|
|
78
|
-
constructor(scope: ConfigForm<TaskBoardSettings
|
|
129
|
+
constructor(scope: ConfigForm<TaskBoardSettings>, private readonly dispatch: TaskBoardExtensionDispatch) {
|
|
79
130
|
this.form = new CardForm(scope, [
|
|
80
131
|
booleanField('enabled'),
|
|
81
132
|
booleanField('announceToAgent'),
|
|
82
133
|
booleanField('preventIdleSleep'),
|
|
83
134
|
subtaskDepthField(),
|
|
135
|
+
booleanField('goalVerification'),
|
|
136
|
+
judgeModelField(),
|
|
137
|
+
judgeEffortField(),
|
|
84
138
|
])
|
|
85
139
|
this.store = this.form.bind(() => this.projection())
|
|
86
140
|
}
|
|
@@ -92,6 +146,9 @@ export class TaskBoardSettingsCardController {
|
|
|
92
146
|
announceToAgent: this.form.field('announceToAgent'),
|
|
93
147
|
preventIdleSleep: this.form.field('preventIdleSleep'),
|
|
94
148
|
maxSubtaskDepth: this.form.field('maxSubtaskDepth'),
|
|
149
|
+
goalVerification: this.form.field('goalVerification'),
|
|
150
|
+
goalVerificationModel: this.form.field('goalVerificationModel'),
|
|
151
|
+
goalVerificationReasoningEffort: this.form.field('goalVerificationReasoningEffort'),
|
|
95
152
|
}
|
|
96
153
|
}
|
|
97
154
|
|
|
@@ -100,7 +157,7 @@ export class TaskBoardSettingsCardController {
|
|
|
100
157
|
* @returns the card's snapshot and its form actions.
|
|
101
158
|
*/
|
|
102
159
|
inject(): TaskBoardSettingsCardFace {
|
|
103
|
-
return { hooks: { taskBoardSettingsCard: this.store }, ...this.form.actions() }
|
|
160
|
+
return { hooks: { taskBoardSettingsCard: this.store }, ...this.form.actions(), dispatch: this.dispatch }
|
|
104
161
|
}
|
|
105
162
|
|
|
106
163
|
/**
|
|
@@ -117,6 +174,18 @@ export type TaskBoardSettingsCardProps =
|
|
|
117
174
|
PropsRuntime<'web-ui.plugin.item'>
|
|
118
175
|
& PropsLocale<'task-board'>
|
|
119
176
|
& InjectFace<TaskBoardSettingsCardFace>
|
|
177
|
+
& PropsRenderSlots<'task-board.settings.section'>
|
|
178
|
+
|
|
179
|
+
/** Kanban mark for the card header: three columns of unequal depth. */
|
|
180
|
+
function BoardMark() {
|
|
181
|
+
return (
|
|
182
|
+
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
|
183
|
+
<rect x="2.2" y="2.6" width="3.9" height="12.8" rx="1.95" fill="currentColor" />
|
|
184
|
+
<rect x="7.05" y="2.6" width="3.9" height="7.6" rx="1.95" fill="currentColor" />
|
|
185
|
+
<rect x="11.9" y="2.6" width="3.9" height="10.2" rx="1.95" fill="currentColor" />
|
|
186
|
+
</svg>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
120
189
|
|
|
121
190
|
/**
|
|
122
191
|
* Render the task-board card.
|
|
@@ -124,15 +193,22 @@ export type TaskBoardSettingsCardProps =
|
|
|
124
193
|
* @returns the card.
|
|
125
194
|
*/
|
|
126
195
|
export function TaskBoardSettingsCard(props: TaskBoardSettingsCardProps) {
|
|
127
|
-
const { t } = props
|
|
196
|
+
const { t, renderSlot, dispatch } = props
|
|
128
197
|
const state = props.useTaskBoardSettingsCard(snapshot => snapshot)
|
|
129
198
|
const disabled = !state.writable
|
|
130
199
|
const [power, setPower] = useState<TaskBoardPowerSnapshot | undefined>()
|
|
200
|
+
const [verification, setVerification] = useState<TaskBoardVerificationOptions | undefined>()
|
|
131
201
|
useEffect(() => {
|
|
132
202
|
// The SSE channel already carries power on every real change and pushes
|
|
133
203
|
// one frame on subscribe; polling the full /state snapshot every 5 s
|
|
134
204
|
// re-cloned and re-serialized the whole ledger server-side for one field.
|
|
135
205
|
let live = true
|
|
206
|
+
void fetch('api/task-board/verification')
|
|
207
|
+
.then(r => r.ok ? r.json() : undefined)
|
|
208
|
+
.then((data: TaskBoardVerificationOptions | undefined) => {
|
|
209
|
+
if (data?.catalog && live) setVerification(data)
|
|
210
|
+
})
|
|
211
|
+
.catch(() => {})
|
|
136
212
|
const events = new EventSource('api/task-board/events')
|
|
137
213
|
events.onmessage = (message: MessageEvent<string>): void => {
|
|
138
214
|
try {
|
|
@@ -144,17 +220,69 @@ export function TaskBoardSettingsCard(props: TaskBoardSettingsCardProps) {
|
|
|
144
220
|
}
|
|
145
221
|
return () => { live = false; events.close() }
|
|
146
222
|
}, [])
|
|
223
|
+
// The staged drafts drive the resolved preview, so an unsaved model or level
|
|
224
|
+
// change already shows what the next execution would freeze (including a
|
|
225
|
+
// reasoning-level fallback the target model forces).
|
|
226
|
+
const catalog: ModelCatalogView = verification?.catalog ?? { groups: [] }
|
|
227
|
+
const stagedSettings: VerificationSettings = {
|
|
228
|
+
enabled: state.goalVerification.text !== 'false',
|
|
229
|
+
model: state.goalVerificationModel.text,
|
|
230
|
+
reasoningEffort: state.goalVerificationReasoningEffort.text,
|
|
231
|
+
}
|
|
232
|
+
const preview = resolveContract(stagedSettings, catalog)
|
|
233
|
+
const inheritRoute = catalog.default
|
|
234
|
+
const modelChoices = [
|
|
235
|
+
{
|
|
236
|
+
value: '',
|
|
237
|
+
label: inheritRoute === undefined
|
|
238
|
+
? t('settings.goalVerificationModelInheritUnknown')
|
|
239
|
+
: t('settings.goalVerificationModelInherit', { model: inheritRoute.provider + '/' + inheritRoute.model }),
|
|
240
|
+
},
|
|
241
|
+
...catalog.groups.flatMap(group => group.models.map(model => ({
|
|
242
|
+
value: group.id + '/' + model.id,
|
|
243
|
+
label: (group.name ?? group.id) + ' · ' + (model.name ?? model.id),
|
|
244
|
+
}))),
|
|
245
|
+
]
|
|
246
|
+
const stagedRoute = parseModelRoute(stagedSettings.model) ?? (inheritRoute === undefined ? undefined : { provider: inheritRoute.provider, model: inheritRoute.model })
|
|
247
|
+
const stagedEfforts = stagedRoute === undefined
|
|
248
|
+
? []
|
|
249
|
+
: catalog.groups.find(group => group.id === stagedRoute.provider)?.models.find(model => model.id === stagedRoute.model)?.reasoning?.efforts ?? []
|
|
250
|
+
const effortChoices = [
|
|
251
|
+
{
|
|
252
|
+
value: '',
|
|
253
|
+
label: stagedRoute === undefined || stagedEfforts.length === 0
|
|
254
|
+
? t('settings.goalVerificationEffortInheritUnknown')
|
|
255
|
+
: t('settings.goalVerificationEffortInherit', {
|
|
256
|
+
effort: catalog.groups.find(group => group.id === stagedRoute.provider)?.models
|
|
257
|
+
.find(model => model.id === stagedRoute.model)?.reasoning?.defaultEffort ?? t('settings.goalVerificationEffortInheritUnknown'),
|
|
258
|
+
}),
|
|
259
|
+
},
|
|
260
|
+
...stagedEfforts.map(effort => ({ value: effort.id, label: effort.name ?? effort.id })),
|
|
261
|
+
]
|
|
147
262
|
const fieldProps = {
|
|
148
263
|
overriddenLabel: t('settings.overridden'),
|
|
149
264
|
resetLabel: t('settings.reset'),
|
|
150
265
|
invalidLabel: t('settings.invalidNumber'),
|
|
151
266
|
disabled,
|
|
152
267
|
}
|
|
268
|
+
// Chrome state of the nested disclosure cards below: they stage into this
|
|
269
|
+
// card's form and are written by its one save, so they carry no footer and no
|
|
270
|
+
// unsaved pill of their own (the outer header owns the pill).
|
|
271
|
+
const nestedShell: CardShell = {
|
|
272
|
+
available: true,
|
|
273
|
+
exposed: true,
|
|
274
|
+
writable: state.writable,
|
|
275
|
+
dirty: false,
|
|
276
|
+
invalid: state.invalid,
|
|
277
|
+
saving: state.saving,
|
|
278
|
+
failed: false,
|
|
279
|
+
}
|
|
153
280
|
return (
|
|
154
281
|
<PluginSettingsCard
|
|
155
282
|
t={t}
|
|
156
283
|
titleKey="settings.title"
|
|
157
284
|
descriptionKey="settings.description"
|
|
285
|
+
icon={<BoardMark />}
|
|
158
286
|
defaultOpen={false}
|
|
159
287
|
state={state}
|
|
160
288
|
renderChildrenWhenNotExposed
|
|
@@ -162,66 +290,177 @@ export function TaskBoardSettingsCard(props: TaskBoardSettingsCardProps) {
|
|
|
162
290
|
onSave={props.save}
|
|
163
291
|
onDiscard={props.discard}
|
|
164
292
|
>
|
|
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
|
-
|
|
293
|
+
{/* The board's own settings are nested disclosure cards, so an expanded
|
|
294
|
+
card reads as a short topic list: the board and its runtime behavior,
|
|
295
|
+
goal acceptance, and whatever a provider contributes (a provider
|
|
296
|
+
extension registers into the seat rendered last). Every one
|
|
297
|
+
of them starts collapsed and shares this card's single save. */}
|
|
298
|
+
<ul className={settingsCss.nestedCards}>
|
|
299
|
+
<PluginSettingsCard
|
|
300
|
+
t={t}
|
|
301
|
+
titleKey="settings.enabled"
|
|
302
|
+
descriptionKey="settings.enabledCardHint"
|
|
303
|
+
defaultOpen={false}
|
|
304
|
+
hideFooter
|
|
305
|
+
state={nestedShell}
|
|
306
|
+
onSave={props.save}
|
|
307
|
+
onDiscard={props.discard}
|
|
308
|
+
>
|
|
309
|
+
<BooleanField
|
|
310
|
+
id="settings-task-board-enabled"
|
|
311
|
+
label={t('settings.enabled')}
|
|
312
|
+
hint={t('settings.enabledHint')}
|
|
313
|
+
inheritLabel={t('settings.inherit')}
|
|
314
|
+
onLabel={t('settings.on')}
|
|
315
|
+
offLabel={t('settings.off')}
|
|
316
|
+
{...fieldProps}
|
|
317
|
+
{...state.enabled}
|
|
318
|
+
onEdit={(text) => { props.edit('enabled', text) }}
|
|
319
|
+
onReset={() => { props.resetField('enabled') }}
|
|
320
|
+
/>
|
|
321
|
+
<BooleanField
|
|
322
|
+
id="settings-task-board-announce"
|
|
323
|
+
label={t('settings.announceToAgent')}
|
|
324
|
+
hint={t('settings.announceToAgentHint')}
|
|
325
|
+
inheritLabel={t('settings.inherit')}
|
|
326
|
+
onLabel={t('settings.on')}
|
|
327
|
+
offLabel={t('settings.off')}
|
|
328
|
+
{...fieldProps}
|
|
329
|
+
{...state.announceToAgent}
|
|
330
|
+
onEdit={(text) => { props.edit('announceToAgent', text) }}
|
|
331
|
+
onReset={() => { props.resetField('announceToAgent') }}
|
|
332
|
+
/>
|
|
333
|
+
<BooleanField
|
|
334
|
+
id="settings-task-board-prevent-idle-sleep"
|
|
335
|
+
label={t('settings.preventIdleSleep')}
|
|
336
|
+
hint={t('settings.preventIdleSleepHint')}
|
|
337
|
+
inheritLabel={t('settings.inherit')}
|
|
338
|
+
onLabel={t('settings.on')}
|
|
339
|
+
offLabel={t('settings.off')}
|
|
340
|
+
{...fieldProps}
|
|
341
|
+
{...state.preventIdleSleep}
|
|
342
|
+
onEdit={(text) => { props.edit('preventIdleSleep', text) }}
|
|
343
|
+
onReset={() => { props.resetField('preventIdleSleep') }}
|
|
344
|
+
/>
|
|
345
|
+
<ChoiceField
|
|
346
|
+
id="settings-task-board-subtask-depth"
|
|
347
|
+
label={t('settings.maxSubtaskDepth')}
|
|
348
|
+
hint={t('settings.maxSubtaskDepthHint')}
|
|
349
|
+
inheritLabel={t('settings.inherit')}
|
|
350
|
+
choices={SUBTASK_DEPTH_CHOICES.map(value => ({
|
|
351
|
+
value,
|
|
352
|
+
label: t('settings.maxSubtaskDepthOption', { depth: value }),
|
|
353
|
+
}))}
|
|
354
|
+
{...fieldProps}
|
|
355
|
+
{...state.maxSubtaskDepth}
|
|
356
|
+
onEdit={(text) => { props.edit('maxSubtaskDepth', text) }}
|
|
357
|
+
onReset={() => { props.resetField('maxSubtaskDepth') }}
|
|
358
|
+
/>
|
|
359
|
+
</PluginSettingsCard>
|
|
360
|
+
|
|
361
|
+
<PluginSettingsCard
|
|
362
|
+
t={t}
|
|
363
|
+
titleKey="settings.goalVerificationTitle"
|
|
364
|
+
descriptionKey="settings.goalVerificationCardHint"
|
|
365
|
+
defaultOpen={false}
|
|
366
|
+
hideFooter
|
|
367
|
+
state={nestedShell}
|
|
368
|
+
onSave={props.save}
|
|
369
|
+
onDiscard={props.discard}
|
|
370
|
+
>
|
|
371
|
+
<BooleanField
|
|
372
|
+
id="settings-task-board-goal-verification"
|
|
373
|
+
label={t('settings.goalVerification')}
|
|
374
|
+
hint={t('settings.goalVerificationHint')}
|
|
375
|
+
inheritLabel={t('settings.inherit')}
|
|
376
|
+
onLabel={t('settings.on')}
|
|
377
|
+
offLabel={t('settings.off')}
|
|
378
|
+
{...fieldProps}
|
|
379
|
+
{...state.goalVerification}
|
|
380
|
+
onEdit={(text) => { props.edit('goalVerification', text) }}
|
|
381
|
+
onReset={() => { props.resetField('goalVerification') }}
|
|
382
|
+
/>
|
|
383
|
+
<ChoiceField
|
|
384
|
+
id="settings-task-board-goal-verification-model"
|
|
385
|
+
label={t('settings.goalVerificationModel')}
|
|
386
|
+
hint={t('settings.goalVerificationModelHint')}
|
|
387
|
+
inheritLabel={t('settings.inherit')}
|
|
388
|
+
choices={modelChoices}
|
|
389
|
+
{...fieldProps}
|
|
390
|
+
{...state.goalVerificationModel}
|
|
391
|
+
onEdit={(text) => { props.edit('goalVerificationModel', text) }}
|
|
392
|
+
onReset={() => { props.resetField('goalVerificationModel') }}
|
|
393
|
+
/>
|
|
394
|
+
<ChoiceField
|
|
395
|
+
id="settings-task-board-goal-verification-effort"
|
|
396
|
+
label={t('settings.goalVerificationEffort')}
|
|
397
|
+
hint={t('settings.goalVerificationEffortHint')}
|
|
398
|
+
inheritLabel={t('settings.inherit')}
|
|
399
|
+
choices={effortChoices}
|
|
400
|
+
{...fieldProps}
|
|
401
|
+
{...state.goalVerificationReasoningEffort}
|
|
402
|
+
onEdit={(text) => { props.edit('goalVerificationReasoningEffort', text) }}
|
|
403
|
+
onReset={() => { props.resetField('goalVerificationReasoningEffort') }}
|
|
404
|
+
/>
|
|
405
|
+
<p className={settingsCss.note}>{t('settings.goalVerificationResolved')}</p>
|
|
406
|
+
{preview.route === undefined
|
|
407
|
+
? <p className={settingsCss.error}>{t('settings.goalVerificationRouteMissing')}</p>
|
|
408
|
+
: (
|
|
409
|
+
<ul className={settingsCss.resolvedList}>
|
|
410
|
+
<li>{t('settings.goalVerificationResolvedModel', { model: preview.route.provider + '/' + preview.route.model })}</li>
|
|
411
|
+
<li>
|
|
412
|
+
{preview.route.reasoningEffort === undefined
|
|
413
|
+
? t('settings.goalVerificationResolvedNoEffort')
|
|
414
|
+
: t('settings.goalVerificationResolvedEffort', { effort: preview.route.reasoningEffort })}
|
|
415
|
+
</li>
|
|
416
|
+
<li>
|
|
417
|
+
{t('settings.goalVerificationResolvedSource', {
|
|
418
|
+
source: preview.modelSource === 'inherit'
|
|
419
|
+
? t('settings.goalVerificationSourceInherit')
|
|
420
|
+
: t('settings.goalVerificationSourceExplicit'),
|
|
421
|
+
})}
|
|
422
|
+
</li>
|
|
423
|
+
<li>{t('settings.goalVerificationResolvedPreset', { threshold: String(preview.threshold) })}</li>
|
|
424
|
+
</ul>
|
|
425
|
+
)}
|
|
426
|
+
{preview.effortFallback !== undefined && (
|
|
427
|
+
<p className={settingsCss.note}>
|
|
428
|
+
{preview.effortFallback.resolved === undefined
|
|
429
|
+
? t('settings.goalVerificationEffortFallbackNone', { requested: preview.effortFallback.requested })
|
|
430
|
+
: t('settings.goalVerificationEffortFallback', {
|
|
431
|
+
requested: preview.effortFallback.requested,
|
|
432
|
+
resolved: preview.effortFallback.resolved,
|
|
433
|
+
})}
|
|
434
|
+
</p>
|
|
435
|
+
)}
|
|
436
|
+
{stagedSettings.model.trim() !== '' && parseModelRoute(stagedSettings.model) === undefined && (
|
|
437
|
+
<p className={settingsCss.note}>{t('settings.goalVerificationModelInvalid')}</p>
|
|
438
|
+
)}
|
|
439
|
+
</PluginSettingsCard>
|
|
440
|
+
|
|
441
|
+
{/* Provider sections: a family extension contributes its own nested
|
|
442
|
+
settings card into this list through the seat this card declares —
|
|
443
|
+
the family provider extension is the one that does. Each
|
|
444
|
+
contributed card keeps its own chrome and save row, and an empty
|
|
445
|
+
seat draws nothing. */}
|
|
446
|
+
{renderSlot('task-board.settings.section', { dispatch })}
|
|
447
|
+
</ul>
|
|
448
|
+
{/* Live power facts and the battery caveat: small print under the topic
|
|
449
|
+
cards, separated from them, instead of two body-size paragraphs. */}
|
|
450
|
+
<div className={settingsCss.power}>
|
|
451
|
+
<p className={settingsCss.powerLine}>
|
|
452
|
+
{t('settings.powerStatus', {
|
|
453
|
+
platform: power?.platform ?? t('settings.powerUnknown'),
|
|
454
|
+
phase: power?.phase ?? t('settings.powerUnknown'),
|
|
455
|
+
running: String(power?.runningSessions ?? 0),
|
|
456
|
+
schedules: String(power?.armedSchedules ?? 0),
|
|
457
|
+
})}
|
|
458
|
+
</p>
|
|
459
|
+
<p className={settingsCss.powerLine}>{t('settings.powerBoundary')}</p>
|
|
460
|
+
{power?.lastError !== undefined && (
|
|
461
|
+
<p className={settingsCss.powerError}>{t('settings.powerError', { error: power.lastError })}</p>
|
|
462
|
+
)}
|
|
463
|
+
</div>
|
|
225
464
|
</PluginSettingsCard>
|
|
226
465
|
)
|
|
227
466
|
}
|
|
@@ -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}>
|
|
@@ -10,9 +10,10 @@ import type { BoardController } from '../../core/controller.ts'
|
|
|
10
10
|
import { collectKnownTags, type TaskRecord, type TaskTag } from '../../core/tasks.ts'
|
|
11
11
|
import { t } from '../locales.ts'
|
|
12
12
|
import { ModalShell, TaskContentFields, TaskTagFields, cleanTags } from './TaskForm.tsx'
|
|
13
|
+
import type { OverlayPhase } from './overlay.tsx'
|
|
13
14
|
|
|
14
15
|
/** Edit-task form overlay. */
|
|
15
|
-
export function EditTaskModal({ controller, task, onClose }: { controller: BoardController; task: TaskRecord; onClose: () => void }) {
|
|
16
|
+
export function EditTaskModal({ controller, task, onClose, phase }: { controller: BoardController; task: TaskRecord; onClose: () => void; phase?: OverlayPhase }) {
|
|
16
17
|
const [title, setTitle] = useState(task.title)
|
|
17
18
|
const [description, setDescription] = useState(task.description)
|
|
18
19
|
const [prompt, setPrompt] = useState(task.prompt)
|
|
@@ -55,6 +56,7 @@ export function EditTaskModal({ controller, task, onClose }: { controller: Board
|
|
|
55
56
|
submitLabel={t('edit.save')}
|
|
56
57
|
onSubmit={() => { void submit() }}
|
|
57
58
|
onClose={onClose}
|
|
59
|
+
phase={phase}
|
|
58
60
|
>
|
|
59
61
|
<TaskContentFields
|
|
60
62
|
title={title}
|
|
@@ -71,7 +73,7 @@ export function EditTaskModal({ controller, task, onClose }: { controller: Board
|
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
/** Edit-tags modal: edit labels only, shown for tasks after first execution. */
|
|
74
|
-
export function EditTagsModal({ controller, task, onClose }: { controller: BoardController; task: TaskRecord; onClose: () => void }) {
|
|
76
|
+
export function EditTagsModal({ controller, task, onClose, phase }: { controller: BoardController; task: TaskRecord; onClose: () => void; phase?: OverlayPhase }) {
|
|
75
77
|
const [tags, setTags] = useState<TaskTag[]>(task.tags ?? [])
|
|
76
78
|
const [error, setError] = useState<string | undefined>(undefined)
|
|
77
79
|
const [pending, setPending] = useState(false)
|
|
@@ -99,6 +101,7 @@ export function EditTagsModal({ controller, task, onClose }: { controller: Board
|
|
|
99
101
|
submitLabel={t('edit.save')}
|
|
100
102
|
onSubmit={() => { void submit() }}
|
|
101
103
|
onClose={onClose}
|
|
104
|
+
phase={phase}
|
|
102
105
|
>
|
|
103
106
|
<TaskTagFields tags={tags} knownTags={collectKnownTags(controller.getSnapshot().tasks)} onChange={setTags} />
|
|
104
107
|
</ModalShell>
|