Skip to content

Commit 68d44dd

Browse files
feat: add TUI model selection with execution preferences and loop improvements
- Add model selection dialog with favorites and recents support - Add execution preferences persistence for per-loop settings - Refactor TUI to use unified select dropdown for model and mode selection - Add comprehensive test coverage for loop helpers, launch, and TUI utilities - Fix TypeScript strict types by replacing explicit any with proper type annotations - Improve loop-helpers with better plan extraction and sanitization - Add tui-execution-preferences and tui-models utility modules
1 parent cb5ae80 commit 68d44dd

18 files changed

Lines changed: 2710 additions & 113 deletions

‎src/hooks/loop.ts‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { LoopService, LoopState } from '../services/loop'
44
import { MAX_RETRIES, MAX_CONSECUTIVE_STALLS } from '../services/loop'
55
import type { Logger, PluginConfig } from '../types'
66
import { retryWithModelFallback } from '../utils/model-fallback'
7-
import { resolveLoopModel } from '../utils/loop-helpers'
7+
import { resolveLoopModel, resolveLoopAuditorModel } from '../utils/loop-helpers'
88
import { execSync, spawnSync } from 'child_process'
99
import { resolve } from 'path'
1010
import type { createSandboxManager } from '../sandbox/manager'
@@ -646,6 +646,9 @@ export function createLoopEventHandler(
646646
loopService.setState(loopName, { ...currentState, phase: 'auditing', errorCount: 0 })
647647
logger.log(`Loop iteration ${currentState.iteration ?? 0} complete, running auditor for session ${currentState.sessionId}`)
648648

649+
const currentConfig = getConfig()
650+
const auditorModel = resolveLoopAuditorModel(currentConfig, loopService, loopName, logger)
651+
649652
const auditPrompt = {
650653
sessionID: currentState.sessionId,
651654
directory: currentState.worktreeDir,
@@ -654,6 +657,7 @@ export function createLoopEventHandler(
654657
agent: 'auditor',
655658
description: `Post-iteration ${currentState.iteration} code review`,
656659
prompt: loopService.buildAuditPrompt(currentState),
660+
...(auditorModel ? { model: auditorModel } : {}),
657661
}],
658662
}
659663

@@ -670,9 +674,12 @@ export function createLoopEventHandler(
670674
return
671675
}
672676

673-
const currentConfig = getConfig()
674-
const configuredModel = currentConfig.auditorModel ?? currentConfig.loop?.model ?? currentConfig.executionModel
675-
logger.log(`auditor using agent-configured model: ${configuredModel ?? 'default'}`)
677+
const modelSource = currentState.auditorModel
678+
? `loop state override: ${currentState.auditorModel}`
679+
: currentConfig.auditorModel
680+
? `config.auditorModel: ${currentConfig.auditorModel}`
681+
: 'default fallback'
682+
logger.log(`auditor using model: ${modelSource}`)
676683

677684
consecutiveStalls.set(loopName, 0)
678685
return

‎src/index.ts‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -244,12 +244,7 @@ export function createForgePlugin(config: PluginConfig): Plugin {
244244
return {
245245
getCleanup,
246246
tool: tools,
247-
config: createConfigHandler(
248-
config.auditorModel
249-
? { ...agents, auditor: { ...agents.auditor, defaultModel: config.auditorModel } }
250-
: agents,
251-
config.agents
252-
),
247+
config: createConfigHandler(agents, config.agents),
253248
'chat.message': async (input, output) => {
254249
await sessionHooks.onMessage(input, output)
255250
},

‎src/services/loop.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ export interface LoopState {
7171
sandbox?: boolean
7272
sandboxContainerName?: string
7373
completionSummary?: string
74+
executionModel?: string
75+
auditorModel?: string
7476
}
7577

7678
export interface LoopService {

‎src/tools/loop.ts‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ interface LoopSetupOptions {
2929
agent?: string
3030
model?: { providerID: string; modelID: string }
3131
worktree?: boolean
32+
executionModel?: string
33+
auditorModel?: string
3234
onLoopStarted?: (loopName: string) => void
3335
}
3436

@@ -196,6 +198,8 @@ export async function setupLoop(
196198
worktree: options.worktree,
197199
sandbox: sandboxEnabled,
198200
sandboxContainerName,
201+
executionModel: options.executionModel,
202+
auditorModel: options.auditorModel,
199203
}
200204

201205
kvService.set(projectId, `plan:${uniqueLoopName}`, options.prompt)
@@ -359,6 +363,8 @@ export function createLoopTools(ctx: ToolContext): Record<string, ReturnType<typ
359363
const sessionTitle = args.title.length > 60 ? `${args.title.substring(0, 57)}...` : args.title
360364
const loopModel = parseModelString(config.loop?.model) ?? parseModelString(config.executionModel)
361365
const audit = config.loop?.defaultAudit ?? true
366+
const executionModel = config.loop?.model ?? config.executionModel
367+
const auditorModel = config.auditorModel ?? config.loop?.model ?? config.executionModel
362368

363369
const loopName = args.loopName ? slugify(args.loopName) : slugify(sessionTitle)
364370

@@ -373,6 +379,8 @@ export function createLoopTools(ctx: ToolContext): Record<string, ReturnType<typ
373379
agent: 'code',
374380
model: loopModel,
375381
worktree: args.worktree,
382+
executionModel: executionModel,
383+
auditorModel: auditorModel,
376384
onLoopStarted: (id) => loopHandler.startWatchdog(id),
377385
})
378386
},
@@ -545,6 +553,8 @@ export function createLoopTools(ctx: ToolContext): Record<string, ReturnType<typ
545553
sandboxContainerName: restartSandbox
546554
? ctx.sandboxManager?.docker.containerName(stoppedState.loopName!)
547555
: undefined,
556+
executionModel: stoppedState.executionModel,
557+
auditorModel: stoppedState.auditorModel,
548558
}
549559

