feat(ui): use acp new session on desktop (only for non recipe session) (#9914)

This commit is contained in:
Lifei Zhou
2026-06-22 11:33:19 +10:00
committed by GitHub
parent b893b5f24e
commit 659257316d
9 changed files with 163 additions and 72 deletions
+9 -3
View File
@@ -985,13 +985,18 @@ impl GooseAcpAgent {
&self,
config: &Config,
mcp_servers: Vec<McpServer>,
goose_extensions: Option<Vec<GooseExtension>>,
) -> Result<Vec<ExtensionConfig>, agent_client_protocol::Error> {
let mut extensions = Vec::new();
for builtin in &self.builtins {
push_or_replace_extension(&mut extensions, builtin_to_extension_config(builtin));
}
if mcp_servers.is_empty() {
if let Some(goose_extensions) = goose_extensions {
for extension in extensions::goose_extensions_to_configs(goose_extensions)? {
push_or_replace_extension(&mut extensions, extension);
}
} else if mcp_servers.is_empty() {
for extension in get_enabled_extensions_with_config(config) {
push_or_replace_extension(&mut extensions, extension);
}
@@ -1113,7 +1118,7 @@ impl GooseAcpAgent {
|| EnabledExtensionsState::from_extension_data(&session.extension_data).is_none()
{
let extension_data =
self.build_enabled_extensions_data(config, &session, mcp_servers)?;
self.build_enabled_extensions_data(config, &session, mcp_servers, None)?;
builder = builder.extension_data(extension_data);
session_needs_update = true;
}
@@ -1142,8 +1147,9 @@ impl GooseAcpAgent {
config: &Config,
session: &Session,
mcp_servers: Vec<McpServer>,
goose_extensions: Option<Vec<GooseExtension>>,
) -> Result<ExtensionData, agent_client_protocol::Error> {
let extensions = self.initial_session_extensions(config, mcp_servers)?;
let extensions = self.initial_session_extensions(config, mcp_servers, goose_extensions)?;
let mut extension_data = session.extension_data.clone();
EnabledExtensionsState::new(extensions)
.to_extension_data(&mut extension_data)
+12 -4
View File
@@ -9,7 +9,7 @@ impl GooseAcpAgent {
req: AddSessionExtensionRequest,
) -> Result<EmptyResponse, agent_client_protocol::Error> {
let session_id = &req.session_id;
let config = goose_extension_to_session_config(req.extension)?;
let config = goose_extension_to_config_without_secrets(req.extension)?;
let agent = self.get_session_agent(&req.session_id).await?;
agent
.add_extension(config, session_id)
@@ -315,19 +315,27 @@ fn goose_extension_to_config(
})
}
fn goose_extension_to_session_config(
fn goose_extension_to_config_without_secrets(
extension: GooseExtension,
) -> Result<ExtensionConfig, agent_client_protocol::Error> {
let conversion = goose_extension_to_config(extension)?;
if !conversion.secret_updates.is_empty() {
return Err(agent_client_protocol::Error::invalid_params().data(
"literal environment values are not supported for session extensions; use envKeys",
"extension env values must be passed via envKeys referencing stored secrets, not inline env",
));
}
Ok(conversion.config)
}
pub(super) fn goose_extensions_to_configs(
extensions: Vec<GooseExtension>,
) -> Result<Vec<ExtensionConfig>, agent_client_protocol::Error> {
extensions
.into_iter()
.map(goose_extension_to_config_without_secrets)
.collect()
}
fn config_entry_to_goose_entry(
entry: ExtensionEntry,
) -> Result<Option<GooseExtensionEntry>, agent_client_protocol::Error> {
+26 -17
View File
@@ -1,22 +1,19 @@
use crate::acp::server::{meta_string, sid_short, validate_absolute_cwd, ResultExt};
use crate::acp::custom_requests::GooseExtension;
use crate::acp::server::{meta_string, validate_absolute_cwd, ResultExt};
use crate::config::{Config, GooseMode};
use crate::session::SessionType;
use super::GooseAcpAgent;
use agent_client_protocol::schema::{NewSessionRequest, NewSessionResponse, SessionId};
use agent_client_protocol::schema::{Meta, NewSessionRequest, NewSessionResponse, SessionId};
use agent_client_protocol::{Client, ConnectionTo};
use std::collections::HashMap;
use tracing::debug;
impl GooseAcpAgent {
#[allow(dead_code)]
pub(super) async fn handle_new_session(
&self,
cx: &ConnectionTo<Client>,
args: NewSessionRequest,
) -> Result<NewSessionResponse, agent_client_protocol::Error> {
debug!(?args, "new session request");
let t_start = std::time::Instant::now();
validate_absolute_cwd(&args.cwd)?;
let project_id = meta_string(args.meta.as_ref(), "projectId")?;
let session_type = match meta_string(args.meta.as_ref(), "client")? {
@@ -33,8 +30,8 @@ impl GooseAcpAgent {
}
None => super::resolve_default_provider_model_config(config)?,
};
let goose_extensions = meta_goose_extensions(args.meta.as_ref())?;
let current_mode: GooseMode = config.get_goose_mode().unwrap_or_default();
let t0 = std::time::Instant::now();
let mut goose_session = self
.session_manager
.create_session(
@@ -46,8 +43,12 @@ impl GooseAcpAgent {
.await
.internal_err_ctx("Failed to create session")?;
let mut builder = self.session_manager.update(&goose_session.id);
let extension_data =
self.build_enabled_extensions_data(config, &goose_session, args.mcp_servers)?;
let extension_data = self.build_enabled_extensions_data(
config,
&goose_session,
args.mcp_servers,
goose_extensions,
)?;
builder = builder
.provider_name(resolved_provider)
.model_config(resolved_model_config)
@@ -66,8 +67,6 @@ impl GooseAcpAgent {
.await
.internal_err_ctx("Failed to reload session")?;
let session_id_str = goose_session.id.clone();
let sid = sid_short(&session_id_str);
debug!(target: "perf", sid = %sid, ms = t0.elapsed().as_millis() as u64, "perf: new_session create_session");
let (_agent, extension_results) = self
.activate_acp_session(cx, &goose_session, HashMap::new())
@@ -101,12 +100,22 @@ impl GooseAcpAgent {
&goose_session,
self.supports_goose_custom_notifications(),
)?;
debug!(
target: "perf",
sid = %sid,
ms = t_start.elapsed().as_millis() as u64,
"perf: new_session done"
);
Ok(response)
}
}
fn meta_goose_extensions(
meta: Option<&Meta>,
) -> Result<Option<Vec<GooseExtension>>, agent_client_protocol::Error> {
let Some(value) = meta.and_then(|m| m.get("enabledExtensions")) else {
return Ok(None);
};
if value.is_null() {
return Ok(None);
}
serde_json::from_value(value.clone())
.map(Some)
.map_err(|e| {
agent_client_protocol::Error::invalid_params().data(format!("enabledExtensions: {e}"))
})
}
@@ -156,14 +156,6 @@ describe('acpChatSessionStore', () => {
expect(snapshot.session?.id).toBe(currentSessionId);
expect(snapshot.messages).toEqual([initialMessage]);
expect(snapshot.tokenState).toMatchObject({
inputTokens: 1,
outputTokens: 2,
totalTokens: 3,
accumulatedInputTokens: 4,
accumulatedOutputTokens: 5,
accumulatedTotalTokens: 9,
});
expect(snapshot.chatState).toBe(ChatState.Idle);
expect(snapshot.sessionLoadError).toBeUndefined();
});
+22 -1
View File
@@ -1,5 +1,6 @@
import { v7 as uuidv7 } from 'uuid';
import { updateSessionUserRecipeValues, type Message } from '../api';
import { updateSessionUserRecipeValues, type Message, type Session } from '../api';
import type { GooseExtension } from '@aaif/goose-sdk';
import { AppEvents } from '../constants/events';
import { ChatState } from '../types/chatState';
import { errorMessage } from '../utils/conversionUtils';
@@ -17,6 +18,7 @@ import { acpCancelPrompt, acpPromptSession } from './prompt';
import {
acpForkSession,
acpLoadSession,
acpNewSession,
acpTruncateSessionConversation,
isAcpSessionLoadInFlight,
sessionInfoToSession,
@@ -35,6 +37,7 @@ export interface AcpSubmitMessageOptions extends AcpSnapshotOptions {
}
export interface AcpChatSessionController {
createSession(cwd: string, gooseExtensions: GooseExtension[]): Promise<Session>;
loadSession(sessionId: string, options?: AcpLoadSessionOptions): Promise<void>;
submitMessage(
sessionId: string,
@@ -73,6 +76,19 @@ function createAcpCreditsExhaustedMessage(error: AcpCreditsExhaustedError): Mess
};
}
async function createSession(cwd: string, gooseExtensions: GooseExtension[]): Promise<Session> {
const { sessionId, sessionInfo, meta } = await acpNewSession(cwd, gooseExtensions);
const session = sessionInfoToSession(sessionInfo, meta);
showExtensionLoadResults(meta.extensionResults);
window.dispatchEvent(
new CustomEvent(AppEvents.SESSION_EXTENSIONS_LOADED, { detail: { sessionId } })
);
acpChatSessionActions.finishSessionLoad(sessionId, session);
return session;
}
async function loadSession(sessionId: string, options: AcpLoadSessionOptions = {}): Promise<void> {
const cached = acpChatSessionStore.getSnapshot(sessionId);
if (cached?.session) {
@@ -106,6 +122,10 @@ async function submitMessage(
userMessage: Message,
options: AcpSubmitMessageOptions
): Promise<void> {
if (acpChatSessionStore.getSnapshot(sessionId)?.activePromptAttemptId) {
return;
}
const promptAttemptId = uuidv7();
acpChatSessionActions.startPromptAttempt(sessionId, promptAttemptId);
@@ -247,6 +267,7 @@ async function setRecipeUserParams(
}
export const acpChatSessionController: AcpChatSessionController = {
createSession,
loadSession,
submitMessage,
stop,
-17
View File
@@ -38,22 +38,6 @@ const initialTokenState: TokenState = {
accumulatedTotalTokens: 0,
};
function tokenStateFromSession(session: Session | undefined): Partial<TokenState> {
return {
inputTokens: session?.usage?.input_tokens ?? 0,
outputTokens: session?.usage?.output_tokens ?? 0,
totalTokens: session?.usage?.total_tokens ?? 0,
cacheReadTokens: session?.usage?.cache_read_input_tokens ?? 0,
cacheWriteTokens: session?.usage?.cache_write_input_tokens ?? 0,
accumulatedInputTokens: session?.accumulated_usage?.input_tokens ?? 0,
accumulatedOutputTokens: session?.accumulated_usage?.output_tokens ?? 0,
accumulatedTotalTokens: session?.accumulated_usage?.total_tokens ?? 0,
accumulatedCacheReadTokens: session?.accumulated_usage?.cache_read_input_tokens ?? 0,
accumulatedCacheWriteTokens: session?.accumulated_usage?.cache_write_input_tokens ?? 0,
...(session?.accumulated_cost != null ? { accumulatedCost: session.accumulated_cost } : {}),
};
}
export interface AcpChatSessionStore {
getSnapshot(sessionId: string): AcpChatSessionSnapshot | undefined;
}
@@ -180,7 +164,6 @@ function createAcpChatSessionStoreInternal(): AcpChatSessionStoreInternal {
const finishSessionLoad: AcpChatSessionActions['finishSessionLoad'] = (sessionId, session) => {
const entry = getOrCreateEntry(sessionId);
entry.session = session;
entry.tokenState = { ...entry.tokenState, ...tokenStateFromSession(session) };
entry.sessionLoadError = undefined;
entry.chatState = entry.activePromptAttemptId ? ChatState.Streaming : ChatState.Idle;
return notify(sessionId, entry);
+11 -1
View File
@@ -1,7 +1,11 @@
import type { ExtensionResponse, ExtensionEntry } from '../api';
import type { GooseExtensionEntry, McpServer } from '@aaif/goose-sdk';
import type { GooseExtension, GooseExtensionEntry, McpServer } from '@aaif/goose-sdk';
import { getAcpClient } from './acpConnection';
export function gooseExtensionName(extension: GooseExtension): string {
return extension.type === 'mcp' ? extension.server.name : extension.name;
}
function headersToRecord(headers: { name: string; value: string }[] = []) {
return Object.fromEntries(headers.map(({ name, value }) => [name, value]));
}
@@ -65,6 +69,12 @@ function gooseExtensionEntryToExtensionEntry(entry: GooseExtensionEntry): Extens
return null;
}
export async function getConfiguredGooseExtensions(): Promise<GooseExtensionEntry[]> {
const client = await getAcpClient();
const response = await client.goose.configExtensionsList_unstable({});
return response.extensions;
}
export async function getConfiguredExtensions(): Promise<ExtensionResponse> {
const client = await getAcpClient();
const response = await client.goose.configExtensionsList_unstable({});
+35 -2
View File
@@ -2,8 +2,10 @@ import type {
ForkSessionRequest,
ListSessionsRequest,
LoadSessionResponse,
NewSessionRequest,
SessionInfo,
} from '@agentclientprotocol/sdk';
import type { GooseExtension } from '@aaif/goose-sdk';
import { getAcpClient } from './acpConnection';
import { DEFAULT_CHAT_TITLE } from '../contexts/ChatContext';
import type { ExtensionLoadResult, Recipe, Session } from '../api';
@@ -56,8 +58,8 @@ export interface AcpLoadSessionResult {
const inFlightSessionLoads = new Map<string, Promise<AcpLoadSessionResult>>();
export function parseLoadMeta(response: LoadSessionResponse): LoadSessionMeta {
const meta = (response._meta ?? {}) as LoadSessionMeta;
function parseSessionResponseMeta(rawMeta: unknown): LoadSessionMeta {
const meta = (rawMeta ?? {}) as LoadSessionMeta;
return {
recipe: meta.recipe,
userRecipeValues: meta.userRecipeValues,
@@ -66,6 +68,10 @@ export function parseLoadMeta(response: LoadSessionResponse): LoadSessionMeta {
};
}
export function parseLoadMeta(response: LoadSessionResponse): LoadSessionMeta {
return parseSessionResponseMeta(response._meta);
}
function sessionInfoMeta(s: SessionInfo): GooseSessionInfoMeta {
return (s._meta ?? {}) as GooseSessionInfoMeta;
}
@@ -197,6 +203,33 @@ async function loadAcpSession(sessionId: string): Promise<AcpLoadSessionResult>
};
}
export interface AcpNewSessionResult {
sessionId: string;
sessionInfo: SessionInfo;
meta: LoadSessionMeta;
}
export async function acpNewSession(
cwd: string,
gooseExtensions: GooseExtension[]
): Promise<AcpNewSessionResult> {
const client = await getAcpClient();
const meta: Record<string, unknown> = { client: 'goose-desktop' };
if (gooseExtensions.length > 0) {
meta.enabledExtensions = gooseExtensions;
}
const request: NewSessionRequest = { cwd, mcpServers: [], _meta: meta };
const response = await client.newSession(request);
const sessionId = String(response.sessionId);
const sessionInfoResponse = await client.goose.sessionInfo_unstable({ sessionId });
return {
sessionId,
sessionInfo: sessionInfoResponse.session,
meta: parseSessionResponseMeta(response._meta),
};
}
export async function acpDeleteSession(sessionId: string): Promise<void> {
const client = await getAcpClient();
await client.goose.sessionDelete({ sessionId });
+48 -19
View File
@@ -4,6 +4,9 @@ import type { setViewType } from './hooks/useNavigation';
import type { FixedExtensionEntry } from './components/ConfigContext';
import { AppEvents } from './constants/events';
import { decodeRecipe, Recipe } from './recipe';
import { USE_ACP_CHAT } from './acpChatFeatureFlag';
import { acpChatSessionController } from './acp/chatSessionController';
import { getConfiguredGooseExtensions, gooseExtensionName } from './acp/extensions';
export function getSessionDisplayName(session: Session): string {
if (session.user_set_name) {
@@ -40,15 +43,51 @@ export function resumeSession(session: Session, setView: setViewType) {
});
}
interface CreateSessionOptions {
recipeDeeplink?: string;
recipeId?: string;
extensionConfigs?: ExtensionConfig[];
allExtensions?: FixedExtensionEntry[];
}
function selectedExtensionConfigs(options?: CreateSessionOptions): ExtensionConfig[] {
if (options?.extensionConfigs && options.extensionConfigs.length > 0) {
return options.extensionConfigs;
}
if (options?.allExtensions) {
return options.allExtensions
.filter((extension) => extension.enabled)
.map((extension) => {
const { enabled: _enabled, ...config } = extension;
return config as ExtensionConfig;
});
}
return [];
}
async function createAcpSession(
workingDir: string,
options?: CreateSessionOptions
): Promise<Session> {
const selectedNames = new Set(selectedExtensionConfigs(options).map((config) => config.name));
const gooseExtensions =
selectedNames.size > 0
? (await getConfiguredGooseExtensions())
.filter((entry) => selectedNames.has(gooseExtensionName(entry.extension)))
.map((entry) => entry.extension)
: [];
return acpChatSessionController.createSession(workingDir, gooseExtensions);
}
export async function createSession(
workingDir: string,
options?: {
recipeDeeplink?: string;
recipeId?: string;
extensionConfigs?: ExtensionConfig[];
allExtensions?: FixedExtensionEntry[];
}
options?: CreateSessionOptions
): Promise<Session> {
const hasRecipe = Boolean(options?.recipeId || options?.recipeDeeplink);
if (USE_ACP_CHAT && !hasRecipe) {
return createAcpSession(workingDir, options);
}
const body: {
working_dir: string;
recipe?: Recipe;
@@ -64,19 +103,9 @@ export async function createSession(
body.recipe = await decodeRecipe(options.recipeDeeplink);
}
if (options?.extensionConfigs && options.extensionConfigs.length > 0) {
body.extension_overrides = options.extensionConfigs;
} else if (options?.allExtensions) {
const extensionConfigs = options.allExtensions
.filter((extension) => extension.enabled)
.map((extension) => {
const { enabled: _enabled, ...config } = extension;
return config as ExtensionConfig;
});
if (extensionConfigs.length > 0) {
body.extension_overrides = extensionConfigs;
}
const extensionConfigs = selectedExtensionConfigs(options);
if (extensionConfigs.length > 0) {
body.extension_overrides = extensionConfigs;
}
const newAgent = await startAgent({