diff --git a/package-lock.json b/package-lock.json index 412682e..ee0ee8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,16 +9,18 @@ "version": "0.11.0-preview.2", "license": "Apache-2.0", "dependencies": { - "@agentclientprotocol/sdk": "^0.14.1", + "@agentclientprotocol/sdk": "^0.28.1", "@codemirror/state": "6.5.0", "@codemirror/view": "^6.35.0", "@tanstack/react-virtual": "^3.13.23", "diff": "^8.0.2", "react": "18.3.1", "react-dom": "18.3.1", - "semver": "^7.7.3" + "semver": "^7.7.3", + "zod": "^4.4.3" }, "devDependencies": { + "@eslint/json": "^0.14.0", "@types/node": "^16.11.6", "@types/react": "18.3.29", "@types/react-dom": "18.3.7", @@ -38,9 +40,9 @@ } }, "node_modules/@agentclientprotocol/sdk": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.14.1.tgz", - "integrity": "sha512-b6r3PS3Nly+Wyw9U+0nOr47bV8tfS476EgyEMhoKvJCZLbgqoDFN7DJwkxL88RR0aiOqOYV1ZnESHqb+RmdH8w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.28.1.tgz", + "integrity": "sha512-Z2Frs6YtPhnZZ+XwFXyQkRDXY0fn8FjCalEs0W4yUhQnY4TztmNq0/RnfzWdFN3vqT3h0jTz5klzYbZHGxCDyQ==", "license": "Apache-2.0", "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" @@ -1026,7 +1028,6 @@ "integrity": "sha512-rvR/EZtvUG3p9uqrSmcDJPYSH7atmWr0RnFWN6m917MAPx82+zQgPUmDu0whPFG6XTyM0vB/hR6c1Q63OaYtCQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@eslint/core": "^0.17.0", "@eslint/plugin-kit": "^0.4.1", @@ -1119,7 +1120,6 @@ "integrity": "sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=18" } @@ -7010,14 +7010,6 @@ "loose-envify": "^1.1.0" } }, - "node_modules/search-insights": { - "version": "2.17.3", - "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", - "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/semver": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", @@ -8817,7 +8809,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index dc24a4c..0c645f0 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "author": "", "license": "Apache-2.0", "devDependencies": { + "@eslint/json": "^0.14.0", "@types/node": "^16.11.6", "@types/react": "18.3.29", "@types/react-dom": "18.3.7", @@ -39,13 +40,14 @@ "vitest": "^4.1.8" }, "dependencies": { - "@agentclientprotocol/sdk": "^0.14.1", + "@agentclientprotocol/sdk": "^0.28.1", "@codemirror/state": "6.5.0", "@codemirror/view": "^6.35.0", "@tanstack/react-virtual": "^3.13.23", "diff": "^8.0.2", "react": "18.3.1", "react-dom": "18.3.1", - "semver": "^7.7.3" + "semver": "^7.7.3", + "zod": "^4.4.3" } } diff --git a/src/acp/acp-client.ts b/src/acp/acp-client.ts index 222d2f3..f369843 100644 --- a/src/acp/acp-client.ts +++ b/src/acp/acp-client.ts @@ -75,6 +75,11 @@ export interface TerminalOutputResult { */ export class AcpClient { // Connection & process + // NOTE: ClientSideConnection is deprecated in ACP 0.28 in favor of the + // client() builder, but that uses a scoped connectWith(...) callback rather + // than a persistent connection object. Migrating AcpClient's lifecycle to it + // is a separate architectural change (tracked as a follow-up). + // eslint-disable-next-line @typescript-eslint/no-deprecated private connection: acp.ClientSideConnection | null = null; private agentProcess: ChildProcess | null = null; private currentConfig: AgentConfig | null = null; @@ -379,6 +384,7 @@ export class AcpClient { ); const stream = acp.ndJsonStream(input, output); + // eslint-disable-next-line @typescript-eslint/no-deprecated -- see note on `connection` field (ClientSideConnection migration is a follow-up) this.connection = new acp.ClientSideConnection( () => this.handler, stream, @@ -686,31 +692,6 @@ export class AcpClient { } } - /** - * DEPRECATED: Use setSessionConfigOption instead. - */ - async setSessionModel(sessionId: string, modelId: string): Promise { - const connection = this.requireConnection(); - - this.logger.log( - `[AcpClient] Setting session model to: ${modelId} for session: ${sessionId}`, - ); - - try { - await connection.unstable_setSessionModel({ - sessionId, - modelId, - }); - this.logger.log(`[AcpClient] Session model set to: ${modelId}`); - } catch (error) { - this.logger.error( - "[AcpClient] Failed to set session model:", - error, - ); - throw error; - } - } - /** * Set a session configuration option. * @@ -789,6 +770,7 @@ export class AcpClient { * Assert that the ACP connection is initialized and return it. * @throws Error if connection is not available */ + // eslint-disable-next-line @typescript-eslint/no-deprecated -- see note on `connection` field (ClientSideConnection migration is a follow-up) private requireConnection(): acp.ClientSideConnection { if (!this.connection) { throw new Error( @@ -848,7 +830,7 @@ export class AcpClient { const filterCwd = cwd ? this.toSessionCwd(cwd) : undefined; - const response = await connection.unstable_listSessions({ + const response = await connection.listSessions({ cwd: filterCwd ?? null, cursor: cursor ?? null, }); @@ -931,7 +913,7 @@ export class AcpClient { try { this.logger.log(`[AcpClient] Resuming session: ${sessionId}...`); - const response = await connection.unstable_resumeSession({ + const response = await connection.resumeSession({ sessionId, cwd: this.toSessionCwd(cwd), mcpServers: [], diff --git a/src/acp/acp-handler.ts b/src/acp/acp-handler.ts index ede01b4..554c367 100644 --- a/src/acp/acp-handler.ts +++ b/src/acp/acp-handler.ts @@ -232,8 +232,8 @@ export class AcpHandler { } killTerminal( - params: acp.KillTerminalCommandRequest, - ): Promise { + params: acp.KillTerminalRequest, + ): Promise { const success = this.terminalManager.killTerminal(params.terminalId); if (!success) { throw new Error(`Terminal ${params.terminalId} not found`); diff --git a/src/acp/type-converter.ts b/src/acp/type-converter.ts index 18574ff..9b7049a 100644 --- a/src/acp/type-converter.ts +++ b/src/acp/type-converter.ts @@ -21,7 +21,6 @@ import type { interface AcpSessionResponse { sessionId?: string; modes?: acp.SessionModeState | null; - models?: acp.SessionModelState | null; configOptions?: acp.SessionConfigOption[] | null; } @@ -100,15 +99,29 @@ export class AcpTypeConverter { static toSessionConfigOptions( acpOptions: acp.SessionConfigOption[], ): SessionConfigOption[] { - return acpOptions.map((opt) => ({ - id: opt.id, - name: opt.name, - description: opt.description ?? undefined, - category: opt.category ?? undefined, - type: opt.type, - currentValue: opt.currentValue, - options: this.toSessionConfigSelectOptions(opt.options), - })); + return acpOptions.map((opt): SessionConfigOption => { + const base = { + id: opt.id, + name: opt.name, + description: opt.description ?? undefined, + category: opt.category ?? undefined, + }; + // boolean options (ACP 0.28+) are carried as data; the UI renders + // and sets only select options for now. + if (opt.type === "boolean") { + return { + ...base, + type: "boolean", + currentValue: opt.currentValue, + }; + } + return { + ...base, + type: "select", + currentValue: opt.currentValue, + options: this.toSessionConfigSelectOptions(opt.options), + }; + }); } private static toSessionConfigSelectOptions( @@ -166,18 +179,6 @@ export class AcpTypeConverter { }; } - let models: SessionResult["models"]; - if (response.models) { - models = { - availableModels: response.models.availableModels.map((m) => ({ - modelId: m.modelId, - name: m.name, - description: m.description ?? undefined, - })), - currentModelId: response.models.currentModelId, - }; - } - const configOptions = response.configOptions ? this.toSessionConfigOptions(response.configOptions) : undefined; @@ -185,7 +186,6 @@ export class AcpTypeConverter { return { sessionId, modes, - models, configOptions, }; } diff --git a/src/hooks/useAgent.ts b/src/hooks/useAgent.ts index 3a8c2b0..8f02d86 100644 --- a/src/hooks/useAgent.ts +++ b/src/hooks/useAgent.ts @@ -29,7 +29,6 @@ import type { ChatMessage, ActivePermission } from "../types/chat"; import type { ChatSession, SessionModeState, - SessionModelState, SessionConfigOption, } from "../types/session"; import type { AgentDisplayInfo } from "../services/session-helpers"; @@ -66,13 +65,11 @@ export interface UseAgentReturn { updateSessionFromLoad: ( sessionId: string, modes?: SessionModeState, - models?: SessionModelState, configOptions?: SessionConfigOption[], ) => Promise; // Config setMode: (modeId: string) => Promise; - setModel: (modelId: string) => Promise; setConfigOption: (configId: string, value: string) => Promise; // Message operations @@ -199,7 +196,6 @@ export function useAgent( // Config setMode: agentSession.setMode, - setModel: agentSession.setModel, setConfigOption: agentSession.setConfigOption, // Message operations @@ -232,7 +228,6 @@ export function useAgent( agentSession.getAvailableAgents, agentSession.updateSessionFromLoad, agentSession.setMode, - agentSession.setModel, agentSession.setConfigOption, agentMessages.sendMessage, agentMessages.clearMessages, diff --git a/src/hooks/useAgentSession.ts b/src/hooks/useAgentSession.ts index 7ded7fc..2f335f8 100644 --- a/src/hooks/useAgentSession.ts +++ b/src/hooks/useAgentSession.ts @@ -11,7 +11,6 @@ const { useState, useCallback, useRef } = React; import type { ChatSession, SessionModeState, - SessionModelState, SessionUpdate, SessionConfigOption, } from "../types/session"; @@ -60,13 +59,11 @@ export interface UseAgentSessionReturn { updateSessionFromLoad: ( sessionId: string, modes?: SessionModeState, - models?: SessionModelState, configOptions?: SessionConfigOption[], ) => Promise; // Config setMode: (modeId: string) => Promise; - setModel: (modelId: string) => Promise; setConfigOption: (configId: string, value: string) => Promise; /** Handle session-level updates (commands, mode, config, usage, error) */ @@ -184,7 +181,6 @@ export function useAgentSession( authMethods: [], availableCommands: undefined, modes: undefined, - models: undefined, configOptions: undefined, usage: undefined, promptCapabilities: prev.promptCapabilities, @@ -225,14 +221,13 @@ export function useAgentSession( const sessionResult = await agentClient.newSession(effectiveCwd); - // Pre-compute restored modes/models/configOptions BEFORE + // Pre-compute restored modes/configOptions BEFORE // marking state as "ready" to avoid a UI race: without this, // the dropdowns briefly show the agent's default values and // a message sent during the window hits the agent in the // wrong mode. With this, the first render after session // creation already shows the user's saved selection. let finalModes = sessionResult.modes; - let finalModels = sessionResult.models; let finalConfigOptions = sessionResult.configOptions; if (sessionResult.configOptions && sessionResult.sessionId) { @@ -262,11 +257,9 @@ export function useAgentSession( const restored = await restoreLegacyConfig( agentClient, sessionResult, - settings.lastUsedModels[agentId], settings.lastUsedModes[agentId], ); finalModes = restored.modes; - finalModels = restored.models; } setSession((prev) => ({ @@ -275,7 +268,6 @@ export function useAgentSession( state: "ready", authMethods: initResult?.authMethods ?? [], modes: finalModes, - models: finalModels, configOptions: finalConfigOptions, promptCapabilities: initResult ? initResult.promptCapabilities @@ -356,7 +348,6 @@ export function useAgentSession( async ( sessionId: string, modes?: SessionModeState, - models?: SessionModelState, configOptions?: SessionConfigOption[], ) => { // Pre-compute restored config BEFORE marking ready to avoid a UI @@ -368,7 +359,6 @@ export function useAgentSession( const agentId = s.agentId; let finalModes = modes; - let finalModels = models; let finalConfigOptions = configOptions; if (configOptions && sessionId) { @@ -397,12 +387,10 @@ export function useAgentSession( } else if (sessionId && modes) { const restored = await restoreLegacyConfig( agentClient, - { sessionId, modes, models, configOptions: undefined }, - settings.lastUsedModels[agentId], + { sessionId, modes, configOptions: undefined }, settings.lastUsedModes[agentId], ); finalModes = restored.modes; - finalModels = restored.models; } setSession((prev) => ({ @@ -410,7 +398,6 @@ export function useAgentSession( sessionId, state: "ready", modes: finalModes ?? prev.modes, - models: finalModels ?? prev.models, configOptions: finalConfigOptions ?? prev.configOptions, lastActivityAt: new Date(), })); @@ -423,44 +410,33 @@ export function useAgentSession( // ============================================================ const setLegacyConfigValue = useCallback( - async (kind: "mode" | "model", value: string) => { + async (value: string) => { const s = sessionRef.current; if (!s.sessionId) { - getLogger().debug(`Cannot set ${kind}: no active session`); + getLogger().debug("Cannot set mode: no active session"); return; } - const previousValue = - kind === "mode" - ? s.modes?.currentModeId - : s.models?.currentModelId; + const previousValue = s.modes?.currentModeId; - setSession((prev) => applyLegacyValue(prev, kind, value)); + setSession((prev) => applyLegacyValue(prev, value)); try { - if (kind === "mode") { - await agentClient.setSessionMode(s.sessionId, value); - } else { - await agentClient.setSessionModel(s.sessionId, value); - } + await agentClient.setSessionMode(s.sessionId, value); if (s.agentId) { - const persistKey = - kind === "mode" ? "lastUsedModes" : "lastUsedModels"; const currentSettings = settingsAccess.getSnapshot(); void settingsAccess.updateSettings({ - [persistKey]: { - ...currentSettings[persistKey], + lastUsedModes: { + ...currentSettings.lastUsedModes, [s.agentId]: value, }, }); } } catch (error) { - getLogger().error(`Failed to set ${kind}:`, error); + getLogger().error("Failed to set mode:", error); if (previousValue) { - setSession((prev) => - applyLegacyValue(prev, kind, previousValue), - ); + setSession((prev) => applyLegacyValue(prev, previousValue)); } } }, @@ -468,12 +444,7 @@ export function useAgentSession( ); const setMode = useCallback( - (modeId: string) => setLegacyConfigValue("mode", modeId), - [setLegacyConfigValue], - ); - - const setModel = useCallback( - (modelId: string) => setLegacyConfigValue("model", modelId), + (modeId: string) => setLegacyConfigValue(modeId), [setLegacyConfigValue], ); @@ -494,7 +465,7 @@ export function useAgentSession( return { ...prev, configOptions: prev.configOptions.map((opt) => - opt.id === configId + opt.id === configId && opt.type === "select" ? { ...opt, currentValue: value } : opt, ), @@ -577,7 +548,6 @@ export function useAgentSession( getAvailableAgents, updateSessionFromLoad, setMode, - setModel, setConfigOption, handleSessionUpdate, }; diff --git a/src/hooks/useChatActions.ts b/src/hooks/useChatActions.ts index bda8a48..583bb56 100644 --- a/src/hooks/useChatActions.ts +++ b/src/hooks/useChatActions.ts @@ -44,7 +44,6 @@ export interface UseChatActionsReturn { // Config actions handleSetMode: (modeId: string) => Promise; - handleSetModel: (modelId: string) => Promise; handleSetConfigOption: (configId: string, value: string) => Promise; // UI state actions @@ -343,13 +342,6 @@ export function useChatActions( [agent.setMode], ); - const handleSetModel = useCallback( - async (modelId: string) => { - await agent.setModel(modelId); - }, - [agent.setModel], - ); - const handleSetConfigOption = useCallback( async (configId: string, value: string) => { await agent.setConfigOption(configId, value); @@ -385,7 +377,6 @@ export function useChatActions( handleSwitchAgent, handleRestartAgent, handleSetMode, - handleSetModel, handleSetConfigOption, handleClearError, handleClearAgentUpdate, diff --git a/src/hooks/useSessionHistory.ts b/src/hooks/useSessionHistory.ts index 3aff8ea..60b3611 100644 --- a/src/hooks/useSessionHistory.ts +++ b/src/hooks/useSessionHistory.ts @@ -7,7 +7,6 @@ import type { SavedSessionInfo, ChatSession, SessionModeState, - SessionModelState, SessionConfigOption, AgentCapabilities, } from "../types/session"; @@ -57,13 +56,11 @@ export interface SessionLoadCallback { /** * @param sessionId - ID of the session (new session ID for fork) * @param modes - Available modes from the session - * @param models - Available models from the session * @param configOptions - Config options from the session */ ( sessionId: string, modes?: SessionModeState, - models?: SessionModelState, configOptions?: SessionConfigOption[], ): void; } @@ -327,9 +324,7 @@ export function useSessionHistory( merged.every((s, i) => s.title === prev[i].title); return unchanged ? prev : merged; }); - setLocalSessionIds( - new Set(localSessions.map((s) => s.sessionId)), - ); + setLocalSessionIds(new Set(localSessions.map((s) => s.sessionId))); }); }, [settingsAccess, session.agentId]); @@ -542,7 +537,7 @@ export function useSessionHistory( try { // IMPORTANT: Update session.sessionId BEFORE calling restore // so that session/update notifications are not ignored - onSessionLoad(sessionId, undefined, undefined, undefined); + onSessionLoad(sessionId, undefined, undefined); if (capabilities.canLoad) { // Check local messages first to decide whether to use them or agent replay @@ -561,7 +556,6 @@ export function useSessionHistory( onSessionLoad( result.sessionId, result.modes, - result.models, result.configOptions, ); onMessagesRestore(localMessages); @@ -577,7 +571,6 @@ export function useSessionHistory( onSessionLoad( result.sessionId, result.modes, - result.models, result.configOptions, ); } @@ -590,7 +583,6 @@ export function useSessionHistory( onSessionLoad( result.sessionId, result.modes, - result.models, result.configOptions, ); @@ -641,7 +633,6 @@ export function useSessionHistory( onSessionLoad( result.sessionId, result.modes, - result.models, result.configOptions, ); @@ -751,11 +742,10 @@ export function useSessionHistory( ); try { - await settingsAccess.updateSessionTitle( - sessionId, - newTitle, - { agentId: session.agentId, cwd: sessionCwd }, - ); + await settingsAccess.updateSessionTitle(sessionId, newTitle, { + agentId: session.agentId, + cwd: sessionCwd, + }); invalidateCache(); } catch (err) { // Rollback diff --git a/src/services/session-helpers.ts b/src/services/session-helpers.ts index 241ba29..f375df4 100644 --- a/src/services/session-helpers.ts +++ b/src/services/session-helpers.ts @@ -184,7 +184,6 @@ export function createInitialSession( authMethods: [], availableCommands: undefined, modes: undefined, - models: undefined, createdAt: new Date(), lastActivityAt: new Date(), workingDirectory, diff --git a/src/services/session-state.ts b/src/services/session-state.ts index e61296d..5ad69c0 100644 --- a/src/services/session-state.ts +++ b/src/services/session-state.ts @@ -24,15 +24,10 @@ import type { AcpClient } from "../acp/acp-client"; */ export function applyLegacyValue( prev: ChatSession, - kind: "mode" | "model", value: string, ): ChatSession { - if (kind === "mode") { - if (!prev.modes) return prev; - return { ...prev, modes: { ...prev.modes, currentModeId: value } }; - } - if (!prev.models) return prev; - return { ...prev, models: { ...prev.models, currentModelId: value } }; + if (!prev.modes) return prev; + return { ...prev, modes: { ...prev.modes, currentModeId: value } }; } // ============================================================================ @@ -53,7 +48,7 @@ export async function tryRestoreConfigOption( if (!savedValue) return configOptions; const option = configOptions.find((o) => o.category === category); - if (!option) return configOptions; + if (!option || option.type !== "select") return configOptions; if (savedValue === option.currentValue) return configOptions; if ( !flattenConfigSelectOptions(option.options).some( @@ -91,7 +86,7 @@ export async function restoreSavedConfigOptions( let result = configOptions; for (const [optionId, savedValue] of Object.entries(savedById)) { const option = result.find((o) => o.id === optionId); - if (!option) continue; + if (!option || option.type !== "select") continue; if (savedValue === option.currentValue) continue; if ( !flattenConfigSelectOptions(option.options).some( @@ -127,34 +122,13 @@ export async function restoreSavedConfigOptions( export async function restoreLegacyConfig( agentClient: AcpClient, sessionResult: SessionResult, - savedModelId: string | undefined, savedModeId: string | undefined, ): Promise<{ modes: SessionResult["modes"]; - models: SessionResult["models"]; }> { let modes = sessionResult.modes; - let models = sessionResult.models; - if (!sessionResult.sessionId) return { modes, models }; - - // Legacy model restore - if (models && savedModelId) { - if ( - savedModelId !== models.currentModelId && - models.availableModels.some((m) => m.modelId === savedModelId) - ) { - try { - await agentClient.setSessionModel( - sessionResult.sessionId, - savedModelId, - ); - models = { ...models, currentModelId: savedModelId }; - } catch { - // Agent default is fine as fallback - } - } - } + if (!sessionResult.sessionId) return { modes }; // Legacy mode restore if (modes && savedModeId) { @@ -174,5 +148,5 @@ export async function restoreLegacyConfig( } } - return { modes, models }; + return { modes }; } diff --git a/src/types/session.ts b/src/types/session.ts index 4b572af..7288632 100644 --- a/src/types/session.ts +++ b/src/types/session.ts @@ -122,44 +122,6 @@ export interface SessionModeState { currentModeId: string; } -// ============================================================================ -// Model (Experimental) -// ============================================================================ - -/** - * Represents an AI model available in a session. - * - * Models determine which AI model is used for responses. - * This is an experimental feature and may change. - */ -/** DEPRECATED: Use SessionConfigOption instead. Kept for backward compatibility. */ -export interface SessionModel { - /** Unique identifier for this model (e.g., "claude-sonnet-4") */ - modelId: string; - - /** Human-readable name for display */ - name: string; - - /** Optional description of this model */ - description?: string; -} - -/** - * State of available models in a session. - * - * Contains both the list of available models and the currently active model. - * Updated via NewSessionResponse initially. - * Note: Unlike modes, there is no dedicated notification for model changes. - */ -/** DEPRECATED: Use SessionConfigOption instead. Kept for backward compatibility. */ -export interface SessionModelState { - /** List of models available in this session */ - availableModels: SessionModel[]; - - /** ID of the currently active model */ - currentModelId: string; -} - /** * Context window usage and cost information for a session. * Reported by the agent via `usage_update` session notifications. @@ -218,12 +180,6 @@ export interface ChatSession { */ modes?: SessionModeState; - /** - * DEPRECATED: Use configOptions instead. Kept for backward compatibility - * with agents that don't support configOptions. - */ - models?: SessionModelState; - /** * Session configuration options (mode, model, thought_level, etc.). * Supersedes legacy modes/models fields. @@ -454,20 +410,31 @@ export interface ProcessErrorUpdate extends SessionUpdateBase { // Config Option Types // ============================================================================ -/** - * A session configuration option (e.g. mode, model, thought_level). - * Part of the ACP configOptions API that supersedes legacy modes/models. - */ -export interface SessionConfigOption { +interface SessionConfigOptionBase { id: string; name: string; description?: string | null; category?: string | null; - type: "select"; - currentValue: string; - options: SessionConfigSelectOption[] | SessionConfigSelectGroup[]; } +/** + * A session configuration option (e.g. mode, model, thought_level). + * Part of the ACP configOptions API that supersedes legacy modes/models. + * + * `select` carries a string value + choices. `boolean` (ACP 0.28+) is held as + * data for future support — it is not yet rendered or settable in the UI. + */ +export type SessionConfigOption = + | (SessionConfigOptionBase & { + type: "select"; + currentValue: string; + options: SessionConfigSelectOption[] | SessionConfigSelectGroup[]; + }) + | (SessionConfigOptionBase & { + type: "boolean"; + currentValue: boolean; + }); + export interface SessionConfigSelectOption { value: string; name: string; @@ -571,9 +538,6 @@ export interface SessionResult { /** DEPRECATED: Use configOptions instead. Kept for backward compatibility. */ modes?: SessionModeState; - /** DEPRECATED: Use configOptions instead. Kept for backward compatibility. */ - models?: SessionModelState; - /** Session config options (supersedes modes/models) */ configOptions?: SessionConfigOption[]; } diff --git a/src/ui/ChatPanel.tsx b/src/ui/ChatPanel.tsx index 2ba3594..00f1dd1 100644 --- a/src/ui/ChatPanel.tsx +++ b/src/ui/ChatPanel.tsx @@ -37,7 +37,6 @@ import { flattenConfigSelectOptions, type SlashCommand, type SessionModeState, - type SessionModelState, type SessionConfigOption, } from "../types/session"; import { checkAgentUpdate } from "../services/update-checker"; @@ -213,23 +212,16 @@ export function ChatPanel({ ( sessionId: string, modes?: SessionModeState, - models?: SessionModelState, configOptions?: SessionConfigOption[], ) => { logger.log( `[ChatPanel] Session loaded/resumed/forked: ${sessionId}`, { modes, - models, configOptions, }, ); - void agent.updateSessionFromLoad( - sessionId, - modes, - models, - configOptions, - ); + void agent.updateSessionFromLoad(sessionId, modes, configOptions); }, [logger, agent.updateSessionFromLoad], ); @@ -312,7 +304,6 @@ export function ChatPanel({ handleSwitchAgent, handleRestartAgent, handleSetMode, - handleSetModel, handleSetConfigOption, handleClearError, handleClearAgentUpdate, @@ -639,14 +630,14 @@ export function ChatPanel({ // Floating: create a shim with listener tracking return { app: plugin.app, - registerDomEvent: (( + registerDomEvent: ( target: Window | Document | HTMLElement, type: string, callback: EventListenerOrEventListenerObject, ) => { target.addEventListener(type, callback); registeredListenersRef.current.push({ target, type, callback }); - }), + }, }; }, [viewHostProp, plugin.app]); @@ -673,16 +664,20 @@ export function ChatPanel({ void agent.createSession(config?.agent || initialAgentId); }, [agent.createSession, config?.agent, initialAgentId]); - // Apply configured model when session is ready + // Apply configured model (a select config option with category "model") + // when session is ready. useEffect(() => { if (!config?.model || !isSessionReady) return; - // Prefer configOptions if available if (session.configOptions) { const modelOption = session.configOptions.find( (o) => o.category === "model", ); - if (modelOption && modelOption.currentValue !== config.model) { + if ( + modelOption && + modelOption.type === "select" && + modelOption.currentValue !== config.model + ) { const valueExists = flattenConfigSelectOptions( modelOption.options, ).some((o) => o.value === config.model); @@ -694,29 +689,12 @@ export function ChatPanel({ void agent.setConfigOption(modelOption.id, config.model); } } - return; - } - - // Fallback to legacy models - if (session.models) { - const modelExists = session.models.availableModels.some( - (m) => m.modelId === config.model, - ); - if (modelExists && session.models.currentModelId !== config.model) { - logger.log( - "[ChatPanel] Applying configured model:", - config.model, - ); - void agent.setModel(config.model); - } } }, [ config?.model, isSessionReady, session.configOptions, - session.models, agent.setConfigOption, - agent.setModel, logger, ]); @@ -829,7 +807,10 @@ export function ChatPanel({ ); // System notification on response completion - if (settings.enableSystemNotifications && !activeDocument.hasFocus()) { + if ( + settings.enableSystemNotifications && + !activeDocument.hasFocus() + ) { new Notification("Agent Client", { body: `${activeAgentLabel} has completed the response.`, }); @@ -1117,7 +1098,8 @@ export function ChatPanel({ if (state === "error") return "error"; if (state === "disconnected") return "disconnected"; if (hasActivePermissionRef.current) return "permission"; - if (isSendingRef.current || sessionHistoryLoadingRef.current) return "busy"; + if (isSendingRef.current || sessionHistoryLoadingRef.current) + return "busy"; if (state === "ready") return "ready"; return "busy"; }, @@ -1262,8 +1244,6 @@ export function ChatPanel({ onRestoredMessageConsumed={handleRestoredMessageConsumed} modes={session.modes} onModeChange={(modeId) => void handleSetMode(modeId)} - models={session.models} - onModelChange={(modelId) => void handleSetModel(modelId)} configOptions={session.configOptions} onConfigOptionChange={(configId, value) => void handleSetConfigOption(configId, value) diff --git a/src/ui/InputArea.tsx b/src/ui/InputArea.tsx index f9706f4..1e8717f 100644 --- a/src/ui/InputArea.tsx +++ b/src/ui/InputArea.tsx @@ -8,7 +8,6 @@ import type { NoteMetadata } from "../services/vault-service"; import type { SlashCommand, SessionModeState, - SessionModelState, SessionUsage, SessionConfigOption, } from "../types/session"; @@ -212,10 +211,6 @@ export interface InputAreaProps { 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; /** Session config options (supersedes modes/models when present) */ configOptions?: SessionConfigOption[]; /** Callback when a config option is changed */ @@ -279,8 +274,6 @@ export function InputArea({ onRestoredMessageConsumed, modes, onModeChange, - models, - onModelChange, configOptions, onConfigOptionChange, usage, @@ -310,7 +303,8 @@ export function InputArea({ // Unofficial Obsidian API (see src/types/obsidian-internals.d.ts) const obsidianSpellcheck = - (plugin.app.vault.getConfig("spellcheck") as boolean | undefined) ?? true; + (plugin.app.vault.getConfig("spellcheck") as boolean | undefined) ?? + true; // Local state (hint and command are still local - not needed for broadcast) const [hintText, setHintText] = useState(null); @@ -1095,8 +1089,6 @@ export function InputArea({ onSendOrStop={() => void handleSendOrStop()} modes={modes} onModeChange={onModeChange} - models={models} - onModelChange={onModelChange} configOptions={configOptions} onConfigOptionChange={onConfigOptionChange} usage={usage} diff --git a/src/ui/InputToolbar.tsx b/src/ui/InputToolbar.tsx index bba8051..635b90a 100644 --- a/src/ui/InputToolbar.tsx +++ b/src/ui/InputToolbar.tsx @@ -5,7 +5,6 @@ import { setIcon, Menu } from "obsidian"; import { flattenConfigSelectOptions, type SessionModeState, - type SessionModelState, type SessionUsage, type SessionConfigOption, type SessionConfigSelectGroup, @@ -152,8 +151,6 @@ export interface InputToolbarProps { onSendOrStop: () => void; modes?: SessionModeState; onModeChange?: (modeId: string) => void; - models?: SessionModelState; - onModelChange?: (modelId: string) => void; configOptions?: SessionConfigOption[]; onConfigOptionChange?: (configId: string, value: string) => void; usage?: SessionUsage; @@ -167,8 +164,6 @@ export function InputToolbar({ onSendOrStop, modes, onModeChange, - models, - onModelChange, configOptions, onConfigOptionChange, usage, @@ -227,29 +222,11 @@ export function InputToolbar({ })); }, [modes]); - const modelItems = useMemo(() => { - if (!models?.availableModels) return []; - return models.availableModels.map((m) => ({ - value: m.modelId, - label: m.name, - })); - }, [models]); - const currentModeLabel = useMemo(() => { const id = modes?.currentModeId; - return ( - modes?.availableModes?.find((m) => m.id === id)?.name ?? "Mode" - ); + return modes?.availableModes?.find((m) => m.id === id)?.name ?? "Mode"; }, [modes]); - const currentModelLabel = useMemo(() => { - const id = models?.currentModelId; - return ( - models?.availableModels?.find((m) => m.modelId === id)?.name ?? - "Model" - ); - }, [models]); - // ----- Render ----- return ( @@ -269,63 +246,68 @@ export function InputToolbar({ )} {/* Config Options (supersedes legacy mode/model selectors) */} - {configOptions && configOptions.length > 0 - ? configOptions.map((option) => { - const flatOptions = flattenConfigSelectOptions( - option.options, - ); - if (flatOptions.length <= 1) return null; + {configOptions && configOptions.length > 0 ? ( + configOptions.map((option) => { + // boolean options (ACP 0.28+) are carried as data but + // not yet rendered; only select options get a dropdown. + if (option.type !== "select") return null; + const flatOptions = flattenConfigSelectOptions( + option.options, + ); + if (flatOptions.length <= 1) return null; - const isGrouped = - option.options.length > 0 && - "group" in option.options[0]; + const isGrouped = + option.options.length > 0 && + "group" in option.options[0]; - let items: ToolbarDropdownItem[]; - if (isGrouped) { - items = []; - for (const group of option.options as SessionConfigSelectGroup[]) { - for (const opt of group.options) { - items.push({ - value: opt.value, - label: `${group.name} / ${opt.name}`, - groupName: group.name, - }); - } + let items: ToolbarDropdownItem[]; + if (isGrouped) { + items = []; + for (const group of option.options as SessionConfigSelectGroup[]) { + for (const opt of group.options) { + items.push({ + value: opt.value, + label: `${group.name} / ${opt.name}`, + groupName: group.name, + }); } - } else { - items = flatOptions.map((opt) => ({ - value: opt.value, - label: opt.name, - })); } + } else { + items = flatOptions.map((opt) => ({ + value: opt.value, + label: opt.name, + })); + } - const currentItem = items.find( - (it) => it.value === option.currentValue, - ); - const label = currentItem?.label ?? option.name; - const title = option.description ?? option.name; + const currentItem = items.find( + (it) => it.value === option.currentValue, + ); + const label = currentItem?.label ?? option.name; + const title = option.description ?? option.name; - return ( - { - onConfigOptionChange?.(option.id, value); - }} - className={ - option.category - ? `agent-client-config-selector-${option.category}` - : undefined - } - /> - ); - }) - : ( - <> - {modes && modes.availableModes.length > 1 && onModeChange && ( + return ( + { + onConfigOptionChange?.(option.id, value); + }} + className={ + option.category + ? `agent-client-config-selector-${option.category}` + : undefined + } + /> + ); + }) + ) : ( + <> + {modes && + modes.availableModes.length > 1 && + onModeChange && ( )} - - {models && - models.availableModels.length > 1 && - onModelChange && ( - - m.modelId === - models.currentModelId, - )?.description ?? "Select model" - } - items={modelItems} - currentValue={ - models.currentModelId ?? undefined - } - onChange={onModelChange} - /> - )} - - )} + + )} {/* Send/Stop Button */}