feat (ui): Add durable ACP chat session state for desktop UI (#9852)
This commit is contained in:
@@ -491,6 +491,18 @@ pub struct GetSessionInfoResponse {
|
||||
pub session: SessionInfo,
|
||||
}
|
||||
|
||||
/// Truncate a session conversation from the given message timestamp onward.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(
|
||||
method = "_goose/unstable/session/conversation/truncate",
|
||||
response = EmptyResponse
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TruncateSessionConversationRequest {
|
||||
pub session_id: String,
|
||||
pub truncate_from: i64,
|
||||
}
|
||||
|
||||
/// Update the project association for a session.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/unstable/session/project/update", response = EmptyResponse)]
|
||||
|
||||
@@ -200,6 +200,11 @@
|
||||
"requestType": "GetSessionInfoRequest_unstable",
|
||||
"responseType": "GetSessionInfoResponse_unstable"
|
||||
},
|
||||
{
|
||||
"method": "_goose/unstable/session/conversation/truncate",
|
||||
"requestType": "TruncateSessionConversationRequest_unstable",
|
||||
"responseType": "EmptyResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/unstable/session/project/update",
|
||||
"requestType": "UpdateSessionProjectRequest_unstable",
|
||||
|
||||
@@ -2768,6 +2768,24 @@
|
||||
"type": "string",
|
||||
"description": "A unique identifier for a conversation session between a client and agent.\n\nSessions maintain their own context, conversation history, and state,\nallowing multiple independent interactions with the same agent.\n\nSee protocol docs: [Session ID](https://agentclientprotocol.com/protocol/session-setup#session-id)"
|
||||
},
|
||||
"TruncateSessionConversationRequest_unstable": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sessionId": {
|
||||
"type": "string"
|
||||
},
|
||||
"truncateFrom": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sessionId",
|
||||
"truncateFrom"
|
||||
],
|
||||
"description": "Truncate a session conversation from the given message timestamp onward.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/unstable/session/conversation/truncate"
|
||||
},
|
||||
"UpdateSessionProjectRequest_unstable": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -4043,6 +4061,15 @@
|
||||
"description": "Params for _goose/unstable/session/info",
|
||||
"title": "GetSessionInfoRequest_unstable"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/TruncateSessionConversationRequest_unstable"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/unstable/session/conversation/truncate",
|
||||
"title": "TruncateSessionConversationRequest_unstable"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
|
||||
@@ -329,6 +329,14 @@ impl GooseAcpAgent {
|
||||
self.on_get_session_info(req).await
|
||||
}
|
||||
|
||||
#[custom_method(TruncateSessionConversationRequest)]
|
||||
async fn dispatch_truncate_session_conversation(
|
||||
&self,
|
||||
req: TruncateSessionConversationRequest,
|
||||
) -> Result<EmptyResponse, agent_client_protocol::Error> {
|
||||
self.on_truncate_session_conversation(req).await
|
||||
}
|
||||
|
||||
#[custom_method(UpdateSessionProjectRequest)]
|
||||
async fn dispatch_update_session_project(
|
||||
&self,
|
||||
|
||||
@@ -142,6 +142,24 @@ impl GooseAcpAgent {
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn on_truncate_session_conversation(
|
||||
&self,
|
||||
req: TruncateSessionConversationRequest,
|
||||
) -> Result<EmptyResponse, agent_client_protocol::Error> {
|
||||
let session_id = req.session_id.trim();
|
||||
if session_id.is_empty() {
|
||||
return Err(
|
||||
agent_client_protocol::Error::invalid_params().data("sessionId cannot be empty")
|
||||
);
|
||||
}
|
||||
|
||||
self.session_manager
|
||||
.truncate_conversation(session_id, req.truncate_from)
|
||||
.await
|
||||
.internal_err()?;
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
pub(super) async fn on_update_session_project(
|
||||
&self,
|
||||
req: UpdateSessionProjectRequest,
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
import type { CreateElicitationRequest, RequestPermissionRequest } from '@agentclientprotocol/sdk';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { Message, Session } from '../../api';
|
||||
import { ChatState } from '../../types/chatState';
|
||||
import { createAcpChatSessionStore, type AcpChatSessionStore } from '../chatSessionStore';
|
||||
|
||||
function message(id: string, text: string): Message {
|
||||
return {
|
||||
id,
|
||||
role: 'user',
|
||||
created: 123,
|
||||
content: [{ type: 'text', text }],
|
||||
metadata: { userVisible: true, agentVisible: true },
|
||||
};
|
||||
}
|
||||
|
||||
function session(id: string, conversation: Message[] = []): Session {
|
||||
return {
|
||||
id,
|
||||
name: `Session ${id}`,
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
updated_at: '2026-01-01T00:00:00Z',
|
||||
working_dir: '/tmp',
|
||||
message_count: conversation.length,
|
||||
extension_data: {},
|
||||
source: 'test',
|
||||
conversation,
|
||||
input_tokens: 1,
|
||||
output_tokens: 2,
|
||||
total_tokens: 3,
|
||||
accumulated_input_tokens: 4,
|
||||
accumulated_output_tokens: 5,
|
||||
accumulated_total_tokens: 9,
|
||||
} as Session;
|
||||
}
|
||||
|
||||
function permissionRequest(sessionId: string, toolCallId = 'tool-1'): RequestPermissionRequest {
|
||||
return {
|
||||
sessionId,
|
||||
options: [{ optionId: 'allow-once', name: 'Allow once', kind: 'allow_once' }],
|
||||
toolCall: {
|
||||
toolCallId,
|
||||
title: 'Edit file',
|
||||
rawInput: { path: 'README.md' },
|
||||
content: [
|
||||
{
|
||||
type: 'content',
|
||||
content: { type: 'text', text: 'Allow editing README.md?' },
|
||||
},
|
||||
],
|
||||
_meta: {
|
||||
goose: {
|
||||
toolCall: {
|
||||
toolName: 'edit_file',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function elicitationRequest(sessionId: string): {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
request: CreateElicitationRequest & {
|
||||
mode: 'form';
|
||||
sessionId: string;
|
||||
};
|
||||
} {
|
||||
return {
|
||||
id: 'acp_elicitation_1',
|
||||
sessionId,
|
||||
request: {
|
||||
mode: 'form',
|
||||
sessionId,
|
||||
message: 'Choose a project',
|
||||
requestedSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
project: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('acpChatSessionStore', () => {
|
||||
let store: AcpChatSessionStore;
|
||||
|
||||
beforeEach(() => {
|
||||
store = createAcpChatSessionStore();
|
||||
});
|
||||
|
||||
it('stores loaded session messages and token state', () => {
|
||||
const initialMessage = message('message-1', 'Hello');
|
||||
|
||||
const snapshot = store.setLoadedSession('session-1', session('session-1', [initialMessage]));
|
||||
|
||||
expect(snapshot.session?.id).toBe('session-1');
|
||||
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();
|
||||
});
|
||||
|
||||
it('keeps multiple session snapshots isolated', () => {
|
||||
store.setMessages('session-1', [message('message-1', 'One')]);
|
||||
store.setMessages('session-2', [message('message-2', 'Two')]);
|
||||
|
||||
expect(store.getSnapshot('session-1')?.messages[0].id).toBe('message-1');
|
||||
expect(store.getSnapshot('session-2')?.messages[0].id).toBe('message-2');
|
||||
});
|
||||
|
||||
it('deletes session snapshots', () => {
|
||||
store.setMessages('session-1', [message('message-1', 'One')]);
|
||||
|
||||
store.deleteSnapshot('session-1');
|
||||
|
||||
expect(store.getSnapshot('session-1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('notifies only listeners for the updated session', () => {
|
||||
const sessionOneListener = vi.fn();
|
||||
const sessionTwoListener = vi.fn();
|
||||
|
||||
store.subscribe('session-1', sessionOneListener);
|
||||
store.subscribe('session-2', sessionTwoListener);
|
||||
|
||||
store.setChatState('session-1', ChatState.Streaming);
|
||||
|
||||
expect(sessionOneListener).toHaveBeenCalledTimes(1);
|
||||
expect(sessionOneListener).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ chatState: ChatState.Streaming })
|
||||
);
|
||||
expect(sessionTwoListener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('stops notifying after unsubscribe', () => {
|
||||
const listener = vi.fn();
|
||||
const unsubscribe = store.subscribe('session-1', listener);
|
||||
|
||||
unsubscribe();
|
||||
store.setChatState('session-1', ChatState.Streaming);
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ignores stale prompt attempts and leaves the current attempt active', () => {
|
||||
store.startPromptAttempt('session-1', 'attempt-a');
|
||||
store.startPromptAttempt('session-1', 'attempt-b');
|
||||
|
||||
expect(store.finishPromptAttemptIfCurrent('session-1', 'attempt-a', 'late error')).toBe(false);
|
||||
|
||||
expect(store.getSnapshot('session-1')).toMatchObject({
|
||||
activePromptAttemptId: 'attempt-b',
|
||||
chatState: ChatState.Streaming,
|
||||
sessionLoadError: undefined,
|
||||
});
|
||||
|
||||
expect(store.finishPromptAttemptIfCurrent('session-1', 'attempt-b')).toBe(true);
|
||||
expect(store.getSnapshot('session-1')).toMatchObject({
|
||||
activePromptAttemptId: null,
|
||||
chatState: ChatState.Idle,
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps loaded sessions streaming when a prompt attempt is active', () => {
|
||||
store.startPromptAttempt('session-1', 'attempt-1');
|
||||
|
||||
const snapshot = store.setLoadedSession('session-1', session('session-1'));
|
||||
|
||||
expect(snapshot.activePromptAttemptId).toBe('attempt-1');
|
||||
expect(snapshot.chatState).toBe(ChatState.Streaming);
|
||||
});
|
||||
|
||||
it('applies permission requests as waiting action-required messages', () => {
|
||||
const snapshot = store.applyPermissionRequest(permissionRequest('session-1', 'tool-1'));
|
||||
|
||||
expect(snapshot.chatState).toBe(ChatState.WaitingForUserInput);
|
||||
expect(snapshot.messages).toHaveLength(1);
|
||||
expect(snapshot.messages[0].role).toBe('assistant');
|
||||
expect(snapshot.messages[0].content[0]).toMatchObject({
|
||||
type: 'actionRequired',
|
||||
data: {
|
||||
actionType: 'toolConfirmation',
|
||||
id: 'tool-1',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('applies elicitation requests as waiting action-required messages', () => {
|
||||
const snapshot = store.applyElicitationRequest(elicitationRequest('session-1'));
|
||||
|
||||
expect(snapshot.chatState).toBe(ChatState.WaitingForUserInput);
|
||||
expect(snapshot.messages).toHaveLength(1);
|
||||
expect(snapshot.messages[0].role).toBe('assistant');
|
||||
expect(snapshot.messages[0].content[0]).toMatchObject({
|
||||
type: 'actionRequired',
|
||||
data: {
|
||||
actionType: 'elicitation',
|
||||
id: 'acp_elicitation_1',
|
||||
message: 'Choose a project',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('stores submitted elicitation status', () => {
|
||||
store.applyElicitationRequest(elicitationRequest('session-1'));
|
||||
|
||||
const snapshot = store.setElicitationStatus('session-1', 'acp_elicitation_1', 'submitted');
|
||||
|
||||
expect(snapshot?.messages[0].content[0]).toMatchObject({
|
||||
type: 'actionRequired',
|
||||
data: {
|
||||
actionType: 'elicitation',
|
||||
id: 'acp_elicitation_1',
|
||||
isSubmitted: true,
|
||||
isCancelled: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('stores cancelled elicitation status', () => {
|
||||
store.applyElicitationRequest(elicitationRequest('session-1'));
|
||||
|
||||
const snapshot = store.setElicitationStatus('session-1', 'acp_elicitation_1', 'cancelled');
|
||||
|
||||
expect(snapshot?.messages[0].content[0]).toMatchObject({
|
||||
type: 'actionRequired',
|
||||
data: {
|
||||
actionType: 'elicitation',
|
||||
id: 'acp_elicitation_1',
|
||||
isSubmitted: false,
|
||||
isCancelled: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
import type { CreateElicitationRequest, CreateElicitationResponse } from '@agentclientprotocol/sdk';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
ACP_ELICITATION_TIMEOUT_SECONDS,
|
||||
cancelAcpElicitationRequestsForSession,
|
||||
requestAcpElicitation,
|
||||
resolveAcpElicitationRequest,
|
||||
} from '../elicitationRequests';
|
||||
import { acpChatSessionStore } from '../chatSessionStore';
|
||||
|
||||
vi.mock('../../acpChatFeatureFlag', () => ({
|
||||
USE_ACP_CHAT: true,
|
||||
}));
|
||||
|
||||
vi.mock('../chatSessionStore', () => ({
|
||||
acpChatSessionStore: {
|
||||
applyElicitationRequest: vi.fn(),
|
||||
setElicitationStatus: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const TEST_SESSION_IDS = ['session-1', 'session-2'];
|
||||
|
||||
function formRequest(sessionId: string): CreateElicitationRequest {
|
||||
return {
|
||||
mode: 'form',
|
||||
sessionId,
|
||||
message: 'Choose a project',
|
||||
requestedSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
project: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['project'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function expectStillPending(promise: Promise<CreateElicitationResponse>): Promise<void> {
|
||||
let settled = false;
|
||||
promise.then(
|
||||
() => {
|
||||
settled = true;
|
||||
},
|
||||
() => {
|
||||
settled = true;
|
||||
}
|
||||
);
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
expect(settled).toBe(false);
|
||||
}
|
||||
|
||||
describe('ACP elicitation requests', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
for (const sessionId of TEST_SESSION_IDS) {
|
||||
cancelAcpElicitationRequestsForSession(sessionId);
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const sessionId of TEST_SESSION_IDS) {
|
||||
cancelAcpElicitationRequestsForSession(sessionId);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps form requests pending until explicit resolve', async () => {
|
||||
const response = requestAcpElicitation(formRequest('session-1'));
|
||||
|
||||
await expectStillPending(response);
|
||||
|
||||
const appliedRequest = vi.mocked(acpChatSessionStore.applyElicitationRequest).mock.calls[0][0];
|
||||
|
||||
expect(appliedRequest.id).toMatch(/^acp_elicitation_/);
|
||||
expect(appliedRequest.sessionId).toBe('session-1');
|
||||
expect(appliedRequest.request.message).toBe('Choose a project');
|
||||
|
||||
expect(
|
||||
resolveAcpElicitationRequest('session-1', appliedRequest.id, {
|
||||
project: 'goose',
|
||||
})
|
||||
).toBe(true);
|
||||
expect(acpChatSessionStore.setElicitationStatus).toHaveBeenCalledWith(
|
||||
'session-1',
|
||||
appliedRequest.id,
|
||||
'submitted'
|
||||
);
|
||||
|
||||
await expect(response).resolves.toEqual({
|
||||
action: 'accept',
|
||||
content: {
|
||||
project: 'goose',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('cancels unsupported requests', async () => {
|
||||
await expect(
|
||||
requestAcpElicitation({
|
||||
mode: 'url',
|
||||
requestId: 'request-1',
|
||||
elicitationId: 'elicitation-1',
|
||||
message: 'Open this page',
|
||||
url: 'https://example.com',
|
||||
})
|
||||
).resolves.toEqual({ action: 'cancel' });
|
||||
});
|
||||
|
||||
it('cancels only pending requests for the requested session', async () => {
|
||||
const sessionOneResponse = requestAcpElicitation(formRequest('session-1'));
|
||||
const sessionTwoResponse = requestAcpElicitation(formRequest('session-2'));
|
||||
|
||||
const applyElicitationRequest = vi.mocked(acpChatSessionStore.applyElicitationRequest);
|
||||
const sessionOneRequest = applyElicitationRequest.mock.calls[0][0];
|
||||
const sessionTwoRequest = applyElicitationRequest.mock.calls[1][0];
|
||||
|
||||
cancelAcpElicitationRequestsForSession('session-1');
|
||||
|
||||
expect(acpChatSessionStore.setElicitationStatus).toHaveBeenCalledWith(
|
||||
'session-1',
|
||||
sessionOneRequest.id,
|
||||
'cancelled'
|
||||
);
|
||||
await expect(sessionOneResponse).resolves.toEqual({ action: 'cancel' });
|
||||
await expectStillPending(sessionTwoResponse);
|
||||
|
||||
expect(resolveAcpElicitationRequest('session-2', sessionTwoRequest.id, {})).toBe(true);
|
||||
await expect(sessionTwoResponse).resolves.toEqual({
|
||||
action: 'accept',
|
||||
content: {},
|
||||
});
|
||||
expect(resolveAcpElicitationRequest('session-1', sessionOneRequest.id, {})).toBe(false);
|
||||
});
|
||||
|
||||
it('cancels pending requests when they expire', async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const response = requestAcpElicitation(formRequest('session-1'));
|
||||
const appliedRequest = vi.mocked(acpChatSessionStore.applyElicitationRequest).mock
|
||||
.calls[0][0];
|
||||
|
||||
await expectStillPending(response);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(ACP_ELICITATION_TIMEOUT_SECONDS * 1000);
|
||||
|
||||
await expect(response).resolves.toEqual({ action: 'cancel' });
|
||||
expect(resolveAcpElicitationRequest('session-1', appliedRequest.id, {})).toBe(false);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { RequestPermissionRequest, RequestPermissionResponse } from '@agentclientprotocol/sdk';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
cancelAcpPermissionRequestsForSession,
|
||||
requestAcpPermission,
|
||||
resolveAcpPermissionRequest,
|
||||
} from '../permissionRequests';
|
||||
|
||||
vi.mock('../../acpChatFeatureFlag', () => ({
|
||||
USE_ACP_CHAT: true,
|
||||
}));
|
||||
|
||||
vi.mock('../chatSessionStore', () => ({
|
||||
acpChatSessionStore: {
|
||||
applyPermissionRequest: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
function permissionRequest(sessionId: string, toolCallId: string): RequestPermissionRequest {
|
||||
return {
|
||||
sessionId,
|
||||
options: [
|
||||
{ optionId: 'allow-once', name: 'Allow once', kind: 'allow_once' },
|
||||
{ optionId: 'reject-once', name: 'Deny once', kind: 'reject_once' },
|
||||
],
|
||||
toolCall: {
|
||||
toolCallId,
|
||||
title: 'Read file',
|
||||
rawInput: { path: 'README.md' },
|
||||
content: [
|
||||
{
|
||||
type: 'content',
|
||||
content: { type: 'text', text: 'Allow reading README.md?' },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const TEST_SESSION_IDS = ['session-1', 'session-2'];
|
||||
|
||||
async function expectStillPending(promise: Promise<RequestPermissionResponse>): Promise<void> {
|
||||
let settled = false;
|
||||
promise.then(
|
||||
() => {
|
||||
settled = true;
|
||||
},
|
||||
() => {
|
||||
settled = true;
|
||||
}
|
||||
);
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
expect(settled).toBe(false);
|
||||
}
|
||||
|
||||
describe('ACP permission requests', () => {
|
||||
beforeEach(() => {
|
||||
for (const sessionId of TEST_SESSION_IDS) {
|
||||
cancelAcpPermissionRequestsForSession(sessionId);
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const sessionId of TEST_SESSION_IDS) {
|
||||
cancelAcpPermissionRequestsForSession(sessionId);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps permission requests pending until explicit resolve', async () => {
|
||||
const response = requestAcpPermission(permissionRequest('session-1', 'tool-1'));
|
||||
|
||||
await expectStillPending(response);
|
||||
|
||||
expect(resolveAcpPermissionRequest('session-1', 'tool-1', 'allow_once')).toBe(true);
|
||||
await expect(response).resolves.toEqual({
|
||||
outcome: {
|
||||
outcome: 'selected',
|
||||
optionId: 'allow-once',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('cancels only pending requests for the requested session', async () => {
|
||||
const sessionOneResponse = requestAcpPermission(permissionRequest('session-1', 'tool-1'));
|
||||
const sessionTwoResponse = requestAcpPermission(permissionRequest('session-2', 'tool-2'));
|
||||
|
||||
cancelAcpPermissionRequestsForSession('session-1');
|
||||
|
||||
await expect(sessionOneResponse).resolves.toEqual({
|
||||
outcome: {
|
||||
outcome: 'cancelled',
|
||||
},
|
||||
});
|
||||
await expectStillPending(sessionTwoResponse);
|
||||
|
||||
expect(resolveAcpPermissionRequest('session-2', 'tool-2', 'deny_once')).toBe(true);
|
||||
await expect(sessionTwoResponse).resolves.toEqual({
|
||||
outcome: {
|
||||
outcome: 'selected',
|
||||
optionId: 'reject-once',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('cancels an older duplicate request for the same session and tool call', async () => {
|
||||
const firstResponse = requestAcpPermission(permissionRequest('session-1', 'tool-1'));
|
||||
const secondResponse = requestAcpPermission(permissionRequest('session-1', 'tool-1'));
|
||||
|
||||
await expect(firstResponse).resolves.toEqual({
|
||||
outcome: {
|
||||
outcome: 'cancelled',
|
||||
},
|
||||
});
|
||||
await expectStillPending(secondResponse);
|
||||
|
||||
expect(resolveAcpPermissionRequest('session-1', 'tool-1', 'allow_once')).toBe(true);
|
||||
await expect(secondResponse).resolves.toEqual({
|
||||
outcome: {
|
||||
outcome: 'selected',
|
||||
optionId: 'allow-once',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,8 +5,12 @@ import {
|
||||
} from '@aaif/goose-sdk';
|
||||
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk';
|
||||
import packageJson from '../../package.json';
|
||||
import { routeAcpGooseSessionNotification, routeAcpSessionNotification } from './chatNotifications';
|
||||
import {
|
||||
handleAcpGooseSessionNotification,
|
||||
handleAcpSessionNotification,
|
||||
} from './chatNotifications';
|
||||
import { createWebSocketStream } from './createWebSocketStream';
|
||||
import { requestAcpElicitation } from './elicitationRequests';
|
||||
import { requestAcpPermission } from './permissionRequests';
|
||||
|
||||
let clientPromise: Promise<GooseClient> | null = null;
|
||||
@@ -15,8 +19,9 @@ let resolvedClient: GooseClient | null = null;
|
||||
function createClientCallbacks(): () => GooseClientCallbacks {
|
||||
return () => ({
|
||||
requestPermission: requestAcpPermission,
|
||||
sessionUpdate: routeAcpSessionNotification,
|
||||
unstable_sessionUpdate: routeAcpGooseSessionNotification,
|
||||
unstable_createElicitation: requestAcpElicitation,
|
||||
sessionUpdate: handleAcpSessionNotification,
|
||||
unstable_sessionUpdate: handleAcpGooseSessionNotification,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -44,6 +49,7 @@ async function initializeConnection(): Promise<GooseClient> {
|
||||
await client.initialize({
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
clientCapabilities: {
|
||||
elicitation: { form: {} },
|
||||
_meta: {
|
||||
goose: {
|
||||
mcpHostCapabilities: DEFAULT_GOOSE_MCP_HOST_CAPABILITIES,
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { Message } from '../../api';
|
||||
import type { AcpElicitationRequest } from '../elicitationRequests';
|
||||
import {
|
||||
type AcpChatStateChange,
|
||||
type AdapterState,
|
||||
DEFAULT_VISIBLE_MESSAGE_METADATA,
|
||||
messagesChange,
|
||||
} from './shared';
|
||||
|
||||
export type ElicitationStatus = 'submitted' | 'cancelled';
|
||||
|
||||
export function applyElicitationRequest(
|
||||
state: AdapterState,
|
||||
request: AcpElicitationRequest
|
||||
): AcpChatStateChange[] {
|
||||
if (hasExistingElicitation(state, request.id)) {
|
||||
return messagesChange(state);
|
||||
}
|
||||
|
||||
state.messages.push({
|
||||
id: request.id,
|
||||
role: 'assistant',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
content: [
|
||||
{
|
||||
type: 'actionRequired',
|
||||
data: {
|
||||
actionType: 'elicitation',
|
||||
id: request.id,
|
||||
message: request.request.message,
|
||||
requested_schema: request.request.requestedSchema,
|
||||
},
|
||||
},
|
||||
],
|
||||
metadata: { ...DEFAULT_VISIBLE_MESSAGE_METADATA },
|
||||
});
|
||||
|
||||
return messagesChange(state);
|
||||
}
|
||||
|
||||
export function applyElicitationStatus(
|
||||
state: AdapterState,
|
||||
elicitationId: string,
|
||||
status: ElicitationStatus
|
||||
): AcpChatStateChange[] {
|
||||
const statusData = {
|
||||
isSubmitted: status === 'submitted',
|
||||
isCancelled: status === 'cancelled',
|
||||
};
|
||||
let changed = false;
|
||||
|
||||
state.messages = state.messages.map((message) => {
|
||||
let messageChanged = false;
|
||||
const content = message.content.map((content) => {
|
||||
if (
|
||||
content.type !== 'actionRequired' ||
|
||||
content.data.actionType !== 'elicitation' ||
|
||||
content.data.id !== elicitationId
|
||||
) {
|
||||
return content;
|
||||
}
|
||||
|
||||
messageChanged = true;
|
||||
changed = true;
|
||||
return {
|
||||
...content,
|
||||
data: {
|
||||
...content.data,
|
||||
...statusData,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return messageChanged ? { ...message, content } : message;
|
||||
});
|
||||
|
||||
return changed ? messagesChange(state) : [];
|
||||
}
|
||||
|
||||
function hasExistingElicitation(state: AdapterState, elicitationId: string): boolean {
|
||||
return state.messages.some((message: Message) =>
|
||||
message.content.some(
|
||||
(content) =>
|
||||
content.type === 'actionRequired' &&
|
||||
content.data.actionType === 'elicitation' &&
|
||||
content.data.id === elicitationId
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
import type { GooseSessionNotification_unstable } from '@aaif/goose-sdk';
|
||||
import type { SessionNotification } from '@agentclientprotocol/sdk';
|
||||
import { createSessionScopedNotificationRouter } from './sessionScopedNotificationRouter';
|
||||
import { USE_ACP_CHAT } from '../acpChatFeatureFlag';
|
||||
import { acpChatSessionStore } from './chatSessionStore';
|
||||
|
||||
const acpSessionRouter = createSessionScopedNotificationRouter<SessionNotification>();
|
||||
const gooseSessionRouter =
|
||||
createSessionScopedNotificationRouter<GooseSessionNotification_unstable>();
|
||||
export function handleAcpSessionNotification(notification: SessionNotification): Promise<void> {
|
||||
if (USE_ACP_CHAT) {
|
||||
acpChatSessionStore.applyAcpSessionNotification(notification);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
export const subscribeToAcpSession = acpSessionRouter.subscribe;
|
||||
export const routeAcpSessionNotification = async (
|
||||
notification: SessionNotification
|
||||
): Promise<void> => {
|
||||
await acpSessionRouter.route(notification);
|
||||
};
|
||||
|
||||
export const subscribeToAcpGooseSession = gooseSessionRouter.subscribe;
|
||||
export const routeAcpGooseSessionNotification = async (
|
||||
export function handleAcpGooseSessionNotification(
|
||||
notification: GooseSessionNotification_unstable
|
||||
): Promise<void> => {
|
||||
await gooseSessionRouter.route(notification);
|
||||
};
|
||||
): Promise<void> {
|
||||
if (USE_ACP_CHAT) {
|
||||
acpChatSessionStore.applyAcpGooseSessionNotification(notification);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
import type { GooseSessionNotification_unstable } from '@aaif/goose-sdk';
|
||||
import type { RequestPermissionRequest, SessionNotification } from '@agentclientprotocol/sdk';
|
||||
import type { Message, Session, TokenState } from '../api';
|
||||
import { ChatState } from '../types/chatState';
|
||||
import {
|
||||
createAcpSessionNotificationAdapter,
|
||||
type AcpChatStateChange,
|
||||
type AcpSessionNotificationAdapter,
|
||||
} from './sessionNotificationAdapter';
|
||||
import type { ElicitationStatus } from './adapter/elicitations';
|
||||
import { cloneMessage } from './adapter/shared';
|
||||
import type { AcpElicitationRequest } from './elicitationRequests';
|
||||
|
||||
export interface AcpChatSessionSnapshot {
|
||||
session: Session | undefined;
|
||||
messages: Message[];
|
||||
tokenState: TokenState;
|
||||
chatState: ChatState;
|
||||
sessionLoadError: string | undefined;
|
||||
activePromptAttemptId: string | null;
|
||||
}
|
||||
|
||||
type SnapshotListener = (snapshot: AcpChatSessionSnapshot) => void;
|
||||
|
||||
interface StoreEntry extends AcpChatSessionSnapshot {
|
||||
adapter: AcpSessionNotificationAdapter;
|
||||
}
|
||||
|
||||
const initialTokenState: TokenState = {
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
totalTokens: 0,
|
||||
accumulatedInputTokens: 0,
|
||||
accumulatedOutputTokens: 0,
|
||||
accumulatedTotalTokens: 0,
|
||||
};
|
||||
|
||||
export interface AcpChatSessionStore {
|
||||
getSnapshot(sessionId: string): AcpChatSessionSnapshot | undefined;
|
||||
subscribe(sessionId: string, listener: (snapshot: AcpChatSessionSnapshot) => void): () => void;
|
||||
deleteSnapshot(sessionId: string): void;
|
||||
setLoadedSession(
|
||||
sessionId: string,
|
||||
session: Session,
|
||||
tokenState?: TokenState
|
||||
): AcpChatSessionSnapshot;
|
||||
setSessionMetadata(sessionId: string, session: Session | undefined): AcpChatSessionSnapshot;
|
||||
setMessages(sessionId: string, messages: Message[]): AcpChatSessionSnapshot;
|
||||
setTokenState(sessionId: string, tokenState: TokenState): AcpChatSessionSnapshot;
|
||||
setChatState(sessionId: string, chatState: ChatState): AcpChatSessionSnapshot;
|
||||
setSessionLoadError(
|
||||
sessionId: string,
|
||||
sessionLoadError: string | undefined
|
||||
): AcpChatSessionSnapshot;
|
||||
startPromptAttempt(sessionId: string, promptAttemptId: string): AcpChatSessionSnapshot;
|
||||
finishPromptAttemptIfCurrent(sessionId: string, promptAttemptId: string, error?: string): boolean;
|
||||
clearActivePromptAttempt(sessionId: string): AcpChatSessionSnapshot | undefined;
|
||||
isCurrentPromptAttempt(sessionId: string, promptAttemptId: string): boolean;
|
||||
applyAcpSessionNotification(notification: SessionNotification): AcpChatSessionSnapshot;
|
||||
applyAcpGooseSessionNotification(
|
||||
notification: GooseSessionNotification_unstable
|
||||
): AcpChatSessionSnapshot;
|
||||
applyPermissionRequest(request: RequestPermissionRequest): AcpChatSessionSnapshot;
|
||||
applyElicitationRequest(request: AcpElicitationRequest): AcpChatSessionSnapshot;
|
||||
setElicitationStatus(
|
||||
sessionId: string,
|
||||
elicitationId: string,
|
||||
status: ElicitationStatus
|
||||
): AcpChatSessionSnapshot | undefined;
|
||||
}
|
||||
|
||||
export function createAcpChatSessionStore(): AcpChatSessionStore {
|
||||
const sessionsById = new Map<string, StoreEntry>();
|
||||
const listenersBySessionId = new Map<string, Set<SnapshotListener>>();
|
||||
|
||||
const getSnapshot: AcpChatSessionStore['getSnapshot'] = (sessionId) => {
|
||||
const entry = sessionsById.get(sessionId);
|
||||
return entry ? snapshotFromEntry(entry) : undefined;
|
||||
};
|
||||
|
||||
const subscribe: AcpChatSessionStore['subscribe'] = (sessionId, listener) => {
|
||||
const listeners = listenersBySessionId.get(sessionId) ?? new Set<SnapshotListener>();
|
||||
listeners.add(listener);
|
||||
listenersBySessionId.set(sessionId, listeners);
|
||||
|
||||
let subscribed = true;
|
||||
return () => {
|
||||
if (!subscribed) {
|
||||
return;
|
||||
}
|
||||
|
||||
subscribed = false;
|
||||
const currentListeners = listenersBySessionId.get(sessionId);
|
||||
if (!currentListeners) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentListeners.delete(listener);
|
||||
if (currentListeners.size === 0) {
|
||||
listenersBySessionId.delete(sessionId);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const deleteSnapshot: AcpChatSessionStore['deleteSnapshot'] = (sessionId) => {
|
||||
sessionsById.delete(sessionId);
|
||||
};
|
||||
|
||||
const getOrCreateEntry = (sessionId: string): StoreEntry => {
|
||||
const existing = sessionsById.get(sessionId);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const entry: StoreEntry = {
|
||||
session: undefined,
|
||||
messages: [],
|
||||
tokenState: { ...initialTokenState },
|
||||
chatState: ChatState.Idle,
|
||||
sessionLoadError: undefined,
|
||||
activePromptAttemptId: null,
|
||||
adapter: createAcpSessionNotificationAdapter(),
|
||||
};
|
||||
sessionsById.set(sessionId, entry);
|
||||
return entry;
|
||||
};
|
||||
|
||||
const notify = (sessionId: string, entry: StoreEntry): AcpChatSessionSnapshot => {
|
||||
const snapshot = snapshotFromEntry(entry);
|
||||
const listeners = listenersBySessionId.get(sessionId);
|
||||
if (listeners) {
|
||||
for (const listener of listeners) {
|
||||
listener(snapshot);
|
||||
}
|
||||
}
|
||||
return snapshot;
|
||||
};
|
||||
|
||||
const setLoadedSession: AcpChatSessionStore['setLoadedSession'] = (
|
||||
sessionId,
|
||||
session,
|
||||
tokenState = tokenStateFromSession(session)
|
||||
) => {
|
||||
const entry = getOrCreateEntry(sessionId);
|
||||
entry.session = session;
|
||||
entry.messages = cloneMessages(session.conversation ?? []);
|
||||
entry.tokenState = { ...tokenState };
|
||||
entry.chatState = entry.activePromptAttemptId ? ChatState.Streaming : ChatState.Idle;
|
||||
entry.sessionLoadError = undefined;
|
||||
entry.adapter = createAcpSessionNotificationAdapter(entry.messages);
|
||||
return notify(sessionId, entry);
|
||||
};
|
||||
|
||||
const setSessionMetadata: AcpChatSessionStore['setSessionMetadata'] = (sessionId, session) => {
|
||||
const entry = getOrCreateEntry(sessionId);
|
||||
entry.session = session;
|
||||
return notify(sessionId, entry);
|
||||
};
|
||||
|
||||
const setMessages: AcpChatSessionStore['setMessages'] = (sessionId, messages) => {
|
||||
const entry = getOrCreateEntry(sessionId);
|
||||
entry.messages = cloneMessages(messages);
|
||||
entry.adapter = createAcpSessionNotificationAdapter(entry.messages);
|
||||
return notify(sessionId, entry);
|
||||
};
|
||||
|
||||
const setTokenState: AcpChatSessionStore['setTokenState'] = (sessionId, tokenState) => {
|
||||
const entry = getOrCreateEntry(sessionId);
|
||||
entry.tokenState = { ...tokenState };
|
||||
return notify(sessionId, entry);
|
||||
};
|
||||
|
||||
const setChatState: AcpChatSessionStore['setChatState'] = (sessionId, chatState) => {
|
||||
const entry = getOrCreateEntry(sessionId);
|
||||
entry.chatState = chatState;
|
||||
return notify(sessionId, entry);
|
||||
};
|
||||
|
||||
const setSessionLoadError: AcpChatSessionStore['setSessionLoadError'] = (
|
||||
sessionId,
|
||||
sessionLoadError
|
||||
) => {
|
||||
const entry = getOrCreateEntry(sessionId);
|
||||
entry.sessionLoadError = sessionLoadError;
|
||||
return notify(sessionId, entry);
|
||||
};
|
||||
|
||||
const startPromptAttempt: AcpChatSessionStore['startPromptAttempt'] = (
|
||||
sessionId,
|
||||
promptAttemptId
|
||||
) => {
|
||||
const entry = getOrCreateEntry(sessionId);
|
||||
entry.activePromptAttemptId = promptAttemptId;
|
||||
entry.chatState = ChatState.Streaming;
|
||||
entry.sessionLoadError = undefined;
|
||||
return notify(sessionId, entry);
|
||||
};
|
||||
|
||||
const finishPromptAttemptIfCurrent: AcpChatSessionStore['finishPromptAttemptIfCurrent'] = (
|
||||
sessionId,
|
||||
promptAttemptId,
|
||||
error
|
||||
) => {
|
||||
const entry = sessionsById.get(sessionId);
|
||||
if (!entry || entry.activePromptAttemptId !== promptAttemptId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
entry.activePromptAttemptId = null;
|
||||
entry.chatState = ChatState.Idle;
|
||||
entry.sessionLoadError = error;
|
||||
notify(sessionId, entry);
|
||||
return true;
|
||||
};
|
||||
|
||||
const clearActivePromptAttempt: AcpChatSessionStore['clearActivePromptAttempt'] = (sessionId) => {
|
||||
const entry = sessionsById.get(sessionId);
|
||||
if (!entry) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
entry.activePromptAttemptId = null;
|
||||
entry.chatState = ChatState.Idle;
|
||||
return notify(sessionId, entry);
|
||||
};
|
||||
|
||||
const isCurrentPromptAttempt: AcpChatSessionStore['isCurrentPromptAttempt'] = (
|
||||
sessionId,
|
||||
promptAttemptId
|
||||
) => sessionsById.get(sessionId)?.activePromptAttemptId === promptAttemptId;
|
||||
|
||||
const applyAcpSessionNotification: AcpChatSessionStore['applyAcpSessionNotification'] = (
|
||||
notification
|
||||
) => {
|
||||
const entry = getOrCreateEntry(notification.sessionId);
|
||||
const changes = entry.adapter.apply(notification);
|
||||
applyChatStateChanges(entry, changes);
|
||||
return notify(notification.sessionId, entry);
|
||||
};
|
||||
|
||||
const applyAcpGooseSessionNotification: AcpChatSessionStore['applyAcpGooseSessionNotification'] =
|
||||
(notification) => {
|
||||
const entry = getOrCreateEntry(notification.sessionId);
|
||||
const changes = entry.adapter.applyGoose(notification);
|
||||
applyChatStateChanges(entry, changes);
|
||||
return notify(notification.sessionId, entry);
|
||||
};
|
||||
|
||||
const applyPermissionRequest: AcpChatSessionStore['applyPermissionRequest'] = (request) => {
|
||||
const entry = getOrCreateEntry(request.sessionId);
|
||||
const changes = entry.adapter.applyPermissionRequest(request);
|
||||
applyChatStateChanges(entry, changes);
|
||||
entry.chatState = ChatState.WaitingForUserInput;
|
||||
return notify(request.sessionId, entry);
|
||||
};
|
||||
|
||||
const applyElicitationRequest: AcpChatSessionStore['applyElicitationRequest'] = (request) => {
|
||||
const entry = getOrCreateEntry(request.sessionId);
|
||||
const changes = entry.adapter.applyElicitationRequest(request);
|
||||
applyChatStateChanges(entry, changes);
|
||||
entry.chatState = ChatState.WaitingForUserInput;
|
||||
return notify(request.sessionId, entry);
|
||||
};
|
||||
|
||||
const setElicitationStatus: AcpChatSessionStore['setElicitationStatus'] = (
|
||||
sessionId,
|
||||
elicitationId,
|
||||
status
|
||||
) => {
|
||||
const entry = sessionsById.get(sessionId);
|
||||
if (!entry) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const changes = entry.adapter.applyElicitationStatus(elicitationId, status);
|
||||
if (changes.length === 0) {
|
||||
return snapshotFromEntry(entry);
|
||||
}
|
||||
|
||||
applyChatStateChanges(entry, changes);
|
||||
return notify(sessionId, entry);
|
||||
};
|
||||
|
||||
return {
|
||||
getSnapshot,
|
||||
subscribe,
|
||||
deleteSnapshot,
|
||||
setLoadedSession,
|
||||
setSessionMetadata,
|
||||
setMessages,
|
||||
setTokenState,
|
||||
setChatState,
|
||||
setSessionLoadError,
|
||||
startPromptAttempt,
|
||||
finishPromptAttemptIfCurrent,
|
||||
clearActivePromptAttempt,
|
||||
isCurrentPromptAttempt,
|
||||
applyAcpSessionNotification,
|
||||
applyAcpGooseSessionNotification,
|
||||
applyPermissionRequest,
|
||||
applyElicitationRequest,
|
||||
setElicitationStatus,
|
||||
};
|
||||
}
|
||||
|
||||
export const acpChatSessionStore = createAcpChatSessionStore();
|
||||
|
||||
export function tokenStateFromSession(session: Session | undefined): TokenState {
|
||||
return {
|
||||
inputTokens: session?.input_tokens ?? 0,
|
||||
outputTokens: session?.output_tokens ?? 0,
|
||||
totalTokens: session?.total_tokens ?? 0,
|
||||
accumulatedInputTokens: session?.accumulated_input_tokens ?? 0,
|
||||
accumulatedOutputTokens: session?.accumulated_output_tokens ?? 0,
|
||||
accumulatedTotalTokens: session?.accumulated_total_tokens ?? 0,
|
||||
...(session?.accumulated_cost !== undefined
|
||||
? { accumulatedCost: session.accumulated_cost }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
function applyChatStateChanges(entry: StoreEntry, changes: AcpChatStateChange[]): void {
|
||||
for (const change of changes) {
|
||||
switch (change.type) {
|
||||
case 'messages':
|
||||
entry.messages = cloneMessages(change.messages);
|
||||
break;
|
||||
case 'tokenState':
|
||||
entry.tokenState = { ...entry.tokenState, ...change.tokenState };
|
||||
break;
|
||||
case 'sessionInfo':
|
||||
if (change.name && entry.session) {
|
||||
entry.session = { ...entry.session, name: change.name };
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function snapshotFromEntry(entry: StoreEntry): AcpChatSessionSnapshot {
|
||||
return {
|
||||
session: entry.session,
|
||||
messages: cloneMessages(entry.messages),
|
||||
tokenState: { ...entry.tokenState },
|
||||
chatState: entry.chatState,
|
||||
sessionLoadError: entry.sessionLoadError,
|
||||
activePromptAttemptId: entry.activePromptAttemptId,
|
||||
};
|
||||
}
|
||||
|
||||
function cloneMessages(messages: Message[]): Message[] {
|
||||
return messages.map(cloneMessage);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import type {
|
||||
CreateElicitationRequest,
|
||||
CreateElicitationResponse,
|
||||
ElicitationContentValue,
|
||||
ElicitationSchema,
|
||||
} from '@agentclientprotocol/sdk';
|
||||
import { v7 as uuidv7 } from 'uuid';
|
||||
import { USE_ACP_CHAT } from '../acpChatFeatureFlag';
|
||||
import { acpChatSessionStore } from './chatSessionStore';
|
||||
|
||||
type SessionScopedFormElicitationRequest = CreateElicitationRequest & {
|
||||
mode: 'form';
|
||||
sessionId: string;
|
||||
requestedSchema: ElicitationSchema;
|
||||
};
|
||||
|
||||
export interface AcpElicitationRequest {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
request: SessionScopedFormElicitationRequest;
|
||||
}
|
||||
|
||||
interface PendingElicitationRequest {
|
||||
request: AcpElicitationRequest;
|
||||
resolve: (response: CreateElicitationResponse) => void;
|
||||
timeoutId: ReturnType<typeof setTimeout>;
|
||||
}
|
||||
|
||||
const pendingRequests = new Map<string, PendingElicitationRequest>();
|
||||
export const ACP_ELICITATION_TIMEOUT_SECONDS = 300;
|
||||
|
||||
export async function requestAcpElicitation(
|
||||
request: CreateElicitationRequest
|
||||
): Promise<CreateElicitationResponse> {
|
||||
if (!USE_ACP_CHAT || !isSessionScopedFormElicitation(request)) {
|
||||
return cancelledElicitationResponse();
|
||||
}
|
||||
|
||||
const elicitationRequest: AcpElicitationRequest = {
|
||||
id: `acp_elicitation_${uuidv7()}`,
|
||||
sessionId: request.sessionId,
|
||||
request,
|
||||
};
|
||||
const key = elicitationRequestKey(elicitationRequest.sessionId, elicitationRequest.id);
|
||||
|
||||
return new Promise<CreateElicitationResponse>((resolve) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
const pending = pendingRequests.get(key);
|
||||
if (!pending) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingRequests.delete(key);
|
||||
pending.resolve(cancelledElicitationResponse());
|
||||
}, ACP_ELICITATION_TIMEOUT_SECONDS * 1000);
|
||||
|
||||
pendingRequests.set(key, { request: elicitationRequest, resolve, timeoutId });
|
||||
acpChatSessionStore.applyElicitationRequest(elicitationRequest);
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveAcpElicitationRequest(
|
||||
sessionId: string,
|
||||
elicitationId: string,
|
||||
userData: Record<string, unknown>
|
||||
): boolean {
|
||||
const key = elicitationRequestKey(sessionId, elicitationId);
|
||||
const pending = pendingRequests.get(key);
|
||||
if (!pending) {
|
||||
return false;
|
||||
}
|
||||
|
||||
pendingRequests.delete(key);
|
||||
clearTimeout(pending.timeoutId);
|
||||
acpChatSessionStore.setElicitationStatus(sessionId, elicitationId, 'submitted');
|
||||
pending.resolve(acceptedElicitationResponse(userData));
|
||||
return true;
|
||||
}
|
||||
|
||||
export function cancelAcpElicitationRequestsForSession(sessionId: string): void {
|
||||
for (const [key, pending] of pendingRequests) {
|
||||
if (pending.request.sessionId === sessionId) {
|
||||
pendingRequests.delete(key);
|
||||
clearTimeout(pending.timeoutId);
|
||||
acpChatSessionStore.setElicitationStatus(sessionId, pending.request.id, 'cancelled');
|
||||
pending.resolve(cancelledElicitationResponse());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isSessionScopedFormElicitation(
|
||||
request: CreateElicitationRequest
|
||||
): request is SessionScopedFormElicitationRequest {
|
||||
return request.mode === 'form' && 'sessionId' in request && typeof request.sessionId === 'string';
|
||||
}
|
||||
|
||||
function acceptedElicitationResponse(userData: Record<string, unknown>): CreateElicitationResponse {
|
||||
return {
|
||||
action: 'accept',
|
||||
content: userData as Record<string, ElicitationContentValue>,
|
||||
};
|
||||
}
|
||||
|
||||
function cancelledElicitationResponse(): CreateElicitationResponse {
|
||||
return { action: 'cancel' };
|
||||
}
|
||||
|
||||
function elicitationRequestKey(sessionId: string, elicitationId: string): string {
|
||||
return `${sessionId}\u0000${elicitationId}`;
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
import type { RequestPermissionRequest, RequestPermissionResponse } from '@agentclientprotocol/sdk';
|
||||
import type { Permission } from '../api';
|
||||
import { createSessionScopedNotificationRouter } from './sessionScopedNotificationRouter';
|
||||
import { USE_ACP_CHAT } from '../acpChatFeatureFlag';
|
||||
import { acpChatSessionStore } from './chatSessionStore';
|
||||
|
||||
interface PendingPermissionRequest {
|
||||
request: RequestPermissionRequest;
|
||||
resolve: (response: RequestPermissionResponse) => void;
|
||||
}
|
||||
|
||||
const permissionRequestRouter = createSessionScopedNotificationRouter<RequestPermissionRequest>();
|
||||
const pendingRequests = new Map<string, PendingPermissionRequest>();
|
||||
|
||||
export const subscribeToAcpPermissionRequests = permissionRequestRouter.subscribe;
|
||||
|
||||
export async function requestAcpPermission(
|
||||
request: RequestPermissionRequest
|
||||
): Promise<RequestPermissionResponse> {
|
||||
@@ -21,28 +19,13 @@ export async function requestAcpPermission(
|
||||
previous.resolve(cancelledPermissionResponse());
|
||||
}
|
||||
|
||||
if (!USE_ACP_CHAT) {
|
||||
return cancelledPermissionResponse();
|
||||
}
|
||||
|
||||
return new Promise<RequestPermissionResponse>((resolve) => {
|
||||
pendingRequests.set(key, { request, resolve });
|
||||
|
||||
permissionRequestRouter
|
||||
.route(request)
|
||||
.then((routed) => {
|
||||
if (!routed) {
|
||||
const pending = pendingRequests.get(key);
|
||||
if (pending?.resolve === resolve) {
|
||||
pendingRequests.delete(key);
|
||||
resolve(cancelledPermissionResponse());
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('Failed to route ACP permission request:', error);
|
||||
const pending = pendingRequests.get(key);
|
||||
if (pending?.resolve === resolve) {
|
||||
pendingRequests.delete(key);
|
||||
resolve(cancelledPermissionResponse());
|
||||
}
|
||||
});
|
||||
acpChatSessionStore.applyPermissionRequest(request);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import type { GooseSessionNotification_unstable } from '@aaif/goose-sdk';
|
||||
import type { RequestPermissionRequest, SessionNotification } from '@agentclientprotocol/sdk';
|
||||
import type { Message } from '../api';
|
||||
import {
|
||||
applyElicitationRequest as applyElicitationRequestToState,
|
||||
applyElicitationStatus as applyElicitationStatusToState,
|
||||
type ElicitationStatus,
|
||||
} from './adapter/elicitations';
|
||||
import { applyGooseSessionNotification } from './adapter/gooseSessionNotifications';
|
||||
import { applyContentChunk, applyThoughtChunk } from './adapter/messages';
|
||||
import { applyPermissionRequest as applyPermissionRequestToState } from './adapter/permissions';
|
||||
import { type AcpChatStateChange, type AdapterState, cloneMessage } from './adapter/shared';
|
||||
import { applyToolCall, applyToolCallUpdate } from './adapter/tools';
|
||||
import type { AcpElicitationRequest } from './elicitationRequests';
|
||||
|
||||
export type { AcpChatStateChange } from './adapter/shared';
|
||||
|
||||
@@ -13,6 +19,8 @@ export interface AcpSessionNotificationAdapter {
|
||||
apply(notification: SessionNotification): AcpChatStateChange[];
|
||||
applyGoose(notification: GooseSessionNotification_unstable): AcpChatStateChange[];
|
||||
applyPermissionRequest(request: RequestPermissionRequest): AcpChatStateChange[];
|
||||
applyElicitationRequest(request: AcpElicitationRequest): AcpChatStateChange[];
|
||||
applyElicitationStatus(elicitationId: string, status: ElicitationStatus): AcpChatStateChange[];
|
||||
getMessages(): Message[];
|
||||
}
|
||||
|
||||
@@ -33,6 +41,12 @@ export function createAcpSessionNotificationAdapter(
|
||||
applyPermissionRequest(request) {
|
||||
return applyPermissionRequestToState(state, request);
|
||||
},
|
||||
applyElicitationRequest(request) {
|
||||
return applyElicitationRequestToState(state, request);
|
||||
},
|
||||
applyElicitationStatus(elicitationId, status) {
|
||||
return applyElicitationStatusToState(state, elicitationId, status);
|
||||
},
|
||||
getMessages() {
|
||||
return state.messages.map(cloneMessage);
|
||||
},
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
type SessionScopedNotificationListener<TNotification> = (
|
||||
notification: TNotification
|
||||
) => Promise<void> | void;
|
||||
|
||||
interface SessionScopedNotification {
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
export function createSessionScopedNotificationRouter<
|
||||
TNotification extends SessionScopedNotification,
|
||||
>() {
|
||||
const listenersBySessionId = new Map<
|
||||
string,
|
||||
Set<SessionScopedNotificationListener<TNotification>>
|
||||
>();
|
||||
|
||||
const subscribe = (
|
||||
sessionId: string,
|
||||
listener: SessionScopedNotificationListener<TNotification>
|
||||
): (() => void) => {
|
||||
const listeners = listenersBySessionId.get(sessionId) ?? new Set();
|
||||
listeners.add(listener);
|
||||
listenersBySessionId.set(sessionId, listeners);
|
||||
|
||||
let subscribed = true;
|
||||
|
||||
return () => {
|
||||
if (!subscribed) {
|
||||
return;
|
||||
}
|
||||
|
||||
subscribed = false;
|
||||
const currentListeners = listenersBySessionId.get(sessionId);
|
||||
if (!currentListeners) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentListeners.delete(listener);
|
||||
if (currentListeners.size === 0) {
|
||||
listenersBySessionId.delete(sessionId);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const route = async (notification: TNotification): Promise<boolean> => {
|
||||
const listeners = listenersBySessionId.get(notification.sessionId);
|
||||
if (!listeners) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await Promise.all([...listeners].map((listener) => listener(notification)));
|
||||
return true;
|
||||
};
|
||||
|
||||
return {
|
||||
route,
|
||||
subscribe,
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { ForkSessionRequest, ListSessionsRequest, SessionInfo } from '@agentclientprotocol/sdk';
|
||||
import type {
|
||||
ForkSessionRequest,
|
||||
ListSessionsRequest,
|
||||
SessionInfo,
|
||||
} from '@agentclientprotocol/sdk';
|
||||
import { getAcpClient } from './acpConnection';
|
||||
import { DEFAULT_CHAT_TITLE } from '../contexts/ChatContext';
|
||||
|
||||
@@ -99,10 +103,27 @@ export async function acpRenameSession(sessionId: string, title: string): Promis
|
||||
await client.goose.sessionRename_unstable({ sessionId, title });
|
||||
}
|
||||
|
||||
export async function acpForkSession(sessionId: string, cwd: string): Promise<void> {
|
||||
export async function acpTruncateSessionConversation(
|
||||
sessionId: string,
|
||||
truncateFrom: number
|
||||
): Promise<void> {
|
||||
const client = await getAcpClient();
|
||||
await client.goose.sessionConversationTruncate_unstable({ sessionId, truncateFrom });
|
||||
}
|
||||
|
||||
export async function acpForkSession(
|
||||
sessionId: string,
|
||||
conversationBefore?: number
|
||||
): Promise<string> {
|
||||
const client = await getAcpClient();
|
||||
const sessionInfo = await client.goose.sessionInfo_unstable({ sessionId });
|
||||
const { cwd } = sessionInfo.session;
|
||||
const request: ForkSessionRequest = { sessionId, cwd };
|
||||
await client.unstable_forkSession(request);
|
||||
if (conversationBefore !== undefined) {
|
||||
request._meta = { conversationBefore };
|
||||
}
|
||||
const response = await client.unstable_forkSession(request);
|
||||
return String(response.sessionId);
|
||||
}
|
||||
|
||||
export async function acpExportSession(sessionId: string): Promise<string> {
|
||||
|
||||
@@ -113,6 +113,7 @@ export default function BaseChat({
|
||||
setRecipeUserParams,
|
||||
tokenState,
|
||||
notifications: toolCallNotifications,
|
||||
pauseQueueOnStop,
|
||||
onMessageUpdate,
|
||||
} = useChatSession({
|
||||
sessionId,
|
||||
@@ -510,6 +511,7 @@ export default function BaseChat({
|
||||
chatState={chatState}
|
||||
setChatState={setChatState}
|
||||
onStop={stopStreaming}
|
||||
pauseQueueOnStop={pauseQueueOnStop}
|
||||
commandHistory={commandHistory}
|
||||
initialValue={initialPrompt}
|
||||
setView={setView}
|
||||
|
||||
@@ -30,11 +30,7 @@ import { DiagnosticsModal } from './ui/Diagnostics';
|
||||
import { getSession, Message } from '../api';
|
||||
import { getInitialWorkingDir } from '../utils/workingDir';
|
||||
import { getPredefinedModelsFromEnv } from './settings/models/predefinedModelsUtils';
|
||||
import {
|
||||
trackFileAttached,
|
||||
trackVoiceDictation,
|
||||
trackDiagnosticsOpened,
|
||||
} from '../utils/analytics';
|
||||
import { trackFileAttached, trackVoiceDictation, trackDiagnosticsOpened } from '../utils/analytics';
|
||||
import { getNavigationShortcutText } from '../utils/keyboardShortcuts';
|
||||
import { UserInput, ImageData } from '../types/message';
|
||||
import { compressImageDataUrl } from '../utils/conversionUtils';
|
||||
@@ -51,9 +47,7 @@ const turndown = new TurndownService({
|
||||
turndown.addRule('complexLinks', {
|
||||
filter: (node) => {
|
||||
return (
|
||||
node.nodeName === 'A' &&
|
||||
!!node.getAttribute('href') &&
|
||||
/\n/.test(node.textContent || '')
|
||||
node.nodeName === 'A' && !!node.getAttribute('href') && /\n/.test(node.textContent || '')
|
||||
);
|
||||
},
|
||||
replacement: (content, node) => {
|
||||
@@ -178,6 +172,7 @@ interface ChatInputProps {
|
||||
chatState: ChatState;
|
||||
setChatState?: (state: ChatState) => void;
|
||||
onStop?: () => void;
|
||||
pauseQueueOnStop?: boolean;
|
||||
commandHistory?: string[];
|
||||
initialValue?: string;
|
||||
droppedFiles?: DroppedFile[];
|
||||
@@ -209,6 +204,7 @@ export default function ChatInput({
|
||||
chatState = ChatState.Idle,
|
||||
setChatState,
|
||||
onStop,
|
||||
pauseQueueOnStop = false,
|
||||
commandHistory = [],
|
||||
initialValue = '',
|
||||
droppedFiles = [],
|
||||
@@ -1378,6 +1374,13 @@ export default function ChatInput({
|
||||
if (onStop) onStop();
|
||||
};
|
||||
|
||||
const handleStop = () => {
|
||||
if (pauseQueueOnStop && queuedMessages.length > 0) {
|
||||
pauseRemainingQueue();
|
||||
}
|
||||
if (onStop) onStop();
|
||||
};
|
||||
|
||||
const handleResumeQueue = () => {
|
||||
queuePausedRef.current = false;
|
||||
setLastInterruption(null);
|
||||
@@ -1588,10 +1591,7 @@ export default function ChatInput({
|
||||
extensions, diagnostics, attach, mic, send. When the bar is narrow
|
||||
(e.g. on a small window), the secondary controls drop out so the
|
||||
model selector + send button always stay visible. */}
|
||||
<div
|
||||
ref={bottomBarRef}
|
||||
className="flex flex-row items-center gap-2 px-3 py-2 relative"
|
||||
>
|
||||
<div ref={bottomBarRef} className="flex flex-row items-center gap-2 px-3 py-2 relative">
|
||||
{/* Left: model selector */}
|
||||
<Tooltip>
|
||||
<div>
|
||||
@@ -1746,7 +1746,7 @@ export default function ChatInput({
|
||||
{isLoading && !hasSubmittableContent ? (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onStop}
|
||||
onClick={handleStop}
|
||||
size="sm"
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
@@ -1804,7 +1804,6 @@ export default function ChatInput({
|
||||
}
|
||||
workingDir={sessionWorkingDir ?? getInitialWorkingDir()}
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { ActionRequired } from '../api';
|
||||
import { IntlTestWrapper } from '../i18n/test-utils';
|
||||
import ElicitationRequest from './ElicitationRequest';
|
||||
|
||||
const actionRequiredContent = {
|
||||
type: 'actionRequired',
|
||||
data: {
|
||||
actionType: 'elicitation',
|
||||
id: 'elicitation-1',
|
||||
message: 'Need more information',
|
||||
requested_schema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
} as ActionRequired & { type: 'actionRequired' };
|
||||
|
||||
type SubmitElicitationResponse = (
|
||||
elicitationId: string,
|
||||
userData: Record<string, unknown>
|
||||
) => Promise<boolean>;
|
||||
|
||||
function renderElicitationRequest(onSubmit: SubmitElicitationResponse) {
|
||||
return render(
|
||||
<ElicitationRequest
|
||||
isCancelledMessage={false}
|
||||
isClicked={false}
|
||||
actionRequiredContent={actionRequiredContent}
|
||||
onSubmit={onSubmit}
|
||||
/>,
|
||||
{ wrapper: IntlTestWrapper }
|
||||
);
|
||||
}
|
||||
|
||||
describe('ElicitationRequest', () => {
|
||||
it('shows submitted state when the response is accepted', async () => {
|
||||
const onSubmit = vi.fn<SubmitElicitationResponse>().mockResolvedValue(true);
|
||||
|
||||
renderElicitationRequest(onSubmit);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Accept' }));
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith('elicitation-1', {});
|
||||
expect(await screen.findByText('Information submitted')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows submitted state while the response is pending', async () => {
|
||||
let resolveSubmission: (value: boolean) => void = () => {};
|
||||
const submission = new Promise<boolean>((resolve) => {
|
||||
resolveSubmission = resolve;
|
||||
});
|
||||
const onSubmit = vi.fn<SubmitElicitationResponse>().mockReturnValue(submission);
|
||||
|
||||
renderElicitationRequest(onSubmit);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Accept' }));
|
||||
|
||||
expect(screen.getByText('Information submitted')).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
resolveSubmission(true);
|
||||
await submission;
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the request actionable when no ACP request is pending', async () => {
|
||||
const onSubmit = vi.fn<SubmitElicitationResponse>().mockResolvedValue(false);
|
||||
|
||||
renderElicitationRequest(onSubmit);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Accept' }));
|
||||
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent(
|
||||
'This request is no longer active. The extension will need to ask again.'
|
||||
);
|
||||
expect(screen.queryByText('Information submitted')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Accept' })).toBeEnabled();
|
||||
});
|
||||
});
|
||||
@@ -34,6 +34,10 @@ const i18n = defineMessages({
|
||||
id: 'elicitationRequest.waitingForResponse',
|
||||
defaultMessage: 'Waiting for your response ({timeRemaining} remaining)',
|
||||
},
|
||||
submitError: {
|
||||
id: 'elicitationRequest.submitError',
|
||||
defaultMessage: 'This request is no longer active. The extension will need to ask again.',
|
||||
},
|
||||
});
|
||||
|
||||
const ELICITATION_TIMEOUT_SECONDS = 300;
|
||||
@@ -42,7 +46,7 @@ interface ElicitationRequestProps {
|
||||
isCancelledMessage: boolean;
|
||||
isClicked: boolean;
|
||||
actionRequiredContent: ActionRequired & { type: 'actionRequired' };
|
||||
onSubmit: (elicitationId: string, userData: Record<string, unknown>) => void;
|
||||
onSubmit: (elicitationId: string, userData: Record<string, unknown>) => Promise<boolean>;
|
||||
}
|
||||
|
||||
function formatTime(seconds: number): string {
|
||||
@@ -59,9 +63,18 @@ export default function ElicitationRequest({
|
||||
}: ElicitationRequestProps) {
|
||||
const intl = useIntl();
|
||||
const [submitted, setSubmitted] = useState(isClicked);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitError, setSubmitError] = useState<string | undefined>();
|
||||
const [timeRemaining, setTimeRemaining] = useState(ELICITATION_TIMEOUT_SECONDS);
|
||||
const startTimeRef = useRef(Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
if (isClicked) {
|
||||
setSubmitted(true);
|
||||
setSubmitError(undefined);
|
||||
}
|
||||
}, [isClicked]);
|
||||
|
||||
useEffect(() => {
|
||||
if (submitted || isCancelledMessage || isClicked) return;
|
||||
|
||||
@@ -87,14 +100,31 @@ export default function ElicitationRequest({
|
||||
const schema = (requested_schema ?? {}) as JsonSchema;
|
||||
const hasSchemaFields = Boolean(schema.properties && Object.keys(schema.properties).length > 0);
|
||||
|
||||
const handleSubmit = (formData: Record<string, unknown>) => {
|
||||
const submitResponse = async (formData: Record<string, unknown>) => {
|
||||
setSubmitted(true);
|
||||
onSubmit(elicitationId, formData);
|
||||
setIsSubmitting(true);
|
||||
setSubmitError(undefined);
|
||||
try {
|
||||
const didSubmit = await onSubmit(elicitationId, formData);
|
||||
if (!didSubmit) {
|
||||
setSubmitted(false);
|
||||
setSubmitError(intl.formatMessage(i18n.submitError));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error submitting elicitation response:', error);
|
||||
setSubmitted(false);
|
||||
setSubmitError(intl.formatMessage(i18n.submitError));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (formData: Record<string, unknown>) => {
|
||||
void submitResponse(formData);
|
||||
};
|
||||
|
||||
const handleAccept = () => {
|
||||
setSubmitted(true);
|
||||
onSubmit(elicitationId, {});
|
||||
void submitResponse({});
|
||||
};
|
||||
|
||||
if (isCancelledMessage) {
|
||||
@@ -165,14 +195,20 @@ export default function ElicitationRequest({
|
||||
schema={schema}
|
||||
onSubmit={handleSubmit}
|
||||
submitLabel={intl.formatMessage(i18n.submit)}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<Button type="button" onClick={handleAccept}>
|
||||
<Button type="button" onClick={handleAccept} disabled={isSubmitting}>
|
||||
{intl.formatMessage(i18n.accept)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{submitError && (
|
||||
<div role="alert" className="mt-3 text-sm text-red-500">
|
||||
{submitError}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`mt-3 pt-3 border-t border-border-primary flex items-center gap-2 text-sm ${isUrgent ? 'text-red-500' : 'text-text-secondary'}`}
|
||||
>
|
||||
|
||||
@@ -34,7 +34,7 @@ interface GooseMessageProps {
|
||||
submitElicitationResponse?: (
|
||||
elicitationId: string,
|
||||
userData: Record<string, unknown>
|
||||
) => Promise<void>;
|
||||
) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export default function GooseMessage({
|
||||
@@ -75,6 +75,13 @@ export default function GooseMessage({
|
||||
);
|
||||
const hasToolConfirmation = toolConfirmationContent !== undefined;
|
||||
const hasElicitation = elicitationContent !== undefined;
|
||||
const elicitationData =
|
||||
elicitationContent?.data.actionType === 'elicitation'
|
||||
? (elicitationContent.data as typeof elicitationContent.data & {
|
||||
isSubmitted?: boolean;
|
||||
isCancelled?: boolean;
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const toolConfirmationShownInline = useMemo(() => {
|
||||
if (!toolConfirmationContent) return false;
|
||||
@@ -203,8 +210,8 @@ export default function GooseMessage({
|
||||
|
||||
{hasElicitation && submitElicitationResponse && (
|
||||
<ElicitationRequest
|
||||
isCancelledMessage={false}
|
||||
isClicked={false}
|
||||
isCancelledMessage={elicitationData?.isCancelled === true}
|
||||
isClicked={elicitationData?.isSubmitted === true}
|
||||
actionRequiredContent={elicitationContent}
|
||||
onSubmit={submitElicitationResponse}
|
||||
/>
|
||||
|
||||
@@ -65,7 +65,7 @@ interface ProgressiveMessageListProps {
|
||||
submitElicitationResponse?: (
|
||||
elicitationId: string,
|
||||
userData: Record<string, unknown>
|
||||
) => Promise<void>;
|
||||
) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export default function ProgressiveMessageList({
|
||||
|
||||
@@ -45,6 +45,9 @@ import {
|
||||
acpRenameSession,
|
||||
type SessionListItem,
|
||||
} from '../../acp/sessions';
|
||||
import { acpChatSessionStore } from '../../acp/chatSessionStore';
|
||||
import { cancelAcpPermissionRequestsForSession } from '../../acp/permissionRequests';
|
||||
import { cancelAcpElicitationRequestsForSession } from '../../acp/elicitationRequests';
|
||||
import { getSearchShortcutText } from '../../utils/keyboardShortcuts';
|
||||
import { clearSessionCache } from '../../hooks/useChatStream';
|
||||
|
||||
@@ -482,7 +485,7 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
||||
const handleDuplicateSession = useCallback(
|
||||
async (session: SessionListItem) => {
|
||||
try {
|
||||
await acpForkSession(session.id, session.workingDir);
|
||||
await acpForkSession(session.id);
|
||||
toast.success(intl.formatMessage(i18n.duplicateSuccess, { name: session.name }));
|
||||
window.dispatchEvent(new CustomEvent(AppEvents.SESSION_CREATED));
|
||||
await loadSessions();
|
||||
@@ -509,6 +512,9 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
||||
new CustomEvent(AppEvents.SESSION_DELETED, { detail: { sessionId: sessionToDeleteId } })
|
||||
);
|
||||
clearSessionCache(sessionToDeleteId);
|
||||
cancelAcpPermissionRequestsForSession(sessionToDeleteId);
|
||||
cancelAcpElicitationRequestsForSession(sessionToDeleteId);
|
||||
acpChatSessionStore.deleteSnapshot(sessionToDeleteId);
|
||||
} catch (error) {
|
||||
console.error('Error deleting session:', error);
|
||||
toast.error(intl.formatMessage(i18n.deleteFailed, { name: sessionName, error: errorMessage(error, 'Unknown error') }));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,12 +17,13 @@ export interface UseChatSessionResult {
|
||||
submitElicitationResponse: (
|
||||
elicitationId: string,
|
||||
userData: Record<string, unknown>
|
||||
) => Promise<void>;
|
||||
) => Promise<boolean>;
|
||||
setRecipeUserParams: (values: Record<string, string>) => Promise<void>;
|
||||
stopStreaming: () => void;
|
||||
sessionLoadError?: string;
|
||||
tokenState: TokenState;
|
||||
notifications: Map<string, NotificationEvent[]>;
|
||||
pauseQueueOnStop: boolean;
|
||||
onMessageUpdate: (
|
||||
messageId: string,
|
||||
newContent: string,
|
||||
|
||||
@@ -929,7 +929,7 @@ export function useChatStream({
|
||||
const currentState = stateRef.current;
|
||||
|
||||
if (!currentState.session || currentState.chatState === ChatState.LoadingConversation) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
// An elicitation response unblocks an in-flight tool call on the original
|
||||
@@ -947,8 +947,10 @@ export function useChatStream({
|
||||
},
|
||||
throwOnError: true,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
onFinish('Submit error: ' + errorMessage(error));
|
||||
return true;
|
||||
}
|
||||
},
|
||||
[sessionId, onFinish]
|
||||
@@ -1141,6 +1143,7 @@ export function useChatStream({
|
||||
setRecipeUserParams,
|
||||
tokenState: state.tokenState,
|
||||
notifications: notificationsMap,
|
||||
pauseQueueOnStop: false,
|
||||
onMessageUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -992,6 +992,9 @@
|
||||
"elicitationRequest.submit": {
|
||||
"defaultMessage": "Submit"
|
||||
},
|
||||
"elicitationRequest.submitError": {
|
||||
"defaultMessage": "This request is no longer active. The extension will need to ask again."
|
||||
},
|
||||
"elicitationRequest.submitted": {
|
||||
"defaultMessage": "Information submitted"
|
||||
},
|
||||
|
||||
@@ -992,6 +992,9 @@
|
||||
"elicitationRequest.submit": {
|
||||
"defaultMessage": "Enviar"
|
||||
},
|
||||
"elicitationRequest.submitError": {
|
||||
"defaultMessage": "This request is no longer active. The extension will need to ask again."
|
||||
},
|
||||
"elicitationRequest.submitted": {
|
||||
"defaultMessage": "Información enviada"
|
||||
},
|
||||
|
||||
@@ -992,6 +992,9 @@
|
||||
"elicitationRequest.submit": {
|
||||
"defaultMessage": "सबमिट करें"
|
||||
},
|
||||
"elicitationRequest.submitError": {
|
||||
"defaultMessage": "This request is no longer active. The extension will need to ask again."
|
||||
},
|
||||
"elicitationRequest.submitted": {
|
||||
"defaultMessage": "जानकारी प्रस्तुत की गई"
|
||||
},
|
||||
|
||||
@@ -992,6 +992,9 @@
|
||||
"elicitationRequest.submit": {
|
||||
"defaultMessage": "送信"
|
||||
},
|
||||
"elicitationRequest.submitError": {
|
||||
"defaultMessage": "This request is no longer active. The extension will need to ask again."
|
||||
},
|
||||
"elicitationRequest.submitted": {
|
||||
"defaultMessage": "情報を送信しました"
|
||||
},
|
||||
|
||||
@@ -992,6 +992,9 @@
|
||||
"elicitationRequest.submit": {
|
||||
"defaultMessage": "Отправить"
|
||||
},
|
||||
"elicitationRequest.submitError": {
|
||||
"defaultMessage": "This request is no longer active. The extension will need to ask again."
|
||||
},
|
||||
"elicitationRequest.submitted": {
|
||||
"defaultMessage": "Информация отправлена"
|
||||
},
|
||||
|
||||
@@ -992,6 +992,9 @@
|
||||
"elicitationRequest.submit": {
|
||||
"defaultMessage": "Gönder"
|
||||
},
|
||||
"elicitationRequest.submitError": {
|
||||
"defaultMessage": "This request is no longer active. The extension will need to ask again."
|
||||
},
|
||||
"elicitationRequest.submitted": {
|
||||
"defaultMessage": "Bilgiler gönderildi"
|
||||
},
|
||||
|
||||
@@ -992,6 +992,9 @@
|
||||
"elicitationRequest.submit": {
|
||||
"defaultMessage": "提交"
|
||||
},
|
||||
"elicitationRequest.submitError": {
|
||||
"defaultMessage": "This request is no longer active. The extension will need to ask again."
|
||||
},
|
||||
"elicitationRequest.submitted": {
|
||||
"defaultMessage": "信息已提交"
|
||||
},
|
||||
|
||||
@@ -101,6 +101,7 @@ import type {
|
||||
SetSessionSystemPromptRequest_unstable,
|
||||
SteerSessionRequest_unstable,
|
||||
SteerSessionResponse_unstable,
|
||||
TruncateSessionConversationRequest_unstable,
|
||||
UnarchiveSessionRequest_unstable,
|
||||
UpdateSessionProjectRequest_unstable,
|
||||
UpdateSourceRequest_unstable,
|
||||
@@ -575,6 +576,15 @@ export class GooseExtClient {
|
||||
) as GetSessionInfoResponse_unstable;
|
||||
}
|
||||
|
||||
async sessionConversationTruncate_unstable(
|
||||
params: TruncateSessionConversationRequest_unstable,
|
||||
): Promise<void> {
|
||||
await this.conn.extMethod(
|
||||
"_goose/unstable/session/conversation/truncate",
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
async sessionProjectUpdate_unstable(
|
||||
params: UpdateSessionProjectRequest_unstable,
|
||||
): Promise<void> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
export type { AddConfigExtensionRequest_unstable, AddExtensionRequest_unstable, Annotations, ArchiveSessionRequest_unstable, AudioContent, BlobResourceContents, ContentBlock, CreateSourceRequest_unstable, CreateSourceResponse_unstable, CustomProviderConfigDto, CustomProviderCreateRequest_unstable, CustomProviderCreateResponse_unstable, CustomProviderDeleteRequest_unstable, CustomProviderDeleteResponse_unstable, CustomProviderReadRequest_unstable, CustomProviderReadResponse_unstable, CustomProviderUpdateRequest_unstable, CustomProviderUpdateResponse_unstable, DefaultsReadRequest_unstable, DefaultsReadResponse_unstable, DefaultsSaveRequest_unstable, DeleteSessionRequest, DeleteSourceRequest_unstable, DictationConfigRequest_unstable, DictationConfigResponse_unstable, DictationDownloadProgress, DictationLocalModelStatus, DictationModelCancelRequest_unstable, DictationModelDeleteRequest_unstable, DictationModelDownloadProgressRequest_unstable, DictationModelDownloadProgressResponse_unstable, DictationModelDownloadRequest_unstable, DictationModelOption, DictationModelSelectRequest_unstable, DictationModelsListRequest_unstable, DictationModelsListResponse_unstable, DictationProviderStatusEntry, DictationSecretDeleteRequest_unstable, DictationSecretSaveRequest_unstable, DictationTranscribeRequest_unstable, DictationTranscribeResponse_unstable, EmbeddedResource, EmbeddedResourceResource, EmptyResponse, EnvVariable, ExportSessionRequest_unstable, ExportSessionResponse_unstable, ExportSourceRequest_unstable, ExportSourceResponse_unstable, ExtNotification, ExtRequest, ExtResponse, GetAvailableExtensionsRequest_unstable, GetAvailableExtensionsResponse_unstable, GetConfigExtensionsRequest_unstable, GetConfigExtensionsResponse_unstable, GetSessionExtensionsRequest_unstable, GetSessionExtensionsResponse_unstable, GetSessionInfoRequest_unstable, GetSessionInfoResponse_unstable, GetToolsRequest_unstable, GetToolsResponse_unstable, GooseExtension, GooseExtensionEntry, GooseSessionNotification_unstable, GooseSessionUpdate, GooseToolCallRequest_unstable, GooseToolCallResponse_unstable, HttpHeader, ImageContent, ImportSessionRequest_unstable, ImportSessionResponse_unstable, ImportSourcesRequest_unstable, ImportSourcesResponse_unstable, ListProvidersRequest_unstable, ListProvidersResponse_unstable, ListSourcesRequest_unstable, ListSourcesResponse_unstable, McpServer, McpServerHttp, McpServerSse, McpServerStdio, OnboardingImportApplyRequest_unstable, OnboardingImportApplyResponse_unstable, OnboardingImportCandidate, OnboardingImportCounts, OnboardingImportScanRequest_unstable, OnboardingImportScanResponse_unstable, OnboardingImportSourceKind, PreferenceKey, PreferencesReadRequest_unstable, PreferencesReadResponse_unstable, PreferencesRemoveRequest_unstable, PreferencesSaveRequest_unstable, PreferenceValue, ProviderCatalogListRequest_unstable, ProviderCatalogListResponse_unstable, ProviderCatalogTemplateRequest_unstable, ProviderCatalogTemplateResponse_unstable, ProviderConfigAuthenticateRequest_unstable, ProviderConfigChangeResponse_unstable, ProviderConfigDeleteRequest_unstable, ProviderConfigFieldUpdate, ProviderConfigFieldValueDto, ProviderConfigKey, ProviderConfigReadRequest_unstable, ProviderConfigReadResponse_unstable, ProviderConfigSaveRequest_unstable, ProviderConfigStatusDto, ProviderConfigStatusRequest_unstable, ProviderConfigStatusResponse_unstable, ProviderInventoryEntryDto, ProviderInventoryModelDto, ProviderSetupCatalogEntryDto, ProviderSetupCatalogListRequest_unstable, ProviderSetupCatalogListResponse_unstable, ProviderSetupCategoryDto, ProviderSetupFieldDto, ProviderSetupGroupDto, ProviderSetupMethodDto, ProviderSupportedModelsListRequest_unstable, ProviderSupportedModelsListResponse_unstable, ProviderTemplateCapabilitiesDto, ProviderTemplateCatalogEntryDto, ProviderTemplateDto, ProviderTemplateModelDto, ReadResourceRequest_unstable, ReadResourceResponse_unstable, RefreshProviderInventoryRequest_unstable, RefreshProviderInventoryResponse_unstable, RefreshProviderInventorySkipDto, RefreshProviderInventorySkipReasonDto, RemoveConfigExtensionRequest_unstable, RemoveExtensionRequest_unstable, RenameSessionRequest_unstable, ResourceLink, Role, SessionId, SessionInfo, SessionSystemPromptMode, SessionUsageUpdate, SetConfigExtensionEnabledRequest_unstable, SetSessionSystemPromptRequest_unstable, SourceEntry, SourceScope, SourceType, StatusMessage, StatusMessageUpdate, SteerSessionRequest_unstable, SteerSessionResponse_unstable, TextContent, TextResourceContents, UnarchiveSessionRequest_unstable, UpdateSessionProjectRequest_unstable, UpdateSourceRequest_unstable, UpdateSourceResponse_unstable, UpdateWorkingDirRequest_unstable } from './types.gen.js';
|
||||
export type { AddConfigExtensionRequest_unstable, AddExtensionRequest_unstable, Annotations, ArchiveSessionRequest_unstable, AudioContent, BlobResourceContents, ContentBlock, CreateSourceRequest_unstable, CreateSourceResponse_unstable, CustomProviderConfigDto, CustomProviderCreateRequest_unstable, CustomProviderCreateResponse_unstable, CustomProviderDeleteRequest_unstable, CustomProviderDeleteResponse_unstable, CustomProviderReadRequest_unstable, CustomProviderReadResponse_unstable, CustomProviderUpdateRequest_unstable, CustomProviderUpdateResponse_unstable, DefaultsReadRequest_unstable, DefaultsReadResponse_unstable, DefaultsSaveRequest_unstable, DeleteSessionRequest, DeleteSourceRequest_unstable, DictationConfigRequest_unstable, DictationConfigResponse_unstable, DictationDownloadProgress, DictationLocalModelStatus, DictationModelCancelRequest_unstable, DictationModelDeleteRequest_unstable, DictationModelDownloadProgressRequest_unstable, DictationModelDownloadProgressResponse_unstable, DictationModelDownloadRequest_unstable, DictationModelOption, DictationModelSelectRequest_unstable, DictationModelsListRequest_unstable, DictationModelsListResponse_unstable, DictationProviderStatusEntry, DictationSecretDeleteRequest_unstable, DictationSecretSaveRequest_unstable, DictationTranscribeRequest_unstable, DictationTranscribeResponse_unstable, EmbeddedResource, EmbeddedResourceResource, EmptyResponse, EnvVariable, ExportSessionRequest_unstable, ExportSessionResponse_unstable, ExportSourceRequest_unstable, ExportSourceResponse_unstable, ExtNotification, ExtRequest, ExtResponse, GetAvailableExtensionsRequest_unstable, GetAvailableExtensionsResponse_unstable, GetConfigExtensionsRequest_unstable, GetConfigExtensionsResponse_unstable, GetSessionExtensionsRequest_unstable, GetSessionExtensionsResponse_unstable, GetSessionInfoRequest_unstable, GetSessionInfoResponse_unstable, GetToolsRequest_unstable, GetToolsResponse_unstable, GooseExtension, GooseExtensionEntry, GooseSessionNotification_unstable, GooseSessionUpdate, GooseToolCallRequest_unstable, GooseToolCallResponse_unstable, HttpHeader, ImageContent, ImportSessionRequest_unstable, ImportSessionResponse_unstable, ImportSourcesRequest_unstable, ImportSourcesResponse_unstable, ListProvidersRequest_unstable, ListProvidersResponse_unstable, ListSourcesRequest_unstable, ListSourcesResponse_unstable, McpServer, McpServerHttp, McpServerSse, McpServerStdio, OnboardingImportApplyRequest_unstable, OnboardingImportApplyResponse_unstable, OnboardingImportCandidate, OnboardingImportCounts, OnboardingImportScanRequest_unstable, OnboardingImportScanResponse_unstable, OnboardingImportSourceKind, PreferenceKey, PreferencesReadRequest_unstable, PreferencesReadResponse_unstable, PreferencesRemoveRequest_unstable, PreferencesSaveRequest_unstable, PreferenceValue, ProviderCatalogListRequest_unstable, ProviderCatalogListResponse_unstable, ProviderCatalogTemplateRequest_unstable, ProviderCatalogTemplateResponse_unstable, ProviderConfigAuthenticateRequest_unstable, ProviderConfigChangeResponse_unstable, ProviderConfigDeleteRequest_unstable, ProviderConfigFieldUpdate, ProviderConfigFieldValueDto, ProviderConfigKey, ProviderConfigReadRequest_unstable, ProviderConfigReadResponse_unstable, ProviderConfigSaveRequest_unstable, ProviderConfigStatusDto, ProviderConfigStatusRequest_unstable, ProviderConfigStatusResponse_unstable, ProviderInventoryEntryDto, ProviderInventoryModelDto, ProviderSetupCatalogEntryDto, ProviderSetupCatalogListRequest_unstable, ProviderSetupCatalogListResponse_unstable, ProviderSetupCategoryDto, ProviderSetupFieldDto, ProviderSetupGroupDto, ProviderSetupMethodDto, ProviderSupportedModelsListRequest_unstable, ProviderSupportedModelsListResponse_unstable, ProviderTemplateCapabilitiesDto, ProviderTemplateCatalogEntryDto, ProviderTemplateDto, ProviderTemplateModelDto, ReadResourceRequest_unstable, ReadResourceResponse_unstable, RefreshProviderInventoryRequest_unstable, RefreshProviderInventoryResponse_unstable, RefreshProviderInventorySkipDto, RefreshProviderInventorySkipReasonDto, RemoveConfigExtensionRequest_unstable, RemoveExtensionRequest_unstable, RenameSessionRequest_unstable, ResourceLink, Role, SessionId, SessionInfo, SessionSystemPromptMode, SessionUsageUpdate, SetConfigExtensionEnabledRequest_unstable, SetSessionSystemPromptRequest_unstable, SourceEntry, SourceScope, SourceType, StatusMessage, StatusMessageUpdate, SteerSessionRequest_unstable, SteerSessionResponse_unstable, TextContent, TextResourceContents, TruncateSessionConversationRequest_unstable, UnarchiveSessionRequest_unstable, UpdateSessionProjectRequest_unstable, UpdateSourceRequest_unstable, UpdateSourceResponse_unstable, UpdateWorkingDirRequest_unstable } from './types.gen.js';
|
||||
|
||||
export const GOOSE_EXT_METHODS = [
|
||||
{
|
||||
@@ -203,6 +203,11 @@ export const GOOSE_EXT_METHODS = [
|
||||
requestType: "GetSessionInfoRequest_unstable",
|
||||
responseType: "GetSessionInfoResponse_unstable",
|
||||
},
|
||||
{
|
||||
method: "_goose/unstable/session/conversation/truncate",
|
||||
requestType: "TruncateSessionConversationRequest_unstable",
|
||||
responseType: "EmptyResponse",
|
||||
},
|
||||
{
|
||||
method: "_goose/unstable/session/project/update",
|
||||
requestType: "UpdateSessionProjectRequest_unstable",
|
||||
|
||||
@@ -1197,6 +1197,14 @@ export type SessionInfo = {
|
||||
*/
|
||||
export type SessionId = string;
|
||||
|
||||
/**
|
||||
* Truncate a session conversation from the given message timestamp onward.
|
||||
*/
|
||||
export type TruncateSessionConversationRequest_unstable = {
|
||||
sessionId: string;
|
||||
truncateFrom: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the project association for a session.
|
||||
*/
|
||||
@@ -1594,7 +1602,7 @@ export type StatusMessageUpdate = {
|
||||
export type ExtRequest = {
|
||||
id: string;
|
||||
method: string;
|
||||
params?: AddExtensionRequest_unstable | RemoveExtensionRequest_unstable | GetToolsRequest_unstable | GooseToolCallRequest_unstable | ReadResourceRequest_unstable | UpdateWorkingDirRequest_unstable | SetSessionSystemPromptRequest_unstable | SteerSessionRequest_unstable | DeleteSessionRequest | GetConfigExtensionsRequest_unstable | GetAvailableExtensionsRequest_unstable | AddConfigExtensionRequest_unstable | RemoveConfigExtensionRequest_unstable | SetConfigExtensionEnabledRequest_unstable | GetSessionExtensionsRequest_unstable | ListProvidersRequest_unstable | ProviderSupportedModelsListRequest_unstable | ProviderCatalogListRequest_unstable | ProviderSetupCatalogListRequest_unstable | ProviderCatalogTemplateRequest_unstable | CustomProviderCreateRequest_unstable | CustomProviderReadRequest_unstable | CustomProviderUpdateRequest_unstable | CustomProviderDeleteRequest_unstable | RefreshProviderInventoryRequest_unstable | ProviderConfigReadRequest_unstable | ProviderConfigStatusRequest_unstable | ProviderConfigSaveRequest_unstable | ProviderConfigDeleteRequest_unstable | ProviderConfigAuthenticateRequest_unstable | PreferencesReadRequest_unstable | PreferencesSaveRequest_unstable | PreferencesRemoveRequest_unstable | DefaultsReadRequest_unstable | DefaultsSaveRequest_unstable | OnboardingImportScanRequest_unstable | OnboardingImportApplyRequest_unstable | ExportSessionRequest_unstable | ImportSessionRequest_unstable | GetSessionInfoRequest_unstable | UpdateSessionProjectRequest_unstable | RenameSessionRequest_unstable | ArchiveSessionRequest_unstable | UnarchiveSessionRequest_unstable | CreateSourceRequest_unstable | ListSourcesRequest_unstable | UpdateSourceRequest_unstable | DeleteSourceRequest_unstable | ExportSourceRequest_unstable | ImportSourcesRequest_unstable | DictationTranscribeRequest_unstable | DictationConfigRequest_unstable | DictationSecretSaveRequest_unstable | DictationSecretDeleteRequest_unstable | DictationModelsListRequest_unstable | DictationModelDownloadRequest_unstable | DictationModelDownloadProgressRequest_unstable | DictationModelCancelRequest_unstable | DictationModelDeleteRequest_unstable | DictationModelSelectRequest_unstable | {
|
||||
params?: AddExtensionRequest_unstable | RemoveExtensionRequest_unstable | GetToolsRequest_unstable | GooseToolCallRequest_unstable | ReadResourceRequest_unstable | UpdateWorkingDirRequest_unstable | SetSessionSystemPromptRequest_unstable | SteerSessionRequest_unstable | DeleteSessionRequest | GetConfigExtensionsRequest_unstable | GetAvailableExtensionsRequest_unstable | AddConfigExtensionRequest_unstable | RemoveConfigExtensionRequest_unstable | SetConfigExtensionEnabledRequest_unstable | GetSessionExtensionsRequest_unstable | ListProvidersRequest_unstable | ProviderSupportedModelsListRequest_unstable | ProviderCatalogListRequest_unstable | ProviderSetupCatalogListRequest_unstable | ProviderCatalogTemplateRequest_unstable | CustomProviderCreateRequest_unstable | CustomProviderReadRequest_unstable | CustomProviderUpdateRequest_unstable | CustomProviderDeleteRequest_unstable | RefreshProviderInventoryRequest_unstable | ProviderConfigReadRequest_unstable | ProviderConfigStatusRequest_unstable | ProviderConfigSaveRequest_unstable | ProviderConfigDeleteRequest_unstable | ProviderConfigAuthenticateRequest_unstable | PreferencesReadRequest_unstable | PreferencesSaveRequest_unstable | PreferencesRemoveRequest_unstable | DefaultsReadRequest_unstable | DefaultsSaveRequest_unstable | OnboardingImportScanRequest_unstable | OnboardingImportApplyRequest_unstable | ExportSessionRequest_unstable | ImportSessionRequest_unstable | GetSessionInfoRequest_unstable | TruncateSessionConversationRequest_unstable | UpdateSessionProjectRequest_unstable | RenameSessionRequest_unstable | ArchiveSessionRequest_unstable | UnarchiveSessionRequest_unstable | CreateSourceRequest_unstable | ListSourcesRequest_unstable | UpdateSourceRequest_unstable | DeleteSourceRequest_unstable | ExportSourceRequest_unstable | ImportSourcesRequest_unstable | DictationTranscribeRequest_unstable | DictationConfigRequest_unstable | DictationSecretSaveRequest_unstable | DictationSecretDeleteRequest_unstable | DictationModelsListRequest_unstable | DictationModelDownloadRequest_unstable | DictationModelDownloadProgressRequest_unstable | DictationModelCancelRequest_unstable | DictationModelDeleteRequest_unstable | DictationModelSelectRequest_unstable | {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
};
|
||||
|
||||
@@ -1173,6 +1173,14 @@ export const zGetSessionInfoResponse_unstable = z.object({
|
||||
session: zSessionInfo
|
||||
});
|
||||
|
||||
/**
|
||||
* Truncate a session conversation from the given message timestamp onward.
|
||||
*/
|
||||
export const zTruncateSessionConversationRequest_unstable = z.object({
|
||||
sessionId: z.string(),
|
||||
truncateFrom: z.number().int()
|
||||
});
|
||||
|
||||
/**
|
||||
* Update the project association for a session.
|
||||
*/
|
||||
@@ -1605,6 +1613,7 @@ export const zExtRequest = z.object({
|
||||
zExportSessionRequest_unstable,
|
||||
zImportSessionRequest_unstable,
|
||||
zGetSessionInfoRequest_unstable,
|
||||
zTruncateSessionConversationRequest_unstable,
|
||||
zUpdateSessionProjectRequest_unstable,
|
||||
zRenameSessionRequest_unstable,
|
||||
zArchiveSessionRequest_unstable,
|
||||
|
||||
Reference in New Issue
Block a user