Conversation
- Add `acp.cachedModels` storage type for per-backend model list caching - Cache model list in AcpAgentManager after agent.start() completes - Add ACP model dropdown selector to Guid page when cached models exist - Pass pre-selected model (currentModelId) through conversation creation - Show cached model info in AcpModelSelector before agent manager is created - Add backend/initialModelId props to AcpModelSelector for pre-first-message display
There was a problem hiding this comment.
Code Review
CRITICAL Issues
1. acp.cachedModels storage type is incorrect (will break cached model lookup)
File: src/common/storage.ts:46-52
// Cached model lists per ACP backend for Guid page pre-selection
'acp.cachedModels'?: Record<string, import('@/types/acpTypes').AcpModelInfo>;Problem: The PR stores one AcpModelInfo per backend (see cached[this.options.backend] = modelInfo), so the type must be Record<string, AcpModelInfo>. That part is fine. The issue is that AcpModelInfo is a single backend’s model info object, but the code in the renderer treats ConfigStorage.get('acp.cachedModels') as Record<string, AcpModelInfo> and then indexes by backend key. With the current type, this compiles, but it’s very likely wrong at runtime because AcpModelInfo typically contains arrays and flags, and you’re persisting it directly. If AcpModelInfo includes non-serializable fields (common for “info” objects), the storage layer may drop/alter them, causing availableModels to be missing and the UI to crash on .length access.
This is production-breaking if availableModels becomes undefined after persistence, because multiple places do:
modelInfo.availableModels.length > 0currentAcpCachedModelInfo.availableModels.map(...)
Fix: Persist a storage-safe DTO type instead of AcpModelInfo, and validate shape on read.
Example:
// src/types/acpTypes.ts (or a new file)
export type AcpCachedModelInfo = {
source: 'models';
currentModelId?: string;
currentModelLabel?: string;
canSwitch: boolean;
availableModels: Array<{ id: string; label: string }>;
};
// src/common/storage.ts
'acp.cachedModels'?: Record<string, import('@/types/acpTypes').AcpCachedModelInfo>;And in both writer/reader, guard:
if (cachedInfo?.availableModels?.length) { ... }HIGH Issues
2. New console.error added in renderer (violates debug hygiene / lint expectations)
File: src/renderer/pages/guid/index.tsx:579-586
ConfigStorage.get('acp.cachedModels')
.then((cached) => {
if (!isActive) return;
setAcpCachedModels(cached || {});
})
.catch((error) => {
console.error('Failed to load cached ACP models:', error);
});Problem: This PR introduces a new console.error in production renderer code. Your ESLint rule says no-console: warn and the PR test plan explicitly expects npm run lint to pass with 0 errors; even if it’s “warn”, this is still a regression in debug hygiene and will likely be flagged in review/CI expectations.
Fix: Replace with the project’s logging/toast mechanism or silently ignore (consistent with AcpModelSelector), e.g.:
.catch(() => {
// Silently ignore - cached models are optional
});or use a centralized logger if the project has one.
3. Immutability violation when updating cached models object before persisting
File: src/process/task/AcpAgentManager.ts:724-733
const cached = (await ProcessConfig.get('acp.cachedModels')) || {};
cached[this.options.backend] = modelInfo;
await ProcessConfig.set('acp.cachedModels', cached);Problem: This mutates the cached object in-place. In this project, immutability is a critical convention (“always create new objects, never mutate”). Even though this is in main process config code, it still risks subtle bugs if ProcessConfig.get() returns a shared reference (or if future code keeps references).
Fix: Create a new object:
const cached = (await ProcessConfig.get('acp.cachedModels')) || {};
await ProcessConfig.set('acp.cachedModels', {
...cached,
[this.options.backend]: modelInfo,
});MEDIUM Issues
4. Cached model fallback in AcpModelSelector unnecessarily requires initialModelId
File: src/renderer/components/AcpModelSelector.tsx:33-74
The fallback only runs when both backend && initialModelId are present:
} else if (backend && initialModelId) {
void loadCachedModelInfo(backend, initialModelId, cancelled);
}Recommendation: If the goal is “show cached model dropdown for ACP agents (when available)”, you can still show the dropdown even without a preselected model ID by using cachedInfo.currentModelId (if present) or leaving selection empty. This would make the feature more robust for conversations created without currentModelId (older history, migrations, etc.).
Concrete adjustment:
- Trigger cached load when
backendexists (not necessarilyinitialModelId) - Inside
loadCachedModelInfo, choosemodelId ?? cachedInfo.currentModelId
Summary
| Level | Count |
|---|---|
| CRITICAL | 1 |
| HIGH | 2 |
| MEDIUM | 1 |
🤖 This review was generated by AI and may contain inaccuracies. Please focus on issues you agree with and feel free to disregard any that seem incorrect. Thank you for your contribution!
Note: The following inputs were truncated due to size limits: file contents (>80K chars).
Please review the omitted portions manually.
- Track userModelOverride to re-assert model before each prompt - Inject model switch notice into Claude prompts (mirrors /model CLI behavior) - Prefer session/set_model over set_config_option for direct CLI control - Eagerly update configOptions cache on setModel/setConfigOption responses - Fix config_options_update → config_option_update event name typo - Handle config_option_update in AcpAdapter (no chat message conversion) - Remove --prefer-offline flag from npx spawn args
- Use immutable spread instead of in-place mutation in cacheModelList() - Add defensive ?. access on availableModels for storage deserialization safety - Replace console.error with silent catch for optional cached models loading - Allow cached model fallback without initialModelId (uses cachedInfo.currentModelId)
Code Review ResponseThanks for the thorough review! All 4 issues have been addressed in commit 1. CRITICAL —
|
OpenCode's mode list (build/plan) has no 'default' entry, so when
getMode IPC returns { mode: 'default', initialized: false } before
the agent manager is created, the mode selector displays incorrectly.
- Thread sessionMode through ChatConversation → AcpChat → AcpSendBox
→ AgentModeSelector as initialMode prop
- Skip overwriting currentMode from getMode IPC when initialized=false
- Prevent agent's initial emitModelInfo from overwriting pre-selected model by checking initialModelId in stream handler - Fall back to cached model list when IPC returns empty availableModels (agent not fully initialized) to keep dropdown always functional - Track manual model switches via hasUserChangedModel ref so the protection only applies until user explicitly changes model
Phase 1: New Codex sessions use ACP protocol (`type: 'acp', backend: 'codex'`) via @zed-industries/codex-acp adapter. Old `type: 'codex'` sessions remain compatible through existing CodexAgentManager. - Add `connectCodex()` to AcpConnection (NPX-based, like connectClaude) - Add 'codex' to ACP_ROUTED_PRESET_TYPES for automatic routing - Update ACP_BACKENDS_ALL codex config with codex-acp defaults - Remove Guid page codex-specific branch (now falls through to ACP) - Remove Codex model dropdown (now uses ACP cached model selector) - Fix codex-acp cwd requirement (absolute path, like Copilot) - Delete unused codexModelCache.ts
…d utils Break down the ~1930-line index.tsx into a modular structure: - types.ts: local type definitions (AvailableAgent, MentionOption, etc.) - constants.ts: CUSTOM_AVATAR_IMAGE_MAP - utils/modelUtils.ts: getAvailableModels, hasAvailableModels - utils/caretUtils.ts: measureCaretTop, scrollCaretToLastLine - hooks/useGuidAgentSelection.ts: agent selection, availability, preset logic - hooks/useGuidMention.ts: @ mention system - hooks/useGuidInput.ts: input state, file handling, drag/paste - hooks/useGuidModelSelection.ts: Gemini model list and selection - hooks/useGuidSend.ts: send logic for all conversation types - hooks/useTypewriterPlaceholder.ts: typewriter animation - components/AgentPillBar.tsx: top agent selector pill bar - components/GuidInputCard.tsx: input card container - components/GuidActionRow.tsx: action row (plus/model/mode/send) - components/GuidModelSelector.tsx: Gemini + ACP model dropdown - components/MentionDropdown.tsx: mention dropdown menu - components/MentionSelectorBadge.tsx: @AgentName badge - components/PresetAgentTag.tsx: preset assistant tag - components/AssistantSelectionArea.tsx: assistant selection (detail/list) - components/QuickActionButtons.tsx: bottom quick action buttons - GuidPage.tsx: main container composing all hooks and components - index.tsx: re-export only
Show shimmer skeletons for AgentPillBar and AssistantSelectionArea while availableAgents is still loading (undefined), replacing them seamlessly once data arrives.
- Parse models from _meta.models fallback in session/new response (iFlow puts model list under _meta instead of top-level models) - Update acp.cachedModels on model switch so Guid page defaults to the user's last selected model in new conversations - Add debug log for session/new response to diagnose model list issues
…-applying Stale cached models (e.g., gpt-5.3-codex) that no longer exist would be blindly passed to setModelByConfigOption, which polluted the local configOptions cache and caused repeated Internal errors. Now validate the persisted model ID exists in the fresh availableModels list from session/new before attempting to re-apply it.
…rade When upgrading npx bridge packages (claude-agent-acp, codex-acp), users with the old version cached hit "notarget" errors because --prefer-offline serves stale metadata. Now detect this error pattern, automatically run `npm cache clean --force`, and retry the connection. Also: - Add --yes flag to claude and codex npx calls (codex was missing it) to prevent interactive prompts from blocking piped stdio - Include stderr in spawnError so the retry logic can match error patterns regardless of exit timing
Previously, when enabledSkills was undefined (non-preset agent scenario), all skills from the skills/ directory were loaded. This affected both ACP (Claude/OpenCode/Codex) and Gemini agent paths: - AcpSkillManager.discoverSkills: early return when enabledSkills is undefined or empty, only builtin skills (_builtin/cron) are loaded - Gemini config.ts: only call loadSkillsFromDir when enabledSkills is explicitly specified with entries - GeminiAgent.initialize: clear all SkillManager skills when enabledSkills is not specified, since aioncli-core rediscovers all skills on initialize Builtin skills (cron) remain unaffected - always injected via system instructions for Gemini or via _builtin/ directory for ACP.
Closes #931
Summary
This PR improves the Guid (home) page across four areas:
1. ACP Model Pre-selection
After the first conversation with an ACP agent, its model list is cached locally. On subsequent visits, the Guid page shows a model dropdown pre-populated from cache. The selected model carries through to the conversation page.
session/set_modelsrc/common/storage.tsacp.cachedModelstype definitionsrc/common/ipcBridge.tscurrentModelIdtoICreateConversationParamssrc/process/task/AcpAgentManager.tscacheModelList(), called afteragent.start()src/process/initAgent.tscurrentModelIdto ACP conversation extrasrc/renderer/components/AcpModelSelector.tsxbackend/initialModelIdprops, fallback to cached modelssrc/agent/acp/index.tsuserModelOverride, re-assert before each prompt, inject model switch notice for Claudesrc/agent/acp/AcpConnection.tsset_model, eagerly update configOptions cache, fix event name typosrc/agent/acp/AcpAdapter.tsconfig_option_updateeventWhy special handling for Claude backend? (env_info cache issue)
When switching models mid-conversation via
set_model, the API call correctly uses the new model, but the AI incorrectly self-identifies as the old model. For example, after switching from Opus to Haiku:Root cause: Claude Code CLI caches the
env_infosystem prompt section (which contains "You are powered by {model}") withcacheBreak: false. Once computed on the first prompt, it's never recomputed for the session lifetime — regardless of model switches.Neither
/modelnorset_modelclears this cache. It's only cleared by/clear(destroys conversation) or conversation compaction (automatic for long conversations).Our workaround: inject a
<system-reminder>into the next user message after a model switch, telling the AI its model has changed. This mirrors what the terminal/modelcommand prints to stdout.This is a Claude Code CLI limitation, not an AionUI bug. Identical across CLI versions 2.1.45–2.1.50.
2. Codex → ACP Migration
New Codex sessions are created as
type: 'acp', backend: 'codex'instead oftype: 'codex', routed through the existing ACP infrastructure via the@zed-industries/codex-acpbridge.Old
type: 'codex'history sessions still work —CodexAgentManagerand all codex-specific code are preserved for backward compatibility.src/agent/acp/AcpConnection.tsconnectCodex()(NPX-based,@zed-industries/codex-acp@0.9.4); fix absolute cwd for codexsrc/types/acpTypes.ts'codex'toACP_ROUTED_PRESET_TYPES; update codex backend configcodex-acp capabilities (verified)
session/newreturnsmodels+configOptions+modessession/set_modelsession/set_config_optionmodel_idormodel_id/reasoning_effort(e.g.o3/high)What's preserved for future cleanup
src/agent/codex/directory,CodexAgentManager, codex conversation components, codex IPC bridgescodexModels.ts(DEFAULT_CODEX_MODELS) — still used byCodexAgentManagerfor old sessions3. Bug Fixes
Mode loss on Guid→conversation navigation
OpenCode's mode list is
build/plan— nodefaultentry. Before the first message,AcpAgentManagerdoesn't exist, sogetModeIPC returns{ mode: 'default', initialized: false }. TheAgentModeSelectorthen overwrites the user's Guid selection (e.g.plan) with'default', which doesn't match any option.Fix: thread
sessionModefrom conversation extra toAgentModeSelectorasinitialMode; skip IPC overwrite wheninitialized === false.src/renderer/components/AgentModeSelector.tsxgetModeoverwrite when uninitializedsrc/renderer/pages/conversation/ChatConversation.tsxsessionModetoAcpChatsrc/renderer/pages/conversation/acp/AcpChat.tsxsessionModepropsrc/renderer/pages/conversation/acp/AcpSendBox.tsxsessionModeasinitialMode4. Guid Page Refactor & Skeleton Loading
Refactor
Split the monolithic 700-line
GuidPageinto modular hooks, components, and utilities:useGuidAgentSelection,useGuidInput,useGuidMention,useGuidModelSelection,useGuidSend,useTypewriterPlaceholderAgentPillBar,AssistantSelectionArea,GuidActionRow,GuidInputCard,GuidModelSelector,GuidSkeleton,MentionDropdown,MentionSelectorBadge,PresetAgentTag,QuickActionButtonsguidUtils.ts(conversation creation, agent resolution)types.ts,constants.tsSkeleton loading
Shimmer placeholders eliminate layout shift while async data loads:
AgentPillBarSkeleton— 1 wide selected pill + 4 circles, shown whileavailableAgents === undefinedAssistantsSkeleton— 3 pill-shaped placeholders (80/100/90px), shown while custom agents load=== undefinedto distinguish "loading" vs "loaded but empty"--color-fill-2/--color-fill-3) for automatic dark/light theme supportsrc/renderer/pages/guid/GuidPage.tsxsrc/renderer/pages/guid/index.module.css@keyframes guid-shimmer,.skeleton,.skeletonPillsrc/renderer/pages/guid/components/GuidSkeleton.tsxsrc/renderer/pages/guid/components/*.tsxsrc/renderer/pages/guid/hooks/*.tssrc/renderer/pages/guid/utils/guidUtils.tssrc/renderer/pages/guid/types.tssrc/renderer/pages/guid/constants.ts5. Skills Loading Fix
Previously, when using a non-preset agent (e.g. directly selecting Claude or Gemini), all optional skills from the
skills/directory were loaded. Now optional skills are only loaded when the corresponding assistant is enabled and hasenabledSkillsconfigured. Builtin skills (_builtin/cron) remain always available.src/process/task/AcpSkillManager.tsdiscoverSkills()whenenabledSkillsis undefined/empty — only builtin skills loadedsrc/agent/gemini/cli/config.tsloadSkillsFromDir()entirely whenenabledSkillsis not specifiedsrc/agent/gemini/index.tsinitialize()whenenabledSkillsis not specified (aioncli-core rediscovers all on init)Test plan
Model pre-selection
Mode preservation
planon Guid → conversation showsPlan(not empty/reset)Codex ACP
type: 'acp'conversationtype: 'codex'history sessions still open and workSkeleton loading
Skills loading
enabledSkillsare loaded_builtin/cronskill always available regardless of agent typeGeneral
npm run lintpasses with 0 errors