mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-19 14:22:19 +01:00
* Adopt AHP 0.6.0 workingDirectories property in agent host Sync the generated agent host protocol copy to spec version 0.6.0 (agent-host-protocol @ 11f1a65e), which renames the singular `workingDirectory` on session/chat state to a `workingDirectories` array in preparation for multiroot session support. This change is a pure, behaviour-preserving property adoption across all consumers: sessions continue to use a single working directory. Reads of a single directory now use `workingDirectories?.[0]`, field-copy sites pass the array through unchanged, and writes from a single URI produce a one-element array. No multiroot client support is added here (no capability advertising, state actions, multi-folder workspaces, or UI); those land in a follow-up milestone. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review feedback - Reject the not-yet-supported multiroot working-directory client actions (session|chat/workingDirectorySet|Removed) in the handwritten dispatch path so a client cannot mutate the synchronized working-directory set without the agent actually reconfiguring its directory access. The protocol declarations remain; only the operational dispatch is deferred until multiroot lands. - Compare workingDirectories by array identity (not just the primary entry) in the state manager summary-equality check, matching the immutable reducers and SessionSummaryNotifier so secondary-directory changes still dirty the summary. - Update ISessionWithDefaultChat / mergeSessionWithDefaultChat API docs to describe a chat's workingDirectories subset overriding or inheriting the session's full set, fixing links to the removed singular members. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix agentHost session-filter test for workingDirectories migration The `sessionAdded notification filters out sessions outside the workspace` test constructed session summaries with the removed singular `workingDirectory` field, so the production filter (which now reads `workingDirectories[0]`) saw no directory and dropped the in-workspace session. Update the two directly-built summaries to the `workingDirectories` array form. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Migrate test createSession wire calls to workingDirectories The createSession dispatch handler now reads the working directory from the renamed `CreateSessionParams.workingDirectories` array (AHP 0.6.0), but several node integration / e2e test call sites still sent the removed singular `workingDirectory` field. Since the field is silently ignored, sessions were created without a working directory and fell back to the default chats folder, failing the Agent Host E2E workspace/fileOperations/hostFeatures suites across all providers (wrong cwd cascades into file completions, renames, worktree resolution, and cd-prefix stripping). Send `workingDirectories: [dir]` from the shared `createProviderSession` helper and the remaining direct wire call sites so the requested directory reaches the session state again. The real VS Code client already sent the array form. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Adopt AHP 0.7.0 primaryWorkingDirectory + requiresPrimary Re-sync the generated protocol copy to the multiroot spec at 148c716b (spec version 0.7.0) and adopt the two follow-up changes: - Capability flag renamed `MultipleWorkingDirectoriesCapability.immutablePrimary` -> `requiresPrimary`, with the new "agent needs one directory designated as its primary root" semantics. No consumers referenced the old name. - New optional `primaryWorkingDirectory` field at both the session level (CreateSessionParams / SessionMetadata -> SessionState + SessionSummary) and the chat level (CreateChatParams / ChatState + ChatSummary). Mirror it through the state<->summary projection layer exactly like `workingDirectories`: createSessionState / createChatState / chatSummaryFromState / mergeSessionWithDefaultChat, plus the state manager's summary projection, field-equality check, SessionSummaryNotifier diff, and markSessionPersisted propagation. The generated SessionChatUpdated partial-summary merge is field-agnostic, so it carries the new field automatically. Purely additive optional fields; no client multiroot behavior is added (consumers still read `workingDirectories[0]` as the single effective root). Also preserve the VS Code-local `CompletionItem.label` field (added in #326807, ahead of the spec) which the verbatim re-sync would otherwise drop. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Reject unsupported working-directory actions via reconciliation path Address PR review feedback on the multiroot working-directory action gate: - Instead of silently dropping the four not-yet-supported session|chat/workingDirectorySet|Removed client actions (which never echoed the origin, leaving the client's optimistic write-ahead action pending until reconnect), emit a rejection envelope through the normal reconciliation path. Added AgentHostStateManager.rejectClientAction, which emits an ActionEnvelope carrying the original ActionOrigin and a rejectionReason without running the reducer (no synchronized state change), so the originating client rolls back its optimistic action. - Guard the write-ahead client reconcile (SessionStateSubscription / ChatStateSubscription) so a rejected envelope is never applied to confirmed state in any branch — this also prevents a broadcast rejection from leaking the rejected action into a non-origin client's state. - Add a table-driven test covering all four action types asserting no dispatch and exactly one rejection envelope preserving the original origin. - Simplify the create-session working-directory read to `URI.parse(params.workingDirectories[0])` now the branch proves index 0 exists. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refine primaryWorkingDirectory to per-chat read-only (AHP ea279d99) Re-sync the protocol copy to spec ea279d99 (0.7.0) and adopt the refined primaryWorkingDirectory design: - Session has NO primary: `primaryWorkingDirectory` is removed from SessionState / SessionSummary. The session is just the equal-peer `workingDirectories` set. Dropped all session-level primary handling (createSessionState, the SessionSummaryNotifier diff, _toSummary, _summaryFieldsEqual, and markSessionPersisted propagation). - Primary is per-chat, read-only, fixed at chat creation: kept the ChatState <-> ChatSummary mirroring (createChatState / chatSummaryFromState) and carry the chat's own primary through the session+default-chat composite (ISessionWithDefaultChat gains its own primaryWorkingDirectory; the merge no longer falls back to a session primary). It is not sent via `session/chatUpdated` (the state manager only ever puts status/activity/title in those changes), so it never mutates post-creation. - Inputs `CreateSessionParams.primaryWorkingDirectory` (seeds the default chat's primary) and `CreateChatParams.primaryWorkingDirectory` are synced; capability `requiresPrimary` unchanged. Also re-preserve the VS Code-local `CompletionItem.label` field (#326807, ahead of the spec) that the verbatim re-sync drops. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
176 lines
6.3 KiB
TypeScript
176 lines
6.3 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { CancellationToken } from '../../../base/common/cancellation.js';
|
|
import { isCancellationError } from '../../../base/common/errors.js';
|
|
import { compareItemsByFuzzyScore, FuzzyScorerCache, IItemAccessor, prepareQuery, scoreItemFuzzy } from '../../../base/common/fuzzyScorer.js';
|
|
import { Schemas } from '../../../base/common/network.js';
|
|
import { basename, relativePath } from '../../../base/common/resources.js';
|
|
import { URI } from '../../../base/common/uri.js';
|
|
import { CompletionItem, CompletionItemKind, CompletionsParams } from '../common/state/protocol/commands.js';
|
|
import { MessageAttachmentKind } from '../common/state/protocol/state.js';
|
|
import { CompletionTriggerCharacter, IAgentHostCompletionItemProvider } from './agentHostCompletions.js';
|
|
import { AgentHostStateManager } from './agentHostStateManager.js';
|
|
import { AgentHostWorkspaceFiles } from './agentHostWorkspaceFiles.js';
|
|
|
|
/** Maximum number of completion items returned per call. */
|
|
const MAX_RESULTS = 50;
|
|
|
|
/**
|
|
* Result of {@link extractAtToken}.
|
|
*/
|
|
interface IAtToken {
|
|
readonly token: string;
|
|
readonly triggerChar: string;
|
|
readonly rangeStart: number;
|
|
readonly rangeEnd: number;
|
|
}
|
|
|
|
/**
|
|
* Walk back from `offset` to find the most recent `@` that is preceded by
|
|
* whitespace (or start-of-string) and not interrupted by whitespace. Returns
|
|
* the substring after `@` together with the range to replace, or `undefined`
|
|
* if no `@`-token is being typed at `offset`.
|
|
*
|
|
* Exported for unit testing.
|
|
*/
|
|
export function extractAtToken(text: string, offset: number): IAtToken | undefined {
|
|
if (offset < 0 || offset > text.length) {
|
|
return undefined;
|
|
}
|
|
for (let i = offset - 1; i >= 0; i--) {
|
|
const ch = text.charCodeAt(i);
|
|
// whitespace terminates the search
|
|
if (ch === 0x20 /* space */ || ch === 0x09 /* tab */ || ch === 0x0a /* \n */ || ch === 0x0d /* \r */) {
|
|
return undefined;
|
|
}
|
|
if (text[i] === CompletionTriggerCharacter.File || text[i] === CompletionTriggerCharacter.Hash) {
|
|
// The trigger character must be at start-of-input or preceded by whitespace.
|
|
if (i > 0) {
|
|
const prev = text.charCodeAt(i - 1);
|
|
const prevIsWs = prev === 0x20 || prev === 0x09 || prev === 0x0a || prev === 0x0d;
|
|
if (!prevIsWs) {
|
|
return undefined;
|
|
}
|
|
}
|
|
return { token: text.slice(i + 1, offset), triggerChar: text[i], rangeStart: i, rangeEnd: offset };
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Item-accessor that exposes a {@link URI} as basename / parent-directory /
|
|
* relative path for the {@link scoreItemFuzzy} family.
|
|
*/
|
|
class UriAccessor implements IItemAccessor<URI> {
|
|
constructor(private readonly _workingDirectory: URI) { }
|
|
|
|
getItemLabel(item: URI): string {
|
|
return basename(item);
|
|
}
|
|
|
|
getItemDescription(item: URI): string | undefined {
|
|
const rel = relativePath(this._workingDirectory, item);
|
|
if (!rel) {
|
|
return undefined;
|
|
}
|
|
const idx = rel.lastIndexOf('/');
|
|
return idx > 0 ? rel.slice(0, idx) : undefined;
|
|
}
|
|
|
|
getItemPath(item: URI): string | undefined {
|
|
const rel = relativePath(this._workingDirectory, item);
|
|
return rel ?? item.fsPath;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generic completion provider that contributes workspace file references
|
|
* for a {@link CompletionItemKind.UserMessage} input — typically used for
|
|
* `@`-mentions in the user message composer.
|
|
*
|
|
* When the user has typed an `@`-prefixed token at the cursor position,
|
|
* this provider enumerates files under the session's working directory
|
|
* (via {@link AgentHostWorkspaceFiles}, which uses ripgrep and respects
|
|
* `.gitignore`), ranks them with the same fuzzy scorer used by the
|
|
* VS Code Quick Open file picker, and returns up to {@link MAX_RESULTS}
|
|
* matches.
|
|
*/
|
|
export class AgentHostFileCompletionProvider implements IAgentHostCompletionItemProvider {
|
|
|
|
readonly kinds: ReadonlySet<CompletionItemKind> = new Set([CompletionItemKind.UserMessage]);
|
|
|
|
readonly triggerCharacters: readonly string[] = [CompletionTriggerCharacter.File, CompletionTriggerCharacter.Hash];
|
|
|
|
constructor(
|
|
private readonly _stateManager: AgentHostStateManager,
|
|
private readonly _workspaceFiles: AgentHostWorkspaceFiles,
|
|
) { }
|
|
|
|
async provideCompletionItems(params: CompletionsParams, token: CancellationToken): Promise<readonly CompletionItem[]> {
|
|
const workingDirectoryStr = this._stateManager.getSessionState(params.channel)?.workingDirectories?.[0];
|
|
if (!workingDirectoryStr) {
|
|
return [];
|
|
}
|
|
const workingDirectory = URI.parse(workingDirectoryStr);
|
|
if (workingDirectory.scheme !== Schemas.file) {
|
|
return [];
|
|
}
|
|
|
|
const at = extractAtToken(params.text, params.offset);
|
|
if (!at) {
|
|
return [];
|
|
}
|
|
|
|
let files: readonly URI[];
|
|
try {
|
|
files = await this._workspaceFiles.getFiles(workingDirectory, token);
|
|
} catch (err) {
|
|
// Cancellation is expected on every keystroke as Monaco cancels
|
|
// the previous request. Don't let it surface as a provider failure
|
|
// in {@link AgentHostCompletions} — it would log noisy errors on
|
|
// normal typing.
|
|
if (isCancellationError(err)) {
|
|
return [];
|
|
}
|
|
throw err;
|
|
}
|
|
if (token.isCancellationRequested || files.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const accessor = new UriAccessor(workingDirectory);
|
|
const query = prepareQuery(at.token);
|
|
const cache: FuzzyScorerCache = Object.create(null);
|
|
|
|
let candidates: URI[];
|
|
if (!query.normalized) {
|
|
// Empty token: return the first MAX_RESULTS files in enumeration order.
|
|
candidates = files.slice(0, MAX_RESULTS);
|
|
} else {
|
|
// Filter out non-matches first to avoid sorting tens of thousands of zeros.
|
|
const matching = files.filter(f => scoreItemFuzzy(f, query, true, accessor, cache).score > 0);
|
|
matching.sort((a, b) => compareItemsByFuzzyScore(a, b, query, true, accessor, cache));
|
|
candidates = matching.slice(0, MAX_RESULTS);
|
|
}
|
|
|
|
return candidates.map((uri): CompletionItem => {
|
|
const name = basename(uri);
|
|
return {
|
|
insertText: at.triggerChar + name,
|
|
rangeStart: at.rangeStart,
|
|
rangeEnd: at.rangeEnd,
|
|
attachment: {
|
|
type: MessageAttachmentKind.Resource,
|
|
uri: uri.toString(),
|
|
label: name,
|
|
displayKind: 'document',
|
|
},
|
|
};
|
|
});
|
|
}
|
|
}
|