Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
const promise = this._sendRequest('createSession', {
channel: session.toString(),
provider,
workingDirectory: config?.workingDirectory ? fromAgentHostUri(config.workingDirectory).toString() : undefined,
workingDirectories: config?.workingDirectory ? [fromAgentHostUri(config.workingDirectory).toString()] : undefined,
config: config?.config,
activeClient: config?.activeClient,
}).then(() => session);
Expand Down Expand Up @@ -955,7 +955,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
summary: s.title,
status: s.status,
activity: s.activity,
workingDirectory: typeof s.workingDirectory === 'string' ? toAgentHostUri(URI.parse(s.workingDirectory), this._connectionAuthority) : undefined,
workingDirectory: typeof s.workingDirectories?.[0] === 'string' ? toAgentHostUri(URI.parse(s.workingDirectories?.[0]), this._connectionAuthority) : undefined,
isRead: !!(s.status & SessionStatus.IsRead),
isArchived: !!(s.status & SessionStatus.IsArchived),
changes: s.changes,
Expand Down
28 changes: 16 additions & 12 deletions src/vs/platform/agentHost/common/state/agentSubscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,19 +313,21 @@ export class SessionStateSubscription extends BaseAgentSubscription<SessionState
}

protected override _reconcile(envelope: ActionEnvelope, isOwnAction: boolean): void {
// A rejected envelope must never mutate confirmed state — it only rolls
// back the originating client's matching optimistic action. Guarding all
// apply branches also prevents a broadcast rejection from leaking the
// rejected action into a non-origin client's state.
if (isOwnAction && envelope.origin) {
const idx = this._pendingActions.findIndex(p => p.clientSeq === envelope.origin!.clientSeq);
if (idx !== -1) {
if (envelope.rejectionReason) {
this._pendingActions.splice(idx, 1);
} else {
if (!envelope.rejectionReason) {
this._confirmedApply(envelope.action);
this._pendingActions.splice(idx, 1);
}
} else {
this._pendingActions.splice(idx, 1);
} else if (!envelope.rejectionReason) {
this._confirmedApply(envelope.action);
}
} else {
} else if (!envelope.rejectionReason) {
this._confirmedApply(envelope.action);
}
this._recomputeOptimistic();
Expand Down Expand Up @@ -457,19 +459,21 @@ export class ChatStateSubscription extends BaseAgentSubscription<ChatState> {
}

protected override _reconcile(envelope: ActionEnvelope, isOwnAction: boolean): void {
// A rejected envelope must never mutate confirmed state — it only rolls
// back the originating client's matching optimistic action. Guarding all
// apply branches also prevents a broadcast rejection from leaking the
// rejected action into a non-origin client's state.
if (isOwnAction && envelope.origin) {
const idx = this._pendingActions.findIndex(p => p.clientSeq === envelope.origin!.clientSeq);
if (idx !== -1) {
if (envelope.rejectionReason) {
this._pendingActions.splice(idx, 1);
} else {
if (!envelope.rejectionReason) {
this._confirmedApply(envelope.action);
this._pendingActions.splice(idx, 1);
}
} else {
this._pendingActions.splice(idx, 1);
} else if (!envelope.rejectionReason) {
this._confirmedApply(envelope.action);
}
} else {
} else if (!envelope.rejectionReason) {
this._promotePendingTurnStartIfTerminal(envelope.action);
this._confirmedApply(envelope.action);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4e554d0e
ea279d99
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
// Generated from types/actions.ts — do not edit
// Run `npm run generate` to regenerate.

import { ActionType, type StateAction, type RootAgentsChangedAction, type RootActiveSessionsChangedAction, type RootTerminalsChangedAction, type RootConfigChangedAction, type SessionReadyAction, type SessionCreationFailedAction, type SessionChatAddedAction, type SessionChatRemovedAction, type SessionChatUpdatedAction, type SessionDefaultChatChangedAction, type SessionTitleChangedAction, type SessionServerToolsChangedAction, type SessionActiveClientSetAction, type SessionActiveClientRemovedAction, type SessionInputNeededSetAction, type SessionInputNeededRemovedAction, type SessionCustomizationsChangedAction, type SessionCustomizationToggledAction, type SessionCustomizationUpdatedAction, type SessionCustomizationRemovedAction, type SessionMcpServerStateChangedAction, type SessionMcpServerStartRequestedAction, type SessionMcpServerStopRequestedAction, type SessionIsReadChangedAction, type SessionIsArchivedChangedAction, type SessionActivityChangedAction, type SessionChangesetsChangedAction, type SessionConfigChangedAction, type SessionMetaChangedAction, type ChatTurnStartedAction, type ChatDeltaAction, type ChatResponsePartAction, type ChatToolCallStartAction, type ChatToolCallDeltaAction, type ChatToolCallReadyAction, type ChatToolCallConfirmedAction, type ChatToolCallCompleteAction, type ChatToolCallResultConfirmedAction, type ChatToolCallContentChangedAction, type ChatToolCallAuthRequiredAction, type ChatToolCallAuthResolvedAction, type ChatTurnCompleteAction, type ChatTurnCancelledAction, type ChatErrorAction, type ChatActivityChangedAction, type ChatUsageAction, type ChatReasoningAction, type ChatPendingMessageSetAction, type ChatPendingMessageRemovedAction, type ChatQueuedMessagesReorderedAction, type ChatDraftChangedAction, type ChatInputRequestedAction, type ChatInputAnswerChangedAction, type ChatInputCompletedAction, type ChatTruncatedAction, type ChatTurnsLoadedAction, type ChangesetStatusChangedAction, type ChangesetFileSetAction, type ChangesetFileRemovedAction, type ChangesetFilesReviewChangedAction, type ChangesetContentChangedAction, type ChangesetOperationsChangedAction, type ChangesetOperationStatusChangedAction, type ChangesetClearedAction, type AnnotationsSetAction, type AnnotationsUpdatedAction, type AnnotationsRemovedAction, type AnnotationsEntrySetAction, type AnnotationsEntryRemovedAction, type TerminalDataAction, type TerminalInputAction, type TerminalResizedAction, type TerminalClaimedAction, type TerminalTitleChangedAction, type TerminalCwdChangedAction, type TerminalExitedAction, type TerminalClearedAction, type TerminalCommandDetectionAvailableAction, type TerminalCommandExecutedAction, type TerminalCommandFinishedAction, type ResourceWatchChangedAction } from './actions.js';
import { ActionType, type StateAction, type RootAgentsChangedAction, type RootActiveSessionsChangedAction, type RootTerminalsChangedAction, type RootConfigChangedAction, type SessionReadyAction, type SessionCreationFailedAction, type SessionChatAddedAction, type SessionChatRemovedAction, type SessionChatUpdatedAction, type SessionDefaultChatChangedAction, type SessionTitleChangedAction, type SessionServerToolsChangedAction, type SessionActiveClientSetAction, type SessionActiveClientRemovedAction, type SessionWorkingDirectorySetAction, type SessionWorkingDirectoryRemovedAction, type SessionInputNeededSetAction, type SessionInputNeededRemovedAction, type SessionCustomizationsChangedAction, type SessionCustomizationToggledAction, type SessionCustomizationUpdatedAction, type SessionCustomizationRemovedAction, type SessionMcpServerStateChangedAction, type SessionMcpServerStartRequestedAction, type SessionMcpServerStopRequestedAction, type SessionIsReadChangedAction, type SessionIsArchivedChangedAction, type SessionActivityChangedAction, type SessionChangesetsChangedAction, type SessionConfigChangedAction, type SessionMetaChangedAction, type ChatTurnStartedAction, type ChatDeltaAction, type ChatResponsePartAction, type ChatToolCallStartAction, type ChatToolCallDeltaAction, type ChatToolCallReadyAction, type ChatToolCallConfirmedAction, type ChatToolCallCompleteAction, type ChatToolCallResultConfirmedAction, type ChatToolCallContentChangedAction, type ChatToolCallAuthRequiredAction, type ChatToolCallAuthResolvedAction, type ChatTurnCompleteAction, type ChatTurnCancelledAction, type ChatErrorAction, type ChatActivityChangedAction, type ChatWorkingDirectorySetAction, type ChatWorkingDirectoryRemovedAction, type ChatUsageAction, type ChatReasoningAction, type ChatPendingMessageSetAction, type ChatPendingMessageRemovedAction, type ChatQueuedMessagesReorderedAction, type ChatDraftChangedAction, type ChatInputRequestedAction, type ChatInputAnswerChangedAction, type ChatInputCompletedAction, type ChatTruncatedAction, type ChatTurnsLoadedAction, type ChangesetStatusChangedAction, type ChangesetFileSetAction, type ChangesetFileRemovedAction, type ChangesetFilesReviewChangedAction, type ChangesetContentChangedAction, type ChangesetOperationsChangedAction, type ChangesetOperationStatusChangedAction, type ChangesetClearedAction, type AnnotationsSetAction, type AnnotationsUpdatedAction, type AnnotationsRemovedAction, type AnnotationsEntrySetAction, type AnnotationsEntryRemovedAction, type TerminalDataAction, type TerminalInputAction, type TerminalResizedAction, type TerminalClaimedAction, type TerminalTitleChangedAction, type TerminalCwdChangedAction, type TerminalExitedAction, type TerminalClearedAction, type TerminalCommandDetectionAvailableAction, type TerminalCommandExecutedAction, type TerminalCommandFinishedAction, type ResourceWatchChangedAction } from './actions.js';


// ─── Root vs Session vs Chat vs Terminal vs Changeset Action Unions ─────────────────
Expand Down Expand Up @@ -46,6 +46,8 @@ export type SessionAction =
| SessionServerToolsChangedAction
| SessionActiveClientSetAction
| SessionActiveClientRemovedAction
| SessionWorkingDirectorySetAction
| SessionWorkingDirectoryRemovedAction
| SessionInputNeededSetAction
| SessionInputNeededRemovedAction
| SessionCustomizationsChangedAction
Expand All @@ -68,6 +70,8 @@ export type ClientSessionAction =
| SessionTitleChangedAction
| SessionActiveClientSetAction
| SessionActiveClientRemovedAction
| SessionWorkingDirectorySetAction
| SessionWorkingDirectoryRemovedAction
| SessionCustomizationToggledAction
| SessionMcpServerStartRequestedAction
| SessionMcpServerStopRequestedAction
Expand Down Expand Up @@ -114,6 +118,8 @@ export type ChatAction =
| ChatTurnCancelledAction
| ChatErrorAction
| ChatActivityChangedAction
| ChatWorkingDirectorySetAction
| ChatWorkingDirectoryRemovedAction
| ChatUsageAction
| ChatReasoningAction
| ChatPendingMessageSetAction
Expand All @@ -135,6 +141,8 @@ export type ClientChatAction =
| ChatToolCallResultConfirmedAction
| ChatToolCallContentChangedAction
| ChatTurnCancelledAction
| ChatWorkingDirectorySetAction
| ChatWorkingDirectoryRemovedAction
| ChatPendingMessageSetAction
| ChatPendingMessageRemovedAction
| ChatQueuedMessagesReorderedAction
Expand Down Expand Up @@ -283,6 +291,8 @@ export const IS_CLIENT_DISPATCHABLE: { readonly [K in StateAction['type']]: bool
[ActionType.SessionServerToolsChanged]: false,
[ActionType.SessionActiveClientSet]: true,
[ActionType.SessionActiveClientRemoved]: true,
[ActionType.SessionWorkingDirectorySet]: true,
[ActionType.SessionWorkingDirectoryRemoved]: true,
Comment thread
sandy081 marked this conversation as resolved.
[ActionType.SessionInputNeededSet]: false,
[ActionType.SessionInputNeededRemoved]: false,
[ActionType.SessionCustomizationsChanged]: false,
Expand Down Expand Up @@ -314,6 +324,8 @@ export const IS_CLIENT_DISPATCHABLE: { readonly [K in StateAction['type']]: bool
[ActionType.ChatTurnCancelled]: true,
[ActionType.ChatError]: false,
[ActionType.ChatActivityChanged]: false,
[ActionType.ChatWorkingDirectorySet]: true,
[ActionType.ChatWorkingDirectoryRemoved]: true,
Comment thread
sandy081 marked this conversation as resolved.
[ActionType.ChatUsage]: false,
[ActionType.ChatReasoning]: false,
[ActionType.ChatPendingMessageSet]: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts

import { ActionType } from '../common/actions.js';
import type { StringOrMarkdown, ErrorInfo, FileEdit, UsageInfo } from '../common/state.js';
import type { StringOrMarkdown, ErrorInfo, FileEdit, UsageInfo, URI } from '../common/state.js';
import type { McpAuthRequirement } from '../channels-session/state.js';
import { ToolCallConfirmationReason, ToolCallCancellationReason, PendingMessageKind, type Message, type ResponsePart, type ToolCallResult, type ToolResultContent, type ChatInputAnswer, type ChatInputRequest, type ChatInputResponseKind, type ConfirmationOption, type ToolCallContributor, type ToolCallRiskAssessment, type Turn } from './state.js';

Expand Down Expand Up @@ -488,6 +488,45 @@ export interface ChatActivityChangedAction {
activity?: string;
}

/**
* A working directory was added to this chat's
* {@link ChatState.workingDirectories} subset.
*
* Membership semantics keyed by the directory URI: the reducer appends
* `directory` when the chat's subset does not already contain it (creating the
* subset if absent) and is a no-op when it is already present. `directory` MUST
* be one of the owning session's {@link SessionState.workingDirectories}; a host
* MUST reject a directory that is not. Only valid when the agent advertises
* {@link AgentCapabilities.multipleWorkingDirectories}.
*
* @category Chat Actions
* @version 1
* @clientDispatchable
*/
export interface ChatWorkingDirectorySetAction {
type: ActionType.ChatWorkingDirectorySet;
/** The working directory to add to this chat's subset. */
directory: URI;
}

/**
* A working directory was removed from this chat's
* {@link ChatState.workingDirectories} subset.
*
* Removes `directory` from the chat's subset; a no-op when it is not present.
* Idempotent, mirroring `session/workingDirectoryRemoved`. Only affects the
* chat's subset — the directory remains in the session's set.
*
* @category Chat Actions
* @version 1
* @clientDispatchable
*/
export interface ChatWorkingDirectoryRemovedAction {
type: ActionType.ChatWorkingDirectoryRemoved;
/** The working directory to remove from this chat's subset. */
directory: URI;
}

/**
* Token usage report for a turn.
*
Expand Down Expand Up @@ -751,6 +790,8 @@ export type ChatAction =
| ChatTurnCancelledAction
| ChatErrorAction
| ChatActivityChangedAction
| ChatWorkingDirectorySetAction
| ChatWorkingDirectoryRemovedAction
| ChatUsageAction
| ChatReasoningAction
| ChatTruncatedAction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,29 @@ export interface CreateChatParams extends BaseParams {
initialMessage?: Message;
/** Optional source chat and turn to fork from. */
source?: ChatForkSource;
/**
* Initial working-directory subset for this chat. Every entry MUST be
* present in the owning session's `workingDirectories`; the server MUST
* reject any entry that is not. When absent, the chat inherits the full
* session set. Forked chats (`source`) inherit the source chat's
* `workingDirectories`; this field is ignored for forked chats.
*
* A client MUST NOT supply this field unless the agent advertises
* {@link AgentCapabilities.multipleWorkingDirectories}.
*/
workingDirectories?: URI[];
/**
* The chat's primary working directory — the distinguished root this chat is
* centered on. When set, it MUST be one of the chat's effective working
* directories ({@link workingDirectories}, or the session's set when that is
* omitted). A client SHOULD supply this when the agent advertises
* {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY
* reject creation that omits it, or fall back to the first of the chat's
* directories. Fixed at creation and reported (read-only) on
* {@link ChatState.primaryWorkingDirectory}. Ignored for forked chats (a fork
* inherits the source chat's primary).
*/
primaryWorkingDirectory?: URI;
}

// ─── disposeChat ─────────────────────────────────────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,30 @@ export function chatReducer(state: ChatState, action: ChatAction, log?: (msg: st
case ActionType.ChatActivityChanged:
return { ...state, activity: action.activity };

// ── Working Directories ───────────────────────────────────────────────

case ActionType.ChatWorkingDirectorySet: {
const list = state.workingDirectories ?? [];
if (list.includes(action.directory)) {
return state;
}
return { ...state, workingDirectories: [...list, action.directory] };
}

case ActionType.ChatWorkingDirectoryRemoved: {
const list = state.workingDirectories;
if (!list) {
return state;
}
const idx = list.indexOf(action.directory);
if (idx < 0) {
return state;
}
const updated = list.slice();
updated.splice(idx, 1);
return { ...state, workingDirectories: updated };
}

// ── Tool Call State Machine ───────────────────────────────────────────

case ActionType.ChatToolCallStart:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,34 @@ export interface ChatState {
*/
interactivity?: ChatInteractivity;
/**
* Optional per-chat working directory.
* The subset of the session's
* {@link SessionState.workingDirectories | `workingDirectories`} that this
* chat's agent has tool access to. Every entry MUST be present in the owning
* session's `workingDirectories`; servers MUST reject a
* `chat/workingDirectorySet` action that violates this constraint.
*
* If absent, the chat inherits
* {@link SessionState.workingDirectory | the session's working directory}.
* Hosts MAY override this for individual chats — for example, to give a
* subordinate chat its own git worktree so multiple chats in a session can
* make independent edits that the orchestrator later merges back.
* When absent, the chat inherits the full session set. When present but empty
* (not recommended), the chat has no working-directory tool access at all.
*
* Dispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to
* update the subset on a running chat.
*/
workingDirectories?: URI[];
/**
* The chat's primary working directory — the distinguished root this chat is
* centered on (e.g. the agent's process root for this chat, the default
* location for relative paths). MUST be one of this chat's effective working
* directories ({@link workingDirectories}, or the session's set when that is
* absent). Present when the agent advertises
* {@link MultipleWorkingDirectoriesCapability.requiresPrimary}.
*
* **Read-only and fixed at creation.** It is set from
* {@link CreateChatParams.primaryWorkingDirectory} (or, for the session's
* default chat, {@link CreateSessionParams.primaryWorkingDirectory}) and does
* not change over the chat's lifetime — there is no action to mutate it, and
* it does not participate in `session/chatUpdated`.
*/
workingDirectory?: URI;
primaryWorkingDirectory?: URI;

// ── Conversation contents ──────────────────────────────────────────
/** Completed turns */
Expand Down Expand Up @@ -127,13 +146,15 @@ export interface ChatSummary {
*/
interactivity?: ChatInteractivity;
/**
* Optional per-chat working directory.
*
* If absent, the chat inherits
* {@link SessionSummary.workingDirectory | the session's working directory}.
* See {@link ChatState.workingDirectory} for usage notes.
* The subset of the session's working directories this chat uses.
* See {@link ChatState.workingDirectories} for the full semantics.
*/
workingDirectories?: URI[];
/**
* The chat's primary working directory.
* See {@link ChatState.primaryWorkingDirectory} for the full semantics.
*/
workingDirectory?: URI;
primaryWorkingDirectory?: URI;
}

/**
Expand Down
Loading
Loading