fix(opencode): switch session models without restart
This commit is contained in:
@@ -863,6 +863,8 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
const abortSession = useOpencodeStore((state) => state.abortSession);
|
||||
const deleteOpencodeSession = useOpencodeStore((state) => state.deleteSession);
|
||||
const sendSessionMessage = useOpencodeStore((state) => state.sendSessionMessage);
|
||||
const sessionModelBySessionId = useOpencodeStore((state) => state.sessionModelBySessionId);
|
||||
const switchSessionModel = useOpencodeStore((state) => state.switchSessionModel);
|
||||
const cancelQueuedSessionPrompt = useOpencodeStore((state) => state.cancelQueuedSessionPrompt);
|
||||
const renameSession = useOpencodeStore((state) => state.renameSession);
|
||||
const shareSession = useOpencodeStore((state) => state.shareSession);
|
||||
@@ -951,6 +953,7 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
const seenCompactionIdsBySessionRef = useRef(new Map<string, Set<string>>());
|
||||
const lastCompactionAnnouncementSessionRef = useRef<string | null>(null);
|
||||
const composerAttachmentInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const modelSelectorRef = useRef<HTMLSelectElement | null>(null);
|
||||
const composerStopArmTimerRef = useRef<number | null>(null);
|
||||
const userModelConfigSyncRef = useRef<Promise<boolean> | null>(null);
|
||||
const localProxyModelConfigSyncedRef = useRef(false);
|
||||
@@ -1040,6 +1043,21 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
() => buildConfiguredModelOptions(providerAccounts, providerStatuses, providerDefaultAccountId),
|
||||
[providerAccounts, providerDefaultAccountId, providerStatuses],
|
||||
);
|
||||
const activeSessionModel = selectedSessionId
|
||||
? sessionModelBySessionId[selectedSessionId] ?? selectedProjectAgent?.model ?? ''
|
||||
: selectedProjectAgent?.model ?? '';
|
||||
const selectableChatModelOptions = useMemo(() => {
|
||||
if (!activeSessionModel || configuredChatModelOptions.some((option) => option.modelRef === activeSessionModel)) {
|
||||
return configuredChatModelOptions;
|
||||
}
|
||||
return [{
|
||||
modelRef: activeSessionModel,
|
||||
label: formatModelRefLabel(activeSessionModel),
|
||||
runtimeProviderKey: activeSessionModel.split('/')[0] ?? '',
|
||||
accountId: '',
|
||||
capability: 'text' as const,
|
||||
}, ...configuredChatModelOptions];
|
||||
}, [activeSessionModel, configuredChatModelOptions]);
|
||||
const canStopSession = canUseSessions && Boolean(selectedSessionId) && sessionBusy && composerStopArmed;
|
||||
const canUseVoiceInput = canUseSessions
|
||||
&& Boolean(selectedSessionId || selectedAgentId)
|
||||
@@ -1128,7 +1146,7 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
hasUndoableUserMessage: Boolean(undoableUserMessage),
|
||||
runtimeReady,
|
||||
busy: sessionBusy || selectedSessionRunning,
|
||||
hasModel: Boolean(selectedProjectAgent?.model?.trim()),
|
||||
hasModel: Boolean(activeSessionModel.trim()),
|
||||
shared: Boolean(currentSession?.share?.url),
|
||||
reverted: Boolean(currentSession?.revert?.messageID),
|
||||
shareEnabled: cachedCommandCatalog?.shareEnabled ?? true,
|
||||
@@ -1596,10 +1614,10 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
|
||||
const ensureChatModelConfigured = useCallback(async () => {
|
||||
if (!(await refreshWorksSquareUserModelConfigIfNeeded())) return false;
|
||||
if (selectedProjectAgent?.model?.trim()) return true;
|
||||
if (activeSessionModel.trim()) return true;
|
||||
setModelConfigPromptOpen(true);
|
||||
return false;
|
||||
}, [refreshWorksSquareUserModelConfigIfNeeded, selectedProjectAgent?.model]);
|
||||
}, [activeSessionModel, refreshWorksSquareUserModelConfigIfNeeded]);
|
||||
|
||||
const openSubscriptionUpgrade = useCallback(() => {
|
||||
void openWorksSquareSubscriptionUpgrade().catch(() => undefined);
|
||||
@@ -1644,6 +1662,9 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
return;
|
||||
}
|
||||
const configuredAgentModel = submissionAgent.model?.trim();
|
||||
const submissionModel = submissionSessionId
|
||||
? useOpencodeStore.getState().sessionModelBySessionId[submissionSessionId] ?? configuredAgentModel
|
||||
: configuredAgentModel;
|
||||
if (!(await refreshWorksSquareUserModelConfigIfNeeded())) return;
|
||||
if (!sessionStillMatches()) return;
|
||||
const latestProviderState = useProviderStore.getState();
|
||||
@@ -1652,7 +1673,7 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
latestProviderState.statuses,
|
||||
latestProviderState.defaultAccountId,
|
||||
);
|
||||
if (!configuredAgentModel || !latestConfiguredChatModelOptions.some((option) => option.modelRef === configuredAgentModel)) {
|
||||
if (!submissionModel || !latestConfiguredChatModelOptions.some((option) => option.modelRef === submissionModel)) {
|
||||
setModelConfigPromptOpen(true);
|
||||
toast.error('这个伙伴的模型不可用,请先更新伙伴配置。');
|
||||
return;
|
||||
@@ -1723,7 +1744,6 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
await sendSessionMessage(submissionSessionId, nextDraft, {
|
||||
attachedFiles,
|
||||
agentId: submissionAgent.id,
|
||||
model: configuredAgentModel,
|
||||
onHostAccepted,
|
||||
});
|
||||
} catch {
|
||||
@@ -2085,6 +2105,32 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
});
|
||||
}, []);
|
||||
|
||||
const openSessionModelSelector = useCallback(() => {
|
||||
if (!selectedSessionId) throw new Error('请先选择会话');
|
||||
const selector = modelSelectorRef.current;
|
||||
if (!selector) throw new Error('当前没有可用的模型选择器');
|
||||
selector.focus();
|
||||
try {
|
||||
selector.showPicker?.();
|
||||
} catch {
|
||||
// Focus is the supported fallback when the platform cannot open a native picker programmatically.
|
||||
}
|
||||
}, [selectedSessionId]);
|
||||
|
||||
const handleSessionModelChange = useCallback(async (event: ChangeEvent<HTMLSelectElement>) => {
|
||||
const sessionId = selectedSessionId;
|
||||
const model = event.target.value;
|
||||
if (!sessionId || !model || model === activeSessionModel) return;
|
||||
try {
|
||||
await switchSessionModel(sessionId, model);
|
||||
toast.success(`当前会话已切换到 ${formatModelRefLabel(model)}`);
|
||||
} catch (switchError) {
|
||||
toast.error('模型切换失败', {
|
||||
description: normalizeCommandError(switchError),
|
||||
});
|
||||
}
|
||||
}, [activeSessionModel, selectedSessionId, switchSessionModel]);
|
||||
|
||||
const runCompact = useCallback(async () => {
|
||||
if (!(await ensureChatModelConfigured())) {
|
||||
throw new Error('模型未就绪,请完成配置或手动重启运行时后重试。');
|
||||
@@ -2092,12 +2138,12 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
const owner = submissionSessionRef.current;
|
||||
const sessionId = owner.id;
|
||||
if (!sessionId) throw new Error('请先选择会话');
|
||||
await compactSession(sessionId, selectedProjectAgent?.model ?? undefined);
|
||||
await compactSession(sessionId, activeSessionModel || undefined);
|
||||
assertSubmissionSessionOwner(
|
||||
owner,
|
||||
submissionSessionRef.current,
|
||||
);
|
||||
}, [compactSession, ensureChatModelConfigured, selectedProjectAgent?.model]);
|
||||
}, [activeSessionModel, compactSession, ensureChatModelConfigured]);
|
||||
|
||||
const runShare = useCallback(async () => {
|
||||
const owner = submissionSessionRef.current;
|
||||
@@ -2281,13 +2327,11 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
return { status: 'cancelled' };
|
||||
}
|
||||
const parts = buildProjectCommandPartsFromAttachedFiles(attachedFiles);
|
||||
const model = selectedProjectAgent?.model;
|
||||
const agent = selectedProjectAgent?.id ?? currentSession?.agent;
|
||||
await executeProjectCommand(submissionSessionId, {
|
||||
command: rawName,
|
||||
arguments: rawArguments,
|
||||
...(agent ? { agent } : {}),
|
||||
...(model ? { model } : {}),
|
||||
parts,
|
||||
});
|
||||
if (!sameSubmissionSessionOwner(
|
||||
@@ -2303,12 +2347,12 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
ensureChatModelConfigured,
|
||||
executeProjectCommand,
|
||||
selectedProjectAgent?.id,
|
||||
selectedProjectAgent?.model,
|
||||
]);
|
||||
|
||||
const commandActions = useMemo<ChatCommandActions>(() => ({
|
||||
rename: runRename,
|
||||
timeline: runTimeline,
|
||||
models: openSessionModelSelector,
|
||||
compact: runCompact,
|
||||
share: runShare,
|
||||
unshare: runUnshare,
|
||||
@@ -2329,6 +2373,7 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
runRename,
|
||||
runShare,
|
||||
runTimeline,
|
||||
openSessionModelSelector,
|
||||
runUndo,
|
||||
runUnshare,
|
||||
toggleThinking,
|
||||
@@ -2964,7 +3009,7 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
<ComposerAttachmentCard
|
||||
key={attachment.id}
|
||||
attachment={attachment}
|
||||
modelRef={selectedProjectAgent?.model}
|
||||
modelRef={activeSessionModel}
|
||||
onSelectVariant={(variant) => (
|
||||
composer.selectVariant(attachment.id, variant)
|
||||
)}
|
||||
@@ -3106,17 +3151,34 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
</div>
|
||||
)}
|
||||
<div data-testid="opencode-composer-right-actions" className="flex min-w-0 items-center gap-0.5">
|
||||
{selectedAgentId ? (
|
||||
<span
|
||||
data-testid="opencode-current-agent-model"
|
||||
className={cn(
|
||||
'shrink-0 whitespace-nowrap px-2 text-xs font-medium text-muted-foreground',
|
||||
!selectedProjectAgent?.model && 'text-destructive',
|
||||
)}
|
||||
title={selectedProjectAgent?.model ?? '未配置模型'}
|
||||
>
|
||||
{selectedProjectAgent?.model ? formatModelRefLabel(selectedProjectAgent.model) : '未配置模型'}
|
||||
</span>
|
||||
{selectedSessionId || selectedAgentId ? (
|
||||
<label className="relative min-w-0 shrink">
|
||||
<span className="sr-only">当前会话模型</span>
|
||||
<select
|
||||
ref={modelSelectorRef}
|
||||
data-testid="opencode-model-selector-trigger"
|
||||
aria-label="当前会话模型"
|
||||
className={cn(
|
||||
'h-8 max-w-44 appearance-none truncate rounded-md border border-transparent bg-transparent py-1 pl-2 pr-7 text-xs font-medium text-muted-foreground outline-none transition-colors hover:border-foreground/10 hover:bg-surface-subtle focus:border-foreground/15 focus:bg-surface-subtle',
|
||||
!activeSessionModel && 'text-destructive',
|
||||
)}
|
||||
value={activeSessionModel}
|
||||
disabled={!selectedSessionId || loading || selectableChatModelOptions.length === 0}
|
||||
onChange={(event) => void handleSessionModelChange(event)}
|
||||
title={activeSessionModel || '未配置模型'}
|
||||
>
|
||||
{!activeSessionModel ? <option value="">未配置模型</option> : null}
|
||||
{selectableChatModelOptions.map((option) => (
|
||||
<option key={option.modelRef} value={option.modelRef}>
|
||||
{formatModelRefLabel(option.modelRef)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute right-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground"
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export type BuiltinChatCommandName =
|
||||
| 'share' | 'rename' | 'timeline' | 'compact' | 'unshare'
|
||||
| 'share' | 'rename' | 'timeline' | 'models' | 'compact' | 'unshare'
|
||||
| 'undo' | 'redo' | 'timestamps' | 'thinking' | 'process' | 'copy' | 'export';
|
||||
|
||||
export interface ProjectCommandInfo {
|
||||
@@ -75,6 +75,7 @@ export const BUILTIN_CHAT_COMMANDS: readonly ChatCommandDefinition[] = [
|
||||
builtin('share', [], '分享会话', '创建或复制分享链接', undefined, 'share-first'),
|
||||
builtin('rename', [], '重命名会话', '修改当前会话标题', '[title]', 'none'),
|
||||
builtin('timeline', [], '消息时间线', '跳转到一条已加载消息', undefined, 'none'),
|
||||
builtin('models', ['model'], '切换模型', '切换当前会话后续轮次使用的模型', undefined, 'none'),
|
||||
builtin('compact', ['summarize'], '压缩会话', '使用当前模型总结上下文', undefined, 'none'),
|
||||
builtin('unshare', [], '撤销分享', '移除公开分享链接', undefined, 'always'),
|
||||
builtin('undo', [], '撤销上一轮', '回退最近用户消息并恢复输入', undefined, 'always'),
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface ChatCommandKeyEvent {
|
||||
export interface ChatCommandActions {
|
||||
rename: (title: string) => Promise<void>;
|
||||
timeline: (messageId: string) => Promise<void> | void;
|
||||
models: () => Promise<void> | void;
|
||||
compact: () => Promise<void>;
|
||||
share: () => Promise<void>;
|
||||
unshare: () => Promise<void>;
|
||||
@@ -267,6 +268,15 @@ export function useChatCommands({
|
||||
dialogOriginRef.current = origin;
|
||||
setDialog({ kind: 'timeline', originatingDraft });
|
||||
return true;
|
||||
case 'models':
|
||||
await actions.models();
|
||||
clearIfOriginating(
|
||||
setDraft,
|
||||
originatingDraft,
|
||||
origin,
|
||||
currentOriginRef,
|
||||
);
|
||||
return true;
|
||||
case 'compact':
|
||||
await actions.compact();
|
||||
clearIfOriginating(
|
||||
|
||||
Reference in New Issue
Block a user