@fonlan/dsh-task-kanban 0.1.1 → 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 +371 -122
- package/lib/client.js.map +1 -1
- package/lib/index.js +214 -24
- package/lib/types/server/models.d.ts +16 -0
- package/lib/types/server/runner.d.ts +39 -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 +107 -16
- package/src/client/board.tsx +65 -29
- package/src/client/index.tsx +11 -9
- package/src/client/locales.ts +24 -4
- 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 +168 -20
- 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
|
}
|