@fonlan/dsh-task-kanban 0.2.0 → 0.2.1
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.md +12 -0
- package/lib/client.js +282 -69
- package/lib/client.js.map +1 -1
- package/lib/index.js +140 -23
- package/lib/types/server/models.d.ts +16 -0
- package/lib/types/server/runner.d.ts +18 -1
- package/lib/types/server/settings.d.ts +35 -7
- package/lib/types/shared/card.d.ts +12 -1
- package/package.json +5 -1
- package/src/client/api.ts +19 -1
- package/src/client/board.css +61 -6
- package/src/client/board.tsx +0 -7
- package/src/client/index.tsx +11 -9
- package/src/client/locales.ts +16 -2
- package/src/client/sections.tsx +244 -48
- package/src/server/models.ts +40 -0
- package/src/server/rpc.ts +33 -4
- package/src/server/runner.ts +71 -18
- package/src/server/settings.ts +52 -7
- package/src/shared/card.ts +13 -1
package/src/client/sections.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react'
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
2
|
import { IconChecklistOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
3
|
-
import { api } from './api'
|
|
3
|
+
import { api, type PresetOption, type ReasoningOptions } from './api'
|
|
4
4
|
import { toggleBoard, useBoardOpen } from './kanban-state'
|
|
5
|
-
import type { KanbanSettingsShape } from '../shared/card'
|
|
5
|
+
import type { KanbanSettingsShape, ModelOption } from '../shared/card'
|
|
6
6
|
|
|
7
7
|
interface FooterActionProps {
|
|
8
8
|
wide?: boolean
|
|
@@ -140,17 +140,141 @@ export function KanbanFooterButton({ wide, t }: FooterActionProps): JSX.Element
|
|
|
140
140
|
)
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
interface
|
|
143
|
+
interface SettingsCardProps {
|
|
144
144
|
t?: (key: string) => string
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
/**
|
|
148
|
-
|
|
147
|
+
/** Fallback shape while settings load (server always resolves the full set). */
|
|
148
|
+
const EMPTY_SETTINGS: KanbanSettingsShape = {
|
|
149
|
+
maxParallelWorkers: 1,
|
|
150
|
+
refinementModel: '',
|
|
151
|
+
refinementProvider: '',
|
|
152
|
+
refinementReasoningEffort: '',
|
|
153
|
+
refinementPreset: '',
|
|
154
|
+
phaseModel: '',
|
|
155
|
+
phaseProvider: '',
|
|
156
|
+
phaseReasoningEffort: '',
|
|
157
|
+
phasePreset: '',
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The setting field names of one session-type group. */
|
|
161
|
+
const GROUP_FIELDS = {
|
|
162
|
+
refinement: {
|
|
163
|
+
model: 'refinementModel',
|
|
164
|
+
provider: 'refinementProvider',
|
|
165
|
+
effort: 'refinementReasoningEffort',
|
|
166
|
+
preset: 'refinementPreset',
|
|
167
|
+
},
|
|
168
|
+
phase: {
|
|
169
|
+
model: 'phaseModel',
|
|
170
|
+
provider: 'phaseProvider',
|
|
171
|
+
effort: 'phaseReasoningEffort',
|
|
172
|
+
preset: 'phasePreset',
|
|
173
|
+
},
|
|
174
|
+
} as const
|
|
175
|
+
|
|
176
|
+
type SessionGroupKey = keyof typeof GROUP_FIELDS
|
|
177
|
+
|
|
178
|
+
interface SessionDefaultsGroupProps {
|
|
179
|
+
kind: SessionGroupKey
|
|
180
|
+
settings: KanbanSettingsShape
|
|
181
|
+
models: ModelOption[]
|
|
182
|
+
presets: PresetOption[]
|
|
183
|
+
/** Reasoning efforts already fetched, keyed `provider/model`. */
|
|
184
|
+
efforts: Record<string, ReasoningOptions>
|
|
185
|
+
tr: (key: string) => string
|
|
186
|
+
onCommit: (patch: Partial<KanbanSettingsShape>) => Promise<void>
|
|
187
|
+
loadEfforts: (provider: string, model: string) => void
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Model / reasoning effort / agent preset defaults for one session type. */
|
|
191
|
+
function SessionDefaultsGroup({ kind, settings, models, presets, efforts, tr, onCommit, loadEfforts }: SessionDefaultsGroupProps): JSX.Element {
|
|
192
|
+
const fields = GROUP_FIELDS[kind]
|
|
193
|
+
const model = settings[fields.model]
|
|
194
|
+
const provider = settings[fields.provider]
|
|
195
|
+
const effort = settings[fields.effort]
|
|
196
|
+
const preset = settings[fields.preset]
|
|
197
|
+
const effortKey = model !== '' && provider !== '' ? `${provider}/${model}` : ''
|
|
198
|
+
const effortOptions = effortKey !== '' ? (efforts[effortKey]?.efforts ?? []) : []
|
|
199
|
+
|
|
200
|
+
const handleModel = (id: string): void => {
|
|
201
|
+
const found = models.find((m) => m.id === id)
|
|
202
|
+
const nextProvider = found?.provider ?? ''
|
|
203
|
+
void onCommit({ [fields.model]: id, [fields.provider]: nextProvider } as Partial<KanbanSettingsShape>)
|
|
204
|
+
if (id !== '' && nextProvider !== '') loadEfforts(nextProvider, id)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<div className="kb-settings-group">
|
|
209
|
+
<div className="kb-settings-group-title">{tr(kind === 'refinement' ? 'settingsRefineGroup' : 'settingsPhaseGroup')}</div>
|
|
210
|
+
<label className="kb-field kb-field-row">
|
|
211
|
+
<span>{tr('model')}</span>
|
|
212
|
+
<select className="kb-input" value={model} onChange={(e) => handleModel(e.target.value)}>
|
|
213
|
+
<option value="">{tr('defaultOption')}</option>
|
|
214
|
+
{models.map((m) => (
|
|
215
|
+
<option key={m.provider + '/' + m.id} value={m.id}>{m.name ?? m.id}</option>
|
|
216
|
+
))}
|
|
217
|
+
</select>
|
|
218
|
+
</label>
|
|
219
|
+
<label className="kb-field kb-field-row">
|
|
220
|
+
<span>{tr('reasoningEffort')}</span>
|
|
221
|
+
<select
|
|
222
|
+
className="kb-input"
|
|
223
|
+
value={effort}
|
|
224
|
+
disabled={model === ''}
|
|
225
|
+
onChange={(e) => void onCommit({ [fields.effort]: e.target.value } as Partial<KanbanSettingsShape>)}
|
|
226
|
+
>
|
|
227
|
+
<option value="">{tr('defaultOption')}</option>
|
|
228
|
+
{effortOptions.map((o) => (
|
|
229
|
+
<option key={o.id} value={o.id}>{o.name}</option>
|
|
230
|
+
))}
|
|
231
|
+
</select>
|
|
232
|
+
</label>
|
|
233
|
+
<label className="kb-field kb-field-row">
|
|
234
|
+
<span>{tr('agentPreset')}</span>
|
|
235
|
+
<select
|
|
236
|
+
className="kb-input"
|
|
237
|
+
value={preset}
|
|
238
|
+
onChange={(e) => void onCommit({ [fields.preset]: e.target.value } as Partial<KanbanSettingsShape>)}
|
|
239
|
+
>
|
|
240
|
+
<option value="">{tr('defaultOption')}</option>
|
|
241
|
+
{presets.map((p) => (
|
|
242
|
+
<option key={p.id} value={p.id}>{p.name ?? p.id}</option>
|
|
243
|
+
))}
|
|
244
|
+
</select>
|
|
245
|
+
</label>
|
|
246
|
+
</div>
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Plugin settings card (设置 → 插件配置): workers + default model + per-session-type defaults. */
|
|
251
|
+
export function KanbanSettingsCard({ t }: SettingsCardProps): JSX.Element {
|
|
149
252
|
const tr = t ?? ((key: string) => key)
|
|
150
|
-
|
|
151
|
-
const [
|
|
253
|
+
// Card-local disclosure: collapsed by default, like the built-in plugin cards.
|
|
254
|
+
const [open, setOpen] = useState(false)
|
|
255
|
+
const [settings, setSettings] = useState<KanbanSettingsShape>(EMPTY_SETTINGS)
|
|
256
|
+
const [models, setModels] = useState<ModelOption[]>([])
|
|
257
|
+
const [presets, setPresets] = useState<PresetOption[]>([])
|
|
258
|
+
const [efforts, setEfforts] = useState<Record<string, ReasoningOptions>>({})
|
|
152
259
|
const [saved, setSaved] = useState(false)
|
|
153
260
|
|
|
261
|
+
const commit = useCallback(async (patch: Partial<KanbanSettingsShape>): Promise<void> => {
|
|
262
|
+
try {
|
|
263
|
+
const next = await api.settingsSet(patch)
|
|
264
|
+
setSettings(next)
|
|
265
|
+
setSaved(true)
|
|
266
|
+
window.setTimeout(() => setSaved(false), 1500)
|
|
267
|
+
} catch {
|
|
268
|
+
// keep the previous value
|
|
269
|
+
}
|
|
270
|
+
}, [])
|
|
271
|
+
|
|
272
|
+
// Refs so async callbacks (effort fetch) read the freshest settings/commit.
|
|
273
|
+
const settingsRef = useRef(settings)
|
|
274
|
+
settingsRef.current = settings
|
|
275
|
+
const commitRef = useRef(commit)
|
|
276
|
+
commitRef.current = commit
|
|
277
|
+
|
|
154
278
|
useEffect(() => {
|
|
155
279
|
let alive = true
|
|
156
280
|
api.settingsGet()
|
|
@@ -159,51 +283,123 @@ export function KanbanSettingsSection({ t }: SettingsSectionProps): JSX.Element
|
|
|
159
283
|
api.models()
|
|
160
284
|
.then((list) => { if (alive) setModels(list) })
|
|
161
285
|
.catch(() => undefined)
|
|
286
|
+
api.presets()
|
|
287
|
+
.then((list) => { if (alive) setPresets(list) })
|
|
288
|
+
.catch(() => undefined)
|
|
162
289
|
return () => { alive = false }
|
|
163
290
|
}, [])
|
|
164
291
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
292
|
+
/** Reasoning routes already requested (dedupe across mount + model changes). */
|
|
293
|
+
const effortFetched = useRef<Set<string>>(new Set())
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Fetch the reasoning efforts of one model route (once per route). A failed
|
|
297
|
+
* fetch is marked `failed` so the stored effort is preserved (see the reset
|
|
298
|
+
* effect below) instead of being wiped by a transient error.
|
|
299
|
+
*/
|
|
300
|
+
const loadEfforts = useCallback((provider: string, model: string): void => {
|
|
301
|
+
const key = `${provider}/${model}`
|
|
302
|
+
if (effortFetched.current.has(key)) return
|
|
303
|
+
effortFetched.current.add(key)
|
|
304
|
+
api.reasoningOptions(provider, model)
|
|
305
|
+
.then((opts) => setEfforts((prev) => ({ ...prev, [key]: opts })))
|
|
306
|
+
.catch(() => setEfforts((prev) => ({ ...prev, [key]: { efforts: [], failed: true } })))
|
|
307
|
+
}, [])
|
|
308
|
+
|
|
309
|
+
// Fetch efforts for the models already configured in settings, so the
|
|
310
|
+
// effort selects show their levels (and validate the stored effort) as soon
|
|
311
|
+
// as settings load — the fetch previously ran only on model change.
|
|
312
|
+
useEffect(() => {
|
|
313
|
+
for (const kind of ['refinement', 'phase'] as const) {
|
|
314
|
+
const fields = GROUP_FIELDS[kind]
|
|
315
|
+
const model = settings[fields.model]
|
|
316
|
+
const provider = settings[fields.provider]
|
|
317
|
+
if (model !== '' && provider !== '') loadEfforts(provider, model)
|
|
173
318
|
}
|
|
174
|
-
}
|
|
319
|
+
}, [settings.refinementModel, settings.refinementProvider, settings.phaseModel, settings.phaseProvider, loadEfforts])
|
|
320
|
+
|
|
321
|
+
// When the efforts of the selected model arrive, drop a stored effort the
|
|
322
|
+
// model no longer offers (e.g. the user switched the model). A failed fetch
|
|
323
|
+
// (`failed: true`) keeps the stored effort: the model may just be
|
|
324
|
+
// temporarily unresolvable, and wiping the user's choice would be worse.
|
|
325
|
+
useEffect(() => {
|
|
326
|
+
const s = settingsRef.current
|
|
327
|
+
for (const kind of ['refinement', 'phase'] as const) {
|
|
328
|
+
const fields = GROUP_FIELDS[kind]
|
|
329
|
+
const model = s[fields.model]
|
|
330
|
+
const provider = s[fields.provider]
|
|
331
|
+
const effort = s[fields.effort]
|
|
332
|
+
if (effort === '' || model === '' || provider === '') continue
|
|
333
|
+
const opts = efforts[`${provider}/${model}`]
|
|
334
|
+
if (opts === undefined || opts.failed === true) continue
|
|
335
|
+
if (!opts.efforts.some((o) => o.id === effort)) {
|
|
336
|
+
void commitRef.current({ [fields.effort]: '' } as Partial<KanbanSettingsShape>)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}, [efforts])
|
|
175
340
|
|
|
176
341
|
return (
|
|
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
|
-
|
|
342
|
+
<li className="kb-settings-card" data-open={open ? '' : undefined}>
|
|
343
|
+
<button
|
|
344
|
+
type="button"
|
|
345
|
+
className="kb-settings-card-head"
|
|
346
|
+
aria-expanded={open}
|
|
347
|
+
aria-label={(open ? tr('settingsCollapse') : tr('settingsExpand')) + ':' + tr('settingsTitle')}
|
|
348
|
+
onClick={() => setOpen(!open)}
|
|
349
|
+
>
|
|
350
|
+
<span className="kb-settings-card-headText">
|
|
351
|
+
<span className="kb-settings-card-title">{tr('settingsTitle')}</span>
|
|
352
|
+
<span className="kb-settings-card-sub">{tr('settingsDescription')}</span>
|
|
353
|
+
</span>
|
|
354
|
+
<span className="kb-settings-card-chevron" data-open={open ? '' : undefined} aria-hidden="true">
|
|
355
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
|
356
|
+
<path d="M3.5 5.25L7 8.75L10.5 5.25" stroke="currentColor" strokeWidth="1.5"
|
|
357
|
+
strokeLinecap="round" strokeLinejoin="round" />
|
|
358
|
+
</svg>
|
|
359
|
+
</span>
|
|
360
|
+
</button>
|
|
361
|
+
{open && (
|
|
362
|
+
<div className="kb-settings-card-body">
|
|
363
|
+
<div className="kb-settings">
|
|
364
|
+
<label className="kb-field kb-field-row">
|
|
365
|
+
<span>{tr('maxParallelWorkers')}</span>
|
|
366
|
+
<input
|
|
367
|
+
className="kb-input kb-input-number"
|
|
368
|
+
type="number"
|
|
369
|
+
min={1}
|
|
370
|
+
step={1}
|
|
371
|
+
value={settings.maxParallelWorkers}
|
|
372
|
+
onChange={(e) => {
|
|
373
|
+
const value = Math.max(1, Math.floor(Number(e.target.value) || 1))
|
|
374
|
+
setSettings((s) => ({ ...s, maxParallelWorkers: value }))
|
|
375
|
+
void commit({ maxParallelWorkers: value })
|
|
376
|
+
}}
|
|
377
|
+
/>
|
|
378
|
+
</label>
|
|
379
|
+
<SessionDefaultsGroup
|
|
380
|
+
kind="refinement"
|
|
381
|
+
settings={settings}
|
|
382
|
+
models={models}
|
|
383
|
+
presets={presets}
|
|
384
|
+
efforts={efforts}
|
|
385
|
+
tr={tr}
|
|
386
|
+
onCommit={commit}
|
|
387
|
+
loadEfforts={loadEfforts}
|
|
388
|
+
/>
|
|
389
|
+
<SessionDefaultsGroup
|
|
390
|
+
kind="phase"
|
|
391
|
+
settings={settings}
|
|
392
|
+
models={models}
|
|
393
|
+
presets={presets}
|
|
394
|
+
efforts={efforts}
|
|
395
|
+
tr={tr}
|
|
396
|
+
onCommit={commit}
|
|
397
|
+
loadEfforts={loadEfforts}
|
|
398
|
+
/>
|
|
399
|
+
{saved && <div className="kb-settings-saved">{tr('saved')}</div>}
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
)}
|
|
403
|
+
</li>
|
|
208
404
|
)
|
|
209
405
|
}
|
package/src/server/models.ts
CHANGED
|
@@ -18,3 +18,43 @@ export async function listModels(ctx: Context): Promise<ModelOption[]> {
|
|
|
18
18
|
}
|
|
19
19
|
return out
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
/** One selectable reasoning effort of a model (display vocabulary for the settings picker). */
|
|
23
|
+
export interface ReasoningEffortOption {
|
|
24
|
+
id: string
|
|
25
|
+
name: string
|
|
26
|
+
description?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ReasoningOptions {
|
|
30
|
+
efforts: ReasoningEffortOption[]
|
|
31
|
+
defaultEffort?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the adapter-owned reasoning efforts of one exact provider/model
|
|
36
|
+
* route. On-demand (one model at a time) so the settings picker can offer the
|
|
37
|
+
* levels the selected model actually supports; failures yield an empty list.
|
|
38
|
+
*/
|
|
39
|
+
export async function reasoningOptions(ctx: Context, provider: string, model: string): Promise<ReasoningOptions> {
|
|
40
|
+
const llm = ctx.get('llm') as
|
|
41
|
+
| { resolveModelInfo(provider: string, model: string, signal?: AbortSignal): Promise<{
|
|
42
|
+
reasoning?: { efforts?: Array<{ id: unknown; name: string; description?: string }>; defaultEffort?: unknown }
|
|
43
|
+
}> }
|
|
44
|
+
| undefined
|
|
45
|
+
if (llm === undefined) return { efforts: [] }
|
|
46
|
+
try {
|
|
47
|
+
const info = await llm.resolveModelInfo(provider, model)
|
|
48
|
+
const efforts = (info.reasoning?.efforts ?? []).map((e) => ({
|
|
49
|
+
id: String(e.id),
|
|
50
|
+
name: e.name,
|
|
51
|
+
...(e.description !== undefined ? { description: e.description } : {}),
|
|
52
|
+
}))
|
|
53
|
+
return {
|
|
54
|
+
efforts,
|
|
55
|
+
...(info.reasoning?.defaultEffort !== undefined ? { defaultEffort: String(info.reasoning.defaultEffort) } : {}),
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
return { efforts: [] }
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/server/rpc.ts
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
7
7
|
import type { Context } from '@deepseek-ai/cordis'
|
|
8
8
|
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
9
|
-
import type { Lane } from '../shared/card.js'
|
|
9
|
+
import type { KanbanSettingsShape, Lane } from '../shared/card.js'
|
|
10
10
|
import type { KanbanRunner } from './runner.js'
|
|
11
11
|
import type { KanbanSettingsFace } from './settings.js'
|
|
12
|
-
import { listModels } from './models.js'
|
|
12
|
+
import { listModels, reasoningOptions } from './models.js'
|
|
13
13
|
|
|
14
14
|
const API_PREFIX = '/plugins/@fonlan/dsh-task-kanban/api'
|
|
15
15
|
|
|
@@ -152,16 +152,45 @@ export function registerApiRoutes(
|
|
|
152
152
|
|
|
153
153
|
route('settings.get', async () => settings.get())
|
|
154
154
|
|
|
155
|
+
/** Every settings field the client may write (per-session-type defaults included). */
|
|
156
|
+
const SETTING_STRING_FIELDS = [
|
|
157
|
+
'refinementModel',
|
|
158
|
+
'refinementProvider',
|
|
159
|
+
'refinementReasoningEffort',
|
|
160
|
+
'refinementPreset',
|
|
161
|
+
'phaseModel',
|
|
162
|
+
'phaseProvider',
|
|
163
|
+
'phaseReasoningEffort',
|
|
164
|
+
'phasePreset',
|
|
165
|
+
] as const
|
|
166
|
+
|
|
155
167
|
route('settings.set', async (p) => {
|
|
156
|
-
const patch:
|
|
168
|
+
const patch: Partial<KanbanSettingsShape> = {}
|
|
157
169
|
if (typeof p.maxParallelWorkers === 'number') patch.maxParallelWorkers = p.maxParallelWorkers
|
|
158
|
-
|
|
170
|
+
for (const key of SETTING_STRING_FIELDS) {
|
|
171
|
+
if (typeof p[key] === 'string') patch[key] = p[key]
|
|
172
|
+
}
|
|
159
173
|
await settings.update(patch)
|
|
160
174
|
return settings.get()
|
|
161
175
|
})
|
|
162
176
|
|
|
163
177
|
route('models.list', async () => listModels(ctx))
|
|
164
178
|
|
|
179
|
+
route('presets.list', async () => {
|
|
180
|
+
const presets = ctx.get('agentPresets') as
|
|
181
|
+
| { list(): Promise<Array<{ id: string; name?: string; description?: string }>> }
|
|
182
|
+
| undefined
|
|
183
|
+
if (presets === undefined) return []
|
|
184
|
+
const rows = await presets.list()
|
|
185
|
+
return rows.map((r) => ({ id: r.id, name: r.name, description: r.description }))
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
route('reasoning.options', async (p) => {
|
|
189
|
+
const provider = requireString(p.provider, 'provider')
|
|
190
|
+
const model = requireString(p.model, 'model')
|
|
191
|
+
return reasoningOptions(ctx, provider, model)
|
|
192
|
+
})
|
|
193
|
+
|
|
165
194
|
return () => {
|
|
166
195
|
for (const d of disposers) d()
|
|
167
196
|
}
|
package/src/server/runner.ts
CHANGED
|
@@ -10,9 +10,9 @@ import { appendFile, readFile } from 'node:fs/promises'
|
|
|
10
10
|
import type { Session, SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
|
|
11
11
|
import { join } from 'node:path'
|
|
12
12
|
import type { Context } from '@deepseek-ai/cordis'
|
|
13
|
-
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
14
|
-
import type
|
|
15
|
-
import type { KanbanCard, ErrorKind, Lane, Plan } from '../shared/card.js'
|
|
13
|
+
import { createUserMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
|
|
14
|
+
import { installModelSelection, type AgentHandle, type ModelSelectionRef } from '@deepseek-ai/dsh-agent'
|
|
15
|
+
import type { KanbanCard, ErrorKind, KanbanSessionKind, Lane, Plan } from '../shared/card.js'
|
|
16
16
|
import type { TaskStore } from './task-store.js'
|
|
17
17
|
import type { KanbanSettingsFace } from './settings.js'
|
|
18
18
|
import { currentBranch, detectBaseRef, hasStashMessage, isGitRepo, isTreeDirty, revParse, runGit, unmergedPaths } from './git.js'
|
|
@@ -425,7 +425,7 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
425
425
|
private async runRefinement(cardId: string, wsPath: string): Promise<void> {
|
|
426
426
|
const card = await this.store.get(wsPath, cardId)
|
|
427
427
|
if (card === undefined) return
|
|
428
|
-
const route = await this.modelRoute(cardId, wsPath)
|
|
428
|
+
const route = await this.modelRoute(cardId, wsPath, 'refine')
|
|
429
429
|
const sessionId = this.newSessionId()
|
|
430
430
|
this.sessionCards.set(sessionId, cardId)
|
|
431
431
|
await this.store.mutate(cardId, (c) => {
|
|
@@ -434,7 +434,7 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
434
434
|
c.sessions.refinement.push(sessionId)
|
|
435
435
|
}, wsPath)
|
|
436
436
|
try {
|
|
437
|
-
const handle = await this.createAgent(sessionId, wsPath, route)
|
|
437
|
+
const handle = await this.createAgent(sessionId, wsPath, route, 'refine')
|
|
438
438
|
this.keepHandle(cardId, handle)
|
|
439
439
|
await this.attachRefinementSession(wsPath, sessionId)
|
|
440
440
|
// The agent scope is the layer where preset skill providers live
|
|
@@ -556,18 +556,54 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
556
556
|
return { promise, dispose }
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
+
/**
|
|
560
|
+
* Create one kanban session agent. `kind` decides the per-session preset:
|
|
561
|
+
* refine/phase use their settings default (falling back to the roster's
|
|
562
|
+
* default preset when unset), merge keeps the historical 'standard' preset.
|
|
563
|
+
* When the kind defaults configure a reasoning effort, an
|
|
564
|
+
* `installModelSelection` ref couples the provider/model + effort into every
|
|
565
|
+
* prompt assembly and model request. Without a configured effort no
|
|
566
|
+
* selection is installed: `agentOptions` carries the route and the loop
|
|
567
|
+
* materializes the model's own default effort — the historical behavior.
|
|
568
|
+
*/
|
|
559
569
|
private async createAgent(
|
|
560
570
|
sessionId: SessionId,
|
|
561
571
|
cwd: string,
|
|
562
|
-
route: { provider?: string; model?: string },
|
|
572
|
+
route: { provider?: string; model?: string; reasoningEffort?: string },
|
|
573
|
+
kind: KanbanSessionKind,
|
|
563
574
|
) {
|
|
564
575
|
const agentOptions =
|
|
565
576
|
route.provider !== undefined && route.model !== undefined && route.model !== ''
|
|
566
577
|
? { provider: route.provider, model: route.model }
|
|
567
578
|
: undefined
|
|
579
|
+
const selectionRef: ModelSelectionRef = {
|
|
580
|
+
current:
|
|
581
|
+
route.provider !== undefined && route.model !== undefined && route.model !== ''
|
|
582
|
+
? {
|
|
583
|
+
provider: route.provider,
|
|
584
|
+
model: route.model,
|
|
585
|
+
...(route.reasoningEffort !== undefined && route.reasoningEffort !== ''
|
|
586
|
+
? { reasoningEffort: ReasoningEffortId(route.reasoningEffort) }
|
|
587
|
+
: {}),
|
|
588
|
+
}
|
|
589
|
+
: undefined,
|
|
590
|
+
assembled: undefined,
|
|
591
|
+
}
|
|
592
|
+
// Resolve the preset id BEFORE creation so the session header records the
|
|
593
|
+
// exact composition it runs under (a resumed kanban session then rebuilds
|
|
594
|
+
// the same preset instead of the deployment default).
|
|
595
|
+
let presetId: string | undefined
|
|
596
|
+
const presets = this.ctx.get('agentPresets') as
|
|
597
|
+
| { resolve(id?: string): Promise<{ id: string }> }
|
|
598
|
+
| undefined
|
|
599
|
+
if (presets !== undefined) {
|
|
600
|
+
const requested = kind === 'merge' ? 'standard' : this.settings.sessionDefaults(kind).preset
|
|
601
|
+
const resolved = await presets.resolve(requested === '' ? undefined : requested)
|
|
602
|
+
presetId = resolved.id
|
|
603
|
+
}
|
|
568
604
|
return this.ctx.agents.create({
|
|
569
605
|
sessionId,
|
|
570
|
-
meta: { cwd },
|
|
606
|
+
meta: { cwd, ...(presetId !== undefined ? { agentPreset: presetId } : {}) },
|
|
571
607
|
...(agentOptions !== undefined ? { agentOptions } : {}),
|
|
572
608
|
// Kanban tools are NOT registered here: `agent/session-start` (see
|
|
573
609
|
// start()) fires for both create and resume, keeping them available
|
|
@@ -577,10 +613,13 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
577
613
|
| { mount(agentCtx: unknown, id?: string): Promise<unknown> }
|
|
578
614
|
| undefined
|
|
579
615
|
if (agentPresets !== undefined) {
|
|
580
|
-
await agentPresets.mount(agentCtx,
|
|
616
|
+
await agentPresets.mount(agentCtx, presetId)
|
|
581
617
|
} else {
|
|
582
618
|
throw new Error('@fonlan/dsh-task-kanban: agent-presets service is not mounted')
|
|
583
619
|
}
|
|
620
|
+
if (selectionRef.current !== undefined && selectionRef.current.reasoningEffort !== undefined) {
|
|
621
|
+
installModelSelection(agentCtx, selectionRef)
|
|
622
|
+
}
|
|
584
623
|
},
|
|
585
624
|
})
|
|
586
625
|
}
|
|
@@ -619,8 +658,19 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
619
658
|
return undefined
|
|
620
659
|
}
|
|
621
660
|
|
|
622
|
-
/**
|
|
623
|
-
|
|
661
|
+
/**
|
|
662
|
+
* Resolve the provider+model route for an agent (both are required), plus
|
|
663
|
+
* the per-kind reasoning effort. Precedence per kind:
|
|
664
|
+
* 1. the card's explicit model (chosen at creation) — wins for every kind;
|
|
665
|
+
* 2. the session-kind defaults (refine/phase settings);
|
|
666
|
+
* 3. the host `agent-default-model` route.
|
|
667
|
+
* Merge sessions skip step 2 (no per-type defaults for them).
|
|
668
|
+
*/
|
|
669
|
+
private async modelRoute(
|
|
670
|
+
cardId: string,
|
|
671
|
+
wsPath: string,
|
|
672
|
+
kind: KanbanSessionKind,
|
|
673
|
+
): Promise<{ provider?: string; model?: string; reasoningEffort?: string }> {
|
|
624
674
|
const card = await this.store.get(wsPath, cardId)
|
|
625
675
|
if (card === undefined) return {}
|
|
626
676
|
if (card.model !== '' && card.provider !== undefined && card.provider !== '') {
|
|
@@ -630,10 +680,13 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
630
680
|
const fallback = this.settings.defaultModelRoute()
|
|
631
681
|
return fallback.provider !== undefined ? { provider: fallback.provider, model: card.model } : { model: card.model }
|
|
632
682
|
}
|
|
633
|
-
const
|
|
634
|
-
if (
|
|
635
|
-
|
|
636
|
-
|
|
683
|
+
const kindDefaults = this.settings.sessionDefaults(kind)
|
|
684
|
+
if (kindDefaults.model !== '') {
|
|
685
|
+
return {
|
|
686
|
+
...(kindDefaults.provider !== '' ? { provider: kindDefaults.provider } : {}),
|
|
687
|
+
model: kindDefaults.model,
|
|
688
|
+
...(kindDefaults.reasoningEffort !== '' ? { reasoningEffort: kindDefaults.reasoningEffort } : {}),
|
|
689
|
+
}
|
|
637
690
|
}
|
|
638
691
|
return this.settings.defaultModelRoute()
|
|
639
692
|
}
|
|
@@ -731,9 +784,9 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
731
784
|
c.error = undefined
|
|
732
785
|
c.sessions.phases.push({ phaseIndex, sessionId, startedAt: Date.now() })
|
|
733
786
|
}, wsPath)
|
|
734
|
-
const route = await this.modelRoute(cardId, wsPath)
|
|
787
|
+
const route = await this.modelRoute(cardId, wsPath, 'phase')
|
|
735
788
|
try {
|
|
736
|
-
const handle = await this.createAgent(sessionId, workdir, route)
|
|
789
|
+
const handle = await this.createAgent(sessionId, workdir, route, 'phase')
|
|
737
790
|
this.keepHandle(cardId, handle)
|
|
738
791
|
entry.cancel = () => handle.agent.cancel({ kind: 'user' })
|
|
739
792
|
handle.agent.followup(this.asUserMessage(phasePrompt({ ...(await this.store.get(wsPath, cardId)) ?? { plan } as KanbanCard, plan } as KanbanCard, phaseIndex, workdir)))
|
|
@@ -984,9 +1037,9 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
984
1037
|
const sessionId = this.newSessionId()
|
|
985
1038
|
this.sessionCards.set(sessionId, cardId)
|
|
986
1039
|
await this.store.mutate(cardId, (c) => { c.sessions.merge.push(sessionId) }, wsPath)
|
|
987
|
-
const route = await this.modelRoute(cardId, wsPath)
|
|
1040
|
+
const route = await this.modelRoute(cardId, wsPath, 'merge')
|
|
988
1041
|
try {
|
|
989
|
-
const handle = await this.createAgent(sessionId, workdir, route)
|
|
1042
|
+
const handle = await this.createAgent(sessionId, workdir, route, 'merge')
|
|
990
1043
|
this.keepHandle(cardId, handle)
|
|
991
1044
|
handle.agent.followup(this.asUserMessage(mergePrompt(workdir, conflicts)))
|
|
992
1045
|
await handle.agent.whenIdle()
|