@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/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'
|
|
@@ -49,6 +49,11 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
49
49
|
private merging = new Set<string>()
|
|
50
50
|
/** Retained agent handles (kept so ended sessions stay in the store/UI). */
|
|
51
51
|
private handles = new Map<string, AgentHandle[]>()
|
|
52
|
+
/** Whether startup recovery + session indexing ran (deferred until workspaces exist). */
|
|
53
|
+
private recovered = false
|
|
54
|
+
/** Agents that already got their scoped kanban tools (event idempotency). */
|
|
55
|
+
private toolScopedAgents = new WeakSet<object>()
|
|
56
|
+
private agentStartOff: (() => void) | undefined
|
|
52
57
|
|
|
53
58
|
constructor(ctx: Context, store: TaskStore, settings: KanbanSettingsFace) {
|
|
54
59
|
this.ctx = ctx
|
|
@@ -62,10 +67,22 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
62
67
|
|
|
63
68
|
start(): void {
|
|
64
69
|
this.pumpTimer = setInterval(() => { void this.pump() }, 2500)
|
|
70
|
+
// `agent/session-start` fires for BOTH agents.create (startup) and
|
|
71
|
+
// agents.resume (host re-open after a restart). Kanban tools are scoped
|
|
72
|
+
// per agent, so a resumed refinement/phase/merge session would otherwise
|
|
73
|
+
// lose them — register there instead of only in createAgent's setup.
|
|
74
|
+
this.agentStartOff = this.ctx.on('agent/session-start', (payload: { agent?: { id?: string; ctx?: Context } }) => {
|
|
75
|
+
void this.onAgentSessionStart(payload.agent)
|
|
76
|
+
})
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
stop(): void {
|
|
68
80
|
if (this.pumpTimer !== undefined) clearInterval(this.pumpTimer)
|
|
81
|
+
this.pumpTimer = undefined
|
|
82
|
+
if (this.agentStartOff !== undefined) {
|
|
83
|
+
this.agentStartOff()
|
|
84
|
+
this.agentStartOff = undefined
|
|
85
|
+
}
|
|
69
86
|
}
|
|
70
87
|
|
|
71
88
|
private newSessionId(): SessionId {
|
|
@@ -157,6 +174,63 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
157
174
|
}
|
|
158
175
|
}
|
|
159
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Rebuild sessionId → cardId from the task store. Runs once the workspace
|
|
179
|
+
* registry is available (see pump), so kanban sessions resumed after a
|
|
180
|
+
* restart still resolve their card.
|
|
181
|
+
*/
|
|
182
|
+
private async indexSessions(): Promise<void> {
|
|
183
|
+
for (const ws of await this.workspaceList()) {
|
|
184
|
+
const cards = await this.store.list(ws.path)
|
|
185
|
+
for (const card of cards) this.mapCardSessions(card.id, card)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private mapCardSessions(cardId: string, card: KanbanCard): void {
|
|
190
|
+
for (const sid of card.sessions.refinement) this.sessionCards.set(sid, cardId)
|
|
191
|
+
for (const attempt of card.sessions.phases) this.sessionCards.set(attempt.sessionId, cardId)
|
|
192
|
+
for (const sid of card.sessions.merge) this.sessionCards.set(sid, cardId)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Fallback lookup when a session starts before indexSessions ran. */
|
|
196
|
+
private async lookupCardOfSession(sessionId: string): Promise<string | undefined> {
|
|
197
|
+
for (const ws of await this.workspaceList()) {
|
|
198
|
+
const cards = await this.store.list(ws.path)
|
|
199
|
+
for (const card of cards) {
|
|
200
|
+
const hit =
|
|
201
|
+
card.sessions.refinement.includes(sessionId) ||
|
|
202
|
+
card.sessions.phases.some((a) => a.sessionId === sessionId) ||
|
|
203
|
+
card.sessions.merge.includes(sessionId)
|
|
204
|
+
if (hit) {
|
|
205
|
+
this.mapCardSessions(card.id, card)
|
|
206
|
+
return card.id
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return undefined
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The single place kanban tools get registered. `agent/session-start` fires
|
|
215
|
+
* for BOTH `agents.create` (startup) and `agents.resume` (host re-open after
|
|
216
|
+
* a restart), so a resumed refinement/phase/merge session keeps its scoped
|
|
217
|
+
* tools. Idempotent per agent object; non-kanban sessions are skipped.
|
|
218
|
+
*/
|
|
219
|
+
private async onAgentSessionStart(agent: { id?: string; ctx?: Context } | undefined): Promise<void> {
|
|
220
|
+
if (agent === undefined || agent.id === undefined || agent.ctx === undefined) return
|
|
221
|
+
if (this.toolScopedAgents.has(agent)) return
|
|
222
|
+
let cardId = this.sessionCards.get(agent.id)
|
|
223
|
+
if (cardId === undefined) cardId = await this.lookupCardOfSession(agent.id)
|
|
224
|
+
if (cardId === undefined) return
|
|
225
|
+
this.sessionCards.set(agent.id, cardId)
|
|
226
|
+
try {
|
|
227
|
+
registerKanbanTools(agent.ctx, this.toolResolver())
|
|
228
|
+
this.toolScopedAgents.add(agent)
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error('[task-kanban] register kanban tools for ' + agent.id + ' failed:', String(error))
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
160
234
|
// ── Card operations (RPC entry points) ────────────────────────────────────
|
|
161
235
|
|
|
162
236
|
async listCards(workspacePath: string): Promise<KanbanCard[]> {
|
|
@@ -287,6 +361,26 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
287
361
|
}
|
|
288
362
|
|
|
289
363
|
async pump(): Promise<void> {
|
|
364
|
+
// Recovery and session indexing need the workspace registry, which may
|
|
365
|
+
// not be populated when the plugin first applies (the old code silently
|
|
366
|
+
// no-opped and left interrupted cards stuck in refining). Retry on the
|
|
367
|
+
// pump tick until the first workspace appears.
|
|
368
|
+
if (!this.recovered) {
|
|
369
|
+
const ws = await this.workspaceList()
|
|
370
|
+
if (ws.length > 0) {
|
|
371
|
+
this.recovered = true
|
|
372
|
+
try {
|
|
373
|
+
await this.recoverInterrupted()
|
|
374
|
+
} catch (error) {
|
|
375
|
+
console.error('[task-kanban] recoverInterrupted failed:', String(error))
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
await this.indexSessions()
|
|
379
|
+
} catch (error) {
|
|
380
|
+
console.error('[task-kanban] indexSessions failed:', String(error))
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
290
384
|
if (this.slots >= this.maxParallel()) return
|
|
291
385
|
const workspaces = await this.workspaceList()
|
|
292
386
|
for (const ws of workspaces) {
|
|
@@ -331,7 +425,7 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
331
425
|
private async runRefinement(cardId: string, wsPath: string): Promise<void> {
|
|
332
426
|
const card = await this.store.get(wsPath, cardId)
|
|
333
427
|
if (card === undefined) return
|
|
334
|
-
const route = await this.modelRoute(cardId, wsPath)
|
|
428
|
+
const route = await this.modelRoute(cardId, wsPath, 'refine')
|
|
335
429
|
const sessionId = this.newSessionId()
|
|
336
430
|
this.sessionCards.set(sessionId, cardId)
|
|
337
431
|
await this.store.mutate(cardId, (c) => {
|
|
@@ -340,7 +434,7 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
340
434
|
c.sessions.refinement.push(sessionId)
|
|
341
435
|
}, wsPath)
|
|
342
436
|
try {
|
|
343
|
-
const handle = await this.createAgent(sessionId, wsPath, route,
|
|
437
|
+
const handle = await this.createAgent(sessionId, wsPath, route, 'refine')
|
|
344
438
|
this.keepHandle(cardId, handle)
|
|
345
439
|
await this.attachRefinementSession(wsPath, sessionId)
|
|
346
440
|
// The agent scope is the layer where preset skill providers live
|
|
@@ -462,30 +556,70 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
462
556
|
return { promise, dispose }
|
|
463
557
|
}
|
|
464
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
|
+
*/
|
|
465
569
|
private async createAgent(
|
|
466
570
|
sessionId: SessionId,
|
|
467
571
|
cwd: string,
|
|
468
|
-
route: { provider?: string; model?: string },
|
|
469
|
-
|
|
572
|
+
route: { provider?: string; model?: string; reasoningEffort?: string },
|
|
573
|
+
kind: KanbanSessionKind,
|
|
470
574
|
) {
|
|
471
575
|
const agentOptions =
|
|
472
576
|
route.provider !== undefined && route.model !== undefined && route.model !== ''
|
|
473
577
|
? { provider: route.provider, model: route.model }
|
|
474
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
|
+
}
|
|
475
604
|
return this.ctx.agents.create({
|
|
476
605
|
sessionId,
|
|
477
|
-
meta: { cwd },
|
|
606
|
+
meta: { cwd, ...(presetId !== undefined ? { agentPreset: presetId } : {}) },
|
|
478
607
|
...(agentOptions !== undefined ? { agentOptions } : {}),
|
|
608
|
+
// Kanban tools are NOT registered here: `agent/session-start` (see
|
|
609
|
+
// start()) fires for both create and resume, keeping them available
|
|
610
|
+
// even when the host re-opens a persisted kanban session.
|
|
479
611
|
setup: async (agentCtx) => {
|
|
480
612
|
const agentPresets = agentCtx.get('agentPresets') as
|
|
481
613
|
| { mount(agentCtx: unknown, id?: string): Promise<unknown> }
|
|
482
614
|
| undefined
|
|
483
615
|
if (agentPresets !== undefined) {
|
|
484
|
-
await agentPresets.mount(agentCtx,
|
|
616
|
+
await agentPresets.mount(agentCtx, presetId)
|
|
485
617
|
} else {
|
|
486
618
|
throw new Error('@fonlan/dsh-task-kanban: agent-presets service is not mounted')
|
|
487
619
|
}
|
|
488
|
-
|
|
620
|
+
if (selectionRef.current !== undefined && selectionRef.current.reasoningEffort !== undefined) {
|
|
621
|
+
installModelSelection(agentCtx, selectionRef)
|
|
622
|
+
}
|
|
489
623
|
},
|
|
490
624
|
})
|
|
491
625
|
}
|
|
@@ -524,8 +658,19 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
524
658
|
return undefined
|
|
525
659
|
}
|
|
526
660
|
|
|
527
|
-
/**
|
|
528
|
-
|
|
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 }> {
|
|
529
674
|
const card = await this.store.get(wsPath, cardId)
|
|
530
675
|
if (card === undefined) return {}
|
|
531
676
|
if (card.model !== '' && card.provider !== undefined && card.provider !== '') {
|
|
@@ -535,10 +680,13 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
535
680
|
const fallback = this.settings.defaultModelRoute()
|
|
536
681
|
return fallback.provider !== undefined ? { provider: fallback.provider, model: card.model } : { model: card.model }
|
|
537
682
|
}
|
|
538
|
-
const
|
|
539
|
-
if (
|
|
540
|
-
|
|
541
|
-
|
|
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
|
+
}
|
|
542
690
|
}
|
|
543
691
|
return this.settings.defaultModelRoute()
|
|
544
692
|
}
|
|
@@ -636,9 +784,9 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
636
784
|
c.error = undefined
|
|
637
785
|
c.sessions.phases.push({ phaseIndex, sessionId, startedAt: Date.now() })
|
|
638
786
|
}, wsPath)
|
|
639
|
-
const route = await this.modelRoute(cardId, wsPath)
|
|
787
|
+
const route = await this.modelRoute(cardId, wsPath, 'phase')
|
|
640
788
|
try {
|
|
641
|
-
const handle = await this.createAgent(sessionId, workdir, route,
|
|
789
|
+
const handle = await this.createAgent(sessionId, workdir, route, 'phase')
|
|
642
790
|
this.keepHandle(cardId, handle)
|
|
643
791
|
entry.cancel = () => handle.agent.cancel({ kind: 'user' })
|
|
644
792
|
handle.agent.followup(this.asUserMessage(phasePrompt({ ...(await this.store.get(wsPath, cardId)) ?? { plan } as KanbanCard, plan } as KanbanCard, phaseIndex, workdir)))
|
|
@@ -889,9 +1037,9 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
889
1037
|
const sessionId = this.newSessionId()
|
|
890
1038
|
this.sessionCards.set(sessionId, cardId)
|
|
891
1039
|
await this.store.mutate(cardId, (c) => { c.sessions.merge.push(sessionId) }, wsPath)
|
|
892
|
-
const route = await this.modelRoute(cardId, wsPath)
|
|
1040
|
+
const route = await this.modelRoute(cardId, wsPath, 'merge')
|
|
893
1041
|
try {
|
|
894
|
-
const handle = await this.createAgent(sessionId, workdir, route,
|
|
1042
|
+
const handle = await this.createAgent(sessionId, workdir, route, 'merge')
|
|
895
1043
|
this.keepHandle(cardId, handle)
|
|
896
1044
|
handle.agent.followup(this.asUserMessage(mergePrompt(workdir, conflicts)))
|
|
897
1045
|
await handle.agent.whenIdle()
|
package/src/server/settings.ts
CHANGED
|
@@ -1,26 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* User-facing plugin settings (global parallel worker count,
|
|
3
|
-
* persisted through the settings service
|
|
4
|
-
* settings service requires a lowercase
|
|
5
|
-
* (/^[a-z][a-z0-9-]*$/), so the scoped package name
|
|
2
|
+
* User-facing plugin settings (global parallel worker count, per-session-type
|
|
3
|
+
* model/effort/preset defaults), persisted through the settings service
|
|
4
|
+
* namespace `task-kanban`. The DSH settings service requires a lowercase
|
|
5
|
+
* kebab-case namespace (/^[a-z][a-z0-9-]*$/), so the scoped package name
|
|
6
|
+
* cannot be used here.
|
|
6
7
|
*/
|
|
7
8
|
import type { Context } from '@deepseek-ai/cordis'
|
|
8
9
|
import z from '@deepseek-ai/schemastery'
|
|
9
10
|
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
10
|
-
import type { KanbanSettingsShape } from '../shared/card.js'
|
|
11
|
+
import type { KanbanSessionKind, KanbanSettingsShape } from '../shared/card.js'
|
|
11
12
|
|
|
12
13
|
export const KANBAN_SETTINGS_NS = settingsNamespace('task-kanban')
|
|
13
14
|
|
|
14
15
|
export const KanbanSettingsSchema = z.object({
|
|
15
16
|
maxParallelWorkers: z.natural().min(1).default(1),
|
|
16
|
-
|
|
17
|
+
refinementModel: z.string().default(''),
|
|
18
|
+
refinementProvider: z.string().default(''),
|
|
19
|
+
refinementReasoningEffort: z.string().default(''),
|
|
20
|
+
refinementPreset: z.string().default(''),
|
|
21
|
+
phaseModel: z.string().default(''),
|
|
22
|
+
phaseProvider: z.string().default(''),
|
|
23
|
+
phaseReasoningEffort: z.string().default(''),
|
|
24
|
+
phasePreset: z.string().default(''),
|
|
17
25
|
})
|
|
18
26
|
|
|
27
|
+
/** The per-session-type default slice a session kind resolves. */
|
|
28
|
+
export interface KanbanSessionDefaults {
|
|
29
|
+
model: string
|
|
30
|
+
provider: string
|
|
31
|
+
reasoningEffort: string
|
|
32
|
+
preset: string
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
export interface KanbanSettingsFace {
|
|
20
36
|
get(): KanbanSettingsShape
|
|
21
37
|
update(patch: Partial<KanbanSettingsShape>): Promise<void>
|
|
22
38
|
/** Default provider/model route from the agent-default-model settings namespace. */
|
|
23
39
|
defaultModelRoute(): { provider?: string; model?: string }
|
|
40
|
+
/**
|
|
41
|
+
* The configured model/effort/preset defaults for one session kind.
|
|
42
|
+
* `merge` sessions deliberately carry no per-type defaults: they keep the
|
|
43
|
+
* legacy resolution chain (card model → host default).
|
|
44
|
+
*/
|
|
45
|
+
sessionDefaults(kind: KanbanSessionKind): KanbanSessionDefaults
|
|
24
46
|
}
|
|
25
47
|
|
|
26
48
|
/** Structural settings-service subset; the real service resolves the schema. */
|
|
@@ -29,8 +51,20 @@ interface SettingsServiceLike {
|
|
|
29
51
|
update(ns: unknown, patch: Record<string, unknown>, expectedRevision?: number): Promise<unknown>
|
|
30
52
|
}
|
|
31
53
|
|
|
54
|
+
const EMPTY_DEFAULTS: KanbanSessionDefaults = { model: '', provider: '', reasoningEffort: '', preset: '' }
|
|
55
|
+
|
|
32
56
|
export function registerSettings(ctx: Context): KanbanSettingsFace {
|
|
33
|
-
let current: KanbanSettingsShape = {
|
|
57
|
+
let current: KanbanSettingsShape = {
|
|
58
|
+
maxParallelWorkers: 1,
|
|
59
|
+
refinementModel: '',
|
|
60
|
+
refinementProvider: '',
|
|
61
|
+
refinementReasoningEffort: '',
|
|
62
|
+
refinementPreset: '',
|
|
63
|
+
phaseModel: '',
|
|
64
|
+
phaseProvider: '',
|
|
65
|
+
phaseReasoningEffort: '',
|
|
66
|
+
phasePreset: '',
|
|
67
|
+
}
|
|
34
68
|
let service: SettingsServiceLike | undefined
|
|
35
69
|
ctx.inject(['settings'], (sctx) => {
|
|
36
70
|
service = sctx.settings as unknown as SettingsServiceLike
|
|
@@ -43,6 +77,17 @@ export function registerSettings(ctx: Context): KanbanSettingsFace {
|
|
|
43
77
|
})
|
|
44
78
|
return {
|
|
45
79
|
get: () => current,
|
|
80
|
+
sessionDefaults: (kind) => {
|
|
81
|
+
const s = current
|
|
82
|
+
switch (kind) {
|
|
83
|
+
case 'refine':
|
|
84
|
+
return { model: s.refinementModel, provider: s.refinementProvider, reasoningEffort: s.refinementReasoningEffort, preset: s.refinementPreset }
|
|
85
|
+
case 'phase':
|
|
86
|
+
return { model: s.phaseModel, provider: s.phaseProvider, reasoningEffort: s.phaseReasoningEffort, preset: s.phasePreset }
|
|
87
|
+
case 'merge':
|
|
88
|
+
return EMPTY_DEFAULTS
|
|
89
|
+
}
|
|
90
|
+
},
|
|
46
91
|
defaultModelRoute: () => {
|
|
47
92
|
if (service === undefined) return {}
|
|
48
93
|
try {
|
package/src/shared/card.ts
CHANGED
|
@@ -111,7 +111,19 @@ export interface ModelOption {
|
|
|
111
111
|
name?: string
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/** Which kanban session kind a per-type default applies to. */
|
|
115
|
+
export type KanbanSessionKind = 'refine' | 'phase' | 'merge'
|
|
116
|
+
|
|
114
117
|
export interface KanbanSettingsShape {
|
|
115
118
|
maxParallelWorkers: number
|
|
116
|
-
|
|
119
|
+
/** Requirement-refinement session defaults. */
|
|
120
|
+
refinementModel: string
|
|
121
|
+
refinementProvider: string
|
|
122
|
+
refinementReasoningEffort: string
|
|
123
|
+
refinementPreset: string
|
|
124
|
+
/** Phase implementation session defaults. */
|
|
125
|
+
phaseModel: string
|
|
126
|
+
phaseProvider: string
|
|
127
|
+
phaseReasoningEffort: string
|
|
128
|
+
phasePreset: string
|
|
117
129
|
}
|