Skip to content

Commit 167a0e8

Browse files
sandy081Copilot
andauthored
agentHost: Preserve workspace transition boundaries (#334534)
* Skip workspace trust prompts in Allow All mode Reuse the effective auto-approval configuration when converting workspace-less sessions so Allow All and global auto-approve bypass trust prompts consistently, including isolated worktree creation. Preserve trust checks for default, assisted, and autopilot modes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Inline workspace trust bypass checks Keep the workspace-conversion helper minimal while preserving the SessionPermissionManager public methods and their existing effective configuration behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Localize workspace trust bypass checks Keep the one-off effective approval decision in workspace conversion and restore session permissions to its prior shape. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: Preserve workspace transition boundaries Keep workspace transitions visible across collapsed progress and Agent Host restarts while hiding the internal continuation request. Persist transition state atomically and serialize database mutations so rollback cannot absorb unrelated writes.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: Address workspace transition review feedback Serialize transition restoration behind earlier turn mapping writes, and preserve access to truncated transition labels with the standard hover service. Avoid the global class-substring selector while retaining compact icon styling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b6fa70e commit 167a0e8

27 files changed

Lines changed: 1908 additions & 229 deletions

src/vs/platform/agentHost/common/agentHostChatContributionsService.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ export type IncomingRequestDisposition =
108108
export interface IHydrationContext {
109109
readonly session: ProtocolURI;
110110
readonly chat: ProtocolURI;
111+
/** Authoritative value from already-loaded host session metadata, when available. */
112+
readonly hasWorkspaceTransitions?: boolean;
111113
}
112114

113115
/**

src/vs/platform/agentHost/common/meta/agentSystemNotificationMeta.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
export const enum AgentSystemNotificationKind {
77
WorktreeCreationFailure = 'worktreeCreationFailure',
8+
/** The session successfully changed to a requested workspace. */
9+
WorkspaceTransition = 'workspaceTransition',
810
/** An automatic approval review did not finish before its deadline. */
911
AutomaticApprovalReviewTimedOut = 'automaticApprovalReviewTimedOut',
1012
/** An automatic approval review stopped before reaching a decision. */
@@ -21,12 +23,18 @@ export const enum AgentSystemNotificationKind {
2123
AgentMergePullRequestMerged = 'agentMergePullRequestMerged',
2224
}
2325

26+
export const enum AgentSystemNotificationWorkspaceKind {
27+
Folder = 'folder',
28+
Worktree = 'worktree',
29+
}
30+
2431
export const enum AgentSystemNotificationSeverity {
2532
Warning = 'warning',
2633
}
2734

2835
const knownKinds: ReadonlySet<string> = new Set<string>([
2936
AgentSystemNotificationKind.WorktreeCreationFailure,
37+
AgentSystemNotificationKind.WorkspaceTransition,
3038
AgentSystemNotificationKind.AutomaticApprovalReviewTimedOut,
3139
AgentSystemNotificationKind.AutomaticApprovalReviewAborted,
3240
AgentSystemNotificationKind.AutomaticApprovalReviewInterrupted,
@@ -43,6 +51,14 @@ interface IHasSystemNotificationMeta {
4351
export interface IAgentSystemNotificationMeta {
4452
readonly kind?: AgentSystemNotificationKind;
4553
readonly severity?: AgentSystemNotificationSeverity;
54+
readonly workspaceKind?: AgentSystemNotificationWorkspaceKind;
55+
readonly workspaceName?: string;
56+
}
57+
58+
export interface IAgentWorkspaceTransitionRecord {
59+
readonly content: string;
60+
readonly workspaceKind: AgentSystemNotificationWorkspaceKind;
61+
readonly workspaceName: string;
4662
}
4763

4864
/** Reads recognized Agent Host system-notification metadata. */
@@ -52,13 +68,46 @@ export function readAgentSystemNotificationMeta(source: IHasSystemNotificationMe
5268
return {};
5369
}
5470
const kind = meta['kind'];
71+
const workspaceKind = meta['workspaceKind'];
5572
return {
5673
kind: typeof kind === 'string' && knownKinds.has(kind) ? kind as AgentSystemNotificationKind : undefined,
5774
severity: meta['severity'] === AgentSystemNotificationSeverity.Warning ? meta['severity'] : undefined,
75+
workspaceKind: workspaceKind === AgentSystemNotificationWorkspaceKind.Folder || workspaceKind === AgentSystemNotificationWorkspaceKind.Worktree ? workspaceKind : undefined,
76+
workspaceName: typeof meta['workspaceName'] === 'string' ? meta['workspaceName'] : undefined,
5877
};
5978
}
6079

6180
/** Serializes Agent Host system-notification metadata for the open protocol bag. */
6281
export function toAgentSystemNotificationMeta(meta: IAgentSystemNotificationMeta): Record<string, unknown> {
6382
return { ...meta };
6483
}
84+
85+
/** Serializes a durable workspace-transition boundary. */
86+
export function serializeAgentWorkspaceTransition(record: IAgentWorkspaceTransitionRecord): string {
87+
return JSON.stringify(record);
88+
}
89+
90+
/** Parses and validates a durable workspace-transition boundary. */
91+
export function parseAgentWorkspaceTransition(value: string): IAgentWorkspaceTransitionRecord | undefined {
92+
let parsed: unknown;
93+
try {
94+
parsed = JSON.parse(value);
95+
} catch {
96+
return undefined;
97+
}
98+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
99+
return undefined;
100+
}
101+
const candidate = parsed as Partial<IAgentWorkspaceTransitionRecord>;
102+
if (typeof candidate.content !== 'string'
103+
|| typeof candidate.workspaceName !== 'string'
104+
|| (candidate.workspaceKind !== AgentSystemNotificationWorkspaceKind.Folder && candidate.workspaceKind !== AgentSystemNotificationWorkspaceKind.Worktree)
105+
) {
106+
return undefined;
107+
}
108+
return {
109+
content: candidate.content,
110+
workspaceKind: candidate.workspaceKind,
111+
workspaceName: candidate.workspaceName,
112+
};
113+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
const AGENT_WORKSPACE_CONTINUATION_META_KEY = 'vscode.chat.workspaceContinuation';
7+
8+
interface IHasAgentWorkspaceContinuationMeta {
9+
readonly _meta?: Record<string, unknown>;
10+
}
11+
12+
/** Whether the message is the internal request that resumes a turn after workspace conversion. */
13+
export function isAgentWorkspaceContinuationMessage(source: IHasAgentWorkspaceContinuationMeta): boolean {
14+
// eslint-disable-next-line local/code-no-untyped-meta-access -- sanctioned first hop into the namespaced workspace-continuation slot; validated here.
15+
return source._meta?.[AGENT_WORKSPACE_CONTINUATION_META_KEY] === true;
16+
}
17+
18+
/** Serializes the workspace-continuation marker for the open protocol bag. */
19+
export function toAgentWorkspaceContinuationMessageMeta(): Record<string, unknown> {
20+
return { [AGENT_WORKSPACE_CONTINUATION_META_KEY]: true };
21+
}

src/vs/platform/agentHost/common/sessionDataService.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,27 @@ export interface ISessionDatabase extends IDisposable {
180180
*/
181181
getTurnDelegations(): Promise<Map<string, string>>;
182182

183+
/**
184+
* Persists the JSON-serialized successful workspace transition for a turn.
185+
* Idempotent — last writer wins per turn.
186+
*/
187+
setTurnWorkspaceTransition(turnId: string, transition: string): Promise<void>;
188+
189+
/**
190+
* Atomically persists converted session metadata and the workspace
191+
* transition associated with its deferred continuation turn.
192+
*/
193+
setWorkspaceConversion(turnId: string, transition: string, metadata: Readonly<Record<string, string>>): Promise<void>;
194+
195+
/** Deletes a persisted workspace transition without deleting its owning turn. */
196+
deleteTurnWorkspaceTransition(turnId: string): Promise<void>;
197+
198+
/**
199+
* Returns every persisted workspace transition, keyed by both the turn's
200+
* own id and its provider event id when one has been recorded.
201+
*/
202+
getTurnWorkspaceTransitions(): Promise<Map<string, string>>;
203+
183204
/**
184205
* Associates a git checkpoint ref (e.g. `refs/agents/<sid>/checkpoints/turn/N`)
185206
* with a turn. Idempotent — last writer wins per turn.

src/vs/platform/agentHost/common/state/sessionState.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { decodeBase64, encodeBase64, VSBuffer } from '../../../../base/common/bu
1515
import { hasKey, type Mutable } from '../../../../base/common/types.js';
1616
import { URI as ResourceURI } from '../../../../base/common/uri.js';
1717
import type { IProductService } from '../../../product/common/productService.js';
18+
import { isAgentWorkspaceContinuationMessage } from '../meta/agentWorkspaceContinuationMeta.js';
1819
import { readToolCallMeta } from '../meta/agentToolCallMeta.js';
1920
import { readLegacyTurnError } from './legacyProtocolCompatibility.js';
2021
import {
@@ -357,10 +358,12 @@ export function withMessageSystemInitiatedLabel(message: Message, label: string)
357358
*
358359
* A *visible* system notification (a background-agent completion, an Agent
359360
* Merge repair prompt) is a real turn and is deliberately not matched.
361+
* A hidden workspace-continuation request is also a real provider turn.
360362
*/
361363
export function isHostNoticeTurn(turn: { readonly message: Message }): boolean {
362364
return turn.message.origin.kind === MessageKind.SystemNotification
363-
&& (isMessageHiddenFromTranscript(turn.message) || isMessageRequestHiddenFromTranscript(turn.message));
365+
&& (isMessageHiddenFromTranscript(turn.message) || isMessageRequestHiddenFromTranscript(turn.message))
366+
&& !isAgentWorkspaceContinuationMessage(turn.message);
364367
}
365368

366369
/** Returns the last turn id that can own file changes, or `undefined` if there is none. */
@@ -2012,6 +2015,12 @@ export const SESSION_META_WORKSPACELESS_KEY = 'workspaceless';
20122015
*/
20132016
export const AH_META_WORKSPACELESS_DB_KEY = 'agentHost.workspaceless';
20142017

2018+
/** Session-database marker indicating that retained turns include workspace-transition boundaries. */
2019+
export const AH_META_HAS_WORKSPACE_TRANSITIONS_DB_KEY = 'agentHost.hasWorkspaceTransitions';
2020+
2021+
/** Summary metadata mirror of {@link AH_META_HAS_WORKSPACE_TRANSITIONS_DB_KEY}. */
2022+
export const SESSION_META_HAS_WORKSPACE_TRANSITIONS_KEY = 'hasWorkspaceTransitions';
2023+
20152024
/** Blocks turns for a session whose provider could not be detached from an untrusted working directory. */
20162025
export const AH_META_WORKSPACE_CONVERSION_QUARANTINED_DB_KEY = 'agentHost.workspaceConversionQuarantined';
20172026

@@ -2073,6 +2082,22 @@ export function withSessionWorkspaceless(meta: SessionSummaryMeta | undefined, w
20732082
return Object.keys(next).length > 0 ? next : undefined;
20742083
}
20752084

2085+
/** Whether retained turns in this session include host-owned workspace transitions. */
2086+
export function readSessionHasWorkspaceTransitions(meta: SessionSummaryMeta | undefined): boolean {
2087+
return meta?.[SESSION_META_HAS_WORKSPACE_TRANSITIONS_KEY] === true;
2088+
}
2089+
2090+
/** Returns summary metadata with the workspace-transition history marker updated. */
2091+
export function withSessionHasWorkspaceTransitions(meta: SessionSummaryMeta | undefined, hasTransitions: boolean): SessionSummaryMeta | undefined {
2092+
const next: { [key: string]: unknown } = { ...meta };
2093+
if (hasTransitions) {
2094+
next[SESSION_META_HAS_WORKSPACE_TRANSITIONS_KEY] = true;
2095+
} else {
2096+
delete next[SESSION_META_HAS_WORKSPACE_TRANSITIONS_KEY];
2097+
}
2098+
return Object.keys(next).length > 0 ? next : undefined;
2099+
}
2100+
20762101
/** Whether the session was first discovered in a provider-native catalog. */
20772102
export function readSessionExternal(meta: SessionSummaryMeta | undefined): boolean {
20782103
return meta?.[SESSION_META_EXTERNAL_KEY] === true;

0 commit comments

Comments
 (0)