Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/common/ipcBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,8 @@ export interface ICreateConversationParams {
sessionMode?: string;
/** User-selected Codex model from Guid page */
codexModel?: string;
/** Pre-selected ACP model from Guid page (cached model list) */
currentModelId?: string;
/** Runtime validation snapshot used for post-switch strong checks (OpenClaw) */
runtimeValidation?: {
expectedWorkspace?: string;
Expand Down
2 changes: 2 additions & 0 deletions src/common/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export interface IConfigStorageRefer {
};
};
'acp.customAgents'?: AcpBackendConfig[];
// Cached model lists per ACP backend for Guid page pre-selection
'acp.cachedModels'?: Record<string, import('@/types/acpTypes').AcpModelInfo>;
'model.config': IProvider[];
'mcp.config': IMcpServer[];
'mcp.agentInstallStatus': Record<string, string[]>;
Expand Down
2 changes: 2 additions & 0 deletions src/process/initAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export const createAcpAgent = async (options: ICreateConversationParams): Promis
presetAssistantId: extra.presetAssistantId,
// Initial session mode selected on Guid page (from AgentModeSelector)
sessionMode: extra.sessionMode,
// Pre-selected model from Guid page (cached model list)
currentModelId: extra.currentModelId,
},
createTime: Date.now(),
modifyTime: Date.now(),
Expand Down
19 changes: 19 additions & 0 deletions src/process/task/AcpAgentManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,11 @@ class AcpAgentManager extends BaseAgentManager<AcpAgentManagerData, AcpPermissio
console.warn(`[AcpAgentManager] Failed to re-apply model ${this.persistedModelId}:`, error);
}
}
// Cache model list for Guid page pre-selection after agent starts
const modelInfo = this.agent.getModelInfo();
if (modelInfo && modelInfo.availableModels.length > 0) {
void this.cacheModelList(modelInfo);
}
return this.agent;
});
})();
Expand Down Expand Up @@ -716,6 +721,20 @@ class AcpAgentManager extends BaseAgentManager<AcpAgentManagerData, AcpPermissio
.finally(doKill);
}

/**
* Cache model list to storage for Guid page pre-selection.
* Keyed by backend name (e.g., 'claude', 'qwen').
*/
private async cacheModelList(modelInfo: AcpModelInfo): Promise<void> {
try {
const cached = (await ProcessConfig.get('acp.cachedModels')) || {};
cached[this.options.backend] = modelInfo;
await ProcessConfig.set('acp.cachedModels', cached);
} catch (error) {
console.warn('[AcpAgentManager] Failed to cache model list:', error);
}
}

/**
* Save ACP session ID to database for resume support.
* 保存 ACP session ID 到数据库以支持会话恢复。
Expand Down
44 changes: 40 additions & 4 deletions src/renderer/components/AcpModelSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import { ipcBridge } from '@/common';
import type { IResponseMessage } from '@/common/ipcBridge';
import { ConfigStorage } from '@/common/storage';
import type { AcpModelInfo } from '@/types/acpTypes';
import { Button, Dropdown, Menu, Tooltip } from '@arco-design/web-react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
Expand All @@ -18,28 +19,63 @@ import { useTranslation } from 'react-i18next';
* - null model info: disabled "Use CLI model" button (backward compatible)
* - canSwitch=false: read-only display of current model name
* - canSwitch=true: clickable dropdown selector
*
* When backend and initialModelId are provided, the component can show
* cached model info before the agent manager is created (pre-first-message).
*/
const AcpModelSelector: React.FC<{
conversationId: string;
}> = ({ conversationId }) => {
/** ACP backend name for loading cached models (e.g., 'claude', 'qwen') */
backend?: string;
/** Pre-selected model ID from Guid page */
initialModelId?: string;
}> = ({ conversationId, backend, initialModelId }) => {
const { t } = useTranslation();
const [modelInfo, setModelInfo] = useState<AcpModelInfo | null>(null);
const modelInfoRef = useRef(modelInfo);
modelInfoRef.current = modelInfo;

// Fetch initial model info on mount
// Fetch initial model info on mount, fallback to cached models if manager not ready
useEffect(() => {
let cancelled = false;
ipcBridge.acpConversation.getModelInfo
.invoke({ conversationId })
.then((result) => {
if (cancelled) return;
if (result.success && result.data?.modelInfo) {
setModelInfo(result.data.modelInfo);
} else if (backend && initialModelId) {
// Manager not yet created — load cached model list from storage
void loadCachedModelInfo(backend, initialModelId, cancelled);
}
})
.catch(() => {
// Silently ignore - model info is optional
if (!cancelled && backend && initialModelId) {
void loadCachedModelInfo(backend, initialModelId, cancelled);
}
});
}, [conversationId]);

return () => {
cancelled = true;
};

async function loadCachedModelInfo(backendKey: string, modelId: string, isCancelled: boolean) {
try {
const cached = await ConfigStorage.get('acp.cachedModels');
if (isCancelled) return;
const cachedInfo = cached?.[backendKey];
if (cachedInfo && cachedInfo.availableModels.length > 0) {
setModelInfo({
...cachedInfo,
currentModelId: modelId,
currentModelLabel: cachedInfo.availableModels.find((m) => m.id === modelId)?.label || modelId,
});
}
} catch {
// Silently ignore
}
}
}, [conversationId, backend, initialModelId]);

