diff --git a/src/adapters/acp/acp.adapter.ts b/src/adapters/acp/acp.adapter.ts index df2c241..bcd36c3 100644 --- a/src/adapters/acp/acp.adapter.ts +++ b/src/adapters/acp/acp.adapter.ts @@ -22,6 +22,7 @@ import type AgentClientPlugin from "../../plugin"; import type { SlashCommand, SessionModeState, + SessionModelState, } from "src/domain/models/chat-session"; import { wrapCommandForWsl, @@ -518,9 +519,29 @@ export class AcpAdapter implements IAgentClient, IAcpClient { ); } + // Convert models from ACP format to domain format (experimental) + let models: SessionModelState | undefined; + if (sessionResult.models) { + models = { + availableModels: sessionResult.models.availableModels.map( + (m) => ({ + modelId: m.modelId, + name: m.name, + // Convert null to undefined for type compatibility + description: m.description ?? undefined, + }), + ), + currentModelId: sessionResult.models.currentModelId, + }; + this.logger.log( + `[AcpAdapter] Session models: ${models.availableModels.map((m) => m.modelId).join(", ")} (current: ${models.currentModelId})`, + ); + } + return { sessionId: sessionResult.sessionId, modes, + models, }; } catch (error) { this.logger.error("[AcpAdapter] New Session Error:", error); @@ -820,6 +841,35 @@ export class AcpAdapter implements IAgentClient, IAcpClient { } } + /** + * Implementation of IAgentClient.setSessionModel() + */ + async setSessionModel(sessionId: string, modelId: string): Promise { + if (!this.connection) { + throw new Error( + "Connection not initialized. Call initialize() first.", + ); + } + + this.logger.log( + `[AcpAdapter] Setting session model to: ${modelId} for session: ${sessionId}`, + ); + + try { + await this.connection.setSessionModel({ + sessionId, + modelId, + }); + this.logger.log(`[AcpAdapter] Session model set to: ${modelId}`); + } catch (error) { + this.logger.error( + "[AcpAdapter] Failed to set session model:", + error, + ); + throw error; + } + } + /** * Register a callback to receive chat messages from the agent. */ diff --git a/src/components/chat/ChatInput.tsx b/src/components/chat/ChatInput.tsx index 61423dd..6445b22 100644 --- a/src/components/chat/ChatInput.tsx +++ b/src/components/chat/ChatInput.tsx @@ -8,6 +8,7 @@ import type { NoteMetadata } from "../../domain/ports/vault-access.port"; import type { SlashCommand, SessionModeState, + SessionModelState, } from "../../domain/models/chat-session"; import type { UseMentionsReturn } from "../../hooks/useMentions"; import type { UseSlashCommandsReturn } from "../../hooks/useSlashCommands"; @@ -51,6 +52,10 @@ export interface ChatInputProps { modes?: SessionModeState; /** Callback when mode is changed */ onModeChange?: (modeId: string) => void; + /** Session model state (available models and current model) - experimental */ + models?: SessionModelState; + /** Callback when model is changed */ + onModelChange?: (modelId: string) => void; } /** @@ -82,6 +87,8 @@ export function ChatInput({ onRestoredMessageConsumed, modes, onModeChange, + models, + onModelChange, }: ChatInputProps) { const logger = useMemo(() => new Logger(plugin), [plugin]); @@ -95,6 +102,8 @@ export function ChatInput({ const sendButtonRef = useRef(null); const modeDropdownRef = useRef(null); const modeDropdownInstance = useRef(null); + const modelDropdownRef = useRef(null); + const modelDropdownInstance = useRef(null); /** * Common logic for setting cursor position after text replacement. @@ -495,6 +504,67 @@ export function ChatInput({ } }, [currentModeId]); + // Stable references for model callbacks + const onModelChangeRef = useRef(onModelChange); + onModelChangeRef.current = onModelChange; + + // Initialize Model dropdown (only when availableModels change) + const availableModels = models?.availableModels; + const currentModelId = models?.currentModelId; + + useEffect(() => { + const containerEl = modelDropdownRef.current; + if (!containerEl) return; + + // Only show dropdown if there are multiple models + if (!availableModels || availableModels.length <= 1) { + // Clean up existing dropdown if models become unavailable + if (modelDropdownInstance.current) { + containerEl.empty(); + modelDropdownInstance.current = null; + } + return; + } + + // Create dropdown if not exists + if (!modelDropdownInstance.current) { + const dropdown = new DropdownComponent(containerEl); + modelDropdownInstance.current = dropdown; + + // Add options + for (const model of availableModels) { + dropdown.addOption(model.modelId, model.name); + } + + // Set initial value + if (currentModelId) { + dropdown.setValue(currentModelId); + } + + // Handle change - use ref to avoid recreating dropdown on callback change + dropdown.onChange((value) => { + if (onModelChangeRef.current) { + onModelChangeRef.current(value); + } + }); + } + + // Cleanup on unmount or when availableModels change + return () => { + if (modelDropdownInstance.current) { + containerEl.empty(); + modelDropdownInstance.current = null; + } + }; + }, [availableModels]); + + // Update dropdown value when currentModelId changes (separate effect) + useEffect(() => { + if (modelDropdownInstance.current && currentModelId) { + modelDropdownInstance.current.setValue(currentModelId); + } + }, [currentModelId]); + // Button disabled state const isButtonDisabled = !isSending && (inputValue.trim() === "" || !isSessionReady); @@ -597,7 +667,7 @@ export function ChatInput({ )} - {/* Input Actions (Mode Selector + Send Button) */} + {/* Input Actions (Mode Selector + Model Selector + Send Button) */}
{/* Mode Selector */} {modes && modes.availableModes.length > 1 && ( @@ -619,6 +689,26 @@ export function ChatInput({
)} + {/* Model Selector (experimental) */} + {models && models.availableModels.length > 1 && ( +
m.modelId === models.currentModelId, + )?.description ?? "Select model" + } + > + { + if (el) setIcon(el, "chevron-down"); + }} + /> +
+ )} + {/* Send/Stop Button */}