feat(acp): migrate to @agentclientprotocol/sdk 0.28.1

Bump the ACP SDK from 0.14.1 to 0.28.1 (PROTOCOL_VERSION unchanged, so a
direct jump). Adapt the existing client to the SDK's breaking changes while
keeping existing features working; new 0.28 features (elicitation, NES,
providers, document lifecycle) are out of scope.

- Renames: unstable_listSessions->listSessions, unstable_resumeSession->
  resumeSession (both stabilized); KillTerminalCommand{Request,Response}->
  KillTerminal{Request,Response} (type-only).
- SessionConfigOption became a select|boolean union. The domain type now
  carries boolean as data (UI renders/sets only select for now); the
  converter and all consumers narrow on type === "select".
- The model API (unstable_setSessionModel, SessionModelState) was removed
  from the SDK; model is now a config option. Remove the legacy model path
  (setSessionModel, the model dropdown, SessionModel(State) domain types)
  but KEEP lastUsedModels + the config-option model restore/save that back
  model persistence. Mode keeps its legacy API (still in the SDK).
- zod is now a runtime (peer) dependency of the SDK for schema validation;
  add it to dependencies so it bundles into main.js.
- Restore @eslint/json devDep (legacy-peer-deps had pruned this optional
  peer that the lint config loads).

ClientSideConnection is deprecated in 0.28 (new: client() builder, scoped
connectWith) but kept for now with documented eslint-disables; migrating
the connection lifecycle is a separate follow-up.
This commit is contained in:
RAIT-09 2026-06-21 02:50:59 +09:00
parent edb1cfc9f0
commit f799ad1f2e
15 changed files with 166 additions and 374 deletions

23
package-lock.json generated
View file

@ -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"
}

View file

@ -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"
}
}

View file

@ -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<void> {
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: [],

View file

@ -232,8 +232,8 @@ export class AcpHandler {
}
killTerminal(
params: acp.KillTerminalCommandRequest,
): Promise<acp.KillTerminalCommandResponse> {
params: acp.KillTerminalRequest,
): Promise<acp.KillTerminalResponse> {
const success = this.terminalManager.killTerminal(params.terminalId);
if (!success) {
throw new Error(`Terminal ${params.terminalId} not found`);

View file

@ -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,
};
}

View file

@ -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<void>;
// Config
setMode: (modeId: string) => Promise<void>;
setModel: (modelId: string) => Promise<void>;
setConfigOption: (configId: string, value: string) => Promise<void>;
// 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,

View file

@ -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<void>;
// Config
setMode: (modeId: string) => Promise<void>;
setModel: (modelId: string) => Promise<void>;
setConfigOption: (configId: string, value: string) => Promise<void>;
/** 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,
};

View file

@ -44,7 +44,6 @@ export interface UseChatActionsReturn {
// Config actions
handleSetMode: (modeId: string) => Promise<void>;
handleSetModel: (modelId: string) => Promise<void>;
handleSetConfigOption: (configId: string, value: string) => Promise<void>;
// 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,

View file

@ -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

View file

@ -184,7 +184,6 @@ export function createInitialSession(
authMethods: [],
availableCommands: undefined,
modes: undefined,
models: undefined,
createdAt: new Date(),
lastActivityAt: new Date(),
workingDirectory,

View file

@ -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 };
}

View file

@ -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[];
}

View file

@ -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)

View file

@ -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<string | null>(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}

View file

@ -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<ToolbarDropdownItem[]>(() => {
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 (
<ToolbarDropdown
key={option.id}
label={label}
title={title}
items={items}
currentValue={option.currentValue}
onChange={(value) => {
onConfigOptionChange?.(option.id, value);
}}
className={
option.category
? `agent-client-config-selector-${option.category}`
: undefined
}
/>
);
})
: (
<>
{modes && modes.availableModes.length > 1 && onModeChange && (
return (
<ToolbarDropdown
key={option.id}
label={label}
title={title}
items={items}
currentValue={option.currentValue}
onChange={(value) => {
onConfigOptionChange?.(option.id, value);
}}
className={
option.category
? `agent-client-config-selector-${option.category}`
: undefined
}
/>
);
})
) : (
<>
{modes &&
modes.availableModes.length > 1 &&
onModeChange && (
<ToolbarDropdown
label={currentModeLabel}
title={
@ -338,28 +320,8 @@ export function InputToolbar({
onChange={onModeChange}
/>
)}
{models &&
models.availableModels.length > 1 &&
onModelChange && (
<ToolbarDropdown
label={currentModelLabel}
title={
models.availableModels.find(
(m) =>
m.modelId ===
models.currentModelId,
)?.description ?? "Select model"
}
items={modelItems}
currentValue={
models.currentModelId ?? undefined
}
onChange={onModelChange}
/>
)}
</>
)}
</>
)}
{/* Send/Stop Button */}
<button