550560
loopService.setState(stoppedState.loopName!, newState)
@@ -555,7 +565,9 @@ export function createLoopTools(ctx: ToolContext): Record<string, ReturnType<typ
555565
promptText += buildCompletionSignalInstructions(stoppedState.completionSignal)
556566
}
557567

558-
const loopModel = parseModelString(config.loop?.model) ?? parseModelString(config.executionModel)
568+
const loopModel = parseModelString(stoppedState.executionModel)
569+
?? parseModelString(config.loop?.model)
570+
?? parseModelString(config.executionModel)
559571

560572
const { result: promptResult } = await retryWithModelFallback(
561573
() => v2.session.promptAsync({
@@ -704,6 +716,10 @@ export function createLoopTools(ctx: ToolContext): Record<string, ReturnType<typ
704716
`Started: ${state.startedAt}`,
705717
...(state.completedAt ? [`Completed: ${state.completedAt}`] : []),
706718
)
719+
statusLines.push(
720+
`Model: ${state.executionModel ?? config.loop?.model ?? config.executionModel ?? 'default'}`,
721+
`Auditor model: ${state.auditorModel ?? config.auditorModel ?? state.executionModel ?? config.loop?.model ?? config.executionModel ?? 'default'}`,
722+
)
707723

708724
if (state.lastAuditResult) {
709725
statusLines.push(...formatAuditResult(state.lastAuditResult))
@@ -790,8 +806,8 @@ export function createLoopTools(ctx: ToolContext): Record<string, ReturnType<typ
790806
`Started: ${state.startedAt}`,
791807
...(state.errorCount && state.errorCount > 0 ? [`Error count: ${state.errorCount} (retries before termination: ${MAX_RETRIES})`] : []),
792808
`Audit count: ${state.auditCount ?? 0}`,
793-
`Model: ${config.loop?.model || config.executionModel || 'default'}`,
794-
`Auditor model: ${config.auditorModel || 'default'}`,
809+
`Model: ${state.executionModel ?? config.loop?.model ?? config.executionModel ?? 'default'}`,
810+
`Auditor model: ${state.auditorModel ?? config.auditorModel ?? state.executionModel ?? config.loop?.model ?? config.executionModel ?? 'default'}`,
795811
...(stallCount > 0 ? [`Stalls: ${stallCount}`] : []),
796812
...(secondsSinceActivity !== null ? [`Last activity: ${secondsSinceActivity}s ago`] : []),
797813
'',

‎src/tools/plan-approval.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ export function createToolExecuteAfterHook(ctx: ToolContext): Hooks['tool.execut
169169
kvService.delete(projectId, `plan:${input.sessionID}`)
170170

171171
const loopModel = parseModelString(config.loop?.model) ?? parseModelString(config.executionModel)
172+
const executionModel = config.loop?.model ?? config.executionModel
173+
const auditorModel = config.auditorModel ?? config.loop?.model ?? config.executionModel
172174

173175
setupLoop(ctx, {
174176
prompt: planText,
@@ -180,6 +182,8 @@ export function createToolExecuteAfterHook(ctx: ToolContext): Hooks['tool.execut
180182
agent: 'code',
181183
model: loopModel,
182184
worktree: isWorktree,
185+
executionModel: executionModel,
186+
auditorModel: auditorModel,
183187
onLoopStarted: (id) => ctx.loopHandler.startWatchdog(id),
184188
}).catch((err) => {
185189
logger.error('Plan approval: failed to start loop', err)

0 commit comments

Comments
 (0)