// Listen for acp_model_info / codex_model_info events from responseStream
useEffect(() => {
Expand Down
6 changes: 5 additions & 1 deletion src/renderer/pages/conversation/ChatConversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,11 @@ const ChatConversation: React.FC<{
// NOTE: This must be placed before the Gemini early return to maintain consistent hook order.
const modelSelector = useMemo(() => {
if (!conversation || isGeminiConversation) return undefined;
if (conversation.type === 'acp' || conversation.type === 'codex') {
if (conversation.type === 'acp') {
const extra = conversation.extra as { backend?: string; currentModelId?: string };
return <AcpModelSelector conversationId={conversation.id} backend={extra.backend} initialModelId={extra.currentModelId} />;
}
if (conversation.type === 'codex') {
return <AcpModelSelector conversationId={conversation.id} />;
}
return <GeminiModelSelector disabled={true} />;
Expand Down
64 changes: 63 additions & 1 deletion src/renderer/pages/guid/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { buildDisplayMessage } from '@/renderer/utils/messageFiles';
import { hasSpecificModelCapability } from '@/renderer/utils/modelCapabilities';
import { updateWorkspaceTime } from '@/renderer/utils/workspaceHistory';
import { DEFAULT_CODEX_MODELS, DEFAULT_CODEX_MODEL_ID } from '@/common/codex/codexModels';
import { isAcpRoutedPresetType, type AcpBackend, type AcpBackendConfig, type PresetAgentType } from '@/types/acpTypes';
import { isAcpRoutedPresetType, type AcpBackend, type AcpBackendConfig, type AcpModelInfo, type PresetAgentType } from '@/types/acpTypes';
import { Button, ConfigProvider, Dropdown, Input, Menu, Message, Tooltip } from '@arco-design/web-react';
import { IconClose } from '@arco-design/web-react/icon';
import { ArrowUp, Down, FolderOpen, Plus, Robot, UploadOne } from '@icon-park/react';
Expand Down Expand Up @@ -314,6 +314,8 @@ const Guid: React.FC = () => {
const isPresetAgent = Boolean(selectedAgentInfo?.isPreset);
const [selectedMode, setSelectedMode] = useState<string>('default');
const [selectedCodexModel, setSelectedCodexModel] = useState<string>(DEFAULT_CODEX_MODEL_ID);
const [acpCachedModels, setAcpCachedModels] = useState<Record<string, AcpModelInfo>>({});
const [selectedAcpModel, setSelectedAcpModel] = useState<string | null>(null);
const [isPlusDropdownOpen, setIsPlusDropdownOpen] = useState(false);
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(true);
const [typewriterPlaceholder, setTypewriterPlaceholder] = useState('');
Expand Down Expand Up @@ -567,6 +569,33 @@ const Guid: React.FC = () => {
};
}, [availableCustomAgentIds]);

// Load cached ACP model lists for Guid page pre-selection
useEffect(() => {
let isActive = true;
ConfigStorage.get('acp.cachedModels')
.then((cached) => {
if (!isActive) return;
setAcpCachedModels(cached || {});
})
.catch((error) => {
console.error('Failed to load cached ACP models:', error);
});
return () => {
isActive = false;
};
}, []);

// Reset selected ACP model when agent changes
useEffect(() => {
const backend = selectedAgentKey.startsWith('custom:') ? 'custom' : selectedAgentKey;
const cachedInfo = acpCachedModels[backend];
if (cachedInfo?.currentModelId) {
setSelectedAcpModel(cachedInfo.currentModelId);
} else {
setSelectedAcpModel(null);
}
}, [selectedAgentKey, acpCachedModels]);

useEffect(() => {
if (mentionOpen) {
setMentionActiveIndex(0);
Expand Down Expand Up @@ -819,6 +848,12 @@ const Guid: React.FC = () => {
return getEffectiveAgentType(selectedAgentInfo);
}, [isPresetAgent, selectedAgent, selectedAgentInfo, getEffectiveAgentType, isMainAgentAvailable]);

// Cached model info for the currently selected ACP backend (for Guid page model selector)
const currentAcpCachedModelInfo = useMemo(() => {
const backend = selectedAgentKey.startsWith('custom:') ? 'custom' : selectedAgentKey;
return acpCachedModels[backend] || null;
}, [selectedAgentKey, acpCachedModels]);

/**
* 自动切换仅适用于 Gemini agent(可以同步检查可用性)
* Auto-switch only applies to Gemini agent (availability can be checked synchronously)
Expand Down Expand Up @@ -1209,6 +1244,8 @@ const Guid: React.FC = () => {
presetAssistantId: isPreset ? agentInfo?.customAgentId || acpAgentInfo?.customAgentId : undefined,
// Initial session mode from Guid page mode selector
sessionMode: selectedMode,
// Pre-selected model from Guid page (cached model list)
currentModelId: selectedAcpModel || undefined,
},
});

Expand Down Expand Up @@ -1735,6 +1772,31 @@ const Guid: React.FC = () => {
{DEFAULT_CODEX_MODELS.find((m) => m.id === selectedCodexModel)?.label || selectedCodexModel}
</Button>
</Dropdown>
) : currentAcpCachedModelInfo && currentAcpCachedModelInfo.availableModels.length > 0 ? (
currentAcpCachedModelInfo.canSwitch ? (
<Dropdown
trigger='click'
droplist={
<Menu selectedKeys={selectedAcpModel ? [selectedAcpModel] : []}>
{currentAcpCachedModelInfo.availableModels.map((model) => (
<Menu.Item key={model.id} className={model.id === selectedAcpModel ? '!bg-2' : ''} onClick={() => setSelectedAcpModel(model.id)}>
<span>{model.label}</span>
</Menu.Item>
))}
</Menu>
}
>
<Button className={'sendbox-model-btn'} shape='round'>
{currentAcpCachedModelInfo.availableModels.find((m) => m.id === selectedAcpModel)?.label || selectedAcpModel || t('conversation.welcome.useCliModel')}
</Button>
</Dropdown>
) : (
<Tooltip content={t('conversation.welcome.modelSwitchNotSupported')} position='top'>
<Button className={'sendbox-model-btn'} shape='round' style={{ cursor: 'default' }}>
{currentAcpCachedModelInfo.currentModelLabel || currentAcpCachedModelInfo.currentModelId || t('conversation.welcome.useCliModel')}
</Button>
</Tooltip>
)
) : (
<Tooltip content={t('conversation.welcome.modelSwitchNotSupported')} position='top'>
<Button className={'sendbox-model-btn'} shape='round' style={{ cursor: 'default' }}>
Expand Down
Loading