Skip to content

Commit 7705b74

Browse files
committed
feat(acp): add session selectors in ACP composer
- track ACP session controls (modes, models, config options) in ACP client - expose setters for mode/model/config option and react to control update events - render dynamic selector chips in ACP input footer using select dialog - support agent-specific config options (including grouped options and thought level)
1 parent 2d93f7c commit 7705b74

File tree

3 files changed

+576
-8
lines changed

3 files changed

+576
-8
lines changed

src/lib/acp/client.ts

Lines changed: 208 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type Implementation,
88
type InitializeResponse as InitializeResult,
99
type ListSessionsResponse as ListSessionsResult,
10+
type LoadSessionResponse as LoadSessionResult,
1011
type McpServer,
1112
type NewSessionResponse as NewSessionResult,
1213
type RequestPermissionRequest as PermissionRequest,
@@ -15,6 +16,9 @@ import {
1516
type PromptResponse as PromptResult,
1617
RequestError,
1718
type RequestId,
19+
type SessionConfigOption,
20+
type SessionModelState,
21+
type SessionModeState,
1822
type SessionUpdate,
1923
} from "@agentclientprotocol/sdk";
2024
import { ConnectionState } from "./models";
@@ -45,6 +49,7 @@ const ACP_METHODS = {
4549
type ClientEventType =
4650
| "state_change"
4751
| "session_update"
52+
| "session_controls_update"
4853
| "error"
4954
| "permission_request";
5055

@@ -80,6 +85,9 @@ export class ACPClient {
8085
private _agentCapabilities: AgentCapabilities | null = null;
8186
private _agentInfo: Implementation | null = null;
8287
private _session: ACPSession | null = null;
88+
private _sessionModes: SessionModeState | null = null;
89+
private _sessionModels: SessionModelState | null = null;
90+
private _sessionConfigOptions: SessionConfigOption[] = [];
8391

8492
get state(): ConnectionState {
8593
return this._state;
@@ -97,6 +105,18 @@ export class ACPClient {
97105
return this._session;
98106
}
99107

108+
get sessionModes(): SessionModeState | null {
109+
return this._sessionModes;
110+
}
111+
112+
get sessionModels(): SessionModelState | null {
113+
return this._sessionModels;
114+
}
115+
116+
get sessionConfigOptions(): SessionConfigOption[] {
117+
return this._sessionConfigOptions;
118+
}
119+
100120
get agentName(): string {
101121
return this._agentInfo?.title || this._agentInfo?.name || "Agent";
102122
}
@@ -232,6 +252,7 @@ export class ACPClient {
232252
connection.newSession(params as never),
233253
)) as NewSessionResult;
234254

255+
this.setSessionConfigurationState(result);
235256
this._session?.dispose();
236257
this._session = new ACPSession(result.sessionId, cwd || "");
237258
return this._session;
@@ -255,18 +276,20 @@ export class ACPClient {
255276
this._session = session;
256277

257278
try {
258-
await this.runWhileConnected(
279+
const result = (await this.runWhileConnected(
259280
connection.loadSession({
260281
sessionId,
261282
cwd,
262283
mcpServers: mcpServers ?? [],
263284
}),
264-
);
285+
)) as LoadSessionResult;
286+
this.setSessionConfigurationState(result);
265287
return session;
266288
} catch (error) {
267289
if (this._session === session) {
268290
session.dispose();
269291
this._session = null;
292+
this.clearSessionConfigurationState(true);
270293
}
271294
throw error;
272295
}
@@ -338,6 +361,55 @@ export class ACPClient {
338361
return await this.prompt([{ type: "text", text }]);
339362
}
340363

364+
async setSessionMode(modeId: string): Promise<void> {
365+
this.ensureReady();
366+
const sessionId = this.ensureSessionId();
367+
const connection = this.getConnection();
368+
369+
await this.runWhileConnected(
370+
connection.setSessionMode({
371+
sessionId,
372+
modeId,
373+
}),
374+
);
375+
this.updateSessionModeId(modeId);
376+
}
377+
378+
async setSessionModel(modelId: string): Promise<void> {
379+
this.ensureReady();
380+
const sessionId = this.ensureSessionId();
381+
const connection = this.getConnection();
382+
383+
await this.runWhileConnected(
384+
connection.unstable_setSessionModel({
385+
sessionId,
386+
modelId,
387+
}),
388+
);
389+
this.updateSessionModelId(modelId);
390+
}
391+
392+
async setSessionConfigOption(configId: string, value: string): Promise<void> {
393+
this.ensureReady();
394+
const sessionId = this.ensureSessionId();
395+
const connection = this.getConnection();
396+
397+
const result = await this.runWhileConnected(
398+
connection.setSessionConfigOption({
399+
sessionId,
400+
configId,
401+
value,
402+
}),
403+
);
404+
405+
if (result.configOptions) {
406+
this.setSessionConfigOptions(result.configOptions);
407+
return;
408+
}
409+
410+
this.updateSessionConfigOptionValue(configId, value);
411+
}
412+
341413
cancel(): void {
342414
if (!this._session || !this.connection) return;
343415

@@ -427,6 +499,7 @@ export class ACPClient {
427499
update: SessionUpdate;
428500
}): Promise<void> {
429501
if (this._session && sessionId === this._session.sessionId) {
502+
this.handleSessionControlUpdate(update);
430503
this._session.handleSessionUpdate(update);
431504
this.emit("session_update", update);
432505
}
@@ -510,11 +583,144 @@ export class ACPClient {
510583
}
511584
}
512585

586+
private setSessionConfigurationState({
587+
modes,
588+
models,
589+
configOptions,
590+
}: {
591+
modes?: SessionModeState | null;
592+
models?: SessionModelState | null;
593+
configOptions?: SessionConfigOption[] | null;
594+
}): void {
595+
this._sessionModes = modes ?? null;
596+
this._sessionModels = models ?? null;
597+
this._sessionConfigOptions = this.normalizeConfigOptions(configOptions);
598+
this.emitSessionControlsUpdate();
599+
}
600+
601+
private setSessionConfigOptions(
602+
configOptions: SessionConfigOption[] | null | undefined,
603+
): void {
604+
this._sessionConfigOptions = this.normalizeConfigOptions(configOptions);
605+
this.emitSessionControlsUpdate();
606+
}
607+
608+
private updateSessionModeId(modeId: string): void {
609+
if (!this._sessionModes || this._sessionModes.currentModeId === modeId)
610+
return;
611+
612+
this._sessionModes = {
613+
...this._sessionModes,
614+
currentModeId: modeId,
615+
};
616+
this.emitSessionControlsUpdate();
617+
}
618+
619+
private updateSessionModelId(modelId: string): void {
620+
if (
621+
!this._sessionModels ||
622+
this._sessionModels.currentModelId === modelId
623+
) {
624+
return;
625+
}
626+
627+
this._sessionModels = {
628+
...this._sessionModels,
629+
currentModelId: modelId,
630+
};
631+
this.emitSessionControlsUpdate();
632+
}
633+
634+
private updateSessionConfigOptionValue(
635+
configId: string,
636+
value: string,
637+
): void {
638+
let didChange = false;
639+
this._sessionConfigOptions = this._sessionConfigOptions.map((option) => {
640+
if (option.id !== configId || option.currentValue === value) {
641+
return option;
642+
}
643+
didChange = true;
644+
return {
645+
...option,
646+
currentValue: value,
647+
};
648+
});
649+
650+
if (didChange) {
651+
this.emitSessionControlsUpdate();
652+
}
653+
}
654+
655+
private handleSessionControlUpdate(update: SessionUpdate): void {
656+
const updateKind = (update as { sessionUpdate?: string }).sessionUpdate;
657+
658+
if (updateKind === "current_mode_update") {
659+
const modeUpdate = update as {
660+
currentModeId?: unknown;
661+
modeId?: unknown;
662+
};
663+
const modeId =
664+
typeof modeUpdate.currentModeId === "string"
665+
? modeUpdate.currentModeId
666+
: typeof modeUpdate.modeId === "string"
667+
? modeUpdate.modeId
668+
: null;
669+
if (modeId) {
670+
this.updateSessionModeId(modeId);
671+
}
672+
return;
673+
}
674+
675+
if (
676+
updateKind === "config_option_update" ||
677+
updateKind === "config_options_update"
678+
) {
679+
const configUpdate = update as { configOptions?: unknown };
680+
if (Array.isArray(configUpdate.configOptions)) {
681+
this.setSessionConfigOptions(
682+
configUpdate.configOptions as SessionConfigOption[],
683+
);
684+
}
685+
}
686+
}
687+
688+
private emitSessionControlsUpdate(): void {
689+
this.emit("session_controls_update", {
690+
modes: this._sessionModes,
691+
models: this._sessionModels,
692+
configOptions: this._sessionConfigOptions,
693+
});
694+
}
695+
696+
private normalizeConfigOptions(
697+
configOptions: SessionConfigOption[] | null | undefined,
698+
): SessionConfigOption[] {
699+
return Array.isArray(configOptions) ? [...configOptions] : [];
700+
}
701+
702+
private clearSessionConfigurationState(emit = false): void {
703+
this._sessionModes = null;
704+
this._sessionModels = null;
705+
this._sessionConfigOptions = [];
706+
if (emit) {
707+
this.emitSessionControlsUpdate();
708+
}
709+
}
710+
711+
private ensureSessionId(): string {
712+
if (!this._session) {
713+
throw new Error("No active session. Call newSession() first.");
714+
}
715+
return this._session.sessionId;
716+
}
717+
513718
private disposeSessionState(): void {
514719
this._session?.dispose();
515720
this._session = null;
516721
this._agentCapabilities = null;
517722
this._agentInfo = null;
723+
this.clearSessionConfigurationState();
518724
}
519725

520726
private handleTransportClosed(reason?: string): void {

0 commit comments

Comments
 (0)