Files
openchamber/packages/ui/src/lib/messages/userModelChoice.ts
T
Cursor AgentandSerhii Dziupin 65a1eec782 fix(ui): keep manual model override after delegated subtask completes
Synthetic subagent-completion nudges were treated as the latest user model
choice and rehydrated the agent default, while setAgent preferred the agent
pin over the session override. Skip synthetic prompts for restore, preserve
manual selection-store overrides, and prefer session agent models in setAgent.

Closes openchamber/openchamber#2404

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
2026-08-04 12:46:48 +00:00

104 lines
3.3 KiB
TypeScript

import type { Message, Part } from '@opencode-ai/sdk/v2'
import { isFullySyntheticMessage } from './synthetic'
type UserModelChoice = {
id: string
agent?: string
providerID?: string
modelID?: string
variant?: string
}
type MessageLike = Message & {
model?: { providerID?: string; modelID?: string; variant?: string }
variant?: string
mode?: string
}
/**
* Extract agent/model selection metadata from a user message, if present.
*/
export const extractUserModelChoice = (message: MessageLike): UserModelChoice | null => {
if (message.role !== 'user') {
return null
}
const providerID = typeof message.model?.providerID === 'string' && message.model.providerID.trim().length > 0
? message.model.providerID
: undefined
const modelID = typeof message.model?.modelID === 'string' && message.model.modelID.trim().length > 0
? message.model.modelID
: undefined
const agent = typeof message.agent === 'string' && message.agent.trim().length > 0
? message.agent
: (typeof message.mode === 'string' && message.mode.trim().length > 0 ? message.mode : undefined)
// OpenCode 1.4.0 moved variant from top-level to model.variant.
const variantCandidate = message.model?.variant ?? message.variant
const variant = typeof variantCandidate === 'string' && variantCandidate.trim().length > 0
? variantCandidate
: undefined
return { id: message.id, agent, providerID, modelID, variant }
}
/**
* Find the latest *real* user prompt's model/agent choice.
*
* Synthetic user messages (e.g. subagent-completion nudges injected when a
* delegated child session goes idle) must not drive the composer model
* selector — restoring from them clobber a manual session override and reset
* to the agent default.
*
* Messages whose parts have not been loaded yet are skipped so an incomplete
* snapshot cannot be treated as authoritative.
*/
export const findLatestUserModelChoice = (
messages: readonly MessageLike[],
getParts: (messageId: string) => Part[] | undefined,
): UserModelChoice | null => {
for (let i = messages.length - 1; i >= 0; i -= 1) {
const message = messages[i]
if (message.role !== 'user') {
continue
}
const parts = getParts(message.id)
if (!Array.isArray(parts) || parts.length === 0) {
continue
}
if (isFullySyntheticMessage(parts)) {
continue
}
return extractUserModelChoice(message)
}
return null
}
/**
* When the user has a manual session model override, historical (or synthetic)
* user-message metadata must not overwrite it. After a real send the selection
* store is updated to match the message, so a conflict means the picker was
* changed after the last prompt — keep the override.
*/
export const shouldPreserveManualModelOverride = ({
selectionSource,
savedSessionModel,
candidate,
}: {
selectionSource: 'auto' | 'manual' | undefined
savedSessionModel: { providerId: string; modelId: string } | null | undefined
candidate: Pick<UserModelChoice, 'providerID' | 'modelID'> | null | undefined
}): boolean => {
if (selectionSource !== 'manual' || !savedSessionModel?.providerId || !savedSessionModel.modelId) {
return false
}
if (!candidate?.providerID || !candidate.modelID) {
return true
}
return savedSessionModel.providerId !== candidate.providerID
|| savedSessionModel.modelId !== candidate.modelID
}