Atlas › Test

agentService.test|title=AgentService (node dispatcher) authenticate reports not authenticated when every matching provider rejects|occurrence=1

Exact test identity: mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/agentService.test|title=AgentService (node dispatcher) authenticate reports not authenticated when every matching provider rejects|occurrence=1

Package
mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node
Suite / test hierarchy
agentService.test|title=AgentService (node dispatcher) authenticate reports not authenticated when every matching provider rejects|occurrence=1
Test
agentService.test|title=AgentService (node dispatcher) authenticate reports not authenticated when every matching provider rejects|occurrence=1
Introduced at
agentHostAuthenticationService.ts ×1 Frontier kind: Joint frontier
Covered ranges
5156
Covered lines
50754
Covered files
304

Covered source

Expand a file to inspect source; the > gutter marks covered lines.

src/vs/platform/agentHost/common/agentService.ts 2051 covered LOC · 16 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentService.ts
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 > import type { CancellationToken } from '../../../base/common/cancellation.js';
7 > import { Event } from '../../../base/common/event.js';
8 > import { IReference } from '../../../base/common/lifecycle.js';
9 > import { truncate } from '../../../base/common/strings.js';
10 > import { IAuthorizationProtectedResourceMetadata } from '../../../base/common/oauth.js';
11 > import type { IObservable } from '../../../base/common/observable.js';
12 > import { URI } from '../../../base/common/uri.js';
13 > import type { IConfigurationChangeEvent, IConfigurationService } from '../../configuration/common/configuration.js';
14 > import { createDecorator } from '../../instantiation/common/instantiation.js';
15 > import type { IAgentServerToolHost } from './agentServerTools.js';
16 > import type { IActiveSubscriptionInfo, IAgentSubscription } from './state/agentSubscription.js';
17 > import type { IRemoteWatchHandle } from './agentHostFileSystemProvider.js';
18 > import type { CompletionsParams, CompletionsResult, CreateTerminalParams, ResolveSessionConfigResult, SessionConfigCompletionsResult } from './state/protocol/commands.js';
19 > import type { InitializeResult } from './state/protocol/common/commands.js';
20 > import type { InvokeChangesetOperationParams, InvokeChangesetOperationResult } from './state/protocol/channels-changeset/commands.js';
21 > import { ProtectedResourceMetadata, type Changeset, type ConfigSchema, type MessageAttachment, type ModelSelection, type AgentSelection, type SessionActiveClient, type ToolCallPendingConfirmationState, type ToolDefinition, ChangesSummary } from './state/protocol/state.js';
22 > import type { ActionEnvelope, AuthRequiredParams, INotification, IRootConfigChangedAction, SessionAction, ChatAction, TerminalAction, ClientAnnotationsAction, ClientChangesetAction } from './state/sessionActions.js';
23 > import type { ResourceCopyParams, ResourceCopyResult, ResourceDeleteParams, ResourceDeleteResult, ResourceListResult, ResourceMkdirParams, ResourceMkdirResult, ResourceMoveParams, ResourceMoveResult, ResourceReadResult, ResourceResolveParams, ResourceResolveResult, ResourceWatchState, ResourceWriteParams, ResourceWriteResult, CreateResourceWatchParams, CreateResourceWatchResult, IStateSnapshot } from './state/sessionProtocol.js';
24 > import { ComponentToState, ChatInputResponseKind, SessionStatus, StateComponents, buildSubagentChatUri, parseRequiredSessionUriFromChatUri, type AgentCapabilities, type ClientPluginCustomization, type Customization, type PendingMessage, type RootState, type ChatInputAnswer, type SessionMeta, type ToolCallResult, type Turn, type PolicyState } from './state/sessionState.js';
25 >
26 > // IPC contract between the renderer and the agent host utility process.
27 > // Defines all serializable event types, the IAgent provider interface,
28 > // and the IAgentService / IAgentHostService service decorators.
29 >
30 > export const enum AgentHostIpcChannels {
31 > /** Channel for the agent host service on the main-process side */
32 > AgentHost = 'agentHost',
33 > /** Channel for log forwarding from the agent host process */
34 > Logger = 'agentHostLogger',
35 > /** Channel for WebSocket client connection count (server process management only) */
36 > ConnectionTracker = 'agentHostConnectionTracker',
37 > /** Channel carrying raw Agent Host Protocol frames over a MessagePort. */
38 > Protocol = 'agentHostProtocol',
39 > /** Narrow local management channel that remains outside of the AHP data plane. */
40 > Management = 'agentHostManagement',
41 > /**
42 > * Channel registered by the remote server that proxies AHP JSON-RPC
43 > * frames between a renderer and the agent host running on the server.
44 > * Pairs with `AgentHostIpcChannelTransport` on the renderer side.
45 > */
46 > RemoteProxy = 'agentHostProxy',
47 > }
48 >
49 > /** Configuration key that controls whether AHP JSONL logs are written for agent host transports. */
50 > export const AgentHostAhpJsonlLoggingSettingId = 'chat.agentHost.ahpJsonlLoggingEnabled';
51 >
52 > /** Configuration key controlling automatic OS system proxy discovery for agent-host Copilot sessions. */
53 > export const AgentHostSystemProxyEnabledSettingId = 'chat.agentHost.systemProxy.enabled';
54 >
55 > // The Copilot-CLI-specific setting IDs (`customTerminalTool`, `opus48Prompt`,
56 > // `reasoningEffortOverride`, `modelCapabilityOverrides`) live with their
57 > // root-config keys in `copilotCliConfig.ts`.
58 >
59 > /**
60 > * Configuration key controlling whether the Claude provider is registered in
61 > * the agent host process. When `false`, the agent host skips registering the
62 > * Claude provider regardless of SDK availability. Defaults to `true`.
63 > *
64 > * Independent of {@link ClaudePreferAgentHostAgentsSettingId} /
65 > * {@link ClaudePreferAgentHostEditorSettingId}, which control whether the
66 > * workbench surfaces the agent host's Claude provider (vs. the GitHub Copilot
67 > * Chat extension's). This setting is strictly about whether the agent host
68 > * advertises Claude at all. The agent host process must be restarted for
69 > * changes to take effect.
70 > */
71 > export const AgentHostClaudeAgentEnabledSettingId = 'chat.agentHost.claudeAgent.enabled';
72 >
73 > /**
74 > * Configuration key controlling whether the Codex provider is registered in
75 > * the agent host process. When `false` (the default), the agent host skips
76 > * registering the Codex provider regardless of SDK availability. The agent
77 > * host process must be restarted for changes to take effect.
78 > */
79 > export const AgentHostCodexAgentEnabledSettingId = 'chat.agentHost.codexAgent.enabled';
80 >
81 > /**
82 > * Configuration key controlling whether the agent host *wires up* the BYOK
83 > * ("bring your own key") language-model bridge: the renderer LM handler, the
84 > * reverse-RPC channel, and the per-connection link to the node-side OpenAI
85 > * proxy + bridge registry. When `true` (the default), the renderer's BYOK
86 > * server channel and the per-connection bridge are wired so extension-provided
87 > * BYOK models are reachable from agent-host sessions. When `false`, the proxy
88 > * and registry are still constructed but stay inert — the BYOK server channel
89 > * and the per-connection bridge are not wired, so the registry stays empty and
90 > * extension-provided BYOK models are never reachable from agent-host sessions.
91 > * The agent host process must be restarted for changes to take effect.
92 > */
93 > export const AgentHostByokModelsEnabledSettingId = 'chat.agentHost.byokModels.enabled';
94 >
95 > /**
96 > * Optional override that points at an **SDK root directory** containing a
97 > * `node_modules/@anthropic-ai/claude-agent-sdk` subtree. When set, the agent
98 > * host loads the Claude SDK from that path instead of the bare import (which
99 > * resolves via this repo's `node_modules` in dev) or the on-demand download
100 > * from `product.agentSdks.claude` (built products). Mainly exists for the
101 > * remote server's `--claude-sdk-root` CLI flag and for one-off developer
102 > * overrides pointing at an out-of-tree SDK build.
103 > */
104 > export const AgentHostClaudeSdkRootEnvVar = 'VSCODE_AGENT_HOST_CLAUDE_SDK_ROOT';
105 >
106 > /**
107 > * Environment variable form of {@link AgentHostClaudeAgentEnabledSettingId}.
108 > * Set by the agent host starters from the setting. Accepts `'true'` /
109 > * `'false'`; absent means "default" (`true` for Claude, `false` for Codex).
110 > */
111 > export const AgentHostClaudeAgentEnabledEnvVar = 'VSCODE_AGENT_HOST_CLAUDE_AGENT_ENABLED';
112 >
113 > /**
114 > * Environment variable form of {@link AgentHostCodexAgentEnabledSettingId}.
115 > * Set by the agent host starters from the setting. Accepts `'true'` /
116 > * `'false'`; absent means "default" (`false`).
117 > */
118 > export const AgentHostCodexAgentEnabledEnvVar = 'VSCODE_AGENT_HOST_CODEX_AGENT_ENABLED';
119 >
120 > /**
121 > * Environment variable form of {@link AgentHostByokModelsEnabledSettingId}.
122 > * Set by the agent host starters from the setting. Accepts `'true'` /
123 > * `'false'`; absent means "default" (`true`).
124 > */
125 > export const AgentHostByokModelsEnabledEnvVar = 'VSCODE_AGENT_HOST_BYOK_MODELS_ENABLED';
126 >
127 > /**
128 > * Overrides the grace period (in milliseconds) before an idle, fully
129 > * unsubscribed session is released from memory. Defaults to 30_000. Primarily a
130 > * test hook so real-SDK integration tests can force a prompt release without
131 > * waiting the full production grace; production does not set it.
132 > */
133 > export const AgentHostSessionReleaseGraceMsEnvVar = 'VSCODE_AGENT_HOST_SESSION_RELEASE_GRACE_MS';
134 >
135 > /**
136 > * Resolves the effective enable state for a Claude/Codex provider from the
137 > * env-var value forwarded by the starter. Recognized values (case- and
138 > * whitespace-insensitive):
139 > *
140 > * - `'true'` / `'1'` → enabled
141 > * - `'false'` / `'0'` → disabled
142 > * - `undefined`, empty string, or any other value → falls through to
143 > * {@link defaultEnabled}
144 > */
145 > export function isAgentEnabled(envValue: string | undefined, defaultEnabled: boolean): boolean {
146 if (envValue === undefined || envValue === '') {
147 return defaultEnabled;
156 return defaultEnabled;
157 }
159 > /**
160 > * Configuration key that controls the sandbox mode for the Copilot SDK's built-in
161 > * shell tool (the path taken when `AgentHostCustomTerminalToolEnabledSettingId`
162 > * is `false`). Values mirror {@link AgentSandboxEnabledValue}:
163 > *
164 > * - `'off'` (the default): no sandbox policy is forwarded for the SDK shell
165 > * path \u2014 commands run unsandboxed.
166 > * - `'on'`: the Agent Host runs the SDK\u2019s shell tool inside a sandbox
167 > * using the user's `chat.agent.sandbox.fileSystem.*` filesystem policy.
168 > * Outbound network is enforced via the user's allow/deny host lists.
169 > * - `'allowNetwork'`: same as `'on'` but with unrestricted outbound network.
170 > *
171 > * Has no effect when `AgentHostCustomTerminalToolEnabledSettingId` is
172 > * `true` \u2014 the host\u2019s own terminal sandbox engine then handles shell
173 > * commands and reads `chat.agent.sandbox.enabled` directly.
174 > */
175 > export const AgentHostSdkSandboxEnabledSettingId = 'chat.agentHost.sdkSandbox.enabled';
176 >
177 > /**
178 > * Selects which Claude integration fulfills Claude sessions opened from the
179 > * **Agents Window**:
180 > * - `true` — Claude is provided by the agent host process.
181 > * - `false` (default) — Claude is provided by the GitHub Copilot Chat extension.
182 > *
183 > * The agent host always registers Claude when its SDK is reachable; this
184 > * setting only controls whether the per-window bridge in
185 > * `AgentHostContribution` actually surfaces the AH provider in the Agents
186 > * Window. The extension's `chatSessions` contribution mirrors the rule
187 > * declaratively (its `when` clause hides the EH provider when this is `true`),
188 > * so flipping the setting takes effect live without a window reload.
189 > *
190 > * Paired with {@link ClaudePreferAgentHostEditorSettingId} which governs the
191 > * regular workbench (sidebar). EXP-backed (`experiment: { mode: 'startup' }`).
192 > */
193 > export const ClaudePreferAgentHostAgentsSettingId = 'chat.agents.claude.preferAgentHost';
194 >
195 > /**
196 > * Sibling of {@link ClaudePreferAgentHostAgentsSettingId} that selects the
197 > * Claude implementation for the **regular workbench** (sidebar chat in a
198 > * non-Agents-Window window). Same shape, same semantics — just a different
199 > * surface scope.
200 > */
201 > export const ClaudePreferAgentHostEditorSettingId = 'chat.editor.claude.preferAgentHost';
202 >
203 > /**
204 > * Selects whether the regular workbench surfaces Codex from the agent host
205 > * instead of the OpenAI extension.
206 > */
207 > export const CodexPreferAgentHostEditorSettingId = 'chat.editor.codex.preferAgentHost';
208 >
209 > export function claudePreferAgentHostSettingId(isSessionsWindow: boolean): string {
210 return isSessionsWindow
211 ? ClaudePreferAgentHostAgentsSettingId
212 : ClaudePreferAgentHostEditorSettingId;
213 }
215 > export function affectsAgentHostProviderPreference(event: IConfigurationChangeEvent, isSessionsWindow: boolean): boolean {
216 return event.affectsConfiguration(claudePreferAgentHostSettingId(isSessionsWindow))
217 || event.affectsConfiguration(isSessionsWindow ? AgentHostCodexAgentEnabledSettingId : CodexPreferAgentHostEditorSettingId);
218 }
220 > export function shouldSurfaceLocalAgentHostProvider(provider: AgentProvider, configurationService: IConfigurationService, isSessionsWindow: boolean): boolean {
221 switch (provider) {
222 case CLAUDE_AGENT_PROVIDER_ID:
228 }
229 }
231 > // -- Codex agent settings --------------------------------------------------------
232 > //
233 > // Codex is opt-in via `chat.agentHost.codexAgent.sdkRoot`. The setting points
234 > // at an absolute path to a directory containing a `node_modules/@openai/codex`
235 > // subtree (the same shape `npm install @openai/codex` produces, and the same
236 > // shape the agent host downloads on demand from `product.agentSdks.codex`).
237 > // The agent host spawns the native codex binary from inside that tree as a
238 > // long-lived child process and speaks JSON-RPC over stdio. The binary is not
239 > // bundled with VS Code; users either install codex themselves (typically via
240 > // `npm install -g @openai/codex` or a platform package manager) or rely on
241 > // the on-demand download.
242 >
243 > /**
244 > * Absolute path to the **SDK root directory** containing a
245 > * `node_modules/@openai/codex` subtree. When non-empty, the agent host treats
246 > * it as a dev override and skips the on-demand download from
247 > * `product.agentSdks.codex`. Empty (the default) falls through to product
248 > * config; if neither is present, the provider is not registered.
249 > */
250 > export const AgentHostCodexAgentSdkRootSettingId = 'chat.agentHost.codexAgent.sdkRoot';
251 >
252 > /**
253 > * Optional override for `$CODEX_HOME`. When set, the codex app-server child
254 > * process inherits this value, controlling where rollouts and config live.
255 > */
256 > export const AgentHostCodexAgentCodexHomeSettingId = 'chat.agentHost.codexAgent.codexHome';
257 >
258 > /**
259 > * Additional command-line arguments passed to `codex app-server`. Mainly for
260 > * debugging (e.g. `--log-level=debug`).
261 > */
262 > export const AgentHostCodexAgentBinaryArgsSettingId = 'chat.agentHost.codexAgent.binaryArgs';
263 >
264 > /**
265 > * Environment variable form of {@link AgentHostCodexAgentSdkRootSettingId}.
266 > * Forwarded by the starters from the setting.
267 > */
268 > export const AgentHostCodexAgentSdkRootEnvVar = 'VSCODE_AGENT_HOST_CODEX_SDK_ROOT';
269 >
270 > /** Forwarded `$CODEX_HOME`. */
271 > export const AgentHostCodexAgentCodexHomeEnvVar = 'CODEX_HOME';
272 >
273 > /** Forwarded extra args for `codex app-server` (JSON-encoded string[]). */
274 > export const AgentHostCodexAgentBinaryArgsEnvVar = 'VSCODE_AGENT_HOST_CODEX_APP_SERVER_ARGS';
275 >
276 > // -- OpenTelemetry settings ------------------------------------------------------
277 > //
278 > // The `chat.agentHost.otel.*` namespace surfaces the same exporter knobs the CLI
279 > // runtime documents in `extensions/copilot/docs/monitoring/agent_monitoring.md`,
280 > // but routes them through the agent host process so the user's settings stay in
281 > // VS Code instead of leaking via shell env.
282 > //
283 > // `chat.agentHost.otel.dbSpanExporter.enabled` switches on the in-process
284 > // loopback receiver + persistent SQLite span store; the other settings still
285 > // apply because the user's external sink (when configured) is then fed by an
286 > // outbound forwarder rather than by the SDK directly.
287 >
288 > /** Master toggle for agent-host OTel. Explicit opt-in; other settings imply this when set. */
289 > export const AgentHostOTelEnabledSettingId = 'chat.agentHost.otel.enabled';
290 > /** Exporter type for the SDK's OTel pipeline. One of: `otlp-http`, `otlp-grpc`, `console`, `file`. */
291 > export const AgentHostOTelExporterTypeSettingId = 'chat.agentHost.otel.exporterType';
292 > /**
293 > * OTLP wire protocol (`http/json`, `http/protobuf`, `grpc`). Policy-only delivery slot (no user UI):
294 > * carries the enterprise-managed `telemetry.protocol` so it can be threaded into the agent host's
295 > * `OTEL_EXPORTER_OTLP_PROTOCOL` env, which the runtime needs to distinguish protobuf from json
296 > * (the `exporterType` setting only models transport, not the HTTP wire encoding).
297 > */
298 > export const AgentHostOTelOtlpProtocolSettingId = 'chat.agentHost.otel.otlpProtocol';
299 > /** OTLP endpoint URL when `exporterType` is `otlp-http` or `otlp-grpc`. */
300 > export const AgentHostOTelOtlpEndpointSettingId = 'chat.agentHost.otel.otlpEndpoint';
301 > /** Whether to include prompt/response content in span attributes (privacy-sensitive). */
302 > export const AgentHostOTelCaptureContentSettingId = 'chat.agentHost.otel.captureContent';
303 > /** Output path when `exporterType` is `file`. */
304 > export const AgentHostOTelOutfileSettingId = 'chat.agentHost.otel.outfile';
305 > /** Policy-only delivery slot for the enterprise-managed OTel `service.name` (no user UI). */
306 > export const AgentHostOTelServiceNameSettingId = 'chat.agentHost.otel.serviceName';
307 > /** Policy-only delivery slot for enterprise-managed OTel resource attributes (no user UI). */
308 > export const AgentHostOTelResourceAttributesSettingId = 'chat.agentHost.otel.resourceAttributes';
309 > /** When true, ALL spans are persisted to a local SQLite store regardless of `exporterType`. */
310 > export const AgentHostOTelDbSpanExporterEnabledSettingId = 'chat.agentHost.otel.dbSpanExporter.enabled';
311 >
312 > /**
313 > * Path of the local SQLite span database, relative to `INativeEnvironmentService.userDataPath`.
314 > * Kept here so both the renderer-side export action and the agent-host-side service
315 > * use the same on-disk location.
316 > */
317 > export const AgentHostOTelSpansDbSubPath = 'agent-host/otel/agent-host-traces.db';
318 >
319 > /**
320 > * Environment variables consumed by `AgentHostOTelService` inside the agent host
321 > * process. The workbench-side agent-host starters translate the corresponding
322 > * `chat.agentHost.otel.*` settings into these variables (settings → env), while
323 > * any value already present on the parent process's env wins (developer override).
324 > *
325 > * These names match the conventions documented in
326 > * `extensions/copilot/docs/monitoring/agent_monitoring.md` so the same external
327 > * tooling and `OTEL_EXPORTER_OTLP_*` config recipes work unchanged.
328 > */
329 > export const AgentHostOTelEnvVars = Object.freeze({
330 > Enabled: 'COPILOT_OTEL_ENABLED',
331 > ExporterType: 'COPILOT_OTEL_EXPORTER_TYPE',
332 > OtlpEndpoint: 'OTEL_EXPORTER_OTLP_ENDPOINT',
333 > OtlpEndpointAlt: 'COPILOT_OTEL_ENDPOINT',
334 > OtlpProtocol: 'OTEL_EXPORTER_OTLP_PROTOCOL',
335 > OtlpTracesProtocol: 'OTEL_EXPORTER_OTLP_TRACES_PROTOCOL',
336 > OtlpMetricsProtocol: 'OTEL_EXPORTER_OTLP_METRICS_PROTOCOL',
337 > OtlpHeaders: 'OTEL_EXPORTER_OTLP_HEADERS',
338 > CaptureContent: 'OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT',
339 > FilePath: 'COPILOT_OTEL_FILE_EXPORTER_PATH',
340 > SourceName: 'COPILOT_OTEL_SOURCE_NAME',
341 > ServiceName: 'OTEL_SERVICE_NAME',
342 > ResourceAttributes: 'OTEL_RESOURCE_ATTRIBUTES',
343 > DbSpanExporterEnabled: 'COPILOT_OTEL_DB_SPAN_EXPORTER_ENABLED',
344 > } as const);
345 >
346 > /**
347 > * Snapshot of the `chat.agentHost.otel.*` settings; produced by the workbench-side
348 > * starters and merged with the parent process's env (env wins on key collision).
349 > */
350 > export interface IAgentHostOTelSettings {
351 > readonly enabled?: boolean;
352 > readonly exporterType?: string;
353 > readonly otlpProtocol?: string;
354 > readonly otlpEndpoint?: string;
355 > readonly captureContent?: boolean;
356 > readonly outfile?: string;
357 > readonly serviceName?: string;
358 > readonly resourceAttributes?: Record<string, string>;
359 > readonly dbSpanExporterEnabled?: boolean;
360 > }
361 >
362 > /**
363 > * IPC channel (renderer -> main) the desktop agent-host path uses to hand the
364 > * enterprise-resolved `chat.agentHost.otel.*` policy to `ElectronAgentHostStarter`.
365 > *
366 > * The main-process configuration service does NOT include the renderer-only
367 > * `AccountPolicyService` (managed settings: server / native-MDM / file channels), so a
368 > * starter running in the main process sees `policyValue === undefined` for these keys.
369 > * The renderer — whose policy layer does include managed settings — forwards the resolved
370 > * values here just before requesting the agent-host connection, so the host is spawned with
371 > * the managed OTel env. See {@link readAgentHostOTelPolicySettings}.
372 > */
373 > export const AgentHostOTelPolicyIpcChannel = 'vscode:agentHostOTelPolicy';
374 >
375 > /**
376 > * Resolve the enterprise-policy values for the `chat.agentHost.otel.*` settings from a
377 > * configuration service whose policy layer includes managed settings (i.e. the renderer's).
378 > * Each field is `undefined` when no policy is set. Intended as the `policySettings` argument
379 > * of {@link buildAgentHostOTelEnv}.
380 > */
381 > export function readAgentHostOTelPolicySettings(configurationService: IConfigurationService): IAgentHostOTelSettings {
382 const policyValue = <T>(key: string): T | undefined => configurationService.inspect<T>(key).policyValue;
383 return {
392 };
393 }
395 > /**
396 > * Validate/normalize an {@link IAgentHostOTelSettings} received over IPC, keeping only
397 > * well-typed fields. Defends the main process against a malformed payload before the values
398 > * are turned into agent-host process env vars.
399 > */
400 > export function sanitizeAgentHostOTelPolicySettings(raw: unknown): IAgentHostOTelSettings {
401 if (!raw || typeof raw !== 'object') {
402 return {};
431 };
432 }
434 > /**
435 > * Serialize an OTel resource-attribute map into the `OTEL_RESOURCE_ATTRIBUTES` env-var format
436 > * (`key1=value1,key2=value2`, W3C Baggage style). Returns `undefined` for an empty/absent map so
437 > * callers can skip emitting the env var. Empty keys and non-string values are dropped.
438 > */
439 function serializeResourceAttributes(attributes: Record<string, string> | undefined): string | undefined {
440 if (!attributes) {
446 return parts.length > 0 ? parts.join(',') : undefined;
447 }
449 > /**
450 > * Build the env-var overlay for the agent host process from user settings and
451 > * inherited env. Settings are translated to env vars, but if the same env var is
452 > * already present on `inheritedEnv` it wins (developer override).
453 > *
454 > * Only sets a key when the underlying setting was explicitly configured — empty
455 > * string / undefined settings are dropped so they don't shadow inherited env.
456 > */
457 > export function buildAgentHostOTelEnv(
458 settings: IAgentHostOTelSettings,
459 inheritedEnv: Readonly<Record<string, string | undefined>>,
527 return out;
528 }
530 > /**
531 > * Settings -> env-var fan-out for the Claude/Codex SDK overrides that the
532 > * agent host process consumes. Shared by both starters
533 > * (`nodeAgentHostStarter.ts`, `electronAgentHostStarter.ts`) so they don't
534 > * drift the next time someone adds a setting.
535 > *
536 > * The shape mirrors {@link buildAgentHostOTelEnv}: only set a key when the
537 > * underlying setting has a non-empty value AND the inherited env doesn't
538 > * already define it (developer override wins). Returns a partial env map
539 > * the caller spreads into the spawned child's environment.
540 > */
541 > export interface IAgentSdkStarterSettings {
542 > readonly codexSdkRoot?: string;
543 > readonly codexHome?: string;
544 > readonly codexBinaryArgs?: readonly string[];
545 > readonly claudeAgentEnabled?: boolean;
546 > readonly codexAgentEnabled?: boolean;
547 > readonly byokModelsEnabled?: boolean;
548 > }
549 >
550 > export function buildAgentSdkEnv(
551 settings: IAgentSdkStarterSettings,
552 inheritedEnv: Readonly<Record<string, string | undefined>>,
575 return out;
576 }
578 > /** Result of starting the agent host WebSocket server on-demand. */
579 > export interface IAgentHostSocketInfo {
580 > readonly socketPath: string;
581 > }
582 >
583 > /** Inspector listener information for the agent host process. */
584 > export interface IAgentHostInspectInfo {
585 > readonly host: string;
586 > readonly port: number;
587 > /** A `devtools://` URL that can be opened with `INativeHostService.openDevToolsWindow`. */
588 > readonly devtoolsUrl: string;
589 > }
590 >
591 > /** A network endpoint the agent host suggests probing, listed on {@link IAgentHostNetworkDiagnosticsInfo.endpoints}. */
592 > export interface IAgentHostNetworkEndpoint {
593 > /** Human-readable name of the endpoint (e.g. "GitHub API"). */
594 > readonly name: string;
595 > /** The URL to probe. */
596 > readonly url: string;
597 > /** Substring the response body is expected to contain; when set, the probe reads the body and fails the check if it is absent. */
598 > readonly expectedContent?: string;
599 > /** HTTP status code the probe treats as success. Defaults to `200` when omitted. */
600 > readonly expectedStatus?: number;
601 > }
602 >
603 > /** Host-level network context for diagnostics, produced by {@link IAgentConnection.getNetworkDiagnosticsInfo}. */
604 > export interface IAgentHostNetworkDiagnosticsInfo {
605 > /** Agent host product version. */
606 > readonly version: string;
607 > /** Operating system platform of the agent host process (`process.platform`). */
608 > readonly os: string;
609 > /** CPU architecture of the agent host process (`process.arch`). */
610 > readonly arch: string;
611 > /** Authenticated GitHub account login, when known. */
612 > readonly account?: string;
613 > /** VS Code `http.*` proxy settings observed by the agent host, keyed by setting id (only those that are set). */
614 > readonly proxySettings: Readonly<Record<string, string>>;
615 > /** Proxy-related environment variables observed by the agent host process, keyed by name (only those that are set). */
616 > readonly proxyEnv: Readonly<Record<string, string>>;
617 > /** Endpoints the agent host suggests probing via {@link IAgentConnection.diagnosticsFetch}. */
618 > readonly endpoints: readonly IAgentHostNetworkEndpoint[];
619 > }
620 >
621 > export interface IAgentHostManagedSettingsSnapshot {
622 > readonly account?: string;
623 > readonly source: 'server' | 'device' | 'none';
624 > readonly serverManaged: boolean;
625 > readonly deviceManaged: boolean;
626 > readonly failClosed: boolean;
627 > readonly bypassPermissionsDisabled: boolean;
628 > readonly permissionsAllowIntersected?: boolean;
629 > readonly managedKeys: readonly string[];
630 > readonly settings?: Readonly<Record<string, unknown>>;
631 > }
632 >
633 > export interface IAgentHostManagedSettingsDiagnostics {
634 > readonly provider: AgentProvider;
635 > readonly snapshot?: IAgentHostManagedSettingsSnapshot;
636 > readonly error?: string;
637 > }
638 >
639 > /** Result of a DNS lookup for a single address family, part of {@link IAgentHostNetworkFetchResult}. */
640 > export interface IAgentHostDnsResult {
641 > /** The resolved address, when the lookup succeeded. */
642 > readonly address?: string;
643 > /** Time taken by the lookup, in milliseconds. */
644 > readonly durationMs?: number;
645 > /** Lookup error message, when it failed. */
646 > readonly error?: string;
647 > }
648 >
649 > /** Result of a single connectivity probe, produced by {@link IAgentConnection.diagnosticsFetch}. */
650 > export interface IAgentHostNetworkFetchResult {
651 > /** The URL that was probed. */
652 > readonly url: string;
653 > /** The resolved proxy URL for this endpoint, or `undefined` for a direct connection. */
654 > readonly proxyUrl?: string;
655 > /** IPv4 DNS lookup result for the host. */
656 > readonly dnsIpv4?: IAgentHostDnsResult;
657 > /** IPv6 DNS lookup result for the host. */
658 > readonly dnsIpv6?: IAgentHostDnsResult;
659 > /** HTTP status code from the probe, when a response arrived. */
660 > readonly statusCode?: number;
661 > /** HTTP status message from the probe, when a response arrived. */
662 > readonly statusMessage?: string;
663 > /** Response body text (possibly truncated), when a response arrived. Callers use it to check expected content. */
664 > readonly body?: string;
665 > /** Time taken by the reachability probe, in milliseconds. */
666 > readonly durationMs?: number;
667 > /** Probe error message, when the connection failed. */
668 > readonly error?: string;
669 > }
670 >
671 > /**
672 > * IPC service exposed on the {@link AgentHostIpcChannels.ConnectionTracker}
673 > * channel. Used by the server process for lifetime management and by the
674 > * shared process to request a local WebSocket listener on-demand.
675 > */
676 > export interface IConnectionTrackerService {
677 > readonly onDidChangeConnectionCount: Event<number>;
678 >
679 > /**
680 > * Request the agent host to start a WebSocket server on a local
681 > * pipe/socket. Returns the socket path.
682 > * If a server is already running, returns the existing info.
683 > */
684 > startWebSocketServer(): Promise<IAgentHostSocketInfo>;
685 >
686 > /**
687 > * Get inspector listener info for the agent host process. If the inspector
688 > * is not currently active and `tryEnable` is true, opens the inspector on
689 > * a random local port. Returns `undefined` if the inspector cannot be
690 > * enabled (e.g. running in an environment without `node:inspector`).
691 > */
692 > getInspectInfo(tryEnable: boolean): Promise<IAgentHostInspectInfo | undefined>;
693 > }
694 >
695 > /**
696 > * Narrow renderer-to-local-agent-host control surface. All stateful agent
697 > * operations travel over {@link AgentHostIpcChannels.Protocol}.
698 > */
699 > export interface IAgentHostManagementService {
700 > readonly _serviceBrand: undefined;
701 >
702 > /**
703 > * Local-only compatibility path for session fields not yet represented by
704 > * AHP `createSession` (`model`, `agent`, and `importConversation`).
705 > */
706 > createSessionWithExtensions(config: IAgentCreateSessionConfig): Promise<URI>;
707 > /**
708 > * Local-only compatibility path for chat fields not yet represented by AHP
709 > * `createChat` (`title` and `model`).
710 > */
711 > createChatWithExtensions(session: URI, chat: URI, options: IAgentCreateChatOptions): Promise<void>;
712 > shutdown(): Promise<void>;
713 > getNetworkDiagnosticsInfo(): Promise<IAgentHostNetworkDiagnosticsInfo>;
714 > getManagedSettingsDiagnostics(): Promise<readonly IAgentHostManagedSettingsDiagnostics[]>;
715 > diagnosticsFetch(url: string): Promise<IAgentHostNetworkFetchResult>;
716 > startWebSocketServer(): Promise<IAgentHostSocketInfo>;
717 > getInspectInfo(tryEnable: boolean): Promise<IAgentHostInspectInfo | undefined>;
718 > }
719 >
720 > // ---- IPC data types (serializable across MessagePort) -----------------------
721 >
722 > export interface IAgentSessionMetadata {
723 > readonly session: URI;
724 > readonly startTime: number;
725 > readonly modifiedTime: number;
726 > readonly project?: IAgentSessionProjectInfo;
727 > readonly summary?: string;
728 > readonly status?: SessionStatus;
729 > /** Human-readable description of what the session is currently doing. */
730 > readonly activity?: string;
731 > readonly workingDirectory?: URI;
732 > readonly isRead?: boolean;
733 > readonly isArchived?: boolean;
734 > /**
735 > * Aggregate counts (additions / deletions / files) describing the
736 > * `changeKind: 'session'` changeset for this session — the chip
737 > * aggregate previously embedded in the catalogue entry. Mirrors
738 > * `SessionSummary.changes`.
739 > */
740 > readonly changes?: ChangesSummary;
741 > /**
742 > * Catalogue of changesets the agent can produce for this session — the
743 > * {@link Changeset | catalogue} that travels on
744 > * `SessionSummary.changesets`. Lightweight summary entries (id / label /
745 > * URI template / aggregate counts) without per-file detail; clients
746 > * subscribe to a specific expanded changeset URI when they need the full
747 > * file list.
748 > */
749 > readonly changesets?: readonly Changeset[];
750 > /**
751 > * Side-channel metadata mirroring {@link SessionState._meta}, propagated
752 > * to clients via per-session state subscriptions and the root-channel
753 > * session summary (the host treats the session-state and session-summary
754 > * `_meta` as the same bag). Producers SHOULD use namespaced keys; consumers
755 > * MUST ignore unknown keys. Use the typed accessors in `sessionState.ts`
756 > * (e.g. `readSessionGitState`, `readSessionGitHubState`) for well-known
757 > * slots.
758 > */
759 > readonly _meta?: SessionMeta;
760 > }
761 >
762 > export interface IAgentSessionProjectInfo {
763 > readonly uri: URI;
764 > readonly displayName: string;
765 > }
766 >
767 > export interface IAgentCreateSessionResult {
768 > readonly session: URI;
769 > readonly project?: IAgentSessionProjectInfo;
770 > /** The resolved working directory, which may differ from the requested one (e.g. worktree). */
771 > readonly workingDirectory?: URI;
772 > /**
773 > * `true` when the agent only allocated an in-memory placeholder for this
774 > * session (no SDK session, no worktree, no on-disk state). Materialization
775 > * happens lazily on the first {@link IAgentChats.sendMessage}, at which point
776 > * the agent fires {@link IAgent.onDidMaterializeSession}. The
777 > * {@link IAgentService} uses this flag to defer the `sessionAdded` protocol
778 > * notification so observers don't see the session in their list until it
779 > * has been persisted.
780 > */
781 > readonly provisional?: boolean;
782 > }
783 >
784 > /**
785 > * Payload of {@link IAgent.onDidMaterializeSession}. Fired once per session
786 > * when a previously {@link IAgentCreateSessionResult.provisional} session has
787 > * its SDK session, worktree (if any), and on-disk metadata in place.
788 > */
789 > export interface IAgentMaterializeSessionEvent {
790 > readonly session: URI;
791 > readonly workingDirectory: URI | undefined;
792 > readonly project: IAgentSessionProjectInfo | undefined;
793 > }
794 >
795 > export type AgentProvider = string;
796 >
797 > /** Well-known agent provider id for the Claude agent-host backend. */
798 > export const CLAUDE_AGENT_PROVIDER_ID = 'claude' as const;
799 >
800 > /** Well-known agent provider id for the Codex agent-host backend. */
801 > export const CODEX_AGENT_PROVIDER_ID = 'codex' as const;
802 >
803 > /**
804 > * Static capability facts an agent backend advertises about itself. Each flag
805 > * is opt-in (absent means unsupported) so single-chat agents (e.g. Codex) can omit
806 > * the bag entirely. Discovered over IPC alongside the rest of
807 > * {@link IAgentDescriptor} and surfaced to the sessions UI so features are
808 > * capability-gated instead of switched on the provider id.
809 > *
810 > * This is the IPC contract alias of the protocol-visible {@link AgentCapabilities}
811 > * type (defined in the root-state protocol); both share a single canonical shape
812 > * so a new flag added in one place is automatically reflected in the other.
813 > */
814 > export type IAgentCapabilities = AgentCapabilities;
815 >
816 > /** Metadata describing an agent backend, discovered over IPC. */
817 > export interface IAgentDescriptor {
818 > readonly provider: AgentProvider;
819 > readonly displayName: string;
820 > readonly description: string;
821 > /** Static capability flags the agent advertises (see {@link IAgentCapabilities}). */
822 > readonly capabilities?: IAgentCapabilities;
823 > }
824 >
825 >
826 > // ---- Auth types (RFC 9728 / RFC 6750 inspired) -----------------------------
827 >
828 > /**
829 > * Parameters for the `authenticate` command.
830 > * Analogous to sending `Authorization: Bearer <token>` (RFC 6750 section 2.1).
831 > */
832 > export interface AuthenticateParams {
833 > /**
834 > * The `resource` identifier from the server's
835 > * {@link IAuthorizationProtectedResourceMetadata} that this token targets.
836 > */
837 > readonly resource: string;
838 > /**
839 > * Scopes that were used to acquire the token. Omitted for legacy clients
840 > * that can only identify tokens by protected resource.
841 > */
842 > readonly scopes?: readonly string[];
843 >
844 > /** The bearer token value (RFC 6750). */
845 > readonly token: string;
846 > }
847 >
848 > /** Request for a previously accepted bearer token. */
849 > export interface IAgentHostAuthTokenRequest {
850 > /** Protected resource identifier from {@link ProtectedResourceMetadata.resource}. */
851 > readonly resource: string;
852 > /** Required token scopes, when the caller needs a scope-specific token. */
853 > readonly scopes?: readonly string[];
854 > }
855 >
856 > /**
857 > * Result of the `authenticate` command.
858 > */
859 > export interface AuthenticateResult {
860 > /** Whether the token was accepted. */
861 > readonly authenticated: boolean;
862 > }
863 >
864 > /**
865 > * Canonical {@link ProtectedResourceMetadata} for the GitHub Copilot
866 > * resource. Shared between every agent provider that consumes a GitHub
867 > * Copilot bearer token (e.g. Copilot CLI, Claude) so they advertise an
868 > * identical resource identifier to the auth flow — clients dispatch by
869 > * `resource`, and divergent metadata would silently route the same
870 > * token down separate code paths.
871 > */
872 > export const GITHUB_COPILOT_PROTECTED_RESOURCE: ProtectedResourceMetadata = {
873 > resource: 'https://api.github.com',
874 > resource_name: 'GitHub Copilot',
875 > authorization_servers: ['https://github.com/login/oauth'],
876 > scopes_supported: ['read:user', 'user:email'],
877 > required: true,
878 > };
879 >
880 > /**
881 > * Canonical {@link ProtectedResourceMetadata} for GitHub repository write
882 > * operations (e.g. creating a pull request). Distinct from
883 > * {@link GITHUB_COPILOT_PROTECTED_RESOURCE} so that the broader `repo`
884 > * scope is only requested when a session actually needs it (e.g. when a
885 > * changeset operation handler throws `AHP_AUTH_REQUIRED` with this
886 > * resource), rather than at session create for every agent.
887 > *
888 > * `required: false` reflects that the resource is only needed on demand —
889 > * agents do not have to advertise it eagerly. The workbench-side auth
890 > * contributor resolves it lazily in response to operation invocations.
891 > */
892 > export const GITHUB_REPO_PROTECTED_RESOURCE: ProtectedResourceMetadata = {
893 > resource: 'https://api.github.com/repos',
894 > resource_name: 'GitHub Repository',
895 > authorization_servers: ['https://github.com/login/oauth'],
896 > scopes_supported: ['repo'],
897 > required: false,
898 > };
899 >
900 > export interface IAgentCreateSessionConfig {
901 > readonly provider?: AgentProvider;
902 > readonly model?: ModelSelection;
903 > /**
904 > * Initial custom agent selection for the new session. Omit to start with
905 > * no custom agent selected (provider default behavior).
906 > */
907 > readonly agent?: AgentSelection;
908 > readonly session?: URI;
909 > readonly workingDirectory?: URI;
910 > readonly config?: Record<string, unknown>;
911 > /**
912 > * Eagerly claim the active client role for the new session. When provided,
913 > * the server initializes the session with this client as the active
914 > * client, equivalent to dispatching a `session/activeClientSet`
915 > * action immediately after creation. The `clientId` MUST match the
916 > * connection's own `clientId`.
917 > */
918 > readonly activeClient?: SessionActiveClient;
919 > /** Fork from an existing session at a specific turn. */
920 > readonly fork?: {
921 > readonly session: URI;
922 > readonly turnIndex: number;
923 > readonly turnId: string;
924 > /**
925 > * Maps old protocol turn IDs to new protocol turn IDs.
926 > * Populated by the service layer after generating fresh UUIDs
927 > * for the forked session's turns. Used by the agent to remap
928 > * per-turn data (e.g. SDK event ID mappings) in the session database.
929 > */
930 > readonly turnIdMapping?: ReadonlyMap<string, string>;
931 > };
932 > /**
933 > * Import an existing (e.g. local) conversation into a brand-new session as
934 > * real, editable turns. The provider translates {@link turns} into a
935 > * Copilot event log seeded on disk and resumes the session so the turns are
936 > * reconstituted as genuine backend events (editable / forkable / truncatable).
937 > *
938 > * The service layer assigns fresh UUID turn ids before handing the turns to
939 > * the provider so the seeded event ids and the seeded protocol turns stay
940 > * aligned. Mutually exclusive with {@link fork}.
941 > */
942 > readonly importConversation?: {
943 > readonly turns: readonly Turn[];
944 > readonly model?: ModelSelection;
945 > };
946 > /**
947 > * MCP-style opt-in progress token from the client's `createSession`. When
948 > * set, the service reports any long-running session bring-up work — chiefly
949 > * the lazy first-use SDK download — as `progress` notifications carrying
950 > * this token, so the client can correlate them to this call.
951 > */
952 > readonly progressToken?: string;
953 > }
954 >
955 > /** Options for creating an additional chat within a session. */
956 > export interface IAgentCreateChatOptions {
957 > /** Optional display title for the new chat. */
958 > readonly title?: string;
959 > /** Optional model override; defaults to the session's model. */
960 > readonly model?: ModelSelection;
961 > /**
962 > * Fork an existing chat into this new chat. The new chat starts
963 > * pre-populated with the source chat's turns up to and including
964 > * {@link IAgentCreateChatForkSource.turnId}, and its backing chat
965 > * is forked from the source so it can continue independently.
966 > */
967 > readonly fork?: IAgentCreateChatForkSource;
968 > /**
969 > * Create this new chat as a side chat branching from a turn in an existing
970 > * chat (via `/btw`). Unlike {@link fork}, inherited context is provider-owned
971 > * and must not appear in the chat's visible history.
972 > */
973 > readonly sideChat?: IAgentCreateChatSideChatSource;
974 > }
975 >
976 > /** Identifies a source chat and turn to fork a new chat from. */
977 > export interface IAgentCreateChatForkSource {
978 > /** URI of the existing chat to fork from. */
979 > readonly source: URI;
980 > /** Turn ID in the source chat; content up to and including this turn is copied. */
981 > readonly turnId: string;
982 > /**
983 > * Maps old source turn IDs to fresh turn IDs for the forked chat. Populated
984 > * by the agent service so the agent can remap per-turn data (e.g. SDK event
985 > * ID mappings) in the forked chat's database.
986 > */
987 > readonly turnIdMapping?: ReadonlyMap<string, string>;
988 > }
989 >
990 > /** Immutable selected-text snapshot captured when a side chat is created. */
991 > export interface IAgentCreateChatSideChatSelection {
992 > /** Exact selected-text snapshot captured at side-chat creation time. */
993 > readonly text: string;
994 > /** Optional provenance for the response part that contained {@link text}. */
995 > readonly responsePartId?: string;
996 > }
997 >
998 > /** Identifies a source chat and turn a side chat (`/btw`) branches from. */
999 > export interface IAgentCreateChatSideChatSource {
1000 > /** URI of the existing chat the side chat branches from. */
1001 > readonly source: URI;
1002 > /** Turn ID in the source chat the side chat records as its provenance. */
1003 > readonly turnId: string;
1004 > /** Optional selected-text snapshot captured from the source chat transcript. */
1005 > readonly selection?: IAgentCreateChatSideChatSelection;
1006 > /** Concrete provider turn ID to fork/resume from when `turnId` names a host-only local turn. */
1007 > readonly providerAnchorTurnId?: string;
1008 > /** Bounded source-chat context captured from host state when the provider transcript lags. */
1009 > readonly sourceContext?: string;
1010 > /** User-visible assistant text captured while the source turn was active. */
1011 > readonly partialResponse?: string;
1012 > }
1013 >
1014 > /** Result of {@link IAgentChats.createChat}: the opaque blob to persist for restore. */
1015 > export interface IAgentCreateChatResult {
1016 > /**
1017 > * Opaque, agent-owned token the orchestrator persists verbatim in the chat
1018 > * catalog and hands back to {@link IAgent.materializeChat} on
1019 > * restore. The orchestrator never parses it. `undefined` means nothing to
1020 > * persist (e.g. the agent keeps no resumable backing).
1021 > */
1022 > readonly providerData?: string;
1023 > /**
1024 > * The SDK-level session URI that backs this peer chat, when the agent mints
1025 > * one in the same session store its own {@link IAgent.listSessions} enumerates
1026 > * (e.g. Claude). First-class and non-opaque — unlike {@link providerData} the
1027 > * orchestrator reads it to correlate and suppress the backing session so it
1028 > * never surfaces as a top-level session. `undefined` when the agent keeps no
1029 > * separately-enumerable backing session.
1030 > */
1031 > readonly backingSession?: URI;
1032 > }
1033 >
1034 > /** Payload of {@link IAgent.onDidChangeChatData}. */
1035 > export interface IAgentChatDataChange {
1036 > /** The peer chat whose backing chat's blob changed. */
1037 > readonly chat: URI;
1038 > /** The new opaque blob to persist (replaces any previously stored value). */
1039 > readonly providerData: string;
1040 > }
1041 >
1042 > /** A legacy peer chat enumerated by {@link IAgent.listLegacyChats} for one-time migration. */
1043 > export interface IAgentLegacyChat {
1044 > /** The peer chat's channel URI (see {@link buildChatUri}). */
1045 > readonly uri: URI;
1046 > /** The opaque, agent-owned backing blob, encoded as {@link materializeChat} expects. */
1047 > readonly providerData?: string;
1048 > }
1049 >
1050 > /**
1051 > * Identifies the parent that spawned a chat. The orchestrator records
1052 > * it as the spawned chat's {@link ChatOriginKind.Tool} origin so clients can
1053 > * render the parent/child relationship (e.g. a sub-agent "team" member spawned
1054 > * by a tool call in the parent chat).
1055 > */
1056 > export interface IAgentSpawnedChatParent {
1057 > /** The parent chat (chat) URI whose tool call performed the spawn. */
1058 > readonly chat: URI;
1059 > /** The id of the tool call in the parent that spawned this chat. */
1060 > readonly toolCallId: string;
1061 > }
1062 >
1063 > /**
1064 > * Payload of {@link IAgent.onDidSpawnChat}: a new chat the
1065 > * agent spawned itself (e.g. a sub-agent delegated by a tool call), as opposed
1066 > * to a user-driven chat created via
1067 > * {@link IAgentChats.createChat}.
1068 > */
1069 > export interface IAgentSpawnChatEvent {
1070 > /** The session URI the spawned chat belongs to. */
1071 > readonly session: URI;
1072 > /** The spawned chat's channel URI (the new chat). */
1073 > readonly chat: URI;
1074 > /**
1075 > * The parent that spawned it, when the spawn was delegated by a tool call.
1076 > * Recorded as the chat's tool origin in the catalog. Absent for a
1077 > * top-level, agent-initiated chat with no spawning tool call.
1078 > */
1079 > readonly parent?: IAgentSpawnedChatParent;
1080 > /** Optional display title for the spawned chat. */
1081 > readonly title?: string;
1082 > }
1083 >
1084 > /** Max characters for a subagent tab title before it is ellipsized. */
1085 > const SUBAGENT_CHAT_TITLE_MAX_LENGTH = 60;
1086 >
1087 > /**
1088 > * Builds the tab title for a subagent peer chat. Prefers the concise
1089 > * per-task description (so two subagents of the same type still get
1090 > * distinct, meaningful names), truncating it so an over-long value never
1091 > * blows out the tab strip or the Subagents dropdown; falls back to the
1092 > * agent type's display name, then a generic label. Shared by the live
1093 > * spawn path and the restore path so both name subagent tabs identically.
1094 > */
1095 > export function subagentChatTitle(taskDescription: string | undefined, agentDisplayName: string | undefined): string {
1096 const task = taskDescription?.trim();
1097 if (task) {
1100 return agentDisplayName?.trim() || 'Subagent';
1101 }
1103 > /**
1104 > * Maps agent `subagent_*` signals to the unified chat catalog's
1105 > * spawn/end events. Shared by the agents' spawn bridges and the orchestrator so
1106 > * subagent membership has one derivation.
1107 > */
1108 > export namespace SubagentChatSignal {
1109 >
1110 > /**
1111 > * Derives the {@link IAgentSpawnChatEvent} for a `subagent_started` signal,
1112 > * addressing the subagent by the stable {@link buildSubagentChatUri} and
1113 > * recording the spawning tool call as its parent edge. Returns `undefined`
1114 > * for any other signal (or an unmappable chat URI).
1115 > */
1116 > export function toSpawnEvent(signal: AgentSignal): IAgentSpawnChatEvent | undefined {
1117 if (signal.kind !== 'subagent_started') {
1118 return undefined;
1135 };
1136 }
1137 > } agentService.ts
1138 >
1139 > // ---- Chat surface --------------------------------------------------
1140 >
1141 > /**
1142 > * The chat-addressed operation surface an agent exposes for the chats
1143 > * within a session.
1144 > *
1145 > * Every operation method addresses a chat by a concrete chat channel URI:
1146 > * the default chat channel for a session's DEFAULT chat, or an additional
1147 > * chat's own channel URI. The orchestrator ({@link IAgentService}) owns the
1148 > * feature-level `(session, chat)` to chat-channel mapping and only ever calls
1149 > * these operations with a concrete chat URI. This replaces the legacy
1150 > * `(session, chat?)` parameter pairs and the per-agent default-chat handling on
1151 > * {@link IAgent}.
1152 > *
1153 > * Optional on {@link IAgent}: agents implement this incrementally (waves
1154 > * C2/C3/C4). Until an agent exposes it, {@link IAgentService} falls back to the
1155 > * agent's legacy `(session, chat?)` methods via a thin adapter.
1156 > */
1157 > export interface IAgentChats {
1158 > /**
1159 > * Create a fresh additional chat within the session the `chat` URI belongs
1160 > * to, sharing the session's working directory, model, agent, and
1161 > * customizations. `chat` is the client-chosen channel URI the new chat is
1162 > * addressed by; its parent session is derived from it.
1163 > * Returns the opaque {@link IAgentCreateChatResult} blob to persist for
1164 > * restore (or `void` when the agent keeps no resumable backing).
1165 > */
1166 > createChat(chat: URI, options?: IAgentCreateChatOptions): Promise<IAgentCreateChatResult | void>;
1167 >
1168 > /**
1169 > * Fork a new chat from an existing one. The new `chat`
1170 > * inherits `source`'s backing up to and including
1171 > * {@link IAgentCreateChatForkSource.turnId} and then continues
1172 > * independently. The new chat's parent session is derived from its URI.
1173 > */
1174 > fork(chat: URI, source: IAgentCreateChatForkSource, options?: IAgentCreateChatOptions): Promise<IAgentCreateChatResult | void>;
1175 >
1176 > /**
1177 > * Dispose an additional chat created via
1178 > * {@link createChat}/{@link fork}, freeing its backing. A session's
1179 > * default chat cannot be disposed in isolation; it lives and dies
1180 > * with the session.
1181 > */
1182 > disposeChat(chat: URI): Promise<void>;
1183 >
1184 > /**
1185 > * Send a user message into `chat`; on first send, the host passes the resolved
1186 > * working directory (or `undefined` for workspace-less sessions).
1187 > */
1188 > sendMessage(chat: URI, prompt: string, workingDirectory: URI | undefined, attachments?: readonly MessageAttachment[], turnId?: string, senderClientId?: string): Promise<void>;
1189 >
1190 > /** Abort the in-flight turn for `chat`. */
1191 > abort(chat: URI): Promise<void>;
1192 >
1193 > /** Change the model for `chat`. */
1194 > changeModel(chat: URI, model: ModelSelection): Promise<void>;
1195 >
1196 > /**
1197 > * Change (or clear) the selected custom agent for `chat`. Passing
1198 > * `undefined` clears the selection (provider default behavior).
1199 > */
1200 > changeAgent(chat: URI, agent: AgentSelection | undefined): Promise<void>;
1201 >
1202 > /** Reconstruct the turns for `chat` (used on restore). */
1203 > getMessages(chat: URI): Promise<readonly Turn[]>;
1204 > }
1205 >
1206 > export interface IAgentResolveSessionConfigParams {
1207 > readonly provider?: AgentProvider;
1208 > readonly workingDirectory?: URI;
1209 > readonly config?: Record<string, unknown>;
1210 > }
1211 >
1212 > export interface IAgentSessionConfigCompletionsParams extends IAgentResolveSessionConfigParams {
1213 > readonly property: string;
1214 > readonly query?: string;
1215 > }
1216 >
1217 > /** Serializable model information from the agent host. */
1218 > export interface IAgentModelInfo {
1219 > readonly provider: AgentProvider;
1220 > readonly id: string;
1221 > readonly name: string;
1222 > readonly maxContextWindow?: number;
1223 > readonly maxOutputTokens?: number;
1224 > readonly maxPromptTokens?: number;
1225 > readonly supportsVision: boolean;
1226 > readonly configSchema?: ConfigSchema;
1227 > readonly policyState?: PolicyState;
1228 > readonly _meta?: Record<string, unknown>;
1229 > }
1230 >
1231 > // ---- Agent signals (sent via IAgent.onDidSessionProgress) -------------------
1232 >
1233 > /**
1234 > * A signal emitted by an agent during session execution.
1235 > *
1236 > * Most signals carry a protocol {@link SessionAction} directly via the
1237 > * `kind: 'action'` shape, eliminating a parallel event ontology. A small
1238 > * number of cases that have no clean protocol action (permission
1239 > * auto-approval, subagent session creation, steering message
1240 > * acknowledgment) remain as discriminated non-action signals so the host
1241 > * can perform side effects before — or instead of — dispatching an action.
1242 > */
1243 > export type AgentSignal =
1244 > | IAgentActionSignal
1245 > | IAgentToolPendingConfirmationSignal
1246 > | IAgentSubagentStartedSignal
1247 > | IAgentSubagentCompletedSignal
1248 > | IAgentSteeringConsumedSignal;
1249 >
1250 > /**
1251 > * Carries a protocol {@link SessionAction} produced by an agent. The host
1252 > * dispatches the action through the state manager after routing via
1253 > * {@link IAgentActionSignal.parentToolCallId} (if set).
1254 > *
1255 > * Agents are responsible for populating the target channel and any `turnId` /
1256 > * `partId` fields on the action.
1257 > */
1258 > export interface IAgentActionSignal {
1259 > readonly kind: 'action';
1260 > /** Target session or chat channel URI. For inner subagent events this is the parent session — see {@link parentToolCallId}. */
1261 > readonly resource: URI;
1262 > /** Protocol action to dispatch. */
1263 > readonly action: SessionAction | ChatAction;
1264 > /** If set, route the action to the subagent session belonging to this tool call. */
1265 > readonly parentToolCallId?: string;
1266 > }
1267 >
1268 > /**
1269 > * A tool has finished collecting parameters and needs the host to decide
1270 > * whether it should run (or, mid-execution, re-confirm). The host applies
1271 > * auto-approval logic over {@link permissionKind} / {@link permissionPath}
1272 > * (see `SessionPermissionManager.getAutoApproval`) and then dispatches the
1273 > * appropriate `ChatToolCallReady` action — with confirmation options
1274 > * baked in when the user must approve, or with `confirmed: NotNeeded` when
1275 > * the host auto-approved.
1276 > *
1277 > * Kept as a non-action signal because the host owns this approval policy;
1278 > * the agent only describes the tool call and the kind of permission being
1279 > * requested. The {@link state} field carries the protocol-shaped tool-call
1280 > * state and is dispatched verbatim into the action.
1281 > */
1282 > export interface IAgentToolPendingConfirmationSignal {
1283 > readonly kind: 'pending_confirmation';
1284 > /** Target chat channel URI containing the tool call. */
1285 > readonly chat: URI;
1286 > /** Protocol-shaped pending-confirmation state, dispatched verbatim into `ChatToolCallReady`. */
1287 > readonly state: ToolCallPendingConfirmationState;
1288 > /** Host-only auto-approval kind (not part of the dispatched action). */
1289 > readonly permissionKind?: 'shell' | 'write' | 'mcp' | 'read' | 'url' | 'skill' | 'custom-tool' | 'hook' | 'memory' | 'extension-management' | 'extension-permission-access';
1290 > /** Host-only auto-approval path target (not part of the dispatched action). */
1291 > readonly permissionPath?: string;
1292 > /**
1293 > * Host-only flag (not part of the dispatched action): the model requested
1294 > * this shell command run OUTSIDE the sandbox (and the host opted in via
1295 > * `sandbox.allowBypass`).
1296 > */
1297 > readonly requestSandboxBypass?: boolean;
1298 > /**
1299 > * If set, the tool call belongs to the subagent rooted at this
1300 > * parent tool call. Used by the host to route the resulting
1301 > * `ChatToolCallReady` to the subagent session — otherwise the
1302 > * action would land on the parent session, where there is no
1303 > * matching `ChatToolCallStart`.
1304 > */
1305 > readonly parentToolCallId?: string;
1306 > }
1307 >
1308 > /**
1309 > * A subagent was spawned by a tool call. The host creates a child session
1310 > * silently and routes subsequent inner-tool events to it.
1311 > *
1312 > * Kept as a non-action signal because subagent session creation has no
1313 > * protocol action — it's a host-side composition primitive.
1314 > */
1315 > export interface IAgentSubagentStartedSignal {
1316 > readonly kind: 'subagent_started';
1317 > readonly chat: URI;
1318 > readonly toolCallId: string;
1319 > readonly agentName: string;
1320 > readonly agentDisplayName: string;
1321 > readonly agentDescription?: string;
1322 > /**
1323 > * The spawning Task tool's short (typically 3-5 word) `description`
1324 > * input, e.g. "Review package.json structure". Distinct from
1325 > * {@link agentDescription} (the agent *type*'s long role blurb) and
1326 > * {@link agentDisplayName} (the agent type's name). Preferred as the
1327 > * peer chat's tab title because it is concise and per-task, so two
1328 > * subagents of the same type still get distinct, meaningful names.
1329 > * Absent when the harness does not surface a task description.
1330 > */
1331 > readonly taskDescription?: string;
1332 > /**
1333 > * The full delegated instruction the parent handed the subagent (the
1334 > * spawning tool's `prompt` input). Populated by each provider at emit
1335 > * time from its own native source, so the shared orchestrator never
1336 > * parses a provider-specific tool-input shape. Seeds the subagent peer
1337 > * chat's opening request. Distinct from {@link taskDescription} (a short
1338 > * tab-title label). Absent when the harness does not surface a prompt.
1339 > */
1340 > readonly taskPrompt?: string;
1341 > /**
1342 > * If set, the spawning tool call ({@link toolCallId}) itself lives
1343 > * inside another subagent's chat — this is the tool call **one level up**
1344 > * from the spawning tool (its parent), i.e. the tool that spawned the
1345 > * immediate parent chat. The host uses it to route the
1346 > * subagent-discovery side effect (the `ChatToolCallContentChanged`
1347 > * block that lets clients find the child chat) to that immediate parent
1348 > * chat rather than the top-level {@link chat}. Because subagent chats
1349 > * are flat (all keyed off the root session + the spawning tool id),
1350 > * this single one-hop reference resolves the correct parent chat at
1351 > * ANY nesting depth — no per-level chain is needed. Absent for a
1352 > * top-level subagent, whose spawning tool call lives directly in
1353 > * {@link chat}.
1354 > */
1355 > readonly parentToolCallId?: string;
1356 > }
1357 >
1358 > /**
1359 > * A subagent has finished — either successfully or with an error. The host
1360 > * uses this to tear down the child session after all of its events have been
1361 > * routed. The parent tool call completing is not a reliable signal for this
1362 > * because background subagents (e.g. Copilot's `mode: background` task) keep
1363 > * emitting events after their parent tool call returns immediately.
1364 > */
1365 > export interface IAgentSubagentCompletedSignal {
1366 > readonly kind: 'subagent_completed';
1367 > readonly chat: URI;
1368 > readonly toolCallId: string;
1369 > }
1370 >
1371 > /** A steering message was consumed (sent to the model). */
1372 > export interface IAgentSteeringConsumedSignal {
1373 > readonly kind: 'steering_consumed';
1374 > readonly chat: URI;
1375 > readonly id: string;
1376 > }
1377 >
1378 > // ---- Session URI helpers ----------------------------------------------------
1379 >
1380 > export namespace AgentSession {
1381 >
1382 > /**
1383 > * Creates a session URI from a provider name and raw session ID.
1384 > * The URI scheme is the provider name (e.g., `copilot:/<rawId>`).
1385 > */
1386 > export function uri(provider: AgentProvider, rawSessionId: string): URI {
1387 > return URI.from({ scheme: provider, path: `/${rawSessionId}` }); agentService.ts
1388 > }
1390 > /**
1391 > * Extracts the raw session ID from a session URI (the path without leading slash).
1392 > * Accepts both a URI object and a URI string.
1393 > */
1394 > export function id(session: URI | string): string {
1395 const parsed = typeof session === 'string' ? URI.parse(session) : session;
1396 return parsed.path.substring(1);
1397 }
1399 > /**
1400 > * Extracts the provider name from a session URI scheme.
1401 > * Accepts both a URI object and a URI string.
1402 > */
1403 > export function provider(session: URI | string): AgentProvider | undefined {
1404 const parsed = typeof session === 'string' ? URI.parse(session) : session;
1405 return parsed.scheme || undefined;
1406 }
1407 > } agentService.ts
1408 >
1409 > // ---- Agent provider interface -----------------------------------------------
1410 >
1411 > /**
1412 > * A notification originating from an MCP server, routed back to the AHP
1413 > * client through the `mcp://` side channel. `channel` is the channel
1414 > * URI advertised on the owning
1415 > * {@link McpServerCustomization.channel | McpServerCustomization}; the
1416 > * client uses it to fan the notification out to the appropriate App.
1417 > * `method` and `params` follow the underlying MCP notification spec
1418 > * (e.g. `notifications/tools/list_changed`).
1419 > */
1420 > export interface IMcpNotification {
1421 > readonly channel: string;
1422 > readonly method: string;
1423 > readonly params?: Record<string, unknown>;
1424 > }
1425 >
1426 > /**
1427 > * A subagent child session discovered in a parent session's event log,
1428 > * returned by {@link IAgent.getSubagentSessions} so a parent restore can
1429 > * register the child's state up-front.
1430 > */
1431 > export interface IRestoredSubagentSession {
1432 > /** Child subagent session URI (subscribable by clients). */
1433 > readonly resource: URI;
1434 > /** Parent tool call id that spawned the subagent. */
1435 > readonly toolCallId: string;
1436 > /** Display title for the subagent session. */
1437 > readonly title: string;
1438 > /** Reconstructed turns for the subagent's transcript. */
1439 > readonly turns: readonly Turn[];
1440 > }
1441 >
1442 > /**
1443 > * A per-session handle for one active client's contributions (tools and
1444 > * plugin customizations) to an agent session, obtained via
1445 > * {@link IAgent.getOrCreateActiveClient}.
1446 > *
1447 > * `tools` and `customizations` are mutable accessor properties: assigning a
1448 > * new array replaces this client's contribution wholesale and triggers the
1449 > * agent's internal reaction (refreshing the merged tool set exposed to the
1450 > * model, or kicking off an asynchronous customization sync). The arrays are
1451 > * `readonly` so callers cannot mutate them in place and silently bypass the
1452 > * setter. The agent merges the contributions of all active clients on a
1453 > * session, deduplicating as needed.
1454 > */
1455 > export interface IActiveClient {
1456 > /** Client identifier (matches `clientId` from `initialize`). */
1457 > readonly clientId: string;
1458 > /** Human-readable client name (e.g. `"VS Code"`), if provided. */
1459 > readonly displayName: string | undefined;
1460 > /** This client's tools. Assigning replaces the set (full replacement). */
1461 > tools: readonly ToolDefinition[];
1462 > /** This client's plugin customizations. Assigning replaces the set and starts an internal sync. */
1463 > customizations: readonly ClientPluginCustomization[];
1464 > }
1465 >
1466 > /**
1467 > * Implemented by each agent backend (e.g. Copilot SDK).
1468 > * The {@link IAgentService} dispatches to the appropriate agent based on
1469 > * the agent id.
1470 > */
1471 > export interface IAgent {
1472 > /** Unique identifier for this provider (e.g. `'copilot'`). */
1473 > readonly id: AgentProvider;
1474 >
1475 > /** Fires when the provider streams progress for a session. */
1476 > readonly onDidSessionProgress: Event<AgentSignal>;
1477 >
1478 > /**
1479 > * Fires once when a previously
1480 > * {@link IAgentCreateSessionResult.provisional} session has been
1481 > * materialized — i.e. its SDK session, worktree (if any), and on-disk
1482 > * metadata are all in place. The {@link IAgentService} uses this event
1483 > * to fire the deferred `sessionAdded` notification with the now-final
1484 > * summary.
1485 > */
1486 > readonly onDidMaterializeSession?: Event<IAgentMaterializeSessionEvent>;
1487 >
1488 > /**
1489 > * Provides the agent host's server-tool host so the provider can advertise
1490 > * and execute the agent host's server tools (feedback "comments" today, more
1491 > * in the future) against a session's state. Optional: providers that do not
1492 > * support server-side tools simply omit it. Called once during registration
1493 > * with the {@link IAgentService}.
1494 > */
1495 > setServerToolHost?(host: IAgentServerToolHost): void;
1496 >
1497 > // ---- Chat surface ------------------------------------------------------
1498 > //
1499 > // `chats` is the chat-addressed operation surface. Its chats are addressed
1500 > // by concrete chat channel URIs. The orchestrator ({@link IAgentService})
1501 > // owns the feature-level `(session, chat)` to chat-channel mapping.
1502 >
1503 > /**
1504 > * Chat-addressed surface for the chats within a session (send/abort/
1505 > * change model/agent, create/fork/dispose chats, read history).
1506 > */
1507 > readonly chats: IAgentChats;
1508 >
1509 > // ---- Session lifecycle / configuration ---------------------------------
1510 >
1511 > /** Create a new session. Host-owned worktree fields are omitted from `config.config`. */
1512 > createSession(config?: IAgentCreateSessionConfig): Promise<IAgentCreateSessionResult>;
1513 >
1514 > /** Resolve provider-owned session configuration; host-owned worktree fields are omitted. */
1515 > resolveSessionConfig(params: IAgentResolveSessionConfigParams): Promise<ResolveSessionConfigResult>;
1516 >
1517 > /** Return dynamic completions for a provider-owned session configuration property. */
1518 > sessionConfigCompletions(params: IAgentSessionConfigCompletionsParams): Promise<SessionConfigCompletionsResult>;
1519 >
1520 > /**
1521 > * Re-attach an agent's in-memory backing for a peer chat on session
1522 > * restore, decoding the opaque `providerData` produced earlier by
1523 > * {@link IAgentChats.createChat} (or the latest
1524 > * {@link onDidChangeChatData}). After this resolves the agent MUST
1525 > * be able to serve {@link getSessionMessages}/
1526 > * {@link IAgentChats.sendMessage} for `chat`.
1527 > * Best-effort: implementations SHOULD NOT throw on a corrupt/unknown blob —
1528 > * log and no-op so the orchestrator restores the chat with history but no
1529 > * live backing. `providerData` is `undefined` only for legacy entries with
1530 > * no stored blob, in which case the agent MAY consult its own legacy
1531 > * persistence once to recover the backing.
1532 > */
1533 > materializeChat?(chat: URI, providerData: string | undefined): Promise<void>;
1534 >
1535 > /**
1536 > * Migration-only enumeration of a session's peer chats persisted in the
1537 > * agent's OWN legacy format (predating the orchestrator-owned catalog). The
1538 > * orchestrator calls this once, when its own catalog is absent, to drain the
1539 > * legacy chats into {@link PEER_CHATS_METADATA_KEY}; subsequent restores read
1540 > * the orchestrator catalog and never consult this again. Each entry's
1541 > * `providerData` uses the same encoding {@link IAgentChats.createChat}
1542 > * produces and {@link materializeChat} decodes. Agents with no legacy
1543 > * format (e.g. Codex) omit this method.
1544 > */
1545 > listLegacyChats?(session: URI): Promise<readonly IAgentLegacyChat[]>;
1546 >
1547 > /**
1548 > * Fires when a peer chat's opaque `providerData` changes after creation
1549 > * (e.g. per-chat model switch, fork remap). The orchestrator re-persists the
1550 > * blob. Agents whose blob is immutable never fire this.
1551 > */
1552 > readonly onDidChangeChatData?: Event<IAgentChatDataChange>;
1553 >
1554 > // ---- Spawned chat (membership) channel -------------------------
1555 > //
1556 > // First-class membership channel for chats the agent spawns itself
1557 > // (e.g. sub-agent / "team" member chats delegated by a tool call),
1558 > // as opposed to user-driven chats created via
1559 > // {@link IAgentChats.createChat}. The orchestrator
1560 > // ({@link IAgentService}) routes these straight into the chat catalog
1561 > // (addChat/removeChat) so harness-spawned and user-driven chats share ONE
1562 > // membership path. Agents that never spawn chats omit both events.
1563 >
1564 > /**
1565 > * Fires when the agent spawns a new chat within a session (e.g. a
1566 > * sub-agent delegated by a tool call). The orchestrator records it in the
1567 > * chat catalog, preserving the {@link IAgentSpawnChatEvent.parent}
1568 > * spawn edge as the chat's {@link ChatOriginKind.Tool} origin.
1569 > */
1570 > readonly onDidSpawnChat?: Event<IAgentSpawnChatEvent>;
1571 >
1572 > /**
1573 > * Called when a chat's pending (steering) message changes.
1574 > * The agent harness decides how to react — e.g. inject steering
1575 > * mid-turn via `mode: 'immediate'`. Steering is always addressed by a
1576 > * concrete chat channel URI — the session's default chat or an additional
1577 > * peer chat — so it never leaks into a sibling chat of the same session.
1578 > *
1579 > * Queued messages are consumed on the server side and are not
1580 > * forwarded to the agent; `queuedMessages` will always be empty.
1581 > */
1582 > setPendingMessages?(chat: URI, steeringMessage: PendingMessage | undefined, queuedMessages: readonly PendingMessage[]): void;
1583 >
1584 > /**
1585 > * Retrieve the reconstructed turns for a session, used when restoring
1586 > * sessions from persistent storage. Each agent owns the conversion from
1587 > * its SDK-specific event log to protocol {@link Turn}s, including
1588 > * subagent sessions (callers pass the subagent URI to retrieve the
1589 > * child session's turns).
1590 > */
1591 > getSessionMessages(session: URI): Promise<readonly Turn[]>;
1592 >
1593 > /**
1594 > * Returns the subagent child sessions discoverable in a session's event
1595 > * log so a parent restore can eagerly register them in a single pass.
1596 > * Without this, every child is restored separately by re-fetching and
1597 > * re-reconstructing the full parent event log (one pass per subagent).
1598 > * Agents that serve this from the same reconstruction they already
1599 > * produced for the parent turns avoid that redundant work entirely.
1600 > * Optional; agents without subagents omit it.
1601 > */
1602 > getSubagentSessions?(session: URI): Promise<readonly IRestoredSubagentSession[]>;
1603 >
1604 > /** Dispose a session, freeing resources. */
1605 > disposeSession(session: URI): Promise<void>;
1606 >
1607 > /**
1608 > * Release a session's in-memory resources (SDK session/connection, cached
1609 > * per-session state) without deleting any durable data. Unlike
1610 > * {@link disposeSession}, this is non-destructive: the on-disk session log,
1611 > * session database, and worktree are all preserved so the session can be
1612 > * transparently resumed later. Used by idle-session eviction to bound
1613 > * memory in long-lived host processes. Optional; providers that hold no
1614 > * releasable in-memory state simply omit it.
1615 > */
1616 > releaseSession?(session: URI): Promise<void>;
1617 >
1618 > /** Respond to a pending permission request from the SDK. */
1619 > respondToPermissionRequest(requestId: string, approved: boolean): void;
1620 >
1621 > /** Respond to a pending user input request from the SDK's ask_user tool. */
1622 > respondToUserInputRequest(requestId: string, response: ChatInputResponseKind, answers?: Record<string, ChatInputAnswer>): void;
1623 >
1624 > /** Return the descriptor for this agent. */
1625 > getDescriptor(): IAgentDescriptor;
1626 >
1627 > /** Available models from this provider. */
1628 > readonly models: IObservable<readonly IAgentModelInfo[]>;
1629 >
1630 > /**
1631 > * Re-enumerate this provider's model list and publish the result to
1632 > * {@link models}. Called both on provider-owned triggers (authentication,
1633 > * transport changes) and periodically by the host's model-refresh
1634 > * scheduler, so implementations MUST coalesce concurrent calls into a
1635 > * single backend request and MUST NOT reject: a failed refresh is logged
1636 > * and leaves the last known-good list in place.
1637 > *
1638 > * Optional so providers without a dynamic model catalog (mocks, test
1639 > * agents) need not implement it.
1640 > */
1641 > refreshModels?(): Promise<void>;
1642 >
1643 > /** List persisted sessions from this provider. */
1644 > listSessions(): Promise<IAgentSessionMetadata[]>;
1645 >
1646 > /** Retrieve metadata for a single persisted session, without enumerating the provider catalog. */
1647 > getSessionMetadata?(session: URI): Promise<IAgentSessionMetadata | undefined>;
1648 >
1649 > /** Declare protected resources this agent requires auth for (RFC 9728). */
1650 > getProtectedResources(): ProtectedResourceMetadata[];
1651 >
1652 > /**
1653 > * Endpoints this provider uses and recommends probing in network
1654 > * diagnostics. Optional.
1655 > */
1656 > getNetworkDiagnosticsEndpoints?(): Promise<readonly IAgentHostNetworkEndpoint[]>;
1657 >
1658 > /** Authenticated account name to display in network diagnostics, when known. */
1659 > getNetworkDiagnosticsAccount?(): Promise<string | undefined>;
1660 >
1661 > /** Resolve the provider's own effective enterprise managed-settings snapshot. */
1662 > getManagedSettingsDiagnostics?(): Promise<IAgentHostManagedSettingsSnapshot>;
1663 >
1664 > /**
1665 > * Fires when the agent's host-owned customizations change
1666 > * (loading state, resolution results, etc.), so infrastructure
1667 > * can republish {@link AgentInfo} and session customization state.
1668 > */
1669 > readonly onDidCustomizationsChange?: Event<void>;
1670 >
1671 > /**
1672 > * Fires when this agent needs the client to (re-)authenticate a
1673 > * protected resource — for example after a runtime transport-mode flip
1674 > * makes a previously-unneeded credential required. The host stamps the
1675 > * root channel and forwards it verbatim as an `auth/required`
1676 > * notification; clients respond via {@link authenticate}.
1677 > */
1678 > readonly onDidRequireAuth?: Event<Omit<AuthRequiredParams, 'channel'>>;
1679 >
1680 > /**
1681 > * Returns the host-owned customizations this agent currently exposes.
1682 > *
1683 > * Used to publish baseline customization metadata on {@link AgentInfo}.
1684 > * Always container customizations ({@link PluginCustomization} or
1685 > * {@link DirectoryCustomization}).
1686 > */
1687 > getCustomizations?(): readonly Customization[];
1688 >
1689 > /**
1690 > * Returns the effective customization list for a session, including
1691 > * source, enablement, and loading/error status.
1692 > */
1693 > getSessionCustomizations?(session: URI): Promise<readonly Customization[]>;
1694 >
1695 > /**
1696 > * Authenticate for a specific resource. Returns true if accepted.
1697 > * The `resource` matches {@link IAuthorizationProtectedResourceMetadata.resource}.
1698 > */
1699 > authenticate(resource: string, token: string): Promise<boolean>;
1700 >
1701 > /**
1702 > * Optional hook for provider-owned session resources that are not advertised
1703 > * as root agent protected resources, such as MCP server OAuth challenges.
1704 > */
1705 > handleAuthenticationToken?(params: AuthenticateParams): Promise<boolean>;
1706 >
1707 > /**
1708 > * Truncate a chat's history. If `turnId` is provided, keeps turns up to
1709 > * and including that turn. If omitted, all turns are removed.
1710 > *
1711 > * `chat` identifies which chat to truncate: the session's default chat
1712 > * (addressed by the session's default chat URI) or a peer (non-default)
1713 > * chat, which has its own backing.
1714 > *
1715 > * Optional — not all providers support truncation.
1716 > */
1717 > truncateSession?(session: URI, turnId: string | undefined, chat: URI): Promise<void>;
1718 >
1719 > /**
1720 > * Notifies the provider that a session's archived state has changed.
1721 > * Providers may use this to clean up or restore per-session resources
1722 > * (for example, removing a session-owned worktree on archive and
1723 > * recreating it on unarchive). Optional.
1724 > */
1725 > onArchivedChanged?(session: URI, isArchived: boolean): Promise<void>;
1726 >
1727 > /**
1728 > * Notifies the provider that a **client** (user) changed this session's
1729 > * config — e.g. via an approvals/model picker. `values` is the post-reducer
1730 > * merged config. Lets the provider propagate a session-mutable change (such
1731 > * as Claude's `permissionMode`) to a running SDK mid-turn. Fires only for
1732 > * client-originated changes; internal server-side config writes (e.g. a tool
1733 > * persisting a mode) do NOT trigger it, so a provider can forward freely
1734 > * without re-entering its own SDK callbacks. Optional.
1735 > */
1736 > onSessionConfigChanged?(session: URI, values: Record<string, unknown>): void;
1737 >
1738 > /**
1739 > * Get (or lazily create) the per-session handle for an active client,
1740 > * identified by `clientId`. Mutating the returned {@link IActiveClient}'s
1741 > * `tools` / `customizations` updates only that client's contribution; the
1742 > * agent merges the contributions of all active clients when exposing them
1743 > * to the model. A session MAY have several active clients at once.
1744 > *
1745 > * @param session The session URI this client contributes to.
1746 > * @param client The client's `clientId` and optional human-readable name.
1747 > */
1748 > getOrCreateActiveClient(session: URI, client: { readonly clientId: string; readonly displayName?: string }): IActiveClient;
1749 >
1750 > /**
1751 > * Remove an active client from a session, clearing its tool and
1752 > * customization contributions. No-op when no active client matches
1753 > * `clientId`.
1754 > *
1755 > * @param session The session the client is leaving.
1756 > * @param clientId The client to remove.
1757 > */
1758 > removeActiveClient(session: URI, clientId: string): void;
1759 >
1760 > /**
1761 > * Called when a client completes a client-provided tool call.
1762 > * Resolves the tool handler's deferred promise so the SDK can continue.
1763 > *
1764 > * @param session The session the tool call belongs to.
1765 > * @param chat The chat channel the tool call was issued on, when known.
1766 > * Agents that track peer chats separately from the default chat (e.g.
1767 > * copilot) use this to route the completion to the right chat;
1768 > * agents without peer chats ignore it and resolve by `session`.
1769 > * @param toolCallId The id of the tool call being completed.
1770 > * @param result The result of the tool call.
1771 > */
1772 > onClientToolCallComplete(session: URI, chat: URI, toolCallId: string, result: ToolCallResult): void;
1773 >
1774 > /** Request a session MCP server start/restart by customization id. */
1775 > startMcpServer?(session: URI, id: string): Promise<void>;
1776 >
1777 > /** Request a session MCP server stop by customization id. */
1778 > stopMcpServer?(session: URI, id: string): Promise<void>;
1779 >
1780 > /** Gracefully shut down all sessions. */
1781 > shutdown(): Promise<void>;
1782 >
1783 > /**
1784 > * Routes a request received on an `mcp://` side channel to the agent's
1785 > * MCP server implementation. The channel carries raw MCP JSON-RPC
1786 > * methods (e.g. `tools/list`, `tools/call`, `resources/read`) tagged
1787 > * with the routing envelope; the protocol server decodes the envelope
1788 > * and forwards `(session, serverName, method, params)` here.
1789 > *
1790 > * The agent MUST reject unknown methods with an error whose message
1791 > * begins with `Method not found` so the protocol server can map it to
1792 > * a JSON-RPC `-32601`.
1793 > *
1794 > * Optional — agents that don't surface any MCP servers (or don't
1795 > * advertise `mcpApp` capabilities) can omit this.
1796 > */
1797 > handleMcpRequest?(session: URI, serverName: string, method: string, params: Record<string, unknown> | undefined): Promise<unknown>;
1798 >
1799 > /**
1800 > * Fires when an MCP server owned by this agent emits a notification
1801 > * that should be forwarded to AHP clients over the `mcp://` side
1802 > * channel. Today this is exclusively
1803 > * `notifications/tools/list_changed` and
1804 > * `notifications/resources/list_changed`. The protocol server
1805 > * fans the notification out to every connected client.
1806 > *
1807 > * Optional — agents that don't expose MCP servers can omit this.
1808 > */
1809 > readonly onMcpNotification?: Event<IMcpNotification>;
1810 >
1811 > /** Dispose this provider and all its resources. */
1812 > dispose(): void;
1813 > }
1814 >
1815 > // ---- Service interfaces -----------------------------------------------------
1816 >
1817 > export const IAgentService = createDecorator<IAgentService>('agentService');
1818 >
1819 > /**
1820 > * Service contract for communicating with the agent host process. Methods here
1821 > * are proxied across MessagePort via `ProxyChannel`.
1822 > *
1823 > * State is synchronized via the subscribe/unsubscribe/dispatchAction protocol.
1824 > * Clients observe root state (agents, models) and session state via subscriptions,
1825 > * and mutate state by dispatching actions (e.g. session/turnStarted, session/turnCancelled).
1826 > */
1827 > export interface IAgentService {
1828 > readonly _serviceBrand: undefined;
1829 >
1830 > /**
1831 > * Authenticate for a protected resource on the server.
1832 > * The {@link AuthenticateParams.resource} must match a resource from
1833 > * the agent's protectedResources in root state. Analogous to RFC 6750
1834 > * bearer token delivery.
1835 > */
1836 > authenticate(params: AuthenticateParams): Promise<AuthenticateResult>;
1837 >
1838 > /** Return a bearer token previously supplied via {@link authenticate}. */
1839 > getAuthToken(request: IAgentHostAuthTokenRequest): string | undefined;
1840 >
1841 > /** List all available sessions from the Copilot CLI. */
1842 > listSessions(): Promise<IAgentSessionMetadata[]>;
1843 >
1844 > /** Create a new session. Returns the session URI. */
1845 > createSession(config?: IAgentCreateSessionConfig): Promise<URI>;
1846 >
1847 > /**
1848 > * Create an additional chat within an existing session. Spins up the
1849 > * backing chat in the harness (sharing the session's session) and
1850 > * registers the chat in the session's catalog so subscribers observe a
1851 > * `session/chatAdded` action. The `chat` URI is the client-chosen channel.
1852 > */
1853 > createChat(session: URI, chat: URI, options?: IAgentCreateChatOptions): Promise<void>;
1854 >
1855 > /** Dispose an additional chat created via {@link createChat}. */
1856 > disposeChat(session: URI, chat: URI): Promise<void>;
1857 >
1858 > /** Resolve the dynamic configuration schema for creating a session. */
1859 > resolveSessionConfig(params: IAgentResolveSessionConfigParams): Promise<ResolveSessionConfigResult>;
1860 >
1861 > /** Return dynamic completions for a session configuration property. */
1862 > sessionConfigCompletions(params: IAgentSessionConfigCompletionsParams): Promise<SessionConfigCompletionsResult>;
1863 >
1864 > /**
1865 > * Return completion items for a partially-typed input (e.g. an `@`-mention
1866 > * inside a user message the user is composing). Delegates to a pluggable
1867 > * set of {@link IAgentHostCompletionItemProvider}s registered with the
1868 > * agent host.
1869 > *
1870 > * Note: this method does not accept a {@link CancellationToken} because
1871 > * `CancellationToken`s do not round-trip through the IPC boundary today
1872 > * (the deserialised value lacks the prototype methods used by
1873 > * subscribers). Callers that need cancellation should race the returned
1874 > * promise on their own side.
1875 > */
1876 > completions(params: CompletionsParams): Promise<CompletionsResult>;
1877 >
1878 > /**
1879 > * Returns the set of characters that, when typed in a {@link UserMessage}
1880 > * input, SHOULD cause the client to issue a `completions` request.
1881 > * Aggregated from every registered {@link IAgentHostCompletionItemProvider}.
1882 > */
1883 > getCompletionTriggerCharacters(): Promise<readonly string[]>;
1884 >
1885 > /** Dispose a session in the agent host, freeing SDK resources. */
1886 > disposeSession(session: URI): Promise<void>;
1887 >
1888 > /** Create a new terminal on the agent host. */
1889 > createTerminal(params: CreateTerminalParams): Promise<void>;
1890 >
1891 > /** Dispose a terminal and kill its process if still running. */
1892 > disposeTerminal(terminal: URI): Promise<void>;
1893 >
1894 > /** Invoke a server-defined changeset operation. */
1895 > invokeChangesetOperation(params: InvokeChangesetOperationParams): Promise<InvokeChangesetOperationResult>;
1896 >
1897 > /**
1898 > * Routes a request received on an `mcp://` AHP side channel to the
1899 > * MCP server implementation owned by the appropriate agent. The
1900 > * channel URI shape is `mcp://<providerId>/<sessionId>/<serverName>`
1901 > * (the latter two segments URL-encoded), matching the
1902 > * {@link McpServerCustomization.channel | channel} the agent host
1903 > * advertises while the server is in
1904 > * {@link McpServerStatus.Ready | `Ready`}.
1905 > *
1906 > * `method` is the raw MCP JSON-RPC method (e.g. `tools/list`,
1907 > * `tools/call`, `resources/read`); `params` are the JSON-RPC params
1908 > * (still carrying the routing envelope's `channel` field, which the
1909 > * agent may ignore). Rejects with an `Error` whose message begins
1910 > * with `Method not found` when the channel is unknown or the agent
1911 > * doesn't recognise the method — the protocol server translates that
1912 > * into a JSON-RPC `-32601`.
1913 > */
1914 > handleMcpRequest(channel: string, method: string, params: Record<string, unknown> | undefined): Promise<unknown>;
1915 >
1916 > /**
1917 > * Aggregated stream of MCP notifications across every agent. The
1918 > * protocol server subscribes once and broadcasts each notification as
1919 > * a JSON-RPC notification to all connected clients (the routing
1920 > * envelope's `channel` field is sufficient for client-side dispatch,
1921 > * so no per-subscription fanout is required).
1922 > */
1923 > readonly onMcpNotification: Event<IMcpNotification>;
1924 >
1925 > /** Gracefully shut down all sessions and the underlying client. */
1926 > shutdown(): Promise<void>;
1927 >
1928 > /**
1929 > * Host-level network context for diagnostics — agent host version, OS/arch,
1930 > * account, proxy settings/env, and the endpoints worth probing (which
1931 > * callers probe via {@link diagnosticsFetch}, plus any additional URLs).
1932 > */
1933 > getNetworkDiagnosticsInfo(): Promise<IAgentHostNetworkDiagnosticsInfo>;
1934 >
1935 > /** Resolve managed settings through each provider's native SDK/runtime implementation. */
1936 > getManagedSettingsDiagnostics(): Promise<readonly IAgentHostManagedSettingsDiagnostics[]>;
1937 >
1938 > /**
1939 > * Probe connectivity from the agent host process to a single `url`,
1940 > * resolving the proxy and timing DNS + reachability. Used by the "Network
1941 > * Diagnostics" developer command.
1942 > */
1943 > diagnosticsFetch(url: string): Promise<IAgentHostNetworkFetchResult>;
1944 >
1945 > // ---- Protocol methods (sessions process protocol) ----------------------
1946 >
1947 > /**
1948 > * Subscribe to state at the given URI. Returns a snapshot of the current
1949 > * state and the serverSeq at snapshot time. Subsequent actions for this
1950 > * resource arrive via {@link onDidAction}. Registers `clientId` against
1951 > * the resource so the server-side refcount knows who is watching, so the
1952 > * caller does not need to invoke {@link addSubscriber} separately. Pair
1953 > * with {@link unsubscribe} when the subscription is released.
1954 > */
1955 > subscribe(resource: URI, clientId: string): Promise<IStateSnapshot>;
1956 >
1957 > /**
1958 > * Counterpart to {@link subscribe}. Drops `clientId` from the refcount
1959 > * for `resource`; when the last subscriber is removed, idle session state
1960 > * for `resource` may be evicted from the server.
1961 > */
1962 > unsubscribe(resource: URI, clientId: string): void;
1963 >
1964 > /**
1965 > * Register `clientId` against `resource` without going through
1966 > * {@link subscribe}. Only needed by callers that hand out snapshots
1967 > * synchronously (e.g. the JSON-RPC handshake serving `initialSubscriptions`
1968 > * out of the in-memory state cache); regular subscribers should call
1969 > * {@link subscribe} instead. Counterpart cleanup is {@link unsubscribe}.
1970 > */
1971 > addSubscriber(resource: URI, clientId: string): void;
1972 >
1973 > /**
1974 > * Fires when the server applies an action to subscribable state.
1975 > * Clients use this alongside {@link subscribe} to keep their local
1976 > * state in sync.
1977 > */
1978 > readonly onDidAction: Event<ActionEnvelope>;
1979 >
1980 > /**
1981 > * Fires when the server broadcasts an ephemeral notification
1982 > * (e.g. sessionAdded, sessionRemoved).
1983 > */
1984 > readonly onDidNotification: Event<INotification>;
1985 >
1986 > /**
1987 > * Dispatch a client-originated action to the server. The server applies
1988 > * it to state, triggers side effects, and echoes it back via
1989 > * {@link onDidAction} with the client's origin for reconciliation.
1990 > *
1991 > * `channel` is the protocol URI string identifying the channel the action
1992 > * targets (a session URI for session actions, terminal URI for terminal
1993 > * actions, or {@link ROOT_STATE_URI} for root actions). Strings are used
1994 > * rather than {@link URI} objects so that authority-less scheme URIs
1995 > * like `ahp-root://` survive the wire format without normalization.
1996 > */
1997 > dispatchAction(channel: string, action: SessionAction | ChatAction | TerminalAction | ClientChangesetAction | ClientAnnotationsAction | IRootConfigChangedAction, clientId: string, clientSeq: number): void;
1998 >
1999 > /**
2000 > * List the contents of a directory on the agent host's filesystem.
2001 > * Used by the client to drive a remote folder picker before session creation.
2002 > */
2003 > resourceList(uri: URI): Promise<ResourceListResult>;
2004 >
2005 > /**
2006 > * Read stored content by URI from the agent host (e.g. file edit snapshots,
2007 > * or reading files from the remote filesystem).
2008 > */
2009 > resourceRead(uri: URI): Promise<ResourceReadResult>;
2010 >
2011 > /**
2012 > * Write content to a file on the agent host's filesystem.
2013 > * Used for undo/redo operations on file edits.
2014 > */
2015 > resourceWrite(params: ResourceWriteParams): Promise<ResourceWriteResult>;
2016 >
2017 > /**
2018 > * Copy a resource from one URI to another on the agent host's filesystem.
2019 > */
2020 > resourceCopy(params: ResourceCopyParams): Promise<ResourceCopyResult>;
2021 >
2022 > /**
2023 > * Delete a resource at a URI on the agent host's filesystem.
2024 > */
2025 > resourceDelete(params: ResourceDeleteParams): Promise<ResourceDeleteResult>;
2026 >
2027 > /**
2028 > * Move (rename) a resource from one URI to another on the agent host's filesystem.
2029 > */
2030 > resourceMove(params: ResourceMoveParams): Promise<ResourceMoveResult>;
2031 >
2032 > /**
2033 > * Resolve a resource (stat + realpath) on the agent host's filesystem.
2034 > */
2035 > resourceResolve(params: ResourceResolveParams): Promise<ResourceResolveResult>;
2036 >
2037 > /**
2038 > * Create a directory (mkdir -p semantics) on the agent host's filesystem.
2039 > */
2040 > resourceMkdir(params: ResourceMkdirParams): Promise<ResourceMkdirResult>;
2041 >
2042 > /**
2043 > * Create a resource watcher on the agent host's filesystem. Returns the
2044 > * `ahp-resource-watch:/<id>` channel URI the caller subscribes to in
2045 > * order to receive `resourceWatch/changed` events. The watcher is
2046 > * tied to the subscriber refcount on that channel — the implementation
2047 > * MUST hold the underlying file-system watcher for a short grace
2048 > * period after the last unsubscribe so reconnects don't drop events.
2049 > */
2050 > createResourceWatch(params: CreateResourceWatchParams): Promise<CreateResourceWatchResult>;
2051 >
2052 > /**
2053 > * Notify the agent service that a client subscribed to the given
2054 > * `ahp-resource-watch:` channel so the per-watch refcount is bumped
2055 > * (and the underlying {@link IFileService} watcher attached on the
2056 > * first subscriber). Returns the decoded watch descriptor when the
2057 > * channel parses successfully and the watcher is live; returns
2058 > * `undefined` for unknown channels so the caller can surface a
2059 > * not-found error.
2060 > */
2061 > onResourceWatchSubscribed(channel: string): ResourceWatchState | undefined;
2062 >
2063 > /**
2064 > * Counterpart to {@link onResourceWatchSubscribed}. Decrements the
2065 > * per-watch refcount; on the last drop the watcher is held for a
2066 > * short grace period before disposal.
2067 > */
2068 > onResourceWatchUnsubscribed(channel: string): boolean;
2069 > }
2070 >
2071 > /**
2072 > * Consumer-facing connection to an agent host. Session handlers, terminal
2073 > * contributions, and other features program against this interface.
2074 > *
2075 > * Implementations wrap an {@link IAgentService} and layer subscription
2076 > * management and optimistic write-ahead on top.
2077 > */
2078 > export interface IAgentConnection {
2079 >
2080 > readonly clientId: string;
2081 >
2082 > // ---- State subscriptions ------------------------------------------------
2083 > readonly rootState: IAgentSubscription<RootState>;
2084 > /**
2085 > * Acquire a refcounted subscription to `resource`. `owner` names the
2086 > * caller holding the reference so inspection surfaces can attribute who
2087 > * is retaining a subscription; use a stable identifier such as the
2088 > * acquiring class name.
2089 > */
2090 > getSubscription<T extends StateComponents>(kind: T, resource: URI, owner: string): IReference<IAgentSubscription<ComponentToState[T]>>;
2091 > getSubscriptionUnmanaged<T extends StateComponents>(kind: T, resource: URI): IAgentSubscription<ComponentToState[T]> | undefined;
2092 >
2093 > /**
2094 > * Returns the in-flight `createSession` Promise for `resource`, or `undefined` if no create is pending. Callers
2095 > * that need to gate work on a racing eager `createSession` (e.g. before deciding whether to fall through to a
2096 > * duplicate create) should await this first.
2097 > */
2098 > getInflightSessionCreate(resource: URI): Promise<unknown> | undefined;
2099 >
2100 > /**
2101 > * Read-only descriptors of every active resource subscription on this
2102 > * connection, for inspection/debug surfaces. Excludes the always-live
2103 > * {@link rootState}.
2104 > */
2105 > getActiveSubscriptions(): readonly IActiveSubscriptionInfo[];
2106 >
2107 > // ---- Action dispatch ----------------------------------------------------
2108 > /**
2109 > * Dispatch a client-originated action. `channel` is the protocol URI
2110 > * string identifying the channel the action targets (a session URI for
2111 > * session actions, terminal URI for terminal actions, or
2112 > * `ROOT_STATE_URI` for root-config actions). Strings are used rather
2113 > * than {@link URI} objects so authority-less scheme URIs like
2114 > * `ahp-root://` survive the wire format without normalization.
2115 > */
2116 > dispatch(channel: string, action: SessionAction | ChatAction | TerminalAction | ClientChangesetAction | ClientAnnotationsAction | IRootConfigChangedAction): void;
2117 >
2118 > // ---- Events (connection-level) ------------------------------------------
2119 > readonly onDidNotification: Event<INotification>;
2120 > readonly onDidAction: Event<ActionEnvelope>;
2121 > /**
2122 > * Fires when the host forwards an MCP server notification (e.g.
2123 > * `notifications/tools/list_changed`) over the `mcp://` side channel.
2124 > * The `channel` field on the notification routes the payload to the
2125 > * matching {@link McpServerCustomization}.
2126 > */
2127 > readonly onMcpNotification: Event<IMcpNotification>;
2128 >
2129 > // ---- MCP side-channel ---------------------------------------------------
2130 > /**
2131 > * Send a request on an `mcp://` AHP side channel. `channel` is the
2132 > * `mcp://` URI advertised by the matching {@link McpServerCustomization}
2133 > * (only available while the server is `ready`). `method` is the raw MCP
2134 > * JSON-RPC method (e.g. `tools/call`, `resources/read`,
2135 > * `sampling/createMessage`); `params` are the JSON-RPC params (the
2136 > * connection adds the routing envelope's `channel` field automatically).
2137 > *
2138 > * Rejects with an `Error` whose message begins with `Method not found`
2139 > * when the channel is unknown or the host doesn't recognise the method.
2140 > */
2141 > handleMcpRequest(channel: string, method: string, params: Record<string, unknown> | undefined): Promise<unknown>;
2142 >
2143 > // ---- Session lifecycle --------------------------------------------------
2144 > authenticate(params: AuthenticateParams): Promise<AuthenticateResult>;
2145 > listSessions(): Promise<IAgentSessionMetadata[]>;
2146 > createSession(config?: IAgentCreateSessionConfig): Promise<URI>;
2147 > resolveSessionConfig(params: IAgentResolveSessionConfigParams): Promise<ResolveSessionConfigResult>;
2148 > sessionConfigCompletions(params: IAgentSessionConfigCompletionsParams): Promise<SessionConfigCompletionsResult>;
2149 > completions(params: CompletionsParams): Promise<CompletionsResult>;
2150 >
2151 > /**
2152 > * Trigger characters announced by the connected agent host that should
2153 > * cause the client to issue a `completions` request when typed in a
2154 > * user-message input. Resolves once on first request and is cached.
2155 > */
2156 > getCompletionTriggerCharacters(): Promise<readonly string[]>;
2157 >
2158 > /**
2159 > * The host's `initialize` handshake result, exposed observably so callers
2160 > * can derive advertised capabilities (e.g. {@link InitializeResult.terminalCommandPrefix},
2161 > * {@link InitializeResult.completionTriggerCharacters}). `undefined` until
2162 > * the handshake completes.
2163 > */
2164 > readonly initializeResult: IObservable<InitializeResult | undefined>;
2165 > disposeSession(session: URI): Promise<void>;
2166 >
2167 > /**
2168 > * Host-level network context for diagnostics (version, OS/arch, account,
2169 > * proxy settings/env, endpoints). Runs on the agent host process (local or
2170 > * remote), so the result reflects the environment the Copilot SDK actually
2171 > * runs in.
2172 > */
2173 > getNetworkDiagnosticsInfo(): Promise<IAgentHostNetworkDiagnosticsInfo>;
2174 >
2175 > /** Resolve managed settings through each provider's native SDK/runtime implementation. */
2176 > getManagedSettingsDiagnostics(): Promise<readonly IAgentHostManagedSettingsDiagnostics[]>;
2177 >
2178 > /**
2179 > * Probe connectivity from the agent host to a single `url`. Runs on the
2180 > * agent host process (local or remote), so the result reflects the
2181 > * environment the Copilot SDK actually runs in.
2182 > */
2183 > diagnosticsFetch(url: string): Promise<IAgentHostNetworkFetchResult>;
2184 >
2185 > /**
2186 > * Create an additional peer chat inside an existing session. `chat` is a
2187 > * client-chosen chat URI (see {@link buildChatUri}). The host adds the
2188 > * chat to the session's catalog and publishes `session/chatAdded`.
2189 > */
2190 > createChat(session: URI, chat: URI, options?: IAgentCreateChatOptions): Promise<void>;
2191 > /** Dispose an additional chat created via {@link createChat}. */
2192 > disposeChat(chat: URI): Promise<void>;
2193 >
2194 > // ---- Terminal lifecycle -------------------------------------------------
2195 > createTerminal(params: CreateTerminalParams): Promise<void>;
2196 > disposeTerminal(terminal: URI): Promise<void>;
2197 >
2198 > // ---- Changeset operations -----------------------------------------------
2199 > invokeChangesetOperation(params: InvokeChangesetOperationParams): Promise<InvokeChangesetOperationResult>;
2200 >
2201 > // ---- Filesystem operations ----------------------------------------------
2202 > resourceList(uri: URI): Promise<ResourceListResult>;
2203 > resourceRead(uri: URI): Promise<ResourceReadResult>;
2204 > resourceWrite(params: ResourceWriteParams): Promise<ResourceWriteResult>;
2205 > resourceCopy(params: ResourceCopyParams): Promise<ResourceCopyResult>;
2206 > resourceDelete(params: ResourceDeleteParams): Promise<ResourceDeleteResult>;
2207 > resourceMove(params: ResourceMoveParams): Promise<ResourceMoveResult>;
2208 > resourceResolve(params: ResourceResolveParams): Promise<ResourceResolveResult>;
2209 > resourceMkdir(params: ResourceMkdirParams): Promise<ResourceMkdirResult>;
2210 > createResourceWatch(params: CreateResourceWatchParams): Promise<CreateResourceWatchResult>;
2211 > /**
2212 > * Convenience method that bundles
2213 > * {@link createResourceWatch} + {@link subscribe} + a typed
2214 > * {@link IFileChange}[] event stream, so consumers (notably
2215 > * `AHPFileSystemProvider.watch`) can drive a watcher without
2216 > * understanding the underlying channel protocol. Disposing the
2217 > * returned handle unsubscribes.
2218 > */
2219 > watchResource(params: CreateResourceWatchParams): Promise<IRemoteWatchHandle>;
2220 > }
2221 >
2222 > export const IAgentHostService = createDecorator<IAgentHostService>('agentHostService');
2223 >
2224 > /**
2225 > * The local wrapper around the agent host process (manages lifecycle, restart,
2226 > * exposes the proxied service). Consumed by the main process and workbench.
2227 > */
2228 > export interface IAgentHostService extends IAgentConnection {
2229 >
2230 > readonly _serviceBrand: undefined;
2231 >
2232 > readonly onAgentHostExit: Event<number>;
2233 > readonly onAgentHostStart: Event<void>;
2234 >
2235 > /**
2236 > * `true` while we are in the middle of authenticating against the local
2237 > * agent host (resolving tokens for any advertised `protectedResources` and
2238 > * pushing them via {@link authenticate}). Defaults to `true` at startup so
2239 > * that the period before the first auth pass is also covered.
2240 > *
2241 > * Producers (the workbench `AgentHostContribution`) flip this around their
2242 > * auth pass; consumers (e.g. the local sessions provider) read it to mark
2243 > * sessions as still loading.
2244 > */
2245 > readonly authenticationPending: IObservable<boolean>;
2246 >
2247 > /** Update {@link authenticationPending}. Internal — only the auth driver should call this. */
2248 > setAuthenticationPending(pending: boolean): void;
2249 >
2250 > restartAgentHost(): Promise<void>;
2251 >
2252 > startWebSocketServer(): Promise<IAgentHostSocketInfo>;
2253 >
2254 > /**
2255 > * Get inspector listener info for the agent host process. If the inspector
2256 > * is not currently active and `tryEnable` is true, opens the inspector on
2257 > * a random local port. Returns `undefined` if the inspector cannot be
2258 > * enabled.
2259 > */
2260 > getInspectInfo(tryEnable: boolean): Promise<IAgentHostInspectInfo | undefined>;
2261 > }
src/vs/platform/agentHost/common/state/protocol/channels-chat/state.ts 1557 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- state.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > import type { ModelSelection } from '../channels-root/state.js';
10 > import type { AgentSelection, McpAuthRequirement, SessionStatus } from '../channels-session/state.js';
11 > import type { ContentRef, ErrorInfo, FileEdit, StringOrMarkdown, TextRange, TextSelection, URI, UsageInfo } from '../common/state.js';
12 >
13 > // ─── Chat State ──────────────────────────────────────────────────────────────
14 >
15 > /**
16 > * Full state for a single chat, loaded when a client subscribes to the chat's
17 > * URI.
18 > *
19 > * The lightweight catalog representation of a chat is {@link ChatSummary},
20 > * carried in {@link SessionState.chats | `SessionState.chats`}. `ChatState`
21 > * **denormalizes** every {@link ChatSummary} field directly onto itself so
22 > * subscribers receive one flat object instead of having to merge a nested
23 > * `summary` sub-object. Producers MUST keep the two representations
24 > * consistent: any change to the inlined fields below SHOULD also be
25 > * announced on the parent session via the matching
26 > * {@link SessionChatUpdatedAction | `session/chatUpdated`} action.
27 > *
28 > * @category Chat State
29 > */
30 > export interface ChatState {
31 > // ── Summary fields (denormalized from ChatSummary) ─────────────────
32 > /** Chat URI */
33 > resource: URI;
34 > /** Chat title */
35 > title: string;
36 > /** Current chat status (reuses SessionStatus shape) */
37 > status: SessionStatus;
38 > /** Human-readable description of what the chat is currently doing */
39 > activity?: string;
40 > /** Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) */
41 > modifiedAt: string;
42 > /** How this chat came into existence */
43 > origin?: ChatOrigin;
44 > /**
45 > * How the user can interact with this chat. See {@link ChatInteractivity}.
46 > *
47 > * Supports agent-team patterns where worker chats are read-only or hidden.
48 > * Absence defaults to {@link ChatInteractivity.Full} for backward
49 > * compatibility.
50 > */
51 > interactivity?: ChatInteractivity;
52 > /**
53 > * The subset of the session's
54 > * {@link SessionState.workingDirectories | `workingDirectories`} that this
55 > * chat's agent has tool access to. Every entry MUST be present in the owning
56 > * session's `workingDirectories`; servers MUST reject a
57 > * `chat/workingDirectorySet` action that violates this constraint.
58 > *
59 > * When absent, the chat inherits the full session set. When present but empty
60 > * (not recommended), the chat has no working-directory tool access at all.
61 > *
62 > * Dispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to
63 > * update the subset on a running chat.
64 > */
65 > workingDirectories?: URI[];
66 > /**
67 > * The chat's primary working directory — the distinguished root this chat is
68 > * centered on (e.g. the agent's process root for this chat, the default
69 > * location for relative paths). MUST be one of this chat's effective working
70 > * directories ({@link workingDirectories}, or the session's set when that is
71 > * absent). Present when the agent advertises
72 > * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}.
73 > *
74 > * **Read-only and fixed at creation.** It is set from
75 > * {@link CreateChatParams.primaryWorkingDirectory} (or, for the session's
76 > * default chat, {@link CreateSessionParams.primaryWorkingDirectory}) and does
77 > * not change over the chat's lifetime — there is no action to mutate it, and
78 > * it does not participate in `session/chatUpdated`.
79 > */
80 > primaryWorkingDirectory?: URI;
81 >
82 > // ── Conversation contents ──────────────────────────────────────────
83 > /** Completed turns */
84 > turns: Turn[];
85 > /**
86 > * Cursor for loading older completed turns into this chat state.
87 > *
88 > * Presence means `turns` is a tail window and more historical turns are
89 > * available. Pass this opaque cursor to `fetchTurns`; the host MUST insert
90 > * the loaded turns into state and update or clear this cursor before
91 > * responding. Absence means the state contains all retained turns.
92 > */
93 > turnsNextCursor?: string;
94 > /** Currently in-progress turn */
95 > activeTurn?: ActiveTurn;
96 > /** Message to inject into the current turn at a convenient point */
97 > steeringMessage?: PendingMessage;
98 > /** Messages to send automatically as new turns after the current turn finishes */
99 > queuedMessages?: PendingMessage[];
100 > /**
101 > * The user's in-progress draft input for this chat — the message they are
102 > * composing but have not sent yet, including its
103 > * {@link Message.model | model} / {@link Message.agent | agent} selection
104 > * and attachments.
105 > *
106 > * Clients MAY periodically sync their local input state into this field so
107 > * a draft survives reloads and is visible to other clients viewing the same
108 > * chat. Eager syncing is **not** required — clients SHOULD debounce and MAY
109 > * sync only at convenient points. When presenting input UI for an existing
110 > * chat, clients SHOULD use any `draft` to initialize their input state.
111 > * Cleared (set to `undefined`) once the message is sent.
112 > */
113 > draft?: Message;
114 > /**
115 > * Additional provider-specific metadata for this chat.
116 > */
117 > _meta?: Record<string, unknown>;
118 > }
119 >
120 > /**
121 > * Lightweight catalog entry for a chat, carried in
122 > * {@link SessionState.chats | `SessionState.chats`}. The full conversation
123 > * lives in {@link ChatState}, which inlines (denormalizes) every field below.
124 > *
125 > * @category Chat State
126 > */
127 > export interface ChatSummary {
128 > /** Chat URI */
129 > resource: URI;
130 > /** Chat title */
131 > title: string;
132 > /** Current chat status (reuses SessionStatus shape) */
133 > status: SessionStatus;
134 > /** Human-readable description of what the chat is currently doing */
135 > activity?: string;
136 > /** Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) */
137 > modifiedAt: string;
138 > /** How this chat came into existence */
139 > origin?: ChatOrigin;
140 > /**
141 > * How the user can interact with this chat. See {@link ChatInteractivity}.
142 > *
143 > * Supports agent-team patterns where worker chats are read-only or hidden.
144 > * Absence defaults to {@link ChatInteractivity.Full} for backward
145 > * compatibility.
146 > */
147 > interactivity?: ChatInteractivity;
148 > /**
149 > * The subset of the session's working directories this chat uses.
150 > * See {@link ChatState.workingDirectories} for the full semantics.
151 > */
152 > workingDirectories?: URI[];
153 > /**
154 > * The chat's primary working directory.
155 > * See {@link ChatState.primaryWorkingDirectory} for the full semantics.
156 > */
157 > primaryWorkingDirectory?: URI;
158 > }
159 >
160 > /**
161 > * Discriminant for {@link ChatOrigin} — how a chat came into existence.
162 > *
163 > * @category Chat State
164 > */
165 > export const enum ChatOriginKind {
166 > /** User created the chat explicitly (e.g. via the host UI). */
167 > User = 'user',
168 > /** Forked from an existing chat at a specific turn. */
169 > Fork = 'fork',
170 > /** Created as an independent side conversation from a specific turn. */
171 > SideChat = 'sideChat',
172 > /** Spawned by a tool call running in another chat (e.g. a sub-agent delegation). */
173 > Tool = 'tool',
174 > }
175 >
176 > /**
177 > * Immutable selected-text snapshot captured when a side chat is created.
178 > *
179 > * The host records this exact text when it accepts `createChat`; later changes
180 > * to the source chat do not alter it.
181 > *
182 > * @category Chat State
183 > */
184 > export interface SideChatSelection {
185 > /**
186 > * Exact selected-text snapshot captured at `createChat` acceptance.
187 > *
188 > * MUST be non-empty.
189 > */
190 > text: string;
191 > /**
192 > * Optional provenance for the response part that contained {@link text} when
193 > * the host took the snapshot.
194 > *
195 > * Advisory only: this is not a live range or offset and MUST NOT be used to
196 > * recompute `text`.
197 > */
198 > responsePartId?: string;
199 > }
200 >
201 > /**
202 > * How a chat came into existence. Clients MAY use it to render
203 > * contextual UI (parent indicators, fork markers, "spawned by tool" badges).
204 > *
205 > * Fork and side-chat origins both carry a stable top-level `turnId` alongside
206 > * their discriminated `kind` value instead of snapshotting whether that turn
207 > * was active or historical at creation time. Consumers resolve the identifier
208 > * against the
209 > * source chat's current `activeTurn` or retained `turns` as needed.
210 > *
211 > * When a host accepts side-chat creation from the source chat's current active
212 > * turn, it snapshots the retained history plus that turn's current user
213 > * message and any partial assistant response already available. Later
214 > * source-turn deltas do not retroactively change the created side chat's
215 > * starting context, and once the source turn completes it is still referenced
216 > * by the same `turnId`. Side-chat origins MAY also retain an immutable
217 > * {@link SideChatSelection | selected-text snapshot} captured at acceptance
218 > * time; any `responsePartId` there is provenance only, not a range.
219 > *
220 > * The `tool` variant records a tool-spawned worker from the worker's side: its
221 > * `chat`/`toolCallId` identify the spawning tool call in the parent chat. This
222 > * is the canonical record of the spawn relationship. The same edge is surfaced
223 > * from the parent's side by {@link ToolResultSubagentContent}, whose `resource`
224 > * is this chat's URI; hosts MUST keep the two consistent.
225 > *
226 > * @category Chat State
227 > */
228 > export type ChatOrigin =
229 > | { kind: ChatOriginKind.User }
230 > | { kind: ChatOriginKind.Fork; chat: URI; turnId: string }
231 > | { kind: ChatOriginKind.SideChat; chat: URI; turnId: string; selection?: SideChatSelection }
232 > | { kind: ChatOriginKind.Tool; chat: URI; toolCallId: string };
233 >
234 > /**
235 > * How a user can interact with a chat.
236 > *
237 > * - `Full` — user can send messages and watch (default when absent)
238 > * - `ReadOnly` — user can watch but not send messages (e.g. agent team workers)
239 > * - `Hidden` — internal worker not shown in UI at all
240 > *
241 > * Supports the agent-team pattern where a lead chat is fully interactive and
242 > * worker chats are read-only (visible for observability) or hidden (internal
243 > * implementation detail). The harness sets this based on the chat's role;
244 > * the UI uses it to show appropriate controls.
245 > *
246 > * @category Chat State
247 > */
248 > export const enum ChatInteractivity {
249 > /** User can send messages and watch (default when absent) */
250 > Full = 'full',
251 > /** User can watch but not send messages */
252 > ReadOnly = 'read-only',
253 > /** Internal worker not shown in UI at all */
254 > Hidden = 'hidden',
255 > }
256 >
257 > // ─── Pending Message Types ───────────────────────────────────────────────────
258 >
259 > /**
260 > * Discriminant for pending message kinds.
261 > *
262 > * @category Pending Message Types
263 > */
264 > export const enum PendingMessageKind {
265 > /** Injected into the current turn at a convenient point */
266 > Steering = 'steering',
267 > /** Sent automatically as a new turn after the current turn finishes */
268 > Queued = 'queued',
269 > }
270 >
271 > /**
272 > * A message queued for future delivery to the agent.
273 > *
274 > * Steering messages are injected into the current turn mid-flight.
275 > * Queued messages are automatically started as new turns after the
276 > * current turn naturally finishes.
277 > *
278 > * @category Pending Message Types
279 > */
280 > export interface PendingMessage {
281 > /** Unique identifier for this pending message */
282 > id: string;
283 > /** The message that will start the next turn */
284 > message: Message;
285 > }
286 >
287 >
288 > // ─── Chat Input Types ────────────────────────────────────────────────────
289 >
290 > /**
291 > * How a client completed an input request.
292 > *
293 > * @category Chat Input Types
294 > */
295 > export const enum ChatInputResponseKind {
296 > Accept = 'accept',
297 > Decline = 'decline',
298 > Cancel = 'cancel',
299 > }
300 >
301 > /**
302 > * Question/input control kind.
303 > *
304 > * @category Chat Input Types
305 > */
306 > export const enum ChatInputQuestionKind {
307 > Text = 'text',
308 > Number = 'number',
309 > Integer = 'integer',
310 > Boolean = 'boolean',
311 > SingleSelect = 'single-select',
312 > MultiSelect = 'multi-select',
313 > }
314 >
315 > /**
316 > * A choice in a select-style question.
317 > *
318 > * @category Chat Input Types
319 > */
320 > export interface ChatInputOption {
321 > /** Stable option identifier; for MCP enum values this is the enum string */
322 > id: string;
323 > /** Display label */
324 > label: string;
325 > /** Optional secondary text */
326 > description?: string;
327 > /** Whether this option is the recommended/default choice */
328 > recommended?: boolean;
329 > }
330 >
331 > interface ChatInputQuestionBase {
332 > /** Stable question identifier used as the key in `answers` */
333 > id: string;
334 > /** Short display title */
335 > title?: string;
336 > /** Prompt shown to the user */
337 > message: string;
338 > /** Whether the user must answer this question to accept the request */
339 > required?: boolean;
340 > }
341 >
342 > /** Text question within a chat input request. */
343 > export interface ChatInputTextQuestion extends ChatInputQuestionBase {
344 > kind: ChatInputQuestionKind.Text;
345 > /** Format hint for text questions, such as `email`, `uri`, `date`, or `date-time` */
346 > format?: string;
347 > /** Minimum string length */
348 > min?: number;
349 > /** Maximum string length */
350 > max?: number;
351 > /** Default text */
352 > defaultValue?: string;
353 > }
354 >
355 > /** Numeric question within a chat input request. */
356 > export interface ChatInputNumberQuestion extends ChatInputQuestionBase {
357 > kind: ChatInputQuestionKind.Number | ChatInputQuestionKind.Integer;
358 > /**
359 > * Minimum value
360 > * @format float
361 > */
362 > min?: number;
363 > /**
364 > * Maximum value
365 > * @format float
366 > */
367 > max?: number;
368 > /**
369 > * Default numeric value
370 > * @format float
371 > */
372 > defaultValue?: number;
373 > }
374 >
375 > /** Boolean question within a chat input request. */
376 > export interface ChatInputBooleanQuestion extends ChatInputQuestionBase {
377 > kind: ChatInputQuestionKind.Boolean;
378 > /** Default boolean value */
379 > defaultValue?: boolean;
380 > }
381 >
382 > /** Single-select question within a chat input request. */
383 > export interface ChatInputSingleSelectQuestion extends ChatInputQuestionBase {
384 > kind: ChatInputQuestionKind.SingleSelect;
385 > /** Options the user may select from */
386 > options: ChatInputOption[];
387 > /** Whether the user may enter text instead of selecting an option */
388 > allowFreeformInput?: boolean;
389 > }
390 >
391 > /** Multi-select question within a chat input request. */
392 > export interface ChatInputMultiSelectQuestion extends ChatInputQuestionBase {
393 > kind: ChatInputQuestionKind.MultiSelect;
394 > /** Options the user may select from */
395 > options: ChatInputOption[];
396 > /** Whether the user may enter text in addition to selecting options */
397 > allowFreeformInput?: boolean;
398 > /** Minimum selected item count */
399 > min?: number;
400 > /** Maximum selected item count */
401 > max?: number;
402 > }
403 >
404 > /**
405 > * One question within a chat input request.
406 > *
407 > * @category Chat Input Types
408 > */
409 > export type ChatInputQuestion = ChatInputTextQuestion
410 > | ChatInputNumberQuestion
411 > | ChatInputBooleanQuestion
412 > | ChatInputSingleSelectQuestion
413 > | ChatInputMultiSelectQuestion;
414 >
415 > /**
416 > * The request payload carried by an {@link InputRequestResponsePart}.
417 > *
418 > * The server creates or replaces the containing response part with
419 > * `chat/inputRequested`. Clients sync drafts with `chat/inputAnswerChanged`
420 > * and submit responses with `chat/inputCompleted`.
421 > *
422 > * @category Chat Input Types
423 > */
424 > export interface ChatInputRequest {
425 > /** Stable request identifier */
426 > id: string;
427 > /** Display message for the request as a whole */
428 > message?: string;
429 > /** URL the user should review or open, for URL-style elicitations */
430 > url?: URI;
431 > /** Ordered questions to ask the user */
432 > questions?: ChatInputQuestion[];
433 > /** Current draft or submitted answers, keyed by question ID */
434 > answers?: Record<string, ChatInputAnswer>;
435 > }
436 >
437 > /**
438 > * Answer value kind.
439 > *
440 > * @category Chat Input Types
441 > */
442 > export const enum ChatInputAnswerValueKind {
443 > Text = 'text',
444 > Number = 'number',
445 > Boolean = 'boolean',
446 > Selected = 'selected',
447 > SelectedMany = 'selected-many',
448 > }
449 >
450 > /**
451 > * Value captured for one answer.
452 > *
453 > * @category Chat Input Types
454 > */
455 > export interface ChatInputTextAnswerValue {
456 > kind: ChatInputAnswerValueKind.Text;
457 > value: string;
458 > }
459 >
460 > export interface ChatInputNumberAnswerValue {
461 > kind: ChatInputAnswerValueKind.Number;
462 > /** @format float */
463 > value: number;
464 > }
465 >
466 > export interface ChatInputBooleanAnswerValue {
467 > kind: ChatInputAnswerValueKind.Boolean;
468 > value: boolean;
469 > }
470 >
471 > export interface ChatInputSelectedAnswerValue {
472 > kind: ChatInputAnswerValueKind.Selected;
473 > value: string;
474 > /** Free-form text entered instead of selecting an option */
475 > freeformValues?: string[];
476 > }
477 >
478 > export interface ChatInputSelectedManyAnswerValue {
479 > kind: ChatInputAnswerValueKind.SelectedMany;
480 > value: string[];
481 > /** Free-form text entered in addition to selected options */
482 > freeformValues?: string[];
483 > }
484 >
485 > export type ChatInputAnswerValue = ChatInputTextAnswerValue
486 > | ChatInputNumberAnswerValue
487 > | ChatInputBooleanAnswerValue
488 > | ChatInputSelectedAnswerValue
489 > | ChatInputSelectedManyAnswerValue;
490 >
491 > export interface ChatInputAnswered {
492 > /** Answer state */
493 > state: ChatInputAnswerState.Draft | ChatInputAnswerState.Submitted;
494 > /** Answer value */
495 > value: ChatInputAnswerValue;
496 > }
497 >
498 > export interface ChatInputSkipped {
499 > /** Answer state */
500 > state: ChatInputAnswerState.Skipped;
501 > /** Free-form reason or value captured while skipping, if any */
502 > freeformValues?: string[];
503 > }
504 >
505 > /**
506 > * Answer lifecycle state.
507 > *
508 > * @category Chat Input Types
509 > */
510 > export const enum ChatInputAnswerState {
511 > Draft = 'draft',
512 > Submitted = 'submitted',
513 > Skipped = 'skipped',
514 > }
515 >
516 > /**
517 > * Draft, submitted, or skipped answer for one question.
518 > *
519 > * @category Chat Input Types
520 > */
521 > export type ChatInputAnswer = ChatInputAnswered | ChatInputSkipped;
522 >
523 >
524 > // ─── Turn Types ──────────────────────────────────────────────────────────────
525 >
526 > /**
527 > * How a turn ended.
528 > *
529 > * @category Turn Types
530 > */
531 > export const enum TurnState {
532 > Complete = 'complete',
533 > Cancelled = 'cancelled',
534 > Error = 'error',
535 > }
536 >
537 > /**
538 > * Discriminant for {@link MessageAttachment} variants.
539 > *
540 > * @category Turn Types
541 > */
542 > export const enum MessageAttachmentKind {
543 > /** A simple, opaque attachment whose representation is described by the producer. */
544 > Simple = 'simple',
545 > /** An attachment whose data is embedded inline as a base64 string. */
546 > EmbeddedResource = 'embeddedResource',
547 > /** An attachment that references a resource by URI. */
548 > Resource = 'resource',
549 > /** An attachment that references annotations on an annotations channel. */
550 > Annotations = 'annotations',
551 > /** An attachment that references a bounded transcript from another chat. */
552 > Chat = 'chat',
553 > }
554 >
555 > /**
556 > * A completed request/response cycle.
557 > *
558 > * @category Turn Types
559 > */
560 > export interface Turn {
561 > /** Turn identifier */
562 > id: string;
563 > /** ISO 8601 timestamp when this turn started. */
564 > startedAt?: string;
565 > /** Turn duration in milliseconds. */
566 > duration?: number;
567 > /** The message that initiated the turn */
568 > message: Message;
569 > /**
570 > * All response content in stream order: text, tool calls, reasoning, and content refs.
571 > *
572 > * Consumers should derive display text by concatenating markdown parts,
573 > * and find tool calls by filtering for `ToolCall` parts.
574 > */
575 > responseParts: ResponsePart[];
576 > /** Token usage info */
577 > usage: UsageInfo | undefined;
578 > /** How the turn ended */
579 > state: TurnState;
580 > /** Error details if state is `'error'` */
581 > error?: ErrorInfo;
582 > }
583 >
584 > /**
585 > * An in-progress turn — the assistant is actively streaming.
586 > *
587 > * @category Turn Types
588 > */
589 > export interface ActiveTurn {
590 > /** Turn identifier */
591 > id: string;
592 > /** ISO 8601 timestamp when this turn started. */
593 > startedAt: string;
594 > /** The message that initiated the turn */
595 > message: Message;
596 > /**
597 > * All response content in stream order: text, tool calls, reasoning, and content refs.
598 > *
599 > * Tool call parts include `pendingPermissions` when permissions are awaiting user approval.
600 > */
601 > responseParts: ResponsePart[];
602 > /** Token usage info */
603 > usage: UsageInfo | undefined;
604 > }
605 >
606 > /**
607 > * Discriminant for {@link MessageOrigin} — identifies who produced a message.
608 > *
609 > * @category Turn Types
610 > */
611 > export enum MessageKind {
612 > /** Sent directly by the user. */
613 > User = 'user',
614 > /**
615 > * Produced by the agent itself rather than the user — for example, an agent
616 > * that seeds the first message of a chat it spawned.
617 > */
618 > Agent = 'agent',
619 > /**
620 > * Produced by a tool rather than the user — for example, a tool that spawns a
621 > * worker chat whose first message carries a seed prompt.
622 > */
623 > Tool = 'tool',
624 > /** A system-generated notification rather than a direct user message. */
625 > SystemNotification = 'systemNotification',
626 > }
627 >
628 > /**
629 > * Identifies the origin of a {@link Message} — who produced it. For the message
630 > * that initiates a turn ({@link Turn.message}), this is also the origin of the
631 > * turn; for steering or queued messages it is just the origin of that message.
632 > *
633 > * @category Turn Types
634 > */
635 > export interface MessageOrigin {
636 > /** The kind of actor that produced the message. */
637 > kind: MessageKind;
638 > }
639 >
640 > /**
641 > * A message that initiates or steers a turn. Messages can originate from the
642 > * user, the agent, a tool, or be system-generated (see {@link MessageOrigin}).
643 > *
644 > * Attachments MAY be referenced inside {@link Message.text} via their
645 > * {@link MessageAttachmentBase.range} field. Attachments without a range are
646 > * still associated with the message but do not correspond to a specific span
647 > * in the text.
648 > *
649 > * @category Turn Types
650 > */
651 > export interface Message {
652 > /** Message text */
653 > text: string;
654 > /** The origin of the message */
655 > origin: MessageOrigin;
656 > /** File/selection attachments */
657 > attachments?: MessageAttachment[];
658 > /**
659 > * The model this message was, or will be, sent with.
660 > *
661 > * For historic user/agent messages this records the model actually used, so
662 > * a client editing or resending the message can retain that selection. For a
663 > * {@link ChatState.draft | draft} it carries the model the user picked for
664 > * the message they are composing. Absent means the agent host's default
665 > * model applies.
666 > */
667 > model?: ModelSelection;
668 > /**
669 > * The custom agent this message was, or will be, sent with.
670 > *
671 > * For historic messages this records the agent actually used; for a
672 > * {@link ChatState.draft | draft} it carries the agent the user picked.
673 > * Absent means no custom agent — the provider's default behavior applies.
674 > */
675 > agent?: AgentSelection;
676 > /**
677 > * Additional provider-specific metadata for this message.
678 > *
679 > * Clients MAY look for well-known keys here to provide enhanced UI, and
680 > * agent hosts MAY use it to carry context that does not fit any other
681 > * field. Mirrors the MCP `_meta` convention.
682 > */
683 > _meta?: Record<string, unknown>;
684 > }
685 >
686 > /**
687 > * Common fields shared by all {@link MessageAttachment} variants.
688 > *
689 > * @category Turn Types
690 > */
691 > export interface MessageAttachmentBase {
692 > /**
693 > * A human-readable label for the attachment (e.g. the filename of a file
694 > * attachment). Used for display in UI.
695 > */
696 > label: string;
697 >
698 > /**
699 > * If defined, the range in {@link Message.text} that references this
700 > * attachment. This is a text range, not a byte range.
701 > */
702 > range?: TextRange;
703 >
704 > /**
705 > * Advisory display hint for clients rendering this attachment. Recognized
706 > * values include:
707 > *
708 > * - `'image'`: the attachment is an image
709 > * - `'document'`: the attachment is a textual document
710 > * - `'symbol'`: the attachment is a code symbol (e.g. a function or class)
711 > * - `'directory'`: the attachment is a folder
712 > * - `'selection'`: the attachment is a selection within a document
713 > *
714 > * Implementations MAY provide additional values; clients SHOULD fall back
715 > * to a reasonable default when an unknown value is encountered.
716 > */
717 > displayKind?: string;
718 >
719 > /**
720 > * Additional implementation-defined metadata for the attachment.
721 > *
722 > * If the attachment was produced by the `completions` command, the client
723 > * MUST preserve every property of `_meta` originally returned by the agent
724 > * host when sending the user message containing the accepted completion.
725 > */
726 > _meta?: Record<string, unknown>;
727 > }
728 >
729 > /**
730 > * A simple, opaque attachment whose model representation is described by
731 > * the producer.
732 > *
733 > * @category Turn Types
734 > */
735 > export interface SimpleMessageAttachment extends MessageAttachmentBase {
736 > /** Discriminant */
737 > type: MessageAttachmentKind.Simple;
738 >
739 > /**
740 > * Representation of the attachment as it should be shown to the model.
741 > *
742 > * If the attachment was produced by the client, this property MUST be
743 > * defined so the agent host can correctly interpret the attachment. This
744 > * property MAY be omitted when the attachment originated from a
745 > * `completions` response.
746 > */
747 > modelRepresentation?: string;
748 > }
749 >
750 > /**
751 > * An attachment whose data is embedded inline as a base64 string.
752 > *
753 > * Use this for small binary payloads (e.g. a pasted image) that should be
754 > * delivered with the user message itself rather than fetched separately.
755 > *
756 > * @category Turn Types
757 > */
758 > export interface MessageEmbeddedResourceAttachment extends MessageAttachmentBase {
759 > /** Discriminant */
760 > type: MessageAttachmentKind.EmbeddedResource;
761 > /** Base64-encoded binary data */
762 > data: string;
763 > /** Content MIME type (e.g. `"image/png"`, `"application/pdf"`) */
764 > contentType: string;
765 > /**
766 > * Optional selection within the attached textual resource.
767 > *
768 > * Only meaningful for textual resources.
769 > */
770 > selection?: TextSelection;
771 > }
772 >
773 > /**
774 > * An attachment that references a resource by URI. The content is not
775 > * delivered inline; consumers can fetch it via `resourceRead` when needed.
776 > *
777 > * @category Turn Types
778 > */
779 > export interface MessageResourceAttachment extends MessageAttachmentBase, ContentRef {
780 > /** Discriminant */
781 > type: MessageAttachmentKind.Resource;
782 > /**
783 > * Optional selection within the referenced textual resource.
784 > *
785 > * Only meaningful for textual resources.
786 > */
787 > selection?: TextSelection;
788 > }
789 >
790 > /**
791 > * An attachment that references annotations on a session's annotations
792 > * channel (see {@link AnnotationsState}).
793 > *
794 > * When {@link annotationIds} is omitted the attachment references every
795 > * annotation on the channel; when present it references only the listed
796 > * {@link Annotation.id | annotation ids}.
797 > *
798 > * @category Turn Types
799 > */
800 > export interface MessageAnnotationsAttachment extends MessageAttachmentBase {
801 > /** Discriminant */
802 > type: MessageAttachmentKind.Annotations;
803 > /**
804 > * The annotations channel URI (typically `ahp-session:/<uuid>/annotations`).
805 > * Matches {@link AnnotationsSummary.resource}.
806 > */
807 > resource: URI;
808 > /**
809 > * Specific {@link Annotation.id | annotation ids} to reference. When
810 > * omitted, the attachment references all annotations on the channel.
811 > */
812 > annotationIds?: string[];
813 > }
814 >
815 > /**
816 > * An attachment that references a chat transcript through a fixed completed
817 > * turn.
818 > *
819 > * The referenced chat MUST belong to the same session as the message's chat.
820 > * The host resolves the transcript from its first retained turn through
821 > * `endTurn`, inclusive, when accepting the message. Later turns do not
822 > * change the context represented by an already-sent attachment.
823 > *
824 > * Hosts MUST NOT recursively expand chat attachments found inside the
825 > * referenced transcript. Clients SHOULD keep rendering `label` if the
826 > * referenced chat is later pruned, and treat opening `resource` as best-effort.
827 > *
828 > * @category Turn Types
829 > */
830 > export interface MessageChatAttachment extends MessageAttachmentBase {
831 > /** Discriminant */
832 > type: MessageAttachmentKind.Chat;
833 > /** URI of the referenced chat. */
834 > resource: URI;
835 > /** Last completed turn included in the referenced transcript. */
836 > endTurn: string;
837 > }
838 >
839 > /**
840 > * An attachment associated with a {@link Message}.
841 > *
842 > * @category Turn Types
843 > */
844 > export type MessageAttachment =
845 > | SimpleMessageAttachment
846 > | MessageEmbeddedResourceAttachment
847 > | MessageResourceAttachment
848 > | MessageAnnotationsAttachment
849 > | MessageChatAttachment;
850 >
851 > // ─── Response Parts ──────────────────────────────────────────────────────────
852 >
853 > /**
854 > * Discriminant for response part types.
855 > *
856 > * @category Response Parts
857 > */
858 > export const enum ResponsePartKind {
859 > Markdown = 'markdown',
860 > ContentRef = 'contentRef',
861 > ToolCall = 'toolCall',
862 > Reasoning = 'reasoning',
863 > SystemNotification = 'systemNotification',
864 > InputRequest = 'inputRequest',
865 > }
866 >
867 > /**
868 > * @category Response Parts
869 > */
870 > export interface MarkdownResponsePart {
871 > /** Discriminant */
872 > kind: ResponsePartKind.Markdown;
873 > /** Part identifier, used by `chat/delta` to target this part for content appends */
874 > id: string;
875 > /** Markdown content */
876 > content: string;
877 > }
878 >
879 > /**
880 > * A content part that's a reference to large content stored outside the state tree.
881 > *
882 > * @category Response Parts
883 > */
884 > export interface ResourceReponsePart extends ContentRef {
885 > /** Discriminant */
886 > kind: ResponsePartKind.ContentRef;
887 > }
888 >
889 > /**
890 > * A tool call represented as a response part.
891 > *
892 > * Tool calls are part of the response stream, interleaved with text and
893 > * reasoning. The `toolCall.toolCallId` serves as the part identifier for
894 > * actions that target this part.
895 > *
896 > * @category Response Parts
897 > */
898 > export interface ToolCallResponsePart {
899 > /** Discriminant */
900 > kind: ResponsePartKind.ToolCall;
901 > /** Full tool call lifecycle state */
902 > toolCall: ToolCallState;
903 > }
904 >
905 > /**
906 > * Reasoning/thinking content from the model.
907 > *
908 > * @category Response Parts
909 > */
910 > export interface ReasoningResponsePart {
911 > /** Discriminant */
912 > kind: ResponsePartKind.Reasoning;
913 > /** Part identifier, used by `chat/reasoning` to target this part for content appends */
914 > id: string;
915 > /** Accumulated reasoning text */
916 > content: string;
917 > }
918 >
919 > /**
920 > * @category Response Parts
921 > */
922 > export type ResponsePart =
923 > | MarkdownResponsePart
924 > | ResourceReponsePart
925 > | ToolCallResponsePart
926 > | ReasoningResponsePart
927 > | SystemNotificationResponsePart
928 > | InputRequestResponsePart;
929 >
930 > /**
931 > * A live or resolved input request (elicitation) in the turn response stream.
932 > *
933 > * The server inserts the part with `chat/inputRequested`. While
934 > * {@link response} is absent, clients can update answer drafts with
935 > * `chat/inputAnswerChanged` and submit a response with `chat/inputCompleted`.
936 > * Completion updates this part in place so its stream position is stable and
937 > * the full interaction remains durable and backfillable via `fetchTurns`.
938 > *
939 > * If the turn ends without a submitted response, the unresolved part remains
940 > * in the completed turn transcript with {@link response} absent.
941 > *
942 > * @category Response Parts
943 > */
944 > export interface InputRequestResponsePart {
945 > /** Discriminant */
946 > kind: ResponsePartKind.InputRequest;
947 > /**
948 > * The request, carrying its `id`, `message`, `url`, `questions`, and current
949 > * draft or submitted `answers`.
950 > */
951 > request: ChatInputRequest;
952 > /**
953 > * How the request was resolved. Absent until a client submits `accept`,
954 > * `decline`, or `cancel` with `chat/inputCompleted`.
955 > */
956 > response?: ChatInputResponseKind;
957 > }
958 >
959 > /**
960 > * A system notification surfaced as part of the response stream.
961 > *
962 > * System notifications are messages authored by the agent harness
963 > * that need to be visible to both the agent (for situational awareness) and
964 > * the user (for transcript continuity). Examples include "background subagent
965 > * X completed" or "task Y was cancelled".
966 > *
967 > * @category Response Parts
968 > */
969 > export interface SystemNotificationResponsePart {
970 > /** Discriminant */
971 > kind: ResponsePartKind.SystemNotification;
972 > /** The text of the system notification */
973 > content: StringOrMarkdown;
974 > /**
975 > * Additional provider-specific metadata for this notification.
976 > *
977 > * A host MAY attach a machine-readable descriptor of what triggered the
978 > * notification so clients can categorize, icon, group, filter, or localize
979 > * it without parsing `content`. Clients MAY look for well-known keys here to
980 > * provide enhanced UI, and MUST render coherently from `content` alone when
981 > * `_meta` is absent or unrecognized.
982 > */
983 > _meta?: Record<string, unknown>;
984 > }
985 >
986 >
987 > // ─── Tool Call Types ─────────────────────────────────────────────────────────
988 >
989 > /**
990 > * Status of a tool call in the lifecycle state machine.
991 > *
992 > * @category Tool Call Types
993 > */
994 > export const enum ToolCallStatus {
995 > Streaming = 'streaming',
996 > PendingConfirmation = 'pending-confirmation',
997 > Running = 'running',
998 > /**
999 > * Running paused because the MCP server backing this call needs
1000 > * authentication (typically step-up auth for insufficient scope,
1001 > * surfacing mid-execution). See {@link ToolCallAuthRequiredState}.
1002 > */
1003 > AuthRequired = 'auth-required',
1004 > PendingResultConfirmation = 'pending-result-confirmation',
1005 > Completed = 'completed',
1006 > Cancelled = 'cancelled',
1007 > }
1008 >
1009 > /**
1010 > * How a tool call was confirmed for execution.
1011 > *
1012 > * - `NotNeeded` — No confirmation required (auto-approved)
1013 > * - `UserAction` — User explicitly approved
1014 > * - `Setting` — Approved by a persistent user setting
1015 > *
1016 > * @category Tool Call Types
1017 > */
1018 > export const enum ToolCallConfirmationReason {
1019 > NotNeeded = 'not-needed',
1020 > UserAction = 'user-action',
1021 > Setting = 'setting',
1022 > }
1023 >
1024 > /**
1025 > * Identifies a model judge as the source of a confirmation requirement.
1026 > *
1027 > * @category Tool Call Types
1028 > */
1029 > export const enum ToolCallRiskAssessmentKind {
1030 > Judge = 'judge',
1031 > }
1032 >
1033 > /**
1034 > * Lifecycle status of an asynchronous model-judge confirmation decision.
1035 > *
1036 > * @category Tool Call Types
1037 > */
1038 > export const enum ToolCallRiskAssessmentStatus {
1039 > Loading = 'loading',
1040 > Complete = 'complete',
1041 > }
1042 >
1043 > interface ToolCallRiskAssessmentBase {
1044 > kind: ToolCallRiskAssessmentKind;
1045 > }
1046 >
1047 > /**
1048 > * The model judge is still evaluating the tool call.
1049 > *
1050 > * @category Tool Call Types
1051 > */
1052 > export interface ToolCallRiskAssessmentLoadingState extends ToolCallRiskAssessmentBase {
1053 > status: ToolCallRiskAssessmentStatus.Loading;
1054 > }
1055 >
1056 > /**
1057 > * The model judge has completed its evaluation.
1058 > *
1059 > * @category Tool Call Types
1060 > */
1061 > export interface ToolCallRiskAssessmentCompleteState extends ToolCallRiskAssessmentBase {
1062 > status: ToolCallRiskAssessmentStatus.Complete;
1063 > reason: StringOrMarkdown;
1064 > /**
1065 > * The judge's normalized safety score, where `0` is unsafe and `1` is safe.
1066 > * @format float
1067 > */
1068 > safety: number;
1069 > }
1070 >
1071 > export type ToolCallRiskAssessment =
1072 > | ToolCallRiskAssessmentLoadingState
1073 > | ToolCallRiskAssessmentCompleteState;
1074 >
1075 > /**
1076 > * Why a tool call was cancelled.
1077 > *
1078 > * @category Tool Call Types
1079 > */
1080 > export const enum ToolCallCancellationReason {
1081 > Denied = 'denied',
1082 > Skipped = 'skipped',
1083 > ResultDenied = 'result-denied',
1084 > }
1085 >
1086 > /**
1087 > * Whether a confirmation option represents an approval or denial action.
1088 > *
1089 > * @category Tool Call Types
1090 > */
1091 > export const enum ConfirmationOptionKind {
1092 > Approve = 'approve',
1093 > Deny = 'deny',
1094 > }
1095 >
1096 > /**
1097 > * A confirmation option that the server offers for a tool call awaiting
1098 > * approval. Allows richer choices beyond simple approve/deny — for example,
1099 > * "Approve in this Session" or "Deny with reason."
1100 > *
1101 > * @category Tool Call Types
1102 > */
1103 > export interface ConfirmationOption {
1104 > /** Unique identifier for the option, returned in the confirmed action */
1105 > id: string;
1106 > /** Human-readable label displayed to the user */
1107 > label: string;
1108 > /** Whether this option represents an approval or denial */
1109 > kind: ConfirmationOptionKind;
1110 > /**
1111 > * Logical group number for visual categorisation.
1112 > *
1113 > * Clients SHOULD display options in the order they are defined and MAY
1114 > * use differing group numbers to insert dividers between logical clusters
1115 > * of options.
1116 > */
1117 > group?: number;
1118 > }
1119 >
1120 > export const enum ToolCallContributorKind {
1121 > Client = 'client',
1122 > MCP = 'mcp',
1123 > }
1124 >
1125 > export interface ToolCallClientContributor {
1126 > kind: ToolCallContributorKind.Client;
1127 > /**
1128 > * If this tool is provided by a client, the `clientId` of the owning client.
1129 > * Absent for server-side tools.
1130 > *
1131 > * When set, the identified client is responsible for executing the tool and
1132 > * dispatching `chat/toolCallComplete` with the result.
1133 > */
1134 > clientId: string;
1135 > }
1136 >
1137 > export interface ToolCallMcpContributor {
1138 > kind: ToolCallContributorKind.MCP;
1139 > /**
1140 > * Customization ID of the corresponding MCP server in {@link SessionState.customizations}.
1141 > */
1142 > customizationId: string;
1143 > }
1144 >
1145 > export type ToolCallContributor = ToolCallClientContributor | ToolCallMcpContributor;
1146 >
1147 > /**
1148 > * Metadata common to all tool call states.
1149 > *
1150 > * @category Tool Call Types
1151 > * @remarks
1152 > * Fields like `toolName` carry agent-specific identifiers on the wire despite the
1153 > * agent-agnostic design principle. These exist for debugging and logging purposes.
1154 > * A future version may move these to a separate diagnostic channel or namespace them
1155 > * more clearly.
1156 > */
1157 > interface ToolCallBase {
1158 > /** Unique tool call identifier */
1159 > toolCallId: string;
1160 > /** Internal tool name (for debugging/logging) */
1161 > toolName: string;
1162 > /** Human-readable tool name */
1163 > displayName: string;
1164 > /** Human-readable description of what the tool invocation intends to do */
1165 > intention?: string;
1166 > /**
1167 > * Reference to the contributor of the tool being called.
1168 > */
1169 > contributor?: ToolCallContributor;
1170 > /**
1171 > * Additional provider-specific metadata for this tool call.
1172 > *
1173 > * This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)
1174 > * `McpUiToolMeta` found in MCP tool calls, which may be used in combination
1175 > * with the {@link contributor} to serve MCP Apps.
1176 > */
1177 > _meta?: Record<string, unknown>;
1178 > }
1179 >
1180 > /**
1181 > * Properties available once tool call parameters are fully received.
1182 > *
1183 > * @category Tool Call Types
1184 > */
1185 > interface ToolCallParameterFields {
1186 > /** Message describing what the tool will do */
1187 > invocationMessage: StringOrMarkdown;
1188 > /** Raw tool input */
1189 > toolInput?: string;
1190 > }
1191 >
1192 > /**
1193 > * Tool execution result details, available after execution completes.
1194 > *
1195 > * @category Tool Call Types
1196 > */
1197 > export interface ToolCallResult {
1198 > /** Whether the tool succeeded */
1199 > success: boolean;
1200 > /** Past-tense description of what the tool did */
1201 > pastTenseMessage: StringOrMarkdown;
1202 > /**
1203 > * Unstructured result content blocks.
1204 > *
1205 > * This mirrors the `content` field of MCP `CallToolResult`.
1206 > */
1207 > content?: ToolResultContent[];
1208 > /**
1209 > * Optional structured result object.
1210 > *
1211 > * This mirrors the `structuredContent` field of MCP `CallToolResult`.
1212 > */
1213 > structuredContent?: Record<string, unknown>;
1214 > /** Error details if the tool failed */
1215 > error?: { message: string; code?: string };
1216 > }
1217 >
1218 > /**
1219 > * LM is streaming the tool call parameters.
1220 > *
1221 > * @category Tool Call Types
1222 > */
1223 > export interface ToolCallStreamingState extends ToolCallBase {
1224 > status: ToolCallStatus.Streaming;
1225 > /** Partial parameters accumulated so far */
1226 > partialInput?: string;
1227 > /** Progress message shown while parameters are streaming */
1228 > invocationMessage?: StringOrMarkdown;
1229 > }
1230 >
1231 > /**
1232 > * Parameters are complete, or a running tool requires re-confirmation
1233 > * (e.g. a mid-execution permission check).
1234 > *
1235 > * @category Tool Call Types
1236 > */
1237 > export interface ToolCallPendingConfirmationState extends ToolCallBase, ToolCallParameterFields {
1238 > status: ToolCallStatus.PendingConfirmation;
1239 > /** Short title for the confirmation prompt (e.g. `"Run in terminal"`, `"Write file"`) */
1240 > confirmationTitle?: StringOrMarkdown;
1241 > /** Risk assessment that informed the confirmation requirement. */
1242 > riskAssessment?: ToolCallRiskAssessment;
1243 > /** File edits that this tool call will perform, for preview before confirmation */
1244 > edits?: { items: FileEdit[] };
1245 > /** Whether the agent host allows the client to edit the tool's input parameters before confirming */
1246 > editable?: boolean;
1247 > /**
1248 > * Options the server offers for this confirmation. When present, the client
1249 > * SHOULD render these instead of a plain approve/deny UI. Each option
1250 > * belongs to a {@link ConfirmationOptionGroup} so the client can still
1251 > * categorise the choices.
1252 > */
1253 > options?: ConfirmationOption[];
1254 > }
1255 >
1256 > /**
1257 > * Fields present on every tool call state that exists **after** confirmation
1258 > * has been resolved: {@link ToolCallRunningState}, {@link ToolCallAuthRequiredState},
1259 > * {@link ToolCallPendingResultConfirmationState}, and {@link ToolCallCompletedState}.
1260 > * `ToolCallPendingConfirmationState` (not yet confirmed) and
1261 > * `ToolCallCancelledState` (the denial path — never ran) don't satisfy this
1262 > * invariant, so they keep their own `selectedOption` field independently
1263 > * rather than extending this one.
1264 > *
1265 > * @category Tool Call Types
1266 > */
1267 > interface ToolCallPostConfirmationFields {
1268 > /** How the tool was confirmed for execution */
1269 > confirmed: ToolCallConfirmationReason;
1270 > /** The confirmation option the user selected, if confirmation options were provided */
1271 > selectedOption?: ConfirmationOption;
1272 > }
1273 >
1274 > /**
1275 > * Tool is actively executing.
1276 > *
1277 > * @category Tool Call Types
1278 > */
1279 > export interface ToolCallRunningState extends ToolCallBase, ToolCallParameterFields, ToolCallPostConfirmationFields {
1280 > status: ToolCallStatus.Running;
1281 > /**
1282 > * Partial content produced while the tool is still executing.
1283 > *
1284 > * For example, a terminal content block lets clients subscribe to live
1285 > * output before the tool completes.
1286 > */
1287 > content?: ToolResultContent[];
1288 > }
1289 >
1290 > /**
1291 > * A running tool call is paused because the MCP server backing it needs
1292 > * authentication — most commonly {@link McpAuthRequirement.reason |
1293 > * `insufficientScope`} step-up auth triggered by the `tools/call` request
1294 > * itself. Only ever reached from {@link ToolCallRunningState}, and normally
1295 > * returns there once authenticated: `running` → `auth-required` → `running`
1296 > * → …. A client MAY instead cancel the invocation without authenticating by
1297 > * dispatching a `chat/toolCallComplete` with a **failed** result, always
1298 > * moving straight to {@link ToolCallCompletedState} —
1299 > * `requiresResultConfirmation` is ignored on this path, so it can never
1300 > * enter {@link ToolCallPendingResultConfirmationState}. A **successful**
1301 > * result dispatched from this state is invalid and MUST be rejected/ignored
1302 > * as a no-op by the reducer, since execution never resumed after the
1303 > * challenge.
1304 > *
1305 > * This is the tool-call-level counterpart to
1306 > * {@link McpServerAuthRequiredState} — that state means the MCP *server*
1307 > * cannot serve any request; this one means *this specific invocation* is
1308 > * waiting on the same kind of challenge. The two are dispatched
1309 > * independently and MAY be true at the same time, or not: an
1310 > * `insufficientScope` challenge triggered by a single tool call, for
1311 > * example, need not block the whole server.
1312 > *
1313 > * Because the challenge is always resolved by pushing a token via the
1314 > * existing `authenticate` command, this state can only originate from a
1315 > * tool call {@link ToolCallContributorKind.MCP | contributed by an MCP
1316 > * server} — `contributor` is narrowed accordingly (unlike the optional,
1317 > * multi-kind `contributor` on other tool call states).
1318 > *
1319 > * @category Tool Call Types
1320 > */
1321 > export interface ToolCallAuthRequiredState extends ToolCallBase, ToolCallParameterFields, ToolCallPostConfirmationFields {
1322 > status: ToolCallStatus.AuthRequired;
1323 > /** The MCP server that contributed this tool call — always MCP, never a client tool. */
1324 > contributor: ToolCallMcpContributor;
1325 > /** The authentication challenge blocking this invocation. */
1326 > auth: McpAuthRequirement;
1327 > /** Partial content produced before the call paused for authentication. */
1328 > content?: ToolResultContent[];
1329 > }
1330 >
1331 > /**
1332 > * Tool finished executing, waiting for client to approve the result.
1333 > *
1334 > * @category Tool Call Types
1335 > */
1336 > export interface ToolCallPendingResultConfirmationState extends ToolCallBase, ToolCallParameterFields, ToolCallResult, ToolCallPostConfirmationFields {
1337 > status: ToolCallStatus.PendingResultConfirmation;
1338 > }
1339 >
1340 > /**
1341 > * Tool completed successfully or with an error.
1342 > *
1343 > * @category Tool Call Types
1344 > */
1345 > export interface ToolCallCompletedState extends ToolCallBase, ToolCallParameterFields, ToolCallResult, ToolCallPostConfirmationFields {
1346 > status: ToolCallStatus.Completed;
1347 > }
1348 >
1349 > /**
1350 > * Tool call was cancelled before execution.
1351 > *
1352 > * @category Tool Call Types
1353 > */
1354 > export interface ToolCallCancelledState extends ToolCallBase, ToolCallParameterFields {
1355 > status: ToolCallStatus.Cancelled;
1356 > /** Why the tool was cancelled */
1357 > reason: ToolCallCancellationReason;
1358 > /** Optional message explaining the cancellation */
1359 > reasonMessage?: StringOrMarkdown;
1360 > /** What the user suggested doing instead */
1361 > userSuggestion?: Message;
1362 > /** The confirmation option the user selected, if confirmation options were provided */
1363 > selectedOption?: ConfirmationOption;
1364 > }
1365 >
1366 > /**
1367 > * Discriminated union of all tool call lifecycle states.
1368 > *
1369 > * See the [state model guide](/guide/state-model.html#tool-call-lifecycle)
1370 > * for the full state machine diagram.
1371 > *
1372 > * @category Tool Call Types
1373 > */
1374 > export type ToolCallState =
1375 > | ToolCallStreamingState
1376 > | ToolCallPendingConfirmationState
1377 > | ToolCallRunningState
1378 > | ToolCallAuthRequiredState
1379 > | ToolCallPendingResultConfirmationState
1380 > | ToolCallCompletedState
1381 > | ToolCallCancelledState;
1382 >
1383 > /**
1384 > * The two tool-call states that block on a client confirmation: parameter
1385 > * confirmation before execution ({@link ToolCallPendingConfirmationState}) and
1386 > * result confirmation after execution
1387 > * ({@link ToolCallPendingResultConfirmationState}).
1388 > *
1389 > * {@link ToolCallAuthRequiredState} is intentionally **not** part of this
1390 > * union: it doesn't block on a `chat/toolCallConfirmed`-style client
1391 > * decision, it blocks on the client completing an OAuth flow and calling
1392 > * `authenticate`. See {@link SessionToolAuthenticationRequest} for its
1393 > * session-level surfacing.
1394 > *
1395 > * Surfaced at the session level by {@link SessionToolConfirmationRequest}.
1396 > *
1397 > * @category Tool Call Types
1398 > */
1399 > export type ToolCallConfirmationState =
1400 > | ToolCallPendingConfirmationState
1401 > | ToolCallPendingResultConfirmationState;
1402 >
1403 >
1404 > // ─── Tool Result Content ─────────────────────────────────────────────────────
1405 >
1406 > /**
1407 > * Discriminant for tool result content types.
1408 > *
1409 > * @category Tool Result Content
1410 > */
1411 > export const enum ToolResultContentType {
1412 > Text = 'text',
1413 > EmbeddedResource = 'embeddedResource',
1414 > Resource = 'resource',
1415 > FileEdit = 'fileEdit',
1416 > Terminal = 'terminal',
1417 > Subagent = 'subagent',
1418 > }
1419 >
1420 > /**
1421 > * Text content in a tool result.
1422 > *
1423 > * Mirrors MCP `TextContent`.
1424 > *
1425 > * @category Tool Result Content
1426 > */
1427 > export interface ToolResultTextContent {
1428 > type: ToolResultContentType.Text;
1429 > /** The text content */
1430 > text: string;
1431 > }
1432 >
1433 > /**
1434 > * Base64-encoded binary content embedded in a tool result.
1435 > *
1436 > * Mirrors MCP `EmbeddedResource` for inline binary data.
1437 > *
1438 > * @category Tool Result Content
1439 > */
1440 > export interface ToolResultEmbeddedResourceContent {
1441 > type: ToolResultContentType.EmbeddedResource;
1442 > /** Base64-encoded data */
1443 > data: string;
1444 > /** Content type (e.g. `"image/png"`, `"application/pdf"`) */
1445 > contentType: string;
1446 > }
1447 >
1448 > /**
1449 > * A reference to a resource stored outside the tool result.
1450 > *
1451 > * Wraps {@link ContentRef} for lazy-loading large results.
1452 > *
1453 > * @category Tool Result Content
1454 > */
1455 > export interface ToolResultResourceContent extends ContentRef {
1456 > type: ToolResultContentType.Resource;
1457 > }
1458 >
1459 > /**
1460 > * Describes a file modification performed by a tool.
1461 > *
1462 > * @category Tool Result Content
1463 > */
1464 > export interface ToolResultFileEditContent extends FileEdit {
1465 > type: ToolResultContentType.FileEdit;
1466 > }
1467 >
1468 > /**
1469 > * A reference to a terminal whose output is relevant to this tool result.
1470 > *
1471 > * Clients can subscribe to the terminal's URI to stream its output in real
1472 > * time, providing live feedback while a tool is executing.
1473 > *
1474 > * When the command exits, {@link result} is filled in on the completed
1475 > * result, retaining the outcome for clients that did not subscribe. This
1476 > * records the command's exit, not the terminal's — the terminal may keep
1477 > * running afterwards.
1478 > *
1479 > * @category Tool Result Content
1480 > */
1481 > export interface ToolResultTerminalContent {
1482 > type: ToolResultContentType.Terminal;
1483 > /** Terminal URI (subscribable for full terminal state) */
1484 > resource: URI;
1485 > /** Display title for the terminal content */
1486 > title: string;
1487 > /**
1488 > * Whether this terminal-style resource is backed by a pseudoterminal.
1489 > * When `false`, output is plain text and clients do not need to parse
1490 > * VT sequences.
1491 > */
1492 > isPty?: boolean;
1493 > /** Outcome of the command, present once it has exited. */
1494 > result?: TerminalCommandResult;
1495 > }
1496 >
1497 > /**
1498 > * Outcome of a command run in a terminal-style tool, filled in on
1499 > * {@link ToolResultTerminalContent.result} once the command exits.
1500 > *
1501 > * @category Tool Result Content
1502 > */
1503 > export interface TerminalCommandResult {
1504 > /** Exit code from the completed command, if reported by the runtime */
1505 > exitCode?: number;
1506 > /**
1507 > * Preview of the command's output, for clients that are not subscribed
1508 > * to the terminal or that arrive after it is disposed. When `isPty` is
1509 > * `true` the preview may contain VT sequences; when `false` it is plain
1510 > * text.
1511 > */
1512 > preview?: string;
1513 > /** Whether `preview` is known to be incomplete or truncated */
1514 > truncated?: boolean;
1515 > }
1516 >
1517 > /**
1518 > * A reference, embedded in a tool result, to a worker chat spawned by the tool
1519 > * call (a sub-agent delegation), referenced by a chat URI (`ahp-chat:/...`).
1520 > *
1521 > * This is the spawning tool call's forward view of the worker. The worker chat
1522 > * records the same edge in reverse via its {@link ChatOrigin} (`kind: 'tool'`),
1523 > * whose `toolCallId` identifies the tool call that emitted this content.
1524 > *
1525 > * @category Tool Result Content
1526 > */
1527 > export interface ToolResultSubagentContent {
1528 > type: ToolResultContentType.Subagent;
1529 > /** Worker chat URI (subscribable for full chat state) */
1530 > resource: URI;
1531 > /** Display title for the subagent */
1532 > title: string;
1533 > /** Internal agent name */
1534 > agentName?: string;
1535 > /** Human-readable description of the subagent's task */
1536 > description?: string;
1537 > }
1538 >
1539 > /**
1540 > * Content block in a tool result.
1541 > *
1542 > * Mirrors the content blocks in MCP `CallToolResult.content`, plus
1543 > * `ToolResultResourceContent` for lazy-loading large results,
1544 > * `ToolResultFileEditContent` for file edit diffs,
1545 > * `ToolResultTerminalContent` for live terminal output and
1546 > * command completion metadata, and
1547 > * `ToolResultSubagentContent` for tool-spawned worker chats (AHP extensions).
1548 > *
1549 > * @category Tool Result Content
1550 > */
1551 > export type ToolResultContent =
1552 > | ToolResultTextContent
1553 > | ToolResultEmbeddedResourceContent
1554 > | ToolResultResourceContent
1555 > | ToolResultFileEditContent
1556 > | ToolResultTerminalContent
1557 > | ToolResultSubagentContent;
src/vs/platform/files/common/files.ts 1408 covered LOC · 63 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- files.ts
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 > import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from '../../../base/common/buffer.js';
7 > import { CancellationToken } from '../../../base/common/cancellation.js';
8 > import { Event } from '../../../base/common/event.js';
9 > import { IExpression, IRelativePattern } from '../../../base/common/glob.js';
10 > import { IDisposable } from '../../../base/common/lifecycle.js';
11 > import { TernarySearchTree } from '../../../base/common/ternarySearchTree.js';
12 > import { sep } from '../../../base/common/path.js';
13 > import { ReadableStreamEvents } from '../../../base/common/stream.js';
14 > import { startsWithIgnoreCase } from '../../../base/common/strings.js';
15 > import { isNumber } from '../../../base/common/types.js';
16 > import { URI } from '../../../base/common/uri.js';
17 > import { localize } from '../../../nls.js';
18 > import { createDecorator } from '../../instantiation/common/instantiation.js';
19 > import { isWeb } from '../../../base/common/platform.js';
20 > import { Schemas } from '../../../base/common/network.js';
21 > import { IMarkdownString } from '../../../base/common/htmlContent.js';
22 > import { Lazy } from '../../../base/common/lazy.js';
23 >
24 > //#region file service & providers
25 >
26 > export const IFileService = createDecorator<IFileService>('fileService');
27 >
28 > export interface IFileService {
29 >
30 > readonly _serviceBrand: undefined;
31 >
32 > /**
33 > * An event that is fired when a file system provider is added or removed
34 > */
35 > readonly onDidChangeFileSystemProviderRegistrations: Event<IFileSystemProviderRegistrationEvent>;
36 >
37 > /**
38 > * An event that is fired when a registered file system provider changes its capabilities.
39 > */
40 > readonly onDidChangeFileSystemProviderCapabilities: Event<IFileSystemProviderCapabilitiesChangeEvent>;
41 >
42 > /**
43 > * An event that is fired when a file system provider is about to be activated. Listeners
44 > * can join this event with a long running promise to help in the activation process.
45 > */
46 > readonly onWillActivateFileSystemProvider: Event<IFileSystemProviderActivationEvent>;
47 >
48 > /**
49 > * Registers a file system provider for a certain scheme.
50 > */
51 > registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable;
52 >
53 > /**
54 > * Returns a file system provider for a certain scheme.
55 > */
56 > getProvider(scheme: string): IFileSystemProvider | undefined;
57 >
58 > /**
59 > * Tries to activate a provider with the given scheme.
60 > */
61 > activateProvider(scheme: string): Promise<void>;
62 >
63 > /**
64 > * Checks if this file service can handle the given resource by
65 > * first activating any extension that wants to be activated
66 > * on the provided resource scheme to include extensions that
67 > * contribute file system providers for the given resource.
68 > */
69 > canHandleResource(resource: URI): Promise<boolean>;
70 >
71 > /**
72 > * Checks if the file service has a registered provider for the
73 > * provided resource.
74 > *
75 > * Note: this does NOT account for contributed providers from
76 > * extensions that have not been activated yet. To include those,
77 > * consider to call `await fileService.canHandleResource(resource)`.
78 > */
79 > hasProvider(resource: URI): boolean;
80 >
81 > /**
82 > * Checks if the provider for the provided resource has the provided file system capability.
83 > */
84 > hasCapability(resource: URI, capability: FileSystemProviderCapabilities): boolean;
85 >
86 > /**
87 > * List the schemes and capabilities for registered file system providers
88 > */
89 > listCapabilities(): Iterable<{ scheme: string; capabilities: FileSystemProviderCapabilities }>;
90 >
91 > /**
92 > * Allows to listen for file changes. The event will fire for every file within the opened workspace
93 > * (if any) as well as all files that have been watched explicitly using the #watch() API.
94 > */
95 > readonly onDidFilesChange: Event<FileChangesEvent>;
96 >
97 > /**
98 > * An event that is fired upon successful completion of a certain file operation.
99 > */
100 > readonly onDidRunOperation: Event<FileOperationEvent>;
101 >
102 > /**
103 > * Resolve the properties of a file/folder identified by the resource. For a folder, children
104 > * information is resolved as well depending on the provided options. Use `stat()` method if
105 > * you do not need children information.
106 > *
107 > * If the optional parameter "resolveTo" is specified in options, the stat service is asked
108 > * to provide a stat object that should contain the full graph of folders up to all of the
109 > * target resources.
110 > *
111 > * If the optional parameter "resolveSingleChildDescendants" is specified in options,
112 > * the stat service is asked to automatically resolve child folders that only
113 > * contain a single element.
114 > *
115 > * If the optional parameter "resolveMetadata" is specified in options,
116 > * the stat will contain metadata information such as size, mtime and etag.
117 > */
118 > resolve(resource: URI, options: IResolveMetadataFileOptions): Promise<IFileStatWithMetadata>;
119 > resolve(resource: URI, options?: IResolveFileOptions): Promise<IFileStat>;
120 >
121 > /**
122 > * Same as `resolve()` but supports resolving multiple resources in parallel.
123 > *
124 > * If one of the resolve targets fails to resolve returns a fake `IFileStat` instead of
125 > * making the whole call fail.
126 > */
127 > resolveAll(toResolve: { resource: URI; options: IResolveMetadataFileOptions }[]): Promise<IFileStatResult[]>;
128 > resolveAll(toResolve: { resource: URI; options?: IResolveFileOptions }[]): Promise<IFileStatResult[]>;
129 >
130 > /**
131 > * Same as `resolve()` but without resolving the children of a folder if the
132 > * resource is pointing to a folder.
133 > */
134 > stat(resource: URI): Promise<IFileStatWithPartialMetadata>;
135 >
136 > /**
137 > * Attempts to resolve the real path of the provided resource. The real path can be
138 > * different from the resource path for example when it is a symlink.
139 > *
140 > * Will return `undefined` if the real path cannot be resolved.
141 > */
142 > realpath(resource: URI): Promise<URI | undefined>;
143 >
144 > /**
145 > * Finds out if a file/folder identified by the resource exists.
146 > */
147 > exists(resource: URI): Promise<boolean>;
148 >
149 > /**
150 > * Read the contents of the provided resource unbuffered.
151 > */
152 > readFile(resource: URI, options?: IReadFileOptions, token?: CancellationToken): Promise<IFileContent>;
153 >
154 > /**
155 > * Read the contents of the provided resource buffered as stream.
156 > */
157 > readFileStream(resource: URI, options?: IReadFileStreamOptions, token?: CancellationToken): Promise<IFileStreamContent>;
158 >
159 > /**
160 > * Updates the content replacing its previous value.
161 > * If `options.append` is true, appends content to the end of the file instead.
162 > *
163 > * Emits a `FileOperation.WRITE` file operation event when successful.
164 > */
165 > writeFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise<IFileStatWithMetadata>;
166 >
167 > /**
168 > * Moves the file/folder to a new path identified by the resource.
169 > *
170 > * The optional parameter overwrite can be set to replace an existing file at the location.
171 > *
172 > * Emits a `FileOperation.MOVE` file operation event when successful.
173 > */
174 > move(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata>;
175 >
176 > /**
177 > * Find out if a move operation is possible given the arguments. No changes on disk will
178 > * be performed. Returns an Error if the operation cannot be done.
179 > */
180 > canMove(source: URI, target: URI, overwrite?: boolean): Promise<Error | true>;
181 >
182 > /**
183 > * Copies the file/folder to a path identified by the resource. A folder is copied
184 > * recursively.
185 > *
186 > * Emits a `FileOperation.COPY` file operation event when successful.
187 > */
188 > copy(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata>;
189 >
190 > /**
191 > * Find out if a copy operation is possible given the arguments. No changes on disk will
192 > * be performed. Returns an Error if the operation cannot be done.
193 > */
194 > canCopy(source: URI, target: URI, overwrite?: boolean): Promise<Error | true>;
195 >
196 > /**
197 > * Clones a file to a path identified by the resource. Folders are not supported.
198 > *
199 > * If the target path exists, it will be overwritten.
200 > */
201 > cloneFile(source: URI, target: URI): Promise<void>;
202 >
203 > /**
204 > * Creates a new file with the given path and optional contents. The returned promise
205 > * will have the stat model object as a result.
206 > *
207 > * The optional parameter content can be used as value to fill into the new file.
208 > *
209 > * Emits a `FileOperation.CREATE` file operation event when successful.
210 > */
211 > createFile(resource: URI, bufferOrReadableOrStream?: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: ICreateFileOptions): Promise<IFileStatWithMetadata>;
212 >
213 > /**
214 > * Find out if a file create operation is possible given the arguments. No changes on disk will
215 > * be performed. Returns an Error if the operation cannot be done.
216 > */
217 > canCreateFile(resource: URI, options?: ICreateFileOptions): Promise<Error | true>;
218 >
219 > /**
220 > * Creates a new folder with the given path. The returned promise
221 > * will have the stat model object as a result.
222 > *
223 > * Emits a `FileOperation.CREATE` file operation event when successful.
224 > */
225 > createFolder(resource: URI): Promise<IFileStatWithMetadata>;
226 >
227 > /**
228 > * Deletes the provided file. The optional useTrash parameter allows to
229 > * move the file to trash. The optional recursive parameter allows to delete
230 > * non-empty folders recursively.
231 > *
232 > * Emits a `FileOperation.DELETE` file operation event when successful.
233 > */
234 > del(resource: URI, options?: Partial<IFileDeleteOptions>): Promise<void>;
235 >
236 > /**
237 > * Find out if a delete operation is possible given the arguments. No changes on disk will
238 > * be performed. Returns an Error if the operation cannot be done.
239 > */
240 > canDelete(resource: URI, options?: Partial<IFileDeleteOptions>): Promise<Error | true>;
241 >
242 > /**
243 > * An event that signals an error when watching for file changes.
244 > */
245 > readonly onDidWatchError: Event<Error>;
246 >
247 > /**
248 > * Allows to start a watcher that reports file/folder change events on the provided resource.
249 > *
250 > * The watcher runs correlated and thus, file events will be reported on the returned
251 > * `IFileSystemWatcher` and not on the generic `IFileService.onDidFilesChange` event.
252 > *
253 > * Note: only non-recursive file watching supports event correlation for now.
254 > */
255 > createWatcher(resource: URI, options: IWatchOptionsWithoutCorrelation & { recursive: false }): IFileSystemWatcher;
256 >
257 > /**
258 > * Allows to start a watcher that reports file/folder change events on the provided resource.
259 > *
260 > * The watcher runs uncorrelated and thus will report all events from `IFileService.onDidFilesChange`.
261 > * This means, most listeners in the application will receive your events. It is encouraged to
262 > * use correlated watchers (via `IWatchOptionsWithCorrelation`) to limit events to your listener.
263 > */
264 > watch(resource: URI, options?: IWatchOptionsWithoutCorrelation): IDisposable;
265 >
266 > /**
267 > * Frees up any resources occupied by this service.
268 > */
269 > dispose(): void;
270 > }
271 >
272 > export interface IFileOverwriteOptions {
273 >
274 > /**
275 > * Set to `true` to overwrite a file if it exists. Will
276 > * throw an error otherwise if the file does exist.
277 > */
278 > readonly overwrite: boolean;
279 > }
280 >
281 > export interface IFileUnlockOptions {
282 >
283 > /**
284 > * Set to `true` to try to remove any write locks the file might
285 > * have. A file that is write locked will throw an error for any
286 > * attempt to write to unless `unlock: true` is provided.
287 > */
288 > readonly unlock: boolean;
289 > }
290 >
291 > export interface IFileAtomicReadOptions {
292 >
293 > /**
294 > * The optional `atomic` flag can be used to make sure
295 > * the `readFile` method is not running in parallel with
296 > * any `write` operations in the same process.
297 > *
298 > * Typically you should not need to use this flag but if
299 > * for example you are quickly reading a file right after
300 > * a file event occurred and the file changes a lot, there
301 > * is a chance that a read returns an empty or partial file
302 > * because a pending write has not finished yet.
303 > *
304 > * Note: this does not prevent the file from being written
305 > * to from a different process. If you need such atomic
306 > * operations, you better use a real database as storage.
307 > */
308 > readonly atomic: boolean;
309 > }
310 >
311 > export interface IFileAtomicOptions {
312 >
313 > /**
314 > * The postfix is used to create a temporary file based
315 > * on the original resource. The resulting temporary
316 > * file will be in the same folder as the resource and
317 > * have `postfix` appended to the resource name.
318 > *
319 > * Example: given a file resource `file:///some/path/foo.txt`
320 > * and a postfix `.vsctmp`, the temporary file will be
321 > * created as `file:///some/path/foo.txt.vsctmp`.
322 > */
323 > readonly postfix: string;
324 > }
325 >
326 > export interface IFileAtomicWriteOptions {
327 >
328 > /**
329 > * The optional `atomic` flag can be used to make sure
330 > * the `writeFile` method updates the target file atomically
331 > * by first writing to a temporary file in the same folder
332 > * and then renaming it over the target.
333 > */
334 > readonly atomic: IFileAtomicOptions | false;
335 > }
336 >
337 > export interface IFileAtomicDeleteOptions {
338 >
339 > /**
340 > * The optional `atomic` flag can be used to make sure
341 > * the `delete` method deletes the target atomically by
342 > * first renaming it to a temporary resource in the same
343 > * folder and then deleting it.
344 > */
345 > readonly atomic: IFileAtomicOptions | false;
346 > }
347 >
348 > export interface IFileReadLimits {
349 >
350 > /**
351 > * If the file exceeds the given size, an error of kind
352 > * `FILE_TOO_LARGE` will be thrown.
353 > */
354 > size?: number;
355 > }
356 >
357 > export interface IFileReadStreamOptions {
358 >
359 > /**
360 > * Is an integer specifying where to begin reading from in the file. If position is undefined,
361 > * data will be read from the current file position.
362 > */
363 > readonly position?: number;
364 >
365 > /**
366 > * Is an integer specifying how many bytes to read from the file. By default, all bytes
367 > * will be read.
368 > */
369 > readonly length?: number;
370 >
371 > /**
372 > * If provided, the size of the file will be checked against the limits
373 > * and an error will be thrown if any limit is exceeded.
374 > */
375 > readonly limits?: IFileReadLimits;
376 > }
377 >
378 > export interface IFileWriteOptions extends IFileOverwriteOptions, IFileUnlockOptions, IFileAtomicWriteOptions {
379 >
380 > /**
381 > * Set to `true` to create a file when it does not exist. Will
382 > * throw an error otherwise if the file does not exist.
383 > */
384 > readonly create: boolean;
385 >
386 > /**
387 > * Set to `true` to append content to the end of the file. Implies `create: true`,
388 > * and set only when the corresponding `FileAppend` capability is defined.
389 > */
390 > readonly append?: boolean;
391 > }
392 >
393 > export type IFileOpenOptions = IFileOpenForReadOptions | IFileOpenForWriteOptions;
394 >
395 > export function isFileOpenForWriteOptions(options: IFileOpenOptions): options is IFileOpenForWriteOptions {
396 return options.create === true;
397 }
398 > files.ts
399 > export interface IFileOpenForReadOptions {
400 >
401 > /**
402 > * A hint that the file should be opened for reading only.
403 > */
404 > readonly create: false;
405 > }
406 >
407 > export interface IFileOpenForWriteOptions extends IFileUnlockOptions {
408 >
409 > /**
410 > * A hint that the file should be opened for reading and writing.
411 > */
412 > readonly create: true;
413 >
414 > /**
415 > * Open the file in append mode. This will write data to the
416 > * end of the file.
417 > */
418 > readonly append?: boolean;
419 > }
420 >
421 > export interface IFileDeleteOptions {
422 >
423 > /**
424 > * Set to `true` to recursively delete any children of the file. This
425 > * only applies to folders and can lead to an error unless provided
426 > * if the folder is not empty.
427 > */
428 > readonly recursive: boolean;
429 >
430 > /**
431 > * Set to `true` to attempt to move the file to trash
432 > * instead of deleting it permanently from disk.
433 > *
434 > * This option maybe not be supported on all providers.
435 > */
436 > readonly useTrash: boolean;
437 >
438 > /**
439 > * The optional `atomic` flag can be used to make sure
440 > * the `delete` method deletes the target atomically by
441 > * first renaming it to a temporary resource in the same
442 > * folder and then deleting it.
443 > *
444 > * This option maybe not be supported on all providers.
445 > */
446 > readonly atomic: IFileAtomicOptions | false;
447 > }
448 >
449 > export enum FileType {
450 >
451 > /**
452 > * File is unknown (neither file, directory nor symbolic link).
453 > */
454 > Unknown = 0,
455 >
456 > /**
457 > * File is a normal file.
458 > */
459 > File = 1,
460 >
461 > /**
462 > * File is a directory.
463 > */
464 > Directory = 2,
465 >
466 > /**
467 > * File is a symbolic link.
468 > *
469 > * Note: even when the file is a symbolic link, you can test for
470 > * `FileType.File` and `FileType.Directory` to know the type of
471 > * the target the link points to.
472 > */
473 > SymbolicLink = 64
474 > }
475 >
476 > export enum FilePermission {
477 >
478 > /**
479 > * File is readonly. Components like editors should not
480 > * offer to edit the contents.
481 > */
482 > Readonly = 1,
483 >
484 > /**
485 > * File is locked. Components like editors should offer
486 > * to edit the contents and ask the user upon saving to
487 > * remove the lock.
488 > */
489 > Locked = 2,
490 >
491 > /**
492 > * File is executable. Relevant for Unix-like systems where
493 > * the executable bit determines if a file can be run.
494 > */
495 > Executable = 4
496 > }
497 >
498 > export interface IStat {
499 >
500 > /**
501 > * The file type.
502 > */
503 > readonly type: FileType;
504 >
505 > /**
506 > * The last modification date represented as millis from unix epoch.
507 > */
508 > readonly mtime: number;
509 >
510 > /**
511 > * The creation date represented as millis from unix epoch.
512 > */
513 > readonly ctime: number;
514 >
515 > /**
516 > * The size of the file in bytes.
517 > */
518 > readonly size: number;
519 >
520 > /**
521 > * The file permissions.
522 > */
523 > readonly permissions?: FilePermission;
524 > }
525 >
526 > export interface IWatchOptionsWithoutCorrelation {
527 >
528 > /**
529 > * Set to `true` to watch for changes recursively in a folder
530 > * and all of its children.
531 > */
532 > recursive: boolean;
533 >
534 > /**
535 > * A set of glob patterns or paths to exclude from watching.
536 > * Paths can be relative or absolute and when relative are
537 > * resolved against the watched folder. Glob patterns are
538 > * always matched relative to the watched folder.
539 > */
540 > excludes: string[];
541 >
542 > /**
543 > * An optional set of glob patterns or paths to include for
544 > * watching. If not provided, all paths are considered for
545 > * events.
546 > * Paths can be relative or absolute and when relative are
547 > * resolved against the watched folder. Glob patterns are
548 > * always matched relative to the watched folder.
549 > */
550 > includes?: Array<string | IRelativePattern>;
551 >
552 > /**
553 > * If provided, allows to filter the events that the watcher should consider
554 > * for emitting. If not provided, all events are emitted.
555 > *
556 > * For example, to emit added and updated events, set to:
557 > * `FileChangeFilter.ADDED | FileChangeFilter.UPDATED`.
558 > */
559 > filter?: FileChangeFilter;
560 > }
561 >
562 > export interface IWatchOptions extends IWatchOptionsWithoutCorrelation {
563 >
564 > /**
565 > * If provided, file change events from the watcher that
566 > * are a result of this watch request will carry the same
567 > * id.
568 > */
569 > readonly correlationId?: number;
570 > }
571 >
572 > export const enum FileChangeFilter {
573 > UPDATED = 1 << 1,
574 > ADDED = 1 << 2,
575 > DELETED = 1 << 3
576 > }
577 >
578 > export interface IWatchOptionsWithCorrelation extends IWatchOptions {
579 > readonly correlationId: number;
580 > }
581 >
582 > export interface IFileSystemWatcher extends IDisposable {
583 >
584 > /**
585 > * An event which fires on file/folder change only for changes
586 > * that correlate to the watch request with matching correlation
587 > * identifier.
588 > */
589 > readonly onDidChange: Event<FileChangesEvent>;
590 > }
591 >
592 > export function isFileSystemWatcher(thing: unknown): thing is IFileSystemWatcher {
593 const candidate = thing as IFileSystemWatcher | undefined;
594
595 return !!candidate && typeof candidate.onDidChange === 'function';
596 }
597 > files.ts
598 > export const enum FileSystemProviderCapabilities {
599 >
600 > /**
601 > * No capabilities.
602 > */
603 > None = 0,
604 >
605 > /**
606 > * Provider supports unbuffered read/write.
607 > */
608 > FileReadWrite = 1 << 1,
609 >
610 > /**
611 > * Provider supports open/read/write/close low level file operations.
612 > */
613 > FileOpenReadWriteClose = 1 << 2,
614 >
615 > /**
616 > * Provider supports stream based reading.
617 > */
618 > FileReadStream = 1 << 4,
619 >
620 > /**
621 > * Provider supports copy operation.
622 > */
623 > FileFolderCopy = 1 << 3,
624 >
625 > /**
626 > * Provider is path case sensitive.
627 > */
628 > PathCaseSensitive = 1 << 10,
629 >
630 > /**
631 > * All files of the provider are readonly.
632 > */
633 > Readonly = 1 << 11,
634 >
635 > /**
636 > * Provider supports to delete via trash.
637 > */
638 > Trash = 1 << 12,
639 >
640 > /**
641 > * Provider support to unlock files for writing.
642 > */
643 > FileWriteUnlock = 1 << 13,
644 >
645 > /**
646 > * Provider support to read files atomically. This implies the
647 > * provider provides the `FileReadWrite` capability too.
648 > */
649 > FileAtomicRead = 1 << 14,
650 >
651 > /**
652 > * Provider support to write files atomically. This implies the
653 > * provider provides the `FileReadWrite` capability too.
654 > */
655 > FileAtomicWrite = 1 << 15,
656 >
657 > /**
658 > * Provider support to delete atomically.
659 > */
660 > FileAtomicDelete = 1 << 16,
661 >
662 > /**
663 > * Provider support to clone files atomically.
664 > */
665 > FileClone = 1 << 17,
666 >
667 > /**
668 > * Provider support to resolve real paths.
669 > */
670 > FileRealpath = 1 << 18,
671 >
672 > /**
673 > * Provider support to append to files.
674 > */
675 > FileAppend = 1 << 19
676 > }
677 >
678 > export interface IFileSystemProvider {
679 >
680 > readonly capabilities: FileSystemProviderCapabilities;
681 > readonly onDidChangeCapabilities: Event<void>;
682 >
683 > readonly onDidChangeFile: Event<readonly IFileChange[]>;
684 > readonly onDidWatchError?: Event<string>;
685 > watch(resource: URI, opts: IWatchOptions): IDisposable;
686 >
687 > stat(resource: URI): Promise<IStat>;
688 > mkdir(resource: URI): Promise<void>;
689 > readdir(resource: URI): Promise<[string, FileType][]>;
690 > delete(resource: URI, opts: IFileDeleteOptions): Promise<void>;
691 >
692 > rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void>;
693 > copy?(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void>;
694 >
695 > readFile?(resource: URI): Promise<Uint8Array>;
696 > writeFile?(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void>;
697 >
698 > readFileStream?(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array>;
699 >
700 > open?(resource: URI, opts: IFileOpenOptions): Promise<number>;
701 > close?(fd: number): Promise<void>;
702 > read?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number>;
703 > write?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number>;
704 >
705 > cloneFile?(from: URI, to: URI): Promise<void>;
706 > }
707 >
708 > export interface IFileSystemProviderWithFileReadWriteCapability extends IFileSystemProvider {
709 > readFile(resource: URI): Promise<Uint8Array>;
710 > writeFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void>;
711 > }
712 >
713 > export function hasReadWriteCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithFileReadWriteCapability {
714 > return !!(provider.capabilities & FileSystemProviderCapabilities.FileReadWrite); files.ts
715 > }
716 > files.ts
717 > export function hasFileAppendCapability(provider: IFileSystemProvider): boolean {
718 return !!(provider.capabilities & FileSystemProviderCapabilities.FileAppend);
719 }
720 > files.ts
721 > export interface IFileSystemProviderWithFileFolderCopyCapability extends IFileSystemProvider {
722 > copy(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void>;
723 > }
724 >
725 > export function hasFileFolderCopyCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithFileFolderCopyCapability {
726 return !!(provider.capabilities & FileSystemProviderCapabilities.FileFolderCopy);
727 }
728 > files.ts
729 > export interface IFileSystemProviderWithFileCloneCapability extends IFileSystemProvider {
730 > cloneFile(from: URI, to: URI): Promise<void>;
731 > }
732 >
733 > export function hasFileCloneCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithFileCloneCapability {
734 return !!(provider.capabilities & FileSystemProviderCapabilities.FileClone);
735 }
736 > files.ts
737 > export interface IFileSystemProviderWithFileRealpathCapability extends IFileSystemProvider {
738 > realpath(resource: URI): Promise<string>;
739 > }
740 >
741 > export function hasFileRealpathCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithFileRealpathCapability {
742 return !!(provider.capabilities & FileSystemProviderCapabilities.FileRealpath);
743 }
744 > files.ts
745 > export interface IFileSystemProviderWithOpenReadWriteCloseCapability extends IFileSystemProvider {
746 > open(resource: URI, opts: IFileOpenOptions): Promise<number>;
747 > close(fd: number): Promise<void>;
748 > read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number>;
749 > write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number>;
750 > }
751 >
752 > export function hasOpenReadWriteCloseCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithOpenReadWriteCloseCapability {
753 > return !!(provider.capabilities & FileSystemProviderCapabilities.FileOpenReadWriteClose); files.ts
754 > }
755 > files.ts
756 > export interface IFileSystemProviderWithFileReadStreamCapability extends IFileSystemProvider {
757 > readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array>;
758 > }
759 >
760 > export function hasFileReadStreamCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithFileReadStreamCapability {
761 return !!(provider.capabilities & FileSystemProviderCapabilities.FileReadStream);
762 }
763 > files.ts
764 > export interface IFileSystemProviderWithFileAtomicReadCapability extends IFileSystemProvider {
765 > readFile(resource: URI, opts?: IFileAtomicReadOptions): Promise<Uint8Array>;
766 > enforceAtomicReadFile?(resource: URI): boolean;
767 > }
768 >
769 > export function hasFileAtomicReadCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithFileAtomicReadCapability {
770 if (!hasReadWriteCapability(provider)) {
771 return false; // we require the `FileReadWrite` capability too
774 return !!(provider.capabilities & FileSystemProviderCapabilities.FileAtomicRead);
775 }
776 > files.ts
777 > export interface IFileSystemProviderWithFileAtomicWriteCapability extends IFileSystemProvider {
778 > writeFile(resource: URI, contents: Uint8Array, opts?: IFileAtomicWriteOptions): Promise<void>;
779 > enforceAtomicWriteFile?(resource: URI): IFileAtomicOptions | false;
780 > }
781 >
782 > export function hasFileAtomicWriteCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithFileAtomicWriteCapability {
783 > if (!hasReadWriteCapability(provider)) { files.ts
784 return false; // we require the `FileReadWrite` capability too
785 }
786 > files.ts
787 > return !!(provider.capabilities & FileSystemProviderCapabilities.FileAtomicWrite);
788 > }
789 > files.ts
790 > export interface IFileSystemProviderWithFileAtomicDeleteCapability extends IFileSystemProvider {
791 > delete(resource: URI, opts: IFileAtomicDeleteOptions): Promise<void>;
792 > enforceAtomicDelete?(resource: URI): IFileAtomicOptions | false;
793 > }
794 >
795 > export function hasFileAtomicDeleteCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithFileAtomicDeleteCapability {
796 return !!(provider.capabilities & FileSystemProviderCapabilities.FileAtomicDelete);
797 }
798 > files.ts
799 > export interface IFileSystemProviderWithReadonlyCapability extends IFileSystemProvider {
800 >
801 > readonly capabilities: FileSystemProviderCapabilities.Readonly & FileSystemProviderCapabilities;
802 >
803 > /**
804 > * An optional message to show in the UI to explain why the file system is readonly.
805 > */
806 > readonly readOnlyMessage?: IMarkdownString;
807 > }
808 >
809 > export function hasReadonlyCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithReadonlyCapability {
810 return !!(provider.capabilities & FileSystemProviderCapabilities.Readonly);
811 }
812 > files.ts
813 > export enum FileSystemProviderErrorCode {
814 > FileExists = 'EntryExists',
815 > FileNotFound = 'EntryNotFound',
816 > FileNotADirectory = 'EntryNotADirectory',
817 > FileIsADirectory = 'EntryIsADirectory',
818 > FileExceedsStorageQuota = 'EntryExceedsStorageQuota',
819 > FileTooLarge = 'EntryTooLarge',
820 > FileWriteLocked = 'EntryWriteLocked',
821 > NoPermissions = 'NoPermissions',
822 > Unavailable = 'Unavailable',
823 > Unknown = 'Unknown'
824 > }
825 >
826 > export interface IFileSystemProviderError extends Error {
827 > readonly name: string;
828 > readonly code: FileSystemProviderErrorCode;
829 > }
830 >
831 > export class FileSystemProviderError extends Error implements IFileSystemProviderError {
832 >
833 > static create(error: Error | string, code: FileSystemProviderErrorCode): FileSystemProviderError {
834 > const providerError = new FileSystemProviderError(error.toString(), code);
835 > markAsFileSystemProviderError(providerError, code);
836 >
837 > return providerError;
838 > }
839 >
840 > private constructor(message: string, readonly code: FileSystemProviderErrorCode) {
841 > super(message); files.ts
842 > }
843 > } files.ts
844 >
845 > export function createFileSystemProviderError(error: Error | string, code: FileSystemProviderErrorCode): FileSystemProviderError {
846 > return FileSystemProviderError.create(error, code); files.ts
847 > }
848 > files.ts
849 > export function ensureFileSystemProviderError(error?: Error): Error {
850 if (!error) {
851 return createFileSystemProviderError(localize('unknownError', "Unknown Error"), FileSystemProviderErrorCode.Unknown); // https://github.com/microsoft/vscode/issues/72798
854 return error;
855 }
856 > files.ts
857 > export function markAsFileSystemProviderError(error: Error, code: FileSystemProviderErrorCode): Error {
858 > error.name = code ? `${code} (FileSystemError)` : `FileSystemError`; files.ts
859 >
860 > return error;
861 > }
862 > files.ts
863 > export function toFileSystemProviderErrorCode(error: Error | undefined | null): FileSystemProviderErrorCode {
864 > files.ts
865 > // Guard against abuse
866 > if (!error) {
867 return FileSystemProviderErrorCode.Unknown;
868 }
869 > files.ts
870 > // FileSystemProviderError comes with the code
871 > if (error instanceof FileSystemProviderError) {
872 > return error.code; files.ts
873 > }
874
875 // Any other error, check for name match by assuming that the error
882 switch (match[1]) {
883 case FileSystemProviderErrorCode.FileExists: return FileSystemProviderErrorCode.FileExists;
884 > case FileSystemProviderErrorCode.FileIsADirectory: return FileSystemProviderErrorCode.FileIsADirectory; files.ts
885 > case FileSystemProviderErrorCode.FileNotADirectory: return FileSystemProviderErrorCode.FileNotADirectory;
886 > case FileSystemProviderErrorCode.FileNotFound: return FileSystemProviderErrorCode.FileNotFound;
887 > case FileSystemProviderErrorCode.FileTooLarge: return FileSystemProviderErrorCode.FileTooLarge;
888 > case FileSystemProviderErrorCode.FileWriteLocked: return FileSystemProviderErrorCode.FileWriteLocked;
889 > case FileSystemProviderErrorCode.NoPermissions: return FileSystemProviderErrorCode.NoPermissions;
890 > case FileSystemProviderErrorCode.Unavailable: return FileSystemProviderErrorCode.Unavailable;
891 > }
892
893 return FileSystemProviderErrorCode.Unknown;
894 }
895 > files.ts
896 > export function toFileOperationResult(error: Error): FileOperationResult {
897
898 // FileSystemProviderError comes with the result already
921 }
922 }
923 > files.ts
924 > export interface IFileSystemProviderRegistrationEvent {
925 > readonly added: boolean;
926 > readonly scheme: string;
927 > readonly provider?: IFileSystemProvider;
928 > }
929 >
930 > export interface IFileSystemProviderCapabilitiesChangeEvent {
931 > readonly provider: IFileSystemProvider;
932 > readonly scheme: string;
933 > }
934 >
935 > export interface IFileSystemProviderActivationEvent {
936 > readonly scheme: string;
937 > join(promise: Promise<void>): void;
938 > }
939 >
940 > export const enum FileOperation {
941 > CREATE,
942 > DELETE,
943 > MOVE,
944 > COPY,
945 > WRITE
946 > }
947 >
948 > export interface IFileOperationEvent {
949 >
950 > readonly resource: URI;
951 > readonly operation: FileOperation;
952 >
953 > isOperation(operation: FileOperation.DELETE | FileOperation.WRITE): boolean;
954 > isOperation(operation: FileOperation.CREATE | FileOperation.MOVE | FileOperation.COPY): this is IFileOperationEventWithMetadata;
955 > }
956 >
957 > export interface IFileOperationEventWithMetadata extends IFileOperationEvent {
958 > readonly target: IFileStatWithMetadata;
959 > }
960 >
961 > export class FileOperationEvent implements IFileOperationEvent {
962 >
963 > constructor(resource: URI, operation: FileOperation.DELETE | FileOperation.WRITE);
964 > constructor(resource: URI, operation: FileOperation.CREATE | FileOperation.MOVE | FileOperation.COPY, target: IFileStatWithMetadata);
965 > constructor(readonly resource: URI, readonly operation: FileOperation, readonly target?: IFileStatWithMetadata) { }
966 >
967 > isOperation(operation: FileOperation.DELETE | FileOperation.WRITE): boolean;
968 > isOperation(operation: FileOperation.CREATE | FileOperation.MOVE | FileOperation.COPY): this is IFileOperationEventWithMetadata;
969 > isOperation(operation: FileOperation): boolean {
970 return this.operation === operation;
971 }
972 > } files.ts
973 >
974 > /**
975 > * Possible changes that can occur to a file.
976 > */
977 > export const enum FileChangeType {
978 > UPDATED,
979 > ADDED,
980 > DELETED
981 > }
982 >
983 > /**
984 > * Identifies a single change in a file.
985 > */
986 > export interface IFileChange {
987 >
988 > /**
989 > * The type of change that occurred to the file.
990 > */
991 > type: FileChangeType;
992 >
993 > /**
994 > * The unified resource identifier of the file that changed.
995 > */
996 > readonly resource: URI;
997 >
998 > /**
999 > * If provided when starting the file watcher, the correlation
1000 > * identifier will match the original file watching request as
1001 > * a way to identify the original component that is interested
1002 > * in the change.
1003 > */
1004 > readonly cId?: number;
1005 > }
1006 >
1007 > export class FileChangesEvent {
1008 >
1009 > private static readonly MIXED_CORRELATION = null;
1010 >
1011 > private readonly correlationId: number | undefined | typeof FileChangesEvent.MIXED_CORRELATION = undefined;
1012 >
1013 > constructor(changes: readonly IFileChange[], private readonly ignorePathCasing: boolean) {
1014 > for (const change of changes) { files.ts
1015 >
1016 > // Split by type
1017 > switch (change.type) {
1018 > case FileChangeType.ADDED:
1019 > this.rawAdded.push(change.resource); files.ts
1020 > break;
1021 > case FileChangeType.UPDATED: files.ts
1022 > this.rawUpdated.push(change.resource); files.ts
1023 > break;
1024 > case FileChangeType.DELETED: files.ts
1025 this.rawDeleted.push(change.resource);
1026 break;
1027 > } files.ts
1028 >
1029 > // Figure out events correlation
1030 > if (this.correlationId !== FileChangesEvent.MIXED_CORRELATION) {
1031 > if (typeof change.cId === 'number') {
1032 if (this.correlationId === undefined) {
1033 this.correlationId = change.cId; // correlation not yet set, just take it
1035 this.correlationId = FileChangesEvent.MIXED_CORRELATION; // correlation mismatch, we have mixed correlation
1036 }
1037 > } else { files.ts
1038 > if (this.correlationId !== undefined) {
1039 this.correlationId = FileChangesEvent.MIXED_CORRELATION; // correlation mismatch, we have mixed correlation
1040 }
1041 > } files.ts
1042 > }
1043 > }
1044 > }
1045 > files.ts
1046 > private readonly added = new Lazy(() => {
1047 const added = TernarySearchTree.forUris<boolean>(() => this.ignorePathCasing);
1048 added.fill(this.rawAdded.map(resource => [resource, true]));
1049
1050 return added;
1051 > }); files.ts
1052 >
1053 > private readonly updated = new Lazy(() => {
1054 const updated = TernarySearchTree.forUris<boolean>(() => this.ignorePathCasing);
1055 updated.fill(this.rawUpdated.map(resource => [resource, true]));
1056
1057 return updated;
1058 > }); files.ts
1059 >
1060 > private readonly deleted = new Lazy(() => {
1061 const deleted = TernarySearchTree.forUris<boolean>(() => this.ignorePathCasing);
1062 deleted.fill(this.rawDeleted.map(resource => [resource, true]));
1063
1064 return deleted;
1065 > }); files.ts
1066 >
1067 > /**
1068 > * Find out if the file change events match the provided resource.
1069 > *
1070 > * Note: when passing `FileChangeType.DELETED`, we consider a match
1071 > * also when the parent of the resource got deleted.
1072 > */
1073 > contains(resource: URI, ...types: FileChangeType[]): boolean {
1074 return this.doContains(resource, { includeChildren: false }, ...types);
1075 }
1076 > files.ts
1077 > /**
1078 > * Find out if the file change events either match the provided
1079 > * resource, or contain a child of this resource.
1080 > */
1081 > affects(resource: URI, ...types: FileChangeType[]): boolean {
1082 return this.doContains(resource, { includeChildren: true }, ...types);
1083 }
1084 > files.ts
1085 > private doContains(resource: URI, options: { includeChildren: boolean }, ...types: FileChangeType[]): boolean {
1086 if (!resource) {
1087 return false;
1125 return false;
1126 }
1127 > files.ts
1128 > /**
1129 > * Returns if this event contains added files.
1130 > */
1131 > gotAdded(): boolean {
1132 return this.rawAdded.length > 0;
1133 }
1134 > files.ts
1135 > /**
1136 > * Returns if this event contains deleted files.
1137 > */
1138 > gotDeleted(): boolean {
1139 return this.rawDeleted.length > 0;
1140 }
1141 > files.ts
1142 > /**
1143 > * Returns if this event contains updated files.
1144 > */
1145 > gotUpdated(): boolean {
1146 return this.rawUpdated.length > 0;
1147 }
1148 > files.ts
1149 > /**
1150 > * Returns if this event contains changes that correlate to the
1151 > * provided `correlationId`.
1152 > *
1153 > * File change event correlation is an advanced watch feature that
1154 > * allows to identify from which watch request the events originate
1155 > * from. This correlation allows to route events specifically
1156 > * only to the requestor and not emit them to all listeners.
1157 > */
1158 > correlates(correlationId: number): boolean {
1159 return this.correlationId === correlationId;
1160 }
1161 > files.ts
1162 > /**
1163 > * Figure out if the event contains changes that correlate to one
1164 > * correlation identifier.
1165 > *
1166 > * File change event correlation is an advanced watch feature that
1167 > * allows to identify from which watch request the events originate
1168 > * from. This correlation allows to route events specifically
1169 > * only to the requestor and not emit them to all listeners.
1170 > */
1171 > hasCorrelation(): boolean {
1172 > return typeof this.correlationId === 'number'; files.ts
1173 > }
1174 > files.ts
1175 > /**
1176 > * @deprecated use the `contains` or `affects` method to efficiently find
1177 > * out if the event relates to a given resource. these methods ensure:
1178 > * - that there is no expensive lookup needed (by using a `TernarySearchTree`)
1179 > * - correctly handles `FileChangeType.DELETED` events
1180 > */
1181 > readonly rawAdded: URI[] = [];
1182 >
1183 > /**
1184 > * @deprecated use the `contains` or `affects` method to efficiently find
1185 > * out if the event relates to a given resource. these methods ensure:
1186 > * - that there is no expensive lookup needed (by using a `TernarySearchTree`)
1187 > * - correctly handles `FileChangeType.DELETED` events
1188 > */
1189 > readonly rawUpdated: URI[] = [];
1190 >
1191 > /**
1192 > * @deprecated use the `contains` or `affects` method to efficiently find
1193 > * out if the event relates to a given resource. these methods ensure:
1194 > * - that there is no expensive lookup needed (by using a `TernarySearchTree`)
1195 > * - correctly handles `FileChangeType.DELETED` events
1196 > */
1197 > readonly rawDeleted: URI[] = [];
1198 > }
1199 >
1200 > export function isParent(path: string, candidate: string, ignoreCase?: boolean): boolean {
1201 if (!path || !candidate || path === candidate) {
1202 return false;
1217 return path.indexOf(candidate) === 0;
1218 }
1219 > files.ts
1220 > export interface IBaseFileStat {
1221 >
1222 > /**
1223 > * The unified resource identifier of this file or folder.
1224 > */
1225 > readonly resource: URI;
1226 >
1227 > /**
1228 > * The name which is the last segment
1229 > * of the {{path}}.
1230 > */
1231 > readonly name: string;
1232 >
1233 > /**
1234 > * The size of the file.
1235 > *
1236 > * The value may or may not be resolved as
1237 > * it is optional.
1238 > */
1239 > readonly size?: number;
1240 >
1241 > /**
1242 > * The last modification date represented as millis from unix epoch.
1243 > *
1244 > * The value may or may not be resolved as
1245 > * it is optional.
1246 > */
1247 > readonly mtime?: number;
1248 >
1249 > /**
1250 > * The creation date represented as millis from unix epoch.
1251 > *
1252 > * The value may or may not be resolved as
1253 > * it is optional.
1254 > */
1255 > readonly ctime?: number;
1256 >
1257 > /**
1258 > * A unique identifier that represents the
1259 > * current state of the file or directory.
1260 > *
1261 > * The value may or may not be resolved as
1262 > * it is optional.
1263 > */
1264 > readonly etag?: string;
1265 >
1266 > /**
1267 > * File is readonly. Components like editors should not
1268 > * offer to edit the contents.
1269 > */
1270 > readonly readonly?: boolean;
1271 >
1272 > /**
1273 > * File is locked. Components like editors should offer
1274 > * to edit the contents and ask the user upon saving to
1275 > * remove the lock.
1276 > */
1277 > readonly locked?: boolean;
1278 >
1279 > /**
1280 > * File is executable. Relevant for Unix-like systems where
1281 > * the executable bit determines if a file can be run.
1282 > */
1283 > readonly executable?: boolean;
1284 > }
1285 >
1286 > export interface IBaseFileStatWithMetadata extends Required<IBaseFileStat> { }
1287 >
1288 > /**
1289 > * A file resource with meta information and resolved children if any.
1290 > */
1291 > export interface IFileStat extends IBaseFileStat {
1292 >
1293 > /**
1294 > * The resource is a file.
1295 > */
1296 > readonly isFile: boolean;
1297 >
1298 > /**
1299 > * The resource is a directory.
1300 > */
1301 > readonly isDirectory: boolean;
1302 >
1303 > /**
1304 > * The resource is a symbolic link. Note: even when the
1305 > * file is a symbolic link, you can test for `FileType.File`
1306 > * and `FileType.Directory` to know the type of the target
1307 > * the link points to.
1308 > */
1309 > readonly isSymbolicLink: boolean;
1310 >
1311 > /**
1312 > * The children of the file stat or undefined if none.
1313 > */
1314 > children: IFileStat[] | undefined;
1315 > }
1316 >
1317 > export interface IFileStatWithMetadata extends IFileStat, IBaseFileStatWithMetadata {
1318 > readonly mtime: number;
1319 > readonly ctime: number;
1320 > readonly etag: string;
1321 > readonly size: number;
1322 > readonly readonly: boolean;
1323 > readonly locked: boolean;
1324 > readonly executable: boolean;
1325 > readonly children: IFileStatWithMetadata[] | undefined;
1326 > }
1327 >
1328 > export interface IFileStatResult {
1329 > readonly stat?: IFileStat;
1330 > readonly success: boolean;
1331 > }
1332 >
1333 > export interface IFileStatResultWithMetadata extends IFileStatResult {
1334 > readonly stat?: IFileStatWithMetadata;
1335 > }
1336 >
1337 > export interface IFileStatWithPartialMetadata extends Omit<IFileStatWithMetadata, 'children'> { }
1338 >
1339 > export interface IFileContent extends IBaseFileStatWithMetadata {
1340 >
1341 > /**
1342 > * The content of a file as buffer.
1343 > */
1344 > readonly value: VSBuffer;
1345 > }
1346 >
1347 > export interface IFileStreamContent extends IBaseFileStatWithMetadata {
1348 >
1349 > /**
1350 > * The content of a file as stream.
1351 > */
1352 > readonly value: VSBufferReadableStream;
1353 > }
1354 >
1355 > export interface IBaseReadFileOptions extends IFileReadStreamOptions {
1356 >
1357 > /**
1358 > * The optional etag parameter allows to return early from resolving the resource if
1359 > * the contents on disk match the etag. This prevents accumulated reading of resources
1360 > * that have been read already with the same etag.
1361 > * It is the task of the caller to makes sure to handle this error case from the promise.
1362 > */
1363 > readonly etag?: string;
1364 > }
1365 >
1366 > export interface IReadFileStreamOptions extends IBaseReadFileOptions { }
1367 >
1368 > export interface IReadFileOptions extends IBaseReadFileOptions {
1369 >
1370 > /**
1371 > * The optional `atomic` flag can be used to make sure
1372 > * the `readFile` method is not running in parallel with
1373 > * any `write` operations in the same process.
1374 > *
1375 > * Typically you should not need to use this flag but if
1376 > * for example you are quickly reading a file right after
1377 > * a file event occurred and the file changes a lot, there
1378 > * is a chance that a read returns an empty or partial file
1379 > * because a pending write has not finished yet.
1380 > *
1381 > * Note: this does not prevent the file from being written
1382 > * to from a different process. If you need such atomic
1383 > * operations, you better use a real database as storage.
1384 > */
1385 > readonly atomic?: boolean;
1386 > }
1387 >
1388 > export interface IWriteFileOptions {
1389 >
1390 > /**
1391 > * The last known modification time of the file. This can be used to prevent dirty writes.
1392 > */
1393 > readonly mtime?: number;
1394 >
1395 > /**
1396 > * The etag of the file. This can be used to prevent dirty writes.
1397 > */
1398 > readonly etag?: string;
1399 >
1400 > /**
1401 > * Whether to attempt to unlock a file before writing.
1402 > */
1403 > readonly unlock?: boolean;
1404 >
1405 > /**
1406 > * The optional `atomic` flag can be used to make sure
1407 > * the `writeFile` method updates the target file atomically
1408 > * by first writing to a temporary file in the same folder
1409 > * and then renaming it over the target.
1410 > */
1411 > readonly atomic?: IFileAtomicOptions | false;
1412 >
1413 > /**
1414 > * If set to true, will append to the end of the file instead of
1415 > * replacing its contents. Will create the file if it doesn't exist.
1416 > */
1417 > readonly append?: boolean;
1418 > }
1419 >
1420 > export interface IResolveFileOptions {
1421 >
1422 > /**
1423 > * Automatically continue resolving children of a directory until the provided resources
1424 > * are found.
1425 > */
1426 > readonly resolveTo?: readonly URI[];
1427 >
1428 > /**
1429 > * Automatically continue resolving children of a directory if the number of children is 1.
1430 > */
1431 > readonly resolveSingleChildDescendants?: boolean;
1432 >
1433 > /**
1434 > * Will resolve mtime, ctime, size and etag of files if enabled. This can have a negative impact
1435 > * on performance and thus should only be used when these values are required.
1436 > */
1437 > readonly resolveMetadata?: boolean;
1438 > }
1439 >
1440 > export interface IResolveMetadataFileOptions extends IResolveFileOptions {
1441 > readonly resolveMetadata: true;
1442 > }
1443 >
1444 > export interface ICreateFileOptions {
1445 >
1446 > /**
1447 > * Overwrite the file to create if it already exists on disk. Otherwise
1448 > * an error will be thrown (FILE_MODIFIED_SINCE).
1449 > */
1450 > readonly overwrite?: boolean;
1451 > }
1452 >
1453 > export class FileOperationError extends Error {
1454 > constructor(
1455 message: string,
1456 readonly fileOperationResult: FileOperationResult,
1459 super(message);
1460 }
1461 > } files.ts
1462 >
1463 > export class TooLargeFileOperationError extends FileOperationError {
1464 > constructor(
1465 message: string,
1466 override readonly fileOperationResult: FileOperationResult.FILE_TOO_LARGE,
1470 super(message, fileOperationResult, options);
1471 }
1472 > } files.ts
1473 >
1474 > export class NotModifiedSinceFileOperationError extends FileOperationError {
1475 >
1476 > constructor(
1477 message: string,
1478 readonly stat: IFileStatWithMetadata,
1481 super(message, FileOperationResult.FILE_NOT_MODIFIED_SINCE, options);
1482 }
1483 > } files.ts
1484 >
1485 > export const enum FileOperationResult {
1486 > FILE_IS_DIRECTORY,
1487 > FILE_NOT_FOUND,
1488 > FILE_NOT_MODIFIED_SINCE,
1489 > FILE_MODIFIED_SINCE,
1490 > FILE_MOVE_CONFLICT,
1491 > FILE_WRITE_LOCKED,
1492 > FILE_PERMISSION_DENIED,
1493 > FILE_TOO_LARGE,
1494 > FILE_INVALID_PATH,
1495 > FILE_NOT_DIRECTORY,
1496 > FILE_OTHER_ERROR
1497 > }
1498 >
1499 > //#endregion
1500 >
1501 > //#region Settings
1502 >
1503 > export const AutoSaveConfiguration = {
1504 > OFF: 'off',
1505 > AFTER_DELAY: 'afterDelay',
1506 > ON_FOCUS_CHANGE: 'onFocusChange',
1507 > ON_WINDOW_CHANGE: 'onWindowChange'
1508 > };
1509 >
1510 > export const HotExitConfiguration = {
1511 > OFF: 'off',
1512 > ON_EXIT: 'onExit',
1513 > ON_EXIT_AND_WINDOW_CLOSE: 'onExitAndWindowClose'
1514 > };
1515 >
1516 > export const FILES_ASSOCIATIONS_CONFIG = 'files.associations';
1517 > export const FILES_EXCLUDE_CONFIG = 'files.exclude';
1518 > export const FILES_READONLY_INCLUDE_CONFIG = 'files.readonlyInclude';
1519 > export const FILES_READONLY_EXCLUDE_CONFIG = 'files.readonlyExclude';
1520 > export const FILES_READONLY_FROM_PERMISSIONS_CONFIG = 'files.readonlyFromPermissions';
1521 >
1522 > export interface IGlobPatterns {
1523 > [filepattern: string]: boolean;
1524 > }
1525 >
1526 > export interface IFilesConfiguration {
1527 > files?: IFilesConfigurationNode;
1528 > }
1529 >
1530 > export interface IFilesConfigurationNode {
1531 > associations: { [filepattern: string]: string };
1532 > exclude: IExpression;
1533 > watcherExclude: IGlobPatterns;
1534 > watcherInclude: string[];
1535 > encoding: string;
1536 > autoGuessEncoding: boolean;
1537 > candidateGuessEncodings: string[];
1538 > defaultLanguage: string;
1539 > trimTrailingWhitespace: boolean;
1540 > autoSave: string;
1541 > autoSaveDelay: number;
1542 > autoSaveWorkspaceFilesOnly: boolean;
1543 > autoSaveWhenNoErrors: boolean;
1544 > eol: string;
1545 > enableTrash: boolean;
1546 > hotExit: string;
1547 > saveConflictResolution: 'askUser' | 'overwriteFileOnDisk';
1548 > readonlyInclude: IGlobPatterns;
1549 > readonlyExclude: IGlobPatterns;
1550 > readonlyFromPermissions: boolean;
1551 > }
1552 >
1553 > //#endregion
1554 >
1555 > //#region Utilities
1556 >
1557 > export enum FileKind {
1558 > FILE,
1559 > FOLDER,
1560 > ROOT_FOLDER
1561 > }
1562 >
1563 > /**
1564 > * A hint to disable etag checking for reading/writing.
1565 > */
1566 > export const ETAG_DISABLED = '';
1567 >
1568 > export function etag(stat: { mtime: number; size: number }): string;
1569 > export function etag(stat: { mtime: number | undefined; size: number | undefined }): string | undefined;
1570 > export function etag(stat: { mtime: number | undefined; size: number | undefined }): string | undefined {
1571 > if (typeof stat.size !== 'number' || typeof stat.mtime !== 'number') { files.ts
1572 return undefined;
1573 }
1574 > files.ts
1575 > return stat.mtime.toString(29) + stat.size.toString(31);
1576 > }
1577 > files.ts
1578 export async function whenProviderRegistered(file: URI, fileService: IFileService): Promise<void> {
1579 if (fileService.hasProvider(URI.from({ scheme: file.scheme }))) {
1590 });
1591 }
1592 > files.ts
1593 > /**
1594 > * Helper to format a raw byte size into a human readable label.
1595 > */
1596 > export class ByteSize {
1597 >
1598 > static readonly KB = 1024;
1599 > static readonly MB = ByteSize.KB * ByteSize.KB;
1600 > static readonly GB = ByteSize.MB * ByteSize.KB;
1601 > static readonly TB = ByteSize.GB * ByteSize.KB;
1602 >
1603 > static formatSize(size: number): string {
1604 if (!isNumber(size)) {
1605 size = 0;
1624 return localize('sizeTB', "{0}TB", (size / ByteSize.TB).toFixed(2));
1625 }
1626 > } files.ts
1627 >
1628 > // File limits
1629 >
1630 > export function getLargeFileConfirmationLimit(remoteAuthority?: string): number;
1631 > export function getLargeFileConfirmationLimit(uri?: URI): number;
1632 > export function getLargeFileConfirmationLimit(arg?: string | URI): number {
1633 const isRemote = typeof arg === 'string' || arg?.scheme === Schemas.vscodeRemote;
1634 const isLocal = typeof arg !== 'string' && arg?.scheme === Schemas.file;
1655 return 1024 * ByteSize.MB;
1656 }
1657 > files.ts
1658 > //#endregion
src/vs/base/common/async.ts 1367 covered LOC · 235 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- async.ts
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 > import { CancellationToken, CancellationTokenSource } from './cancellation.js';
7 > import { BugIndicatingError, CancellationError, isCancellationError } from './errors.js';
8 > import { Emitter, Event } from './event.js';
9 > import { Disposable, DisposableMap, DisposableStore, IDisposable, isDisposable, MutableDisposable, toDisposable } from './lifecycle.js';
10 > import { extUri as defaultExtUri, IExtUri } from './resources.js';
11 > import { URI } from './uri.js';
12 > import { setTimeout0 } from './platform.js';
13 > import { MicrotaskDelay } from './symbols.js';
14 > import { Lazy } from './lazy.js';
15 >
16 > export function isThenable<T>(obj: unknown): obj is Promise<T> {
17 return !!obj && typeof (obj as unknown as Promise<T>).then === 'function';
18 }
19 > async.ts
20 > export interface CancelablePromise<T> extends Promise<T> {
21 > cancel(): void;
22 > }
23 >
24 > /**
25 > * Returns a promise that can be cancelled using the provided cancellation token.
26 > *
27 > * @remarks When cancellation is requested, the promise will be rejected with a {@link CancellationError}.
28 > * If the promise resolves to a disposable object, it will be automatically disposed when cancellation
29 > * is requested.
30 > *
31 > * @param callback A function that accepts a cancellation token and returns a promise
32 > * @returns A promise that can be cancelled
33 > */
34 > export function createCancelablePromise<T>(callback: (token: CancellationToken) => Promise<T>): CancelablePromise<T> {
35 const source = new CancellationTokenSource();
36
80 };
81 }
82 > async.ts
83 > /**
84 > * Returns a promise that resolves with `undefined` as soon as the passed token is cancelled.
85 > * @see {@link raceCancellationError}
86 > */
87 > export function raceCancellation<T>(promise: Promise<T>, token: CancellationToken): Promise<T | undefined>;
88 >
89 > /**
90 > * Returns a promise that resolves with `defaultValue` as soon as the passed token is cancelled.
91 > * @see {@link raceCancellationError}
92 > */
93 > export function raceCancellation<T>(promise: Promise<T>, token: CancellationToken, defaultValue: T): Promise<T>;
94 >
95 > export function raceCancellation<T>(promise: Promise<T>, token: CancellationToken, defaultValue?: T): Promise<T | undefined> {
96 return new Promise((resolve, reject) => {
97 const ref = token.onCancellationRequested(() => {
102 });
103 }
104 > async.ts
105 > /**
106 > * Returns a promise that rejects with an {@CancellationError} as soon as the passed token is cancelled.
107 > * @see {@link raceCancellation}
108 > */
109 > export function raceCancellationError<T>(promise: Promise<T>, token: CancellationToken): Promise<T> {
110 return new Promise((resolve, reject) => {
111 const ref = token.onCancellationRequested(() => {
116 });
117 }
118 > async.ts
119 > export function rejectIfNotCanceled(err: unknown): undefined {
120 if (isCancellationError(err)) {
121 return undefined;
123 return Promise.reject(err) as never;
124 }
125 > async.ts
126 > /**
127 > * Wraps a cancellable promise such that it is no cancellable. Can be used to
128 > * avoid issues with shared promises that would normally be returned as
129 > * cancellable to consumers.
130 > */
131 > export function notCancellablePromise<T>(promise: CancelablePromise<T>): Promise<T> {
132 return new Promise<T>((resolve, reject) => {
133 promise.then(resolve, reject);
134 });
135 }
136 > async.ts
137 > /**
138 > * Returns as soon as one of the promises resolves or rejects and cancels remaining promises
139 > */
140 > export function raceCancellablePromises<T>(cancellablePromises: (CancelablePromise<T> | Promise<T>)[]): CancelablePromise<T> {
141 let resolvedPromiseIndex = -1;
142 const promises = cancellablePromises.map((promise, index) => promise.then(result => { resolvedPromiseIndex = index; return result; }));
154 return promise;
155 }
156 > async.ts
157 > export function raceTimeout<T>(promise: Promise<T>, timeout: number, onTimeout?: () => void): Promise<T | undefined> {
158 let promiseResolve: ((value: T | undefined) => void) | undefined = undefined;
159
168 ]);
169 }
170 > async.ts
171 > export function asPromise<T>(callback: () => T | Thenable<T>): Promise<T> {
172 return new Promise<T>((resolve, reject) => {
173 const item = callback();
179 });
180 }
181 > async.ts
182 > /**
183 > * Creates and returns a new promise, plus its `resolve` and `reject` callbacks.
184 > *
185 > * Replace with standardized [`Promise.withResolvers`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers) once it is supported
186 > */
187 > export function promiseWithResolvers<T>(): { promise: Promise<T>; resolve: (value: T | PromiseLike<T>) => void; reject: (err?: any) => void } {
188 let resolve: (value: T | PromiseLike<T>) => void;
189 let reject: (reason?: any) => void;
194 return { promise, resolve: resolve!, reject: reject! };
195 }
196 > async.ts
197 > export interface ITask<T> {
198 > (): T;
199 > }
200 >
201 > export interface ICancellableTask<T> {
202 > (token: CancellationToken): T;
203 > }
204 >
205 > /**
206 > * A helper to prevent accumulation of sequential async tasks.
207 > *
208 > * Imagine a mail man with the sole task of delivering letters. As soon as
209 > * a letter submitted for delivery, he drives to the destination, delivers it
210 > * and returns to his base. Imagine that during the trip, N more letters were submitted.
211 > * When the mail man returns, he picks those N letters and delivers them all in a
212 > * single trip. Even though N+1 submissions occurred, only 2 deliveries were made.
213 > *
214 > * The throttler implements this via the queue() method, by providing it a task
215 > * factory. Following the example:
216 > *
217 > * const throttler = new Throttler();
218 > * const letters = [];
219 > *
220 > * function deliver() {
221 > * const lettersToDeliver = letters;
222 > * letters = [];
223 > * return makeTheTrip(lettersToDeliver);
224 > * }
225 > *
226 > * function onLetterReceived(l) {
227 > * letters.push(l);
228 > * throttler.queue(deliver);
229 > * }
230 > */
231 > export class Throttler implements IDisposable {
232 >
233 > private activePromise: Promise<any> | null;
234 > private queuedPromise: Promise<any> | null;
235 > private queuedPromiseFactory: ICancellableTask<Promise<any>> | null;
236 > private cancellationTokenSource: CancellationTokenSource;
237 >
238 > constructor() {
239 this.activePromise = null;
240 this.queuedPromise = null;
243 this.cancellationTokenSource = new CancellationTokenSource();
244 }
245 > async.ts
246 > queue<T>(promiseFactory: ICancellableTask<Promise<T>>): Promise<T> {
247 if (this.cancellationTokenSource.token.isCancellationRequested) {
248 return Promise.reject(new Error('Throttler is disposed'));
288 });
289 }
290 > async.ts
291 > dispose(): void {
292 this.cancellationTokenSource.cancel();
293 }
294 > } async.ts
295 >
296 > export class Sequencer {
297
298 private current: Promise<unknown> = Promise.resolve(null);
299 > async.ts
300 > queue<T>(promiseTask: ITask<Promise<T>>): Promise<T> {
301 return this.current = this.current.then(() => promiseTask(), () => promiseTask());
302 }
303 > } async.ts
304 >
305 > /**
306 > * A {@link Throttler} per key. Calls for the same key coalesce (only the most
307 > * recently queued task runs after the active one settles); calls for different
308 > * keys are independent. Idle keys are cleaned up automatically.
309 > */
310 > export class ThrottlerByKey<TKey> implements IDisposable {
311 > async.ts
312 > private readonly throttlers = new Map<TKey, { throttler: Throttler; count: number }>();
313 > async.ts
314 > queue<T>(key: TKey, task: ITask<Promise<T>>): Promise<T> {
315 let entry = this.throttlers.get(key);
316 if (!entry) {
327 });
328 }
329 > async.ts
330 > dispose(): void {
331 > for (const { throttler } of this.throttlers.values()) { async.ts
332 throttler.dispose();
333 }
334 > this.throttlers.clear(); async.ts
335 > }
336 > } async.ts
337 >
338 > export class SequencerByKey<TKey> {
339 > async.ts
340 > private promiseMap = new Map<TKey, Promise<unknown>>();
341 > async.ts
342 > queue<T>(key: TKey, promiseTask: ITask<Promise<T>>): Promise<T> {
343 const runningPromise = this.promiseMap.get(key) ?? Promise.resolve();
344 const newPromise = runningPromise
353 return newPromise;
354 }
355 > async.ts
356 > peek(key: TKey): Promise<unknown> | undefined {
357 return this.promiseMap.get(key) || undefined;
358 }
359 > async.ts
360 > keys(): IterableIterator<TKey> {
361 return this.promiseMap.keys();
362 }
363 > } async.ts
364 >
365 > interface IScheduledLater extends IDisposable {
366 > isTriggered(): boolean;
367 > }
368 >
369 > const timeoutDeferred = (timeout: number, fn: () => void): IScheduledLater => {
370 let scheduled = true;
371 const handle = setTimeout(() => {
381 };
382 };
383 > async.ts
384 > const microtaskDeferred = (fn: () => void): IScheduledLater => {
385 let scheduled = true;
386 queueMicrotask(() => {
396 };
397 };
398 > async.ts
399 > /**
400 > * A helper to delay (debounce) execution of a task that is being requested often.
401 > *
402 > * Following the throttler, now imagine the mail man wants to optimize the number of
403 > * trips proactively. The trip itself can be long, so he decides not to make the trip
404 > * as soon as a letter is submitted. Instead he waits a while, in case more
405 > * letters are submitted. After said waiting period, if no letters were submitted, he
406 > * decides to make the trip. Imagine that N more letters were submitted after the first
407 > * one, all within a short period of time between each other. Even though N+1
408 > * submissions occurred, only 1 delivery was made.
409 > *
410 > * The delayer offers this behavior via the trigger() method, into which both the task
411 > * to be executed and the waiting period (delay) must be passed in as arguments. Following
412 > * the example:
413 > *
414 > * const delayer = new Delayer(WAITING_PERIOD);
415 > * const letters = [];
416 > *
417 > * function letterReceived(l) {
418 > * letters.push(l);
419 > * delayer.trigger(() => { return makeTheTrip(); });
420 > * }
421 > */
422 > export class Delayer<T> implements IDisposable {
423 >
424 > private deferred: IScheduledLater | null;
425 > private completionPromise: Promise<any> | null;
426 > private doResolve: ((value?: any | Promise<any>) => void) | null;
427 > private doReject: ((err: unknown) => void) | null;
428 > private task: ITask<T | Promise<T>> | null;
429 >
430 > constructor(public defaultDelay: number | typeof MicrotaskDelay) {
431 this.deferred = null;
432 this.completionPromise = null;
435 this.task = null;
436 }
437 > async.ts
438 > trigger(task: ITask<T | Promise<T>>, delay = this.defaultDelay): Promise<T> {
439 this.task = task;
440 this.cancelTimeout();
465 return this.completionPromise;
466 }
467 > async.ts
468 > isTriggered(): boolean {
469 return !!this.deferred?.isTriggered();
470 }
471 > async.ts
472 > cancel(): void {
473 this.cancelTimeout();
474
478 }
479 }
480 > async.ts
481 > private cancelTimeout(): void {
482 this.deferred?.dispose();
483 this.deferred = null;
484 }
485 > async.ts
486 > dispose(): void {
487 this.cancel();
488 }
489 > } async.ts
490 >
491 > /**
492 > * A helper to delay execution of a task that is being requested often, while
493 > * preventing accumulation of consecutive executions, while the task runs.
494 > *
495 > * The mail man is clever and waits for a certain amount of time, before going
496 > * out to deliver letters. While the mail man is going out, more letters arrive
497 > * and can only be delivered once he is back. Once he is back the mail man will
498 > * do one more trip to deliver the letters that have accumulated while he was out.
499 > */
500 > export class ThrottledDelayer<T> {
501 >
502 > private delayer: Delayer<Promise<T>>;
503 > private throttler: Throttler;
504 >
505 > constructor(defaultDelay: number) {
506 this.delayer = new Delayer(defaultDelay);
507 this.throttler = new Throttler();
508 }
509 > async.ts
510 > trigger(promiseFactory: ICancellableTask<Promise<T>>, delay?: number): Promise<T> {
511 return this.delayer.trigger(() => this.throttler.queue(promiseFactory), delay) as unknown as Promise<T>;
512 }
513 > async.ts
514 > isTriggered(): boolean {
515 return this.delayer.isTriggered();
516 }
517 > async.ts
518 > cancel(): void {
519 this.delayer.cancel();
520 }
521 > async.ts
522 > dispose(): void {
523 this.delayer.dispose();
524 this.throttler.dispose();
525 }
526 > } async.ts
527 >
528 > /**
529 > * A barrier that is initially closed and then becomes opened permanently.
530 > */
531 > export class Barrier {
532 > private _isOpen: boolean;
533 > private _promise: Promise<boolean>;
534 > private _completePromise!: (v: boolean) => void;
535 >
536 > constructor() {
537 this._isOpen = false;
538 this._promise = new Promise<boolean>((c, e) => {
540 });
541 }
542 > async.ts
543 > isOpen(): boolean {
544 return this._isOpen;
545 }
546 > async.ts
547 > open(): void {
548 this._isOpen = true;
549 this._completePromise(true);
550 }
551 > async.ts
552 > wait(): Promise<boolean> {
553 return this._promise;
554 }
555 > } async.ts
556 >
557 > /**
558 > * A barrier that is initially closed and then becomes opened permanently after a certain period of
559 > * time or when open is called explicitly
560 > */
561 > export class AutoOpenBarrier extends Barrier {
562 >
563 > private readonly _timeout: Timeout;
564 >
565 > constructor(autoOpenTimeMs: number) {
566 super();
567 this._timeout = setTimeout(() => this.open(), autoOpenTimeMs);
568 }
569 > async.ts
570 > override open(): void {
571 clearTimeout(this._timeout);
572 super.open();
573 }
574 > } async.ts
575 >
576 > export function timeout(millis: number): CancelablePromise<void>;
577 > export function timeout(millis: number, token: CancellationToken): Promise<void>;
578 > export function timeout(millis: number, token?: CancellationToken): CancelablePromise<void> | Promise<void> {
579 if (!token) {
580 return createCancelablePromise(token => timeout(millis, token));
593 });
594 }
595 > async.ts
596 > /**
597 > * Creates a timeout that can be disposed using its returned value.
598 > * @param handler The timeout handler.
599 > * @param timeout An optional timeout in milliseconds.
600 > * @param store An optional {@link DisposableStore} that will have the timeout disposable managed automatically.
601 > *
602 > * @example
603 > * const store = new DisposableStore;
604 > * // Call the timeout after 1000ms at which point it will be automatically
605 > * // evicted from the store.
606 > * const timeoutDisposable = disposableTimeout(() => {}, 1000, store);
607 > *
608 > * if (foo) {
609 > * // Cancel the timeout and evict it from store.
610 > * timeoutDisposable.dispose();
611 > * }
612 > */
613 > export function disposableTimeout(handler: () => void, timeout = 0, store?: DisposableStore): IDisposable {
614 const timer = setTimeout(() => {
615 handler();
625 return disposable;
626 }
627 > async.ts
628 > /**
629 > * The largest delay (in milliseconds) a single `setTimeout` can represent.
630 > * Larger values overflow its internal 32-bit signed integer and fire (almost)
631 > * immediately instead of waiting.
632 > */
633 > export const MAX_TIMEOUT_DELAY = 2 ** 31 - 1; // ~24.8 days
634 >
635 > /**
636 > * Like {@link disposableTimeout}, but supports delays larger than
637 > * {@link MAX_TIMEOUT_DELAY} (~24.8 days), which a single `setTimeout` cannot
638 > * represent. The wait is split into chunks and re-armed until the target time is
639 > * reached, so the handler fires at approximately `Date.now() + timeout`.
640 > *
641 > * Note: like `setTimeout`, firing is best-effort and may drift across system
642 > * sleep or wall-clock changes; do not rely on it for precise scheduling.
643 > *
644 > * @param handler The timeout handler.
645 > * @param timeout The timeout in milliseconds. May exceed {@link MAX_TIMEOUT_DELAY}.
646 > * @param store An optional {@link DisposableStore} that will have the timeout disposable managed automatically.
647 > */
648 > export function disposableLongTimeout(handler: () => void, timeout: number, store?: DisposableStore): IDisposable {
649 const target = Date.now() + timeout;
650 let timer: Timeout;
671 return disposable;
672 }
673 > async.ts
674 > /**
675 > * Runs the provided list of promise factories in sequential order. The returned
676 > * promise will complete to an array of results from each promise.
677 > */
678 >
679 > export function sequence<T>(promiseFactories: ITask<Promise<T>>[]): Promise<T[]> {
680 const results: T[] = [];
681 let index = 0;
701 return Promise.resolve(null).then(thenHandler);
702 }
703 > async.ts
704 > export function first<T>(promiseFactories: ITask<Promise<T>>[], shouldStop: (t: T) => boolean = t => !!t, defaultValue: T | null = null): Promise<T | null> {
705 let index = 0;
706 const len = promiseFactories.length;
725 return loop();
726 }
727 > async.ts
728 > /**
729 > * Returns the result of the first promise that matches the "shouldStop",
730 > * running all promises in parallel. Supports cancelable promises.
731 > */
732 > export function firstParallel<T>(promiseList: Promise<T>[], shouldStop?: (t: T) => boolean, defaultValue?: T | null): Promise<T | null>;
733 > export function firstParallel<T, R extends T>(promiseList: Promise<T>[], shouldStop: (t: T) => t is R, defaultValue?: R | null): Promise<R | null>;
734 > export function firstParallel<T>(promiseList: Promise<T>[], shouldStop: (t: T) => boolean = t => !!t, defaultValue: T | null = null) {
735 if (promiseList.length === 0) {
736 return Promise.resolve(defaultValue);
764 });
765 }
766 > async.ts
767 > interface ILimitedTaskFactory<T> {
768 > factory: ITask<Promise<T>>;
769 > c: (value: T | Promise<T>) => void;
770 > e: (error?: unknown) => void;
771 > }
772 >
773 > export interface ILimiter<T> {
774 >
775 > readonly size: number;
776 >
777 > queue(factory: ITask<Promise<T>>): Promise<T>;
778 >
779 > clear(): void;
780 > }
781 >
782 > /**
783 > * A helper to queue N promises and run them all with a max degree of parallelism. The helper
784 > * ensures that at any time no more than M promises are running at the same time.
785 > */
786 > export class Limiter<T> implements ILimiter<T> {
787 >
788 > private _size = 0;
789 > private _isDisposed = false;
790 > private runningPromises: number;
791 > private readonly maxDegreeOfParalellism: number;
792 > private readonly outstandingPromises: ILimitedTaskFactory<T>[];
793 > private readonly _onDrained: Emitter<void>;
794 >
795 > constructor(maxDegreeOfParalellism: number) {
796 > this.maxDegreeOfParalellism = maxDegreeOfParalellism; async.ts
797 > this.outstandingPromises = [];
798 > this.runningPromises = 0;
799 > this._onDrained = new Emitter<void>();
800 > }
801 > async.ts
802 > /**
803 > *
804 > * @returns A promise that resolved when all work is done (onDrained) or when
805 > * there is nothing to do
806 > */
807 > whenIdle(): Promise<void> {
808 return this.size > 0
809 ? Event.toPromise(this.onDrained)
810 : Promise.resolve();
811 }
812 > async.ts
813 > get onDrained(): Event<void> {
814 > return this._onDrained.event; async.ts
815 > }
816 > async.ts
817 > get size(): number {
818 return this._size;
819 }
820 > async.ts
821 > queue(factory: ITask<Promise<T>>): Promise<T> {
822 > if (this._isDisposed) { async.ts
823 throw new Error('Object has been disposed');
824 }
825 > this._size++; async.ts
826 >
827 > return new Promise<T>((c, e) => {
828 > this.outstandingPromises.push({ factory, c, e });
829 > this.consume();
830 > });
831 > }
832 > async.ts
833 > private consume(): void {
834 > while (this.outstandingPromises.length && this.runningPromises < this.maxDegreeOfParalellism) { async.ts
835 > const iLimitedTask = this.outstandingPromises.shift()!;
836 > this.runningPromises++;
837 >
838 > const promise = iLimitedTask.factory();
839 > promise.then(iLimitedTask.c, iLimitedTask.e);
840 > promise.then(() => this.consumed(), () => this.consumed());
841 > }
842 > }
843 > async.ts
844 > private consumed(): void {
845 > if (this._isDisposed) { async.ts
846 return;
847 }
848 > this.runningPromises--; async.ts
849 > if (--this._size === 0) {
850 > this._onDrained.fire();
851 > }
852 >
853 > if (this.outstandingPromises.length > 0) {
854 this.consume();
855 }
856 > } async.ts
857 > async.ts
858 > clear(): void {
859 if (this._isDisposed) {
860 throw new Error('Object has been disposed');
863 this._size = this.runningPromises;
864 }
865 > async.ts
866 > dispose(): void {
867 > this._isDisposed = true; async.ts
868 > this.outstandingPromises.length = 0; // stop further processing
869 > this._size = 0;
870 > this._onDrained.dispose();
871 > }
872 > } async.ts
873 >
874 > /**
875 > * A queue is handles one promise at a time and guarantees that at any time only one promise is executing.
876 > */
877 > export class Queue<T> extends Limiter<T> {
878 >
879 > constructor() {
880 > super(1); async.ts
881 > }
882 > } async.ts
883 >
884 > /**
885 > * Same as `Queue`, ensures that only 1 task is executed at the same time. The difference to `Queue` is that
886 > * there is only 1 task about to be scheduled next. As such, calling `queue` while a task is executing will
887 > * replace the currently queued task until it executes.
888 > *
889 > * As such, the returned promise may not be from the factory that is passed in but from the next factory that
890 > * is running after having called `queue`.
891 > */
892 > export class LimitedQueue {
893
894 private readonly sequentializer = new TaskSequentializer();
895
896 private tasks = 0;
897 > async.ts
898 > queue(factory: ITask<Promise<void>>): Promise<void> {
899 if (!this.sequentializer.isRunning()) {
900 return this.sequentializer.run(this.tasks++, factory());
905 });
906 }
907 > } async.ts
908 >
909 > /**
910 > * A helper to organize queues per resource. The ResourceQueue makes sure to manage queues per resource
911 > * by disposing them once the queue is empty.
912 > */
913 > export class ResourceQueue implements IDisposable {
914 > async.ts
915 > private readonly queues = new Map<string, Queue<void>>();
916 >
917 > private readonly drainers = new Set<DeferredPromise<void>>();
918 >
919 > private drainListeners: DisposableMap<number> | undefined = undefined;
920 > private drainListenerCount = 0;
921 > async.ts
922 > async whenDrained(): Promise<void> {
923 if (this.isDrained()) {
924 return;
930 return promise.p;
931 }
932 > async.ts
933 > private isDrained(): boolean {
934 > for (const [, queue] of this.queues) { async.ts
935 if (queue.size > 0) {
936 return false;
937 }
938 }
939 > async.ts
940 > return true;
941 > }
942 > async.ts
943 > queueSize(resource: URI, extUri: IExtUri = defaultExtUri): number {
944 const key = extUri.getComparisonKey(resource);
945
946 return this.queues.get(key)?.size ?? 0;
947 }
948 > async.ts
949 > queueFor(resource: URI, factory: ITask<Promise<void>>, extUri: IExtUri = defaultExtUri): Promise<void> {
950 > const key = extUri.getComparisonKey(resource); async.ts
951 >
952 > let queue = this.queues.get(key);
953 > if (!queue) {
954 > queue = new Queue<void>();
955 > const drainListenerId = this.drainListenerCount++;
956 > const drainListener = Event.once(queue.onDrained)(() => {
957 > queue?.dispose();
958 > this.queues.delete(key);
959 > this.onDidQueueDrain();
960 >
961 > this.drainListeners?.deleteAndDispose(drainListenerId);
962 >
963 > if (this.drainListeners?.size === 0) {
964 > this.drainListeners.dispose();
965 > this.drainListeners = undefined;
966 > }
967 > });
968 >
969 > if (!this.drainListeners) {
970 > this.drainListeners = new DisposableMap();
971 > }
972 > this.drainListeners.set(drainListenerId, drainListener);
973 >
974 > this.queues.set(key, queue);
975 > }
976 >
977 > return queue.queue(factory);
978 > }
979 > async.ts
980 > private onDidQueueDrain(): void {
981 > if (!this.isDrained()) { async.ts
982 return; // not done yet
983 }
984 > async.ts
985 > this.releaseDrainers();
986 > }
987 > async.ts
988 > private releaseDrainers(): void {
989 > for (const drainer of this.drainers) { async.ts
990 drainer.complete();
991 }
992 > async.ts
993 > this.drainers.clear();
994 > }
995 > async.ts
996 > dispose(): void {
997 > for (const [, queue] of this.queues) { async.ts
998 queue.dispose();
999 }
1000 > async.ts
1001 > this.queues.clear();
1002 >
1003 > // Even though we might still have pending
1004 > // tasks queued, after the queues have been
1005 > // disposed, we can no longer track them, so
1006 > // we release drainers to prevent hanging
1007 > // promises when the resource queue is being
1008 > // disposed.
1009 > this.releaseDrainers();
1010 >
1011 > this.drainListeners?.dispose();
1012 > }
1013 > } async.ts
1014 >
1015 > export type Task<T = void> = () => (Promise<T> | T);
1016 >
1017 > /**
1018 > * Wrap a type in an optional promise. This can be useful to avoid the runtime
1019 > * overhead of creating a promise.
1020 > */
1021 > export type MaybePromise<T> = Promise<T> | T;
1022 >
1023 > /**
1024 > * Processes tasks in the order they were scheduled.
1025 > */
1026 > export class TaskQueue {
1027 private _runningTask: Task<any> | undefined = undefined;
1028 private _pendingTasks: { task: Task<any>; deferred: DeferredPromise<any>; setUndefinedWhenCleared: boolean }[] = [];
1029 > async.ts
1030 > /**
1031 > * Waits for the current and pending tasks to finish, then runs and awaits the given task.
1032 > * If the task is skipped because of clearPending, the promise is rejected with a CancellationError.
1033 > */
1034 > public schedule<T>(task: Task<T>): Promise<T> {
1035 const deferred = new DeferredPromise<T>();
1036 this._pendingTasks.push({ task, deferred, setUndefinedWhenCleared: false });
1038 return deferred.p;
1039 }
1040 > async.ts
1041 > /**
1042 > * Waits for the current and pending tasks to finish, then runs and awaits the given task.
1043 > * If the task is skipped because of clearPending, the promise is resolved with undefined.
1044 > */
1045 > public scheduleSkipIfCleared<T>(task: Task<T>): Promise<T | undefined> {
1046 const deferred = new DeferredPromise<T>();
1047 this._pendingTasks.push({ task, deferred, setUndefinedWhenCleared: true });
1049 return deferred.p;
1050 }
1051 > async.ts
1052 > private _runIfNotRunning(): void {
1053 if (this._runningTask === undefined) {
1054 this._processQueue();
1055 }
1056 }
1057 > async.ts
1058 > private async _processQueue(): Promise<void> {
1059 if (this._pendingTasks.length === 0) {
1060 return;
1082 }
1083 }
1084 > async.ts
1085 > /**
1086 > * Clears all pending tasks. Does not cancel the currently running task.
1087 > */
1088 > public clearPending(): void {
1089 const tasks = this._pendingTasks;
1090 this._pendingTasks = [];
1097 }
1098 }
1099 > } async.ts
1100 >
1101 > export class TimeoutTimer implements IDisposable {
1102 > private _token: Timeout | undefined;
1103 > private _isDisposed = false;
1104 >
1105 > constructor();
1106 > constructor(runner: () => void, timeout: number);
1107 > constructor(runner?: () => void, timeout?: number) {
1108 this._token = undefined;
1109
1112 }
1113 }
1114 > async.ts
1115 > dispose(): void {
1116 this.cancel();
1117 this._isDisposed = true;
1118 }
1119 > async.ts
1120 > cancel(): void {
1121 if (this._token !== undefined) {
1122 clearTimeout(this._token);
1124 }
1125 }
1126 > async.ts
1127 > cancelAndSet(runner: () => void, timeout: number): void {
1128 if (this._isDisposed) {
1129 throw new BugIndicatingError(`Calling 'cancelAndSet' on a disposed TimeoutTimer`);
1136 }, timeout);
1137 }
1138 > async.ts
1139 > setIfNotSet(runner: () => void, timeout: number): void {
1140 if (this._isDisposed) {
1141 throw new BugIndicatingError(`Calling 'setIfNotSet' on a disposed TimeoutTimer`);
1151 }, timeout);
1152 }
1153 > } async.ts
1154 >
1155 > export class IntervalTimer implements IDisposable {
1156
1157 private disposable: IDisposable | undefined = undefined;
1158 private isDisposed = false;
1159 > async.ts
1160 > cancel(): void {
1161 this.disposable?.dispose();
1162 this.disposable = undefined;
1163 }
1164 > async.ts
1165 > cancelAndSet(runner: () => void, interval: number, context = globalThis): void {
1166 if (this.isDisposed) {
1167 throw new BugIndicatingError(`Calling 'cancelAndSet' on a disposed IntervalTimer`);
1178 });
1179 }
1180 > async.ts
1181 > dispose(): void {
1182 this.cancel();
1183 this.isDisposed = true;
1184 }
1185 > } async.ts
1186 >
1187 > export class RunOnceScheduler<Runner extends (...args: any[]) => any = () => any> implements IDisposable {
1188 >
1189 > protected runner: Runner | null;
1190 >
1191 > private timeoutToken: Timeout | undefined;
1192 > private timeout: number;
1193 > private timeoutHandler: () => void;
1194 >
1195 > constructor(runner: Runner, delay: number) {
1196 > this.timeoutToken = undefined; async.ts
1197 > this.runner = runner;
1198 > this.timeout = delay;
1199 > this.timeoutHandler = this.onTimeout.bind(this);
1200 > }
1201 > async.ts
1202 > /**
1203 > * Dispose RunOnceScheduler
1204 > */
1205 > dispose(): void {
1206 > this.cancel(); async.ts
1207 > this.runner = null;
1208 > }
1209 > async.ts
1210 > /**
1211 > * Cancel current scheduled runner (if any).
1212 > */
1213 > cancel(): void {
1214 > if (this.isScheduled()) { async.ts
1215 clearTimeout(this.timeoutToken);
1216 this.timeoutToken = undefined;
1217 }
1218 > } async.ts
1219 > async.ts
1220 > /**
1221 > * Cancel previous runner (if any) & schedule a new runner.
1222 > */
1223 > schedule(delay = this.timeout): void {
1224 this.cancel();
1225 this.timeoutToken = setTimeout(this.timeoutHandler, delay);
1226 }
1227 > async.ts
1228 > get delay(): number {
1229 return this.timeout;
1230 }
1231 > async.ts
1232 > set delay(value: number) {
1233 this.timeout = value;
1234 }
1235 > async.ts
1236 > /**
1237 > * Returns true if scheduled.
1238 > */
1239 > isScheduled(): boolean {
1240 > return this.timeoutToken !== undefined; async.ts
1241 > }
1242 > async.ts
1243 > flush(): void {
1244 if (this.isScheduled()) {
1245 this.cancel();
1247 }
1248 }
1249 > async.ts
1250 > private onTimeout() {
1251 this.timeoutToken = undefined;
1252 if (this.runner) {
1254 }
1255 }
1256 > async.ts
1257 > protected doRun(): void {
1258 this.runner?.();
1259 }
1260 > } async.ts
1261 >
1262 > /**
1263 > * Same as `RunOnceScheduler`, but doesn't count the time spent in sleep mode.
1264 > * > **NOTE**: Only offers 1s resolution.
1265 > *
1266 > * When calling `setTimeout` with 3hrs, and putting the computer immediately to sleep
1267 > * for 8hrs, `setTimeout` will fire **as soon as the computer wakes from sleep**. But
1268 > * this scheduler will execute 3hrs **after waking the computer from sleep**.
1269 > */
1270 > export class ProcessTimeRunOnceScheduler {
1271 >
1272 > private runner: (() => void) | null;
1273 > private timeout: number;
1274 >
1275 > private counter: number;
1276 > private intervalToken: Timeout | undefined;
1277 > private intervalHandler: () => void;
1278 >
1279 > constructor(runner: () => void, delay: number) {
1280 if (delay % 1000 !== 0) {
1281 console.warn(`ProcessTimeRunOnceScheduler resolution is 1s, ${delay}ms is not a multiple of 1000ms.`);
1287 this.intervalHandler = this.onInterval.bind(this);
1288 }
1289 > async.ts
1290 > dispose(): void {
1291 this.cancel();
1292 this.runner = null;
1293 }
1294 > async.ts
1295 > cancel(): void {
1296 if (this.isScheduled()) {
1297 clearInterval(this.intervalToken);
1299 }
1300 }
1301 > async.ts
1302 > /**
1303 > * Cancel previous runner (if any) & schedule a new runner.
1304 > */
1305 > schedule(delay = this.timeout): void {
1306 if (delay % 1000 !== 0) {
1307 console.warn(`ProcessTimeRunOnceScheduler resolution is 1s, ${delay}ms is not a multiple of 1000ms.`);
1311 this.intervalToken = setInterval(this.intervalHandler, 1000);
1312 }
1313 > async.ts
1314 > /**
1315 > * Returns true if scheduled.
1316 > */
1317 > isScheduled(): boolean {
1318 return this.intervalToken !== undefined;
1319 }
1320 > async.ts
1321 > private onInterval() {
1322 this.counter--;
1323 if (this.counter > 0) {
1331 this.runner?.();
1332 }
1333 > } async.ts
1334 >
1335 > export class RunOnceWorker<T> extends RunOnceScheduler<(units: T[]) => void> {
1336 >
1337 > private units: T[] = [];
1338 >
1339 > constructor(runner: (units: T[]) => void, timeout: number) {
1340 super(runner, timeout);
1341 }
1342 > async.ts
1343 > work(unit: T): void {
1344 this.units.push(unit);
1345
1348 }
1349 }
1350 > async.ts
1351 > protected override doRun(): void {
1352 const units = this.units;
1353 this.units = [];
1355 this.runner?.(units);
1356 }
1357 > async.ts
1358 > override dispose(): void {
1359 this.units = [];
1360
1361 super.dispose();
1362 }
1363 > } async.ts
1364 >
1365 > export interface IThrottledWorkerOptions {
1366 >
1367 > /**
1368 > * maximum of units the worker will pass onto handler at once
1369 > */
1370 > maxWorkChunkSize: number;
1371 >
1372 > /**
1373 > * maximum of units the worker will keep in memory for processing
1374 > */
1375 > maxBufferedWork: number | undefined;
1376 >
1377 > /**
1378 > * delay before processing the next round of chunks when chunk size exceeds limits
1379 > */
1380 > throttleDelay: number;
1381 >
1382 > /**
1383 > * When enabled will guarantee that two distinct calls to `work()` are not executed
1384 > * without throttle delay between them.
1385 > * Otherwise if the worker isn't currently throttling it will execute work immediately.
1386 > */
1387 > waitThrottleDelayBetweenWorkUnits?: boolean;
1388 > }
1389 >
1390 > /**
1391 > * The `ThrottledWorker` will accept units of work `T`
1392 > * to handle. The contract is:
1393 > * * there is a maximum of units the worker can handle at once (via `maxWorkChunkSize`)
1394 > * * there is a maximum of units the worker will keep in memory for processing (via `maxBufferedWork`)
1395 > * * after having handled `maxWorkChunkSize` units, the worker needs to rest (via `throttleDelay`)
1396 > */
1397 > export class ThrottledWorker<T> extends Disposable {
1398 >
1399 > private readonly pendingWork: T[] = [];
1400 >
1401 > private readonly throttler = this._register(new MutableDisposable<RunOnceScheduler>());
1402 > private disposed = false;
1403 > private lastExecutionTime = 0;
1404 >
1405 > constructor(
1406 private options: IThrottledWorkerOptions,
1407 private readonly handler: (units: T[]) => void
1409 super();
1410 }
1411 > async.ts
1412 > /**
1413 > * The number of work units that are pending to be processed.
1414 > */
1415 > get pending(): number { return this.pendingWork.length; }
1416 >
1417 > /**
1418 > * Add units to be worked on. Use `pending` to figure out
1419 > * how many units are not yet processed after this method
1420 > * was called.
1421 > *
1422 > * @returns whether the work was accepted or not. If the
1423 > * worker is disposed, it will not accept any more work.
1424 > * If the number of pending units would become larger
1425 > * than `maxPendingWork`, more work will also not be accepted.
1426 > */
1427 > work(units: readonly T[]): boolean {
1428 if (this.disposed) {
1429 return false; // work not accepted: disposed
1469 return true; // work accepted
1470 }
1471 > async.ts
1472 > private doWork(): void {
1473 this.lastExecutionTime = Date.now();
1474
1481 }
1482 }
1483 > async.ts
1484 > private scheduleThrottler(delay = this.options.throttleDelay): void {
1485 this.throttler.value = new RunOnceScheduler(() => {
1486 this.throttler.clear();
1490 this.throttler.value.schedule();
1491 }
1492 > async.ts
1493 > override dispose(): void {
1494 super.dispose();
1495
1497 this.disposed = true;
1498 }
1499 > } async.ts
1500 >
1501 > //#region -- run on idle tricks ------------
1502 >
1503 > export interface IdleDeadline {
1504 > readonly didTimeout: boolean;
1505 > timeRemaining(): number;
1506 > }
1507 >
1508 > type IdleApi = Pick<typeof globalThis, 'requestIdleCallback' | 'cancelIdleCallback'>;
1509 >
1510 >
1511 > /**
1512 > * Execute the callback the next time the browser is idle, returning an
1513 > * {@link IDisposable} that will cancel the callback when disposed. This wraps
1514 > * [requestIdleCallback] so it will fallback to [setTimeout] if the environment
1515 > * doesn't support it.
1516 > *
1517 > * @param callback The callback to run when idle, this includes an
1518 > * [IdleDeadline] that provides the time alloted for the idle callback by the
1519 > * browser. Not respecting this deadline will result in a degraded user
1520 > * experience.
1521 > * @param timeout A timeout at which point to queue no longer wait for an idle
1522 > * callback but queue it on the regular event loop (like setTimeout). Typically
1523 > * this should not be used.
1524 > *
1525 > * [IdleDeadline]: https://developer.mozilla.org/en-US/docs/Web/API/IdleDeadline
1526 > * [requestIdleCallback]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
1527 > * [setTimeout]: https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout
1528 > *
1529 > * **Note** that there is `dom.ts#runWhenWindowIdle` which is better suited when running inside a browser
1530 > * context
1531 > */
1532 > export let runWhenGlobalIdle: (callback: (idle: IdleDeadline) => void, timeout?: number) => IDisposable;
1533 >
1534 > export let _runWhenIdle: (targetWindow: IdleApi, callback: (idle: IdleDeadline) => void, timeout?: number) => IDisposable;
1535 >
1536 > (function () {
1537 > const safeGlobal: any = globalThis;
1538 > if (typeof safeGlobal.requestIdleCallback !== 'function' || typeof safeGlobal.cancelIdleCallback !== 'function') {
1539 > _runWhenIdle = (_targetWindow, runner, timeout?) => {
1540 setTimeout0(() => {
1541 if (disposed) {
1561 };
1562 };
1563 > } else { async.ts
1564 _runWhenIdle = (targetWindow: typeof safeGlobal, runner, timeout?) => {
1565 const handle: number = targetWindow.requestIdleCallback(runner, typeof timeout === 'number' ? { timeout } : undefined);
1576 };
1577 }
1578 > runWhenGlobalIdle = (runner, timeout) => _runWhenIdle(globalThis, runner, timeout); async.ts
1579 > })();
1580 >
1581 > export function installFakeRunWhenIdle(fakeImpl: typeof _runWhenIdle): IDisposable {
1582 const origRunWhenIdle = _runWhenIdle;
1583 const origRunWhenGlobalIdle = runWhenGlobalIdle;
1589 });
1590 }
1591 > async.ts
1592 > export abstract class AbstractIdleValue<T> {
1593 >
1594 > private readonly _executor: () => void;
1595 > private readonly _handle: IDisposable;
1596 >
1597 > private _didRun: boolean = false;
1598 > private _value?: T;
1599 > private _error: unknown;
1600 >
1601 > constructor(targetWindow: IdleApi, executor: () => T) {
1602 this._executor = () => {
1603 try {
1611 this._handle = _runWhenIdle(targetWindow, () => this._executor());
1612 }
1613 > async.ts
1614 > dispose(): void {
1615 this._handle.dispose();
1616 }
1617 > async.ts
1618 > get value(): T {
1619 if (!this._didRun) {
1620 this._handle.dispose();
1626 return this._value!;
1627 }
1628 > async.ts
1629 > get isInitialized(): boolean {
1630 return this._didRun;
1631 }
1632 > } async.ts
1633 >
1634 > /**
1635 > * An `IdleValue` that always uses the current window (which might be throttled or inactive)
1636 > *
1637 > * **Note** that there is `dom.ts#WindowIdleValue` which is better suited when running inside a browser
1638 > * context
1639 > */
1640 > export class GlobalIdleValue<T> extends AbstractIdleValue<T> {
1641 >
1642 > constructor(executor: () => T) {
1643 super(globalThis, executor);
1644 }
1645 > } async.ts
1646 >
1647 > //#endregion
1648 >
1649 export async function retry<T>(task: ITask<Promise<T>>, delay: number, retries: number): Promise<T> {
1650 let lastError: Error | undefined;
1662 throw lastError;
1663 }
1664 > async.ts
1665 > //#region Task Sequentializer
1666 >
1667 > interface IRunningTask {
1668 > readonly taskId: number;
1669 > readonly cancel: () => void;
1670 > readonly promise: Promise<void>;
1671 > }
1672 >
1673 > interface IQueuedTask {
1674 > readonly promise: Promise<void>;
1675 > readonly promiseResolve: () => void;
1676 > readonly promiseReject: (error: Error) => void;
1677 > run: ITask<Promise<void>>;
1678 > }
1679 >
1680 > export interface ITaskSequentializerWithRunningTask {
1681 > readonly running: Promise<void>;
1682 > }
1683 >
1684 > export interface ITaskSequentializerWithQueuedTask {
1685 > readonly queued: IQueuedTask;
1686 > }
1687 >
1688 > /**
1689 > * @deprecated use `LimitedQueue` instead for an easier to use API
1690 > */
1691 > export class TaskSequentializer {
1692 >
1693 > private _running?: IRunningTask;
1694 > private _queued?: IQueuedTask;
1695 >
1696 > isRunning(taskId?: number): this is ITaskSequentializerWithRunningTask {
1697 if (typeof taskId === 'number') {
1698 return this._running?.taskId === taskId;
1701 return !!this._running;
1702 }
1703 > async.ts
1704 > get running(): Promise<void> | undefined {
1705 return this._running?.promise;
1706 }
1707 > async.ts
1708 > cancelRunning(): void {
1709 this._running?.cancel();
1710 }
1711 > async.ts
1712 > run(taskId: number, promise: Promise<void>, onCancel?: () => void,): Promise<void> {
1713 this._running = { taskId, cancel: () => onCancel?.(), promise };
1714
1717 return promise;
1718 }
1719 > async.ts
1720 > private doneRunning(taskId: number): void {
1721 if (this._running && taskId === this._running.taskId) {
1722
1728 }
1729 }
1730 > async.ts
1731 > private runQueued(): void {
1732 if (this._queued) {
1733 const queued = this._queued;
1738 }
1739 }
1740 > async.ts
1741 > /**
1742 > * Note: the promise to schedule as next run MUST itself call `run`.
1743 > * Otherwise, this sequentializer will report `false` for `isRunning`
1744 > * even when this task is running. Missing this detail means that
1745 > * suddenly multiple tasks will run in parallel.
1746 > */
1747 > queue(run: ITask<Promise<void>>): Promise<void> {
1748
1749 // this is our first queued task, so we create associated promise with it
1767 return this._queued.promise;
1768 }
1769 > async.ts
1770 > hasQueued(): this is ITaskSequentializerWithQueuedTask {
1771 return !!this._queued;
1772 }
1773 > async.ts
1774 > async join(): Promise<void> {
1775 return this._queued?.promise ?? this._running?.promise;
1776 }
1777 > } async.ts
1778 >
1779 > //#endregion
1780 >
1781 > //#region
1782 >
1783 > /**
1784 > * The `IntervalCounter` allows to count the number
1785 > * of calls to `increment()` over a duration of
1786 > * `interval`. This utility can be used to conditionally
1787 > * throttle a frequent task when a certain threshold
1788 > * is reached.
1789 > */
1790 > export class IntervalCounter {
1791 >
1792 > private lastIncrementTime = 0;
1793 >
1794 > private value = 0;
1795 >
1796 > constructor(private readonly interval: number, private readonly nowFn = () => Date.now()) { }
1797 >
1798 > increment(): number {
1799 const now = this.nowFn();
1800
1810 return this.value;
1811 }
1812 > } async.ts
1813 >
1814 > //#endregion
1815 >
1816 > //#region
1817 >
1818 > export type ValueCallback<T = unknown> = (value: T | Promise<T>) => void;
1819 >
1820 > const enum DeferredOutcome {
1821 > Resolved,
1822 > Rejected
1823 > }
1824 >
1825 > /**
1826 > * Creates a promise whose resolution or rejection can be controlled imperatively.
1827 > */
1828 > export class DeferredPromise<T> {
1829 >
1830 > public static fromPromise<T>(promise: Promise<T>): DeferredPromise<T> {
1831 const deferred = new DeferredPromise<T>();
1832 deferred.settleWith(promise);
1833 return deferred;
1834 }
1835 > async.ts
1836 > private completeCallback!: ValueCallback<T>;
1837 > private errorCallback!: (err: unknown) => void;
1838 > private outcome?: { outcome: DeferredOutcome.Rejected; value: unknown } | { outcome: DeferredOutcome.Resolved; value: T };
1839 >
1840 > public get isRejected() {
1841 return this.outcome?.outcome === DeferredOutcome.Rejected;
1842 }
1843 > async.ts
1844 > public get isResolved() {
1845 return this.outcome?.outcome === DeferredOutcome.Resolved;
1846 }
1847 > async.ts
1848 > public get isSettled() {
1849 return !!this.outcome;
1850 }
1851 > async.ts
1852 > public get value() {
1853 return this.outcome?.outcome === DeferredOutcome.Resolved ? this.outcome?.value : undefined;
1854 }
1855 > async.ts
1856 > public readonly p: Promise<T>;
1857 >
1858 > constructor() {
1859 this.p = new Promise<T>((c, e) => {
1860 this.completeCallback = c;
1862 });
1863 }
1864 > async.ts
1865 > public complete(value: T) {
1866 if (this.isSettled) {
1867 return Promise.resolve();
1874 });
1875 }
1876 > async.ts
1877 > public error(err: unknown) {
1878 if (this.isSettled) {
1879 return Promise.resolve();
1886 });
1887 }
1888 > async.ts
1889 > public settleWith(promise: Promise<T>): Promise<void> {
1890 return promise.then(
1891 value => this.complete(value),
1893 );
1894 }
1895 > async.ts
1896 > public cancel() {
1897 return this.error(new CancellationError());
1898 }
1899 > } async.ts
1900 >
1901 > //#endregion
1902 >
1903 > //#region Promises
1904 >
1905 > export namespace Promises {
1906 >
1907 > /**
1908 > * A drop-in replacement for `Promise.all` with the only difference
1909 > * that the method awaits every promise to either fulfill or reject.
1910 > *
1911 > * Similar to `Promise.all`, only the first error will be returned
1912 > * if any.
1913 > */
1914 > export async function settled<T>(promises: Promise<T>[]): Promise<T[]> {
1915 > let firstError: Error | undefined = undefined; async.ts
1916 >
1917 > const result = await Promise.all(promises.map(promise => promise.then(value => value, error => {
1918 if (!firstError) {
1919 firstError = error;
1921
1922 return undefined; // do not rethrow so that other promises can settle
1923 > }))); async.ts
1924 >
1925 > if (typeof firstError !== 'undefined') {
1926 throw firstError;
1927 }
1928 > async.ts
1929 > return result as unknown as T[]; // cast is needed and protected by the `throw` above
1930 > } async.ts
1931 > async.ts
1932 > /**
1933 > * A helper to create a new `Promise<T>` with a body that is a promise
1934 > * itself. By default, an error that raises from the async body will
1935 > * end up as a unhandled rejection, so this utility properly awaits the
1936 > * body and rejects the promise as a normal promise does without async
1937 > * body.
1938 > *
1939 > * This method should only be used in rare cases where otherwise `async`
1940 > * cannot be used (e.g. when callbacks are involved that require this).
1941 > */
1942 > export function withAsyncBody<T, E = Error>(bodyFn: (resolve: (value: T) => unknown, reject: (error: E) => unknown) => Promise<unknown>): Promise<T> {
1943 // eslint-disable-next-line no-async-promise-executor
1944 return new Promise<T>(async (resolve, reject) => {
1950 });
1951 }
1952 > } async.ts
1953 >
1954 > export class StatefulPromise<T> {
1955 > private _value: T | undefined = undefined;
1956 > get value(): T | undefined { return this._value; }
1957 >
1958 > private _error: unknown = undefined;
1959 > get error(): unknown { return this._error; }
1960 >
1961 > private _isResolved = false;
1962 > get isResolved() { return this._isResolved; }
1963 >
1964 > public readonly promise: Promise<T>;
1965 >
1966 > constructor(promise: Promise<T>) {
1967 this.promise = promise.then(
1968 value => {
1978 );
1979 }
1980 > async.ts
1981 > /**
1982 > * Returns the resolved value.
1983 > * Throws if the promise is not resolved yet.
1984 > */
1985 > public requireValue(): T {
1986 if (!this._isResolved) {
1987 throw new BugIndicatingError('Promise is not resolved yet');
1992 return this._value!;
1993 }
1994 > } async.ts
1995 >
1996 > export class LazyStatefulPromise<T> {
1997 > private readonly _promise = new Lazy(() => new StatefulPromise(this._compute()));
1998 >
1999 > constructor(
2000 private readonly _compute: () => Promise<T>,
2001 ) { }
2002 > async.ts
2003 > /**
2004 > * Returns the resolved value.
2005 > * Throws if the promise is not resolved yet.
2006 > */
2007 > public requireValue(): T {
2008 return this._promise.value.requireValue();
2009 }
2010 > async.ts
2011 > /**
2012 > * Returns the promise (and triggers a computation of the promise if not yet done so).
2013 > */
2014 > public getPromise(): Promise<T> {
2015 return this._promise.value.promise;
2016 }
2017 > async.ts
2018 > /**
2019 > * Reads the current value without triggering a computation of the promise.
2020 > */
2021 > public get currentValue(): T | undefined {
2022 return this._promise.rawValue?.value;
2023 }
2024 > } async.ts
2025 >
2026 > //#endregion
2027 >
2028 > //#region
2029 >
2030 > const enum AsyncIterableSourceState {
2031 > Initial,
2032 > DoneOK,
2033 > DoneError,
2034 > }
2035 >
2036 > /**
2037 > * An object that allows to emit async values asynchronously or bring the iterable to an error state using `reject()`.
2038 > * This emitter is valid only for the duration of the executor (until the promise returned by the executor settles).
2039 > */
2040 > export interface AsyncIterableEmitter<T> {
2041 > /**
2042 > * The value will be appended at the end.
2043 > *
2044 > * **NOTE** If `reject()` has already been called, this method has no effect.
2045 > */
2046 > emitOne(value: T): void;
2047 > /**
2048 > * The values will be appended at the end.
2049 > *
2050 > * **NOTE** If `reject()` has already been called, this method has no effect.
2051 > */
2052 > emitMany(values: T[]): void;
2053 > /**
2054 > * Writing an error will permanently invalidate this iterable.
2055 > * The current users will receive an error thrown, as will all future users.
2056 > *
2057 > * **NOTE** If `reject()` have already been called, this method has no effect.
2058 > */
2059 > reject(error: Error): void;
2060 > }
2061 >
2062 > /**
2063 > * An executor for the `AsyncIterableObject` that has access to an emitter.
2064 > */
2065 > export interface AsyncIterableExecutor<T> {
2066 > /**
2067 > * @param emitter An object that allows to emit async values valid only for the duration of the executor.
2068 > */
2069 > (emitter: AsyncIterableEmitter<T>): unknown | Promise<unknown>;
2070 > }
2071 >
2072 > /**
2073 > * A rich implementation for an `AsyncIterable<T>`.
2074 > */
2075 > export class AsyncIterableObject<T> implements AsyncIterable<T> {
2076 >
2077 > public static fromArray<T>(items: T[]): AsyncIterableObject<T> {
2078 > return new AsyncIterableObject<T>((writer) => {
2079 > writer.emitMany(items);
2080 > });
2081 > }
2082 >
2083 > public static fromPromise<T>(promise: Promise<T[]>): AsyncIterableObject<T> {
2084 return new AsyncIterableObject<T>(async (emitter) => {
2085 emitter.emitMany(await promise);
2086 });
2087 }
2088 > async.ts
2089 > public static fromPromisesResolveOrder<T>(promises: Promise<T>[]): AsyncIterableObject<T> {
2090 return new AsyncIterableObject<T>(async (emitter) => {
2091 await Promise.all(promises.map(async (p) => emitter.emitOne(await p)));
2092 });
2093 }
2094 > async.ts
2095 > public static merge<T>(iterables: AsyncIterable<T>[]): AsyncIterableObject<T> {
2096 return new AsyncIterableObject(async (emitter) => {
2097 await Promise.all(iterables.map(async (iterable) => {
2102 });
2103 }
2104 > async.ts
2105 > public static EMPTY = AsyncIterableObject.fromArray<any>([]);
2106 >
2107 > private _state: AsyncIterableSourceState;
2108 > private _results: T[];
2109 > private _error: Error | null;
2110 > private readonly _onReturn?: () => void | Promise<void>;
2111 > private readonly _onStateChanged: Emitter<void>;
2112 >
2113 > constructor(executor: AsyncIterableExecutor<T>, onReturn?: () => void | Promise<void>) {
2114 > this._state = AsyncIterableSourceState.Initial;
2115 > this._results = [];
2116 > this._error = null;
2117 > this._onReturn = onReturn;
2118 > this._onStateChanged = new Emitter<void>();
2119 >
2120 > queueMicrotask(async () => {
2121 > const writer: AsyncIterableEmitter<T> = {
2122 > emitOne: (item) => this.emitOne(item),
2123 > emitMany: (items) => this.emitMany(items),
2124 > reject: (error) => this.reject(error)
2125 > };
2126 > try {
2127 > await Promise.resolve(executor(writer));
2128 > this.resolve();
2129 > } catch (err) {
2130 this.reject(err);
2131 > } finally { async.ts
2132 > // The executor has settled; emitting afterwards must be a no-op per the
2133 > // documented "no effect after resolve()/reject()" contract (see emitOne).
2134 > writer.emitOne = () => { };
2135 > writer.emitMany = () => { };
2136 > writer.reject = () => { };
2137 > }
2138 > });
2139 > }
2140 >
2141 > [Symbol.asyncIterator](): AsyncIterator<T, undefined, undefined> {
2142 let i = 0;
2143 return {
2162 };
2163 }
2164 > async.ts
2165 > public static map<T, R>(iterable: AsyncIterable<T>, mapFn: (item: T) => R): AsyncIterableObject<R> {
2166 return new AsyncIterableObject<R>(async (emitter) => {
2167 for await (const item of iterable) {
2170 });
2171 }
2172 > async.ts
2173 > public map<R>(mapFn: (item: T) => R): AsyncIterableObject<R> {
2174 return AsyncIterableObject.map(this, mapFn);
2175 }
2176 > async.ts
2177 > public static filter<T>(iterable: AsyncIterable<T>, filterFn: (item: T) => boolean): AsyncIterableObject<T> {
2178 return new AsyncIterableObject<T>(async (emitter) => {
2179 for await (const item of iterable) {
2184 });
2185 }
2186 > async.ts
2187 > public filter<T2 extends T>(filterFn: (item: T) => item is T2): AsyncIterableObject<T2>;
2188 > public filter(filterFn: (item: T) => boolean): AsyncIterableObject<T>;
2189 > public filter(filterFn: (item: T) => boolean): AsyncIterableObject<T> {
2190 return AsyncIterableObject.filter(this, filterFn);
2191 }
2192 > async.ts
2193 > public static coalesce<T>(iterable: AsyncIterable<T | undefined | null>): AsyncIterableObject<T> {
2194 return <AsyncIterableObject<T>>AsyncIterableObject.filter(iterable, item => !!item);
2195 }
2196 > async.ts
2197 > public coalesce(): AsyncIterableObject<NonNullable<T>> {
2198 return AsyncIterableObject.coalesce(this) as AsyncIterableObject<NonNullable<T>>;
2199 }
2200 > async.ts
2201 > public static async toPromise<T>(iterable: AsyncIterable<T>): Promise<T[]> {
2202 const result: T[] = [];
2203 for await (const item of iterable) {
2206 return result;
2207 }
2208 > async.ts
2209 > public toPromise(): Promise<T[]> {
2210 return AsyncIterableObject.toPromise(this);
2211 }
2212 > async.ts
2213 > /**
2214 > * The value will be appended at the end.
2215 > *
2216 > * **NOTE** If `resolve()` or `reject()` have already been called, this method has no effect.
2217 > */
2218 > private emitOne(value: T): void {
2219 if (this._state !== AsyncIterableSourceState.Initial) {
2220 return;
2225 this._onStateChanged.fire();
2226 }
2227 > async.ts
2228 > /**
2229 > * The values will be appended at the end.
2230 > *
2231 > * **NOTE** If `resolve()` or `reject()` have already been called, this method has no effect.
2232 > */
2233 > private emitMany(values: T[]): void {
2234 > if (this._state !== AsyncIterableSourceState.Initial) {
2235 return;
2236 }
2237 > // it is important to add new values at the end, async.ts
2238 > // as we may have iterators already running on the array
2239 > this._results = this._results.concat(values);
2240 > this._onStateChanged.fire();
2241 > }
2242 >
2243 > /**
2244 > * Calling `resolve()` will mark the result array as complete.
2245 > *
2246 > * **NOTE** `resolve()` must be called, otherwise all consumers of this iterable will hang indefinitely, similar to a non-resolved promise.
2247 > * **NOTE** If `resolve()` or `reject()` have already been called, this method has no effect.
2248 > */
2249 > private resolve(): void {
2250 > if (this._state !== AsyncIterableSourceState.Initial) {
2251 return;
2252 }
2253 > this._state = AsyncIterableSourceState.DoneOK; async.ts
2254 > this._onStateChanged.fire();
2255 > }
2256 >
2257 > /**
2258 > * Writing an error will permanently invalidate this iterable.
2259 > * The current users will receive an error thrown, as will all future users.
2260 > *
2261 > * **NOTE** If `resolve()` or `reject()` have already been called, this method has no effect.
2262 > */
2263 > private reject(error: Error) {
2264 if (this._state !== AsyncIterableSourceState.Initial) {
2265 return;
2269 this._onStateChanged.fire();
2270 }
2271 > } async.ts
2272 >
2273 >
2274 > export function createCancelableAsyncIterableProducer<T>(callback: (token: CancellationToken) => AsyncIterable<T>): CancelableAsyncIterableProducer<T> {
2275 const source = new CancellationTokenSource();
2276 const innerIterable = callback(source.token);
2299 });
2300 }
2301 > async.ts
2302 > export class AsyncIterableSource<T> {
2303 >
2304 > private readonly _deferred = new DeferredPromise<void>();
2305 > private readonly _asyncIterable: AsyncIterableObject<T>;
2306 >
2307 > private _errorFn: (error: Error) => void;
2308 > private _emitOneFn: (item: T) => void;
2309 > private _emitManyFn: (item: T[]) => void;
2310 >
2311 > /**
2312 > *
2313 > * @param onReturn A function that will be called when consuming the async iterable
2314 > * has finished by the consumer, e.g the for-await-loop has be existed (break, return) early.
2315 > * This is NOT called when resolving this source by its owner.
2316 > */
2317 > constructor(onReturn?: () => Promise<void> | void) {
2318 this._asyncIterable = new AsyncIterableObject(emitter => {
2319
2354 };
2355 }
2356 > async.ts
2357 > get asyncIterable(): AsyncIterableObject<T> {
2358 return this._asyncIterable;
2359 }
2360 > async.ts
2361 > resolve(): void {
2362 this._deferred.complete();
2363 }
2364 > async.ts
2365 > reject(error: Error): void {
2366 this._errorFn(error);
2367 this._deferred.complete();
2368 }
2369 > async.ts
2370 > emitOne(item: T): void {
2371 this._emitOneFn(item);
2372 }
2373 > async.ts
2374 > emitMany(items: T[]) {
2375 this._emitManyFn(items);
2376 }
2377 > } async.ts
2378 >
2379 > export function cancellableIterable<T>(iterableOrIterator: AsyncIterator<T> | AsyncIterable<T>, token: CancellationToken): AsyncIterableIterator<T> {
2380 const iterator = Symbol.asyncIterator in iterableOrIterator ? iterableOrIterator[Symbol.asyncIterator]() : iterableOrIterator;
2381
2395 };
2396 }
2397 > async.ts
2398 > type ProducerConsumerValue<T> = {
2399 > ok: true;
2400 > value: T;
2401 > } | {
2402 > ok: false;
2403 > error: Error;
2404 > };
2405 >
2406 > class ProducerConsumer<T> {
2407 > private readonly _unsatisfiedConsumers: DeferredPromise<T>[] = [];
2408 > private readonly _unconsumedValues: ProducerConsumerValue<T>[] = [];
2409 > private _finalValue: ProducerConsumerValue<T> | undefined;
2410 >
2411 > public get hasFinalValue(): boolean {
2412 > return !!this._finalValue;
2413 > }
2414 >
2415 > produce(value: ProducerConsumerValue<T>): void {
2416 this._ensureNoFinalValue();
2417 if (this._unsatisfiedConsumers.length > 0) {
2422 }
2423 }
2424 > async.ts
2425 > produceFinal(value: ProducerConsumerValue<T>): void {
2426 > this._ensureNoFinalValue();
2427 > this._finalValue = value;
2428 > for (const deferred of this._unsatisfiedConsumers) {
2429 this._resolveOrRejectDeferred(deferred, value);
2430 }
2431 > this._unsatisfiedConsumers.length = 0; async.ts
2432 > }
2433 >
2434 > private _ensureNoFinalValue(): void {
2435 > if (this._finalValue) {
2436 throw new BugIndicatingError('ProducerConsumer: cannot produce after final value has been set');
2437 }
2438 > } async.ts
2439 >
2440 > private _resolveOrRejectDeferred(deferred: DeferredPromise<T>, value: ProducerConsumerValue<T>): void {
2441 if (value.ok) {
2442 deferred.complete(value.value);
2445 }
2446 }
2447 > async.ts
2448 > consume(): Promise<T> {
2449 if (this._unconsumedValues.length > 0 || this._finalValue) {
2450 const value = this._unconsumedValues.length > 0 ? this._unconsumedValues.shift()! : this._finalValue!;
2460 }
2461 }
2462 > } async.ts
2463 >
2464 > /**
2465 > * Important difference to AsyncIterableObject:
2466 > * If it is iterated two times, the second iterator will not see the values emitted by the first iterator.
2467 > */
2468 > export class AsyncIterableProducer<T> implements AsyncIterable<T> {
2469 > private readonly _producerConsumer = new ProducerConsumer<IteratorResult<T>>();
2470 >
2471 > constructor(executor: AsyncIterableExecutor<T>, private readonly _onReturn?: () => void) {
2472 > queueMicrotask(async () => {
2473 > const p = executor({
2474 > emitOne: value => this._producerConsumer.produce({ ok: true, value: { done: false, value: value } }),
2475 > emitMany: values => {
2476 > for (const value of values) {
2477 this._producerConsumer.produce({ ok: true, value: { done: false, value: value } });
2478 }
2479 > }, async.ts
2480 > reject: error => this._finishError(error),
2481 > });
2482 >
2483 > if (!this._producerConsumer.hasFinalValue) {
2484 > try {
2485 > await p;
2486 > this._finishOk();
2487 > } catch (error) {
2488 this._finishError(error);
2489 }
2490 > } async.ts
2491 > });
2492 > }
2493 >
2494 > public static fromArray<T>(items: T[]): AsyncIterableProducer<T> {
2495 > return new AsyncIterableProducer<T>((writer) => {
2496 > writer.emitMany(items);
2497 > });
2498 > }
2499 >
2500 > public static fromPromise<T>(promise: Promise<T[]>): AsyncIterableProducer<T> {
2501 return new AsyncIterableProducer<T>(async (emitter) => {
2502 emitter.emitMany(await promise);
2503 });
2504 }
2505 > async.ts
2506 > public static fromPromisesResolveOrder<T>(promises: Promise<T>[]): AsyncIterableProducer<T> {
2507 return new AsyncIterableProducer<T>(async (emitter) => {
2508 await Promise.all(promises.map(async (p) => emitter.emitOne(await p)));
2509 });
2510 }
2511 > async.ts
2512 > public static merge<T>(iterables: AsyncIterable<T>[]): AsyncIterableProducer<T> {
2513 return new AsyncIterableProducer(async (emitter) => {
2514 await Promise.all(iterables.map(async (iterable) => {
2519 });
2520 }
2521 > async.ts
2522 > public static EMPTY = AsyncIterableProducer.fromArray<any>([]);
2523 >
2524 > public static map<T, R>(iterable: AsyncIterable<T>, mapFn: (item: T) => R): AsyncIterableProducer<R> {
2525 return new AsyncIterableProducer<R>(async (emitter) => {
2526 for await (const item of iterable) {
2529 });
2530 }
2531 > async.ts
2532 > public static tee<T>(iterable: AsyncIterable<T>): [AsyncIterableProducer<T>, AsyncIterableProducer<T>] {
2533 let emitter1: AsyncIterableEmitter<T> | undefined;
2534 let emitter2: AsyncIterableEmitter<T> | undefined;
2565 return [p1, p2];
2566 }
2567 > async.ts
2568 > public map<R>(mapFn: (item: T) => R): AsyncIterableProducer<R> {
2569 return AsyncIterableProducer.map(this, mapFn);
2570 }
2571 > async.ts
2572 > public static coalesce<T>(iterable: AsyncIterable<T | undefined | null>): AsyncIterableProducer<T> {
2573 return <AsyncIterableProducer<T>>AsyncIterableProducer.filter(iterable, item => !!item);
2574 }
2575 > async.ts
2576 > public coalesce(): AsyncIterableProducer<NonNullable<T>> {
2577 return AsyncIterableProducer.coalesce(this) as AsyncIterableProducer<NonNullable<T>>;
2578 }
2579 > async.ts
2580 > public static filter<T>(iterable: AsyncIterable<T>, filterFn: (item: T) => boolean): AsyncIterableProducer<T> {
2581 return new AsyncIterableProducer<T>(async (emitter) => {
2582 for await (const item of iterable) {
2587 });
2588 }
2589 > async.ts
2590 > public filter<T2 extends T>(filterFn: (item: T) => item is T2): AsyncIterableProducer<T2>;
2591 > public filter(filterFn: (item: T) => boolean): AsyncIterableProducer<T>;
2592 > public filter(filterFn: (item: T) => boolean): AsyncIterableProducer<T> {
2593 return AsyncIterableProducer.filter(this, filterFn);
2594 }
2595 > async.ts
2596 > private _finishOk(): void {
2597 > if (!this._producerConsumer.hasFinalValue) {
2598 > this._producerConsumer.produceFinal({ ok: true, value: { done: true, value: undefined } });
2599 > }
2600 > }
2601 >
2602 > private _finishError(error: Error): void {
2603 if (!this._producerConsumer.hasFinalValue) {
2604 this._producerConsumer.produceFinal({ ok: false, error: error });
2606 // Warning: this can cause to dropped errors.
2607 }
2608 > async.ts
2609 > private readonly _iterator: AsyncIterator<T, void, void> = {
2610 > next: () => this._producerConsumer.consume(),
2611 > return: () => {
2612 this._onReturn?.();
2613 return Promise.resolve({ done: true, value: undefined });
2614 },
2615 > throw: async (e) => { async.ts
2616 this._finishError(e);
2617 return { done: true, value: undefined };
2618 },
2619 > }; async.ts
2620 >
2621 > [Symbol.asyncIterator](): AsyncIterator<T, void, void> {
2622 return this._iterator;
2623 }
2624 > } async.ts
2625 >
2626 > export class CancelableAsyncIterableProducer<T> extends AsyncIterableProducer<T> {
2627 > constructor(
2628 private readonly _source: CancellationTokenSource,
2629 executor: AsyncIterableExecutor<T>
2631 super(executor);
2632 }
2633 > async.ts
2634 > cancel(): void {
2635 this._source.cancel();
2636 }
2637 > } async.ts
2638 >
2639 > //#endregion
2640 >
2641 > export const AsyncReaderEndOfStream = Symbol('AsyncReaderEndOfStream');
2642 >
2643 > export class AsyncReader<T> {
2644 > private _buffer: T[] = [];
2645 > private _atEnd = false;
2646 >
2647 > public get endOfStream(): boolean { return this._buffer.length === 0 && this._atEnd; }
2648 > private _extendBufferPromise: Promise<void> | undefined;
2649 >
2650 > constructor(
2651 private readonly _source: AsyncIterator<T>
2652 ) {
2653 }
2654 > async.ts
2655 > public async read(): Promise<T | typeof AsyncReaderEndOfStream> {
2656 if (this._buffer.length === 0 && !this._atEnd) {
2657 await this._extendBuffer();
2662 return this._buffer.shift()!;
2663 }
2664 > async.ts
2665 > public async readWhile(predicate: (value: T) => boolean, callback: (element: T) => unknown): Promise<void> {
2666 do {
2667 const piece = await this.peek();
2676 } while (true);
2677 }
2678 > async.ts
2679 > public readBufferedOrThrow(): T | typeof AsyncReaderEndOfStream {
2680 const value = this.peekBufferedOrThrow();
2681 this._buffer.shift();
2682 return value;
2683 }
2684 > async.ts
2685 > public async consumeToEnd(): Promise<void> {
2686 while (!this.endOfStream) {
2687 await this.read();
2688 }
2689 }
2690 > async.ts
2691 > public async peek(): Promise<T | typeof AsyncReaderEndOfStream> {
2692 if (this._buffer.length === 0 && !this._atEnd) {
2693 await this._extendBuffer();
2698 return this._buffer[0];
2699 }
2700 > async.ts
2701 > public peekBufferedOrThrow(): T | typeof AsyncReaderEndOfStream {
2702 if (this._buffer.length === 0) {
2703 if (this._atEnd) {
2709 return this._buffer[0];
2710 }
2711 > async.ts
2712 > public async peekTimeout(timeoutMs: number): Promise<T | typeof AsyncReaderEndOfStream | undefined> {
2713 if (this._buffer.length === 0 && !this._atEnd) {
2714 await raceTimeout(this._extendBuffer(), timeoutMs);
2722 return this._buffer[0];
2723 }
2724 > async.ts
2725 > private _extendBuffer(): Promise<void> {
2726 if (this._atEnd) {
2727 return Promise.resolve();
2742 return this._extendBufferPromise;
2743 }
2744 > } async.ts
2745 >
2746 > export function createTimeout(ms: number, cb: () => void): IDisposable {
2747 const t = setTimeout(cb, ms);
2748 return toDisposable(() => clearTimeout(t));
src/vs/platform/agentHost/common/state/protocol/channels-session/state.ts 1351 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- state.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > import type { Changeset } from '../channels-changeset/state.js';
10 > import type { AnnotationsSummary } from '../channels-annotations/state.js';
11 > import type { ChatSummary, ChatInputRequest, ToolCallConfirmationState, ToolCallState, ToolCallAuthRequiredState } from '../channels-chat/state.js';
12 > import type { ConfigPropertySchema, ErrorInfo, Icon, ProtectedResourceMetadata, TextRange, URI } from '../common/state.js';
13 >
14 > // ─── Session State ───────────────────────────────────────────────────────────
15 >
16 > /**
17 > * Session initialization state.
18 > *
19 > * @category Session State
20 > */
21 > export const enum SessionLifecycle {
22 > Creating = 'creating',
23 > Ready = 'ready',
24 > CreationFailed = 'creationFailed',
25 > }
26 >
27 > /**
28 > * Bitset of summary-level session status flags.
29 > *
30 > * Use bitwise checks instead of equality for non-terminal activity. For example,
31 > * `status & SessionStatus.InProgress` matches both ordinary in-progress turns
32 > * and turns that are paused waiting for input.
33 > *
34 > * @category Session State
35 > */
36 > export const enum SessionStatus {
37 > /** Session is idle — no turn is active. */
38 > Idle = 1,
39 > /** Session ended with an error. */
40 > Error = 1 << 1,
41 > /** A turn is actively streaming. */
42 > InProgress = 1 << 3,
43 > /** A turn is in progress but blocked waiting for user input or tool confirmation. */
44 > InputNeeded = (1 << 3) | (1 << 4),
45 > /** The client has viewed this session since its last modification. */
46 > IsRead = 1 << 5,
47 > /** The session has been archived by the client. */
48 > IsArchived = 1 << 6,
49 > }
50 >
51 > /**
52 > * Metadata shared between the full {@link SessionState} (delivered when a
53 > * client subscribes to a session's URI) and the lightweight
54 > * {@link SessionSummary} (carried in the root-channel session catalog).
55 > *
56 > * These fields describe the session at a glance and appear in both places.
57 > * `SessionState` owns the authoritative values for a subscribed session;
58 > * `SessionSummary` mirrors them into the catalog so clients that only render a
59 > * session list don't have to subscribe to every session URI. The host keeps
60 > * the catalog in sync via `root/sessionSummaryChanged`.
61 > *
62 > * @category Session State
63 > */
64 > export interface SessionMetadata {
65 > /** Agent provider ID */
66 > provider: string;
67 > /** Session title */
68 > title: string;
69 > /** Current session status */
70 > status: SessionStatus;
71 > /** Human-readable description of what the session is currently doing */
72 > activity?: string;
73 > /** Server-owned project for this session */
74 > project?: ProjectInfo;
75 > /**
76 > * The working directories the session's agent has tool access to, as
77 > * maintained by the `session/workingDirectorySet` /
78 > * `session/workingDirectoryRemoved` actions. Directories are **equal peers** —
79 > * the session has no primary. Individual chats MAY restrict to a subset via
80 > * {@link ChatSummary.workingDirectories | their own `workingDirectories`} and
81 > * designate one of their own directories as primary (see
82 > * {@link ChatState.primaryWorkingDirectory}); a chat that sets no subset
83 > * operates against this full set.
84 > */
85 > workingDirectories?: URI[];
86 > /**
87 > * Lightweight summary of this session's inline annotations channel
88 > * (`ahp-session:/<uuid>/annotations`). Surfaced so badge UI can render
89 > * annotation / entry counts without subscribing. Absent when the session
90 > * does not expose an annotations channel.
91 > */
92 > annotations?: AnnotationsSummary;
93 > }
94 >
95 > /**
96 > * Full state for a single session, loaded when a client subscribes to the session's URI.
97 > *
98 > * Inlines (denormalizes) every {@link SessionMetadata} field directly onto
99 > * itself so subscribers receive one flat object instead of a nested summary.
100 > * The lightweight catalog representation is {@link SessionSummary}, surfaced on
101 > * the root channel; the host keeps the two in sync via
102 > * `root/sessionSummaryChanged`.
103 > *
104 > * @category Session State
105 > */
106 > export interface SessionState extends SessionMetadata {
107 > /** Session initialization state */
108 > lifecycle: SessionLifecycle;
109 > /** Error details if creation failed */
110 > creationError?: ErrorInfo;
111 > /** Tools provided by the server (agent host) for this session */
112 > serverTools?: ToolDefinition[];
113 > /**
114 > * The clients currently providing tools and interactive capabilities to this
115 > * session. If multiple tools or customizations are provided by the same
116 > * active client, an agent host MAY deduplicate them when exposed to a model,
117 > * with a preference given to the client that started the turn.
118 > *
119 > * Membership is host-managed: clients add (or refresh) themselves with
120 > * `session/activeClientSet`, and the host removes them with
121 > * `session/activeClientRemoved` when they unsubscribe, disconnect without
122 > * reconnecting in time, or reconnect without resubscribing to the session.
123 > */
124 > activeClients: SessionActiveClient[];
125 > /** Catalog of chats in this session. */
126 > chats: ChatSummary[];
127 > /**
128 > * The chat that receives input when the user addresses the session without
129 > * selecting a specific chat. This is a UI routing hint, not a hierarchy
130 > * marker — chats remain equal peers at the protocol level. Hosts MAY change
131 > * this over the session's lifetime.
132 > */
133 > defaultChat?: URI;
134 > /** Session configuration schema and current values */
135 > config?: SessionConfigState;
136 > /**
137 > * Top-level customizations active in this session.
138 > *
139 > * Always one of the {@link Customization} variants:
140 > *
141 > * - Container customizations ({@link PluginCustomization},
142 > * {@link DirectoryCustomization}) whose children — agents, skills,
143 > * prompts, rules, hooks, MCP servers — live in each container's
144 > * {@link ContainerCustomizationBase.children | `children`} array.
145 > * - Top-level {@link McpServerCustomization} entries the host
146 > * surfaces directly (for example a globally-configured MCP server
147 > * that isn't bundled in a plugin or directory). MCP servers may
148 > * also appear as children of a container.
149 > *
150 > * Client-published plugins arrive via
151 > * {@link SessionActiveClient.customizations | `activeClients[].customizations`}
152 > * and the host propagates them into this list (typically with the
153 > * container's `clientId` set and `children` populated). Clients
154 > * publish in container shape only; bare MCP servers at the top level
155 > * are server-originated.
156 > */
157 > customizations?: Customization[];
158 > /**
159 > * Catalogue of changesets the server can produce for this session. Each
160 > * entry advertises a subscribable view of file changes (uncommitted,
161 > * session-wide, per-turn, etc.) and the URI template the client expands
162 > * before subscribing. See {@link Changeset} for the full shape and
163 > * {@link /guide/changesets | Changesets} for an overview of the model.
164 > */
165 > changesets?: Changeset[];
166 > /**
167 > * Outstanding input the session is blocked on, aggregated across every chat
168 > * so a client can discover and answer it from the session channel alone,
169 > * without subscribing to individual chats.
170 > *
171 > * Each entry is self-sufficient: it carries the owning chat's URI plus every
172 > * identifier the client needs to respond. A client answers by dispatching the
173 > * ordinary `chat/*` action to that chat's channel — see
174 > * {@link SessionInputRequest} for the per-variant response path. A present,
175 > * non-empty list implies {@link SessionStatus.InputNeeded} on
176 > * {@link SessionSummary.status}.
177 > *
178 > * Host-managed: the host upserts entries with `session/inputNeededSet` as
179 > * chats raise requests and removes them with `session/inputNeededRemoved`
180 > * once the underlying request resolves.
181 > */
182 > inputNeeded?: SessionInputRequest[];
183 > /**
184 > * Additional provider-specific metadata for this session.
185 > *
186 > * Clients MAY look for well-known keys here to provide enhanced UI.
187 > * For example, a `git` key may provide extra git metadata about the session's
188 > * working directories.
189 > */
190 > _meta?: Record<string, unknown>;
191 > }
192 >
193 > /**
194 > * A client currently providing tools and interactive capabilities to a session.
195 > *
196 > * A session MAY have several active clients at once; entries in
197 > * {@link SessionState.activeClients} are keyed by `clientId`. The server SHOULD
198 > * automatically remove an active client when that client disconnects.
199 > *
200 > * @category Session State
201 > */
202 > export interface SessionActiveClient {
203 > /** Client identifier (matches `clientId` from `initialize`) */
204 > clientId: string;
205 > /** Human-readable client name (e.g. `"VS Code"`) */
206 > displayName?: string;
207 > /** Tools this client provides to the session */
208 > tools: ToolDefinition[];
209 > /**
210 > * Plugin customizations this client contributes to the session.
211 > *
212 > * Clients publish in [Open Plugins](https://open-plugins.com/) format
213 > * — i.e. always container-shaped plugins. They MAY synthesize virtual
214 > * plugins in memory and rely on the host to expand them into concrete
215 > * children inside {@link SessionState.customizations}.
216 > */
217 > customizations?: ClientPluginCustomization[];
218 > }
219 >
220 > // ─── Session Input Requests ──────────────────────────────────────────────────
221 >
222 > /**
223 > * Discriminant for the kinds of outstanding input a session can surface in
224 > * {@link SessionState.inputNeeded}.
225 > *
226 > * This is a general/typological union (not a lifecycle), so the discriminant is
227 > * a `*Kind`.
228 > *
229 > * @category Session Input Types
230 > */
231 > export const enum SessionInputRequestKind {
232 > /** A user-facing elicitation mirrored from an unresolved chat response part. */
233 > ChatInput = 'chatInput',
234 > /** A tool call awaiting parameter- or result-confirmation. */
235 > ToolConfirmation = 'toolConfirmation',
236 > /** A running tool the session wants an active client to execute. */
237 > ToolClientExecution = 'toolClientExecution',
238 > /** A tool call blocked on MCP authentication mid-execution. */
239 > ToolAuthentication = 'toolAuthentication',
240 > }
241 >
242 > /**
243 > * Fields common to every {@link SessionInputRequest} variant.
244 > *
245 > * @category Session Input Types
246 > */
247 > interface SessionInputRequestBase {
248 > /**
249 > * Stable key for this entry, unique within the session's
250 > * {@link SessionState.inputNeeded} list. The host derives it however it likes
251 > * (for example from the chat URI plus the underlying request or tool-call
252 > * id); consumers MUST treat it as opaque. It is the key for the
253 > * `session/inputNeededSet` / `session/inputNeededRemoved` upsert convention.
254 > */
255 > id: string;
256 > /**
257 > * The chat the underlying request lives in. This is the channel a client
258 > * dispatches its response to — it does not need to have subscribed to that
259 > * chat first.
260 > */
261 > chat: URI;
262 > }
263 >
264 > /**
265 > * A user-input elicitation surfaced at the session level, mirroring the request
266 > * from an unresolved {@link InputRequestResponsePart} in the owning chat.
267 > *
268 > * Respond by dispatching `chat/inputCompleted` (or syncing drafts with
269 > * `chat/inputAnswerChanged`) to {@link SessionInputRequestBase.chat | `chat`},
270 > * keyed by {@link ChatInputRequest.id | `request.id`}.
271 > *
272 > * @category Session Input Types
273 > */
274 > export interface SessionChatInputRequest extends SessionInputRequestBase {
275 > kind: SessionInputRequestKind.ChatInput;
276 > /** The mirrored chat input request. */
277 > request: ChatInputRequest;
278 > }
279 >
280 > /**
281 > * A tool call blocked on confirmation — either parameter confirmation before
282 > * execution or result confirmation after — surfaced at the session level.
283 > *
284 > * Respond by dispatching `chat/toolCallConfirmed` (for
285 > * {@link ToolCallPendingConfirmationState}) or `chat/toolCallResultConfirmed`
286 > * (for {@link ToolCallPendingResultConfirmationState}) to
287 > * {@link SessionInputRequestBase.chat | `chat`}, keyed by `turnId` and
288 > * `toolCall.toolCallId`.
289 > *
290 > * @category Session Input Types
291 > */
292 > export interface SessionToolConfirmationRequest extends SessionInputRequestBase {
293 > kind: SessionInputRequestKind.ToolConfirmation;
294 > /** The turn the tool call belongs to. */
295 > turnId: string;
296 > /** The tool call awaiting confirmation. */
297 > toolCall: ToolCallConfirmationState;
298 > }
299 >
300 > /**
301 > * A running tool whose execution is delegated to an active client. Surfaced so
302 > * a client that provides the tool can pick up the work without subscribing to
303 > * the owning chat.
304 > *
305 > * The {@link toolCall} is always a {@link ToolCallRunningState} (a
306 > * {@link ToolCallState} in `running` status) whose
307 > * {@link ToolCallRunningState.contributor | `contributor`} is a client
308 > * {@link ToolCallClientContributor} whose `clientId` matches the denormalized
309 > * {@link clientId} here. Execute and report the result by dispatching
310 > * `chat/toolCallComplete` (and optionally streaming with
311 > * `chat/toolCallContentChanged`) to {@link SessionInputRequestBase.chat |
312 > * `chat`}, keyed by `turnId` and `toolCall.toolCallId`.
313 > *
314 > * @category Session Input Types
315 > */
316 > export interface SessionToolClientExecutionRequest extends SessionInputRequestBase {
317 > kind: SessionInputRequestKind.ToolClientExecution;
318 > /** The turn the tool call belongs to. */
319 > turnId: string;
320 > /**
321 > * The `clientId` expected to execute the tool. Matches the `clientId` of the
322 > * tool call's client {@link ToolCallContributor}.
323 > */
324 > clientId: string;
325 > /**
326 > * The running tool call the session wants the owning client to execute. The
327 > * host only ever populates this with a {@link ToolCallRunningState} (i.e. a
328 > * {@link ToolCallState} in `running` status).
329 > */
330 > toolCall: ToolCallState;
331 > }
332 >
333 > /**
334 > * A tool call blocked on MCP authentication mid-execution, surfaced at the
335 > * session level.
336 > *
337 > * The {@link toolCall} is always a {@link ToolCallAuthRequiredState} (a
338 > * {@link ToolCallState} in `auth-required` status). Unlike
339 > * {@link SessionToolConfirmationRequest}, this is **not** answered by
340 > * dispatching a `chat/*` action directly: the client obtains a token for
341 > * {@link ToolCallAuthRequiredState.auth | `toolCall.auth`}`.resource` and
342 > * pushes it via the existing `authenticate` command (see
343 > * {@link /specification/authentication | Authentication}). The host resumes
344 > * the tool call and dispatches `chat/toolCallAuthResolved` once the token is
345 > * accepted, at which point it also removes this entry with
346 > * `session/inputNeededRemoved`.
347 > *
348 > * @category Session Input Types
349 > */
350 > export interface SessionToolAuthenticationRequest extends SessionInputRequestBase {
351 > kind: SessionInputRequestKind.ToolAuthentication;
352 > /** The turn the tool call belongs to. */
353 > turnId: string;
354 > /** The tool call awaiting authentication. */
355 > toolCall: ToolCallAuthRequiredState;
356 > }
357 >
358 > /**
359 > * One outstanding piece of input a session is blocked on, aggregated across all
360 > * chats in {@link SessionState.inputNeeded}.
361 > *
362 > * Each entry is self-sufficient: it carries the owning
363 > * {@link SessionInputRequestBase.chat | `chat`} URI plus every identifier needed
364 > * to construct the response, so a client can answer by dispatching the ordinary
365 > * `chat/*` action (`chat/inputCompleted`, `chat/toolCallConfirmed`,
366 > * `chat/toolCallComplete`, …) to that chat's channel **without having subscribed
367 > * to the chat** — except {@link SessionToolAuthenticationRequest}, which is
368 > * resolved via the `authenticate` command instead. The host removes the entry
369 > * with `session/inputNeededRemoved` once the underlying request resolves.
370 > *
371 > * @category Session Input Types
372 > */
373 > export type SessionInputRequest =
374 > | SessionChatInputRequest
375 > | SessionToolConfirmationRequest
376 > | SessionToolClientExecutionRequest
377 > | SessionToolAuthenticationRequest;
378 >
379 > /**
380 > * Server-owned project metadata for a session.
381 > *
382 > * @category Session State
383 > */
384 > export interface ProjectInfo {
385 > /** Project URI */
386 > uri: URI;
387 > /** Human-readable project name */
388 > displayName: string;
389 > }
390 >
391 > /**
392 > * Lightweight catalog entry summarizing one session. Surfaced via
393 > * {@link RootChannelCommands.listSessions | `root/listSessions`} and
394 > * `root/sessionAdded`/`root/sessionSummaryChanged` notifications.
395 > *
396 > * **Aggregation across chats.** Once a session contains more than one chat,
397 > * several `SessionSummary` fields are derived from the underlying
398 > * {@link SessionState.chats | chat catalog}. Producers SHOULD follow these
399 > * rules so clients that only consume the session summary (e.g. a session
400 > * list) still see meaningful state:
401 > *
402 > * - `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /
403 > * `Error` — bits 0–4) from the
404 > * {@link SessionState.defaultChat | default chat} when present, else from
405 > * the most recently modified chat. **Promote** `InputNeeded` whenever any
406 > * chat in the session needs input, and **promote** `Error` whenever any
407 > * chat is in an error state — both override the default-chat bits. The
408 > * orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.
409 > * - `activity`: mirror the activity string of the default chat, or of the
410 > * chat currently driving the promoted status bits when a non-default chat
411 > * wins (e.g. the chat that raised `InputNeeded`).
412 > * - `modifiedAt`: the max of all chats' `modifiedAt`.
413 > * - `workingDirectories`: the session-level set. Individual chats MAY restrict
414 > * to a subset via {@link ChatSummary.workingDirectories}; aggregating these
415 > * up is meaningless and SHOULD NOT be attempted.
416 > * - `changes`: optional roll-up across all chats. Producers MAY sum the
417 > * per-chat changeset stats or report the most expensive chat's stats —
418 > * whichever is cheaper for the host to compute.
419 > *
420 > * Sessions with a single chat trivially satisfy all of the above (the chat's
421 > * values pass through unchanged). The rules only matter once a session
422 > * carries multiple chats.
423 > *
424 > * @category Session State
425 > */
426 > export interface SessionSummary extends SessionMetadata {
427 > /** Session URI */
428 > resource: URI;
429 > /** Creation timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) */
430 > createdAt: string;
431 > /** Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) */
432 > modifiedAt: string;
433 > /**
434 > * Aggregate summary of file changes associated with this session. Servers
435 > * may populate this to give clients a quick at-a-glance view of the
436 > * session's footprint (e.g., for list rendering) without requiring the
437 > * client to subscribe to a changeset.
438 > */
439 > changes?: ChangesSummary;
440 > /**
441 > * Lightweight server-defined metadata clients may use for the session
442 > * presentation. The protocol does not interpret these values; producers
443 > * SHOULD keep the payload small because summaries appear in session lists
444 > * and session notifications.
445 > */
446 > _meta?: Record<string, unknown>;
447 > }
448 >
449 > /**
450 > * Aggregate counts describing the file changes associated with a session.
451 > *
452 > * All fields are optional so servers can populate only the metrics they
453 > * cheaply have available.
454 > *
455 > * @category Session State
456 > */
457 > export interface ChangesSummary {
458 > /** Total number of inserted lines across all changed files. */
459 > additions?: number;
460 > /** Total number of deleted lines across all changed files. */
461 > deletions?: number;
462 > /** Number of files that have changes. */
463 > files?: number;
464 > }
465 >
466 > // ─── Agent Selection ─────────────────────────────────────────────────────────
467 >
468 > /**
469 > * A selected custom agent for a session.
470 > *
471 > * The `uri` identifies a specific custom agent (matching an
472 > * {@link AgentCustomization.uri | `AgentCustomization.uri`} exposed via
473 > * the session's effective customizations). Consumers resolve the agent's
474 > * display name by looking up `uri` in the session's customization tree.
475 > *
476 > * A message with no `agent` selected uses the provider's default behavior.
477 > *
478 > * @category Session State
479 > */
480 > export interface AgentSelection {
481 > /** Stable agent URI (matches an {@link AgentCustomization.uri}). */
482 > uri: URI;
483 > }
484 >
485 > // ─── Session Config Types ────────────────────────────────────────────────────
486 >
487 > /**
488 > * A session configuration property descriptor.
489 > *
490 > * Extends the generic {@link ConfigPropertySchema} with session-specific
491 > * display extensions.
492 > *
493 > * @category Session Config Types
494 > */
495 > export interface SessionConfigPropertySchema extends ConfigPropertySchema {
496 > /**
497 > * Display extension: when `true`, the full set of allowed values is too large
498 > * to enumerate statically. The client SHOULD use `sessionConfigCompletions`
499 > * to fetch matching values based on user input. Any values in `enum` are
500 > * seed/recent values for initial display.
501 > */
502 > enumDynamic?: boolean;
503 > /** When `true`, the user may change this property after session creation */
504 > sessionMutable?: boolean;
505 > }
506 >
507 > /**
508 > * A JSON Schema object describing available session configuration metadata.
509 > *
510 > * @category Session Config Types
511 > */
512 > export interface SessionConfigSchema {
513 > /** JSON Schema: always `'object'` */
514 > type: 'object';
515 > /** JSON Schema: property descriptors keyed by property id */
516 > properties: Record<string, SessionConfigPropertySchema>;
517 > /** JSON Schema: list of required property ids */
518 > required?: string[];
519 > }
520 >
521 > /**
522 > * Live session configuration metadata.
523 > *
524 > * The schema describes the available configuration properties and the values
525 > * contain the current value for each resolved property.
526 > *
527 > * @category Session Config Types
528 > */
529 > export interface SessionConfigState {
530 > /** JSON Schema describing available configuration properties */
531 > schema: SessionConfigSchema;
532 > /** Current configuration values */
533 > values: Record<string, unknown>;
534 > }
535 >
536 > // ─── Tool Definition Types ───────────────────────────────────────────────────
537 >
538 > /**
539 > * Describes a tool available in a session, provided by either the server or the active client.
540 > *
541 > * @category Tool Definition Types
542 > */
543 > export interface ToolDefinition {
544 > /** Unique tool identifier */
545 > name: string;
546 > /** Human-readable display name */
547 > title?: string;
548 > /** Description of what the tool does */
549 > description?: string;
550 > /**
551 > * JSON Schema defining the expected input parameters.
552 > *
553 > * Optional because client-provided tools may not have formal schemas.
554 > * Mirrors MCP `Tool.inputSchema`.
555 > */
556 > inputSchema?: {
557 > type: 'object';
558 > properties?: Record<string, object>;
559 > required?: string[];
560 > };
561 > /**
562 > * JSON Schema defining the structure of the tool's output.
563 > *
564 > * Mirrors MCP `Tool.outputSchema`.
565 > */
566 > outputSchema?: {
567 > type: 'object';
568 > properties?: Record<string, object>;
569 > required?: string[];
570 > };
571 > /** Behavioral hints about the tool. All properties are advisory. */
572 > annotations?: ToolAnnotations;
573 > /**
574 > * Additional provider-specific metadata.
575 > *
576 > * Mirrors the MCP `_meta` convention.
577 > */
578 > _meta?: Record<string, unknown>;
579 > }
580 >
581 > /**
582 > * Behavioral hints about a tool. All properties are advisory and not
583 > * guaranteed to faithfully describe tool behavior.
584 > *
585 > * Mirrors MCP `ToolAnnotations` from the Model Context Protocol specification.
586 > *
587 > * @category Tool Definition Types
588 > */
589 > export interface ToolAnnotations {
590 > /** Alternate human-readable title */
591 > title?: string;
592 > /** Tool does not modify its environment (default: false) */
593 > readOnlyHint?: boolean;
594 > /** Tool may perform destructive updates (default: true) */
595 > destructiveHint?: boolean;
596 > /** Repeated calls with the same arguments have no additional effect (default: false) */
597 > idempotentHint?: boolean;
598 > /** Tool may interact with external entities (default: true) */
599 > openWorldHint?: boolean;
600 > }
601 >
602 > // ─── Customization Types ─────────────────────────────────────────────────────
603 >
604 > /**
605 > * Discriminant for the kind of customization.
606 > *
607 > * Top-level entries in {@link SessionState.customizations} and
608 > * {@link AgentInfo.customizations} are either container customizations
609 > * ({@link CustomizationType.Plugin | `Plugin`} or
610 > * {@link CustomizationType.Directory | `Directory`}) or
611 > * {@link CustomizationType.McpServer | `McpServer`} entries surfaced
612 > * directly by the host. The remaining types appear only as children of
613 > * a container.
614 > *
615 > * @category Customization Types
616 > */
617 > export const enum CustomizationType {
618 > Plugin = 'plugin',
619 > Directory = 'directory',
620 > Agent = 'agent',
621 > Skill = 'skill',
622 > Prompt = 'prompt',
623 > Rule = 'rule',
624 > Hook = 'hook',
625 > McpServer = 'mcpServer',
626 > }
627 >
628 > /**
629 > * Customization types that appear as children of a
630 > * {@link PluginCustomization} or {@link DirectoryCustomization}.
631 > *
632 > * @category Customization Types
633 > */
634 > export type ChildCustomizationType =
635 > | CustomizationType.Agent
636 > | CustomizationType.Skill
637 > | CustomizationType.Prompt
638 > | CustomizationType.Rule
639 > | CustomizationType.Hook
640 > | CustomizationType.McpServer;
641 >
642 > /**
643 > * Fields shared by every customization variant.
644 > *
645 > * @category Customization Types
646 > */
647 > interface CustomizationBase {
648 > /**
649 > * Session-unique opaque identifier. Used by every action that targets a
650 > * specific customization. Minted by whoever publishes the customization
651 > * (typically the agent host).
652 > */
653 > id: string;
654 > /**
655 > * Source URI for this customization. A plugin URL, a file URI, or a
656 > * directory URI.
657 > *
658 > * For declarations that live inside a larger file — e.g. an MCP
659 > * server declared inline in a `plugins.json` manifest — `uri` points
660 > * to the containing file and {@link CustomizationBase.range | `range`}
661 > * narrows it to the declaration's span.
662 > */
663 > uri: URI;
664 > /** Human-readable name. */
665 > name: string;
666 > /** Icons for UI display. */
667 > icons?: Icon[];
668 > /**
669 > * Optional span within {@link CustomizationBase.uri | `uri`} when this
670 > * customization is a subset of a larger file (for example, one entry
671 > * in an inline `mcpServers` block of a `plugins.json` manifest).
672 > * Absent when the customization covers the whole resource.
673 > */
674 > range?: TextRange;
675 > /**
676 > * Additional provider-specific metadata for this customization.
677 > *
678 > * Mirrors the MCP `_meta` convention. Optional and opaque to the
679 > * protocol; producers and consumers agree on its contents
680 > * out-of-band.
681 > */
682 > _meta?: Record<string, unknown>;
683 > }
684 >
685 > /**
686 > * Discriminant values for {@link CustomizationLoadState}.
687 > *
688 > * @category Customization Types
689 > */
690 > export const enum CustomizationLoadStatus {
691 > Loading = 'loading',
692 > Loaded = 'loaded',
693 > Degraded = 'degraded',
694 > Error = 'error',
695 > }
696 >
697 > /**
698 > * Container is being loaded by the host.
699 > *
700 > * @category Customization Types
701 > */
702 > export interface CustomizationLoadingState {
703 > kind: CustomizationLoadStatus.Loading;
704 > }
705 >
706 > /**
707 > * Container loaded successfully.
708 > *
709 > * @category Customization Types
710 > */
711 > export interface CustomizationLoadedState {
712 > kind: CustomizationLoadStatus.Loaded;
713 > }
714 >
715 > /**
716 > * Container partially loaded but has warnings.
717 > *
718 > * @category Customization Types
719 > */
720 > export interface CustomizationDegradedState {
721 > kind: CustomizationLoadStatus.Degraded;
722 > /** Human-readable description of the warning. */
723 > message: string;
724 > }
725 >
726 > /**
727 > * Container failed to load.
728 > *
729 > * @category Customization Types
730 > */
731 > export interface CustomizationErrorState {
732 > kind: CustomizationLoadStatus.Error;
733 > /** Human-readable error message. */
734 > message: string;
735 > }
736 >
737 > /**
738 > * Discriminated load state for a container customization
739 > * ({@link PluginCustomization} or {@link DirectoryCustomization}).
740 > *
741 > * @category Customization Types
742 > */
743 > export type CustomizationLoadState =
744 > | CustomizationLoadingState
745 > | CustomizationLoadedState
746 > | CustomizationDegradedState
747 > | CustomizationErrorState;
748 >
749 > /**
750 > * Fields shared by container customizations.
751 > *
752 > * @category Customization Types
753 > */
754 > interface ContainerCustomizationBase extends CustomizationBase {
755 > /** Whether this container is currently enabled. */
756 > enabled: boolean;
757 > /**
758 > * `clientId` of the client that contributed this container. Absent for
759 > * server-originated entries.
760 > */
761 > clientId?: string;
762 > /**
763 > * Host-reported load state. Absent means the host has not yet reported
764 > * a load state for this container.
765 > */
766 > load?: CustomizationLoadState;
767 > /**
768 > * Children discovered inside this container.
769 > *
770 > * Absent means the host has not parsed this container yet. An empty
771 > * array means the host parsed the container and it contributes
772 > * nothing.
773 > */
774 > children?: ChildCustomization[];
775 > }
776 >
777 > /**
778 > * An [Open Plugins](https://open-plugins.com/) plugin.
779 > *
780 > * @category Customization Types
781 > */
782 > export interface PluginCustomization extends ContainerCustomizationBase {
783 > type: CustomizationType.Plugin;
784 > /**
785 > * Version of the plugin, sourced from the
786 > * [Open Plugins](https://open-plugins.com/) manifest's optional
787 > * `version` field (semver, e.g. `"1.2.0"`). Absent when the manifest
788 > * declares no version — the field is optional there — or the source
789 > * has no version concept. Provenance / display only: the host neither
790 > * parses nor enforces it.
791 > */
792 > version?: string;
793 > }
794 >
795 > /**
796 > * A {@link PluginCustomization} as published by a client. Extends the
797 > * server-facing shape with an opaque `nonce` so the host can detect when
798 > * the client's view of a plugin has changed and re-parse only as needed.
799 > *
800 > * Clients SHOULD include a `nonce`. Server-side fields like
801 > * {@link ContainerCustomizationBase.children | `children`} and
802 > * {@link ContainerCustomizationBase.load | `load`} are typically left
803 > * absent on publication and populated by the host when the resolved
804 > * plugin appears in {@link SessionState.customizations}.
805 > *
806 > * @category Customization Types
807 > */
808 > export interface ClientPluginCustomization extends PluginCustomization {
809 > /** Opaque version token used by the host to detect changes. */
810 > nonce?: string;
811 > }
812 >
813 > /**
814 > * A directory the host watches for this session.
815 > *
816 > * Presence in the customization list signals that the host may discover
817 > * customizations from this directory. When `writable` is `true`, clients
818 > * MAY persist new customizations into the directory using
819 > * [`resourceWrite`](/reference/common#resourcewrite); the host will
820 > * then surface the resulting child via the customization actions.
821 > *
822 > * The directory may not yet exist on disk.
823 > *
824 > * @category Customization Types
825 > */
826 > export interface DirectoryCustomization extends ContainerCustomizationBase {
827 > type: CustomizationType.Directory;
828 > /** Which child customization type this directory holds. */
829 > contents: ChildCustomizationType;
830 > /** Whether clients may write into this directory. */
831 > writable: boolean;
832 > }
833 >
834 > /**
835 > * Fields shared by the leaf child customizations that live inside a
836 > * container — {@link AgentCustomization}, {@link SkillCustomization},
837 > * {@link PromptCustomization}, {@link RuleCustomization}, and
838 > * {@link HookCustomization}.
839 > *
840 > * {@link McpServerCustomization} is also a child but does not extend this
841 > * base: it always carries an explicit {@link McpServerCustomization.enabled}
842 > * because it can appear as a top-level customization too.
843 > *
844 > * @category Customization Types
845 > */
846 > interface ChildCustomizationBase extends CustomizationBase {
847 > /**
848 > * Whether this child is individually enabled. Absent means enabled, so a
849 > * producer only needs to set it to surface a child that exists but is
850 > * turned off on its own.
851 > *
852 > * This flag is independent of the parent container's: the **effective**
853 > * enabled state of a child is
854 > * `container.enabled && (child.enabled ?? true)`, so a disabled container
855 > * disables every child regardless of each child's own flag.
856 > *
857 > * A child is turned on or off by id with
858 > * {@link SessionCustomizationToggledAction | `session/customizationToggled`}.
859 > */
860 > enabled?: boolean;
861 > }
862 >
863 > /**
864 > * A custom agent contributed by a plugin or directory.
865 > *
866 > * Mirrors the [Open Plugins agent](https://open-plugins.com/agent-builders/components/agents)
867 > * format: a markdown file with YAML frontmatter, where the body is the
868 > * agent's system prompt.
869 > *
870 > * @category Customization Types
871 > */
872 > export interface AgentCustomization extends ChildCustomizationBase {
873 > type: CustomizationType.Agent;
874 > /**
875 > * Short description of what the agent specializes in and when to
876 > * invoke it. Sourced from the agent file's frontmatter `description`.
877 > */
878 > description?: string;
879 > /**
880 > * Model the agent is pinned to, sourced from the agent file's
881 > * frontmatter `model`. Absent means the agent inherits the session's
882 > * default model.
883 > */
884 > model?: string;
885 > /**
886 > * Allowlist of tool names the agent is scoped to, sourced from the
887 > * agent file's frontmatter `tools`. A non-empty list restricts the
888 > * agent to exactly those tools. Absent — or an empty list — imposes no
889 > * restriction beyond the session default: the agent may use any
890 > * available tool. Producers express "no restriction" by omitting the
891 > * field rather than sending an empty array, so an empty list carries no
892 > * meaning distinct from absence.
893 > */
894 > tools?: string[];
895 > /**
896 > * When `true`, the agent will not auto-delegate to this custom agent
897 > * as a sub-agent; it can only be selected by the user. Absent or
898 > * `false` means the agent may delegate to it.
899 > */
900 > disableModelInvocation?: boolean;
901 > /**
902 > * When `true`, the user cannot select this custom agent (for example,
903 > * in a picker); it remains available for the agent to auto-delegate
904 > * to. Absent or `false` means the user may select it.
905 > */
906 > disableUserInvocation?: boolean;
907 > }
908 >
909 > /**
910 > * A skill contributed by a plugin or directory.
911 > *
912 > * Covers both [Open Plugins skill formats](https://open-plugins.com/agent-builders/components/skills)
913 > * — the `skills/` directory layout (one subdirectory per skill, each with
914 > * a `SKILL.md`) and the flatter `commands/` directory of slash-command
915 > * skills.
916 > *
917 > * @category Customization Types
918 > */
919 > export interface SkillCustomization extends ChildCustomizationBase {
920 > type: CustomizationType.Skill;
921 > /**
922 > * Short description used for help text and auto-invocation matching.
923 > * Sourced from the skill's frontmatter `description`.
924 > */
925 > description?: string;
926 > /**
927 > * When `true`, only the user can invoke this skill — the agent will not
928 > * auto-invoke it. Sourced from the command skill's frontmatter
929 > * `disable-model-invocation` flag.
930 > */
931 > disableModelInvocation?: boolean;
932 > /**
933 > * When `true`, the user cannot directly invoke this skill (for example,
934 > * as a slash command); it remains available for the agent to
935 > * auto-invoke. Absent or `false` means the user may invoke it.
936 > */
937 > disableUserInvocation?: boolean;
938 > }
939 >
940 > /**
941 > * A prompt contributed by a plugin or directory.
942 > *
943 > * @category Customization Types
944 > */
945 > export interface PromptCustomization extends ChildCustomizationBase {
946 > type: CustomizationType.Prompt;
947 > /** Short description of what the prompt does. */
948 > description?: string;
949 > }
950 >
951 > /**
952 > * A rule contributed by a plugin or directory.
953 > *
954 > * Mirrors the [Open Plugins rule](https://open-plugins.com/agent-builders/components/rules)
955 > * format: a markdown file (e.g. `.mdc`) whose body is injected into
956 > * context while the rule is active. This type also covers tool-specific
957 > * "instruction" formats (e.g. VS Code Copilot's
958 > * `.github/instructions/*.md`), which differ only in naming — they
959 > * share the same semantics of `description`, optional always-on
960 > * activation, and optional glob scoping.
961 > *
962 > * @category Customization Types
963 > */
964 > export interface RuleCustomization extends ChildCustomizationBase {
965 > type: CustomizationType.Rule;
966 > /**
967 > * Description of what the rule enforces.
968 > */
969 > description?: string;
970 > /**
971 > * When `true`, the rule is always active (subject to `globs` if any).
972 > * When `false` or absent, the agent or user decides whether to apply
973 > * the rule.
974 > */
975 > alwaysApply?: boolean;
976 > /**
977 > * Glob patterns the rule applies to. When present, the rule is only
978 > * active for matching files.
979 > */
980 > globs?: string[];
981 > }
982 >
983 > /**
984 > * A hook manifest contributed by a plugin or directory.
985 > *
986 > * @category Customization Types
987 > */
988 > export interface HookCustomization extends ChildCustomizationBase {
989 > type: CustomizationType.Hook;
990 > }
991 >
992 > /**
993 > * An MCP server contributed by a plugin or directory.
994 > *
995 > * When the server is declared inline in the containing plugin manifest,
996 > * `uri` points at the manifest file and
997 > * {@link CustomizationBase.range | `range`} narrows it to the
998 > * declaration's span.
999 > *
1000 > * The MCP server customization also reflects its current status.
1001 > *
1002 > * @category Customization Types
1003 > */
1004 > export interface McpServerCustomization extends CustomizationBase {
1005 > type: CustomizationType.McpServer;
1006 > /**
1007 > * Whether this MCP server is currently enabled.
1008 > */
1009 > enabled: boolean;
1010 > /**
1011 > * Current lifecycle state of the MCP server.
1012 > */
1013 > state: McpServerState;
1014 > /**
1015 > * An `mcp://`-protocol channel the client uses to side-channel traffic
1016 > * into the upstream MCP server itself. The channel is NOT a fresh raw MCP
1017 > * connection: it piggybacks on the AHP transport
1018 > * and skips the MCP `initialize` sequence.
1019 > *
1020 > * The agent host MAY only serve a subset of MCP on this
1021 > * channel; the served subset is described by domain-specific
1022 > * capabilities such as those in
1023 > * {@link McpServerCustomizationApps.capabilities}.
1024 > *
1025 > * The channel URI SHOULD be stable across the server's lifetime, but
1026 > * the agent host MAY change it (for example across a restart) and
1027 > * MAY only expose it while the server is in
1028 > * {@link McpServerStatus.Ready | `Ready`}. Absence means no
1029 > * side-channel is currently available.
1030 > */
1031 > channel?: URI;
1032 > /**
1033 > * MCP App support. This property SHOULD be advertised for MCP servers
1034 > * which support apps.
1035 > */
1036 > mcpApp?: McpServerCustomizationApps;
1037 > }
1038 >
1039 > /**
1040 > * Information from the agent host needed to render MCP Apps served
1041 > * by this MCP server.
1042 > *
1043 > * @category MCP Server State
1044 > */
1045 > export interface McpServerCustomizationApps {
1046 > /**
1047 > * The subset of MCP App
1048 > * [`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)
1049 > * the AHP host can satisfy for Views backed by this server. The
1050 > * client feeds these straight through into the `hostCapabilities` of
1051 > * the `ui/initialize` response delivered to the View.
1052 > */
1053 > capabilities: AhpMcpUiHostCapabilities;
1054 > }
1055 >
1056 > /**
1057 > * The subset of MCP App
1058 > * [`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)
1059 > * an AHP host can derive from the upstream MCP server (and from AHP's own
1060 > * forwarding plumbing). Advertised on
1061 > * {@link McpServerCustomizationApps.capabilities} so clients can pass it
1062 > * through into the `hostCapabilities` of the `ui/initialize` response
1063 > * delivered to an MCP App View.
1064 > *
1065 > * Field names mirror the MCP Apps spec exactly, so the AHP-side producer
1066 > * can pass them straight through into the `hostCapabilities` of the
1067 > * `ui/initialize` response delivered to the View.
1068 > *
1069 > * Capabilities outside this set (`openLinks`, `downloadFile`, `sandbox`,
1070 > * `experimental`) are decided locally by whichever AHP client renders the
1071 > * View and are NOT part of this AHP-level advertisement — only the
1072 > * server-derived subset is.
1073 > *
1074 > * An agent host MUST only advertise a capability when it actually accepts the
1075 > * corresponding methods/notifications on the `mcp://` channel:
1076 > *
1077 > * - {@link serverTools}: host proxies `tools/list` and `tools/call` to
1078 > * the MCP server. When `listChanged` is `true`, the host also forwards
1079 > * `notifications/tools/list_changed`.
1080 > * - {@link serverResources}: host proxies `resources/read`,
1081 > * `resources/list`, and `resources/templates/list` to the MCP server.
1082 > * When `listChanged` is `true`, the host also forwards
1083 > * `notifications/resources/list_changed`.
1084 > * - {@link logging}: host accepts `notifications/message` log entries
1085 > * from the App and forwards them via `mcpNotification` (and forwards
1086 > * `logging/setLevel` calls to the server).
1087 > * - {@link sampling}: host serves `sampling/createMessage` via
1088 > * `mcpMethodCall`. When `sampling.tools` is present, the host also
1089 > * accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks
1090 > * inside `CreateMessageRequest`.
1091 > *
1092 > * @category MCP Server State
1093 > * @see {@link https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx | MCP Apps spec (SEP-1865)}
1094 > */
1095 > export interface AhpMcpUiHostCapabilities {
1096 > /** Producer proxies the MCP `tools/*` methods to the upstream server. */
1097 > serverTools?: {
1098 > /** Producer forwards `notifications/tools/list_changed` from the server. */
1099 > listChanged?: boolean;
1100 > };
1101 > /** Producer proxies the MCP `resources/*` methods to the upstream server. */
1102 > serverResources?: {
1103 > /** Producer forwards `notifications/resources/list_changed` from the server. */
1104 > listChanged?: boolean;
1105 > };
1106 > /** Producer accepts `notifications/message` log entries from the App via `mcpNotification`. */
1107 > logging?: Record<string, never>;
1108 > /** Producer serves `sampling/createMessage` via `mcpMethodCall`. */
1109 > sampling?: {
1110 > /**
1111 > * Producer accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content
1112 > * blocks inside `CreateMessageRequest`.
1113 > */
1114 > tools?: Record<string, never>;
1115 > };
1116 > }
1117 >
1118 > /**
1119 > * Child customizations that live inside a {@link PluginCustomization} or
1120 > * {@link DirectoryCustomization}.
1121 > *
1122 > * @category Customization Types
1123 > */
1124 > export type ChildCustomization =
1125 > | AgentCustomization
1126 > | SkillCustomization
1127 > | PromptCustomization
1128 > | RuleCustomization
1129 > | HookCustomization
1130 > | McpServerCustomization;
1131 >
1132 > /**
1133 > * A top-level customization active in a session. Either a container
1134 > * ({@link PluginCustomization} or {@link DirectoryCustomization}) whose
1135 > * leaf customizations live in its
1136 > * {@link ContainerCustomizationBase.children | `children`} array, or a
1137 > * bare {@link McpServerCustomization} surfaced directly by the host.
1138 > *
1139 > * @category Customization Types
1140 > */
1141 > export type Customization =
1142 > | PluginCustomization
1143 > | DirectoryCustomization
1144 > | McpServerCustomization;
1145 >
1146 >
1147 > // ─── MCP Server State ────────────────────────────────────────────────────────
1148 >
1149 > /**
1150 > * Discriminant for the {@link McpServerState} union.
1151 > *
1152 > * @category MCP Server State
1153 > */
1154 > export const enum McpServerStatus {
1155 > /** Server has been registered but is not yet running. */
1156 > Starting = 'starting',
1157 > /** Server is running and serving requests. */
1158 > Ready = 'ready',
1159 > /**
1160 > * Server is reachable but requires additional authentication before it
1161 > * can start, or before it can serve a particular request. Carries the
1162 > * RFC 9728 Protected Resource Metadata the client needs to obtain a
1163 > * token; the client then pushes the token via the existing
1164 > * `authenticate` command.
1165 > */
1166 > AuthRequired = 'authRequired',
1167 > /** Server failed to start, crashed, or otherwise transitioned to a fatal error. */
1168 > Error = 'error',
1169 > /** Server has been shut down. */
1170 > Stopped = 'stopped',
1171 > }
1172 >
1173 > /**
1174 > * Why an MCP server is currently in the {@link McpServerStatus.AuthRequired}
1175 > * state. Mirrors the three failure modes defined by the
1176 > * [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md).
1177 > *
1178 > * @category MCP Server State
1179 > */
1180 > export const enum McpAuthRequiredReason {
1181 > /** No token has been provided yet (HTTP 401, no prior token). */
1182 > Required = 'required',
1183 > /** A previously valid token expired or was revoked (HTTP 401). */
1184 > Expired = 'expired',
1185 > /**
1186 > * Step-up auth: a token is present but its scopes are insufficient for
1187 > * the requested operation (HTTP 403 with
1188 > * `WWW-Authenticate: Bearer error="insufficient_scope"`).
1189 > *
1190 > * Unlike {@link Required} and {@link Expired} — which typically surface
1191 > * before any tool work is in flight — `InsufficientScope` is almost
1192 > * always triggered by an MCP request issued mid-turn (a `tools/call`,
1193 > * `resources/read`, etc.). The host SHOULD pair the
1194 > * {@link McpServerAuthRequiredState} transition with
1195 > * {@link SessionStatus.InputNeeded} on
1196 > * {@link SessionSummary.status | the session} so the activity becomes
1197 > * visible at the session-summary level, and clients SHOULD watch for
1198 > * this kind on any
1199 > * {@link McpServerCustomization | MCP server} backing a running tool
1200 > * call so they can present an explicit "grant more access" affordance
1201 > * tied to the blocked tool call.
1202 > */
1203 > InsufficientScope = 'insufficientScope',
1204 > }
1205 >
1206 > /**
1207 > * Server is registered with the host but has not yet started.
1208 > *
1209 > * @category MCP Server State
1210 > */
1211 > export interface McpServerStartingState {
1212 > kind: McpServerStatus.Starting;
1213 > }
1214 >
1215 > /**
1216 > * Server is running and serving requests.
1217 > *
1218 > * @category MCP Server State
1219 > */
1220 > export interface McpServerReadyState {
1221 > kind: McpServerStatus.Ready;
1222 > }
1223 >
1224 > /**
1225 > * A pre-registered OAuth client that clients use instead of dynamic client
1226 > * registration when resolving an MCP authentication challenge.
1227 > *
1228 > * @category MCP Server State
1229 > */
1230 > export interface McpOAuthClient {
1231 > /** OAuth client identifier registered with the authorization server. */
1232 > clientId: string;
1233 > /**
1234 > * OAuth client secret for a confidential client. Absence means the client is
1235 > * public and uses a secretless flow such as authorization code with PKCE.
1236 > */
1237 > clientSecret?: string;
1238 > }
1239 >
1240 > /**
1241 > * Reusable MCP authentication challenge — the RFC 9728 discovery info a
1242 > * client needs to obtain a token and push it via the `authenticate` command.
1243 > * Deliberately carries **no token**: this describes what is being asked for,
1244 > * never the ****** itself.
1245 > *
1246 > * Shared by two independent state machines that describe the same OAuth
1247 > * challenge from different vantage points:
1248 > *
1249 > * - {@link McpServerAuthRequiredState} — the MCP server itself cannot serve
1250 > * *any* request until the client authenticates.
1251 > * - {@link ToolCallAuthRequiredState} — a specific in-flight tool call is
1252 > * paused pending authentication (typically
1253 > * {@link McpAuthRequiredReason.InsufficientScope} step-up auth
1254 > * mid-execution). The server state and the tool-call state remain
1255 > * separate on purpose: the server saying "I need auth" and a tool
1256 > * invocation saying "I am waiting on that auth" are different facts that
1257 > * can be true independently.
1258 > *
1259 > * @category MCP Server State
1260 > */
1261 > export interface McpAuthRequirement {
1262 > /** Why authentication is required. */
1263 > reason: McpAuthRequiredReason;
1264 > /**
1265 > * Pre-registered OAuth client to use for authorization. When present, clients
1266 > * MUST use these credentials instead of dynamic client registration.
1267 > */
1268 > oauthClient?: McpOAuthClient;
1269 > /**
1270 > * RFC 9728 Protected Resource Metadata. The `resource` field is the
1271 > * canonical MCP server URI per RFC 8707, used as the OAuth `resource`
1272 > * indicator. `authorization_servers` is REQUIRED by the MCP
1273 > * authorization spec.
1274 > */
1275 > resource: ProtectedResourceMetadata;
1276 > /**
1277 > * Scopes required for the current challenge, parsed from the
1278 > * `WWW-Authenticate: ******"…"` header (or `scopes_supported`
1279 > * fallback). Authoritative for the next authorization request — clients
1280 > * MUST NOT assume any subset/superset relationship to
1281 > * `resource.scopes_supported`.
1282 > */
1283 > requiredScopes?: string[];
1284 > /** Human-readable hint, typically from the OAuth `error_description`. */
1285 > description?: string;
1286 > }
1287 >
1288 > /**
1289 > * Server is reachable but cannot serve requests until the client
1290 > * authenticates. Mirrors the discovery flow defined by
1291 > * [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)
1292 > * (Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge
1293 > * semantics required by the MCP authorization spec.
1294 > *
1295 > * Clients react to this state by calling the existing `authenticate`
1296 > * command with the {@link ProtectedResourceMetadata.resource | resource}
1297 > * carried here. There is **no** `notify/authRequired` notification for
1298 > * MCP servers — the action stream is the single source of truth.
1299 > *
1300 > * When the transition is triggered by a request issued during a turn
1301 > * — most commonly
1302 > * {@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}
1303 > * surfacing mid-tool-call — the host SHOULD also raise
1304 > * {@link SessionStatus.InputNeeded} on the session so the block is
1305 > * visible at the summary level. Clients SHOULD watch this status on
1306 > * any MCP server backing a running tool call and surface an explicit
1307 > * affordance (e.g. a "grant additional access" prompt) tied to that
1308 > * tool call, rather than relying on the user to notice the
1309 > * customization’s status badge.
1310 > *
1311 > * @category MCP Server State
1312 > */
1313 > export interface McpServerAuthRequiredState extends McpAuthRequirement {
1314 > kind: McpServerStatus.AuthRequired;
1315 > }
1316 >
1317 > /**
1318 > * Server failed to start, crashed, or otherwise transitioned to a
1319 > * non-recoverable error. Use {@link McpServerStatus.AuthRequired}
1320 > * for authentication failures.
1321 > *
1322 > * @category MCP Server State
1323 > */
1324 > export interface McpServerErrorState {
1325 > kind: McpServerStatus.Error;
1326 > /** Error details. */
1327 > error: ErrorInfo;
1328 > }
1329 >
1330 > /**
1331 > * Server has been shut down. The host MAY remove the server from the
1332 > * session entirely shortly after this state.
1333 > *
1334 > * @category MCP Server State
1335 > */
1336 > export interface McpServerStoppedState {
1337 > kind: McpServerStatus.Stopped;
1338 > }
1339 >
1340 > /**
1341 > * Discriminated union of all MCP server lifecycle states.
1342 > * Discriminated by `kind` (a {@link McpServerStatus} value).
1343 > *
1344 > * @category MCP Server State
1345 > */
1346 > export type McpServerState =
1347 > | McpServerStartingState
1348 > | McpServerReadyState
1349 > | McpServerAuthRequiredState
1350 > | McpServerErrorState
1351 > | McpServerStoppedState;
src/vs/platform/terminal/common/terminal.ts 1253 covered LOC · 4 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- terminal.ts
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 > import { Event } from '../../../base/common/event.js';
7 > import { IProcessEnvironment, OperatingSystem } from '../../../base/common/platform.js';
8 > import { URI, UriComponents } from '../../../base/common/uri.js';
9 > import { createDecorator } from '../../instantiation/common/instantiation.js';
10 > import { IPtyHostProcessReplayEvent, ISerializedCommandDetectionCapability, ITerminalCapabilityStore, type ITerminalCommand } from './capabilities/capabilities.js';
11 > import { IGetTerminalLayoutInfoArgs, IProcessDetails, ISetTerminalLayoutInfoArgs } from './terminalProcess.js';
12 > import { ThemeIcon } from '../../../base/common/themables.js';
13 > import { ISerializableEnvironmentVariableCollections } from './environmentVariable.js';
14 > import { IWorkspaceFolder } from '../../workspace/common/workspace.js';
15 > import { Registry } from '../../registry/common/platform.js';
16 > import type * as performance from '../../../base/common/performance.js';
17 > import { ILogService } from '../../log/common/log.js';
18 > import type { IAction } from '../../../base/common/actions.js';
19 > import type { IDisposable } from '../../../base/common/lifecycle.js';
20 > import type { SingleOrMany } from '../../../base/common/types.js';
21 >
22 > export const enum TerminalSettingPrefix {
23 > AutomationProfile = 'terminal.integrated.automationProfile.',
24 > DefaultProfile = 'terminal.integrated.defaultProfile.',
25 > Profiles = 'terminal.integrated.profiles.'
26 > }
27 >
28 > export const enum TerminalSettingId {
29 > SendKeybindingsToShell = 'terminal.integrated.sendKeybindingsToShell',
30 > AutomationProfileLinux = 'terminal.integrated.automationProfile.linux',
31 > AutomationProfileMacOs = 'terminal.integrated.automationProfile.osx',
32 > AutomationProfileWindows = 'terminal.integrated.automationProfile.windows',
33 > AgentHostProfileLinux = 'terminal.integrated.agentHostProfile.linux',
34 > AgentHostProfileMacOs = 'terminal.integrated.agentHostProfile.osx',
35 > AgentHostProfileWindows = 'terminal.integrated.agentHostProfile.windows',
36 > ProfilesWindows = 'terminal.integrated.profiles.windows',
37 > ProfilesMacOs = 'terminal.integrated.profiles.osx',
38 > ProfilesLinux = 'terminal.integrated.profiles.linux',
39 > DefaultProfileLinux = 'terminal.integrated.defaultProfile.linux',
40 > DefaultProfileMacOs = 'terminal.integrated.defaultProfile.osx',
41 > DefaultProfileWindows = 'terminal.integrated.defaultProfile.windows',
42 > UseWslProfiles = 'terminal.integrated.useWslProfiles',
43 > TabsDefaultColor = 'terminal.integrated.tabs.defaultColor',
44 > TabsDefaultIcon = 'terminal.integrated.tabs.defaultIcon',
45 > TabsEnabled = 'terminal.integrated.tabs.enabled',
46 > TabsEnableAnimation = 'terminal.integrated.tabs.enableAnimation',
47 > TabsHideCondition = 'terminal.integrated.tabs.hideCondition',
48 > TabsShowActiveTerminal = 'terminal.integrated.tabs.showActiveTerminal',
49 > TabsShowActions = 'terminal.integrated.tabs.showActions',
50 > TabsLocation = 'terminal.integrated.tabs.location',
51 > TabsFocusMode = 'terminal.integrated.tabs.focusMode',
52 > TabsAllowAgentCliTitle = 'terminal.integrated.tabs.allowAgentCliTitle',
53 > MacOptionIsMeta = 'terminal.integrated.macOptionIsMeta',
54 > MacOptionClickForcesSelection = 'terminal.integrated.macOptionClickForcesSelection',
55 > AltClickMovesCursor = 'terminal.integrated.altClickMovesCursor',
56 > CopyOnSelection = 'terminal.integrated.copyOnSelection',
57 > EnableMultiLinePasteWarning = 'terminal.integrated.enableMultiLinePasteWarning',
58 > DrawBoldTextInBrightColors = 'terminal.integrated.drawBoldTextInBrightColors',
59 > FontFamily = 'terminal.integrated.fontFamily',
60 > FontSize = 'terminal.integrated.fontSize',
61 > LetterSpacing = 'terminal.integrated.letterSpacing',
62 > LineHeight = 'terminal.integrated.lineHeight',
63 > MinimumContrastRatio = 'terminal.integrated.minimumContrastRatio',
64 > TabStopWidth = 'terminal.integrated.tabStopWidth',
65 > FastScrollSensitivity = 'terminal.integrated.fastScrollSensitivity',
66 > MouseWheelScrollSensitivity = 'terminal.integrated.mouseWheelScrollSensitivity',
67 > BellDuration = 'terminal.integrated.bellDuration',
68 > FontWeight = 'terminal.integrated.fontWeight',
69 > FontWeightBold = 'terminal.integrated.fontWeightBold',
70 > CursorBlinking = 'terminal.integrated.cursorBlinking',
71 > TextBlinking = 'terminal.integrated.textBlinking',
72 > CursorStyle = 'terminal.integrated.cursorStyle',
73 > CursorStyleInactive = 'terminal.integrated.cursorStyleInactive',
74 > CursorWidth = 'terminal.integrated.cursorWidth',
75 > Scrollback = 'terminal.integrated.scrollback',
76 > DetectLocale = 'terminal.integrated.detectLocale',
77 > DefaultLocation = 'terminal.integrated.defaultLocation',
78 > GpuAcceleration = 'terminal.integrated.gpuAcceleration',
79 > TerminalTitleSeparator = 'terminal.integrated.tabs.separator',
80 > TerminalTitle = 'terminal.integrated.tabs.title',
81 > TerminalDescription = 'terminal.integrated.tabs.description',
82 > RightClickBehavior = 'terminal.integrated.rightClickBehavior',
83 > MiddleClickBehavior = 'terminal.integrated.middleClickBehavior',
84 > Cwd = 'terminal.integrated.cwd',
85 > ConfirmOnExit = 'terminal.integrated.confirmOnExit',
86 > ConfirmOnKill = 'terminal.integrated.confirmOnKill',
87 > EnableBell = 'terminal.integrated.enableBell',
88 > EnableVisualBell = 'terminal.integrated.enableVisualBell',
89 > CommandsToSkipShell = 'terminal.integrated.commandsToSkipShell',
90 > AllowChords = 'terminal.integrated.allowChords',
91 > AllowMnemonics = 'terminal.integrated.allowMnemonics',
92 > TabFocusMode = 'terminal.integrated.tabFocusMode',
93 > EnvMacOs = 'terminal.integrated.env.osx',
94 > EnvLinux = 'terminal.integrated.env.linux',
95 > EnvWindows = 'terminal.integrated.env.windows',
96 > EnvironmentChangesRelaunch = 'terminal.integrated.environmentChangesRelaunch',
97 > ShowExitAlert = 'terminal.integrated.showExitAlert',
98 > SplitCwd = 'terminal.integrated.splitCwd',
99 > WindowsUseConptyDll = 'terminal.integrated.windowsUseConptyDll',
100 > WordSeparators = 'terminal.integrated.wordSeparators',
101 > EnableFileLinks = 'terminal.integrated.enableFileLinks',
102 > AllowedLinkSchemes = 'terminal.integrated.allowedLinkSchemes',
103 > UnicodeVersion = 'terminal.integrated.unicodeVersion',
104 > EnablePersistentSessions = 'terminal.integrated.enablePersistentSessions',
105 > PersistentSessionReviveProcess = 'terminal.integrated.persistentSessionReviveProcess',
106 > HideOnStartup = 'terminal.integrated.hideOnStartup',
107 > HideOnLastClosed = 'terminal.integrated.hideOnLastClosed',
108 > CustomGlyphs = 'terminal.integrated.customGlyphs',
109 > RescaleOverlappingGlyphs = 'terminal.integrated.rescaleOverlappingGlyphs',
110 > PersistentSessionScrollback = 'terminal.integrated.persistentSessionScrollback',
111 > InheritEnv = 'terminal.integrated.inheritEnv',
112 > ShowLinkHover = 'terminal.integrated.showLinkHover',
113 > IgnoreProcessNames = 'terminal.integrated.ignoreProcessNames',
114 > ShellIntegrationEnabled = 'terminal.integrated.shellIntegration.enabled',
115 > ShellIntegrationShowWelcome = 'terminal.integrated.shellIntegration.showWelcome',
116 > ShellIntegrationDecorationsEnabled = 'terminal.integrated.shellIntegration.decorationsEnabled',
117 > ShellIntegrationTimeout = 'terminal.integrated.shellIntegration.timeout',
118 > ShellIntegrationQuickFixEnabled = 'terminal.integrated.shellIntegration.quickFixEnabled',
119 > ShellIntegrationEnvironmentReporting = 'terminal.integrated.shellIntegration.environmentReporting',
120 > EnableImages = 'terminal.integrated.enableImages',
121 > SmoothScrolling = 'terminal.integrated.smoothScrolling',
122 > IgnoreBracketedPasteMode = 'terminal.integrated.ignoreBracketedPasteMode',
123 > FocusAfterRun = 'terminal.integrated.focusAfterRun',
124 > FontLigaturesEnabled = 'terminal.integrated.fontLigatures.enabled',
125 > FontLigaturesFeatureSettings = 'terminal.integrated.fontLigatures.featureSettings',
126 > FontLigaturesFallbackLigatures = 'terminal.integrated.fontLigatures.fallbackLigatures',
127 > EnableKittyKeyboardProtocol = 'terminal.integrated.enableKittyKeyboardProtocol',
128 > EnableWin32InputMode = 'terminal.integrated.enableWin32InputMode',
129 > AllowInUntrustedWorkspace = 'terminal.integrated.allowInUntrustedWorkspace',
130 >
131 > // Developer/debug settings
132 >
133 > /** Simulated latency applied to all calls made to the pty host */
134 > DeveloperPtyHostLatency = 'terminal.integrated.developer.ptyHost.latency',
135 > /** Simulated startup delay of the pty host process */
136 > DeveloperPtyHostStartupDelay = 'terminal.integrated.developer.ptyHost.startupDelay',
137 > /** Shows the textarea element */
138 > DevMode = 'terminal.integrated.developer.devMode'
139 > }
140 >
141 > export const enum PosixShellType {
142 > Bash = 'bash',
143 > Fish = 'fish',
144 > Sh = 'sh',
145 > Csh = 'csh',
146 > Ksh = 'ksh',
147 > Zsh = 'zsh',
148 >
149 > }
150 > export const enum WindowsShellType {
151 > CommandPrompt = 'cmd',
152 > Wsl = 'wsl',
153 > GitBash = 'gitbash',
154 > }
155 >
156 > export const enum GeneralShellType {
157 > Claude = 'claude',
158 > Codex = 'codex',
159 > CommandCode = 'commandcode',
160 > Copilot = 'copilot',
161 > Gemini = 'gemini',
162 > PowerShell = 'pwsh',
163 > Python = 'python',
164 > Julia = 'julia',
165 > NuShell = 'nu',
166 > Node = 'node',
167 > Xonsh = 'xonsh',
168 > }
169 > export type TerminalShellType = PosixShellType | WindowsShellType | GeneralShellType | undefined;
170 >
171 > export interface IRawTerminalInstanceLayoutInfo<T> {
172 > relativeSize: number;
173 > terminal: T;
174 > }
175 > export type ITerminalInstanceLayoutInfoById = IRawTerminalInstanceLayoutInfo<number>;
176 > export type ITerminalInstanceLayoutInfo = IRawTerminalInstanceLayoutInfo<IPtyHostAttachTarget>;
177 >
178 > export interface IRawTerminalTabLayoutInfo<T> {
179 > isActive: boolean;
180 > activePersistentProcessId: number | undefined;
181 > terminals: IRawTerminalInstanceLayoutInfo<T>[];
182 > }
183 >
184 > export type ITerminalTabLayoutInfoById = IRawTerminalTabLayoutInfo<number>;
185 >
186 > export interface IRawTerminalsLayoutInfo<T> {
187 > tabs: IRawTerminalTabLayoutInfo<T>[];
188 > background: T[] | null;
189 > }
190 >
191 > export interface IPtyHostAttachTarget {
192 > id: number;
193 > pid: number;
194 > title: string;
195 > titleSource: TitleEventSource;
196 > cwd: string;
197 > workspaceId: string;
198 > workspaceName: string;
199 > isOrphan: boolean;
200 > icon: TerminalIcon | undefined;
201 > fixedDimensions: IFixedTerminalDimensions | undefined;
202 > environmentVariableCollections: ISerializableEnvironmentVariableCollections | undefined;
203 > reconnectionProperties?: IReconnectionProperties;
204 > waitOnExit?: WaitOnExitValue;
205 > hideFromUser?: boolean;
206 > isFeatureTerminal?: boolean;
207 > type?: TerminalType;
208 > hasChildProcesses: boolean;
209 > shellIntegrationNonce: string;
210 > tabActions?: ITerminalTabAction[];
211 > }
212 >
213 > export interface IReconnectionProperties {
214 > ownerId: string;
215 > data?: unknown;
216 > }
217 >
218 > export type TerminalType = 'Task' | 'Local' | undefined;
219 >
220 > export enum TitleEventSource {
221 > /** From the API or the rename command that overrides any other type */
222 > Api,
223 > /** From the process name property*/
224 > Process,
225 > /** From the VT sequence */
226 > Sequence,
227 > /** Config changed */
228 > Config
229 > }
230 >
231 > export type ITerminalsLayoutInfo = IRawTerminalsLayoutInfo<IPtyHostAttachTarget | null>;
232 > export type ITerminalsLayoutInfoById = IRawTerminalsLayoutInfo<number>;
233 >
234 > export enum TerminalIpcChannels {
235 > /**
236 > * Communicates between the renderer process and shared process.
237 > */
238 > LocalPty = 'localPty',
239 > /**
240 > * Communicates between the shared process and the pty host process.
241 > */
242 > PtyHost = 'ptyHost',
243 > /**
244 > * Communicates between the renderer process and the pty host process.
245 > */
246 > PtyHostWindow = 'ptyHostWindow',
247 > /**
248 > * Deals with logging from the pty host process.
249 > */
250 > Logger = 'logger',
251 > /**
252 > * Enables the detection of unresponsive pty hosts.
253 > */
254 > Heartbeat = 'heartbeat'
255 > }
256 >
257 > export const enum ProcessPropertyType {
258 > Cwd = 'cwd',
259 > InitialCwd = 'initialCwd',
260 > FixedDimensions = 'fixedDimensions',
261 > Title = 'title',
262 > ShellType = 'shellType',
263 > HasChildProcesses = 'hasChildProcesses',
264 > ResolvedShellLaunchConfig = 'resolvedShellLaunchConfig',
265 > OverrideDimensions = 'overrideDimensions',
266 > FailedShellIntegrationActivation = 'failedShellIntegrationActivation',
267 > UsedShellIntegrationInjection = 'usedShellIntegrationInjection',
268 > ShellIntegrationInjectionFailureReason = 'shellIntegrationInjectionFailureReason',
269 > }
270 >
271 > export interface IProcessProperty<T extends ProcessPropertyType = ProcessPropertyType> {
272 > type: T;
273 > value: IProcessPropertyMap[T];
274 > }
275 >
276 > export interface IProcessPropertyMap {
277 > [ProcessPropertyType.Cwd]: string;
278 > [ProcessPropertyType.InitialCwd]: string;
279 > [ProcessPropertyType.FixedDimensions]: IFixedTerminalDimensions;
280 > [ProcessPropertyType.Title]: string;
281 > [ProcessPropertyType.ShellType]: TerminalShellType | undefined;
282 > [ProcessPropertyType.HasChildProcesses]: boolean;
283 > [ProcessPropertyType.ResolvedShellLaunchConfig]: IShellLaunchConfig;
284 > [ProcessPropertyType.OverrideDimensions]: ITerminalDimensionsOverride | undefined;
285 > [ProcessPropertyType.FailedShellIntegrationActivation]: boolean | undefined;
286 > [ProcessPropertyType.UsedShellIntegrationInjection]: boolean | undefined;
287 > [ProcessPropertyType.ShellIntegrationInjectionFailureReason]: ShellIntegrationInjectionFailureReason | undefined;
288 > }
289 >
290 > export interface IFixedTerminalDimensions {
291 > /**
292 > * The fixed columns of the terminal.
293 > */
294 > cols?: number;
295 >
296 > /**
297 > * The fixed rows of the terminal.
298 > */
299 > rows?: number;
300 > }
301 >
302 > export interface ITerminalLaunchResult {
303 > injectedArgs: string[];
304 > }
305 >
306 > /**
307 > * A service that communicates with a pty host.
308 > */
309 > export interface IPtyService {
310 > readonly _serviceBrand: undefined;
311 >
312 > readonly onProcessData: Event<{ id: number; event: IProcessDataEvent | string }>;
313 > readonly onProcessReady: Event<{ id: number; event: IProcessReadyEvent }>;
314 > readonly onProcessReplay: Event<{ id: number; event: IPtyHostProcessReplayEvent }>;
315 > readonly onProcessOrphanQuestion: Event<{ id: number }>;
316 > readonly onDidRequestDetach: Event<{ requestId: number; workspaceId: string; instanceId: number }>;
317 > readonly onDidChangeProperty: Event<{ id: number; property: IProcessProperty }>;
318 > readonly onProcessExit: Event<{ id: number; event: number | undefined }>;
319 >
320 > createProcess(
321 > shellLaunchConfig: IShellLaunchConfig,
322 > cwd: string,
323 > cols: number,
324 > rows: number,
325 > unicodeVersion: '6' | '11',
326 > env: IProcessEnvironment,
327 > executableEnv: IProcessEnvironment,
328 > options: ITerminalProcessOptions,
329 > shouldPersist: boolean,
330 > workspaceId: string,
331 > workspaceName: string
332 > ): Promise<number>;
333 > attachToProcess(id: number): Promise<void>;
334 > detachFromProcess(id: number, forcePersist?: boolean): Promise<void>;
335 > shutdownAll(): Promise<void>;
336 >
337 > /**
338 > * Lists all orphaned processes, ie. those without a connected frontend.
339 > */
340 > listProcesses(): Promise<IProcessDetails[]>;
341 > getPerformanceMarks(): Promise<performance.PerformanceMark[]>;
342 > /**
343 > * Measures and returns the latency of the current and all other processes to the pty host.
344 > */
345 > getLatency(): Promise<IPtyHostLatencyMeasurement[]>;
346 >
347 > start(id: number): Promise<ITerminalLaunchError | ITerminalLaunchResult | undefined>;
348 > shutdown(id: number, immediate: boolean): Promise<void>;
349 > input(id: number, data: string): Promise<void>;
350 > sendSignal(id: number, signal: string): Promise<void>;
351 > resize(id: number, cols: number, rows: number, pixelWidth?: number, pixelHeight?: number): Promise<void>;
352 > clearBuffer(id: number): Promise<void>;
353 > getInitialCwd(id: number): Promise<string>;
354 > getCwd(id: number): Promise<string>;
355 > acknowledgeDataEvent(id: number, charCount: number): Promise<void>;
356 > setNextCommandId(id: number, commandLine: string, commandId: string): Promise<void>;
357 > setUnicodeVersion(id: number, version: '6' | '11'): Promise<void>;
358 > processBinary(id: number, data: string): Promise<void>;
359 > /** Confirm the process is _not_ an orphan. */
360 > orphanQuestionReply(id: number): Promise<void>;
361 > updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise<void>;
362 > updateIcon(id: number, userInitiated: boolean, icon: TerminalIcon, color?: string): Promise<void>;
363 >
364 > getDefaultSystemShell(osOverride?: OperatingSystem): Promise<string>;
365 > getEnvironment(): Promise<IProcessEnvironment>;
366 > getWslPath(original: string, direction: 'unix-to-win' | 'win-to-unix'): Promise<string>;
367 > getRevivedPtyNewId(workspaceId: string, id: number): Promise<number | undefined>;
368 > setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise<void>;
369 > getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise<ITerminalsLayoutInfo | undefined>;
370 > reduceConnectionGraceTime(): Promise<void>;
371 > requestDetachInstance(workspaceId: string, instanceId: number): Promise<IProcessDetails | undefined>;
372 > acceptDetachInstanceReply(requestId: number, persistentProcessId?: number): Promise<void>;
373 > freePortKillProcess(port: string): Promise<{ port: string; processId: string }>;
374 > /**
375 > * Serializes and returns terminal state.
376 > * @param ids The persistent terminal IDs to serialize.
377 > */
378 > serializeTerminalState(ids: number[]): Promise<string>;
379 > /**
380 > * Revives a workspaces terminal processes, these can then be reconnected to using the normal
381 > * flow for restoring terminals after reloading.
382 > */
383 > reviveTerminalProcesses(workspaceId: string, state: ISerializedTerminalState[], dateTimeFormatLocate: string): Promise<void>;
384 > refreshProperty<T extends ProcessPropertyType>(id: number, property: T): Promise<IProcessPropertyMap[T]>;
385 > updateProperty<T extends ProcessPropertyType>(id: number, property: T, value: IProcessPropertyMap[T]): Promise<void>;
386 >
387 > // TODO: Make mandatory and remove impl from pty host service
388 > refreshIgnoreProcessNames?(names: string[]): Promise<void>;
389 >
390 > // #region Pty service contribution RPC calls
391 >
392 > installAutoReply(match: string, reply: string): Promise<void>;
393 > uninstallAllAutoReplies(): Promise<void>;
394 >
395 > // #endregion
396 > }
397 > export const IPtyService = createDecorator<IPtyService>('ptyService');
398 >
399 > export interface IPtyServiceContribution {
400 > handleProcessReady(persistentProcessId: number, process: ITerminalChildProcess): void;
401 > handleProcessDispose(persistentProcessId: number): void;
402 > handleProcessInput(persistentProcessId: number, data: string): void;
403 > handleProcessResize(persistentProcessId: number, cols: number, rows: number, pixelWidth?: number, pixelHeight?: number): void;
404 > }
405 >
406 > export interface IPtyHostController {
407 > readonly onPtyHostExit: Event<number>;
408 > readonly onPtyHostStart: Event<void>;
409 > readonly onPtyHostUnresponsive: Event<void>;
410 > readonly onPtyHostResponsive: Event<void>;
411 > readonly onPtyHostRequestResolveVariables: Event<IRequestResolveVariablesEvent>;
412 >
413 > restartPtyHost(): Promise<void>;
414 > acceptPtyHostResolvedVariables(requestId: number, resolved: string[]): Promise<void>;
415 > getProfiles(workspaceId: string, profiles: unknown, defaultProfile: unknown, includeDetectedProfiles?: boolean): Promise<ITerminalProfile[]>;
416 > }
417 >
418 > /**
419 > * A service that communicates with a pty host controller (eg. main or server
420 > * process) and is able to launch and forward requests to the pty host.
421 > */
422 > export interface IPtyHostService extends IPtyService, IPtyHostController {
423 > }
424 >
425 > export interface IPtyHostLatencyMeasurement {
426 > label: string;
427 > latency: number;
428 > }
429 >
430 > /**
431 > * Serialized terminal state matching the interface that can be used across versions, the version
432 > * should be verified before using the state payload.
433 > */
434 > export interface ICrossVersionSerializedTerminalState {
435 > version: number;
436 > state: unknown;
437 > }
438 >
439 > export interface ISerializedTerminalState {
440 > id: number;
441 > shellLaunchConfig: IShellLaunchConfig;
442 > processDetails: IProcessDetails;
443 > processLaunchConfig: IPersistentTerminalProcessLaunchConfig;
444 > unicodeVersion: '6' | '11';
445 > replayEvent: IPtyHostProcessReplayEvent;
446 > timestamp: number;
447 > }
448 >
449 > export interface IPersistentTerminalProcessLaunchConfig {
450 > env: IProcessEnvironment;
451 > executableEnv: IProcessEnvironment;
452 > options: ITerminalProcessOptions;
453 > }
454 >
455 > export interface IRequestResolveVariablesEvent {
456 > requestId: number;
457 > workspaceId: string;
458 > originalText: string[];
459 > }
460 >
461 > export enum HeartbeatConstants {
462 > /**
463 > * The duration between heartbeats
464 > */
465 > BeatInterval = 5000,
466 > /**
467 > * The duration of the first heartbeat while the pty host is starting up. This is much larger
468 > * than the regular BeatInterval to accommodate slow machines, we still want to warn about the
469 > * pty host's unresponsiveness eventually though.
470 > */
471 > ConnectingBeatInterval = 20000,
472 > /**
473 > * Defines a multiplier for BeatInterval for how long to wait before starting the second wait
474 > * timer.
475 > */
476 > FirstWaitMultiplier = 1.2,
477 > /**
478 > * Defines a multiplier for BeatInterval for how long to wait before telling the user about
479 > * non-responsiveness. The second timer is to avoid informing the user incorrectly when waking
480 > * the computer up from sleep
481 > */
482 > SecondWaitMultiplier = 1,
483 > /**
484 > * How long to wait before telling the user about non-responsiveness when they try to create a
485 > * process. This short circuits the standard wait timeouts to tell the user sooner and only
486 > * create process is handled to avoid additional perf overhead.
487 > */
488 > CreateProcessTimeout = 5000
489 > }
490 >
491 > export interface IHeartbeatService {
492 > readonly onBeat: Event<void>;
493 > }
494 >
495 >
496 > export interface IShellLaunchConfig {
497 > /**
498 > * The name of the terminal, if this is not set the name of the process will be used.
499 > */
500 > name?: string;
501 >
502 > /**
503 > * A string to follow the name of the terminal with, indicating the type of terminal
504 > */
505 > type?: 'Task' | 'Local';
506 >
507 > /**
508 > * The shell executable (bash, cmd, etc.).
509 > */
510 > executable?: string;
511 >
512 > /**
513 > * The CLI arguments to use with executable, a string[] is in argv format and will be escaped,
514 > * a string is in "CommandLine" pre-escaped format and will be used as is. The string option is
515 > * only supported on Windows and will throw an exception if used on macOS or Linux.
516 > */
517 > args?: string[] | string;
518 >
519 > /**
520 > * The current working directory of the terminal, this overrides the `terminal.integrated.cwd`
521 > * settings key.
522 > */
523 > cwd?: string | URI;
524 >
525 > /**
526 > * A custom environment for the terminal, if this is not set the environment will be inherited
527 > * from the VS Code process.
528 > */
529 > env?: ITerminalEnvironment;
530 >
531 > /**
532 > * Whether to ignore a custom cwd from the `terminal.integrated.cwd` settings key (e.g. if the
533 > * shell is being launched by an extension).
534 > */
535 > ignoreConfigurationCwd?: boolean;
536 >
537 > /**
538 > * The reconnection properties for this terminal
539 > */
540 > reconnectionProperties?: IReconnectionProperties;
541 >
542 > /** Whether to wait for a key press before closing the terminal. */
543 > waitOnExit?: WaitOnExitValue;
544 >
545 > /**
546 > * A string including ANSI escape sequences that will be written to the terminal emulator
547 > * _before_ the terminal process has launched, when a string is specified, a trailing \n is
548 > * added at the end. This allows for example the terminal instance to display a styled message
549 > * as the first line of the terminal. Use \x1b over \033 or \e for the escape control character.
550 > */
551 > initialText?: string | { text: string; trailingNewLine: boolean };
552 >
553 > /**
554 > * Custom PTY/pseudoterminal process to use.
555 > */
556 > customPtyImplementation?: (terminalId: number, cols: number, rows: number) => ITerminalChildProcess;
557 >
558 > /**
559 > * A UUID generated by the extension host process for terminals created on the extension host process.
560 > */
561 > extHostTerminalId?: string;
562 >
563 > /**
564 > * This is a terminal that attaches to an already running terminal.
565 > */
566 > attachPersistentProcess?: {
567 > id: number;
568 > findRevivedId?: boolean;
569 > pid: number;
570 > title: string;
571 > titleSource: TitleEventSource;
572 > cwd: string;
573 > icon?: TerminalIcon;
574 > color?: string;
575 > hasChildProcesses?: boolean;
576 > fixedDimensions?: IFixedTerminalDimensions;
577 > environmentVariableCollections?: ISerializableEnvironmentVariableCollections;
578 > reconnectionProperties?: IReconnectionProperties;
579 > type?: TerminalType;
580 > waitOnExit?: WaitOnExitValue;
581 > hideFromUser?: boolean;
582 > isFeatureTerminal?: boolean;
583 > shellIntegrationNonce: string;
584 > tabActions?: ITerminalTabAction[];
585 > };
586 >
587 > /**
588 > * Whether the terminal process environment should be exactly as provided in
589 > * `TerminalOptions.env`. When this is false (default), the environment will be based on the
590 > * window's environment and also apply configured platform settings like
591 > * `terminal.integrated.env.windows` on top. When this is true, the complete environment must be
592 > * provided as nothing will be inherited from the process or any configuration.
593 > */
594 > strictEnv?: boolean;
595 >
596 > /**
597 > * Whether the terminal process environment will inherit VS Code's "shell environment" that may
598 > * get sourced from running a login shell depnding on how the application was launched.
599 > * Consumers that rely on development tools being present in the $PATH should set this to true.
600 > * This will overwrite the value of the inheritEnv setting.
601 > */
602 > useShellEnvironment?: boolean;
603 >
604 > /**
605 > * When enabled the terminal will run the process as normal but not be surfaced to the user
606 > * until `Terminal.show` is called. The typical usage for this is when you need to run
607 > * something that may need interactivity but only want to tell the user about it when
608 > * interaction is needed. Note that the terminals will still be exposed to all extensions
609 > * as normal. The hidden terminals will not be restored when the workspace is next opened.
610 > */
611 > hideFromUser?: boolean;
612 >
613 > /**
614 > * Whether to force the terminal to persist across sessions regardless of the other
615 > * launch config, like `hideFromUser`.
616 > */
617 > forcePersist?: boolean;
618 >
619 > /**
620 > * Whether this terminal is not a terminal that the user directly created and uses, but rather
621 > * a terminal used to drive some VS Code feature.
622 > */
623 > isFeatureTerminal?: boolean;
624 >
625 > /**
626 > * Whether this terminal was created by an extension.
627 > */
628 > isExtensionOwnedTerminal?: boolean;
629 >
630 > /**
631 > * The icon for the terminal, used primarily in the terminal tab.
632 > */
633 > icon?: TerminalIcon;
634 >
635 > /**
636 > * The color ID to use for this terminal. If not specified it will use the default fallback
637 > */
638 > color?: string;
639 >
640 > /**
641 > * When a parent terminal is provided via API, the group needs
642 > * to find the index in order to place the child
643 > * directly to the right of its parent.
644 > */
645 > parentTerminalId?: number;
646 >
647 > /**
648 > * The dimensions for the instance as set by the user
649 > * or via Size to Content Width
650 > */
651 > fixedDimensions?: IFixedTerminalDimensions;
652 >
653 > /**
654 > * Opt-out of the default terminal persistence on restart and reload
655 > */
656 > isTransient?: boolean;
657 >
658 > /**
659 > * Attempt to force shell integration to be enabled by bypassing the {@link isFeatureTerminal}
660 > * equals false requirement.
661 > */
662 > forceShellIntegration?: boolean;
663 >
664 > /**
665 > * Create a terminal without shell integration even when it's enabled
666 > */
667 > ignoreShellIntegration?: boolean;
668 >
669 > /**
670 > * Actions to include inline on hover of the terminal tab. E.g. the "Rerun task" action
671 > */
672 > tabActions?: ITerminalTabAction[];
673 > /**
674 > * Report terminal's shell environment variables to VS Code and extensions
675 > */
676 > shellIntegrationEnvironmentReporting?: boolean;
677 >
678 > /**
679 > * A custom nonce to use for shell integration when provided by an extension.
680 > * This allows extensions to control shell integration for terminals they create.
681 > */
682 > shellIntegrationNonce?: string;
683 >
684 > /**
685 > * A title template string that supports the same variables as the
686 > * `terminal.integrated.tabs.title` setting. When set, this overrides the config-based
687 > * title template for this terminal instance.
688 > */
689 > titleTemplate?: string;
690 > }
691 >
692 > export interface ITerminalTabAction {
693 > id: string;
694 > label: string;
695 > icon?: ThemeIcon;
696 > }
697 >
698 > export type WaitOnExitValue = boolean | string | ((exitCode: number) => string);
699 >
700 > export interface ICreateContributedTerminalProfileOptions {
701 > icon?: URI | string | { light: URI; dark: URI };
702 > color?: string;
703 > location?: TerminalLocation | { viewColumn: number; preserveState?: boolean } | { splitActiveTerminal: boolean };
704 > cwd?: string | URI;
705 > titleTemplate?: string;
706 > }
707 >
708 > export enum TerminalLocation {
709 > Panel = 1,
710 > Editor = 2
711 > }
712 >
713 > export const enum TerminalLocationConfigValue {
714 > TerminalView = 'view',
715 > Editor = 'editor'
716 > }
717 >
718 > export type TerminalIcon = ThemeIcon | URI | { light: URI; dark: URI };
719 >
720 > export interface IShellLaunchConfigDto {
721 > name?: string;
722 > executable?: string;
723 > args?: string[] | string;
724 > cwd?: string | UriComponents;
725 > env?: ITerminalEnvironment;
726 > useShellEnvironment?: boolean;
727 > hideFromUser?: boolean;
728 > reconnectionProperties?: IReconnectionProperties;
729 > type?: 'Task' | 'Local';
730 > isFeatureTerminal?: boolean;
731 > forceShellIntegration?: boolean;
732 > tabActions?: ITerminalTabAction[];
733 > shellIntegrationEnvironmentReporting?: boolean;
734 > titleTemplate?: string;
735 > }
736 >
737 > /**
738 > * A set of options for the terminal process. These differ from the shell launch config in that they
739 > * are set internally to the terminal component, not from the outside.
740 > */
741 > export interface ITerminalProcessOptions {
742 > shellIntegration: {
743 > enabled: boolean;
744 > suggestEnabled: boolean;
745 > nonce: string;
746 > };
747 > windowsUseConptyDll: boolean;
748 > environmentVariableCollections: ISerializableEnvironmentVariableCollections | undefined;
749 > workspaceFolder: IWorkspaceFolder | undefined;
750 > isScreenReaderOptimized: boolean;
751 > }
752 >
753 > export interface ITerminalEnvironment {
754 > [key: string]: string | null | undefined;
755 > }
756 >
757 > export interface ITerminalLaunchError {
758 > message: string;
759 > code?: number;
760 > }
761 >
762 > export interface IProcessReadyEvent {
763 > pid: number;
764 > cwd: string;
765 > windowsPty: IProcessReadyWindowsPty | undefined;
766 > }
767 >
768 > export interface IProcessReadyWindowsPty {
769 > /**
770 > * What pty emulation backend is being used.
771 > */
772 > backend: 'conpty';
773 > /**
774 > * The Windows build version (eg. 19045)
775 > */
776 > buildNumber: number;
777 > }
778 >
779 > /**
780 > * An interface representing a raw terminal child process, this contains a subset of the
781 > * child_process.ChildProcess node.js interface.
782 > */
783 > export interface ITerminalChildProcess {
784 > /**
785 > * A unique identifier for the terminal process. Note that the uniqueness only applies to a
786 > * given pty service connection, IDs will be duplicated for remote and local terminals for
787 > * example. The ID will be 0 if it does not support reconnection.
788 > */
789 > id: number;
790 >
791 > /**
792 > * Whether the process should be persisted across reloads.
793 > */
794 > shouldPersist: boolean;
795 >
796 > readonly onProcessData: Event<IProcessDataEvent | string>;
797 > readonly onProcessReady: Event<IProcessReadyEvent>;
798 > readonly onProcessReplayComplete?: Event<void>;
799 > readonly onDidChangeProperty: Event<IProcessProperty>;
800 > readonly onProcessExit: Event<number | undefined>;
801 > readonly onRestoreCommands?: Event<ISerializedCommandDetectionCapability>;
802 >
803 > /**
804 > * Starts the process.
805 > *
806 > * @returns undefined when the process was successfully started, otherwise an object containing
807 > * information on what went wrong.
808 > */
809 > start(): Promise<ITerminalLaunchError | ITerminalLaunchResult | undefined>;
810 >
811 > /**
812 > * Detach the process from the UI and await reconnect.
813 > * @param forcePersist Whether to force the process to persist if it supports persistence.
814 > */
815 > detach?(forcePersist?: boolean): Promise<void>;
816 >
817 > /**
818 > * Frees the port and kills the process
819 > */
820 > freePortKillProcess?(port: string): Promise<{ port: string; processId: string }>;
821 >
822 > /**
823 > * Shutdown the terminal process.
824 > *
825 > * @param immediate When true the process will be killed immediately, otherwise the process will
826 > * be given some time to make sure no additional data comes through.
827 > */
828 > shutdown(immediate: boolean): void;
829 > input(data: string): void;
830 > sendSignal(signal: string): void;
831 > processBinary(data: string): Promise<void>;
832 > resize(cols: number, rows: number, pixelWidth?: number, pixelHeight?: number): void;
833 > clearBuffer(): void | Promise<void>;
834 >
835 > /**
836 > * Acknowledge a data event has been parsed by the terminal, this is used to implement flow
837 > * control to ensure remote processes to not get too far ahead of the client and flood the
838 > * connection.
839 > * @param charCount The number of characters being acknowledged.
840 > */
841 > acknowledgeDataEvent(charCount: number): void;
842 >
843 > /**
844 > * Sets the unicode version for the process, this drives the size of some characters in the
845 > * xterm-headless instance.
846 > */
847 > setUnicodeVersion(version: '6' | '11'): Promise<void>;
848 >
849 > getInitialCwd(): Promise<string>;
850 > getCwd(): Promise<string>;
851 > refreshProperty<T extends ProcessPropertyType>(property: T): Promise<IProcessPropertyMap[T]>;
852 > updateProperty<T extends ProcessPropertyType>(property: T, value: IProcessPropertyMap[T]): Promise<void>;
853 > }
854 >
855 > export interface IReconnectConstants {
856 > graceTime: number;
857 > shortGraceTime: number;
858 > scrollback: number;
859 > }
860 >
861 > export const enum LocalReconnectConstants {
862 > /**
863 > * If there is no reconnection within this time-frame, consider the connection permanently closed...
864 > */
865 > GraceTime = 60000, // 60 seconds
866 > /**
867 > * Maximal grace time between the first and the last reconnection...
868 > */
869 > ShortGraceTime = 6000, // 6 seconds
870 > }
871 >
872 > export const enum FlowControlConstants {
873 > /**
874 > * The number of _unacknowledged_ chars to have been sent before the pty is paused in order for
875 > * the client to catch up.
876 > */
877 > HighWatermarkChars = 100000,
878 > /**
879 > * After flow control pauses the pty for the client the catch up, this is the number of
880 > * _unacknowledged_ chars to have been caught up to on the client before resuming the pty again.
881 > * This is used to attempt to prevent pauses in the flowing data; ideally while the pty is
882 > * paused the number of unacknowledged chars would always be greater than 0 or the client will
883 > * appear to stutter. In reality this balance is hard to accomplish though so heavy commands
884 > * will likely pause as latency grows, not flooding the connection is the important thing as
885 > * it's shared with other core functionality.
886 > */
887 > LowWatermarkChars = 5000,
888 > /**
889 > * The number characters that are accumulated on the client side before sending an ack event.
890 > * This must be less than or equal to LowWatermarkChars or the terminal max never unpause.
891 > */
892 > CharCountAckSize = 5000
893 > }
894 >
895 > export interface IProcessDataEvent {
896 > data: string;
897 > trackCommit: boolean;
898 > /**
899 > * When trackCommit is set, this will be set to a promise that resolves when the data is parsed.
900 > */
901 > writePromise?: Promise<void>;
902 > }
903 >
904 > export interface ITerminalDimensions {
905 > /**
906 > * The columns of the terminal.
907 > */
908 > cols: number;
909 >
910 > /**
911 > * The rows of the terminal.
912 > */
913 > rows: number;
914 > }
915 >
916 > export interface ITerminalProfile {
917 > profileName: string;
918 > path: string;
919 > isDefault: boolean;
920 > /**
921 > * Whether the terminal profile contains a potentially unsafe {@link path}. For example, the path
922 > * `C:\Cygwin` is the default install for Cygwin on Windows, but it could be created by any
923 > * user in a multi-user environment. As such, we don't want to blindly present it as a profile
924 > * without a warning.
925 > */
926 > isUnsafePath?: boolean;
927 > /**
928 > * An additional unsafe path that must exist, for example a script that appears in {@link args}.
929 > */
930 > requiresUnsafePath?: string;
931 > isAutoDetected?: boolean;
932 > /**
933 > * Whether the profile path was found on the `$PATH` environment variable, if so it will be
934 > * cleaner to display this profile in the UI using only `basename(path)`.
935 > */
936 > isFromPath?: boolean;
937 > args?: SingleOrMany<string> | undefined;
938 > env?: ITerminalEnvironment;
939 > overrideName?: boolean;
940 > color?: string;
941 > icon?: ThemeIcon | URI | { light: URI; dark: URI };
942 > }
943 >
944 > export interface ITerminalDimensionsOverride extends Readonly<ITerminalDimensions> {
945 > /**
946 > * indicate that xterm must receive these exact dimensions, even if they overflow the ui!
947 > */
948 > forceExactSize?: boolean;
949 > }
950 >
951 > export const enum ProfileSource {
952 > GitBash = 'Git Bash',
953 > Pwsh = 'PowerShell'
954 > }
955 >
956 > export interface IBaseUnresolvedTerminalProfile {
957 > args?: SingleOrMany<string> | undefined;
958 > isAutoDetected?: boolean;
959 > overrideName?: boolean;
960 > icon?: string | ThemeIcon | URI | { light: URI; dark: URI };
961 > color?: string;
962 > env?: ITerminalEnvironment;
963 > requiresPath?: string | ITerminalUnsafePath;
964 > }
965 >
966 > export interface ITerminalUnsafePath {
967 > path: string;
968 > isUnsafe: true;
969 > }
970 >
971 > export interface ITerminalExecutable extends IBaseUnresolvedTerminalProfile {
972 > path: SingleOrMany<string | ITerminalUnsafePath>;
973 > }
974 >
975 > export interface ITerminalProfileSource extends IBaseUnresolvedTerminalProfile {
976 > source: ProfileSource;
977 > }
978 >
979 > export interface ITerminalProfileContribution {
980 > title: string;
981 > id: string;
982 > icon?: URI | { light: URI; dark: URI } | string;
983 > color?: string;
984 > titleTemplate?: string;
985 > }
986 >
987 > export interface IExtensionTerminalProfile extends ITerminalProfileContribution {
988 > extensionIdentifier: string;
989 > }
990 >
991 > export type ITerminalProfileObject = ITerminalExecutable | ITerminalProfileSource | IExtensionTerminalProfile | null;
992 >
993 > export interface IShellIntegration {
994 > readonly capabilities: ITerminalCapabilityStore;
995 > readonly seenSequences: ReadonlySet<string>;
996 > readonly status: ShellIntegrationStatus;
997 >
998 > readonly onDidChangeStatus: Event<ShellIntegrationStatus>;
999 > readonly onDidChangeSeenSequences: Event<ReadonlySet<string>>;
1000 >
1001 > deserialize(serialized: ISerializedCommandDetectionCapability): void;
1002 >
1003 > setNextCommandId(command: string, commandId: string): void;
1004 > }
1005 >
1006 > export interface IDecorationAddon {
1007 > registerMenuItems(command: ITerminalCommand, items: IAction[]): IDisposable;
1008 > }
1009 >
1010 > export interface ITerminalCompletionProviderContribution {
1011 > description?: string;
1012 > }
1013 >
1014 > export interface ITerminalContributions {
1015 > profiles?: ITerminalProfileContribution[];
1016 > completionProviders?: ITerminalCompletionProviderContribution[];
1017 > }
1018 >
1019 > export const enum ShellIntegrationStatus {
1020 > /** No shell integration sequences have been encountered. */
1021 > Off,
1022 > /** Final term shell integration sequences have been encountered. */
1023 > FinalTerm,
1024 > /** VS Code shell integration sequences have been encountered. Supercedes FinalTerm. */
1025 > VSCode
1026 > }
1027 >
1028 >
1029 > export const enum ShellIntegrationInjectionFailureReason {
1030 > /**
1031 > * The setting is disabled.
1032 > */
1033 > InjectionSettingDisabled = 'injectionSettingDisabled',
1034 > /**
1035 > * There is no executable (so there's no way to determine how to inject).
1036 > */
1037 > NoExecutable = 'noExecutable',
1038 > /**
1039 > * It's a feature terminal (tasks, debug), unless it's explicitly being forced.
1040 > */
1041 > FeatureTerminal = 'featureTerminal',
1042 > /**
1043 > * The ignoreShellIntegration flag is passed (eg. relaunching without shell integration).
1044 > */
1045 > IgnoreShellIntegrationFlag = 'ignoreShellIntegrationFlag',
1046 > /**
1047 > * Shell integration doesn't work on older Windows builds that don't support ConPTY.
1048 > */
1049 > UnsupportedWindowsBuild = 'unsupportedWindowsBuild',
1050 > /**
1051 > * We're conservative whether we inject when we don't recognize the arguments used for the
1052 > * shell as we would prefer launching one without shell integration than breaking their profile.
1053 > */
1054 > UnsupportedArgs = 'unsupportedArgs',
1055 > /**
1056 > * The shell doesn't have built-in shell integration. Note that this doesn't mean the shell
1057 > * won't have shell integration in the end.
1058 > */
1059 > UnsupportedShell = 'unsupportedShell',
1060 >
1061 >
1062 > /**
1063 > * For zsh, we failed to set the sticky bit on the shell integration script folder.
1064 > */
1065 > FailedToSetStickyBit = 'failedToSetStickyBit',
1066 >
1067 > /**
1068 > * For zsh, we failed to create a temp directory for the shell integration script.
1069 > */
1070 > FailedToCreateTmpDir = 'failedToCreateTmpDir',
1071 > }
1072 >
1073 > export const enum ShellIntegrationTimeoutOverride {
1074 > DisableForTests = -2
1075 > }
1076 >
1077 > export enum TerminalExitReason {
1078 > Unknown = 0,
1079 > Shutdown = 1,
1080 > Process = 2,
1081 > User = 3,
1082 > Extension = 4,
1083 > }
1084 >
1085 > export interface ITerminalOutputMatch {
1086 > regexMatch: RegExpMatchArray;
1087 > outputLines: string[];
1088 > }
1089 >
1090 > /**
1091 > * A matcher that runs on a sub-section of a terminal command's output
1092 > */
1093 > export interface ITerminalOutputMatcher {
1094 > /**
1095 > * A string or regex to match against the unwrapped line. If this is a regex with the multiline
1096 > * flag, it will scan an amount of lines equal to `\n` instances in the regex + 1.
1097 > */
1098 > lineMatcher: string | RegExp;
1099 > /**
1100 > * Which side of the output to anchor the {@link offset} and {@link length} against.
1101 > */
1102 > anchor: 'top' | 'bottom';
1103 > /**
1104 > * The number of rows above or below the {@link anchor} to start matching against.
1105 > */
1106 > offset: number;
1107 > /**
1108 > * The number of rows to match against, this should be as small as possible for performance
1109 > * reasons. This is capped at 40.
1110 > */
1111 > length: number;
1112 >
1113 > /**
1114 > * If multiple matches are expected - this will result in {@link outputLines} being returned
1115 > * when there's a {@link regexMatch} from {@link offset} to {@link length}
1116 > */
1117 > multipleMatches?: boolean;
1118 > }
1119 >
1120 > export interface ITerminalCommandSelector {
1121 > id: string;
1122 > commandLineMatcher: string | RegExp;
1123 > outputMatcher?: ITerminalOutputMatcher;
1124 > exitStatus: boolean;
1125 > commandExitResult: 'success' | 'error';
1126 > kind?: 'fix' | 'explain';
1127 > }
1128 >
1129 > export interface ITerminalBackend extends ITerminalBackendPtyServiceContributions {
1130 > readonly remoteAuthority: string | undefined;
1131 >
1132 > readonly isResponsive: boolean;
1133 >
1134 > /**
1135 > * A promise that resolves when the backend is ready to be used, ie. after terminal persistence
1136 > * has been actioned.
1137 > */
1138 > readonly whenReady: Promise<void>;
1139 >
1140 > /**
1141 > * Signal to the backend that persistence has been actioned and is ready for use.
1142 > */
1143 > setReady(): void;
1144 >
1145 > /**
1146 > * Fired when the ptyHost process becomes non-responsive, this should disable stdin for all
1147 > * terminals using this pty host connection and mark them as disconnected.
1148 > */
1149 > readonly onPtyHostUnresponsive: Event<void>;
1150 > /**
1151 > * Fired when the ptyHost process becomes responsive after being non-responsive. Allowing
1152 > * previously disconnected terminals to reconnect.
1153 > */
1154 > readonly onPtyHostResponsive: Event<void>;
1155 > /**
1156 > * Fired when the ptyHost has been restarted, this is used as a signal for listening terminals
1157 > * that its pty has been lost and will remain disconnected.
1158 > */
1159 > readonly onPtyHostRestart: Event<void>;
1160 >
1161 > readonly onDidRequestDetach: Event<{ requestId: number; workspaceId: string; instanceId: number }>;
1162 >
1163 > attachToProcess(id: number): Promise<ITerminalChildProcess | undefined>;
1164 > attachToRevivedProcess(id: number): Promise<ITerminalChildProcess | undefined>;
1165 > listProcesses(): Promise<IProcessDetails[]>;
1166 > getLatency(): Promise<IPtyHostLatencyMeasurement[]>;
1167 > getDefaultSystemShell(osOverride?: OperatingSystem): Promise<string>;
1168 > getProfiles(profiles: unknown, defaultProfile: unknown, includeDetectedProfiles?: boolean): Promise<ITerminalProfile[]>;
1169 > getWslPath(original: string, direction: 'unix-to-win' | 'win-to-unix'): Promise<string>;
1170 > getEnvironment(): Promise<IProcessEnvironment>;
1171 > getShellEnvironment(): Promise<IProcessEnvironment | undefined>;
1172 > setTerminalLayoutInfo(layoutInfo?: ITerminalsLayoutInfoById): Promise<void>;
1173 > updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise<void>;
1174 > updateIcon(id: number, userInitiated: boolean, icon: TerminalIcon, color?: string): Promise<void>;
1175 > setNextCommandId(id: number, commandLine: string, commandId: string): Promise<void>;
1176 > getTerminalLayoutInfo(): Promise<ITerminalsLayoutInfo | undefined>;
1177 > getPerformanceMarks(): Promise<performance.PerformanceMark[]>;
1178 > reduceConnectionGraceTime(): Promise<void>;
1179 > requestDetachInstance(workspaceId: string, instanceId: number): Promise<IProcessDetails | undefined>;
1180 > acceptDetachInstanceReply(requestId: number, persistentProcessId?: number): Promise<void>;
1181 > persistTerminalState(): Promise<void>;
1182 >
1183 > createProcess(
1184 > shellLaunchConfig: IShellLaunchConfig,
1185 > cwd: string,
1186 > cols: number,
1187 > rows: number,
1188 > unicodeVersion: '6' | '11',
1189 > env: IProcessEnvironment,
1190 > options: ITerminalProcessOptions,
1191 > shouldPersist: boolean
1192 > ): Promise<ITerminalChildProcess>;
1193 >
1194 > restartPtyHost(): void;
1195 > }
1196 >
1197 > export interface ITerminalBackendPtyServiceContributions {
1198 > installAutoReply(match: string, reply: string): Promise<void>;
1199 > uninstallAllAutoReplies(): Promise<void>;
1200 > }
1201 >
1202 > export const TerminalExtensions = {
1203 > Backend: 'workbench.contributions.terminal.processBackend'
1204 > };
1205 >
1206 > export interface ITerminalBackendRegistry {
1207 > /**
1208 > * Gets all backends in the registry.
1209 > */
1210 > backends: ReadonlyMap<string, ITerminalBackend>;
1211 >
1212 > /**
1213 > * Registers a terminal backend for a remote authority.
1214 > */
1215 > registerTerminalBackend(backend: ITerminalBackend): void;
1216 >
1217 > /**
1218 > * Returns the registered terminal backend for a remote authority.
1219 > */
1220 > getTerminalBackend(remoteAuthority?: string): ITerminalBackend | undefined;
1221 > }
1222 >
1223 > class TerminalBackendRegistry implements ITerminalBackendRegistry {
1224 > private readonly _backends = new Map<string, ITerminalBackend>();
1225 >
1226 > get backends(): ReadonlyMap<string, ITerminalBackend> { return this._backends; }
1227 >
1228 > registerTerminalBackend(backend: ITerminalBackend): void {
1229 const key = this._sanitizeRemoteAuthority(backend.remoteAuthority);
1230 if (this._backends.has(key)) {
1233 this._backends.set(key, backend);
1234 }
1235 > terminal.ts
1236 > getTerminalBackend(remoteAuthority: string | undefined): ITerminalBackend | undefined {
1237 return this._backends.get(this._sanitizeRemoteAuthority(remoteAuthority));
1238 }
1239 > terminal.ts
1240 > private _sanitizeRemoteAuthority(remoteAuthority: string | undefined) {
1241 // Normalize the key to lowercase as the authority is case-insensitive
1242 return remoteAuthority?.toLowerCase() ?? '';
1243 }
1244 > } terminal.ts
1245 > Registry.add(TerminalExtensions.Backend, new TerminalBackendRegistry());
1246 >
1247 > export const ILocalPtyService = createDecorator<ILocalPtyService>('localPtyService');
1248 >
1249 > /**
1250 > * A service responsible for communicating with the pty host process on Electron.
1251 > *
1252 > * **This service should only be used within the terminal component.**
1253 > */
1254 > export interface ILocalPtyService extends IPtyHostService { }
1255 >
1256 > export const ITerminalLogService = createDecorator<ITerminalLogService>('terminalLogService');
1257 > export interface ITerminalLogService extends ILogService {
1258 > /**
1259 > * Similar to _serviceBrand but used to differentiate this service at compile time from
1260 > * ILogService; ITerminalLogService is an ILogService, but ILogService is not an
1261 > * ITerminalLogService.
1262 > */
1263 > readonly _logBrand: undefined;
1264 > }
src/vs/platform/agentHost/node/agentService.ts 1183 covered LOC · 148 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentService.ts
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 > import { open, unlink, type FileHandle } from 'fs/promises';
7 > import { decodeBase64, VSBuffer } from '../../../base/common/buffer.js';
8 > import { DeferredPromise, disposableTimeout, ResourceQueue } from '../../../base/common/async.js';
9 > import { toErrorMessage } from '../../../base/common/errorMessage.js';
10 > import { Emitter } from '../../../base/common/event.js';
11 > import { Disposable, DisposableMap, DisposableResourceMap, DisposableStore, IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js';
12 > import { ResourceMap } from '../../../base/common/map.js';
13 > import { getExtensionForMimeType, getMediaMime } from '../../../base/common/mime.js';
14 > import { Schemas } from '../../../base/common/network.js';
15 > import { IObservable, observableValue } from '../../../base/common/observable.js';
16 > import { dirname as resourcesDirname, extname as resourcesExtname, extUriBiasedIgnorePathCase, isEqual, isEqualOrParent, joinPath } from '../../../base/common/resources.js';
17 > import { URI } from '../../../base/common/uri.js';
18 > import { generateUuid } from '../../../base/common/uuid.js';
19 > import { hasKey } from '../../../base/common/types.js';
20 > import { localize } from '../../../nls.js';
21 > import { FileChangeType, FileOperationResult, IFileChange, IFileService, toFileOperationResult, type FileChangesEvent } from '../../files/common/files.js';
22 > import { InstantiationService } from '../../instantiation/common/instantiationService.js';
23 > import { ServiceCollection } from '../../instantiation/common/serviceCollection.js';
24 > import { ILogService } from '../../log/common/log.js';
25 > import { AgentProvider, AgentSession, AgentSignal, AgentHostSessionReleaseGraceMsEnvVar, IAgent, IAgentChatDataChange, IAgentCreateChatOptions, IAgentCreateChatResult, IAgentCreateChatSideChatSelection, IAgentCreateChatSideChatSource, IAgentCreateSessionConfig, IAgentCreateSessionResult, IAgentHostAuthTokenRequest, IAgentHostManagedSettingsDiagnostics, IAgentHostNetworkDiagnosticsInfo, IAgentHostNetworkEndpoint, IAgentHostNetworkFetchResult, IAgentMaterializeSessionEvent, IAgentModelInfo, IAgentResolveSessionConfigParams, IAgentService, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, IAgentSpawnChatEvent, AuthenticateParams, AuthenticateResult, IMcpNotification, IRestoredSubagentSession, SubagentChatSignal } from '../common/agentService.js';
26 > import { ISessionDataService, SESSION_ATTACHMENTS_DIRNAME } from '../common/sessionDataService.js';
27 > import { SessionConfigKey } from '../common/sessionConfigKeys.js';
28 > import type { IAgentCustomizationSettingsRegistration } from '../common/agentCustomizationSettings.js';
29 > import { parseChangesetUri } from '../common/changesetUri.js';
30 > import { ActionType, ActionEnvelope, AuthRequiredReason, INotification, type ChatAction, type IRootConfigChangedAction, type SessionAction, type TerminalAction, type ClientAnnotationsAction, type ClientChangesetAction } from '../common/state/sessionActions.js';
31 > import type { CompletionsParams, CompletionsResult, CreateTerminalParams, ResolveSessionConfigResult, SessionConfigCompletionsResult, SessionConfigPropertySchema } from '../common/state/protocol/commands.js';
32 > import type { InvokeChangesetOperationParams, InvokeChangesetOperationResult } from '../common/state/protocol/channels-changeset/commands.js';
33 > import { AhpErrorCodes, AHP_SESSION_NOT_FOUND, ContentEncoding, JSON_RPC_INTERNAL_ERROR, ProtocolError, ResourceChangeType, ResourceType, ResourceWriteMode, type CreateResourceWatchParams, type CreateResourceWatchResult, type DirectoryEntry, type ResourceCopyParams, type ResourceCopyResult, type ResourceDeleteParams, type ResourceDeleteResult, type ResourceListResult, type ResourceMkdirParams, type ResourceMkdirResult, type ResourceMoveParams, type ResourceMoveResult, type ResourceReadResult, type ResourceResolveParams, type ResourceResolveResult, type ResourceWatchState, type ResourceWriteParams, type ResourceWriteResult, type IStateSnapshot } from '../common/state/sessionProtocol.js';
34 > import { ChangesSummary, ChatInteractivity, ChatOriginKind, MessageAttachmentKind, type ChatOrigin, type Message, type MessageAttachment, type MessageResourceAttachment } from '../common/state/protocol/state.js';
35 > import type { ChatPendingMessageSetAction, ChatTurnStartedAction } from '../common/state/protocol/actions.js';
36 > import { ISessionGitHubState, ISessionGitState, MessageKind, ResponsePartKind, SESSION_META_GITHUB_KEY, SESSION_META_GIT_KEY, readSessionSpawnDepth, withSessionSpawnDepth, SessionStatus, ToolCallStatus, ToolResultContentType, AH_META_WORKSPACELESS_DB_KEY, AH_META_IS_ARCHIVED_DB_KEY, AH_META_IS_DONE_DB_KEY, buildChatUri, buildDefaultChatUri, buildResourceWatchChannelUri, buildSubagentChatUri, buildSubagentSessionUriPrefix, hostBuildInfoFromProduct, isAhpChatChannel, isDefaultChatUri, isSubagentChatUri, isSubagentSession, parseDefaultChatUri, parseRequiredSessionUriFromChatUri, parseResourceWatchChannelUri, parseSubagentSessionUri, readSessionGitState, readSessionWorkspaceless, withSessionGitHubState, withSessionGitState, withSessionWorkspaceless, type SessionConfigState, type SessionSummary, type ToolResultSubagentContent, type Turn } from '../common/state/sessionState.js';
37 > import { readToolCallMeta } from '../common/meta/agentToolCallMeta.js';
38 > import { IProductService } from '../../product/common/productService.js';
39 > import { buildBoundedSideChatSourceContext, getSideChatPartialResponse } from './agentPeerChats.js';
40 > import { AgentConfigurationService, IAgentConfigurationService } from './agentConfigurationService.js';
41 > import { AgentHostTerminalManager, IAgentHostTerminalManager } from './agentHostTerminalManager.js';
42 > import { ISessionDbUriFields, parseSessionDbUri } from './shared/fileEditTracker.js';
43 > import { IGitBlobUriFields, parseGitBlobUri } from './gitDiffContent.js';
44 > import { AgentHostStateManager, IAgentHostStateManager } from './agentHostStateManager.js';
45 > import { IAgentHostGitService } from '../common/agentHostGitService.js';
46 > import { AgentSideEffects } from './agentSideEffects.js';
47 > import { AgentHostLocalTurns } from './agentHostLocalTurns.js';
48 > import { AgentServerToolHost } from './shared/agentServerToolHost.js';
49 > import { buildServerToolGroups } from './shared/serverToolGroups.js';
50 > import { type IChatContextSnapshot, type ISessionServerToolAccessor } from './shared/sessionServerTools.js';
51 >
52 > import { WorktreeIsolation, WORKTREE_META_REPOSITORY_ROOT, worktreeProjectFromRepositoryRoot } from './shared/worktreeIsolation.js';
53 > import { AgentHostChangesetService } from './agentHostChangesetService.js';
54 > import { AgentHostFileMonitorService, IAgentHostFileMonitorService } from './agentHostFileMonitorService.js';
55 > import { IAgentHostCheckpointService, NULL_CHECKPOINT_SERVICE } from '../common/agentHostCheckpointService.js';
56 > import { IAgentHostReviewService } from '../common/agentHostReviewService.js';
57 > import { AgentHostChangesetCoordinator } from './agentHostChangesetCoordinator.js';
58 > import { AgentHostCompletions, IAgentHostCompletions } from './agentHostCompletions.js';
59 > import { AgentHostFileCompletionProvider } from './agentHostFileCompletionProvider.js';
60 > import { AgentHostRenameCompletionProvider } from './agentHostRenameCommand.js';
61 > import { AgentHostSkillCompletionProvider } from './agentHostSkillCompletionProvider.js';
62 > import { AgentHostWorkspaceFiles } from './agentHostWorkspaceFiles.js';
63 > import { CopilotApiService, ICopilotApiService } from './shared/copilotApiService.js';
64 > import { INetworkDiagnosticsService } from './networkDiagnosticsService.js';
65 > import { parseMcpChannelUri } from './shared/mcpCustomizationController.js';
66 > import { toAgentClientUri } from '../common/agentClientUri.js';
67 > import { AgentHostChangesetOperationService } from './agentHostChangesetOperationService.js';
68 > import { AgentHostGitStateService } from './agentHostGitStateService.js';
69 > import { AgentHostGitHubEndpointService, IAgentHostGitHubEndpointService } from './agentHostGitHubEndpointService.js';
70 > import { ITelemetryService } from '../../telemetry/common/telemetry.js';
71 > import { NullTelemetryService } from '../../telemetry/common/telemetryUtils.js';
72 > import { AgentHostAuthenticationService } from './agentHostAuthenticationService.js';
73 > import { updateAgentHostTelemetryLevelFromConfig } from './agentHostTelemetryService.js';
74 > import { AgentHostOctoKitService, IAgentHostOctoKitService } from './shared/agentHostOctoKitService.js';
75 > import { IAgentHostChangesetService, CHANGESET_DB_METADATA_KEYS, META_CHANGES_SUMMARY } from '../common/agentHostChangesetService.js';
76 > import { IAgentHostChangesetSubscriptionService } from '../common/agentHostChangesetSubscriptionService.js';
77 > import { AgentHostChangesetSubscriptionService } from './agentHostChangesetSubscriptionService.js';
78 > import { GIT_DB_METADATA_KEYS, IAgentHostGitStateService, META_GIT_STATE, META_GITHUB_STATE } from '../common/agentHostGitStateService.js';
79 > import { IAgentHostChangesetOperationService } from '../common/agentHostChangesetOperationService.js';
80 > import { AgentHostCommitOperationContribution } from './agentHostCommitOperationProvider.js';
81 > import { AgentHostDiscardChangesOperationContribution } from './agentHostDiscardChangesOperationProvider.js';
82 > import { AgentHostPullRequestOperationContribution } from './agentHostPullRequestOperationProvider.js';
83 > import { AgentHostSyncOperationContribution } from './agentHostSyncOperationProvider.js';
84 > import { AgentHostReviewService } from './agentHostReviewService.js';
85 >
86 > /**
87 > * Grace period before an empty, unsubscribed session is garbage-collected
88 > * via {@link AgentService._runSessionGc}. Gives a disconnected client time
89 > * to reconnect (or a workspace switch to settle) before we tear down the
90 > * provider-side session, worktree, and on-disk state.
91 > */
92 > const SESSION_GC_GRACE_MS = 30_000;
93 >
94 > const HOST_OWNED_SESSION_CONFIG_KEYS = [
95 > SessionConfigKey.Isolation,
96 > SessionConfigKey.Branch,
97 > SessionConfigKey.WorktreeBranchPrefix,
98 > SessionConfigKey.WorktreeIncludeFiles,
99 > ] as const;
100 >
101 function omitHostOwnedSessionConfig<T>(config: Record<string, T>): Record<string, T> {
102 const result = { ...config };
106 return result;
107 }
109 > /**
110 > * Grace period before an idle resource watch is torn down after its last
111 > * subscriber unsubscribes (mirrors {@link SESSION_GC_GRACE_MS}). Within
112 > * this window, a re-subscribe (or reconnect) reuses the still-running
113 > * {@link IFileService} watcher so transient drop-outs don't miss change
114 > * events. Resource watch action envelopes flow through the normal
115 > * envelope replay buffer for the same reason.
116 > */
117 > const RESOURCE_WATCH_GRACE_MS = 30_000;
118 >
119 > /** Bound on how long {@link AgentService.subscribe} waits for a pending subagent chat to register before giving up. */
120 > const SUBAGENT_CHAT_PENDING_TIMEOUT_MS = 15_000;
121 >
122 > /**
123 > * Grace period before an idle session (one with turns, no remaining
124 > * subscribers) is released from memory via {@link AgentService._maybeEvictIdleSession}.
125 > * Deferring the release aligns it with the client disconnect-grace window: a
126 > * client that disconnects and quickly reconnects (or a rapid unsubscribe/
127 > * re-subscribe) reuses the live provider SDK session instead of forcing an
128 > * immediate {@link IAgent.releaseSession} (SDK `disconnect`) followed by a
129 > * resume-from-disk. Releasing synchronously on every last-unsubscribe churns
130 > * the shared provider runtime and races concurrent session operations.
131 > *
132 > * Overridable via {@link AgentHostSessionReleaseGraceMsEnvVar} (test hook).
133 > */
134 > const SESSION_RELEASE_GRACE_MS = (() => {
135 > const raw = process.env[AgentHostSessionReleaseGraceMsEnvVar];
136 > const parsed = raw !== undefined ? parseInt(raw, 10) : NaN;
137 > return Number.isFinite(parsed) && parsed >= 0 ? parsed : 30_000;
138 > })();
139 >
140 > /**
141 > * Session-database metadata key under which the orchestrator persists its own
142 > * catalog of additional (non-default) peer chats for a session. The value is a
143 > * JSON array of {@link IPersistedPeerChat}. This is the orchestrator's single
144 > * source of truth for peer-chat enumeration on restore. When the key is absent
145 > * the session predates orchestrator-owned persistence and a one-time migration
146 > * drains the agent's legacy `*.chats` (see
147 > * {@link AgentService._migrateLegacyPeerChats}).
148 > */
149 > const PEER_CHATS_METADATA_KEY = 'peerChats';
150 >
151 > /**
152 > * Session-database metadata key written on a peer chat's *backing* SDK session
153 > * (see {@link IAgentCreateChatResult.backingSession}). Its presence marks that
154 > * session as an internal peer-chat backing that must never surface as a
155 > * top-level session; the value is the owning peer chat's channel URI string.
156 > * Persisted, so it survives a host restart without re-stamping.
157 > */
158 > const PEER_CHAT_BACKING_METADATA_KEY = 'peerChatBacking';
159 >
160 > /**
161 > * A single entry in the orchestrator's persisted peer-chat catalog. `uri` is
162 > * the peer chat's channel URI; `providerData` is the opaque, agent-owned blob
163 > * (see {@link IAgentCreateChatResult.providerData}) handed back to the agent on
164 > * restore — the orchestrator never parses it. `providerData` may be omitted,
165 > * in which case the agent recovers its backing from its own persistence on
166 > * {@link IAgent.materializeChat}. `origin` records the chat's provenance
167 > * (currently only {@link ChatOriginKind.SideChat}, carrying the source chat and
168 > * stable source turn id) so it survives a restart; omitted for plain peer chats.
169 > */
170 > interface IPersistedPeerChat {
171 > readonly uri: string;
172 > readonly providerData?: string;
173 > readonly origin?: ChatOrigin;
174 > }
175 >
176 > /**
177 > * The agent service implementation that runs inside the agent-host utility
178 > * process. Dispatches to registered {@link IAgent} instances based
179 > * on the provider identifier in the session configuration.
180 > */
181 > export class AgentService extends Disposable implements IAgentService {
182 > declare readonly _serviceBrand: undefined;
183 >
184 > private readonly _resourceWriteQueue = this._register(new ResourceQueue());
185 >
186 > /** Protocol: fires when state is mutated by an action. */
187 > private readonly _onDidAction = this._register(new Emitter<ActionEnvelope>());
188 > readonly onDidAction = this._onDidAction.event;
189 >
190 > /** Protocol: fires for ephemeral notifications (sessionAdded/Removed). */
191 > private readonly _onDidNotification = this._register(new Emitter<INotification>());
192 > readonly onDidNotification = this._onDidNotification.event;
193 >
194 > /** Protocol: fires for MCP server-originated notifications routed over `mcp://` channels. */
195 > private readonly _onMcpNotification = this._register(new Emitter<IMcpNotification>());
196 > readonly onMcpNotification = this._onMcpNotification.event;
197 >
198 > /** Authoritative state manager for the sessions process protocol. */
199 > private readonly _stateManager: AgentHostStateManager;
200 >
201 > /** Exposes the state manager for co-hosting a WebSocket protocol server. */
202 > get stateManager(): AgentHostStateManager { return this._stateManager; }
203 >
204 > /** Exposes the configuration service so agent providers can share root config plumbing. */
205 > get configurationService(): IAgentConfigurationService { return this._configurationService; }
206 >
207 > /** Exposes the GitHub endpoint service so agent providers share GitHub (Enterprise) resource resolution. */
208 > get gitHubEndpointService(): IAgentHostGitHubEndpointService { return this._gitHubEndpointService; }
209 >
210 > /** Registered providers keyed by their {@link AgentProvider} id. */
211 > private readonly _providers = new Map<AgentProvider, IAgent>();
212 > /** Maps each active session URI (toString) to its owning provider. */
213 > private readonly _sessionToProvider = new Map<string, AgentProvider>();
214 > /**
215 > * Sessions that have opted in to bring-up progress, keyed by provider id.
216 > * A session is added here when its `createSession` carries a
217 > * {@link IAgentCreateSessionConfig.progressToken} and removed once it
218 > * materializes (the SDK is now resolved) or is disposed. The SDK download is
219 > * host-level and shared across every session of a provider, so this only
220 > * records *interest*: as long as one or more sessions of a provider is
221 > * registered, {@link emitDownloadProgress} surfaces that provider's download as a single
222 > * progress stream keyed by the download's own identity (the package id),
223 > * rather than one stream per session.
224 > */
225 > private readonly _downloadProgressInterest = new Map<AgentProvider, Set<string>>();
226 > /** Subscriptions to provider progress events; cleared when providers change. */
227 > private readonly _providerSubscriptions = this._register(new DisposableStore());
228 > /**
229 > * Per-session tail of in-flight persisted peer-chat catalog writes, keyed by
230 > * session URI string. Read-modify-write updates to the {@link
231 > * PEER_CHATS_METADATA_KEY} blob are chained per session so a `createChat`,
232 > * `disposeChat`, and `onDidChangeChatData` racing for the same
233 > * session can't clobber each other's edits.
234 > */
235 > private readonly _peerChatCatalogWrites = new Map<string, Promise<void>>();
236 > private readonly _authService: AgentHostAuthenticationService;
237 > /** Default provider used when no explicit provider is specified. */
238 > private _defaultProvider: AgentProvider | undefined;
239 > /** Observable registered agents, drives `root/agentsChanged` via {@link AgentSideEffects}. */
240 > private readonly _agents = observableValue<readonly IAgent[]>('agents', []);
241 > /** Shared side-effect handler for action dispatch and session lifecycle. */
242 > private readonly _sideEffects: AgentSideEffects;
243 > /** Owns static / per-turn changeset compute, publish, persist, restore. */
244 > private readonly _changesets: IAgentHostChangesetService;
245 > /** Shared active changeset subscription registry. */
246 > private readonly _changesetSubscriptions: IAgentHostChangesetSubscriptionService;
247 > /** Owns changeset operation contributions and handler activation. */
248 > private readonly _changesetOperationService: IAgentHostChangesetOperationService;
249 > private readonly _reviewService: IAgentHostReviewService;
250 > /** Owns AgentService-side orchestration of the changeset feature. */
251 > private readonly _changesetCoordinator: AgentHostChangesetCoordinator;
252 > /** Owns session git-state probing and git-backed catalogue decoration. */
253 > private readonly _gitStateService: IAgentHostGitStateService;
254 > /** Manages PTY-backed terminals for the agent host protocol. */
255 > private readonly _terminalManager: AgentHostTerminalManager;
256 > /** Persists host-injected `/rename` / `!command` turns for restore & fork/truncate. */
257 > private readonly _localTurns: AgentHostLocalTurns;
258 > /** Server-side host for the agent host's server tools. */
259 > private readonly _serverToolHost: AgentServerToolHost;
260 > private readonly _configurationService: AgentConfigurationService;
261 > /**
262 > * Host-owned worktree isolation controller. Set post-construction via
263 > * {@link setWorktreeIsolation} because it depends on the branch-name
264 > * generator, which is wired after this service is built. All worktree
265 > * behavior — schema contribution, first-send resolution, project /
266 > * announcement, archive, and cleanup — is driven from the host so individual
267 > * agents stay unaware of the folder-vs-worktree distinction.
268 > */
269 > private _worktree: WorktreeIsolation | undefined;
270 > /** Single source of truth for GitHub (Enterprise) endpoints and protected resources. */
271 > private readonly _gitHubEndpointService: IAgentHostGitHubEndpointService;
272 > /** Pluggable completion item providers (e.g. workspace file completions, agent-specific @-mentions). */
273 > private readonly _completions: IAgentHostCompletions;
274 > private _skillCompletionProviderRegistered = false;
275 > /** Backs {@link getNetworkDiagnosticsInfo} / {@link diagnosticsFetch}; wired via {@link setNetworkDiagnosticsService}. */
276 > private _networkDiagnostics: INetworkDiagnosticsService | undefined;
277 >
278 > /**
279 > * Authoritative server-side per-resource subscription refcount, keyed by
280 > * resource URI string and valued by the set of subscribed protocol
281 > * client IDs. Populated by {@link subscribe} (or {@link addSubscriber}
282 > * for handshake fast-paths) and drained by {@link unsubscribe}. When a
283 > * resource's set becomes empty, the resource is dropped from the map and
284 > * {@link _maybeEvictIdleSession} is invoked to release any cached state
285 > * for it.
286 > */
287 > private readonly _resourceSubscribers = new ResourceMap<Set<string>>();
288 > private readonly _restoreSessionInFlight = new Map<string, Promise<void>>();
289 > private readonly _restoreSubagentInFlight = new Map<string, Promise<void>>();
290 >
291 > /** Subagent chats armed for a bounded wait (once execution is confirmed); resolved by {@link _onChatSpawned}, awaited by {@link subscribe}. */
292 > private readonly _pendingSubagentChats = new Map<string /* subagentChatUri */, DeferredPromise<void>>();
293 > private readonly _pendingSubagentChatTimeouts = this._register(new DisposableMap<string /* subagentChatUri */, IDisposable>());
294 > /** Subagent chats announced via `_meta.subagentChatUri` but still awaiting confirmation, keyed by `${channel}:${toolCallId}`. */
295 > private readonly _pendingSubagentToolCalls = new Map<string, string /* subagentChatUri */>();
296 >
297 > /**
298 > * Pending {@link _runSessionGc} timers, keyed by session URI. A timer is
299 > * armed when a session loses its last subscriber while still empty (no
300 > * turns, no active turn) — see {@link _maybeScheduleSessionGc}. Cleared
301 > * whenever any client subscribes again or the timer fires.
302 > */
303 > private readonly _pendingSessionGc = this._register(new DisposableResourceMap<IDisposable>());
304 >
305 > /**
306 > * Pending {@link _maybeEvictIdleSession} timers, keyed by session URI. A
307 > * timer is armed when an idle session (with turns) loses its last subscriber
308 > * — see {@link unsubscribe}. Cleared when any client subscribes again
309 > * ({@link addSubscriber}) or the timer fires. Deferring the release avoids
310 > * churning the provider SDK session on rapid disconnect/reconnect cycles.
311 > */
312 > private readonly _pendingSessionRelease = this._register(new DisposableResourceMap<IDisposable>());
313 >
314 > /**
315 > * Active resource watches keyed by the channel URI string
316 > * (`ahp-resource-watch:/<encoded>`).
317 > *
318 > * Each entry owns the {@link IFileService} watcher together with the
319 > * decoded descriptor, the subscriber refcount, and the optional
320 > * grace-window dispose timer. The watch URI itself is fully
321 > * self-describing — {@link createResourceWatch} just encodes the
322 > * caller's params into the URI and returns it. State only exists
323 > * here once at least one client has subscribed.
324 > *
325 > * Lifecycle:
326 > * - First subscriber to a channel: {@link onResourceWatchSubscribed}
327 > * parses the URI, creates the {@link IFileService} watcher, and
328 > * installs the entry with `subscribers = 1`.
329 > * - Subsequent subscribers bump the refcount and cancel any pending
330 > * grace-window dispose timer.
331 > * - {@link onResourceWatchUnsubscribed} drops the refcount; when it
332 > * reaches zero we arm a {@link RESOURCE_WATCH_GRACE_MS} dispose
333 > * timer rather than tearing down immediately, giving disconnected
334 > * clients time to reconnect.
335 > */
336 > private readonly _resourceWatches = this._register(new DisposableMap<string, IActiveResourceWatch>());
337 >
338 > /** Exposes the terminal manager for use by agent providers. */
339 > get terminalManager(): IAgentHostTerminalManager { return this._terminalManager; }
340 >
341 > /** Exposes the completions service for use by agent providers (e.g. to register agent-scoped completion item providers). */
342 > get completionsService(): IAgentHostCompletions { return this._completions; }
343 >
344 > /**
345 > * Trigger characters announced to clients via `InitializeResult.completionTriggerCharacters`.
346 > * Aggregated from all registered {@link IAgentHostCompletionItemProvider}s.
347 > */
348 > get completionTriggerCharacters(): readonly string[] { return this._completions.triggerCharacters; }
349 >
350 > constructor(
351 > private readonly _logService: ILogService, agentService.ts
352 > private readonly _fileService: IFileService,
353 > private readonly _sessionDataService: ISessionDataService,
354 > private readonly _productService: IProductService,
355 > private readonly _gitService: IAgentHostGitService,
356 > private readonly _checkpointService: IAgentHostCheckpointService = NULL_CHECKPOINT_SERVICE,
357 > private readonly _rootConfigResource?: URI,
358 > private readonly _telemetryService: ITelemetryService = NullTelemetryService,
359 > _fileMonitorService?: IAgentHostFileMonitorService,
360 > copilotApiService?: ICopilotApiService,
361 > fetchFn?: typeof globalThis.fetch,
362 > providerConfigurations: readonly IAgentCustomizationSettingsRegistration[] = [],
363 > ) {
364 > super();
365 > this._logService.info('AgentService initialized');
366 > this._authService = new AgentHostAuthenticationService(_logService);
367 > this._stateManager = this._register(new AgentHostStateManager(_logService, {
368 > hostBuildInfo: hostBuildInfoFromProduct(this._productService),
369 > changesetStateRetention: {
370 > // The cache calls this lazily after construction. If a future state-manager
371 > // initialization path registers changesets before `_changesets` is assigned,
372 > // keep the entry pinned rather than evicting with incomplete liveness data.
373 > canEvict: changeset => this._changesets ? this._isChangesetEvictable(changeset) : false,
374 > },
375 > }));
376 > this._register(this._stateManager.onDidEmitEnvelope(e => this._onDidAction.fire(e)));
377 > this._register(this._stateManager.onDidEmitEnvelope(e => this._trackPendingSubagentChatFromEnvelope(e)));
378 > this._register(this._stateManager.onDidEmitNotification(e => this._onDidNotification.fire(e)));
379 >
380 > // Build a local instantiation scope so downstream components can
381 > // consume {@link IAgentConfigurationService} (and later {@link ILogService})
382 > // via DI rather than being plumbed plain-class references.
383 > const configurationService = this._register(new AgentConfigurationService(this._stateManager, this._logService, this._rootConfigResource, providerConfigurations));
384 > this._configurationService = configurationService;
385 > const fileMonitorService = _fileMonitorService ?? this._register(new AgentHostFileMonitorService(this._fileService, this._logService));
386 > updateAgentHostTelemetryLevelFromConfig(this._telemetryService, this._stateManager.rootState.config?.values);
387 > const services = new ServiceCollection(
388 > [ILogService, this._logService],
389 > [IAgentService, this],
390 > [IProductService, this._productService],
391 > [IAgentConfigurationService, configurationService],
392 > [IAgentHostStateManager, this._stateManager],
393 > [IAgentHostFileMonitorService, fileMonitorService],
394 > [IAgentHostGitService, this._gitService],
395 > [ITelemetryService, this._telemetryService],
396 > // The outer agent-host process DI registers `ISessionDataService`,
397 > // but this nested strict `InstantiationService` does not inherit it.
398 > // Add it explicitly so `@ISessionDataService` injection into the
399 > // changeset service (and any future sibling) resolves correctly.
400 > [ISessionDataService, this._sessionDataService],
401 > );
402 > const instantiationService = this._register(new InstantiationService(services, /*strict*/ true));
403 > this._gitHubEndpointService = this._register(instantiationService.createInstance(AgentHostGitHubEndpointService));
404 > services.set(IAgentHostGitHubEndpointService, this._gitHubEndpointService);
405 > // A GitHub Enterprise URI change repoints every agent's GitHub resource
406 > // identity to a different authorization server, so the client must obtain a
407 > // token for the new resource. One root-channel `auth/required` covers all
408 > // agents (the URI is host-level config).
409 > this._register(this._gitHubEndpointService.onDidChange(() => {
410 this._stateManager.emitAuthRequired({
411 resource: this._gitHubEndpointService.getCopilotResource().resource,
412 reason: AuthRequiredReason.Required,
413 });
414 > })); agentService.ts
415 > const agentHostOctoKitService = instantiationService.createInstance(AgentHostOctoKitService, fetchFn);
416 > services.set(IAgentHostOctoKitService, agentHostOctoKitService);
417 > const effectiveCopilotApiService = copilotApiService ?? instantiationService.createInstance(CopilotApiService, fetchFn);
418 > services.set(ICopilotApiService, effectiveCopilotApiService);
419 >
420 > this._gitStateService = this._register(instantiationService.createInstance(AgentHostGitStateService));
421 > services.set(IAgentHostGitStateService, this._gitStateService);
422 >
423 > // The checkpoint service is constructed in the outer agent-host
424 > // DI scope and passed via {@link _checkpointService}; register it
425 > // in the inner service collection so the changeset service /
426 > // side effects can resolve it via DI.
427 > services.set(IAgentHostCheckpointService, this._checkpointService);
428 >
429 > // The subscription service manages the lifecycle of changeset subscriptions. The service
430 > // is also consulted by other services when refreshing changesets and changeset operations.
431 > this._changesetSubscriptions = instantiationService.createInstance(AgentHostChangesetSubscriptionService);
432 > services.set(IAgentHostChangesetSubscriptionService, this._changesetSubscriptions);
433 >
434 > // The operation contribution service manages the lifecycle of changeset operations.
435 > this._changesetOperationService = this._register(instantiationService.createInstance(AgentHostChangesetOperationService));
436 > services.set(IAgentHostChangesetOperationService, this._changesetOperationService);
437 >
438 > // The changes review service is responsible for managing review/unreview state for changeset changes.
439 > this._reviewService = this._register(instantiationService.createInstance(AgentHostReviewService));
440 > services.set(IAgentHostReviewService, this._reviewService);
441 >
442 > // The changeset service is responsible for computing, publishing, and persisting changesets.
443 > this._changesets = this._register(instantiationService.createInstance(AgentHostChangesetService));
444 > services.set(IAgentHostChangesetService, this._changesets);
445 >
446 > // The coordinator owns all AgentService-side orchestration of the changeset feature: lifecycle
447 > // hooks, listSessions overlay, subscription URI routing, and the deferred-refresh state machine.
448 > this._changesetCoordinator = this._register(instantiationService.createInstance(AgentHostChangesetCoordinator));
449 > this._register(this._stateManager.onDidChangeSessionActiveTurn(e => this._changesetCoordinator.onSessionTurnActiveChanged(e.session, e.active)));
450 >
451 > // Register the changeset operation contributions.
452 > this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostCommitOperationContribution)));
453 > this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostPullRequestOperationContribution)));
454 > this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostSyncOperationContribution)));
455 > this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostDiscardChangesOperationContribution)));
456 >
457 > this._completions = this._register(instantiationService.createInstance(AgentHostCompletions));
458 > // Built-in generic provider: completes files in the session's workspace folder.
459 > const workspaceFiles = this._register(instantiationService.createInstance(AgentHostWorkspaceFiles));
460 > this._register(this._completions.registerProvider(
461 > new AgentHostFileCompletionProvider(this._stateManager, workspaceFiles),
462 > ));
463 > // Built-in generic provider: offers the `/rename` slash command for any
464 > // session that already has history. Execution is handled server-side in
465 > // AgentSideEffects (redirected to a SessionTitleChanged action).
466 > this._register(this._completions.registerProvider(
467 > new AgentHostRenameCompletionProvider(
468 > session => (this._stateManager.getSessionState(session)?.turns.length ?? 0) > 0,
469 > ),
470 > ));
471 >
472 > // Terminal management — the terminal manager listens to the state
473 > // manager's action stream and dispatches PTY output back through it.
474 > // Created before AgentSideEffects and registered in the local scope so
475 > // AgentSideEffects can consume it via DI (for inline `!command`
476 > // execution).
477 > this._terminalManager = this._register(instantiationService.createInstance(AgentHostTerminalManager));
478 > services.set(IAgentHostTerminalManager, this._terminalManager);
479 >
480 > this._localTurns = new AgentHostLocalTurns(this._sessionDataService, this._logService);
481 >
482 > this._sideEffects = this._register(instantiationService.createInstance(AgentSideEffects, this._stateManager, {
483 > getAgent: session => this._findProviderForSession(session),
484 > sessionDataService: this._sessionDataService,
485 > localTurns: this._localTurns,
486 > agents: this._agents,
487 > copilotApiService: effectiveCopilotApiService,
488 > getGitHubCopilotToken: () => {
489 return this.getAuthToken({
490 resource: this._gitHubEndpointService.getCopilotResource().resource,
492 });
493 },
494 > resolveWorkingDirectoryBeforeSend: params => this._resolveWorkingDirectoryBeforeSend(params), agentService.ts
495 > resolveChatAttachmentTurns: resource => this._resolveChatAttachmentTurns(resource),
496 > onTurnComplete: async session => {
497 // Refresh the git state for the session.
498 const workingDirStr = this._stateManager.getSessionState(session)?.workingDirectories?.[0];
502 void this._gitStateService.attachSessionGitHubPullRequest(session.toString());
503 },
504 > })); agentService.ts
505 >
506 > // Server-side tools, executed in-process against each session's own
507 > // state. The set of groups (and their display) is the single source of
508 > // truth in `serverToolGroups.ts`; the session-management group's runtime
509 > // dependency (this service) is injected via the accessor.
510 > this._serverToolHost = new AgentServerToolHost(this._stateManager, buildServerToolGroups(this._createSessionServerToolAccessor()));
511 > }
513 > /**
514 > * The registered providers. Exposed so process-lifetime background jobs
515 > * (notably {@link AgentModelRefreshScheduler}) can observe registrations
516 > * without this service owning an ambient recurring timer of its own.
517 > */
518 > get agents(): IObservable<readonly IAgent[]> {
519 return this._agents;
520 }
522 > // ---- provider registration ----------------------------------------------
523 >
524 > /**
525 > * Injects the host-owned {@link WorktreeIsolation} controller and forwards it
526 > * to the collaborators that consult it. Called once at startup (from
527 > * agentHostMain / agentHostServerMain) after the branch-name generator has
528 > * been wired.
529 > */
530 > setWorktreeIsolation(worktree: WorktreeIsolation): void {
531 this._worktree = worktree;
532 this._configurationService.setWorktreeIsolation(worktree);
533 this._sideEffects.setWorktreeIsolation(worktree);
534 }
536 > private _toProviderConfig<T extends { readonly config?: Record<string, unknown> }>(request: T): T {
537 if (!this._worktree || !request.config) {
538 return request;
540 return { ...request, config: omitHostOwnedSessionConfig(request.config) };
541 }
543 > /**
544 > * Host-owned first-send hook (invoked by {@link AgentSideEffects} before the
545 > * agent locks its subprocess cwd). Resolves the working directory the session
546 > * will actually run in and hands it to the agent at send time:
547 > * - `worktree` isolation: the isolated worktree, created here on the first
548 > * send (see {@link _resolveWorktreeBeforeSend});
549 > * - `folder` isolation: the picked folder;
550 > * - workspace-less: `undefined` (the agent runs in its own scratch dir).
551 > */
552 > private async _resolveWorkingDirectoryBeforeSend(params: { session: string; chat: string; turnId: string; prompt: string }): Promise<URI | undefined> {
553 const sessionId = AgentSession.id(params.session);
554 const pickedFolder = this._configurationService.getEffectiveWorkingDirectory(params.session);
568 return await this._resolveWorktreeBeforeSend({ ...params, sessionId, pickedFolderUri }) ?? pickedFolderUri;
569 }
571 > private async _resolveChatAttachmentTurns(resource: string): Promise<readonly Turn[]> {
572 const readTurns = () => {
573 const state = this._stateManager.getChatState(resource) ?? this._stateManager.getDefaultChatState(resource);
590 return readTurns() ?? [];
591 }
593 > /**
594 > * Creates the session's isolated worktree on the first send (deferred so the
595 > * user's prompt can name the branch), surfaces the "Created isolated worktree"
596 > * announcement as the first markdown response part of the turn, and returns
597 > * the created worktree URI. Idempotent; safe to call once the worktree exists.
598 > * Returns `undefined` when worktree creation failed. Only invoked for sessions
599 > * whose worktree is still pending (see {@link _resolveWorkingDirectoryBeforeSend}).
600 > */
601 > private async _resolveWorktreeBeforeSend(params: { session: string; chat: string; turnId: string; prompt: string; sessionId: string; pickedFolderUri: URI | undefined }): Promise<URI | undefined> {
602 const { sessionId, pickedFolderUri } = params;
603 const worktree = this._worktree;
630 return worktree.getResolvedWorktree(sessionId);
631 }
633 > registerProvider(provider: IAgent): void {
634 > if (this._providers.has(provider.id)) { agentService.ts
635 throw new Error(`Agent provider already registered: ${provider.id}`);
636 }
637 > this._logService.info(`Registering agent provider: ${provider.id}`); agentService.ts
638 > this._providers.set(provider.id, provider);
639 > provider.setServerToolHost?.(this._serverToolHost);
640 > // Deterministic subagent membership ordering: apply a spawned subagent's
641 > // catalog membership (via the spawn-channel handlers) BEFORE
642 > // AgentSideEffects — registered next — handles the same signal and starts
643 > // a turn on the subagent chat, which requires that chat to already exist.
644 > // Registering this listener ahead of the side-effects listener makes the
645 > // ordering independent of when the agent registers its own subagent->spawn
646 > // bridge; addChat/removeChat are idempotent, so the overlap is safe.
647 > this._providerSubscriptions.add(provider.onDidSessionProgress(signal => this._sequenceSpawnedChat(signal)));
648 > this._providerSubscriptions.add(this._sideEffects.registerProgressListener(provider));
649 > if (provider.onDidMaterializeSession) {
650 this._providerSubscriptions.add(provider.onDidMaterializeSession(e => this._onDidMaterializeSession(e)));
651 }
652 > if (provider.onMcpNotification) { agentService.ts
653 this._providerSubscriptions.add(provider.onMcpNotification(e => this._onMcpNotification.fire(e)));
654 }
655 > if (provider.onDidChangeChatData) { agentService.ts
656 this._providerSubscriptions.add(provider.onDidChangeChatData(e => this._onChatDataChanged(e)));
657 }
658 > if (provider.onDidSpawnChat) { agentService.ts
659 this._providerSubscriptions.add(provider.onDidSpawnChat(e => this._onChatSpawned(e)));
660 }
661 > this._registerSkillCompletionProvider(); agentService.ts
662 > if (!this._defaultProvider) {
663 > this._defaultProvider = provider.id;
664 > }
665 >
666 > // Update root state with current agents list
667 > this._updateAgents();
668 > }
670 > private _registerSkillCompletionProvider(): void {
671 > if (this._skillCompletionProviderRegistered) { agentService.ts
672 > return; agentService.ts
673 > }
674 > this._skillCompletionProviderRegistered = true; agentService.ts
675 > const provider = this._register(new AgentHostSkillCompletionProvider(
676 > session => this._findProviderForSession(session),
677 > ));
678 > this._register(this._completions.registerProvider(provider));
679 > }
681 > // ---- auth ---------------------------------------------------------------
682 >
683 > async authenticate(params: AuthenticateParams): Promise<AuthenticateResult> {
684 > return this._authService.authenticate(params, this._providers.values()); agentService.ts
685 > }
687 > getAuthToken(request: IAgentHostAuthTokenRequest): string | undefined {
688 return this._authService.getAuthToken(request);
689 }
691 > // ---- Changeset operation handlers --------------------------------------
692 >
693 > async invokeChangesetOperation(params: InvokeChangesetOperationParams): Promise<InvokeChangesetOperationResult> {
694 return this._changesetOperationService.invokeChangesetOperation(params);
695 }
697 > // ---- MCP `mcp://` channel routing --------------------------------------
698 >
699 > async handleMcpRequest(channel: string, method: string, params: Record<string, unknown> | undefined): Promise<unknown> {
700 const route = parseMcpChannelUri(channel);
701 if (!route) {
709 return provider.handleMcpRequest(sessionUri, route.serverName, method, params);
710 }
712 > // ---- session management -------------------------------------------------
713 >
714 > /**
715 > * Builds the dependency surface the session server-tool group needs, bound
716 > * to this service so the group stays decoupled from the concrete host.
717 > */
718 > private _createSessionServerToolAccessor(): ISessionServerToolAccessor {
719 > return { agentService.ts
720 > listSessions: () => this.listSessions(),
721 > createSession: config => this.createSession(config),
722 > getModels: () => {
723 const models: IAgentModelInfo[] = [];
724 for (const provider of this._providers.values()) {
727 return models;
728 },
729 > startPrompt: (session, chat, prompt) => this._startSessionPrompt(session, chat, prompt), agentService.ts
730 > createChat: (session, chat, options) => this.createChat(session, chat, (options?.title !== undefined || options?.model !== undefined)
731 ? { ...(options.title !== undefined ? { title: options.title } : {}), ...(options.model !== undefined ? { model: { id: options.model.id } } : {}) }
732 : undefined),
733 > deleteSession: session => this.disposeSession(session), agentService.ts
734 > getChatContext: (session, chatId) => this._getChatContext(session, chatId),
735 > // Reads the `create_session` spawn depth from a session's `_meta` (0 when absent).
736 > getSessionSpawnDepth: session => readSessionSpawnDepth(this._stateManager.getSessionSummary(session.toString())?._meta),
737 > // Stamps a session's `create_session` spawn depth into its `_meta` (merging existing keys).
738 > setSessionSpawnDepth: (session, depth) => this._stateManager.dispatchServerAction(session.toString(), {
739 type: ActionType.SessionMetaChanged,
740 _meta: withSessionSpawnDepth(this._stateManager.getSessionSummary(session.toString())?._meta, depth),
741 }),
742 > }; agentService.ts
743 > }
745 > /**
746 > * Starts the first turn on a freshly-created session by dispatching a
747 > * `ChatTurnStarted` and routing it through the same side-effects path a
748 > * client-initiated turn takes (which sends the message to the provider).
749 > */
750 > private async _startSessionPrompt(session: URI, chat: URI, prompt: string): Promise<void> {
751 const message: Message = { text: prompt, origin: { kind: MessageKind.User } };
752 const action = { type: ActionType.ChatTurnStarted, turnId: generateUuid(), startedAt: new Date().toISOString(), message } as const;
754 this._sideEffects.handleAction(chat.toString(), action);
755 }
757 > /**
758 > * Reads a point-in-time snapshot of a session's chat conversation for the
759 > * `get_session_context` server tool. Targets the session's default chat, or a
760 > * specific peer chat when `chatId` is provided. Returns `undefined` when no
761 > * live conversation state exists (e.g. a cold/unsubscribed session).
762 > */
763 > private _getChatContext(session: URI, chatId?: string): IChatContextSnapshot | undefined {
764 const chatState = chatId
765 ? this._stateManager.getChatState(buildChatUri(session.toString(), chatId))
774 };
775 }
777 > async listSessions(): Promise<IAgentSessionMetadata[]> {
778 this._logService.trace('[AgentService] listSessions called');
779 const results = await Promise.all(
955 return combined;
956 }
958 > async createSession(config?: IAgentCreateSessionConfig): Promise<URI> {
959 const providerId = config?.provider ?? this._defaultProvider;
960 const provider = providerId ? this._providers.get(providerId) : undefined;
1174 return session;
1175 }
1177 > async createChat(session: URI, chat: URI, options?: IAgentCreateChatOptions): Promise<void> {
1178 const sessionKey = session.toString();
1179 const provider = this._findProviderForSession(session);
1285 }
1286 }
1288 > /**
1289 > * Validates a side chat's source and returns its {@link ChatOriginKind.SideChat}
1290 > * origin. Throws when the source chat is not part of `session` or when the
1291 > * referenced completed or active turn is absent.
1292 > */
1293 > private _resolveSideChatOrigin(session: URI, sideChat: IAgentCreateChatSideChatSource): { origin: ChatOrigin; sourceChat: string; selection?: IAgentCreateChatSideChatSelection; providerAnchorTurnId?: string; sourceContext?: string; partialResponse?: string } {
1294 const sessionKey = session.toString();
1295 const sourceKey = sideChat.source.toString();
1332 };
1333 }
1335 > private _resolveSessionSourceChat(session: URI, source: URI): { sourceChatKey: string; sourceSessionKey: string; sourceState: ReturnType<AgentHostStateManager['getChatState']> | undefined } {
1336 const sessionKey = session.toString();
1337 const sourceKey = source.toString();
1353 };
1354 }
1356 > async disposeChat(session: URI, chat: URI): Promise<void> {
1357 const sessionKey = session.toString();
1358 const provider = this._findProviderForSession(session);
1365 }
1366 }
1368 > // ---- Chat dispatch adapter ---------------------------------------------
1369 > //
1370 > // The orchestrator owns the feature-level `(session, chat)` →
1371 > // `(agent, session, chat)` mapping. It dispatches against an agent's
1372 > // chat-addressed surface ({@link IAgent.chats}) and session lifecycle
1373 > // ({@link IAgent.createSession}/{@link IAgent.disposeSession}).
1374 >
1375 > /** Whether `provider` can host additional (peer) chats. */
1376 > private _supportsChats(provider: IAgent): boolean {
1377 return !!provider.chats;
1378 }
1380 > private async _createProviderSession(provider: IAgent, config: IAgentCreateSessionConfig | undefined, deferWorktreeCreation: boolean): Promise<IAgentCreateSessionResult> {
1381 const requestedSessionId = deferWorktreeCreation && config?.session ? AgentSession.id(config.session) : undefined;
1382 if (requestedSessionId) {
1398 }
1399 }
1401 > private async _disposeSession(provider: IAgent, session: URI): Promise<void> {
1402 await provider.disposeSession(session);
1403 }
1405 > /**
1406 > * Reconstruct the turns for a chat. `chat` is the concrete chat channel URI,
1407 > * except for legacy restore paths that still address subagent sessions.
1408 > */
1409 > private async _getChatMessages(provider: IAgent, chat: URI): Promise<readonly Turn[]> {
1410 const turns = await provider.chats.getMessages(chat);
1411 // Host-owned worktree restore announcement: re-inject the "Created isolated
1418 return turns;
1419 }
1421 > /**
1422 > * Merges persisted host-injected local turns (`/rename`, `!command`) for
1423 > * `chatUri` back into that chat's SDK-derived `turns`, positioned after
1424 > * their anchor turn (the concrete turn they were recorded after). Locals
1425 > * anchored before any real turn are prepended; locals whose anchor is absent
1426 > * from the SDK turns (e.g. truncated away) are dropped. Also seeds the
1427 > * in-memory local-turn index so fork/truncate resolve correctly before the
1428 > * next reload.
1429 > */
1430 > private async _interleaveLocalTurns(sessionStr: string, chatUri: string, turns: readonly Turn[]): Promise<Turn[]> {
1431 const records = await this._localTurns.loadForChat(sessionStr, chatUri);
1432 if (records.length === 0) {
1462 return merged;
1463 }
1465 > /**
1466 > * Re-persists forked host-injected local turns (`/rename`, `!command`) into
1467 > * a newly forked chat so they survive reload and anchor future
1468 > * fork/truncate. `originalSlice[i]` and `forkedTurns[i]` are the source turn
1469 > * and its remapped copy (same length, 1:1); `mapping` is the old→new turn id
1470 > * map used to remap each local turn's anchor. `persistSession` owns the
1471 > * destination database; `sourceChatUri` / `newChatUri` key the source and
1472 > * destination local-turn indexes.
1473 > *
1474 > * Shared by the {@link createSession} (default-chat) and {@link createChat}
1475 > * (peer-chat) fork paths.
1476 > */
1477 > private _persistForkedLocalTurns(persistSession: string, sourceChatUri: string, newChatUri: string, originalSlice: readonly Turn[], forkedTurns: readonly Turn[], mapping: ReadonlyMap<string, string>): void {
1478 for (let i = 0; i < originalSlice.length; i++) {
1479 const original = originalSlice[i];
1486 }
1487 }
1489 > /**
1490 > * Create (or fork) the peer chat `chat` within `session`. `chat` is
1491 > * always a peer URI here (the default chat is created implicitly with
1492 > * the session), so no default-chat resolution is needed.
1493 > */
1494 > private _createChat(provider: IAgent, chat: URI, options: IAgentCreateChatOptions | undefined): Promise<IAgentCreateChatResult | void> {
1495 const convOptions: IAgentCreateChatOptions | undefined = options && (options.title !== undefined || options.model !== undefined || options.sideChat !== undefined)
1496 ? {
1504 : provider.chats.createChat(chat, convOptions);
1505 }
1507 > private async _disposeChat(provider: IAgent, chat: URI): Promise<void> {
1508 await provider.chats.disposeChat(chat);
1509 }
1511 > /**
1512 > * Derives a placeholder title for an imported session from its first user
1513 > * turn (imports seed pre-existing turns, so the normal first-message title
1514 > * generation never fires). Deliberately unprefixed: an imported session is a
1515 > * continuation of the source chat, not a distinct kind of session, so it
1516 > * should read like any other. The placeholder is later refined into a
1517 > * generated title (see the `importConversation` branch in `createSession`),
1518 > * but a neutral non-empty fallback is kept so the session still reads like a
1519 > * normal chat when generation is unavailable or fails.
1520 > */
1521 > private _buildImportedTitle(turns: readonly Turn[]): string {
1522 const firstText = turns.find(t => t.message?.text?.trim())?.message.text.trim();
1523 if (!firstText) {
1527 return firstText.length > MAX ? `${firstText.slice(0, MAX)}...` : firstText;
1528 }
1530 > private _buildInitialSummary(provider: IAgent, session: URI, config: IAgentCreateSessionConfig | undefined, created: { project?: { uri: URI; displayName: string }; workingDirectory?: URI }, title: string): SessionSummary {
1531 const now = new Date().toISOString();
1532 const primaryWorkingDir = (created.workingDirectory ?? config?.workingDirectory)?.toString();
1546 };
1547 }
1549 > /**
1550 > * Listen for an agent transitioning a provisional session into a fully
1551 > * materialized SDK session. The agent has already created the worktree
1552 > * (if any) and persisted on-disk metadata; we need to:
1553 > * - Refresh the in-memory summary with the resolved working directory
1554 > * and project metadata.
1555 > * - Persist any config values now that we have a real on-disk session.
1556 > * - Emit the deferred `notify/sessionAdded` so other clients learn of
1557 > * the session.
1558 > * - Dispatch `SessionReady` so subscribers see the lifecycle transition.
1559 > * - Lazily attach git state for the (possibly new) working directory.
1560 > */
1561 > private _onDidMaterializeSession(e: IAgentMaterializeSessionEvent): void {
1562 const sessionKey = e.session.toString();
1563 // The session is now materialized — its SDK is resolved (any cold
1605 this._changesetCoordinator.onSessionMaterialized(sessionKey);
1606 }
1608 > /** Drop a session's download-progress opt-in, if any. */
1609 > private _clearDownloadProgressInterest(sessionKey: string): void {
1610 for (const [provider, sessions] of this._downloadProgressInterest) {
1611 if (sessions.delete(sessionKey) && sessions.size === 0) {
1614 }
1615 }
1617 > /**
1618 > * Surface a host-level SDK download as client progress. The downloader fires
1619 > * process-global frames keyed by package id (which equals the provider id);
1620 > * because the download is shared across every session of that provider, we
1621 > * emit a SINGLE `progress` stream keyed by that package id — not one per
1622 > * session — so the client shows exactly one indicator no matter how many
1623 > * sessions of the provider are awaiting it. Frames are only emitted while at
1624 > * least one session has opted in (supplied a
1625 > * {@link IAgentCreateSessionConfig.progressToken} on `createSession`). A
1626 > * terminal frame reports `total === progress` (using `receivedBytes` when the
1627 > * size was never known) so the client dismisses the indicator deterministically.
1628 > *
1629 > * `displayName` is the provider's brand noun (e.g. `Claude`). It is woven
1630 > * into the notification's localized, human-readable `message` (e.g.
1631 > * "Downloading Claude agent") so a generic client can render the indicator
1632 > * verbatim without knowing the resource is an agent SDK. No trailing
1633 > * ellipsis: clients render progress as "<title>: <percent>", so an ellipsis
1634 > * would read as an unusual "…:" (see #324455).
1635 > */
1636 > emitDownloadProgress(packageId: string, displayName: string, receivedBytes: number, totalBytes: number | undefined, terminal: boolean): void {
1637 const sessions = this._downloadProgressInterest.get(packageId);
1638 if (!sessions || sessions.size === 0) {
1653 }
1654 }
1656 > private _persistWorkspaceless(session: URI, workspaceless: boolean): void {
1657 let ref;
1658 try {
1668 });
1669 }
1671 > private _persistConfigValues(session: URI, values: Record<string, unknown>): void {
1672 let ref;
1673 try {
1683 });
1684 }
1686 > private async _resolveCreatedSessionConfig(provider: IAgent, config: IAgentCreateSessionConfig | undefined): Promise<SessionConfigState | undefined> {
1687 if (!config?.config && !config?.workingDirectory) {
1688 return undefined;
1707 }
1708 }
1710 > async resolveSessionConfig(params: IAgentResolveSessionConfigParams): Promise<ResolveSessionConfigResult> {
1711 const providerId = params.provider ?? this._defaultProvider;
1712 const provider = providerId ? this._providers.get(providerId) : undefined;
1716 return this._withIsolationSchema(await provider.resolveSessionConfig(this._toProviderConfig(params)), params);
1717 }
1719 > /**
1720 > * Host-owned contribution of the shared `isolation` (folder / worktree),
1721 > * `branch`, `worktreeBranchPrefix`, and `worktreeIncludeFiles` session-config
1722 > * properties on top of whatever an agent returned from `resolveSessionConfig`. Provider-returned
1723 > * properties and values with these keys are replaced by the host contribution.
1724 > */
1725 > private async _withIsolationSchema(result: ResolveSessionConfigResult, params: IAgentResolveSessionConfigParams): Promise<ResolveSessionConfigResult> {
1726 if (!this._worktree) {
1727 return result;
1756 return { schema: { ...result.schema, properties }, values };
1757 }
1759 > async sessionConfigCompletions(params: IAgentSessionConfigCompletionsParams): Promise<SessionConfigCompletionsResult> {
1760 // The host owns branch completions for every agent (they share the same
1761 // git-backed branch list); all other properties stay provider-specific.
1770 return provider.sessionConfigCompletions(this._toProviderConfig(params));
1771 }
1773 > async completions(params: CompletionsParams): Promise<CompletionsResult> {
1774 return this._completions.completions(params);
1775 }
1777 > async getCompletionTriggerCharacters(): Promise<readonly string[]> {
1778 return this._completions.triggerCharacters;
1779 }
1781 > async disposeSession(session: URI): Promise<void> {
1782 this._logService.trace(`[AgentService] disposeSession: ${session.toString()}`);
1783 const provider = this._findProviderForSession(session);
1799 await this._sessionDataService.deleteSessionData(session);
1800 }
1802 > // ---- Protocol methods ---------------------------------------------------
1803 >
1804 > async createTerminal(params: CreateTerminalParams): Promise<void> {
1805 await this._terminalManager.createTerminal(params);
1806 }
1808 > async disposeTerminal(terminal: URI): Promise<void> {
1809 this._terminalManager.disposeTerminal(terminal.toString());
1810 }
1812 > async subscribe(resource: URI, clientId: string): Promise<IStateSnapshot> {
1813 this._logService.trace(`[AgentService] subscribe: ${resource.toString()}`);
1814 const resourceStr = resource.toString();
1904 }
1905 }
1907 > /** Waits for an armed subagent chat to register (or its wait to time out); returns `undefined` if not armed or never registered. */
1908 > private async _awaitPendingSubagentChat(subagentChatUri: string): Promise<IStateSnapshot | undefined> {
1909 const pending = this._pendingSubagentChats.get(subagentChatUri);
1910 if (!pending) {
1914 return this._stateManager.getSnapshot(subagentChatUri);
1915 }
1917 > addSubscriber(resource: URI, clientId: string): void {
1918 let set = this._resourceSubscribers.get(resource);
1919 const wasUnsubscribed = !set || set.size === 0;
1935 }
1936 }
1938 > unsubscribe(resource: URI, clientId: string): void {
1939 const set = this._resourceSubscribers.get(resource);
1940 if (!set) {
1969 }, SESSION_RELEASE_GRACE_MS));
1970 }
1972 > private _cancelPendingSessionRelease(resource: URI): void {
1973 this._pendingSessionRelease.deleteAndDispose(resource);
1974 }
1976 > /**
1977 > * If `resource` names a session that no client is still subscribed to and
1978 > * that has produced no turns (and has no active turn), schedule a delayed
1979 > * {@link _runSessionGc} to fully tear it down — provider session, worktree,
1980 > * persisted state and all. Sessions with at least one turn are left to the
1981 > * existing {@link _maybeEvictIdleSession} path which only drops cached
1982 > * state and lets the session be restored from disk later.
1983 > *
1984 > * The delay ({@link SESSION_GC_GRACE_MS}) gives a disconnected client time
1985 > * to reconnect or a workspace switch to settle. Any subsequent subscribe
1986 > * (or createSession on the same URI) cancels the timer via
1987 > * {@link _cancelPendingSessionGc}.
1988 > *
1989 > * Returns `true` if a GC timer was armed (existing or newly scheduled),
1990 > * so callers can skip alternative cleanup paths.
1991 > */
1992 > private _maybeScheduleSessionGc(resource: URI): boolean {
1993 // Subagent URIs are backed by the parent session; the parent's GC is
1994 // scheduled when its own subscriber count reaches zero.
2012 return true;
2013 }
2015 > private _cancelPendingSessionGc(resource: URI): void {
2016 this._pendingSessionGc.deleteAndDispose(resource);
2017 }
2019 > /**
2020 > * Fires {@link SESSION_GC_GRACE_MS} after a session lost its last
2021 > * subscriber while empty. Re-checks both invariants (still no subscribers,
2022 > * still empty) before tearing the session down via {@link disposeSession}.
2023 > * The cached state may already have been evicted by
2024 > * {@link _maybeEvictIdleSession}; in that case we still proceed because
2025 > * "evicted + no resubscribe" implies no client is observing the session.
2026 > */
2027 > private async _runSessionGc(resource: URI): Promise<void> {
2028 const key = resource.toString();
2029 if (this._resourceSubscribers.has(resource)) {
2037 await this.disposeSession(resource);
2038 }
2040 > /**
2041 > * If `resource` names an idle session and no client is still subscribed to
2042 > * it (or, for a subagent URI, no sibling subagent under the same parent is
2043 > * still subscribed), release its in-memory footprint: drop the cached AHP
2044 > * state from the state manager AND ask the provider to release the session's
2045 > * SDK resources ({@link IAgent.releaseSession}). Subagent URIs evict the
2046 > * parent session entry; the parent owns the materialized turn tree that
2047 > * backs every subagent view. Nothing durable is deleted — the next subscribe
2048 > * rehydrates the session via {@link restoreSession} and the provider resumes
2049 > * the SDK session on demand.
2050 > */
2051 > private _maybeEvictIdleSession(resource: URI): void {
2052 const key = resource.toString();
2053 if (this._resourceSubscribers.has(resource)) {
2102 });
2103 }
2105 > // Returns true when a changeset is safe to drop from the in-memory cache.
2106 > private _isChangesetEvictable(changeset: string): boolean {
2107 const changesetUri = URI.parse(changeset);
2108 // A direct changeset subscriber is rendering this expanded URI. Keep
2134 return !this._changesets.isStaticChangesetComputeActive(changeset);
2135 }
2137 > private _isSubagentDescendantOf(resource: URI, parent: URI): boolean {
2138 let parsed = parseSubagentSessionUri(resource);
2139 while (parsed) {
2145 return false;
2146 }
2148 > /**
2149 > * Per-client sequencer that serialises action dispatches whose
2150 > * processing requires an asynchronous prelude (e.g. snapshotting
2151 > * user-message attachments into the session database before the
2152 > * action is reduced into state). Actions that don't need any
2153 > * asynchronous prelude bypass the queue entirely as long as no
2154 > * earlier action from the same client is still pending.
2155 > *
2156 > * todo@connor4312: we can drop this when sending a message become a command
2157 > */
2158 > private readonly _clientDispatchQueues = new Map<string, Promise<void>>();
2159 >
2160 > dispatchAction(channel: string, action: SessionAction | ChatAction | TerminalAction | ClientChangesetAction | ClientAnnotationsAction | IRootConfigChangedAction, clientId: string, clientSeq: number): void {
2161 this._logService.trace(`[AgentService] dispatchAction: type=${action.type}, clientId=${clientId}, clientSeq=${clientSeq}`, action);
2162
2197 }));
2198 }
2200 > private _dispatchActionNow(channel: string, sessionChannel: string, action: SessionAction | ChatAction | TerminalAction | ClientChangesetAction | ClientAnnotationsAction | IRootConfigChangedAction, clientId: string, clientSeq: number): void {
2201 const origin = { clientId, clientSeq };
2202 this._stateManager.dispatchClientAction(channel, action, origin);
2206 this._sideEffects.handleAction(channel, action, clientId);
2207 }
2209 > private _needsAsyncRewrite(channel: string, action: SessionAction | ChatAction | TerminalAction | ClientChangesetAction | ClientAnnotationsAction | IRootConfigChangedAction): action is ChatTurnStartedAction | ChatPendingMessageSetAction {
2210 if (action.type !== ActionType.ChatTurnStarted && action.type !== ActionType.ChatPendingMessageSet) {
2211 return false;
2214 return !!action.message.attachments?.some(a => this._isRewritableAttachment(a, attachmentsRootStr));
2215 }
2216 > private _isRewritableAttachment(attachment: MessageAttachment, attachmentsRootStr: string): boolean { agentService.ts
2217 if (attachment.type === MessageAttachmentKind.EmbeddedResource) {
2218 return true;
2231 return false;
2232 }
2234 > private _attachmentsRoot(session: string): URI {
2235 return joinPath(this._sessionDataService.getSessionDataDir(URI.parse(session)), SESSION_ATTACHMENTS_DIRNAME);
2236 }
2238 > /**
2239 > * Snapshot inline / client-resident attachment payloads onto disk
2240 > * under the session's data directory and rewrite the action to
2241 > * reference them via local `file:` URIs. Keeps potentially large
2242 > * blobs (e.g. pasted images) out of the in-memory state tree while
2243 > * letting the agent consume them via the standard {@link IFileService}
2244 > * surface — no special URI scheme or blob round-tripping needed.
2245 > *
2246 > * Failures are isolated per-attachment: if a rewrite cannot be
2247 > * performed (no client connection registered, `resourceRead` rejects,
2248 > * etc.) the original attachment is preserved so the agent still has a
2249 > * chance to make use of it.
2250 > */
2251 > private async _rewriteUserMessageAttachments<T extends ChatTurnStartedAction | ChatPendingMessageSetAction>(channel: string, action: T, clientId: string): Promise<T> {
2252 const attachments = action.message.attachments;
2253 if (!attachments?.length) {
2262 };
2263 }
2265 > private async _rewriteSingleAttachment(attachment: MessageAttachment, attachmentsRoot: URI, attachmentsRootStr: string, clientId: string): Promise<MessageAttachment> {
2266 try {
2267 if (attachment.type === MessageAttachmentKind.EmbeddedResource) {
2287 return attachment;
2288 }
2290 > /**
2291 > * Like {@link IFileService.exists} but never throws (e.g. when no provider
2292 > * is registered for the URI scheme), returning `false` in that case.
2293 > */
2294 > private async _fileExistsSafe(uri: URI): Promise<boolean> {
2295 try {
2296 return await this._fileService.exists(uri);
2299 }
2300 }
2302 > /**
2303 > * Reads `originalUri` through the `vscode-agent-client` filesystem
2304 > * provider so it is fetched from the originating client. Falls back to
2305 > * a direct read against `originalUri` when no client filesystem
2306 > * authority is registered for `clientId` (e.g. unit tests, in-process
2307 > * agent host with a local URI).
2308 > */
2309 > private async _readClientResource(originalUri: URI, clientId: string): Promise<Uint8Array> {
2310 const proxiedUri = clientId ? toAgentClientUri(originalUri, clientId) : originalUri;
2311 try {
2324 }
2325 }
2327 > private async _writeAndRewrite(
2328 original: MessageAttachment,
2329 bytes: Uint8Array,
2347 return rewritten;
2348 }
2350 > /**
2351 > * Pick a sensible on-disk basename for the snapshotted attachment,
2352 > * preserving a usable extension where possible so the SDK and other
2353 > * downstream consumers can detect the right type from the path alone.
2354 > */
2355 > private _attachmentBasename(label: string, contentType: string | undefined): string {
2356 const safeLabel = (label || 'attachment').replace(/[\\/:*?"<>|\u0000-\u001f]/g, '_');
2357 if (resourcesExtname(URI.file(safeLabel))) {
2361 return ext ? `${safeLabel}${ext}` : safeLabel;
2362 }
2364 > async resourceList(uri: URI): Promise<ResourceListResult> {
2365 let stat;
2366 try {
2380 return { entries };
2381 }
2383 > async restoreSession(session: URI): Promise<void> {
2384 const sessionStr = session.toString();
2385
2404 }
2405 }
2407 > private async _doRestoreSession(session: URI, sessionStr: string): Promise<void> {
2408 if (this._stateManager.getSessionState(sessionStr)) {
2409 return;
2629 void this._gitStateService.attachSessionGitHubPullRequest(sessionStr);
2630 }
2632 > /**
2633 > * Restores the additional (non-default) peer chats for a session.
2634 > *
2635 > * Enumeration is driven by the orchestrator's OWN persisted catalog (the
2636 > * {@link PEER_CHATS_METADATA_KEY} blob). For each catalog entry the agent's
2637 > * in-memory backing is re-attached via
2638 > * {@link IAgent.materializeChat} (handing back the opaque
2639 > * `providerData` blob) BEFORE its history is read, then the chat is
2640 > * re-registered in the state manager with its persisted title and draft so
2641 > * it reappears after a process restart. Best-effort: a chat whose history
2642 > * fails to load is restored with no turns rather than dropped.
2643 > *
2644 > * When the orchestrator catalog is absent ({@link _readPersistedPeerChatCatalog}
2645 > * returns `undefined`) the session predates orchestrator-owned persistence:
2646 > * a one-time migration ({@link _migrateLegacyPeerChats}) drains the agent's
2647 > * legacy `*.chats` enumeration into the catalog so it is never consulted
2648 > * again.
2649 > */
2650 > private async _restorePeerChats(agent: IAgent, session: URI): Promise<void> {
2651 const persisted = await this._readPersistedPeerChatCatalog(session);
2652 if (persisted !== undefined) {
2658 await this._migrateLegacyPeerChats(agent, session);
2659 }
2661 > /**
2662 > * One-time migration for sessions persisted before the orchestrator owned
2663 > * the peer-chat catalog: enumerate the agent's legacy `*.chats`
2664 > * ({@link IAgent.listLegacyChats}), restore them via the same path as the
2665 > * new catalog, then write the orchestrator {@link PEER_CHATS_METADATA_KEY}
2666 > * blob so subsequent restores read the new catalog and never consult the
2667 > * legacy read again. No-op when the agent has no legacy enumeration or none
2668 > * is persisted.
2669 > */
2670 > private async _migrateLegacyPeerChats(agent: IAgent, session: URI): Promise<void> {
2671 const legacy = await agent.listLegacyChats?.(session);
2672 if (!legacy || legacy.length === 0) {
2688 await this._enqueuePeerChatCatalogWrite(session, () => [...entries]);
2689 }
2691 > /**
2692 > * Restores a set of peer chats from an enumerated catalog. Loads each
2693 > * chat's history in parallel (after re-attaching its backing) but restores
2694 > * them in catalog order, so the catalog never reorders by which chat's
2695 > * history/title happened to resolve first.
2696 > */
2697 > private async _restorePeerChatsFromCatalog(agent: IAgent, session: URI, entries: readonly IPersistedPeerChat[]): Promise<void> {
2698 const restored = await Promise.all(entries.map(async (entry) => {
2699 let chatUri: URI;
2743 }
2744 }
2746 > /**
2747 > * Re-persists a peer chat's opaque `providerData` blob when the agent
2748 > * reports it changed (e.g. per-chat model switch or fork remap).
2749 > */
2750 > private _onChatDataChanged(e: IAgentChatDataChange): void {
2751 const sessionStr = parseDefaultChatUri(e.chat);
2752 if (sessionStr === undefined) {
2756 void this._persistPeerChat(URI.parse(sessionStr), e.chat, e.providerData);
2757 }
2759 > /**
2760 > * Deterministic membership sequencer for agent-spawned chats,
2761 > * driven off {@link IAgent.onDidSessionProgress}: a `subagent_started` adds
2762 > * the subagent chat to the catalog via the same spawn-channel handler
2763 > * ({@link _onChatSpawned}) used by {@link IAgent.onDidSpawnChat}.
2764 > * A completed subagent chat stays live and subscribable, so completion is
2765 > * not sequenced here; subagent chats are removed only on session teardown.
2766 > * Registered before {@link AgentSideEffects} so the subagent chat exists
2767 > * before its turn starts; addChat is idempotent so overlapping with the
2768 > * agent's own spawn bridge is safe.
2769 > */
2770 > private _sequenceSpawnedChat(signal: AgentSignal): void {
2771 const spawn = SubagentChatSignal.toSpawnEvent(signal);
2772 if (spawn) {
2774 }
2775 }
2777 > /** Marks a subagent chat as pending once its confirmed tool call reaches (or is about to reach) `Running`. */
2778 > private _trackPendingSubagentChatFromEnvelope(envelope: ActionEnvelope): void {
2779 > const { channel, action } = envelope; agentService.ts
2780 > if (action.type === ActionType.ChatToolCallStart || action.type === ActionType.ChatToolCallDelta || action.type === ActionType.ChatToolCallReady) {
2781 const key = `${channel}:${action.toolCallId}`;
2782 // Providers stamp `toolKind`/`subagentChatUri` on whichever action
2800 return;
2801 }
2802 > if (action.type === ActionType.ChatToolCallConfirmed) { agentService.ts
2803 const key = `${channel}:${action.toolCallId}`;
2804 const subagentChatUri = this._pendingSubagentToolCalls.get(key);
2814 return;
2815 }
2816 > if (action.type === ActionType.ChatToolCallComplete) { agentService.ts
2817 // Defensive cleanup: a tool call can complete without ever being
2818 // confirmed (e.g. cancelled by other means) while still tracked.
2819 this._pendingSubagentToolCalls.delete(`${channel}:${action.toolCallId}`);
2820 }
2821 > } agentService.ts
2823 > private _armPendingSubagentChat(subagentChatUri: string): void {
2824 if (this._pendingSubagentChats.has(subagentChatUri) || this._stateManager.getSnapshot(subagentChatUri)) {
2825 return;
2833 }, SUBAGENT_CHAT_PENDING_TIMEOUT_MS));
2834 }
2836 > private _resolvePendingSubagentChat(resource: string): void {
2837 const deferred = this._pendingSubagentChats.get(resource);
2838 if (!deferred) {
2843 deferred.complete();
2844 }
2846 > /**
2847 > * Routes an agent-spawned chat (e.g. a sub-agent delegated by a tool
2848 > * call) straight into the chat catalog via {@link IAgentHostStateManager.addChat},
2849 > * so harness-spawned chats and user-driven chats share ONE membership path.
2850 > * The {@link IAgentSpawnChatEvent.parent} spawn edge is recorded as
2851 > * the chat's {@link ChatOriginKind.Tool} origin. Spawned chats are
2852 > * not written to the orchestrator's persisted peer-chat catalog — they are
2853 > * transient children re-derived from the parent's event log on restore.
2854 > */
2855 > private _onChatSpawned(e: IAgentSpawnChatEvent): void {
2856 this._stateManager.addChat(e.session.toString(), e.chat.toString(), {
2857 ...(e.title !== undefined ? { title: e.title } : {}),
2866 this._resolvePendingSubagentChat(e.chat.toString());
2867 }
2869 > /**
2870 > * Reads the orchestrator's persisted peer-chat catalog for a session.
2871 > * Returns `undefined` when the session has no catalog yet (a legacy session
2872 > * predating orchestrator-owned persistence, or a corrupt blob); the caller
2873 > * then performs a one-time migration from the agent's legacy `*.chats`
2874 > * enumeration (see {@link _restorePeerChats} / {@link _migrateLegacyPeerChats}).
2875 > * An empty array means the session is known to have no peer chats, so
2876 > * migration is skipped.
2877 > */
2878 > private async _readPersistedPeerChatCatalog(session: URI): Promise<IPersistedPeerChat[] | undefined> {
2879 const ref = await this._sessionDataService.tryOpenDatabase?.(session);
2880 if (!ref) {
2905 }
2906 }
2908 > /**
2909 > * Marks a peer chat's backing SDK session (in that session's own DB) so
2910 > * {@link listSessions} filters it out of the top-level session list. The
2911 > * marker is persisted, so it survives a host restart. Best-effort: a failure
2912 > * only means the backing session may transiently reappear in the list.
2913 > */
2914 > private _markPeerChatBacking(backingSession: URI, chat: URI): void {
2915 let ref;
2916 try {
2926 });
2927 }
2929 > /**
2930 > * Inserts or updates a single peer chat in the orchestrator's persisted
2931 > * catalog, recording its opaque `providerData` verbatim (or clearing it when
2932 > * `undefined`). When `origin` is supplied it is stored as the chat's
2933 > * provenance; when omitted (e.g. a provider-driven `providerData` refresh via
2934 > * {@link _onChatDataChanged}) any previously persisted origin is preserved so
2935 > * a data refresh never drops a side chat's source boundary. Serialized per
2936 > * session via {@link _enqueuePeerChatCatalogWrite}.
2937 > */
2938 > private _persistPeerChat(session: URI, chat: URI, providerData: string | undefined, origin?: ChatOrigin): Promise<void> {
2939 const chatUri = chat.toString();
2940 return this._enqueuePeerChatCatalogWrite(session, entries => {
2950 });
2951 }
2953 > /**
2954 > * Removes a peer chat from the orchestrator's persisted catalog. Serialized
2955 > * per session via {@link _enqueuePeerChatCatalogWrite}.
2956 > */
2957 > private _removePersistedPeerChat(session: URI, chat: URI): Promise<void> {
2958 const chatUri = chat.toString();
2959 return this._enqueuePeerChatCatalogWrite(session, entries => entries.filter(entry => entry.uri !== chatUri));
2960 }
2962 > /**
2963 > * Chains a read-modify-write of a session's persisted peer-chat catalog
2964 > * behind any in-flight write for the same session, so concurrent
2965 > * create/dispose/data-change updates can't clobber each other.
2966 > */
2967 > private _enqueuePeerChatCatalogWrite(session: URI, mutate: (entries: IPersistedPeerChat[]) => IPersistedPeerChat[]): Promise<void> {
2968 const key = session.toString();
2969 const previous = this._peerChatCatalogWrites.get(key) ?? Promise.resolve();
2978 return next;
2979 }
2981 > private async _applyPeerChatCatalogWrite(session: URI, mutate: (entries: IPersistedPeerChat[]) => IPersistedPeerChat[]): Promise<void> {
2982 const ref = await this._sessionDataService.tryOpenDatabase?.(session);
2983 if (!ref) {
3011 }
3012 }
3014 > /** Reads a chat's persisted custom title (default or peer chat), if any. */
3015 > private async _readPersistedChatTitle(session: URI, chatUri: URI): Promise<string | undefined> {
3016 const ref = await this._sessionDataService.tryOpenDatabase?.(session);
3017 if (!ref) {
3026 }
3027 }
3029 > private async _getChatDraft(session: URI, chatUri: URI): Promise<Message | undefined> {
3030 const ref = await this._sessionDataService.tryOpenDatabase(session);
3031 if (!ref) {
3038 }
3039 }
3041 > private async _getSessionMetadataForRestore(agent: IAgent, session: URI): Promise<IAgentSessionMetadata | undefined> {
3042 const sessionStr = session.toString();
3043 if (agent.getSessionMetadata) {
3065 return this._withWorktreeProject(session, await this._getSessionMetadataFromCatalog(agent, session));
3066 }
3068 > /**
3069 > * Merges the repository project for a worktree-isolated session onto its
3070 > * restored metadata so the session groups under the repository (not the
3071 > * `<repo>.worktrees/<name>` directory) in the sessions UI. No-op for folder
3072 > * sessions and for `undefined` metadata. Host-owned so agents stay unaware.
3073 > */
3074 > private async _withWorktreeProject(session: URI, meta: IAgentSessionMetadata | undefined): Promise<IAgentSessionMetadata | undefined> {
3075 if (!meta || !this._worktree) {
3076 return meta;
3079 return project ? { ...meta, project } : meta;
3080 }
3082 > private async _getSessionMetadataFromCatalog(agent: IAgent, session: URI): Promise<IAgentSessionMetadata | undefined> {
3083 const sessionStr = session.toString();
3084 let allSessions;
3094 return allSessions.find(s => s.session.toString() === sessionStr);
3095 }
3097 > async resourceRead(uri: URI): Promise<ResourceReadResult> {
3098 // Handle session-db: URIs that reference file-edit content stored
3099 // in a per-session SQLite database.
3131 }
3132 }
3134 > async resourceWrite(params: ResourceWriteParams): Promise<ResourceWriteResult> {
3135 const fileUri = typeof params.uri === 'string' ? URI.parse(params.uri) : URI.revive(params.uri);
3136 try {
3191 }
3192 }
3194 > private async _createFileExclusive(fileUri: URI, content: VSBuffer): Promise<void> {
3195 if (fileUri.scheme !== Schemas.file) {
3196 await this._fileService.createFile(fileUri, content, { overwrite: false });
3228 }
3229 }
3231 > /**
3232 > * Slow-path for {@link resourceWrite} when the caller requested a
3233 > * non-default {@link ResourceWriteMode}, supplied a `position`, or
3234 > * provided an `ifMatch` etag precondition. Reads the current file
3235 > * contents (when needed) and produces a single `writeFile` call that
3236 > * realises the requested splice. A missing file is treated as
3237 > * empty for `append` and `insert` (so the operation behaves like a
3238 > * create); for `truncate` it falls through to a normal write.
3239 > */
3240 > private async _resourceWriteWithMode(
3241 fileUri: URI,
3242 data: VSBuffer,
3298 }
3299 }
3301 > async resourceCopy(params: ResourceCopyParams): Promise<ResourceCopyResult> {
3302 const source = URI.parse(params.source);
3303 const destination = URI.parse(params.destination);
3316 }
3317 }
3319 > async resourceDelete(params: ResourceDeleteParams): Promise<ResourceDeleteResult> {
3320 const fileUri = URI.parse(params.uri);
3321 try {
3329 }
3330 }
3332 > async resourceMove(params: ResourceMoveParams): Promise<ResourceMoveResult> {
3333 const source = URI.parse(params.source);
3334 const destination = URI.parse(params.destination);
3347 }
3348 }
3350 > async resourceResolve(params: ResourceResolveParams): Promise<ResourceResolveResult> {
3351 const uri = typeof params.uri === 'string' ? URI.parse(params.uri) : URI.revive(params.uri);
3352 try {
3380 }
3381 }
3383 > async resourceMkdir(params: ResourceMkdirParams): Promise<ResourceMkdirResult> {
3384 const uri = typeof params.uri === 'string' ? URI.parse(params.uri) : URI.revive(params.uri);
3385 try {
3403 }
3404 }
3406 > async createResourceWatch(params: CreateResourceWatchParams): Promise<CreateResourceWatchResult> {
3407 const root = typeof params.uri === 'string' ? URI.parse(params.uri) : URI.revive(params.uri);
3408 // Verify the URI exists before we mint a channel; spec requires
3430 return { channel };
3431 }
3433 > /**
3434 > * Notifies the agent service that a client subscribed to a resource
3435 > * watch channel. On the first subscriber the underlying
3436 > * {@link IFileService} watcher is attached; subsequent subscribers
3437 > * bump the refcount and cancel any pending grace dispose. Returns
3438 > * the decoded descriptor for use as the subscribe snapshot, or
3439 > * `undefined` when `channel` is not a recognisable
3440 > * `ahp-resource-watch:` URI.
3441 > */
3442 > onResourceWatchSubscribed(channel: string): ResourceWatchState | undefined {
3443 const descriptor = parseResourceWatchChannelUri(channel);
3444 if (!descriptor) {
3495 return descriptor;
3496 }
3498 > /**
3499 > * Counterpart to {@link onResourceWatchSubscribed}. Decrements the
3500 > * subscriber refcount for a watch channel; when it reaches zero the
3501 > * watcher is held for {@link RESOURCE_WATCH_GRACE_MS} before being
3502 > * disposed, giving a transient disconnect time to resubscribe.
3503 > */
3504 > onResourceWatchUnsubscribed(channel: string): boolean {
3505 const entry = this._resourceWatches.get(channel);
3506 if (!entry) {
3520 return true;
3521 }
3523 > private _dispatchResourceWatchChanges(channel: string, raw: readonly IFileChange[]): void {
3524 if (raw.length === 0) {
3525 return;
3536 });
3537 }
3539 > async shutdown(): Promise<void> {
3540 this._logService.info('AgentService: shutting down all providers...');
3541 const promises: Promise<void>[] = [];
3549 this._downloadProgressInterest.clear();
3550 }
3552 > /**
3553 > * Wire the network diagnostics service backing {@link getNetworkDiagnosticsInfo}
3554 > * and {@link diagnosticsFetch}. A setter rather than a constructor argument
3555 > * because the service depends on the agent-host proxy resolver, which the
3556 > * remote server constructs lazily — after this service.
3557 > */
3558 > setNetworkDiagnosticsService(service: INetworkDiagnosticsService): void {
3559 this._networkDiagnostics = service;
3560 }
3562 > async getNetworkDiagnosticsInfo(): Promise<IAgentHostNetworkDiagnosticsInfo> {
3563 if (!this._networkDiagnostics) {
3564 throw new Error('Network diagnostics unavailable: service not wired');
3597 return this._networkDiagnostics.getInfo(endpoints, accounts.find(account => !!account));
3598 }
3600 > async getManagedSettingsDiagnostics(): Promise<readonly IAgentHostManagedSettingsDiagnostics[]> {
3601 const providers = [...this._providers.values()].filter(provider => provider.getManagedSettingsDiagnostics);
3602 return Promise.all(providers.map(async provider => {
3608 }));
3609 }
3611 > async diagnosticsFetch(url: string): Promise<IAgentHostNetworkFetchResult> {
3612 if (!this._networkDiagnostics) {
3613 throw new Error('Network diagnostics unavailable: service not wired');
3615 return this._networkDiagnostics.fetch(url);
3616 }
3618 > // ---- helpers ------------------------------------------------------------
3619 >
3620 > private async _fetchSessionDbContent(fields: ISessionDbUriFields): Promise<ResourceReadResult> {
3621 const sessionUri = URI.parse(fields.sessionUri);
3622 const ref = this._sessionDataService.openDatabase(sessionUri);
3639 }
3640 }
3642 > private async _fetchGitBlobContent(fields: IGitBlobUriFields): Promise<ResourceReadResult> {
3643 if (!this._gitService) {
3644 throw new ProtocolError(AhpErrorCodes.NotFound, `git service unavailable for: ${fields.repoRelativePath}`);
3658 };
3659 }
3661 > /**
3662 > * Restores a subagent session from its parent session's event history.
3663 > * Loads the parent's raw messages, filters for events belonging to
3664 > * the subagent (by `parentToolCallId`), and builds the child session's
3665 > * turns from those events.
3666 > */
3667 > private async _restoreSubagentSession(subagentUri: string, parentSession: URI): Promise<void> {
3668 if (this._stateManager.getSessionState(subagentUri)) {
3669 return;
3685 }
3686 }
3688 > private async _doRestoreSubagentSession(subagentUri: string, parentSession: URI): Promise<void> {
3689 // Ensure the parent session is loaded first
3690 const parentSessionKey = parentSession.toString();
3768 this._logService.info(`[AgentService] Restored subagent session: ${subagentUri} with ${childTurns.length} turn(s)`);
3769 }
3771 > /**
3772 > * Registers a subagent child session's state up-front from data the agent
3773 > * already reconstructed for the parent, so a later subscribe-driven
3774 > * {@link _restoreSubagentSession} finds it present and returns early
3775 > * instead of re-reading the parent event log. No-op if already registered.
3776 > */
3777 > private _registerRestoredSubagent(child: IRestoredSubagentSession, parentSummary: SessionSummary, parentSessionStr: string): void {
3778 const resourceStr = child.resource.toString();
3779 if (this._stateManager.getSessionState(resourceStr)) {
3807 });
3808 }
3810 > private _findProviderForSession(session: URI | string): IAgent | undefined {
3811 const key = typeof session === 'string' ? session : session.toString();
3812 const providerId = this._sessionToProvider.get(key);
3824 return undefined;
3825 }
3827 > /**
3828 > * Sets the agents observable to trigger model re-fetch and
3829 > * `root/agentsChanged` via the autorun in {@link AgentSideEffects}.
3830 > */
3831 > private _updateAgents(): void {
3832 > this._agents.set([...this._providers.values()], undefined); agentService.ts
3833 > }
3835 > override dispose(): void {
3836 > for (const provider of this._providers.values()) { agentService.ts
3837 > provider.dispose(); agentService.ts
3838 > }
3839 > this._providers.clear(); agentService.ts
3840 > super.dispose();
3841 > }
3842 > } agentService.ts
3843 >
3844 function isErrorWithCode(error: unknown, code: string): boolean {
3845 return error instanceof Error && hasErrorCode(error, code);
3846 }
3848 function hasErrorCode(error: Error | { code: unknown }, code: string): boolean {
3849 return hasKey(error, { code: true }) && error.code === code;
3850 }
3852 > /**
3853 > * Runtime owner of an active resource watch — pairs the {@link IFileService}
3854 > * watcher disposables with the subscriber refcount and the optional
3855 > * grace-window timer used to delay disposal after the last unsubscribe.
3856 > */
3857 > interface IActiveResourceWatch extends IDisposable {
3858 > readonly channel: string;
3859 > readonly descriptor: ResourceWatchState;
3860 > subscribers: number;
3861 > readonly disposables: DisposableStore;
3862 > pendingGc: MutableDisposable<IDisposable>;
3863 > }
3864 >
3865 > /**
3866 > * Flatten a {@link FileChangesEvent} into a synthetic {@link IFileChange}
3867 > * list. The event stores only URI arrays publicly (the underlying
3868 > * `IFileChange[]` is private), so we reconstruct one entry per URI per
3869 > * change type. The synthetic shape is sufficient for translation into
3870 > * `ResourceWatchChangedAction` items.
3871 > */
3872 function collectChanges(event: FileChangesEvent): IFileChange[] {
3873 const out: IFileChange[] = [];
3883 return out;
3884 }
3886 > /**
3887 > * Variant of {@link collectChanges} that restricts the output to changes
3888 > * inside `root` (inclusive). Used for the recursive watch fallback,
3889 > * which feeds off the uncorrelated global stream and must filter out
3890 > * unrelated events.
3891 > */
3892 function collectChangesUnderRoot(event: FileChangesEvent, root: URI): IFileChange[] {
3893 const out: IFileChange[] = [];
src/vs/platform/agentHost/common/state/protocol/common/commands.ts 1071 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- commands.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > import type { URI, Snapshot } from './state.js';
10 > import type { ActionEnvelope, StateAction } from './actions.js';
11 > import type { TelemetryCapabilities } from '../channels-otlp/state.js';
12 >
13 > // ─── BaseParams ──────────────────────────────────────────────────────────────
14 >
15 > /**
16 > * Base shape every command's params extends.
17 > *
18 > * `channel` identifies the channel the command targets, mirroring the
19 > * `channel` field on every protocol notification. For commands that operate
20 > * on a specific channel (a session, terminal, or changeset), `channel` is
21 > * that channel's URI. For commands that are connection-level rather than
22 > * channel-scoped (e.g. {@link InitializeParams | `initialize`},
23 > * {@link PingParams | `ping`}, {@link ListSessionsParams | `listSessions`},
24 > * the `resource*` filesystem commands, and {@link AuthenticateParams |
25 > * `authenticate`}), the params type narrows `channel` to the literal
26 > * root URI `'ahp-root://'`.
27 > *
28 > * This invariant lets implementations route every incoming message —
29 > * request, response, or notification — by inspecting `params.channel`
30 > * without needing to know the per-method param shape.
31 > *
32 > * @category Commands
33 > */
34 > export interface BaseParams {
35 > /** Channel URI this command targets. */
36 > channel: URI;
37 > }
38 >
39 > // ─── Pagination ──────────────────────────────────────────────────────────────
40 >
41 > /**
42 > * Cursor-based pagination inputs, mixed into the params of any list command
43 > * that can page a large result set (e.g. {@link ListSessionsParams |
44 > * `listSessions`}). The paired output is {@link PaginatedResult}.
45 > *
46 > * Pagination is **opaque and cursor-based**, mirroring the shape `fetchTurns`
47 > * already uses for chat history: the server owns the ordering and keyset, and
48 > * the client walks pages by echoing the cursor from the previous
49 > * {@link PaginatedResult.nextCursor} back on the next request.
50 > *
51 > * The contract every paginated command shares:
52 > *
53 > * - To fetch the first page, omit `cursor`. Supply `limit` to bound the page.
54 > * - If the result carries a {@link PaginatedResult.nextCursor}, more entries
55 > * exist — pass it back as `cursor` to fetch the following page. A missing
56 > * `nextCursor` signals the end of the collection.
57 > * - Cursors are **server-defined and opaque**: clients MUST NOT parse, modify,
58 > * or persist them across connections. An unrecognised cursor SHOULD be
59 > * rejected with an `InvalidParams` error.
60 > * - Pagination is **fully additive**: a client that omits `limit`/`cursor` and
61 > * ignores `nextCursor` sees the pre-pagination behaviour (subject to any
62 > * server-imposed cap), and a server that does not paginate ignores the inputs
63 > * and returns everything in a single page.
64 > *
65 > * @category Commands
66 > */
67 > export interface PaginatedParams {
68 > /**
69 > * Maximum number of entries to return in this page. The server SHOULD respect
70 > * this bound but MAY return fewer entries and MAY impose its own upper cap.
71 > * Omit to let the server choose the page size.
72 > */
73 > limit?: number;
74 > /**
75 > * Opaque pagination cursor from a previous {@link PaginatedResult.nextCursor}.
76 > * Omit to fetch the first page. Cursors are server-defined and MUST be treated
77 > * as opaque — do not parse, modify, or persist them across connections. An
78 > * unrecognised cursor SHOULD be rejected with an `InvalidParams` error.
79 > */
80 > cursor?: string;
81 > }
82 >
83 > /**
84 > * Cursor-based pagination output, extended by the result of any list command
85 > * that can page a large result set (e.g. {@link ListSessionsResult |
86 > * `listSessions`}). See {@link PaginatedParams} for the full pagination
87 > * contract shared by every paginated command.
88 > *
89 > * @category Commands
90 > */
91 > export interface PaginatedResult {
92 > /**
93 > * Opaque cursor for the next page. Present when more entries exist beyond the
94 > * returned page; absent signals the end of the collection. Pass it back as
95 > * {@link PaginatedParams.cursor} to fetch the following page.
96 > */
97 > nextCursor?: string;
98 > }
99 >
100 > // ─── initialize ──────────────────────────────────────────────────────────────
101 >
102 > /**
103 > * Identifies a protocol implementation — the software (and build) on one end
104 > * of the connection, as distinct from the {@link AgentInfo | agent persona} it
105 > * hosts. Carried as {@link InitializeParams.clientInfo | `clientInfo`} on the
106 > * client side and {@link InitializeResult.serverInfo | `serverInfo`} on the
107 > * server side, mirroring LSP's `clientInfo`/`serverInfo` and MCP's
108 > * `Implementation`.
109 > *
110 > * This is **informational only**: it exists for logging, telemetry, an
111 > * about/status affordance, and — as a last resort — a known-issue workaround
112 > * for a specific buggy build. It is **not** a feature-detection mechanism.
113 > * Feature availability stays with the capability model
114 > * ({@link ClientCapabilities} and the various `*.capabilities` declarations);
115 > * implementations SHOULD NOT gate protocol behaviour on parsing
116 > * {@link Implementation.version | `version`}.
117 > *
118 > * @category Commands
119 > */
120 > export interface Implementation {
121 > /** Implementation name, e.g. a product or package identifier. */
122 > name: string;
123 > /**
124 > * Implementation version. A [SemVer](https://semver.org) string is
125 > * recommended but not required.
126 > */
127 > version?: string;
128 > /** Optional human-readable display name. */
129 > title?: string;
130 > }
131 >
132 > /**
133 > * Establishes a new connection and negotiates the protocol version.
134 > * This MUST be the first message sent by the client.
135 > *
136 > * @category Commands
137 > * @method initialize
138 > * @direction Client → Server
139 > * @messageType Request
140 > * @version 1
141 > * @see {@link /specification/lifecycle | Lifecycle} for the full handshake flow.
142 > */
143 > export interface InitializeParams extends BaseParams {
144 > channel: 'ahp-root://';
145 > /**
146 > * Protocol versions the client is willing to speak, ordered from most
147 > * preferred to least preferred. Each entry is a [SemVer](https://semver.org)
148 > * `MAJOR.MINOR.PATCH` string (e.g. `"0.1.0"`).
149 > *
150 > * The server selects one entry and returns it as `InitializeResult.protocolVersion`.
151 > * If the server cannot speak any of the offered versions, it MUST return
152 > * error code `-32005` (`UnsupportedProtocolVersion`).
153 > */
154 > protocolVersions: string[];
155 > /** Unique client identifier */
156 > clientId: string;
157 > /**
158 > * Optional identity of the client implementation (name and version).
159 > * Informational only — see {@link Implementation} for how it may and may not
160 > * be used. Distinct from {@link InitializeParams.clientId | `clientId`},
161 > * which is an opaque per-connection identifier used for reconnection, not a
162 > * human-readable implementation name.
163 > */
164 > clientInfo?: Implementation;
165 > /** URIs to subscribe to during handshake */
166 > initialSubscriptions?: URI[];
167 > /**
168 > * IETF BCP 47 language tag indicating the client's preferred locale
169 > * (e.g. `"en-US"`, `"ja"`). The server SHOULD use this to localise
170 > * user-facing strings such as confirmation option labels.
171 > */
172 > locale?: string;
173 > /**
174 > * Optional client capability declarations.
175 > *
176 > * Servers SHOULD only advertise features whose corresponding client
177 > * capability is set here. Absent means "not declared" — the server
178 > * MUST assume the client does not support the feature.
179 > */
180 > capabilities?: ClientCapabilities;
181 > }
182 >
183 > /**
184 > * Optional capabilities a client declares during `initialize`.
185 > *
186 > * Each field is a presence flag: an empty object `{}` means "supported",
187 > * absence means "not supported". Sub-fields on individual capabilities
188 > * are reserved for future per-capability options.
189 > *
190 > * @category Commands
191 > */
192 > export interface ClientCapabilities {
193 > /**
194 > * Client can render
195 > * [MCP Apps](https://github.com/modelcontextprotocol/ext-apps) — i.e.
196 > * it can host the View sandbox, run the `ui/*` protocol against it,
197 > * and forward `mcp://`-channel traffic on the App's behalf.
198 > *
199 > * Hosts SHOULD only populate
200 > * {@link McpServerCustomization.mcpApp | `McpServerCustomization.mcpApp`}
201 > * (and expose the corresponding
202 > * {@link McpServerCustomization.channel | `mcp://` channel}) when this
203 > * capability is declared. Clients that omit it MUST treat
204 > * App-bearing tool calls as ordinary MCP tool calls.
205 > */
206 > mcpApps?: Record<string, never>;
207 > }
208 >
209 > /**
210 > * Result of the `initialize` command.
211 > *
212 > * `protocolVersion` is the version the server has selected from the client's
213 > * `protocolVersions` list. The client and server MUST use this version for
214 > * the rest of the connection. If the server cannot speak any of the offered
215 > * versions it MUST return error code `-32005` (`UnsupportedProtocolVersion`)
216 > * instead of a result.
217 > */
218 > export interface InitializeResult {
219 > /**
220 > * Protocol version selected by the server. MUST be one of the entries in
221 > * `InitializeParams.protocolVersions`. Formatted as a [SemVer](https://semver.org)
222 > * `MAJOR.MINOR.PATCH` string (e.g. `"0.1.0"`).
223 > */
224 > protocolVersion: string;
225 > /** Current server sequence number */
226 > serverSeq: number;
227 > /**
228 > * Optional identity of the server implementation (name and version).
229 > * Informational only — see {@link Implementation} for how it may and may not
230 > * be used. Whereas {@link InitializeResult.protocolVersion | `protocolVersion`}
231 > * identifies the negotiated protocol, `serverInfo` identifies the host
232 > * software behind it.
233 > */
234 > serverInfo?: Implementation;
235 > /** Snapshots for each `initialSubscriptions` URI */
236 > snapshots: Snapshot[];
237 > /** Suggested default directory for remote filesystem browsing */
238 > defaultDirectory?: URI;
239 > /**
240 > * Characters that, when typed in a {@link Message} input, SHOULD cause
241 > * the client to issue a `completions` request with
242 > * {@link CompletionItemKind.UserMessage}. Typically includes characters like
243 > * `'@'` or `'/'`.
244 > */
245 > completionTriggerCharacters?: string[];
246 > /**
247 > * Prefix that the host recognizes at the start of a user {@link Message.text}
248 > * as a shorthand for executing the remainder as a terminal command. Currently
249 > * the standardized convention is `"!"`; absence means the host does not
250 > * support command prefixes.
251 > */
252 > terminalCommandPrefix?: string;
253 > /**
254 > * OTLP telemetry channels the host emits, if any. Each populated field is
255 > * either a literal `ahp-otlp:` channel URI or an RFC 6570 URI template a
256 > * client expands before subscribing (currently only the `logs` channel
257 > * defines a template variable, `{level}`, for subscriber-side severity
258 > * filtering). Clients MAY ignore signals they cannot process.
259 > *
260 > * @see {@link /specification/telemetry-channel | Telemetry Channel}
261 > */
262 > telemetry?: TelemetryCapabilities;
263 > }
264 >
265 > // ─── ping ────────────────────────────────────────────────────────────────────
266 >
267 > /**
268 > * Verifies that the AHP connection is still alive and keeps it from being
269 > * closed by idle-timeout intermediaries (proxies, load balancers, etc.).
270 > *
271 > * The server MUST respond regardless of whether the client has completed
272 > * `initialize` or holds any subscriptions. Ping carries no payload in either
273 > * direction; the response itself is the signal.
274 > *
275 > * @category Commands
276 > * @method ping
277 > * @direction Client → Server
278 > * @messageType Request
279 > * @version 1
280 > */
281 > export interface PingParams extends BaseParams {
282 > channel: 'ahp-root://';
283 > }
284 >
285 > // ─── reconnect ───────────────────────────────────────────────────────────────
286 >
287 > /**
288 > * Discriminant for reconnect result types.
289 > *
290 > * @category Commands
291 > */
292 > export const enum ReconnectResultType {
293 > Replay = 'replay',
294 > Snapshot = 'snapshot',
295 > }
296 >
297 > /**
298 > * Re-establishes a dropped connection. The server replays missed actions or
299 > * provides fresh snapshots.
300 > *
301 > * @category Commands
302 > * @method reconnect
303 > * @direction Client → Server
304 > * @messageType Request
305 > * @version 1
306 > * @see {@link /specification/lifecycle | Lifecycle} for details.
307 > */
308 > export interface ReconnectParams extends BaseParams {
309 > channel: 'ahp-root://';
310 > /** Client identifier from the original connection */
311 > clientId: string;
312 > /** Last `serverSeq` the client received */
313 > lastSeenServerSeq: number;
314 > /** URIs the client was subscribed to */
315 > subscriptions: URI[];
316 > }
317 >
318 > /**
319 > * Reconnect result when the server can replay from the requested sequence.
320 > *
321 > * The server MUST include all replayed data in the response.
322 > */
323 > export interface ReconnectReplayResult {
324 > /** Discriminant */
325 > type: ReconnectResultType.Replay;
326 > /** Missed action envelopes since `lastSeenServerSeq` */
327 > actions: ActionEnvelope[];
328 > /**
329 > * URIs from `ReconnectParams.subscriptions` that the server cannot resume.
330 > * This includes resources that no longer exist (e.g. disposed sessions or
331 > * terminals) as well as resources the client is no longer permitted to
332 > * observe. Clients SHOULD drop these from their local subscription set.
333 > */
334 > missing: URI[];
335 > }
336 >
337 > /**
338 > * Reconnect result when the gap exceeds the replay buffer.
339 > */
340 > export interface ReconnectSnapshotResult {
341 > /** Discriminant */
342 > type: ReconnectResultType.Snapshot;
343 > /** Fresh snapshots for each subscription */
344 > snapshots: Snapshot[];
345 > }
346 >
347 > /** Result of the `reconnect` command. */
348 > export type ReconnectResult = ReconnectReplayResult | ReconnectSnapshotResult;
349 >
350 > // ─── subscribe ───────────────────────────────────────────────────────────────
351 >
352 > /**
353 > * Subscribe to a URI-identified channel.
354 > *
355 > * A channel MAY have state associated with it (e.g. root, sessions,
356 > * terminals) or be stateless (pure pub/sub for streaming data). For
357 > * state-bearing channels the result includes a snapshot; for stateless
358 > * channels `snapshot` is omitted.
359 > *
360 > * @category Commands
361 > * @method subscribe
362 > * @direction Client → Server
363 > * @messageType Request
364 > * @version 1
365 > * @see {@link /specification/subscriptions | Subscriptions}
366 > */
367 > export interface SubscribeParams extends BaseParams {
368 > /**
369 > * Optional delivery preferences for this subscription.
370 > *
371 > * Servers MAY use these preferences to buffer and coalesce high-frequency
372 > * updates while preserving the same reduced state. Omit this field for the
373 > * server's default delivery behavior.
374 > */
375 > delivery?: SubscriptionDeliveryOptions;
376 > /**
377 > * Optional client-requested shape for the returned snapshot.
378 > *
379 > * Servers that do not understand a requested view ignore it and return their
380 > * default snapshot. Clients MUST tolerate receiving more state than requested.
381 > */
382 > view?: SubscribeView;
383 > }
384 >
385 > /**
386 > * Optional client-requested shape for a subscription snapshot.
387 > *
388 > * @category Commands
389 > */
390 > export interface SubscribeView {
391 > /**
392 > * Advisory number of most-recent completed turns to expose in a chat
393 > * snapshot.
394 > *
395 > * Servers MAY return more or fewer turns than requested. When omitted, the
396 > * host MUST return all retained turns. When older turns remain available, the
397 > * returned {@link ChatState} carries `turnsNextCursor`; clients pass that
398 > * cursor to `fetchTurns` to ask the host to page more turns into the chat
399 > * state.
400 > */
401 > turns?: number;
402 > }
403 >
404 > /**
405 > * Advisory delivery preferences for a single subscription.
406 > *
407 > * @category Commands
408 > */
409 > export interface SubscriptionDeliveryOptions {
410 > /**
411 > * Maximum time, in milliseconds, that the server may intentionally delay
412 > * delivery while buffering/coalescing updates for this subscription.
413 > *
414 > * A value of `0` requests immediate delivery with no intentional coalescing.
415 > */
416 > maxLatencyMs?: number;
417 > }
418 >
419 > /**
420 > * Result of the `subscribe` command.
421 > *
422 > * `snapshot` is present when the subscribed channel has associated state, and
423 > * absent for stateless channels.
424 > */
425 > export interface SubscribeResult {
426 > /** Snapshot of the subscribed channel's state (omitted for stateless channels) */
427 > snapshot?: Snapshot;
428 > }
429 >
430 > // ─── unsubscribe ─────────────────────────────────────────────────────────────
431 >
432 > /**
433 > * Stop receiving updates for a channel.
434 > *
435 > * @category Commands
436 > * @method unsubscribe
437 > * @direction Client → Server
438 > * @messageType Notification
439 > * @version 1
440 > * @see {@link /specification/subscriptions | Subscriptions}
441 > */
442 > export interface UnsubscribeParams {
443 > /** Channel URI to unsubscribe from */
444 > channel: URI;
445 > }
446 >
447 > // ─── dispatchAction ──────────────────────────────────────────────────────────
448 >
449 > /**
450 > * Fire-and-forget action dispatch (write-ahead). The client applies actions
451 > * optimistically to local state and the server echoes them back as an
452 > * {@link ActionEnvelope} once accepted.
453 > *
454 > * The client → server method is named `dispatchAction`; the server's reply
455 > * arrives on the server → client `action` notification (params:
456 > * {@link ActionEnvelope}).
457 > *
458 > * @category Commands
459 > * @method dispatchAction
460 > * @direction Client → Server
461 > * @messageType Notification
462 > * @version 1
463 > * @see {@link /guide/actions | Actions} for the full list of client-dispatchable actions.
464 > */
465 > export interface DispatchActionParams {
466 > /** Channel URI this action targets */
467 > channel: URI;
468 > /** Client sequence number */
469 > clientSeq: number;
470 > /** The action to dispatch */
471 > action: StateAction;
472 > }
473 >
474 > // ─── resourceRead ────────────────────────────────────────────────────────
475 >
476 > /**
477 > * Encoding of fetched content data.
478 > *
479 > * @category Commands
480 > */
481 > export const enum ContentEncoding {
482 > Base64 = 'base64',
483 > Utf8 = 'utf-8',
484 > }
485 >
486 > /**
487 > * Reads the content of a resource by URI.
488 > *
489 > * Content references keep the state tree small by storing large data (images,
490 > * long tool outputs) by reference rather than inline.
491 > *
492 > * Binary content (images, etc.) MUST use `base64` encoding. Text content MAY
493 > * use `utf-8` encoding.
494 > *
495 > * Like all `resource*` methods, `resourceRead` is symmetrical and MAY be
496 > * sent in either direction. Hosts use it to fetch content from a
497 > * client-published URI (e.g. `virtual://my-client/...` plugins); clients
498 > * use it to read host-side files. The receiver enforces access via the
499 > * same permission/`resourceRequest` flow regardless of which peer initiated.
500 > *
501 > * @category Commands
502 > * @method resourceRead
503 > * @direction Client ↔ Server
504 > * @messageType Request
505 > * @version 1
506 > * @throws `NotFound` (`-32008`) if the URI does not exist.
507 > * @throws `PermissionDenied` (`-32009`) if the client is not permitted to read the URI.
508 > * @example
509 > * ```jsonc
510 > * // Client → Server
511 > * { "jsonrpc": "2.0", "id": 10, "method": "resourceRead",
512 > * "params": { "uri": "ahp-session:/<uuid>/content/img-1" } }
513 > *
514 > * // Server → Client
515 > * { "jsonrpc": "2.0", "id": 10, "result": {
516 > * "data": "iVBORw0KGgo...",
517 > * "encoding": "base64",
518 > * "contentType": "image/png"
519 > * }}
520 > * ```
521 > */
522 > export interface ResourceReadParams extends BaseParams {
523 > channel: 'ahp-root://';
524 > /** Content URI from a `ContentRef` */
525 > uri: string;
526 > /** Preferred encoding for the returned data (default: server-chosen) */
527 > encoding?: ContentEncoding;
528 > }
529 >
530 > /**
531 > * Result of the `resourceRead` command.
532 > *
533 > * The server SHOULD honor the `encoding` requested in the params. If the
534 > * server cannot provide the requested encoding, it MUST fall back to either
535 > * `base64` or `utf-8`.
536 > */
537 > export interface ResourceReadResult {
538 > /** Content encoded as a string */
539 > data: string;
540 > /** How `data` is encoded */
541 > encoding: ContentEncoding;
542 > /** Content type (e.g. `"image/png"`, `"text/plain"`) */
543 > contentType?: string;
544 > }
545 >
546 > // ─── resourceWrite ───────────────────────────────────────────────────────────
547 >
548 > /**
549 > * How {@link ResourceWriteParams.data} is placed within the target file.
550 > *
551 > * Each mode interprets {@link ResourceWriteParams.position} differently:
552 > *
553 > * - `truncate` (default): rooted at the **start** of the file. The file is
554 > * truncated at `position` (0 by default) and `data` is written from that
555 > * offset, so the resulting file is `existing[0..position] + data`. With
556 > * `position` omitted this is a full overwrite.
557 > * - `append`: rooted at the **end** of the file. `position` counts bytes
558 > * backwards from EOF, so `position: 0` (the default) writes at EOF —
559 > * POSIX append — and `position: 5` inserts `data` 5 bytes before the
560 > * current EOF, shifting those trailing 5 bytes after the inserted region.
561 > * The server MUST evaluate the effective EOF and write atomically with
562 > * respect to other appenders so concurrent `append` writes do not
563 > * clobber each other.
564 > * - `insert`: rooted at the **start** of the file. `position` (0 by default)
565 > * is the byte offset at which `data` is spliced in; bytes at or after
566 > * `position` are shifted right by `data.length`. `insert` always grows
567 > * the file — use `truncate` to overwrite bytes in place.
568 > *
569 > * @category Commands
570 > */
571 > export const enum ResourceWriteMode {
572 > Truncate = 'truncate',
573 > Append = 'append',
574 > Insert = 'insert',
575 > }
576 >
577 > /**
578 > * Writes content to a file on the server's filesystem.
579 > *
580 > * Binary content (images, etc.) MUST use `base64` encoding. Text content MAY
581 > * use `utf-8` encoding.
582 > *
583 > * If the file does not exist, it is created. If the file already exists, the
584 > * effect on existing bytes depends on {@link ResourceWriteParams.mode}:
585 > * `truncate` (default) overwrites from the chosen offset onward, `append`
586 > * preserves all existing bytes and adds `data` at a position rooted at EOF,
587 > * and `insert` preserves all existing bytes and splices `data` in at an
588 > * offset rooted at the start of the file.
589 > *
590 > * Like all `resource*` methods, `resourceWrite` is symmetrical and MAY be
591 > * sent in either direction.
592 > *
593 > * @category Commands
594 > * @method resourceWrite
595 > * @direction Client ↔ Server
596 > * @messageType Request
597 > * @version 1
598 > * @throws `NotFound` (`-32008`) if the parent directory does not exist.
599 > * @throws `PermissionDenied` (`-32009`) if the client is not permitted to write to the path.
600 > * @throws `AlreadyExists` (`-32010`) if `createOnly` is set and the file already exists.
601 > * @throws `Conflict` (`-32011`) if `ifMatch` is set and the current `etag` does not match.
602 > * @example
603 > * ```jsonc
604 > * // Client → Server
605 > * { "jsonrpc": "2.0", "id": 11, "method": "resourceWrite",
606 > * "params": { "uri": "file:///workspace/hello.txt", "data": "SGVsbG8=",
607 > * "encoding": "base64", "contentType": "text/plain" } }
608 > *
609 > * // Server → Client
610 > * { "jsonrpc": "2.0", "id": 11, "result": {} }
611 > * ```
612 > */
613 > export interface ResourceWriteParams extends BaseParams {
614 > channel: 'ahp-root://';
615 > /** Target file URI on the server filesystem */
616 > uri: URI;
617 > /** Content encoded as a string */
618 > data: string;
619 > /** How `data` is encoded */
620 > encoding: ContentEncoding;
621 > /** Content type (e.g. `"text/plain"`, `"image/png"`) */
622 > contentType?: string;
623 > /**
624 > * If `true`, the server MUST fail if the file already exists instead of
625 > * overwriting it. Useful for safe creation of new files.
626 > */
627 > createOnly?: boolean;
628 > /**
629 > * How `data` is placed within the target file. Defaults to `'truncate'`
630 > * (full overwrite) when omitted. See {@link ResourceWriteMode} for the
631 > * meaning of each mode and how it interprets {@link position}.
632 > */
633 > mode?: ResourceWriteMode;
634 > /**
635 > * Byte offset interpreted according to {@link mode}. Defaults to `0`.
636 > * - `truncate`: offset from the start of the file at which to truncate
637 > * before writing.
638 > * - `append`: bytes back from EOF at which to insert `data`.
639 > * - `insert`: offset from the start of the file at which to splice in
640 > * `data`.
641 > */
642 > position?: number;
643 > /**
644 > * Optimistic-concurrency token previously returned by
645 > * {@link ResourceResolveResult.etag}. When set, the server MUST fail with
646 > * `Conflict` if the current `etag` does not match — preventing lost
647 > * updates between a `resourceResolve` and a subsequent `resourceWrite`.
648 > */
649 > ifMatch?: string;
650 > }
651 >
652 > /**
653 > * Result of the `resourceWrite` command.
654 > *
655 > * An empty object on success.
656 > */
657 > export interface ResourceWriteResult {
658 > }
659 >
660 > // ─── resourceList ────────────────────────────────────────────────────────
661 >
662 > /**
663 > * Lists directory entries at a file URI on the server's filesystem.
664 > *
665 > * This is intended for remote folder pickers and similar UI that needs to let
666 > * users navigate the server's local filesystem.
667 > *
668 > * The server MUST return success only if the target exists and is a directory.
669 > * If the target does not exist, is not a directory, or cannot be accessed, the
670 > * server MUST return a JSON-RPC error.
671 > *
672 > * Like all `resource*` methods, `resourceList` is symmetrical and MAY be
673 > * sent in either direction.
674 > *
675 > * @category Commands
676 > * @method resourceList
677 > * @direction Client ↔ Server
678 > * @messageType Request
679 > * @version 1
680 > * @throws `NotFound` (`-32008`) if the directory does not exist.
681 > * @throws `PermissionDenied` (`-32009`) if the client is not permitted to browse the directory.
682 > */
683 > export interface ResourceListParams extends BaseParams {
684 > channel: 'ahp-root://';
685 > /** Directory URI on the server filesystem */
686 > uri: URI;
687 > }
688 >
689 > /**
690 > * Directory entry returned by `resourceList`.
691 > */
692 > export interface DirectoryEntry {
693 > /** Base name of the entry */
694 > name: string;
695 > /** Whether the entry is a file or directory */
696 > type: 'file' | 'directory';
697 > }
698 >
699 > /**
700 > * Result of the `resourceList` command.
701 > */
702 > export interface ResourceListResult {
703 > /** Entries directly contained in the requested directory */
704 > entries: DirectoryEntry[];
705 > }
706 >
707 > // ─── resourceCopy ────────────────────────────────────────────────────────────
708 >
709 > /**
710 > * Copies a resource from one URI to another on the server's filesystem.
711 > *
712 > * If the destination already exists, it is overwritten unless `failIfExists`
713 > * is set.
714 > *
715 > * Like all `resource*` methods, `resourceCopy` is symmetrical and MAY be
716 > * sent in either direction.
717 > *
718 > * @category Commands
719 > * @method resourceCopy
720 > * @direction Client ↔ Server
721 > * @messageType Request
722 > * @version 1
723 > * @throws `NotFound` (`-32008`) if the source does not exist.
724 > * @throws `PermissionDenied` (`-32009`) if the client is not permitted to read the source or write to the destination.
725 > * @throws `AlreadyExists` (`-32010`) if `failIfExists` is set and the destination already exists.
726 > */
727 > export interface ResourceCopyParams extends BaseParams {
728 > channel: 'ahp-root://';
729 > /** Source URI to copy from */
730 > source: URI;
731 > /** Destination URI to copy to */
732 > destination: URI;
733 > /**
734 > * If `true`, the server MUST fail if the destination already exists instead
735 > * of overwriting it.
736 > */
737 > failIfExists?: boolean;
738 > }
739 >
740 > /**
741 > * Result of the `resourceCopy` command.
742 > *
743 > * An empty object on success.
744 > */
745 > export interface ResourceCopyResult {
746 > }
747 >
748 > // ─── resourceDelete ──────────────────────────────────────────────────────────
749 >
750 > /**
751 > * Deletes a resource at a URI on the server's filesystem.
752 > *
753 > * Like all `resource*` methods, `resourceDelete` is symmetrical and MAY be
754 > * sent in either direction.
755 > *
756 > * @category Commands
757 > * @method resourceDelete
758 > * @direction Client ↔ Server
759 > * @messageType Request
760 > * @version 1
761 > * @throws `NotFound` (`-32008`) if the resource does not exist.
762 > * @throws `PermissionDenied` (`-32009`) if the client is not permitted to delete the resource.
763 > */
764 > export interface ResourceDeleteParams extends BaseParams {
765 > channel: 'ahp-root://';
766 > /** URI of the resource to delete */
767 > uri: URI;
768 > /**
769 > * If `true` and the target is a directory, delete it and all its contents
770 > * recursively. If `false` (default), deleting a non-empty directory MUST fail.
771 > */
772 > recursive?: boolean;
773 > }
774 >
775 > /**
776 > * Result of the `resourceDelete` command.
777 > *
778 > * An empty object on success.
779 > */
780 > export interface ResourceDeleteResult {
781 > }
782 >
783 > // ─── resourceRequest ─────────────────────────────────────────────────────────
784 >
785 > /**
786 > * Requests permission to access a resource on the receiver's filesystem.
787 > *
788 > * `resourceRequest` is symmetrical and MAY be sent in either direction: a
789 > * client asks the server to grant access to a server-side resource, or a
790 > * server asks the client to grant access to a client-side resource. The
791 > * receiver decides whether to allow, deny, or prompt the user for the
792 > * requested access.
793 > *
794 > * If the receiver denies access, it MUST respond with `PermissionDenied`
795 > * (-32009). The error data MAY include a `ResourceRequestParams` value
796 > * describing the access the caller would need to be granted for the
797 > * operation to succeed; see `PermissionDeniedErrorData` in
798 > * `types/errors.ts`.
799 > *
800 > * After a successful `resourceRequest`, the caller MAY use the corresponding
801 > * `resource*` commands (e.g. `resourceRead`, `resourceWrite`) to perform the
802 > * operation. Receivers MAY rescind access at any time by returning
803 > * `PermissionDenied` on subsequent operations.
804 > *
805 > * Either `read`, `write`, or both SHOULD be set to `true`. A request with
806 > * neither flag set is treated as `read: true` by receivers.
807 > *
808 > * @category Commands
809 > * @method resourceRequest
810 > * @direction Client ↔ Server
811 > * @messageType Request
812 > * @version 1
813 > * @throws `PermissionDenied` (`-32009`) if access is denied.
814 > */
815 > export interface ResourceRequestParams extends BaseParams {
816 > channel: 'ahp-root://';
817 > /**
818 > * Resource URI being requested. Typically a `file:` URI on the receiver's
819 > * filesystem, but any URI scheme that the receiver mediates access to is
820 > * allowed.
821 > */
822 > uri: URI;
823 > /** Whether the caller needs read access to the resource. */
824 > read?: boolean;
825 > /** Whether the caller needs write access to the resource. */
826 > write?: boolean;
827 > }
828 >
829 > /**
830 > * Result of the `resourceRequest` command.
831 > *
832 > * An empty object on success.
833 > */
834 > export interface ResourceRequestResult {
835 > }
836 >
837 > // ─── resourceMove ────────────────────────────────────────────────────────────
838 >
839 > /**
840 > * Moves (renames) a resource from one URI to another on the server's filesystem.
841 > *
842 > * If the destination already exists, it is overwritten unless `failIfExists`
843 > * is set.
844 > *
845 > * Like all `resource*` methods, `resourceMove` is symmetrical and MAY be
846 > * sent in either direction.
847 > *
848 > * @category Commands
849 > * @method resourceMove
850 > * @direction Client ↔ Server
851 > * @messageType Request
852 > * @version 1
853 > * @throws `NotFound` (`-32008`) if the source does not exist.
854 > * @throws `PermissionDenied` (`-32009`) if the client is not permitted to move the resource.
855 > * @throws `AlreadyExists` (`-32010`) if `failIfExists` is set and the destination already exists.
856 > */
857 > export interface ResourceMoveParams extends BaseParams {
858 > channel: 'ahp-root://';
859 > /** Source URI to move from */
860 > source: URI;
861 > /** Destination URI to move to */
862 > destination: URI;
863 > /**
864 > * If `true`, the server MUST fail if the destination already exists instead
865 > * of overwriting it.
866 > */
867 > failIfExists?: boolean;
868 > }
869 >
870 > /**
871 > * Result of the `resourceMove` command.
872 > *
873 > * An empty object on success.
874 > */
875 > export interface ResourceMoveResult {
876 > }
877 >
878 > // ─── resourceResolve ─────────────────────────────────────────────────────────
879 >
880 > /**
881 > * Discriminant for {@link ResourceResolveResult.type}.
882 > *
883 > * @category Commands
884 > */
885 > export const enum ResourceType {
886 > File = 'file',
887 > Directory = 'directory',
888 > Symlink = 'symlink',
889 > }
890 >
891 > /**
892 > * Resolves a resource — the combination of POSIX `stat` and `realpath`.
893 > *
894 > * `resourceResolve` returns metadata about the resource together with its
895 > * canonical URI after symlink resolution. Use this in place of any
896 > * `resourceExists` shim: a missing resource MUST surface as a `NotFound`
897 > * JSON-RPC error rather than a success with a sentinel value. Callers that
898 > * truly need a boolean check should attempt `resourceResolve` and treat
899 > * `NotFound` as "does not exist".
900 > *
901 > * Like all `resource*` methods, `resourceResolve` is symmetrical and MAY be
902 > * sent in either direction.
903 > *
904 > * @category Commands
905 > * @method resourceResolve
906 > * @direction Client ↔ Server
907 > * @messageType Request
908 > * @version 1
909 > * @throws `NotFound` (`-32008`) if the resource does not exist.
910 > * @throws `PermissionDenied` (`-32009`) if the caller is not permitted to stat the URI.
911 > * @example
912 > * ```jsonc
913 > * // Client → Server
914 > * { "jsonrpc": "2.0", "id": 20, "method": "resourceResolve",
915 > * "params": { "channel": "ahp-root://", "uri": "file:///workspace/hello.txt" } }
916 > *
917 > * // Server → Client
918 > * { "jsonrpc": "2.0", "id": 20, "result": {
919 > * "uri": "file:///workspace/hello.txt",
920 > * "type": "file",
921 > * "size": 5,
922 > * "mtime": "2026-01-15T12:34:56.789Z",
923 > * "etag": "W/\"5-abc123\""
924 > * }}
925 > * ```
926 > */
927 > export interface ResourceResolveParams extends BaseParams {
928 > channel: 'ahp-root://';
929 > /** URI to resolve */
930 > uri: URI;
931 > /**
932 > * When `true` (default), follow symlinks and report the metadata of the
933 > * link target — and set `uri` in the result to the canonical (realpath)
934 > * URI. When `false`, stat the link itself (lstat semantics) and report
935 > * `type: 'symlink'`.
936 > */
937 > followSymlinks?: boolean;
938 > }
939 >
940 > /**
941 > * Result of the `resourceResolve` command.
942 > */
943 > export interface ResourceResolveResult {
944 > /**
945 > * Canonical URI after symlink resolution. Equal to the requested URI when
946 > * `followSymlinks` is `false` or the URI does not traverse a symlink.
947 > */
948 > uri: URI;
949 > /** Resource kind. */
950 > type: ResourceType;
951 > /**
952 > * Size in bytes. Omitted for directories when the provider cannot
953 > * cheaply compute it.
954 > */
955 > size?: number;
956 > /** Last-modified time in ISO 8601 format, when known. */
957 > mtime?: string;
958 > /** Creation time in ISO 8601 format, when known. */
959 > ctime?: string;
960 > /** Sniffed MIME type, when known (e.g. `"text/plain"`, `"image/png"`). */
961 > contentType?: string;
962 > /**
963 > * Opaque per-provider version token. When present, pass it as
964 > * {@link ResourceWriteParams.ifMatch} on a subsequent `resourceWrite` to
965 > * detect concurrent modifications.
966 > */
967 > etag?: string;
968 > }
969 >
970 > // ─── resourceMkdir ───────────────────────────────────────────────────────────
971 >
972 > /**
973 > * Creates a directory on the server's filesystem with `mkdir -p` semantics.
974 > *
975 > * The server MUST create any missing parent directories. Creating a
976 > * directory that already exists is a no-op success. If `uri` already
977 > * exists but is **not** a directory, the server MUST fail with
978 > * `AlreadyExists`.
979 > *
980 > * Like all `resource*` methods, `resourceMkdir` is symmetrical and MAY be
981 > * sent in either direction.
982 > *
983 > * @category Commands
984 > * @method resourceMkdir
985 > * @direction Client ↔ Server
986 > * @messageType Request
987 > * @version 1
988 > * @throws `PermissionDenied` (`-32009`) if the caller is not permitted to create the directory.
989 > * @throws `AlreadyExists` (`-32010`) if `uri` already exists as a non-directory.
990 > */
991 > export interface ResourceMkdirParams extends BaseParams {
992 > channel: 'ahp-root://';
993 > /** Directory URI to create (parents created as needed). */
994 > uri: URI;
995 > }
996 >
997 > /**
998 > * Result of the `resourceMkdir` command.
999 > *
1000 > * An empty object on success.
1001 > */
1002 > export interface ResourceMkdirResult {
1003 > }
1004 >
1005 > // ─── authenticate ────────────────────────────────────────────────────────────
1006 >
1007 > /**
1008 > * Pushes a ****** for a protected resource. The `resource` field MUST
1009 > * match a protected-resource identifier the client has discovered from the
1010 > * server — whether declared statically in `AgentInfo.protectedResources`,
1011 > * or discovered dynamically from a live `McpServerAuthRequiredState.resource`
1012 > * or `ToolCallAuthRequiredState.auth.resource` (both surfaced only once the
1013 > * corresponding MCP server or tool call actually challenges for auth).
1014 > * Servers MUST accept any `resource` value they have themselves advertised
1015 > * through one of these three mechanisms.
1016 > *
1017 > * Tokens are delivered using [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750)
1018 > * (****** Usage) semantics. The client obtains the token from the
1019 > * authorization server(s) listed in the resource's metadata and pushes it
1020 > * to the server via this command.
1021 > *
1022 > * @category Commands
1023 > * @method authenticate
1024 > * @direction Client → Server
1025 > * @messageType Request
1026 > * @version 1
1027 > * @see {@link /specification/authentication | Authentication}
1028 > * @example
1029 > * ```jsonc
1030 > * // Client → Server
1031 > * { "jsonrpc": "2.0", "id": 3, "method": "authenticate",
1032 > * "params": { "channel": "ahp-root://", "resource": "https://api.github.com", "token": "gho_xxxx" } }
1033 > *
1034 > * // Server → Client (success)
1035 > * { "jsonrpc": "2.0", "id": 3, "result": {} }
1036 > *
1037 > * // Server → Client (failure — invalid token)
1038 > * { "jsonrpc": "2.0", "id": 3, "error": { "code": -32007, "message": "Invalid token" } }
1039 > * ```
1040 > */
1041 > export interface AuthenticateParams extends BaseParams {
1042 > channel: 'ahp-root://';
1043 > /**
1044 > * The protected resource identifier. MUST match a `resource` value the
1045 > * server has advertised — via `ProtectedResourceMetadata` in
1046 > * `AgentInfo.protectedResources`, or via a live
1047 > * `McpServerAuthRequiredState.resource` / `ToolCallAuthRequiredState.auth.resource`.
1048 > */
1049 > resource: string;
1050 > /** ****** obtained from the resource's authorization server */
1051 > token: string;
1052 > /**
1053 > * OAuth scopes the token grants, when known. Lets the server determine
1054 > * whether a specific challenge — e.g. the `requiredScopes` on a live
1055 > * `McpServerAuthRequiredState` or `ToolCallAuthRequiredState.auth` — is
1056 > * satisfied without decoding the (opaque, server-specific) token itself.
1057 > * Omit when the client doesn't track granted scopes separately from the
1058 > * token.
1059 > */
1060 > scopes?: string[];
1061 > }
1062 >
1063 > /**
1064 > * Result of the `authenticate` command.
1065 > *
1066 > * An empty object on success. If the token is invalid or the resource is
1067 > * unrecognized, the server MUST return a JSON-RPC error (e.g. `AuthRequired`
1068 > * `-32007` or `InvalidParams` `-32602`).
1069 > */
1070 > export interface AuthenticateResult {
1071 > }
src/vs/base/common/event.ts 998 covered LOC · 145 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- event.ts
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 > import { CancelablePromise } from './async.js';
7 > import { CancellationToken } from './cancellation.js';
8 > import { diffSets } from './collections.js';
9 > import { onUnexpectedError } from './errors.js';
10 > import { createSingleCallFunction } from './functional.js';
11 > import { combinedDisposable, Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from './lifecycle.js';
12 > import { LinkedList } from './linkedList.js';
13 > import { IObservable, IObservableWithChange, IObserver } from './observable.js';
14 > import { env } from './process.js';
15 > import { StopWatch } from './stopwatch.js';
16 > import { MicrotaskDelay } from './symbols.js';
17 >
18 >
19 > // -----------------------------------------------------------------------------------------------------------------------
20 > // Uncomment the next line to print warnings whenever an emitter with listeners is disposed. That is a sign of code smell.
21 > // -----------------------------------------------------------------------------------------------------------------------
22 > const _enableDisposeWithListenerWarning = false
23 > // || Boolean("TRUE") // causes a linter warning so that it cannot be pushed
24 > ;
25 >
26 >
27 > // -----------------------------------------------------------------------------------------------------------------------
28 > // Uncomment the next line to print warnings whenever a snapshotted event is used repeatedly without cleanup.
29 > // See https://github.com/microsoft/vscode/issues/142851
30 > // -----------------------------------------------------------------------------------------------------------------------
31 > const _enableSnapshotPotentialLeakWarning = false
32 > // || Boolean("TRUE") // causes a linter warning so that it cannot be pushed
33 > ;
34 >
35 >
36 > const _bufferLeakWarnCountThreshold = 100;
37 > const _bufferLeakWarnTimeThreshold = 60_000; // 1 minute
38 >
39 function _isBufferLeakWarningEnabled(): boolean {
40 return !!env['VSCODE_DEV'];
41 }
42 > event.ts
43 > /**
44 > * An event with zero or one parameters that can be subscribed to. The event is a function itself.
45 > */
46 > export interface Event<T> {
47 > (listener: (e: T) => unknown, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable;
48 > }
49 >
50 > export namespace Event {
51 > export const None: Event<any> = () => Disposable.None;
52 >
53 > function _addLeakageTraceLogic(options: EmitterOptions) {
54 if (_enableSnapshotPotentialLeakWarning) {
55 const { onDidAddListener: origListenerDidAdd } = options;
65 }
66 }
67 > event.ts
68 > /**
69 > * Given an event, returns another event which debounces calls and defers the listeners to a later task via a shared
70 > * `setTimeout`. The event is converted into a signal (`Event<void>`) to avoid additional object creation as a
71 > * result of merging events and to try prevent race conditions that could arise when using related deferred and
72 > * non-deferred events.
73 > *
74 > * This is useful for deferring non-critical work (eg. general UI updates) to ensure it does not block critical work
75 > * (eg. latency of keypress to text rendered).
76 > *
77 > * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
78 > * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
79 > * returned event causes this utility to leak a listener on the original event.
80 > *
81 > * @param event The event source for the new event.
82 > * @param flushOnListenerRemove Whether to fire all debounced events when a listener is removed. If this is not
83 > * specified, some events could go missing. Use this if it's important that all events are processed, even if the
84 > * listener gets disposed before the debounced event fires.
85 > * @param disposable A disposable store to add the new EventEmitter to.
86 > */
87 > export function defer(event: Event<unknown>, flushOnListenerRemove?: boolean, disposable?: DisposableStore): Event<void> {
88 return debounce<unknown, void>(event, () => void 0, 0, undefined, flushOnListenerRemove ?? true, undefined, disposable);
89 }
90 > event.ts
91 > /**
92 > * Given an event, returns another event which only fires once.
93 > *
94 > * @param event The event source for the new event.
95 > */
96 > export function once<T>(event: Event<T>): Event<T> {
97 > return (listener, thisArgs = null, disposables?) => { event.ts
98 > // we need this, in case the event fires during the listener call
99 > let didFire = false;
100 > let result: IDisposable | undefined = undefined;
101 > result = event(e => {
102 > if (didFire) { event.ts
103 return;
104 > } else if (result) { event.ts
105 > result.dispose(); event.ts
106 > } else { event.ts
107 didFire = true;
108 }
109 > event.ts
110 > return listener.call(thisArgs, e);
111 > }, null, disposables); event.ts
112 >
113 > if (didFire) {
114 result.dispose();
115 }
116 > event.ts
117 > return result;
118 > };
119 > }
120 > event.ts
121 > /**
122 > * Given an event, returns another event which only fires once, and only when the condition is met.
123 > *
124 > * @param event The event source for the new event.
125 > */
126 > export function onceIf<T>(event: Event<T>, condition: (e: T) => boolean): Event<T> {
127 return Event.once(Event.filter(event, condition));
128 }
129 > event.ts
130 > /**
131 > * Maps an event of one type into an event of another type using a mapping function, similar to how
132 > * `Array.prototype.map` works.
133 > *
134 > * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
135 > * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
136 > * returned event causes this utility to leak a listener on the original event.
137 > *
138 > * @param event The event source for the new event.
139 > * @param map The mapping function.
140 > * @param disposable A disposable store to add the new EventEmitter to.
141 > */
142 > export function map<I, O>(event: Event<I>, map: (i: I) => O, disposable?: DisposableStore): Event<O> {
143 return snapshot((listener, thisArgs = null, disposables?) => event(i => listener.call(thisArgs, map(i)), null, disposables), disposable);
144 }
145 > event.ts
146 > /**
147 > * Wraps an event in another event that performs some function on the event object before firing.
148 > *
149 > * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
150 > * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
151 > * returned event causes this utility to leak a listener on the original event.
152 > *
153 > * @param event The event source for the new event.
154 > * @param each The function to perform on the event object.
155 > * @param disposable A disposable store to add the new EventEmitter to.
156 > */
157 > export function forEach<I>(event: Event<I>, each: (i: I) => void, disposable?: DisposableStore): Event<I> {
158 return snapshot((listener, thisArgs = null, disposables?) => event(i => { each(i); listener.call(thisArgs, i); }, null, disposables), disposable);
159 }
160 > event.ts
161 > /**
162 > * Wraps an event in another event that fires only when some condition is met.
163 > *
164 > * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
165 > * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
166 > * returned event causes this utility to leak a listener on the original event.
167 > *
168 > * @param event The event source for the new event.
169 > * @param filter The filter function that defines the condition. The event will fire for the object if this function
170 > * returns true.
171 > * @param disposable A disposable store to add the new EventEmitter to.
172 > */
173 > export function filter<T, U>(event: Event<T | U>, filter: (e: T | U) => e is T, disposable?: DisposableStore): Event<T>;
174 > export function filter<T>(event: Event<T>, filter: (e: T) => boolean, disposable?: DisposableStore): Event<T>;
175 > export function filter<T, R>(event: Event<T | R>, filter: (e: T | R) => e is R, disposable?: DisposableStore): Event<R>;
176 > export function filter<T>(event: Event<T>, filter: (e: T) => boolean, disposable?: DisposableStore): Event<T> {
177 return snapshot((listener, thisArgs = null, disposables?) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables), disposable);
178 }
179 > event.ts
180 > /**
181 > * Given an event, returns the same event but typed as `Event<void>`.
182 > */
183 > export function signal<T>(event: Event<T>): Event<void> {
184 return event as Event<any> as Event<void>;
185 }
186 > event.ts
187 > /**
188 > * Given a collection of events, returns a single event which emits whenever any of the provided events emit.
189 > */
190 > export function any<T>(...events: Event<T>[]): Event<T>;
191 > export function any(...events: Event<any>[]): Event<void>;
192 > export function any<T>(...events: Event<T>[]): Event<T> {
193 return (listener, thisArgs = null, disposables?) => {
194 const disposable = combinedDisposable(...events.map(event => event(e => listener.call(thisArgs, e))));
196 };
197 }
198 > event.ts
199 > /**
200 > * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
201 > * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
202 > * returned event causes this utility to leak a listener on the original event.
203 > */
204 > export function reduce<I, O>(event: Event<I>, merge: (last: O | undefined, event: I) => O, initial?: O, disposable?: DisposableStore): Event<O> {
205 let output: O | undefined = initial;
206
210 }, disposable);
211 }
212 > event.ts
213 > function snapshot<T>(event: Event<T>, disposable: DisposableStore | undefined): Event<T> {
214 let listener: IDisposable | undefined;
215
233 return emitter.event;
234 }
235 > event.ts
236 > /**
237 > * Adds the IDisposable to the store if it's set, and returns it. Useful to
238 > * Event function implementation.
239 > */
240 > function addAndReturnDisposable<T extends IDisposable>(d: T, store: DisposableStore | IDisposable[] | undefined): T {
241 if (store instanceof Array) {
242 store.push(d);
246 return d;
247 }
248 > event.ts
249 > /**
250 > * Given an event, creates a new emitter that event that will debounce events based on {@link delay} and give an
251 > * array event object of all events that fired.
252 > *
253 > * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
254 > * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
255 > * returned event causes this utility to leak a listener on the original event.
256 > *
257 > * @param event The original event to debounce.
258 > * @param merge A function that reduces all events into a single event.
259 > * @param delay The number of milliseconds to debounce.
260 > * @param leading Whether to fire a leading event without debouncing.
261 > * @param flushOnListenerRemove Whether to fire all debounced events when a listener is removed. If this is not
262 > * specified, some events could go missing. Use this if it's important that all events are processed, even if the
263 > * listener gets disposed before the debounced event fires.
264 > * @param leakWarningThreshold See {@link EmitterOptions.leakWarningThreshold}.
265 > * @param disposable A disposable store to register the debounce emitter to.
266 > */
267 > export function debounce<T>(event: Event<T>, merge: (last: T | undefined, event: T) => T, delay?: number | typeof MicrotaskDelay, leading?: boolean, flushOnListenerRemove?: boolean, leakWarningThreshold?: number, disposable?: DisposableStore): Event<T>;
268 > export function debounce<I, O>(event: Event<I>, merge: (last: O | undefined, event: I) => O, delay?: number | typeof MicrotaskDelay, leading?: boolean, flushOnListenerRemove?: boolean, leakWarningThreshold?: number, disposable?: DisposableStore): Event<O>;
269 > export function debounce<I, O>(event: Event<I>, merge: (last: O | undefined, event: I) => O, delay: number | typeof MicrotaskDelay = 100, leading = false, flushOnListenerRemove = false, leakWarningThreshold?: number, disposable?: DisposableStore): Event<O> {
270 let subscription: IDisposable;
271 let output: O | undefined = undefined;
330 return emitter.event;
331 }
332 > event.ts
333 > /**
334 > * Debounces an event, firing after some delay (default=0) with an array of all event original objects.
335 > *
336 > * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
337 > * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
338 > * returned event causes this utility to leak a listener on the original event.
339 > *
340 > * @param event The event source for the new event.
341 > * @param delay The number of milliseconds to debounce.
342 > * @param flushOnListenerRemove Whether to fire all debounced events when a listener is removed. If this is not
343 > * specified, some events could go missing. Use this if it's important that all events are processed, even if the
344 > * listener gets disposed before the debounced event fires.
345 > * @param disposable A disposable store to add the new EventEmitter to.
346 > */
347 > export function accumulate<T>(event: Event<T>, delay: number | typeof MicrotaskDelay = 0, flushOnListenerRemove?: boolean, disposable?: DisposableStore): Event<T[]> {
348 return Event.debounce<T, T[]>(event, (last, e) => {
349 if (!last) {
354 }, delay, undefined, flushOnListenerRemove ?? true, undefined, disposable);
355 }
356 > event.ts
357 > /**
358 > * Throttles an event, ensuring the event is fired at most once during the specified delay period.
359 > * Unlike debounce, throttle will fire immediately on the leading edge and/or after the delay on the trailing edge.
360 > *
361 > * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
362 > * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
363 > * returned event causes this utility to leak a listener on the original event.
364 > *
365 > * @param event The event source for the new event.
366 > * @param merge An accumulator function that merges events if multiple occur during the throttle period.
367 > * @param delay The number of milliseconds to throttle.
368 > * @param leading Whether to fire on the leading edge (immediately on first event).
369 > * @param trailing Whether to fire on the trailing edge (after delay with the last value).
370 > * @param leakWarningThreshold See {@link EmitterOptions.leakWarningThreshold}.
371 > * @param disposable A disposable store to register the throttle emitter to.
372 > */
373 > export function throttle<T>(event: Event<T>, merge: (last: T | undefined, event: T) => T, delay?: number | typeof MicrotaskDelay, leading?: boolean, trailing?: boolean, leakWarningThreshold?: number, disposable?: DisposableStore): Event<T>;
374 > export function throttle<I, O>(event: Event<I>, merge: (last: O | undefined, event: I) => O, delay?: number | typeof MicrotaskDelay, leading?: boolean, trailing?: boolean, leakWarningThreshold?: number, disposable?: DisposableStore): Event<O>;
375 > export function throttle<I, O>(event: Event<I>, merge: (last: O | undefined, event: I) => O, delay: number | typeof MicrotaskDelay = 100, leading = true, trailing = true, leakWarningThreshold?: number, disposable?: DisposableStore): Event<O> {
376 let subscription: IDisposable;
377 let output: O | undefined = undefined;
437 return emitter.event;
438 }
439 > event.ts
440 > /**
441 > * Filters an event such that some condition is _not_ met more than once in a row, effectively ensuring duplicate
442 > * event objects from different sources do not fire the same event object.
443 > *
444 > * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
445 > * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
446 > * returned event causes this utility to leak a listener on the original event.
447 > *
448 > * @param event The event source for the new event.
449 > * @param equals The equality condition.
450 > * @param disposable A disposable store to add the new EventEmitter to.
451 > *
452 > * @example
453 > * ```
454 > * // Fire only one time when a single window is opened or focused
455 > * Event.latch(Event.any(onDidOpenWindow, onDidFocusWindow))
456 > * ```
457 > */
458 > export function latch<T>(event: Event<T>, equals: (a: T, b: T) => boolean = (a, b) => a === b, disposable?: DisposableStore): Event<T> {
459 let firstCall = true;
460 let cache: T;
467 }, disposable);
468 }
469 > event.ts
470 > /**
471 > * Splits an event whose parameter is a union type into 2 separate events for each type in the union.
472 > *
473 > * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
474 > * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
475 > * returned event causes this utility to leak a listener on the original event.
476 > *
477 > * @example
478 > * ```
479 > * const event = new EventEmitter<number | undefined>().event;
480 > * const [numberEvent, undefinedEvent] = Event.split(event, isUndefined);
481 > * ```
482 > *
483 > * @param event The event source for the new event.
484 > * @param isT A function that determines what event is of the first type.
485 > * @param disposable A disposable store to add the new EventEmitter to.
486 > */
487 > export function split<T, U>(event: Event<T | U>, isT: (e: T | U) => e is T, disposable?: DisposableStore): [Event<T>, Event<U>] {
488 return [
489 Event.filter(event, isT, disposable),
491 ];
492 }
493 > event.ts
494 > /**
495 > * Buffers an event until it has a listener attached.
496 > *
497 > * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
498 > * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
499 > * returned event causes this utility to leak a listener on the original event.
500 > *
501 > * @param event The event source for the new event.
502 > * @param debugName A name for this buffer, used in leak detection warnings.
503 > * @param flushAfterTimeout Determines whether to flush the buffer after a timeout immediately or after a
504 > * `setTimeout` when the first event listener is added.
505 > * @param _buffer Internal: A source event array used for tests.
506 > *
507 > * @example
508 > * ```
509 > * // Start accumulating events, when the first listener is attached, flush
510 > * // the event after a timeout such that multiple listeners attached before
511 > * // the timeout would receive the event
512 > * this.onInstallExtension = Event.buffer(service.onInstallExtension, 'onInstallExtension', true);
513 > * ```
514 > */
515 > export function buffer<T>(event: Event<T>, debugName: string, flushAfterTimeout = false, _buffer: T[] = [], disposable?: DisposableStore): Event<T> {
516 let buffer: T[] | null = _buffer.slice();
517
600 return emitter.event;
601 }
602 > /** event.ts
603 > * Wraps the event in an {@link IChainableEvent}, allowing a more functional programming style.
604 > *
605 > * @example
606 > * ```
607 > * // Normal
608 > * const onEnterPressNormal = Event.filter(
609 > * Event.map(onKeyPress.event, e => new StandardKeyboardEvent(e)),
610 > * e.keyCode === KeyCode.Enter
611 > * ).event;
612 > *
613 > * // Using chain
614 > * const onEnterPressChain = Event.chain(onKeyPress.event, $ => $
615 > * .map(e => new StandardKeyboardEvent(e))
616 > * .filter(e => e.keyCode === KeyCode.Enter)
617 > * );
618 > * ```
619 > */
620 > export function chain<T, R>(event: Event<T>, sythensize: ($: IChainableSythensis<T>) => IChainableSythensis<R>): Event<R> {
621 const fn: Event<R> = (listener, thisArgs, disposables) => {
622 const cs = sythensize(new ChainableSynthesis()) as ChainableSynthesis;
631 return fn;
632 }
633 > event.ts
634 > const HaltChainable = Symbol('HaltChainable');
635 >
636 > class ChainableSynthesis implements IChainableSythensis<any> {
637 private readonly steps: ((input: any) => unknown)[] = [];
638 > event.ts
639 > map<O>(fn: (i: any) => O): this {
640 this.steps.push(fn);
641 return this;
642 }
643 > event.ts
644 > forEach(fn: (i: any) => void): this {
645 this.steps.push(v => {
646 fn(v);
649 return this;
650 }
651 > event.ts
652 > filter(fn: (e: any) => boolean): this {
653 this.steps.push(v => fn(v) ? v : HaltChainable);
654 return this;
655 }
656 > event.ts
657 > reduce<R>(merge: (last: R | undefined, event: any) => R, initial?: R | undefined): this {
658 let last = initial;
659 this.steps.push(v => {
663 return this;
664 }
665 > event.ts
666 > latch(equals: (a: any, b: any) => boolean = (a, b) => a === b): ChainableSynthesis {
667 let firstCall = true;
668 let cache: any;
676 return this;
677 }
678 > event.ts
679 > public evaluate(value: any) {
680 for (const step of this.steps) {
681 value = step(value);
687 return value;
688 }
689 > } event.ts
690 >
691 > export interface IChainableSythensis<T> {
692 > map<O>(fn: (i: T) => O): IChainableSythensis<O>;
693 > forEach(fn: (i: T) => void): IChainableSythensis<T>;
694 > filter<R extends T>(fn: (e: T) => e is R): IChainableSythensis<R>;
695 > filter(fn: (e: T) => boolean): IChainableSythensis<T>;
696 > reduce<R>(merge: (last: R, event: T) => R, initial: R): IChainableSythensis<R>;
697 > reduce<R>(merge: (last: R | undefined, event: T) => R): IChainableSythensis<R>;
698 > latch(equals?: (a: T, b: T) => boolean): IChainableSythensis<T>;
699 > }
700 >
701 > export interface NodeEventEmitter {
702 > on(event: string | symbol, listener: Function): unknown;
703 > removeListener(event: string | symbol, listener: Function): unknown;
704 > }
705 >
706 > /**
707 > * Creates an {@link Event} from a node event emitter.
708 > */
709 > export function fromNodeEventEmitter<T>(emitter: NodeEventEmitter, eventName: string, map: (...args: any[]) => T = id => id): Event<T> {
710 const fn = (...args: unknown[]) => result.fire(map(...args));
711 const onFirstListenerAdd = () => emitter.on(eventName, fn);
715 return result.event;
716 }
717 > event.ts
718 > export interface DOMEventEmitter {
719 > addEventListener(event: string | symbol, listener: Function): void;
720 > removeEventListener(event: string | symbol, listener: Function): void;
721 > }
722 >
723 > /**
724 > * Creates an {@link Event} from a DOM event emitter.
725 > */
726 > export function fromDOMEventEmitter<T>(emitter: DOMEventEmitter, eventName: string, map: (...args: any[]) => T = id => id): Event<T> {
727 const fn = (...args: unknown[]) => result.fire(map(...args));
728 const onFirstListenerAdd = () => emitter.addEventListener(eventName, fn);
732 return result.event;
733 }
734 > event.ts
735 > /**
736 > * Creates a promise out of an event, using the {@link Event.once} helper.
737 > */
738 > export function toPromise<T>(event: Event<T>, disposables?: IDisposable[] | DisposableStore): CancelablePromise<T> {
739 let cancelRef: () => void;
740 let listener: IDisposable;
756 return promise;
757 }
758 > event.ts
759 > /**
760 > * A convenience function for forwarding an event to another emitter which
761 > * improves readability.
762 > *
763 > * This is similar to {@link Relay} but allows instantiating and forwarding
764 > * on a single line and also allows for multiple source events.
765 > * @param from The event to forward.
766 > * @param to The emitter to forward the event to.
767 > * @example
768 > * Event.forward(event, emitter);
769 > * // equivalent to
770 > * event(e => emitter.fire(e));
771 > * // equivalent to
772 > * event(emitter.fire, emitter);
773 > */
774 > export function forward<T>(from: Event<T>, to: Emitter<T>): IDisposable {
775 return from(e => to.fire(e));
776 }
777 > event.ts
778 > /**
779 > * Adds a listener to an event and calls the listener immediately with undefined as the event object.
780 > *
781 > * @example
782 > * ```
783 > * // Initialize the UI and update it when dataChangeEvent fires
784 > * runAndSubscribe(dataChangeEvent, () => this._updateUI());
785 > * ```
786 > */
787 > export function runAndSubscribe<T>(event: Event<T>, handler: (e: T) => unknown, initial: T): IDisposable;
788 > export function runAndSubscribe<T>(event: Event<T>, handler: (e: T | undefined) => unknown): IDisposable;
789 > export function runAndSubscribe<T>(event: Event<T>, handler: (e: T | undefined) => unknown, initial?: T): IDisposable {
790 handler(initial);
791 return event(e => handler(e));
792 }
793 > event.ts
794 > class EmitterObserver<T> implements IObserver {
795 >
796 > readonly emitter: Emitter<T>;
797 >
798 > private _counter = 0;
799 > private _hasChanged = false;
800 >
801 > constructor(readonly _observable: IObservable<T>, store: DisposableStore | undefined) {
802 const options: EmitterOptions = {
803 onWillAddFirstListener: () => {
819 }
820 }
821 > event.ts
822 > beginUpdate<T>(_observable: IObservable<T>): void {
823 // assert(_observable === this.obs);
824 this._counter++;
825 }
826 > event.ts
827 > handlePossibleChange<T>(_observable: IObservable<T>): void {
828 // assert(_observable === this.obs);
829 }
830 > event.ts
831 > handleChange<T, TChange>(_observable: IObservableWithChange<T, TChange>, _change: TChange): void {
832 // assert(_observable === this.obs);
833 this._hasChanged = true;
834 }
835 > event.ts
836 > endUpdate<T>(_observable: IObservable<T>): void {
837 // assert(_observable === this.obs);
838 this._counter--;
845 }
846 }
847 > } event.ts
848 >
849 > /**
850 > * Creates an event emitter that is fired when the observable changes.
851 > * Each listeners subscribes to the emitter.
852 > */
853 > export function fromObservable<T>(obs: IObservable<T>, store?: DisposableStore): Event<T> {
854 const observer = new EmitterObserver(obs, store);
855 return observer.emitter.event;
856 }
857 > event.ts
858 > /**
859 > * Each listener is attached to the observable directly.
860 > */
861 > export function fromObservableLight(observable: IObservable<unknown>): Event<void> {
862 return (listener, thisArgs, disposables) => {
863 let count = 0;
897 };
898 }
899 > } event.ts
900 >
901 > export interface EmitterOptions {
902 > /**
903 > * Optional function that's called *before* the very first listener is added
904 > */
905 > onWillAddFirstListener?: Function;
906 > /**
907 > * Optional function that's called *after* the very first listener is added
908 > */
909 > onDidAddFirstListener?: Function;
910 > /**
911 > * Optional function that's called after a listener is added
912 > */
913 > onDidAddListener?: Function;
914 > /**
915 > * Optional function that's called *after* remove the very last listener
916 > */
917 > onDidRemoveLastListener?: Function;
918 > /**
919 > * Optional function that's called *before* a listener is removed
920 > */
921 > onWillRemoveListener?: Function;
922 > /**
923 > * Optional function that's called when a listener throws an error. Defaults to
924 > * {@link onUnexpectedError}
925 > */
926 > onListenerError?: (e: any) => void;
927 > /**
928 > * Number of listeners that are allowed before assuming a leak. Default to
929 > * a globally configured value
930 > *
931 > * @see setGlobalLeakWarningThreshold
932 > */
933 > leakWarningThreshold?: number;
934 > /**
935 > * Human-readable name for the emitter, included in leak warning error
936 > * messages to help identify which emitter is leaking in telemetry.
937 > */
938 > leakWarningName?: string;
939 > /**
940 > * Pass in a delivery queue, which is useful for ensuring
941 > * in order event delivery across multiple emitters.
942 > */
943 > deliveryQueue?: EventDeliveryQueue;
944 >
945 > /** ONLY enable this during development */
946 > _profName?: string;
947 > }
948 >
949 >
950 > export class EventProfiling {
951 >
952 > static readonly all = new Set<EventProfiling>();
953 >
954 > private static _idPool = 0;
955 >
956 > readonly name: string;
957 > public listenerCount: number = 0;
958 > public invocationCount = 0;
959 > public elapsedOverall = 0;
960 > public durations: number[] = [];
961 >
962 > private _stopWatch?: StopWatch;
963 >
964 > constructor(name: string) {
965 this.name = `${name}_${EventProfiling._idPool++}`;
966 EventProfiling.all.add(this);
967 }
968 > event.ts
969 > start(listenerCount: number): void {
970 this._stopWatch = new StopWatch();
971 this.listenerCount = listenerCount;
972 }
973 > event.ts
974 > stop(): void {
975 if (this._stopWatch) {
976 const elapsed = this._stopWatch.elapsed();
981 }
982 }
983 > } event.ts
984 >
985 > let _globalLeakWarningThreshold = -1;
986 > export function setGlobalLeakWarningThreshold(n: number): IDisposable {
987 const oldValue = _globalLeakWarningThreshold;
988 _globalLeakWarningThreshold = n;
993 };
994 }
995 > event.ts
996 > class LeakageMonitor {
997 >
998 > private static _idPool = 1;
999 >
1000 > private _stacks: Map<string, number> | undefined;
1001 > private _warnCountdown: number = 0;
1002 >
1003 > constructor(
1004 private readonly _errorHandler: (err: Error) => void,
1005 readonly threshold: number,
1006 readonly name: string = (LeakageMonitor._idPool++).toString(16).padStart(3, '0')
1007 ) { }
1008 > event.ts
1009 > dispose(): void {
1010 this._stacks?.clear();
1011 }
1012 > event.ts
1013 > check(stack: Stacktrace, listenerCount: number): undefined | (() => void) {
1014
1015 const threshold = this.threshold;
1046 };
1047 }
1048 > event.ts
1049 > getMostFrequentStack(): [string, number] | undefined {
1050 if (!this._stacks) {
1051 return undefined;
1061 return topStack;
1062 }
1063 > } event.ts
1064 >
1065 > class Stacktrace {
1066 >
1067 > static create() {
1068 > const err = new Error();
1069 > return new Stacktrace(err.stack ?? '');
1070 > }
1071 >
1072 > private constructor(readonly value: string) { }
1073 >
1074 > print() {
1075 console.warn(this.value.split('\n').slice(2).join('\n'));
1076 }
1077 > } event.ts
1078 >
1079 > // error that is logged when going over the configured listener threshold
1080 > export class ListenerLeakError extends Error {
1081 > readonly kind: string;
1082 > readonly listenerCount: number;
1083 > /**
1084 > * The detailed message including listener count and most frequent stack.
1085 > * Available locally for debugging but intentionally not used as the error
1086 > * `message`. When `emitterName` is provided, errors group by emitter name
1087 > * and kind in telemetry; otherwise they group by kind alone.
1088 > */
1089 > readonly details: string;
1090 > constructor(kind: 'dominated' | 'popular', details: string, stack: string, listenerCount: number, emitterName?: string) {
1091 super(emitterName
1092 ? `[${emitterName}] potential listener LEAK detected, ${kind}`
1098 this.stack = stack;
1099 }
1100 > event.ts
1101 > static is(err: unknown): err is ListenerLeakError {
1102 return err instanceof ListenerLeakError
1103 || (err instanceof Error && typeof (err as Error & { kind: unknown; listenerCount: unknown }).kind === 'string' && typeof (err as Error & { kind: unknown; listenerCount: unknown }).listenerCount === 'number');
1104 }
1105 > } event.ts
1106 >
1107 > // SEVERE error that is logged when having gone way over the configured listener
1108 > // threshold so that the emitter refuses to accept more listeners
1109 > export class ListenerRefusalError extends ListenerLeakError {
1110 > constructor(kind: 'dominated' | 'popular', details: string, stack: string, listenerCount: number, emitterName?: string) {
1111 super(kind, details, stack, listenerCount, emitterName);
1112 this.name = 'ListenerRefusalError';
1113 }
1114 > } event.ts
1115 >
1116 > let id = 0;
1117 > class UniqueContainer<T> {
1118 > stack?: Stacktrace;
1119 > public id = id++;
1120 > constructor(public readonly value: T) { }
1121 > }
1122 > const compactionThreshold = 2;
1123 >
1124 > type ListenerContainer<T> = UniqueContainer<(data: T) => void>;
1125 > type ListenerOrListeners<T> = (ListenerContainer<T> | undefined)[] | ListenerContainer<T>;
1126 >
1127 > const forEachListener = <T>(listeners: ListenerOrListeners<T>, fn: (c: ListenerContainer<T>) => void) => {
1128 if (listeners instanceof UniqueContainer) {
1129 fn(listeners);
1137 }
1138 };
1139 > event.ts
1140 > /**
1141 > * The Emitter can be used to expose an Event to the public
1142 > * to fire it from the insides.
1143 > * Sample:
1144 > class Document {
1145 >
1146 > private readonly _onDidChange = new Emitter<(value:string)=>any>();
1147 >
1148 > public onDidChange = this._onDidChange.event;
1149 >
1150 > // getter-style
1151 > // get onDidChange(): Event<(value:string)=>any> {
1152 > // return this._onDidChange.event;
1153 > // }
1154 >
1155 > private _doIt() {
1156 > //...
1157 > this._onDidChange.fire(value);
1158 > }
1159 > }
1160 > */
1161 > export class Emitter<T> {
1162 >
1163 > private readonly _options?: EmitterOptions;
1164 > private readonly _leakageMon?: LeakageMonitor;
1165 > private readonly _perfMon?: EventProfiling;
1166 > private _disposed?: true;
1167 > private _event?: Event<T>;
1168 >
1169 > /**
1170 > * A listener, or list of listeners. A single listener is the most common
1171 > * for event emitters (#185789), so we optimize that special case to avoid
1172 > * wrapping it in an array (just like Node.js itself.)
1173 > *
1174 > * A list of listeners never 'downgrades' back to a plain function if
1175 > * listeners are removed, for two reasons:
1176 > *
1177 > * 1. That's complicated (especially with the deliveryQueue)
1178 > * 2. A listener with >1 listener is likely to have >1 listener again at
1179 > * some point, and swapping between arrays and functions may[citation needed]
1180 > * introduce unnecessary work and garbage.
1181 > *
1182 > * The array listeners can be 'sparse', to avoid reallocating the array
1183 > * whenever any listener is added or removed. If more than `1 / compactionThreshold`
1184 > * of the array is empty, only then is it resized.
1185 > */
1186 > protected _listeners?: ListenerOrListeners<T>;
1187 >
1188 > /**
1189 > * Always to be defined if _listeners is an array. It's no longer a true
1190 > * queue, but holds the dispatching 'state'. If `fire()` is called on an
1191 > * emitter, any work left in the _deliveryQueue is finished first.
1192 > */
1193 > private _deliveryQueue?: EventDeliveryQueuePrivate;
1194 > protected _size = 0;
1195 >
1196 > constructor(options?: EmitterOptions) {
1197 > this._options = options; event.ts
1198 > this._leakageMon = (_globalLeakWarningThreshold > 0 || this._options?.leakWarningThreshold)
1199 ? new LeakageMonitor(options?.onListenerError ?? onUnexpectedError, this._options?.leakWarningThreshold ?? _globalLeakWarningThreshold, this._options?.leakWarningName) :
1200 > undefined; event.ts
1201 > this._perfMon = this._options?._profName ? new EventProfiling(this._options._profName) : undefined;
1202 > this._deliveryQueue = this._options?.deliveryQueue as EventDeliveryQueuePrivate | undefined;
1203 > }
1204 > event.ts
1205 > dispose() {
1206 > if (!this._disposed) { event.ts
1207 > this._disposed = true;
1208 >
1209 > // It is bad to have listeners at the time of disposing an emitter, it is worst to have listeners keep the emitter
1210 > // alive via the reference that's embedded in their disposables. Therefore we loop over all remaining listeners and
1211 > // unset their subscriptions/disposables. Looping and blaming remaining listeners is done on next tick because the
1212 > // the following programming pattern is very popular:
1213 > //
1214 > // const someModel = this._disposables.add(new ModelObject()); // (1) create and register model
1215 > // this._disposables.add(someModel.onDidChange(() => { ... }); // (2) subscribe and register model-event listener
1216 > // ...later...
1217 > // this._disposables.dispose(); disposes (1) then (2): don't warn after (1) but after the "overall dispose" is done
1218 >
1219 > if (this._deliveryQueue?.current === this) {
1220 this._deliveryQueue.reset();
1221 }
1222 > if (this._listeners) { event.ts
1223 > if (_enableDisposeWithListenerWarning) { event.ts
1224 const listeners = this._listeners;
1225 queueMicrotask(() => {
1227 });
1228 }
1229 > event.ts
1230 > this._listeners = undefined;
1231 > this._size = 0;
1232 > }
1233 > this._options?.onDidRemoveLastListener?.(); event.ts
1234 > this._leakageMon?.dispose();
1235 > }
1236 > }
1237 > event.ts
1238 > /**
1239 > * For the public to allow to subscribe
1240 > * to events from this Emitter
1241 > */
1242 > get event(): Event<T> {
1243 > this._event ??= (callback: (e: T) => unknown, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => { event.ts
1244 > if (this._leakageMon && this._size > this._leakageMon.threshold ** 2) { event.ts
1245 const message = `[${this._leakageMon.name}] REFUSES to accept new listeners because it exceeded its threshold by far (${this._size} vs ${this._leakageMon.threshold})`;
1246 console.warn(message);
1254 return Disposable.None;
1255 }
1256 > event.ts
1257 > if (this._disposed) {
1258 // todo: should we warn if a listener is added to a disposed emitter? This happens often
1259 return Disposable.None;
1260 }
1261 > event.ts
1262 > if (thisArgs) {
1263 callback = callback.bind(thisArgs);
1264 }
1265 > event.ts
1266 > const contained = new UniqueContainer(callback);
1267 >
1268 > let removeMonitor: Function | undefined;
1269 > let stack: Stacktrace | undefined;
1270 > if (this._leakageMon && this._size >= Math.ceil(this._leakageMon.threshold * 0.2)) {
1271 // check and record this emitter for potential leakage
1272 contained.stack = Stacktrace.create();
1273 removeMonitor = this._leakageMon.check(contained.stack, this._size + 1);
1274 }
1275 > event.ts
1276 > if (_enableDisposeWithListenerWarning) {
1277 contained.stack = stack ?? Stacktrace.create();
1278 }
1279 > event.ts
1280 > if (!this._listeners) {
1281 > this._options?.onWillAddFirstListener?.(this);
1282 > this._listeners = contained;
1283 > this._options?.onDidAddFirstListener?.(this);
1284 > } else if (this._listeners instanceof UniqueContainer) {
1285 > this._deliveryQueue ??= new EventDeliveryQueuePrivate(); event.ts
1286 > this._listeners = [this._listeners, contained];
1287 > } else {
1288 > this._listeners.push(contained); event.ts
1289 > }
1290 > this._options?.onDidAddListener?.(this); event.ts
1291 >
1292 > this._size++;
1293 >
1294 >
1295 > const result = toDisposable(() => {
1296 > removeMonitor?.(); event.ts
1297 > this._removeListener(contained);
1298 > }); event.ts
1299 > addToDisposables(result, disposables);
1300 >
1301 > return result;
1302 > };
1303 > event.ts
1304 > return this._event;
1305 > }
1306 > event.ts
1307 > private _removeListener(listener: ListenerContainer<T>) {
1308 > this._options?.onWillRemoveListener?.(this); event.ts
1309 >
1310 > if (!this._listeners) {
1311 > return; // expected if a listener gets disposed event.ts
1312 > }
1313 > event.ts
1314 > if (this._size === 1) {
1315 > this._listeners = undefined; event.ts
1316 > this._options?.onDidRemoveLastListener?.(this);
1317 > this._size = 0;
1318 > return;
1319 > }
1320
1321 // size > 1 which requires that listeners be a list:
1348 listeners.length = n;
1349 }
1350 > } event.ts
1351 > event.ts
1352 > private _deliver(listener: undefined | UniqueContainer<(value: T) => void>, value: T) {
1353 > if (!listener) { event.ts
1354 return;
1355 }
1356 > event.ts
1357 > const errorHandler = this._options?.onListenerError || onUnexpectedError;
1358 > if (!errorHandler) {
1359 listener.value(value);
1360 return;
1361 }
1362 > event.ts
1363 > try {
1364 > listener.value(value);
1365 > } catch (e) {
1366 errorHandler(e);
1367 }
1368 > } event.ts
1369 > event.ts
1370 > /** Delivers items in the queue. Assumes the queue is ready to go. */
1371 > private _deliverQueue(dq: EventDeliveryQueuePrivate) {
1372 > const listeners = dq.current!._listeners! as (ListenerContainer<T> | undefined)[]; event.ts
1373 > while (dq.i < dq.end) {
1374 > // important: dq.i is incremented before calling deliver() because it might reenter deliverQueue()
1375 > this._deliver(listeners[dq.i++], dq.value as T);
1376 > }
1377 > dq.reset();
1378 > }
1379 > event.ts
1380 > /**
1381 > * To be kept private to fire an event to
1382 > * subscribers
1383 > */
1384 > fire(event: T): void {
1385 > if (this._deliveryQueue?.current) { event.ts
1386 this._deliverQueue(this._deliveryQueue);
1387 this._perfMon?.stop(); // last fire() will have starting perfmon, stop it before starting the next dispatch
1388 }
1389 > event.ts
1390 > this._perfMon?.start(this._size);
1391 >
1392 > if (!this._listeners) {
1393 > // no-op event.ts
1394 > } else if (this._listeners instanceof UniqueContainer) { event.ts
1395 > this._deliver(this._listeners, event); event.ts
1396 > } else { event.ts
1397 > const dq = this._deliveryQueue!; event.ts
1398 > dq.enqueue(this, event, this._listeners.length);
1399 > this._deliverQueue(dq);
1400 > }
1401 > event.ts
1402 > this._perfMon?.stop();
1403 > }
1404 > event.ts
1405 > hasListeners(): boolean {
1406 return this._size > 0;
1407 }
1408 > } event.ts
1409 >
1410 > export interface EventDeliveryQueue {
1411 > _isEventDeliveryQueue: true;
1412 > }
1413 >
1414 > export const createEventDeliveryQueue = (): EventDeliveryQueue => new EventDeliveryQueuePrivate();
1415 >
1416 > class EventDeliveryQueuePrivate implements EventDeliveryQueue { event.ts
1417 > declare _isEventDeliveryQueue: true;
1418 >
1419 > /**
1420 > * Index in current's listener list.
1421 > */
1422 > public i = -1;
1423 >
1424 > /**
1425 > * The last index in the listener's list to deliver.
1426 > */
1427 > public end = 0;
1428 > event.ts
1429 > /**
1430 > * Emitter currently being dispatched on. Emitter._listeners is always an array.
1431 > */
1432 > public current?: Emitter<any>;
1433 > /**
1434 > * Currently emitting value. Defined whenever `current` is.
1435 > */
1436 > public value?: unknown;
1437 >
1438 > public enqueue<T>(emitter: Emitter<T>, value: T, end: number) {
1439 > this.i = 0; event.ts
1440 > this.end = end;
1441 > this.current = emitter;
1442 > this.value = value;
1443 > }
1444 > event.ts
1445 > public reset() {
1446 > this.i = this.end; // force any current emission loop to stop, mainly for during dispose event.ts
1447 > this.current = undefined;
1448 > this.value = undefined;
1449 > }
1450 > } event.ts
1451 >
1452 > export interface IWaitUntil {
1453 > token: CancellationToken;
1454 > waitUntil(thenable: Promise<unknown>): void;
1455 > }
1456 >
1457 > export type IWaitUntilData<T> = Omit<Omit<T, 'waitUntil'>, 'token'>;
1458 >
1459 > export class AsyncEmitter<T extends IWaitUntil> extends Emitter<T> {
1460 >
1461 > private _asyncDeliveryQueue?: LinkedList<[(ev: T) => void, IWaitUntilData<T>]>;
1462 >
1463 > async fireAsync(data: IWaitUntilData<T>, token: CancellationToken, promiseJoin?: (p: Promise<unknown>, listener: Function) => Promise<unknown>): Promise<void> {
1464 if (!this._listeners) {
1465 return;
1512 }
1513 }
1514 > } event.ts
1515 >
1516 >
1517 > export class PauseableEmitter<T> extends Emitter<T> {
1518 >
1519 > private _isPaused = 0;
1520 > protected _eventQueue = new LinkedList<T>();
1521 > private _mergeFn?: (input: T[]) => T;
1522 >
1523 > public get isPaused(): boolean {
1524 > return this._isPaused !== 0;
1525 > }
1526 >
1527 > constructor(options?: EmitterOptions & { merge?: (input: T[]) => T }) {
1528 super(options);
1529 this._mergeFn = options?.merge;
1530 }
1531 > event.ts
1532 > pause(): void {
1533 this._isPaused++;
1534 }
1535 > event.ts
1536 > resume(): void {
1537 if (this._isPaused !== 0 && --this._isPaused === 0) {
1538 if (this._mergeFn) {
1554 }
1555 }
1556 > event.ts
1557 > override fire(event: T): void {
1558 if (this._size) {
1559 if (this._isPaused !== 0) {
1564 }
1565 }
1566 > } event.ts
1567 >
1568 > export class DebounceEmitter<T> extends PauseableEmitter<T> {
1569 >
1570 > private readonly _delay: number;
1571 > private _handle: Timeout | undefined;
1572 >
1573 > constructor(options: EmitterOptions & { merge: (input: T[]) => T; delay?: number }) {
1574 super(options);
1575 this._delay = options.delay ?? 100;
1576 }
1577 > event.ts
1578 > override fire(event: T): void {
1579 if (!this._handle) {
1580 this.pause();
1586 super.fire(event);
1587 }
1588 > } event.ts
1589 >
1590 > /**
1591 > * An emitter which queue all events and then process them at the
1592 > * end of the event loop.
1593 > */
1594 > export class MicrotaskEmitter<T> extends Emitter<T> {
1595 > private _queuedEvents: T[] = [];
1596 > private _mergeFn?: (input: T[]) => T;
1597 >
1598 > constructor(options?: EmitterOptions & { merge?: (input: T[]) => T }) {
1599 super(options);
1600 this._mergeFn = options?.merge;
1601 }
1602 > override fire(event: T): void { event.ts
1603
1604 if (!this.hasListeners()) {
1618 }
1619 }
1620 > } event.ts
1621 >
1622 > /**
1623 > * An event emitter that multiplexes many events into a single event.
1624 > *
1625 > * @example Listen to the `onData` event of all `Thing`s, dynamically adding and removing `Thing`s
1626 > * to the multiplexer as needed.
1627 > *
1628 > * ```typescript
1629 > * const anythingDataMultiplexer = new EventMultiplexer<{ data: string }>();
1630 > *
1631 > * const thingListeners = DisposableMap<Thing, IDisposable>();
1632 > *
1633 > * thingService.onDidAddThing(thing => {
1634 > * thingListeners.set(thing, anythingDataMultiplexer.add(thing.onData);
1635 > * });
1636 > * thingService.onDidRemoveThing(thing => {
1637 > * thingListeners.deleteAndDispose(thing);
1638 > * });
1639 > *
1640 > * anythingDataMultiplexer.event(e => {
1641 > * console.log('Something fired data ' + e.data)
1642 > * });
1643 > * ```
1644 > */
1645 > export class EventMultiplexer<T> implements IDisposable {
1646 >
1647 > private readonly emitter: Emitter<T>;
1648 > private hasListeners = false;
1649 > private events: { event: Event<T>; listener: IDisposable | null }[] = [];
1650 >
1651 > constructor() {
1652 this.emitter = new Emitter<T>({
1653 onWillAddFirstListener: () => this.onFirstListenerAdd(),
1655 });
1656 }
1657 > event.ts
1658 > get event(): Event<T> {
1659 return this.emitter.event;
1660 }
1661 > event.ts
1662 > add(event: Event<T>): IDisposable {
1663 const e = { event: event, listener: null };
1664 this.events.push(e);
1679 return toDisposable(createSingleCallFunction(dispose));
1680 }
1681 > event.ts
1682 > private onFirstListenerAdd(): void {
1683 this.hasListeners = true;
1684 this.events.forEach(e => this.hook(e));
1685 }
1686 > event.ts
1687 > private onLastListenerRemove(): void {
1688 this.hasListeners = false;
1689 this.events.forEach(e => this.unhook(e));
1690 }
1691 > event.ts
1692 > private hook(e: { event: Event<T>; listener: IDisposable | null }): void {
1693 e.listener = e.event(r => this.emitter.fire(r));
1694 }
1695 > event.ts
1696 > private unhook(e: { event: Event<T>; listener: IDisposable | null }): void {
1697 e.listener?.dispose();
1698 e.listener = null;
1699 }
1700 > event.ts
1701 > dispose(): void {
1702 this.emitter.dispose();
1703
1707 this.events = [];
1708 }
1709 > } event.ts
1710 >
1711 > export interface IDynamicListEventMultiplexer<TEventType> extends IDisposable {
1712 > readonly event: Event<TEventType>;
1713 > }
1714 > export class DynamicListEventMultiplexer<TItem, TEventType> implements IDynamicListEventMultiplexer<TEventType> {
1715 > private readonly _store = new DisposableStore();
1716 >
1717 > readonly event: Event<TEventType>;
1718 >
1719 > constructor(
1720 items: TItem[],
1721 onAddItem: Event<TItem>,
1747 this.event = multiplexer.event;
1748 }
1749 > event.ts
1750 > dispose() {
1751 this._store.dispose();
1752 }
1753 > } event.ts
1754 >
1755 > /**
1756 > * The EventBufferer is useful in situations in which you want
1757 > * to delay firing your events during some code.
1758 > * You can wrap that code and be sure that the event will not
1759 > * be fired during that wrap.
1760 > *
1761 > * ```
1762 > * const emitter: Emitter;
1763 > * const delayer = new EventDelayer();
1764 > * const delayedEvent = delayer.wrapEvent(emitter.event);
1765 > *
1766 > * delayedEvent(console.log);
1767 > *
1768 > * delayer.bufferEvents(() => {
1769 > * emitter.fire(); // event will not be fired yet
1770 > * });
1771 > *
1772 > * // event will only be fired at this point
1773 > * ```
1774 > */
1775 > export class EventBufferer {
1776
1777 private data: { buffers: Function[] }[] = [];
1778 > event.ts
1779 > wrapEvent<T>(event: Event<T>): Event<T>;
1780 > wrapEvent<T>(event: Event<T>, reduce: (last: T | undefined, event: T) => T): Event<T>;
1781 > wrapEvent<T, O>(event: Event<T>, reduce: (last: O | undefined, event: T) => O, initial: O): Event<O>;
1782 > wrapEvent<T, O>(event: Event<T>, reduce?: (last: T | O | undefined, event: T) => T | O, initial?: O): Event<O | T> {
1783 return (listener, thisArgs?, disposables?) => {
1784 return event(i => {
1832 };
1833 }
1834 > event.ts
1835 > bufferEvents<R = void>(fn: () => R): R {
1836 const data = { buffers: new Array<Function>() };
1837 this.data.push(data);
1841 return r;
1842 }
1843 > } event.ts
1844 >
1845 > /**
1846 > * A Relay is an event forwarder which functions as a replugabble event pipe.
1847 > * Once created, you can connect an input event to it and it will simply forward
1848 > * events from that input event through its own `event` property. The `input`
1849 > * can be changed at any point in time.
1850 > */
1851 > export class Relay<T> implements IDisposable {
1852
1853 private listening = false;
1867
1868 readonly event: Event<T> = this.emitter.event;
1869 > event.ts
1870 > set input(event: Event<T>) {
1871 this.inputEvent = event;
1872
1876 }
1877 }
1878 > event.ts
1879 > dispose() {
1880 this.inputEventListener.dispose();
1881 this.emitter.dispose();
1882 }
1883 > } event.ts
1884 >
1885 > export interface IValueWithChangeEvent<T> {
1886 > readonly onDidChange: Event<void>;
1887 > get value(): T;
1888 > }
1889 >
1890 > export class ValueWithChangeEvent<T> implements IValueWithChangeEvent<T> {
1891 > public static const<T>(value: T): IValueWithChangeEvent<T> {
1892 > return new ConstValueWithChangeEvent(value);
1893 > }
1894 >
1895 > private readonly _onDidChange = new Emitter<void>();
1896 > readonly onDidChange: Event<void> = this._onDidChange.event;
1897 >
1898 > constructor(private _value: T) { }
1899 >
1900 > get value(): T {
1901 return this._value;
1902 }
1903 > event.ts
1904 > set value(value: T) {
1905 if (value !== this._value) {
1906 this._value = value;
1908 }
1909 }
1910 > } event.ts
1911 >
1912 > class ConstValueWithChangeEvent<T> implements IValueWithChangeEvent<T> {
1913 > public readonly onDidChange: Event<void> = Event.None;
1914 >
1915 > constructor(readonly value: T) { }
1916 > }
1917 >
1918 > /**
1919 > * @param handleItem Is called for each item in the set (but only the first time the item is seen in the set).
1920 > * The returned disposable is disposed if the item is no longer in the set.
1921 > */
1922 > export function trackSetChanges<T>(getData: () => ReadonlySet<T>, onDidChangeData: Event<unknown>, handleItem: (d: T) => IDisposable): IDisposable {
1923 const map = new DisposableMap<T, IDisposable>();
1924 let oldData = new Set(getData());
1942 return store;
1943 }
1944 > event.ts
1945 >
1946 > function addToDisposables(result: IDisposable, disposables: DisposableStore | IDisposable[] | undefined) { event.ts
1947 > if (disposables instanceof DisposableStore) {
1948 disposables.add(result);
1949 > } else if (Array.isArray(disposables)) { event.ts
1950 disposables.push(result);
1951 }
1952 > } event.ts
1953 > event.ts
1954 function disposeAndRemove(result: IDisposable, disposables: DisposableStore | IDisposable[] | undefined) {
1955 if (disposables instanceof DisposableStore) {
src/vs/platform/agentHost/common/state/sessionState.ts 844 covered LOC · 65 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- sessionState.ts
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 > // Immutable state types for the sessions process protocol.
7 > // See protocol.md for the full design rationale.
8 > //
9 > // Most types are imported from the auto-generated protocol layer
10 > // (synced from the agent-host-protocol repo). This file adds VS Code-specific
11 > // helpers and re-exports.
12 >
13 > import { decodeBase64, encodeBase64, VSBuffer } from '../../../../base/common/buffer.js';
14 > import { hasKey, type Mutable } from '../../../../base/common/types.js';
15 > import { URI as ResourceURI } from '../../../../base/common/uri.js';
16 > import type { IProductService } from '../../../product/common/productService.js';
17 > import { readToolCallMeta } from '../meta/agentToolCallMeta.js';
18 > import {
19 > ResponsePartKind,
20 > SessionStatus,
21 > ToolCallStatus,
22 > SessionLifecycle,
23 > TerminalState,
24 > ToolResultContentType,
25 > ToolResultFileEditContent,
26 > ChatOriginKind,
27 > ChatInteractivity,
28 > type ActiveTurn,
29 > type ChangesetState,
30 > type ChatState,
31 > type ChatSummary,
32 > type PendingMessage,
33 > type Turn,
34 > type AnnotationsState,
35 > type URI as ProtocolURI,
36 > type RootState,
37 > type SessionState,
38 > type SessionSummary,
39 > type TextRange,
40 > type ToolCallCancelledState,
41 > type ToolCallCompletedState,
42 > type ToolCallResult,
43 > type ToolCallState,
44 > type ToolResultContent,
45 > type ToolResultSubagentContent,
46 > type ToolResultTextContent,
47 > type UsageInfo,
48 > type Message,
49 > } from './protocol/state.js';
50 >
51 > // Re-export everything from the protocol state module
52 > export {
53 > ChangesetOperationScope, ChangesetOperationStatus, ChangesetStatus, CustomizationLoadStatus,
54 > CustomizationType, MessageAttachmentKind, MessageKind,
55 > PendingMessageKind,
56 > PolicyState,
57 > ResponsePartKind,
58 > ChatInputAnswerState as SessionInputAnswerState,
59 > ChatInputAnswerValueKind as SessionInputAnswerValueKind,
60 > ChatInputQuestionKind as SessionInputQuestionKind,
61 > ChatInputResponseKind as SessionInputResponseKind,
62 > ChatInteractivity,
63 > ChatOriginKind,
64 > SessionLifecycle,
65 > SessionStatus, ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallContributorKind, ToolCallRiskAssessmentKind, ToolCallRiskAssessmentStatus, ToolCallStatus,
66 > ToolResultContentType,
67 > TurnState, type ActiveTurn, type AgentCustomization, type AgentCapabilities, type AgentInfo, type AgentSelection, type Annotation, type AnnotationEntry, type AnnotationsState, type AnnotationsSummary, type Changeset, type ChangesetFile,
68 > type ChangesetOperation, type ChangesetState, type ChatState, type ChatSummary, type ChatOrigin, type ChildCustomization, type ClientPluginCustomization, type ConfigPropertySchema,
69 > type ConfigSchema,
70 > type ContentRef, type Customization, type CustomizationDegradedState,
71 > type CustomizationErrorState, type CustomizationLoadedState, type CustomizationLoadingState, type CustomizationLoadState, type DirectoryCustomization, type ErrorInfo, type HookCustomization, type FileEdit as ISessionFileDiff, type ToolResultEmbeddedResourceContent as IToolResultBinaryContent, type MarkdownResponsePart, type McpServerCustomization, type MessageAttachment,
72 > type MessageResourceAttachment, type MessageEmbeddedResourceAttachment, type MessageAnnotationsAttachment, type ModelSelection, type PendingMessage, type PluginCustomization, type ProjectInfo, type PromptCustomization, type ReasoningResponsePart,
73 > type ResponsePart,
74 > type RootState, type RuleCustomization, type SessionActiveClient,
75 > type SessionConfigState, type ChatInputAnswer as SessionInputAnswer,
76 > type ChatInputOption as SessionInputOption, type ChatInputQuestion as SessionInputQuestion, type ChatInputRequest as SessionInputRequest, type SessionModelInfo,
77 > type SessionState,
78 > type SessionSummary, type SkillCustomization, type Snapshot, type StringOrMarkdown, type TerminalState, type TextRange,
79 > type ToolAnnotations,
80 > type ToolCallCancelledState,
81 > type ToolCallCompletedState,
82 > type ToolCallPendingConfirmationState,
83 > type ToolCallPendingResultConfirmationState,
84 > type ToolCallResponsePart,
85 > type ToolCallResult,
86 > type ToolCallRiskAssessment,
87 > type ToolCallRiskAssessmentCompleteState,
88 > type ToolCallRiskAssessmentLoadingState,
89 > type ToolCallRunningState,
90 > type ToolCallState,
91 > type ToolCallStreamingState,
92 > type ToolCallContributor,
93 > type ToolDefinition, type ToolResultContent,
94 > type ToolResultFileEditContent,
95 > type TerminalCommandResult,
96 > type ToolResultSubagentContent,
97 > type ToolResultTerminalContent,
98 > type ToolResultTextContent,
99 > type Turn, type URI, type UsageInfo,
100 > type Message
101 > } from './protocol/state.js';
102 >
103 > /**
104 > * Well-known keys that may appear on {@link UsageInfo._meta}.
105 > * Clients MAY read these to provide enhanced UI (e.g. credit cost display).
106 > */
107 > export interface UsageInfoMeta {
108 > /** Per-turn credit cost reported by the backend. */
109 > cost?: number;
110 > /** The concrete model selected by Copilot Auto and the routing explanation. */
111 > autoModeResolved?: IAutoModeResolvedInfo;
112 > /** Copilot-specific usage breakdown, including nano-AIU totals. */
113 > copilotUsage?: {
114 > totalNanoAiu?: number;
115 > [key: string]: unknown;
116 > };
117 > /**
118 > * Per-category account quota snapshots reported by the backend on the
119 > * model-call usage event, keyed by quota type (e.g. `chat`,
120 > * `premium_interactions`). Clients MAY use these to keep the account quota
121 > * UI current without a separate quota fetch.
122 > */
123 > quotaSnapshots?: {
124 > [quotaType: string]: {
125 > readonly isUnlimitedEntitlement?: boolean;
126 > readonly entitlementRequests?: number;
127 > readonly usedRequests?: number;
128 > readonly remainingPercentage?: number;
129 > readonly overage?: number;
130 > readonly overageAllowedWithExhaustedQuota?: boolean;
131 > /** ISO 8601 date when the quota resets, if applicable. */
132 > readonly resetDate?: string;
133 > } | undefined;
134 > };
135 > /**
136 > * Per-source context-window attribution breakdown reported by the SDK's
137 > * `session.rpc.metadata.getContextAttribution()`. Populated asynchronously
138 > * after each usage event and piped to the context-usage widget as
139 > * `promptTokenDetails`.
140 > */
141 > contextAttribution?: IContextAttributionData;
142 > [key: string]: unknown;
143 > }
144 >
145 > export interface IAutoModeResolvedInfo {
146 > readonly chosenModel: string;
147 > readonly reasoningBucket?: 'low' | 'medium' | 'high';
148 > readonly categoryScores?: Readonly<Record<string, number | undefined>>;
149 > readonly predictedLabel?: string;
150 > readonly confidence?: number;
151 > readonly candidateModels?: readonly string[];
152 > }
153 >
154 > /**
155 > * Mirrors the SDK's `SessionContextAttribution` shape — a flat list of
156 > * per-source entries describing what occupies the session's context window.
157 > */
158 > export interface IContextAttributionData {
159 > readonly totalTokens: number;
160 > readonly entries: readonly IContextAttributionEntry[];
161 > readonly compactions: { readonly count: number };
162 > }
163 >
164 > export interface IContextAttributionEntry {
165 > readonly kind: string;
166 > readonly id: string;
167 > readonly label: string;
168 > readonly tokens: number;
169 > readonly parentId?: string;
170 > readonly attributes?: Readonly<Record<string, string | undefined>>;
171 > }
172 >
173 > type AccountQuotaSnapshot = NonNullable<NonNullable<UsageInfoMeta['quotaSnapshots']>[string]>;
174 >
175 function readAccountQuotaSnapshot(value: unknown): AccountQuotaSnapshot | undefined {
176 if (!value || typeof value !== 'object' || Array.isArray(value)) {
188 return snapshot;
189 }
191 > /**
192 > * Reads the well-known {@link UsageInfoMeta} keys from a usage report's open
193 > * `_meta` bag, ignoring unrelated provider-specific keys and validating each
194 > * field's type. Always read {@link UsageInfo._meta} through this helper rather
195 > * than casting the bag to {@link UsageInfoMeta}, so a malformed or partial bag
196 > * degrades to absent fields instead of producing values of the wrong runtime
197 > * type. Returns an empty object when the bag is absent.
198 > */
199 > export function readUsageInfoMeta(usage: UsageInfo | undefined): UsageInfoMeta {
200 const meta = usage?._meta;
201 if (!meta) {
227 return result;
228 }
230 function readAutoModeResolvedInfo(value: unknown): IAutoModeResolvedInfo | undefined {
231 if (!value || typeof value !== 'object' || Array.isArray(value)) {
258 return result;
259 }
261 function readContextAttribution(value: unknown): IContextAttributionData | undefined {
262 if (!value || typeof value !== 'object' || Array.isArray(value)) {
295 return { totalTokens: raw['totalTokens'] as number, entries, compactions };
296 }
298 function filterStringAttributes(raw: Record<string, unknown>): Record<string, string | undefined> {
299 const result: Record<string, string | undefined> = {};
305 return result;
306 }
308 > export {
309 > ChangesetOperationTargetKind, type ChangesetOperationFollowUp, type ChangesetOperationTarget
310 > } from './protocol/commands.js';
311 >
312 > // Canonical chat-input type names (the protocol renamed the former
313 > // `SessionInput*` types to `ChatInput*` when input requests moved onto the
314 > // chat channel). Re-exported here so consumers can import them from the glue
315 > // layer alongside the legacy `SessionInput*` aliases above.
316 > export {
317 > ChatInputAnswerState,
318 > ChatInputAnswerValueKind,
319 > ChatInputQuestionKind,
320 > ChatInputResponseKind,
321 > type ChatInputAnswer,
322 > type ChatInputOption,
323 > type ChatInputQuestion,
324 > type ChatInputRequest,
325 > type InputRequestResponsePart,
326 > } from './protocol/state.js';
327 >
328 > // ---- File edit kind ---------------------------------------------------------
329 >
330 > /**
331 > * The kind of file edit operation. Derived from the presence/absence of
332 > * `before`/`after` in {@link ToolResultFileEditContent}.
333 > */
334 > export const enum FileEditKind {
335 > /** Content edit (same file URI, different content). */
336 > Edit = 'edit',
337 > /** File creation (no before state). */
338 > Create = 'create',
339 > /** File deletion (no after state). */
340 > Delete = 'delete',
341 > /** File rename/move (different before and after URIs). */
342 > Rename = 'rename',
343 > }
344 >
345 > // ---- Well-known URIs --------------------------------------------------------
346 >
347 > /** URI for the root state subscription. */
348 > export const ROOT_STATE_URI = 'ahp-root://';
349 >
350 > /** Scheme used by {@link ROOT_STATE_URI}. */
351 > export const AHP_ROOT_SCHEME = 'ahp-root';
352 >
353 > /** Scheme used by resource-watch channel URIs (`ahp-resource-watch:/<encoded>`). */
354 > export const AHP_RESOURCE_WATCH_SCHEME = 'ahp-resource-watch';
355 >
356 > /**
357 > * Encode a resource-watch descriptor into its canonical channel URI. The
358 > * descriptor is serialised into the URI path so the receiver can recover
359 > * the watch parameters without any server-side bookkeeping — subscribe is
360 > * the only point where state is materialised (an `IFileService` watcher
361 > * is attached on the first subscriber and held through a grace window
362 > * after the last drops).
363 > */
364 > export function buildResourceWatchChannelUri(descriptor: {
365 readonly root: string;
366 readonly recursive?: boolean;
380 return `${AHP_RESOURCE_WATCH_SCHEME}://r/${json}`;
381 }
383 > /**
384 > * Inverse of {@link buildResourceWatchChannelUri}. Returns `undefined` if
385 > * `uri` is not a well-formed `ahp-resource-watch:` URI — callers should
386 > * surface that as a not-found error to the client.
387 > */
388 > export function parseResourceWatchChannelUri(uri: string): {
389 root: string;
390 recursive: boolean;
421 }
422 }
424 > /** Returns `true` when `uri` identifies a resource-watch channel. */
425 > export function isAhpResourceWatchChannel(uri: string): boolean {
426 try {
427 return ResourceURI.parse(uri).scheme === AHP_RESOURCE_WATCH_SCHEME;
430 }
431 }
433 > /**
434 > * Returns `true` when `uri` identifies the root channel, regardless of
435 > * whether the caller passes the canonical wire form (`'ahp-root://'`) or a
436 > * variant that has been round-tripped through the workbench {@link URI} class
437 > * (which normalizes the authority-less form to `'ahp-root:'`). Always prefer
438 > * this helper over a direct `=== ROOT_STATE_URI` comparison so the two
439 > * spellings stay interchangeable.
440 > */
441 > export function isAhpRootChannel(uri: string): boolean {
442 if (uri === ROOT_STATE_URI) {
443 return true;
449 }
450 }
452 > /**
453 > * Mints a session-unique opaque id for a customization, derived from its
454 > * source URI and (when present) its `range` within the source. Plugins MAY
455 > * declare multiple children (e.g. MCP servers, hooks) inside the same
456 > * manifest file; including the range disambiguates them without an extra
457 > * mapping table.
458 > *
459 > * The range is appended as a reserved `#range=` query-style suffix; any
460 > * existing `#` in the URI is percent-encoded first so a source URI that
461 > * already contains a fragment cannot collide with a ranged id.
462 > */
463 > export function customizationId(uri: string, range?: TextRange): string {
464 if (!range) {
465 return uri;
468 return `${safeUri}#range=${range.start.line}:${range.start.character}-${range.end.line}:${range.end.character}`;
469 }
471 > // ---- VS Code-specific derived types -----------------------------------------
472 >
473 > /**
474 > * A tool call in a terminal state, stored in completed turns.
475 > */
476 > export type ICompletedToolCall = ToolCallCompletedState | ToolCallCancelledState;
477 >
478 > /**
479 > * Derived status type for the tool call lifecycle.
480 > */
481 > export type ToolCallStatusString = ToolCallState['status'];
482 >
483 > // ---- Tool output helper -----------------------------------------------------
484 >
485 > /**
486 > * Extracts a plain-text tool output string from a tool call result's `content`
487 > * array. Joins all text-type content parts into a single string.
488 > *
489 > * Returns `undefined` if there are no text content parts.
490 > */
491 > export function getToolOutputText(result: ToolCallResult): string | undefined {
492 if (!result.content || result.content.length === 0) {
493 return undefined;
504 return textParts.map(p => p.text).join('\n');
505 }
507 > /**
508 > * Extracts file edit content entries from a tool call result's `content` array.
509 > * Returns an empty array if there are no file edit content parts.
510 > */
511 > export function getToolFileEdits(result: ToolCallResult): ToolResultFileEditContent[] {
512 if (!result.content || result.content.length === 0) {
513 return [];
521 return edits;
522 }
524 > /**
525 > * Extracts the first subagent content entry from a tool call's `content` array.
526 > * Works with both completed tool call results and running tool call states.
527 > * Returns `undefined` if there are no subagent content parts.
528 > */
529 > export function getToolSubagentContent(result: { content?: readonly ToolResultContent[] }): ToolResultSubagentContent | undefined {
530 if (!result.content || result.content.length === 0) {
531 return undefined;
538 return undefined;
539 }
541 > // ---- Subagent URI helpers ---------------------------------------------------
542 >
543 > const SUBAGENT_URI_SEGMENT = 'subagent';
544 > const SUBAGENT_URI_MARKER = `/${SUBAGENT_URI_SEGMENT}/`;
545 > const SUBAGENT_URI_PATH_REGEX = /^(?<parentPath>.+)\/subagent\/(?<toolCallId>.+)$/;
546 >
547 function asResourceUri(uri: ProtocolURI | ResourceURI): ResourceURI {
548 return typeof uri === 'string' ? ResourceURI.parse(uri) : uri;
549 }
551 function getSubagentBasePath(parentSession: ProtocolURI | ResourceURI): { parent: ResourceURI; path: string } {
552 const parent = asResourceUri(parentSession);
554 return { parent, path: `${parentPath}${SUBAGENT_URI_MARKER}` };
555 }
557 > /**
558 > * Builds a subagent session URI from a parent session URI and tool call ID.
559 > * Convention: `{parentSessionUri}/subagent/{toolCallId}`
560 > */
561 > export function buildSubagentSessionUri(parentSession: ProtocolURI | ResourceURI, toolCallId: string): string {
562 const { parent, path } = getSubagentBasePath(parentSession);
563 return parent.with({ path: `${path}${toolCallId}` }).toString();
564 }
566 > /**
567 > * Parses a subagent session URI into its parent session URI and tool call ID.
568 > * Returns `undefined` if the URI does not follow the subagent convention.
569 > */
570 > export function parseSubagentSessionUri(uri: ProtocolURI | ResourceURI): { parentSession: ResourceURI; toolCallId: string } | undefined {
571 const resource = asResourceUri(uri);
572 const match = SUBAGENT_URI_PATH_REGEX.exec(resource.path);
579 };
580 }
582 > /**
583 > * Returns whether a session URI represents a subagent session.
584 > */
585 > export function isSubagentSession(uri: ProtocolURI | ResourceURI): boolean {
586 return parseSubagentSessionUri(uri) !== undefined;
587 }
589 > /**
590 > * Builds the string prefix used by the state manager for cached subagent sessions.
591 > */
592 > export function buildSubagentSessionUriPrefix(parentSession: ProtocolURI | ResourceURI): string {
593 const { parent, path } = getSubagentBasePath(parentSession);
594 return parent.with({ path }).toString();
595 }
597 > // ---- Factory helpers --------------------------------------------------------
598 >
599 > export function createRootState(): RootState {
600 > return { sessionState.ts
601 > agents: [],
602 > activeSessions: 0,
603 > };
604 > }
606 > /**
607 > * Creates the initial flat {@link SessionState} for a session from its
608 > * root-channel {@link SessionSummary} catalog entry. Session metadata
609 > * ({@link SessionMetadata}) — and the shared `_meta` bag — are inlined directly
610 > * onto the state.
611 > */
612 > export function createSessionState(summary: SessionSummary): SessionState {
613 const state: SessionState = {
614 provider: summary.provider,
627 return state;
628 }
630 > /**
631 > * Creates an empty {@link ChatState} for a chat. The summary fields are
632 > * denormalized onto the chat state per the protocol contract; callers pass
633 > * the chat's catalog summary and this seeds an empty conversation.
634 > */
635 > export function createChatState(summary: ChatSummary): ChatState {
636 return {
637 resource: summary.resource,
648 };
649 }
651 > /**
652 > * Derives the default-chat {@link ChatSummary} for a session from its
653 > * {@link SessionSummary}. The default chat inherits the session's title,
654 > * status, activity and working directory, and is marked as a
655 > * {@link ChatOriginKind.User | user-originated} chat. Both the session and
656 > * chat `modifiedAt` are ISO-8601 strings, so it is carried over directly.
657 > */
658 > export function createDefaultChatSummary(session: SessionSummary, chatUri: ProtocolURI): ChatSummary {
659 const summary: ChatSummary = {
660 resource: chatUri,
675 return summary;
676 }
678 > /** Activity bits (0-4) of {@link SessionStatus}; the high bits carry orthogonal flags (IsRead / IsArchived). */
679 > const STATUS_ACTIVITY_MASK = (1 << 5) - 1;
680 >
681 > /** Whether the active turn has a `PendingConfirmation` tool call auto-approved by the session's bypass setting. */
682 function hasAutoApprovedPendingConfirmation(state: ChatState): boolean {
683 return !!state.activeTurn?.responseParts.some(part =>
687 );
688 }
690 > /** Whether the chat is genuinely blocked on user input (an open input request, an auth-required tool, or a non-auto-approved confirmation gate). */
691 function chatAwaitsUserInput(state: ChatState): boolean {
692 return !!state.activeTurn?.responseParts.some(part => {
708 });
709 }
711 > /**
712 > * Projects a chat's status for session-summary aggregation, demoting an
713 > * `InputNeeded` back to `InProgress` only when it is caused solely by an
714 > * auto-approved confirmation — otherwise a session with bypass approvals flashes
715 > * "input needed" in the sessions list while an auto-approved tool runs.
716 > */
717 function chatSummaryStatus(state: ChatState): SessionStatus {
718 const status = state.status;
728 return status;
729 }
731 > /**
732 > * Derives a {@link ChatSummary} from a fully-populated {@link ChatState} by
733 > * projecting out the denormalized summary fields. Used to keep the parent
734 > * session's `chats` catalog in sync with a chat's denormalized state.
735 > */
736 > export function chatSummaryFromState(state: ChatState): ChatSummary {
737 const summary: ChatSummary = {
738 resource: state.resource,
748 return summary;
749 }
751 > /**
752 > * The effective interactivity of a chat given its session's archived state.
753 > *
754 > * `interactivity` is the general read-only mechanism (e.g. subagent worker
755 > * chats are `ReadOnly`). An archived session is read-only too, so its
756 > * interactive chats are downgraded to `ReadOnly`. `Hidden` chats stay hidden —
757 > * archiving only downgrades `Full` chats. Absent interactivity defaults to
758 > * `Full` for backward compatibility.
759 > *
760 > * The host uses this to enforce read-only turns off a single signal
761 > * ({@link isChatReadOnly}) rather than special-casing archived; the same rule
762 > * is mirrored client-side to hide the composer.
763 > */
764 > export function effectiveChatInteractivity(interactivity: ChatInteractivity | undefined, sessionArchived: boolean): ChatInteractivity {
765 if (interactivity === ChatInteractivity.Hidden) {
766 return ChatInteractivity.Hidden;
771 return interactivity ?? ChatInteractivity.Full;
772 }
774 > /**
775 > * Whether a chat rejects user-dispatched turns, given its own interactivity and
776 > * its session's archived state. `true` for `ReadOnly` chats (including archived
777 > * sessions' interactive chats). See {@link effectiveChatInteractivity}.
778 > */
779 > export function isChatReadOnly(interactivity: ChatInteractivity | undefined, sessionArchived: boolean): boolean {
780 return effectiveChatInteractivity(interactivity, sessionArchived) === ChatInteractivity.ReadOnly;
781 }
783 > export function createActiveTurn(id: string, message: Message, startedAt: string): ActiveTurn {
784 return {
785 id,
790 };
791 }
793 > export const enum StateComponents {
794 > Root,
795 > Session,
796 > Chat,
797 > Terminal,
798 > Changeset,
799 > Annotations,
800 > }
801 >
802 > export type ComponentToState = {
803 > [StateComponents.Root]: RootState;
804 > [StateComponents.Session]: SessionState;
805 > [StateComponents.Chat]: ChatState;
806 > [StateComponents.Terminal]: TerminalState;
807 > [StateComponents.Changeset]: ChangesetState;
808 > [StateComponents.Annotations]: AnnotationsState;
809 > };
810 >
811 > // ---- Default chat URI helpers ----------------------------------------------
812 >
813 > /** Scheme used by chat channel URIs (`ahp-chat://...`). */
814 > export const AHP_CHAT_SCHEME = 'ahp-chat';
815 >
816 > /** Chat id of the default chat that every session owns. */
817 > export const DEFAULT_CHAT_ID = 'default';
818 >
819 > /**
820 > * Derives the deterministic channel URI for a chat within a session. Every chat
821 > * — the default chat and any additional peer chats — encodes its owning session
822 > * URI into the path so producers and consumers can recover the session without a
823 > * lookup table (see {@link parseChatUri}). The chat id is carried in the URI
824 > * authority.
825 > *
826 > * `ahp-chat://<chatId>/<base64(sessionUri)>`
827 > */
828 > export function buildChatUri(sessionUri: ProtocolURI | ResourceURI, chatId: string): string {
829 const session = typeof sessionUri === 'string' ? sessionUri : sessionUri.toString();
830 const encoded = encodeBase64(VSBuffer.fromString(session), false, true);
831 return `${AHP_CHAT_SCHEME}://${chatId}/${encoded}`;
832 }
834 > /**
835 > * Derives the deterministic default-chat channel URI for a session. While the
836 > * protocol allows a session to contain many chats, every session always owns a
837 > * default chat whose URI is derived from the owning session URI so producers and
838 > * consumers can compute it without a lookup table.
839 > *
840 > * The session URI is encoded into the path so {@link parseChatUri} can recover
841 > * it.
842 > */
843 > export function buildDefaultChatUri(sessionUri: ProtocolURI | ResourceURI): string {
844 return buildChatUri(sessionUri, DEFAULT_CHAT_ID);
845 }
847 > const SUBAGENT_CHAT_ID = 'subagent';
848 >
849 > export function isSubagentChatUri(uri: ProtocolURI | ResourceURI): boolean {
850 const parsed = typeof uri === 'string' ? ResourceURI.parse(uri) : uri;
851 return parsed.scheme === AHP_CHAT_SCHEME && parsed.authority === SUBAGENT_CHAT_ID;
852 }
854 > export function buildSubagentChatUri(sessionUri: ProtocolURI | ResourceURI, toolCallId: string): string {
855 const session = typeof sessionUri === 'string' ? sessionUri : sessionUri.toString();
856 const encoded = encodeBase64(VSBuffer.fromString(session), false, true);
857 return `${AHP_CHAT_SCHEME}://${SUBAGENT_CHAT_ID}/${encoded}/${encodeURIComponent(toolCallId)}`;
858 }
860 > /**
861 > * Inverse of {@link buildChatUri}: recovers the owning session URI and chat id
862 > * from any chat channel URI. Returns `undefined` when `uri` is not a well-formed
863 > * chat URI.
864 > */
865 > export function parseChatUri(uri: ProtocolURI | ResourceURI): { session: string; chatId: string } | undefined {
866 let parsed: ResourceURI;
867 try {
891 }
892 }
894 > /**
895 > * Inverse of {@link buildDefaultChatUri}: recovers the owning session URI from a
896 > * chat channel URI. Returns `undefined` when `uri` is not a well-formed chat URI.
897 > * Accepts any chat URI (default or additional) so callers that only need the
898 > * parent session can use it uniformly.
899 > */
900 > export function parseDefaultChatUri(uri: ProtocolURI | ResourceURI): string | undefined {
901 return parseChatUri(uri)?.session;
902 }
904 > export function parseRequiredSessionUriFromChatUri(uri: ProtocolURI | ResourceURI): string {
905 const session = parseDefaultChatUri(uri);
906 if (session === undefined) {
909 return session;
910 }
912 > /** Returns `true` when `uri` is the default chat of its session. */
913 > export function isDefaultChatUri(uri: ProtocolURI | ResourceURI): boolean {
914 return parseChatUri(uri)?.chatId === DEFAULT_CHAT_ID;
915 }
917 > /**
918 > * Resolves a feature-level `(session, chat)` pair to the single chat URI used by
919 > * the agent session/chat surface. A session always owns a DEFAULT chat addressed
920 > * by the session URI itself; additional (peer) chats are addressed by their own
921 > * chat channel URIs. This is the one place default-chat resolution lives so
922 > * agents never re-derive "is this the default chat?".
923 > */
924 > export function resolveChatUri(session: ResourceURI, chat: ResourceURI): ResourceURI {
925 return isDefaultChatUri(chat) ? session : chat;
926 }
928 > /** Returns `true` when `uri` identifies a chat channel. */
929 > export function isAhpChatChannel(uri: string): boolean {
930 > try { sessionState.ts
931 > return ResourceURI.parse(uri).scheme === AHP_CHAT_SCHEME;
932 > } catch {
933 return false;
934 }
935 > } sessionState.ts
937 > // ---- Session + default-chat composite --------------------------------------
938 >
939 > /**
940 > * A single chat's effective session context: the shared {@link SessionState}
941 > * (working directories, active clients, config, customizations/MCP scope, …)
942 > * resolved for one chat and merged with that chat's conversation contents.
943 > *
944 > * The protocol moved turns and pending state off the session and onto a
945 > * per-chat channel, and lets a chat override the session's working directories
946 > * with a subset (e.g. {@link ChatState.workingDirectories}) and carry its own
947 > * read-only {@link ChatState.primaryWorkingDirectory | primary} (fixed at chat
948 > * creation — the session has no primary). This composite recombines the session
949 > * with one of its chats — default or peer — so consumers read the chat's
950 > * effective context and conversation through one object without walking back to
951 > * the session to re-derive shared state. The {@link ISessionWithDefaultChat.workingDirectories}
952 > * carry the chat's *effective* working directories (its own subset override when
953 > * present, else the session's full set); {@link ISessionWithDefaultChat.primaryWorkingDirectory}
954 > * is the chat's own primary.
955 > */
956 > export interface ISessionWithDefaultChat extends SessionState {
957 > /** The chat's read-only primary working directory (fixed at chat creation). */
958 > primaryWorkingDirectory?: ProtocolURI;
959 > /** Completed turns of this chat. */
960 > turns: Turn[];
961 > /** Currently in-progress turn of this chat. */
962 > activeTurn?: ActiveTurn;
963 > /** Steering message pending on this chat. */
964 > steeringMessage?: PendingMessage;
965 > /** Queued messages pending on this chat. */
966 > queuedMessages?: PendingMessage[];
967 > /** Draft input of this chat. */
968 > draft?: Message;
969 > }
970 >
971 > /**
972 > * Projects a {@link SessionState} and one of its {@link ChatState | chats}
973 > * (default or peer) into that chat's {@link ISessionWithDefaultChat | effective
974 > * session context}. Per-chat overrides (the working-directories subset and the
975 > * chat's own primary) are layered over the session defaults, and the
976 > * conversation fields are taken from the chat. When the chat state is absent
977 > * (e.g. not yet hydrated) the conversation fields default to empty and the
978 > * session defaults apply.
979 > */
980 > export function mergeSessionWithDefaultChat(session: SessionState, chat: ChatState | undefined): ISessionWithDefaultChat {
981 return {
982 ...session,
990 };
991 }
993 > /**
994 > * Resolves the active turn of a session's default chat, if any.
995 > */
996 > export function getActiveTurn(chat: ChatState | undefined): ActiveTurn | undefined {
997 return chat?.activeTurn;
998 }
1000 > /**
1001 > * Resolves the default chat's catalog summary from a session, if present.
1002 > */
1003 > export function getDefaultChat(session: SessionState): ChatSummary | undefined {
1004 if (session.defaultChat !== undefined) {
1005 const match = session.chats.find(c => c.resource === session.defaultChat);
1010 return session.chats[0];
1011 }
1013 > // ---- SessionMeta accessors -------------------------------------------------
1014 >
1015 > /**
1016 > * VS Code-side alias for the protocol's open `_meta` property bag on
1017 > * {@link SessionState}. Keys SHOULD be namespaced (e.g. `git`, `vscode.foo`)
1018 > * to avoid collisions; values MUST be JSON-serializable.
1019 > */
1020 > export type SessionMeta = Record<string, unknown>;
1021 >
1022 > /**
1023 > * VS Code-side alias for the protocol's open `_meta` property bag on
1024 > * {@link SessionSummary}. Keys SHOULD be namespaced (e.g. `git`, `vscode.foo`)
1025 > * to avoid collisions; values MUST be JSON-serializable.
1026 > */
1027 > export type SessionSummaryMeta = Record<string, unknown>;
1028 >
1029 > /**
1030 > * Reserved key under {@link SessionMeta} for the well-known git-state
1031 > * payload. Value at this key, when present, MUST be shaped like
1032 > * {@link ISessionGitState}. This is a VS Code-specific convention layered
1033 > * on top of the protocol's generic `_meta` bag — the protocol itself does
1034 > * not know about git state.
1035 > */
1036 > export const SESSION_META_GIT_KEY = 'git';
1037 >
1038 > /**
1039 > * Reserved key under {@link SessionMeta} for the well-known GitHub-state
1040 > * payload. Value at this key, when present, MUST be shaped like
1041 > * {@link ISessionGitHubState}. This is a VS Code-specific convention layered
1042 > * on top of the protocol's generic `_meta` bag — the protocol itself does
1043 > * not know about GitHub state.
1044 > */
1045 > export const SESSION_META_GITHUB_KEY = 'github';
1046 >
1047 > export const SESSION_META_PROMPT_CACHE_KEY = 'vscode.promptCache';
1048 >
1049 > /** Latest known prompt-cache state for the model active in an agent session. */
1050 > export interface ISessionPromptCacheState {
1051 > readonly modelId: string;
1052 > readonly cacheExpiresAt: string;
1053 > }
1054 >
1055 > /** Reads the latest known prompt-cache state from session metadata. */
1056 > export function readSessionPromptCacheState(meta: SessionMeta | undefined): ISessionPromptCacheState | undefined {
1057 const value = meta?.[SESSION_META_PROMPT_CACHE_KEY];
1058 if (!value || typeof value !== 'object' || Array.isArray(value)) {
1064 : undefined;
1065 }
1067 > /** Returns session metadata with the prompt-cache slot updated or removed. */
1068 > export function withSessionPromptCacheState(meta: SessionMeta | undefined, promptCache: ISessionPromptCacheState | undefined): SessionMeta | undefined {
1069 const next: SessionMeta = { ...meta };
1070 if (promptCache) {
1075 return Object.keys(next).length > 0 ? next : undefined;
1076 }
1078 > /**
1079 > * Git state of a session's working directory, carried under
1080 > * {@link SessionMeta} at {@link SESSION_META_GIT_KEY}. Used by clients to
1081 > * drive source-control affordances (e.g. PR/merge buttons in the Agents
1082 > * app).
1083 > *
1084 > * All fields are optional — agents that do not track a particular field
1085 > * should omit it rather than send a placeholder, so clients can distinguish
1086 > * "unknown" from "known to be zero".
1087 > */
1088 > export interface ISessionGitState {
1089 > /** Whether the working directory has a `github.com` git remote. */
1090 > readonly hasGitHubRemote?: boolean;
1091 > /** Current branch name. */
1092 > readonly branchName?: string;
1093 > /** Base branch the work targets (e.g. `main`). */
1094 > readonly baseBranchName?: string;
1095 > /** Upstream tracking branch (e.g. `origin/feature`). */
1096 > readonly upstreamBranchName?: string;
1097 > /** Number of commits the upstream branch has ahead of the local branch. */
1098 > readonly incomingChanges?: number;
1099 > /** Number of commits the local branch has ahead of the upstream branch. */
1100 > readonly outgoingChanges?: number;
1101 > /** Number of files with uncommitted changes. */
1102 > readonly uncommittedChanges?: number;
1103 > /** GitHub repository owner parsed from the working copy's GitHub remote (preferring `origin`, falling back to the first GitHub remote). */
1104 > readonly githubOwner?: string;
1105 > /** GitHub repository name parsed from the working copy's GitHub remote (preferring `origin`, falling back to the first GitHub remote). */
1106 > readonly githubRepo?: string;
1107 > }
1108 >
1109 > /**
1110 > * GitHub state of a session, carried under {@link SessionMeta} at
1111 > * {@link SESSION_META_GITHUB_KEY}. Used by clients to drive GitHub-specific
1112 > * affordances (e.g. PR/merge buttons in the Agents app).
1113 > *
1114 > * All fields are optional — agents that do not track a particular field
1115 > * should omit it rather than send a placeholder, so clients can distinguish
1116 > * "unknown" from "known to be zero".
1117 > */
1118 > export interface ISessionGitHubState {
1119 > /** The owner of the GitHub repository. */
1120 > readonly owner?: string;
1121 > /** The name of the GitHub repository. */
1122 > readonly repo?: string;
1123 > /** The URL of the GitHub pull request. */
1124 > readonly pullRequestUrl?: string;
1125 > }
1126 >
1127 > /**
1128 > * Reads the well-known git-state payload from {@link SessionMeta}, if
1129 > * present. Returns `undefined` when the meta bag is absent or the value at
1130 > * the git key is not a plain object (e.g. an array or a primitive).
1131 > * Individual fields with wrong types are silently dropped so partial state
1132 > * still propagates.
1133 > *
1134 > * Unlike the other typed readers, this takes the raw {@link SessionMeta} value
1135 > * rather than its parent {@link SessionState}: the sessions provider stores and
1136 > * reads a detached meta snapshot without retaining the owning state.
1137 > */
1138 > export function readSessionGitState(meta: SessionMeta | undefined): ISessionGitState | undefined {
1139 const value = meta?.[SESSION_META_GIT_KEY];
1140 if (!value || typeof value !== 'object' || Array.isArray(value)) {
1164 return result;
1165 }
1167 > /**
1168 > * Returns a new {@link SessionMeta} with the git-state payload set to
1169 > * `gitState`, or with the git slot removed if `gitState` is `undefined`.
1170 > * Returns `undefined` if the result would be empty.
1171 > */
1172 > export function withSessionGitState(meta: SessionMeta | undefined, gitState: ISessionGitState | undefined): SessionMeta | undefined {
1173 const next: { [key: string]: unknown } = { ...meta };
1174 if (gitState !== undefined) {
1179 return Object.keys(next).length > 0 ? next : undefined;
1180 }
1182 > /**
1183 > * Reads the well-known GitHub state payload from {@link SessionSummaryMeta}, if
1184 > * present. Returns `undefined` when the meta bag is absent or the value at the
1185 > * GitHub key is not a plain object (e.g. an array or a primitive).
1186 > * Individual fields with wrong types are silently dropped so partial state
1187 > * still propagates.
1188 > *
1189 > * Unlike the other typed readers, this takes the raw {@link SessionSummaryMeta}
1190 > * value rather than its parent {@link SessionState}: the sessions provider stores and
1191 > * reads a detached meta snapshot without retaining the owning state.
1192 > */
1193 > export function readSessionGitHubState(meta: SessionSummaryMeta | undefined): ISessionGitHubState | undefined {
1194 const value = meta?.[SESSION_META_GITHUB_KEY];
1195 if (!value || typeof value !== 'object' || Array.isArray(value)) {
1208 return result;
1209 }
1211 > /**
1212 > * Returns a new {@link SessionSummaryMeta} with the GitHub-state payload set to
1213 > * `gitHubState`, or with the GitHub slot removed if `gitHubState` is `undefined`.
1214 > * Returns `undefined` if the result would be empty.
1215 > */
1216 > export function withSessionGitHubState(meta: SessionSummaryMeta | undefined, gitHubState: ISessionGitHubState | undefined): SessionSummaryMeta | undefined {
1217 const next: { [key: string]: unknown } = { ...meta };
1218 if (gitHubState !== undefined) {
1223 return Object.keys(next).length > 0 ? next : undefined;
1224 }
1226 > /**
1227 > * Reserved key under {@link SessionSummaryMeta} recording how deeply a session
1228 > * was spawned via the `create_session` host tool (0 for a top-level, user-created
1229 > * session). Used to bound recursive session creation. VS Code-specific convention
1230 > * layered on top of the protocol's generic `_meta` bag.
1231 > */
1232 > export const SESSION_META_SPAWN_DEPTH_KEY = 'agentHost/sessionSpawnDepth';
1233 >
1234 > /**
1235 > * Reads the `create_session` spawn depth from a {@link SessionSummaryMeta} bag,
1236 > * returning `0` when the key is absent or not a finite number.
1237 > */
1238 > export function readSessionSpawnDepth(meta: SessionSummaryMeta | undefined): number {
1239 const value = meta?.[SESSION_META_SPAWN_DEPTH_KEY];
1240 return typeof value === 'number' && Number.isFinite(value) ? value : 0;
1241 }
1243 > /**
1244 > * Returns a new {@link SessionSummaryMeta} with the `create_session` spawn depth
1245 > * set to `depth`, preserving any other keys in the bag.
1246 > */
1247 > export function withSessionSpawnDepth(meta: SessionSummaryMeta | undefined, depth: number): SessionSummaryMeta {
1248 return { ...meta, [SESSION_META_SPAWN_DEPTH_KEY]: depth };
1249 }
1251 > /**
1252 > * Reserved key under {@link SessionSummaryMeta} marking a session as
1253 > * workspace-less: a session with no workspace/folder binding (surfaced in the
1254 > * UI as a "Quick Chat"). Carried on the summary bag (not the full state) so
1255 > * clients can group/style such sessions in session lists without subscribing to
1256 > * full session state. VS Code-specific convention layered on the protocol's
1257 > * generic `_meta` bag.
1258 > */
1259 > export const SESSION_META_WORKSPACELESS_KEY = 'workspaceless';
1260 >
1261 > /**
1262 > * Session-database metadata key recording whether a session is workspace-less (a
1263 > * workspace-less chat). Owned by the AH service: `AgentService` writes it centrally at
1264 > * create/materialize and overlays it onto every agent's summary `_meta` in
1265 > * `listSessions`; agents only read it (e.g. to pick the workspace-less system prompt
1266 > * on resume) and never persist it themselves.
1267 > */
1268 > export const AH_META_WORKSPACELESS_DB_KEY = 'agentHost.workspaceless';
1269 >
1270 > /**
1271 > * Session-database metadata key recording whether a session is archived. Written by
1272 > * the AH orchestrator (`AgentSideEffects` on `SessionIsArchivedChanged`) and read by
1273 > * both the orchestrator (`AgentService` restore/list) and agents (e.g. `CopilotAgent`
1274 > * decides whether to recreate a missing worktree vs. resume read-only for history).
1275 > * {@link AH_META_IS_DONE_DB_KEY} is the legacy name kept for sessions persisted before
1276 > * the rename; readers fall back to it when {@link AH_META_IS_ARCHIVED_DB_KEY} is absent.
1277 > */
1278 > export const AH_META_IS_ARCHIVED_DB_KEY = 'isArchived';
1279 >
1280 > /** Legacy metadata key for the archived flag; see {@link AH_META_IS_ARCHIVED_DB_KEY}. */
1281 > export const AH_META_IS_DONE_DB_KEY = 'isDone';
1282 >
1283 > /**
1284 > * Reads the workspace-less marker from {@link SessionSummaryMeta}. Returns
1285 > * `true` only when the well-known key is present and set to boolean `true`.
1286 > */
1287 > export function readSessionWorkspaceless(meta: SessionSummaryMeta | undefined): boolean {
1288 return meta?.[SESSION_META_WORKSPACELESS_KEY] === true;
1289 }
1291 > /**
1292 > * Returns a new {@link SessionSummaryMeta} with the workspace-less marker set,
1293 > * or with the slot removed when `workspaceless` is `false`. Returns `undefined`
1294 > * if the result would be empty.
1295 > */
1296 > export function withSessionWorkspaceless(meta: SessionSummaryMeta | undefined, workspaceless: boolean): SessionSummaryMeta | undefined {
1297 const next: { [key: string]: unknown } = { ...meta };
1298 if (workspaceless) {
1303 return Object.keys(next).length > 0 ? next : undefined;
1304 }
1306 > // ---- RootState _meta accessors ---------------------------------------------
1307 >
1308 > /**
1309 > * VS Code-side alias for the protocol's open `_meta` property bag on
1310 > * {@link RootState}. Keys SHOULD be namespaced to avoid collisions; values MUST
1311 > * be JSON-serializable.
1312 > */
1313 > export type RootMeta = Record<string, unknown>;
1314 >
1315 > /**
1316 > * Reserved key under {@link RootMeta} for the well-known host-build payload.
1317 > * Value at this key, when present, MUST be shaped like {@link IHostBuildInfo}.
1318 > * This is a VS Code-specific convention layered on top of the protocol's
1319 > * generic `_meta` bag — the protocol itself does not know about build info.
1320 > */
1321 > export const ROOT_META_HOST_BUILD_KEY = 'hostBuild';
1322 >
1323 > /**
1324 > * Build information about the program hosting the agent host (the VS Code CLI),
1325 > * carried under {@link RootMeta} at {@link ROOT_META_HOST_BUILD_KEY}. Lets a
1326 > * client see which build is hosting it — useful when inspecting the output of a
1327 > * remote agent host.
1328 > *
1329 > * All fields except {@link version} are optional — a build that does not track
1330 > * a particular field should omit it.
1331 > */
1332 > export interface IHostBuildInfo {
1333 > /** Product version (e.g. `1.96.0`). */
1334 > readonly version: string;
1335 > /** Commit SHA of the build, if known. */
1336 > readonly commit?: string;
1337 > /** Build date (ISO 8601), if known. */
1338 > readonly date?: string;
1339 > /** Release quality (e.g. `stable`, `insider`), if known. */
1340 > readonly quality?: string;
1341 > }
1342 >
1343 > /**
1344 > * Derives {@link IHostBuildInfo} from the host's {@link IProductService}.
1345 > */
1346 > export function hostBuildInfoFromProduct(productService: IProductService): IHostBuildInfo {
1347 > return { sessionState.ts
1348 > version: productService.version,
1349 > commit: productService.commit,
1350 > date: productService.date,
1351 > quality: productService.quality,
1352 > };
1353 > }
1355 > /**
1356 > * Reads the well-known host-build payload from {@link RootMeta}, if present.
1357 > * Returns `undefined` when the meta bag is absent or the value at the host-build
1358 > * key is not a plain object with a string `version`. Optional fields with wrong
1359 > * types are silently dropped.
1360 > */
1361 > export function readHostBuildInfo(state: RootState | undefined): IHostBuildInfo | undefined {
1362 const meta = state?._meta;
1363 const value = meta?.[ROOT_META_HOST_BUILD_KEY];
1377 return result;
1378 }
1380 > /**
1381 > * Returns a new {@link RootMeta} with the host-build payload set to
1382 > * `buildInfo`, or with the slot removed if `buildInfo` is `undefined`. Returns
1383 > * `undefined` if the result would be empty.
1384 > */
1385 > export function withHostBuildInfo(meta: RootMeta | undefined, buildInfo: IHostBuildInfo | undefined): RootMeta | undefined {
1386 > const next: { [key: string]: unknown } = { ...meta }; sessionState.ts
1387 > if (buildInfo !== undefined) {
1388 > next[ROOT_META_HOST_BUILD_KEY] = buildInfo; sessionState.ts
1389 > } else { sessionState.ts
1390 delete next[ROOT_META_HOST_BUILD_KEY];
1391 }
1392 > return Object.keys(next).length > 0 ? next : undefined; sessionState.ts
1393 > }
1395 > /**
1396 > * Formats {@link IHostBuildInfo} as a short single-line human-readable string,
1397 > * e.g. `1.96.0 (commit abc1234, 2024-01-02T03:04:05Z, insider)`.
1398 > */
1399 > export function formatHostBuildInfo(info: IHostBuildInfo): string {
1400 const details: string[] = [];
1401 if (info.commit) { details.push(`commit ${info.commit}`); }
src/vs/platform/contextkey/common/contextkey.ts 832 covered LOC · 218 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- contextkey.ts
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 > import { CharCode } from '../../../base/common/charCode.js';
7 > import { Event } from '../../../base/common/event.js';
8 > import { isChrome, isEdge, isFirefox, isLinux, isMacintosh, isSafari, isWeb, isWindows } from '../../../base/common/platform.js';
9 > import { isFalsyOrWhitespace } from '../../../base/common/strings.js';
10 > import { Scanner, LexingError, Token, TokenType } from './scanner.js';
11 > import { createDecorator } from '../../instantiation/common/instantiation.js';
12 > import { localize } from '../../../nls.js';
13 > import { IDisposable } from '../../../base/common/lifecycle.js';
14 > import { illegalArgument } from '../../../base/common/errors.js';
15 >
16 > const CONSTANT_VALUES = new Map<string, boolean>();
17 > CONSTANT_VALUES.set('false', false);
18 > CONSTANT_VALUES.set('true', true);
19 > CONSTANT_VALUES.set('isMac', isMacintosh);
20 > CONSTANT_VALUES.set('isLinux', isLinux);
21 > CONSTANT_VALUES.set('isWindows', isWindows);
22 > CONSTANT_VALUES.set('isWeb', isWeb);
23 > CONSTANT_VALUES.set('isMacNative', isMacintosh && !isWeb);
24 > CONSTANT_VALUES.set('isEdge', isEdge);
25 > CONSTANT_VALUES.set('isFirefox', isFirefox);
26 > CONSTANT_VALUES.set('isChrome', isChrome);
27 > CONSTANT_VALUES.set('isSafari', isSafari);
28 >
29 > /** allow register constant context keys that are known only after startup; requires running `substituteConstants` on the context key - https://github.com/microsoft/vscode/issues/174218#issuecomment-1437972127 */
30 > export function setConstant(key: string, value: boolean) {
31 if (CONSTANT_VALUES.get(key) !== undefined) { throw illegalArgument('contextkey.setConstant(k, v) invoked with already set constant `k`'); }
32
33 CONSTANT_VALUES.set(key, value);
34 }
36 > const hasOwnProperty = Object.prototype.hasOwnProperty;
37 >
38 > export const enum ContextKeyExprType {
39 > False = 0,
40 > True = 1,
41 > Defined = 2,
42 > Not = 3,
43 > Equals = 4,
44 > NotEquals = 5,
45 > And = 6,
46 > Regex = 7,
47 > NotRegex = 8,
48 > Or = 9,
49 > In = 10,
50 > NotIn = 11,
51 > Greater = 12,
52 > GreaterEquals = 13,
53 > Smaller = 14,
54 > SmallerEquals = 15,
55 > }
56 >
57 > export interface IContextKeyExprMapper {
58 > mapDefined(key: string): ContextKeyExpression;
59 > mapNot(key: string): ContextKeyExpression;
60 > mapEquals(key: string, value: any): ContextKeyExpression;
61 > mapNotEquals(key: string, value: any): ContextKeyExpression;
62 > mapGreater(key: string, value: any): ContextKeyExpression;
63 > mapGreaterEquals(key: string, value: any): ContextKeyExpression;
64 > mapSmaller(key: string, value: any): ContextKeyExpression;
65 > mapSmallerEquals(key: string, value: any): ContextKeyExpression;
66 > mapRegex(key: string, regexp: RegExp | null): ContextKeyRegexExpr;
67 > mapIn(key: string, valueKey: string): ContextKeyInExpr;
68 > mapNotIn(key: string, valueKey: string): ContextKeyNotInExpr;
69 > }
70 >
71 > export interface IContextKeyExpression {
72 > cmp(other: ContextKeyExpression): number;
73 > equals(other: ContextKeyExpression): boolean;
74 > substituteConstants(): ContextKeyExpression | undefined;
75 > evaluate(context: IContext): boolean;
76 > serialize(): string;
77 > keys(): string[];
78 > map(mapFnc: IContextKeyExprMapper): ContextKeyExpression;
79 > negate(): ContextKeyExpression;
80 >
81 > }
82 >
83 > export type ContextKeyExpression = (
84 > ContextKeyFalseExpr | ContextKeyTrueExpr | ContextKeyDefinedExpr | ContextKeyNotExpr
85 > | ContextKeyEqualsExpr | ContextKeyNotEqualsExpr | ContextKeyRegexExpr
86 > | ContextKeyNotRegexExpr | ContextKeyAndExpr | ContextKeyOrExpr | ContextKeyInExpr
87 > | ContextKeyNotInExpr | ContextKeyGreaterExpr | ContextKeyGreaterEqualsExpr
88 > | ContextKeySmallerExpr | ContextKeySmallerEqualsExpr
89 > );
90 >
91 >
92 > /*
93 >
94 > Syntax grammar:
95 >
96 > ```ebnf
97 >
98 > expression ::= or
99 >
100 > or ::= and { '||' and }*
101 >
102 > and ::= term { '&&' term }*
103 >
104 > term ::=
105 > | '!' (KEY | true | false | parenthesized)
106 > | primary
107 >
108 > primary ::=
109 > | 'true'
110 > | 'false'
111 > | parenthesized
112 > | KEY '=~' REGEX
113 > | KEY [ ('==' | '!=' | '<' | '<=' | '>' | '>=' | 'not' 'in' | 'in') value ]
114 >
115 > parenthesized ::=
116 > | '(' expression ')'
117 >
118 > value ::=
119 > | 'true'
120 > | 'false'
121 > | 'in' // we support `in` as a value because there's an extension that uses it, ie "when": "languageId == in"
122 > | VALUE // matched by the same regex as KEY; consider putting the value in single quotes if it's a string (e.g., with spaces)
123 > | SINGLE_QUOTED_STR
124 > | EMPTY_STR // this allows "when": "foo == " which's used by existing extensions
125 >
126 > ```
127 > */
128 >
129 > export type ParserConfig = {
130 > /**
131 > * with this option enabled, the parser can recover from regex parsing errors, e.g., unescaped slashes: `/src//` is accepted as `/src\//` would be
132 > */
133 > regexParsingWithErrorRecovery: boolean;
134 > };
135 >
136 > const defaultConfig: ParserConfig = {
137 > regexParsingWithErrorRecovery: true
138 > };
139 >
140 > export type ParsingError = {
141 > message: string;
142 > offset: number;
143 > lexeme: string;
144 > additionalInfo?: string;
145 > };
146 >
147 > const errorEmptyString = localize('contextkey.parser.error.emptyString', "Empty context key expression");
148 > const hintEmptyString = localize('contextkey.parser.error.emptyString.hint', "Did you forget to write an expression? You can also put 'false' or 'true' to always evaluate to false or true, respectively.");
149 > const errorNoInAfterNot = localize('contextkey.parser.error.noInAfterNot', "'in' after 'not'.");
150 > const errorClosingParenthesis = localize('contextkey.parser.error.closingParenthesis', "closing parenthesis ')'");
151 > const errorUnexpectedToken = localize('contextkey.parser.error.unexpectedToken', "Unexpected token");
152 > const hintUnexpectedToken = localize('contextkey.parser.error.unexpectedToken.hint', "Did you forget to put && or || before the token?");
153 > const errorUnexpectedEOF = localize('contextkey.parser.error.unexpectedEOF', "Unexpected end of expression");
154 > const hintUnexpectedEOF = localize('contextkey.parser.error.unexpectedEOF.hint', "Did you forget to put a context key?");
155 >
156 > /**
157 > * A parser for context key expressions.
158 > *
159 > * Example:
160 > * ```ts
161 > * const parser = new Parser();
162 > * const expr = parser.parse('foo == "bar" && baz == true');
163 > *
164 > * if (expr === undefined) {
165 > * // there were lexing or parsing errors
166 > * // process lexing errors with `parser.lexingErrors`
167 > * // process parsing errors with `parser.parsingErrors`
168 > * } else {
169 > * // expr is a valid expression
170 > * }
171 > * ```
172 > */
173 > export class Parser {
174 > // Note: this doesn't produce an exact syntax tree but a normalized one
175 > // ContextKeyExpression's that we use as AST nodes do not expose constructors that do not normalize
176 >
177 > private static _parseError = new Error();
178 >
179 > // lifetime note: `_scanner` lives as long as the parser does, i.e., is not reset between calls to `parse`
180 > private readonly _scanner = new Scanner();
181 >
182 > // lifetime note: `_tokens`, `_current`, and `_parsingErrors` must be reset between calls to `parse`
183 > private _tokens: Token[] = [];
184 > private _current = 0; // invariant: 0 <= this._current < this._tokens.length ; any incrementation of this value must first call `_isAtEnd`
185 > private _parsingErrors: ParsingError[] = [];
186 >
187 > get lexingErrors(): Readonly<LexingError[]> {
188 return this._scanner.errors;
189 }
191 > get parsingErrors(): Readonly<ParsingError[]> {
192 return this._parsingErrors;
193 }
195 > constructor(private readonly _config: ParserConfig = defaultConfig) {
196 > }
197 >
198 > /**
199 > * Parse a context key expression.
200 > *
201 > * @param input the expression to parse
202 > * @returns the parsed expression or `undefined` if there's an error - call `lexingErrors` and `parsingErrors` to see the errors
203 > */
204 > parse(input: string): ContextKeyExpression | undefined {
205
206 if (input === '') {
231 }
232 }
234 > private _expr(): ContextKeyExpression | undefined {
235 return this._or();
236 }
238 > private _or(): ContextKeyExpression | undefined {
239 const expr = [this._and()];
240
246 return expr.length === 1 ? expr[0] : ContextKeyExpr.or(...expr);
247 }
249 > private _and(): ContextKeyExpression | undefined {
250 const expr = [this._term()];
251
257 return expr.length === 1 ? expr[0] : ContextKeyExpr.and(...expr);
258 }
260 > private _term(): ContextKeyExpression | undefined {
261 if (this._matchOne(TokenType.Neg)) {
262 const peek = this._peek();
283 return this._primary();
284 }
286 > private _primary(): ContextKeyExpression | undefined {
287
288 const peek = this._peek();
498 }
499 }
501 > private _value(): string {
502 const token = this._peek();
503 switch (token.type) {
521 }
522 }
524 > private _flagsGYRe = /g|y/g;
525 > private _removeFlagsGY(flags: string): string {
526 return flags.replaceAll(this._flagsGYRe, '');
527 }
529 > // careful: this can throw if current token is the initial one (ie index = 0)
530 > private _previous() {
531 return this._tokens[this._current - 1];
532 }
534 > private _matchOne(token: TokenType) {
535 if (this._check(token)) {
536 this._advance();
540 return false;
541 }
543 > private _advance() {
544 if (!this._isAtEnd()) {
545 this._current++;
547 return this._previous();
548 }
550 > private _consume(type: TokenType, message: string) {
551 if (this._check(type)) {
552 return this._advance();
555 throw this._errExpectedButGot(message, this._peek());
556 }
558 > private _errExpectedButGot(expected: string, got: Token, additionalInfo?: string) {
559 const message = localize('contextkey.parser.error.expectedButGot', "Expected: {0}\nReceived: '{1}'.", expected, Scanner.getLexeme(got));
560 const offset = got.offset;
563 return Parser._parseError;
564 }
566 > private _check(type: TokenType) {
567 return this._peek().type === type;
568 }
570 > private _peek() {
571 return this._tokens[this._current];
572 }
574 > private _isAtEnd() {
575 return this._peek().type === TokenType.EOF;
576 }
577 > } contextkey.ts
578 >
579 > export abstract class ContextKeyExpr {
580 >
581 > public static false(): ContextKeyExpression {
582 return ContextKeyFalseExpr.INSTANCE;
583 }
584 > public static true(): ContextKeyExpression { contextkey.ts
585 return ContextKeyTrueExpr.INSTANCE;
586 }
587 > public static has(key: string): ContextKeyExpression { contextkey.ts
588 return ContextKeyDefinedExpr.create(key);
589 }
590 > public static equals(key: string, value: any): ContextKeyExpression { contextkey.ts
591 return ContextKeyEqualsExpr.create(key, value);
592 }
593 > public static notEquals(key: string, value: any): ContextKeyExpression { contextkey.ts
594 return ContextKeyNotEqualsExpr.create(key, value);
595 }
596 > public static regex(key: string, value: RegExp): ContextKeyExpression { contextkey.ts
597 return ContextKeyRegexExpr.create(key, value);
598 }
599 > public static in(key: string, value: string): ContextKeyExpression { contextkey.ts
600 return ContextKeyInExpr.create(key, value);
601 }
602 > public static notIn(key: string, value: string): ContextKeyExpression { contextkey.ts
603 return ContextKeyNotInExpr.create(key, value);
604 }
605 > public static not(key: string): ContextKeyExpression { contextkey.ts
606 return ContextKeyNotExpr.create(key);
607 }
608 > public static and(...expr: Array<ContextKeyExpression | undefined | null>): ContextKeyExpression | undefined { contextkey.ts
609 return ContextKeyAndExpr.create(expr, null, true);
610 }
611 > public static or(...expr: Array<ContextKeyExpression | undefined | null>): ContextKeyExpression | undefined { contextkey.ts
612 return ContextKeyOrExpr.create(expr, null, true);
613 }
614 > public static greater(key: string, value: number): ContextKeyExpression { contextkey.ts
615 return ContextKeyGreaterExpr.create(key, value);
616 }
617 > public static greaterEquals(key: string, value: number): ContextKeyExpression { contextkey.ts
618 return ContextKeyGreaterEqualsExpr.create(key, value);
619 }
620 > public static smaller(key: string, value: number): ContextKeyExpression { contextkey.ts
621 return ContextKeySmallerExpr.create(key, value);
622 }
623 > public static smallerEquals(key: string, value: number): ContextKeyExpression { contextkey.ts
624 return ContextKeySmallerEqualsExpr.create(key, value);
625 }
627 > private static _parser = new Parser({ regexParsingWithErrorRecovery: false });
628 > public static deserialize(serialized: string | null | undefined): ContextKeyExpression | undefined {
629 if (serialized === undefined || serialized === null) { // an empty string needs to be handled by the parser to get a corresponding parsing error reported
630 return undefined;
634 return expr;
635 }
637 > }
638 >
639 >
640 > export function validateWhenClauses(whenClauses: string[]): any {
641
642 const parser = new Parser({ regexParsingWithErrorRecovery: false }); // we run with no recovery to guide users to use correct regexes
664 });
665 }
667 > export function expressionsAreEqualWithConstantSubstitution(a: ContextKeyExpression | null | undefined, b: ContextKeyExpression | null | undefined): boolean {
668 const aExpr = a ? a.substituteConstants() : undefined;
669 const bExpr = b ? b.substituteConstants() : undefined;
676 return aExpr.equals(bExpr);
677 }
679 function cmp(a: ContextKeyExpression, b: ContextKeyExpression): number {
680 return a.cmp(b);
681 }
683 > export class ContextKeyFalseExpr implements IContextKeyExpression {
684 > public static INSTANCE = new ContextKeyFalseExpr();
685 >
686 > public readonly type = ContextKeyExprType.False;
687 >
688 > protected constructor() {
689 > }
690 >
691 > public cmp(other: ContextKeyExpression): number {
692 return this.type - other.type;
693 }
695 > public equals(other: ContextKeyExpression): boolean {
696 return (other.type === this.type);
697 }
699 > public substituteConstants(): ContextKeyExpression | undefined {
700 return this;
701 }
703 > public evaluate(context: IContext): boolean {
704 return false;
705 }
707 > public serialize(): string {
708 return 'false';
709 }
711 > public keys(): string[] {
712 return [];
713 }
715 > public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
716 return this;
717 }
719 > public negate(): ContextKeyExpression {
720 return ContextKeyTrueExpr.INSTANCE;
721 }
722 > } contextkey.ts
723 >
724 > export class ContextKeyTrueExpr implements IContextKeyExpression {
725 > public static INSTANCE = new ContextKeyTrueExpr();
726 >
727 > public readonly type = ContextKeyExprType.True;
728 >
729 > protected constructor() {
730 > }
731 >
732 > public cmp(other: ContextKeyExpression): number {
733 return this.type - other.type;
734 }
736 > public equals(other: ContextKeyExpression): boolean {
737 return (other.type === this.type);
738 }
740 > public substituteConstants(): ContextKeyExpression | undefined {
741 return this;
742 }
744 > public evaluate(context: IContext): boolean {
745 return true;
746 }
748 > public serialize(): string {
749 return 'true';
750 }
752 > public keys(): string[] {
753 return [];
754 }
756 > public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
757 return this;
758 }
760 > public negate(): ContextKeyExpression {
761 return ContextKeyFalseExpr.INSTANCE;
762 }
763 > } contextkey.ts
764 >
765 > export class ContextKeyDefinedExpr implements IContextKeyExpression {
766 > public static create(key: string, negated: ContextKeyExpression | null = null): ContextKeyExpression {
767 > const constantValue = CONSTANT_VALUES.get(key);
768 > if (typeof constantValue === 'boolean') {
769 > return constantValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE; contextkey.ts
770 > }
771 > return new ContextKeyDefinedExpr(key, negated); contextkey.ts
772 > }
773 >
774 > public readonly type = ContextKeyExprType.Defined;
775 >
776 > protected constructor(
777 > readonly key: string, contextkey.ts
778 > private negated: ContextKeyExpression | null
779 > ) {
780 > }
782 > public cmp(other: ContextKeyExpression): number {
783 if (other.type !== this.type) {
784 return this.type - other.type;
786 return cmp1(this.key, other.key);
787 }
789 > public equals(other: ContextKeyExpression): boolean {
790 if (other.type === this.type) {
791 return (this.key === other.key);
793 return false;
794 }
796 > public substituteConstants(): ContextKeyExpression | undefined {
797 const constantValue = CONSTANT_VALUES.get(this.key);
798 if (typeof constantValue === 'boolean') {
801 return this;
802 }
804 > public evaluate(context: IContext): boolean {
805 return (!!context.getValue(this.key));
806 }
808 > public serialize(): string {
809 return this.key;
810 }
812 > public keys(): string[] {
813 return [this.key];
814 }
816 > public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
817 return mapFnc.mapDefined(this.key);
818 }
820 > public negate(): ContextKeyExpression {
821 if (!this.negated) {
822 this.negated = ContextKeyNotExpr.create(this.key, this);
824 return this.negated;
825 }
826 > } contextkey.ts
827 >
828 > export class ContextKeyEqualsExpr implements IContextKeyExpression {
829 >
830 > public static create(key: string, value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {
831 > if (typeof value === 'boolean') {
832 > return (value ? ContextKeyDefinedExpr.create(key, negated) : ContextKeyNotExpr.create(key, negated)); contextkey.ts
833 > }
834 > const constantValue = CONSTANT_VALUES.get(key); contextkey.ts
835 > if (typeof constantValue === 'boolean') {
836 > const trueValue = constantValue ? 'true' : 'false'; contextkey.ts
837 > return (value === trueValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE);
838 > }
839 > return new ContextKeyEqualsExpr(key, value, negated); contextkey.ts
840 > } contextkey.ts
841 >
842 > public readonly type = ContextKeyExprType.Equals;
843 >
844 > private constructor(
845 private readonly key: string,
846 private readonly value: any,
848 ) {
849 }
851 > public cmp(other: ContextKeyExpression): number {
852 if (other.type !== this.type) {
853 return this.type - other.type;
855 return cmp2(this.key, this.value, other.key, other.value);
856 }
858 > public equals(other: ContextKeyExpression): boolean {
859 if (other.type === this.type) {
860 return (this.key === other.key && this.value === other.value);
862 return false;
863 }
865 > public substituteConstants(): ContextKeyExpression | undefined {
866 const constantValue = CONSTANT_VALUES.get(this.key);
867 if (typeof constantValue === 'boolean') {
871 return this;
872 }
874 > public evaluate(context: IContext): boolean {
875 // Intentional ==
876 // eslint-disable-next-line eqeqeq
877 return (context.getValue(this.key) == this.value);
878 }
880 > public serialize(): string {
881 return `${this.key} == '${this.value}'`;
882 }
884 > public keys(): string[] {
885 return [this.key];
886 }
888 > public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
889 return mapFnc.mapEquals(this.key, this.value);
890 }
892 > public negate(): ContextKeyExpression {
893 if (!this.negated) {
894 this.negated = ContextKeyNotEqualsExpr.create(this.key, this.value, this);
896 return this.negated;
897 }
898 > } contextkey.ts
899 >
900 > export class ContextKeyInExpr implements IContextKeyExpression {
901 >
902 > public static create(key: string, valueKey: string): ContextKeyInExpr {
903 > return new ContextKeyInExpr(key, valueKey);
904 > }
905 >
906 > public readonly type = ContextKeyExprType.In;
907 > private negated: ContextKeyExpression | null = null;
908 >
909 > private constructor(
910 private readonly key: string,
911 private readonly valueKey: string,
912 ) {
913 }
915 > public cmp(other: ContextKeyExpression): number {
916 if (other.type !== this.type) {
917 return this.type - other.type;
919 return cmp2(this.key, this.valueKey, other.key, other.valueKey);
920 }
922 > public equals(other: ContextKeyExpression): boolean {
923 if (other.type === this.type) {
924 return (this.key === other.key && this.valueKey === other.valueKey);
926 return false;
927 }
929 > public substituteConstants(): ContextKeyExpression | undefined {
930 return this;
931 }
933 > public evaluate(context: IContext): boolean {
934 const source = context.getValue(this.valueKey);
935
964 return false;
965 }
967 > public serialize(): string {
968 return `${this.key} in '${this.valueKey}'`;
969 }
971 > public keys(): string[] {
972 return [this.key, this.valueKey];
973 }
975 > public map(mapFnc: IContextKeyExprMapper): ContextKeyInExpr {
976 return mapFnc.mapIn(this.key, this.valueKey);
977 }
979 > public negate(): ContextKeyExpression {
980 if (!this.negated) {
981 this.negated = ContextKeyNotInExpr.create(this.key, this.valueKey);
983 return this.negated;
984 }
985 > } contextkey.ts
986 >
987 > export class ContextKeyNotInExpr implements IContextKeyExpression {
988 >
989 > public static create(key: string, valueKey: string): ContextKeyNotInExpr {
990 > return new ContextKeyNotInExpr(key, valueKey);
991 > }
992 >
993 > public readonly type = ContextKeyExprType.NotIn;
994 >
995 > private readonly _negated: ContextKeyInExpr;
996 >
997 > private constructor(
998 private readonly key: string,
999 private readonly valueKey: string,
1001 this._negated = ContextKeyInExpr.create(key, valueKey);
1002 }
1003 > contextkey.ts
1004 > public cmp(other: ContextKeyExpression): number {
1005 if (other.type !== this.type) {
1006 return this.type - other.type;
1008 return this._negated.cmp(other._negated);
1009 }
1010 > contextkey.ts
1011 > public equals(other: ContextKeyExpression): boolean {
1012 if (other.type === this.type) {
1013 return this._negated.equals(other._negated);
1015 return false;
1016 }
1017 > contextkey.ts
1018 > public substituteConstants(): ContextKeyExpression | undefined {
1019 return this;
1020 }
1021 > contextkey.ts
1022 > public evaluate(context: IContext): boolean {
1023 return !this._negated.evaluate(context);
1024 }
1025 > contextkey.ts
1026 > public serialize(): string {
1027 return `${this.key} not in '${this.valueKey}'`;
1028 }
1029 > contextkey.ts
1030 > public keys(): string[] {
1031 return this._negated.keys();
1032 }
1033 > contextkey.ts
1034 > public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1035 return mapFnc.mapNotIn(this.key, this.valueKey);
1036 }
1037 > contextkey.ts
1038 > public negate(): ContextKeyExpression {
1039 return this._negated;
1040 }
1041 > } contextkey.ts
1042 >
1043 > export class ContextKeyNotEqualsExpr implements IContextKeyExpression {
1044 >
1045 > public static create(key: string, value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {
1046 > if (typeof value === 'boolean') {
1047 > if (value) { contextkey.ts
1048 > return ContextKeyNotExpr.create(key, negated); contextkey.ts
1049 > }
1050 > return ContextKeyDefinedExpr.create(key, negated); contextkey.ts
1051 > }
1052 > const constantValue = CONSTANT_VALUES.get(key); contextkey.ts
1053 > if (typeof constantValue === 'boolean') {
1054 > const falseValue = constantValue ? 'true' : 'false'; contextkey.ts
1055 > return (value === falseValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE);
1056 > }
1057 > return new ContextKeyNotEqualsExpr(key, value, negated); contextkey.ts
1058 > } contextkey.ts
1059 >
1060 > public readonly type = ContextKeyExprType.NotEquals;
1061 >
1062 > private constructor(
1063 private readonly key: string,
1064 private readonly value: any,
1066 ) {
1067 }
1068 > contextkey.ts
1069 > public cmp(other: ContextKeyExpression): number {
1070 if (other.type !== this.type) {
1071 return this.type - other.type;
1073 return cmp2(this.key, this.value, other.key, other.value);
1074 }
1075 > contextkey.ts
1076 > public equals(other: ContextKeyExpression): boolean {
1077 if (other.type === this.type) {
1078 return (this.key === other.key && this.value === other.value);
1080 return false;
1081 }
1082 > contextkey.ts
1083 > public substituteConstants(): ContextKeyExpression | undefined {
1084 const constantValue = CONSTANT_VALUES.get(this.key);
1085 if (typeof constantValue === 'boolean') {
1089 return this;
1090 }
1091 > contextkey.ts
1092 > public evaluate(context: IContext): boolean {
1093 // Intentional !=
1094 // eslint-disable-next-line eqeqeq
1095 return (context.getValue(this.key) != this.value);
1096 }
1097 > contextkey.ts
1098 > public serialize(): string {
1099 return `${this.key} != '${this.value}'`;
1100 }
1101 > contextkey.ts
1102 > public keys(): string[] {
1103 return [this.key];
1104 }
1105 > contextkey.ts
1106 > public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1107 return mapFnc.mapNotEquals(this.key, this.value);
1108 }
1109 > contextkey.ts
1110 > public negate(): ContextKeyExpression {
1111 if (!this.negated) {
1112 this.negated = ContextKeyEqualsExpr.create(this.key, this.value, this);
1114 return this.negated;
1115 }
1116 > } contextkey.ts
1117 >
1118 > export class ContextKeyNotExpr implements IContextKeyExpression {
1119 >
1120 > public static create(key: string, negated: ContextKeyExpression | null = null): ContextKeyExpression {
1121 > const constantValue = CONSTANT_VALUES.get(key);
1122 > if (typeof constantValue === 'boolean') {
1123 > return (constantValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE); contextkey.ts
1124 > }
1125 > return new ContextKeyNotExpr(key, negated); contextkey.ts
1126 > }
1127 >
1128 > public readonly type = ContextKeyExprType.Not;
1129 >
1130 > private constructor(
1131 private readonly key: string,
1132 private negated: ContextKeyExpression | null
1133 ) {
1134 }
1135 > contextkey.ts
1136 > public cmp(other: ContextKeyExpression): number {
1137 if (other.type !== this.type) {
1138 return this.type - other.type;
1140 return cmp1(this.key, other.key);
1141 }
1142 > contextkey.ts
1143 > public equals(other: ContextKeyExpression): boolean {
1144 if (other.type === this.type) {
1145 return (this.key === other.key);
1147 return false;
1148 }
1149 > contextkey.ts
1150 > public substituteConstants(): ContextKeyExpression | undefined {
1151 const constantValue = CONSTANT_VALUES.get(this.key);
1152 if (typeof constantValue === 'boolean') {
1155 return this;
1156 }
1157 > contextkey.ts
1158 > public evaluate(context: IContext): boolean {
1159 return (!context.getValue(this.key));
1160 }
1161 > contextkey.ts
1162 > public serialize(): string {
1163 return `!${this.key}`;
1164 }
1165 > contextkey.ts
1166 > public keys(): string[] {
1167 return [this.key];
1168 }
1169 > contextkey.ts
1170 > public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1171 return mapFnc.mapNot(this.key);
1172 }
1173 > contextkey.ts
1174 > public negate(): ContextKeyExpression {
1175 if (!this.negated) {
1176 this.negated = ContextKeyDefinedExpr.create(this.key, this);
1178 return this.negated;
1179 }
1180 > } contextkey.ts
1181 >
1182 function withFloatOrStr<T extends ContextKeyExpression>(value: any, callback: (value: number | string) => T): T | ContextKeyFalseExpr {
1183 if (typeof value === 'string') {
1192 return ContextKeyFalseExpr.INSTANCE;
1193 }
1194 > contextkey.ts
1195 > export class ContextKeyGreaterExpr implements IContextKeyExpression {
1196 >
1197 > public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {
1198 > return withFloatOrStr(_value, (value) => new ContextKeyGreaterExpr(key, value, negated));
1199 > }
1200 >
1201 > public readonly type = ContextKeyExprType.Greater;
1202 >
1203 > private constructor(
1204 private readonly key: string,
1205 private readonly value: number | string,
1206 private negated: ContextKeyExpression | null
1207 ) { }
1208 > contextkey.ts
1209 > public cmp(other: ContextKeyExpression): number {
1210 if (other.type !== this.type) {
1211 return this.type - other.type;
1213 return cmp2(this.key, this.value, other.key, other.value);
1214 }
1215 > contextkey.ts
1216 > public equals(other: ContextKeyExpression): boolean {
1217 if (other.type === this.type) {
1218 return (this.key === other.key && this.value === other.value);
1220 return false;
1221 }
1222 > contextkey.ts
1223 > public substituteConstants(): ContextKeyExpression | undefined {
1224 return this;
1225 }
1226 > contextkey.ts
1227 > public evaluate(context: IContext): boolean {
1228 if (typeof this.value === 'string') {
1229 return false;
1231 return (parseFloat(context.getValue<any>(this.key)) > this.value);
1232 }
1233 > contextkey.ts
1234 > public serialize(): string {
1235 return `${this.key} > ${this.value}`;
1236 }
1237 > contextkey.ts
1238 > public keys(): string[] {
1239 return [this.key];
1240 }
1241 > contextkey.ts
1242 > public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1243 return mapFnc.mapGreater(this.key, this.value);
1244 }
1245 > contextkey.ts
1246 > public negate(): ContextKeyExpression {
1247 if (!this.negated) {
1248 this.negated = ContextKeySmallerEqualsExpr.create(this.key, this.value, this);
1250 return this.negated;
1251 }
1252 > } contextkey.ts
1253 >
1254 > export class ContextKeyGreaterEqualsExpr implements IContextKeyExpression {
1255 >
1256 > public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {
1257 > return withFloatOrStr(_value, (value) => new ContextKeyGreaterEqualsExpr(key, value, negated));
1258 > }
1259 >
1260 > public readonly type = ContextKeyExprType.GreaterEquals;
1261 >
1262 > private constructor(
1263 private readonly key: string,
1264 private readonly value: number | string,
1265 private negated: ContextKeyExpression | null
1266 ) { }
1267 > contextkey.ts
1268 > public cmp(other: ContextKeyExpression): number {
1269 if (other.type !== this.type) {
1270 return this.type - other.type;
1272 return cmp2(this.key, this.value, other.key, other.value);
1273 }
1274 > contextkey.ts
1275 > public equals(other: ContextKeyExpression): boolean {
1276 if (other.type === this.type) {
1277 return (this.key === other.key && this.value === other.value);
1279 return false;
1280 }
1281 > contextkey.ts
1282 > public substituteConstants(): ContextKeyExpression | undefined {
1283 return this;
1284 }
1285 > contextkey.ts
1286 > public evaluate(context: IContext): boolean {
1287 if (typeof this.value === 'string') {
1288 return false;
1290 return (parseFloat(context.getValue<any>(this.key)) >= this.value);
1291 }
1292 > contextkey.ts
1293 > public serialize(): string {
1294 return `${this.key} >= ${this.value}`;
1295 }
1296 > contextkey.ts
1297 > public keys(): string[] {
1298 return [this.key];
1299 }
1300 > contextkey.ts
1301 > public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1302 return mapFnc.mapGreaterEquals(this.key, this.value);
1303 }
1304 > contextkey.ts
1305 > public negate(): ContextKeyExpression {
1306 if (!this.negated) {
1307 this.negated = ContextKeySmallerExpr.create(this.key, this.value, this);
1309 return this.negated;
1310 }
1311 > } contextkey.ts
1312 >
1313 > export class ContextKeySmallerExpr implements IContextKeyExpression {
1314 >
1315 > public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {
1316 > return withFloatOrStr(_value, (value) => new ContextKeySmallerExpr(key, value, negated));
1317 > }
1318 >
1319 > public readonly type = ContextKeyExprType.Smaller;
1320 >
1321 > private constructor(
1322 private readonly key: string,
1323 private readonly value: number | string,
1325 ) {
1326 }
1327 > contextkey.ts
1328 > public cmp(other: ContextKeyExpression): number {
1329 if (other.type !== this.type) {
1330 return this.type - other.type;
1332 return cmp2(this.key, this.value, other.key, other.value);
1333 }
1334 > contextkey.ts
1335 > public equals(other: ContextKeyExpression): boolean {
1336 if (other.type === this.type) {
1337 return (this.key === other.key && this.value === other.value);
1339 return false;
1340 }
1341 > contextkey.ts
1342 > public substituteConstants(): ContextKeyExpression | undefined {
1343 return this;
1344 }
1345 > contextkey.ts
1346 > public evaluate(context: IContext): boolean {
1347 if (typeof this.value === 'string') {
1348 return false;
1350 return (parseFloat(context.getValue<any>(this.key)) < this.value);
1351 }
1352 > contextkey.ts
1353 > public serialize(): string {
1354 return `${this.key} < ${this.value}`;
1355 }
1356 > contextkey.ts
1357 > public keys(): string[] {
1358 return [this.key];
1359 }
1360 > contextkey.ts
1361 > public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1362 return mapFnc.mapSmaller(this.key, this.value);
1363 }
1364 > contextkey.ts
1365 > public negate(): ContextKeyExpression {
1366 if (!this.negated) {
1367 this.negated = ContextKeyGreaterEqualsExpr.create(this.key, this.value, this);
1369 return this.negated;
1370 }
1371 > } contextkey.ts
1372 >
1373 > export class ContextKeySmallerEqualsExpr implements IContextKeyExpression {
1374 >
1375 > public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {
1376 > return withFloatOrStr(_value, (value) => new ContextKeySmallerEqualsExpr(key, value, negated));
1377 > }
1378 >
1379 > public readonly type = ContextKeyExprType.SmallerEquals;
1380 >
1381 > private constructor(
1382 private readonly key: string,
1383 private readonly value: number | string,
1385 ) {
1386 }
1387 > contextkey.ts
1388 > public cmp(other: ContextKeyExpression): number {
1389 if (other.type !== this.type) {
1390 return this.type - other.type;
1392 return cmp2(this.key, this.value, other.key, other.value);
1393 }
1394 > contextkey.ts
1395 > public equals(other: ContextKeyExpression): boolean {
1396 if (other.type === this.type) {
1397 return (this.key === other.key && this.value === other.value);
1399 return false;
1400 }
1401 > contextkey.ts
1402 > public substituteConstants(): ContextKeyExpression | undefined {
1403 return this;
1404 }
1405 > contextkey.ts
1406 > public evaluate(context: IContext): boolean {
1407 if (typeof this.value === 'string') {
1408 return false;
1410 return (parseFloat(context.getValue<any>(this.key)) <= this.value);
1411 }
1412 > contextkey.ts
1413 > public serialize(): string {
1414 return `${this.key} <= ${this.value}`;
1415 }
1416 > contextkey.ts
1417 > public keys(): string[] {
1418 return [this.key];
1419 }
1420 > contextkey.ts
1421 > public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1422 return mapFnc.mapSmallerEquals(this.key, this.value);
1423 }
1424 > contextkey.ts
1425 > public negate(): ContextKeyExpression {
1426 if (!this.negated) {
1427 this.negated = ContextKeyGreaterExpr.create(this.key, this.value, this);
1429 return this.negated;
1430 }
1431 > } contextkey.ts
1432 >
1433 > export class ContextKeyRegexExpr implements IContextKeyExpression {
1434 >
1435 > public static create(key: string, regexp: RegExp | null): ContextKeyRegexExpr {
1436 > return new ContextKeyRegexExpr(key, regexp);
1437 > }
1438 >
1439 > public readonly type = ContextKeyExprType.Regex;
1440 > private negated: ContextKeyExpression | null = null;
1441 >
1442 > private constructor(
1443 private readonly key: string,
1444 private readonly regexp: RegExp | null
1446 //
1447 }
1448 > contextkey.ts
1449 > public cmp(other: ContextKeyExpression): number {
1450 if (other.type !== this.type) {
1451 return this.type - other.type;
1467 return 0;
1468 }
1469 > contextkey.ts
1470 > public equals(other: ContextKeyExpression): boolean {
1471 if (other.type === this.type) {
1472 const thisSource = this.regexp ? this.regexp.source : '';
1476 return false;
1477 }
1478 > contextkey.ts
1479 > public substituteConstants(): ContextKeyExpression | undefined {
1480 return this;
1481 }
1482 > contextkey.ts
1483 > public evaluate(context: IContext): boolean {
1484 const value = context.getValue<any>(this.key);
1485 return this.regexp ? this.regexp.test(value) : false;
1486 }
1487 > contextkey.ts
1488 > public serialize(): string {
1489 const value = this.regexp
1490 ? `/${this.regexp.source}/${this.regexp.flags}`
1492 return `${this.key} =~ ${value}`;
1493 }
1494 > contextkey.ts
1495 > public keys(): string[] {
1496 return [this.key];
1497 }
1498 > contextkey.ts
1499 > public map(mapFnc: IContextKeyExprMapper): ContextKeyRegexExpr {
1500 return mapFnc.mapRegex(this.key, this.regexp);
1501 }
1502 > contextkey.ts
1503 > public negate(): ContextKeyExpression {
1504 if (!this.negated) {
1505 this.negated = ContextKeyNotRegexExpr.create(this);
1507 return this.negated;
1508 }
1509 > } contextkey.ts
1510 >
1511 > export class ContextKeyNotRegexExpr implements IContextKeyExpression {
1512 >
1513 > public static create(actual: ContextKeyRegexExpr): ContextKeyExpression {
1514 > return new ContextKeyNotRegexExpr(actual);
1515 > }
1516 >
1517 > public readonly type = ContextKeyExprType.NotRegex;
1518 >
1519 > private constructor(private readonly _actual: ContextKeyRegexExpr) {
1520 //
1521 }
1522 > contextkey.ts
1523 > public cmp(other: ContextKeyExpression): number {
1524 if (other.type !== this.type) {
1525 return this.type - other.type;
1527 return this._actual.cmp(other._actual);
1528 }
1529 > contextkey.ts
1530 > public equals(other: ContextKeyExpression): boolean {
1531 if (other.type === this.type) {
1532 return this._actual.equals(other._actual);
1534 return false;
1535 }
1536 > contextkey.ts
1537 > public substituteConstants(): ContextKeyExpression | undefined {
1538 return this;
1539 }
1540 > contextkey.ts
1541 > public evaluate(context: IContext): boolean {
1542 return !this._actual.evaluate(context);
1543 }
1544 > contextkey.ts
1545 > public serialize(): string {
1546 return `!(${this._actual.serialize()})`;
1547 }
1548 > contextkey.ts
1549 > public keys(): string[] {
1550 return this._actual.keys();
1551 }
1552 > contextkey.ts
1553 > public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1554 return new ContextKeyNotRegexExpr(this._actual.map(mapFnc));
1555 }
1556 > contextkey.ts
1557 > public negate(): ContextKeyExpression {
1558 return this._actual;
1559 }
1560 > } contextkey.ts
1561 >
1562 > /**
1563 > * @returns the same instance if nothing changed.
1564 > */
1565 function eliminateConstantsInArray(arr: ContextKeyExpression[]): (ContextKeyExpression | undefined)[] {
1566 // Allocate array only if there is a difference
1591 return newArr;
1592 }
1593 > contextkey.ts
1594 > export class ContextKeyAndExpr implements IContextKeyExpression {
1595 >
1596 > public static create(_expr: ReadonlyArray<ContextKeyExpression | null | undefined>, negated: ContextKeyExpression | null, extraRedundantCheck: boolean): ContextKeyExpression | undefined {
1597 > return ContextKeyAndExpr._normalizeArr(_expr, negated, extraRedundantCheck);
1598 > }
1599 >
1600 > public readonly type = ContextKeyExprType.And;
1601 >
1602 > private constructor(
1603 public readonly expr: ContextKeyExpression[],
1604 private negated: ContextKeyExpression | null
1605 ) {
1606 }
1607 > contextkey.ts
1608 > public cmp(other: ContextKeyExpression): number {
1609 if (other.type !== this.type) {
1610 return this.type - other.type;
1624 return 0;
1625 }
1626 > contextkey.ts
1627 > public equals(other: ContextKeyExpression): boolean {
1628 if (other.type === this.type) {
1629 if (this.expr.length !== other.expr.length) {
1639 return false;
1640 }
1641 > contextkey.ts
1642 > public substituteConstants(): ContextKeyExpression | undefined {
1643 const exprArr = eliminateConstantsInArray(this.expr);
1644 if (exprArr === this.expr) {
1648 return ContextKeyAndExpr.create(exprArr, this.negated, false);
1649 }
1650 > contextkey.ts
1651 > public evaluate(context: IContext): boolean {
1652 for (let i = 0, len = this.expr.length; i < len; i++) {
1653 if (!this.expr[i].evaluate(context)) {
1657 return true;
1658 }
1659 > contextkey.ts
1660 > private static _normalizeArr(arr: ReadonlyArray<ContextKeyExpression | null | undefined>, negated: ContextKeyExpression | null, extraRedundantCheck: boolean): ContextKeyExpression | undefined {
1661 const expr: ContextKeyExpression[] = [];
1662 let hasTrue = false;
1762 return new ContextKeyAndExpr(expr, negated);
1763 }
1764 > contextkey.ts
1765 > public serialize(): string {
1766 return this.expr.map(e => e.serialize()).join(' && ');
1767 }
1768 > contextkey.ts
1769 > public keys(): string[] {
1770 const result: string[] = [];
1771 for (const expr of this.expr) {
1774 return result;
1775 }
1776 > contextkey.ts
1777 > public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1778 return new ContextKeyAndExpr(this.expr.map(expr => expr.map(mapFnc)), null);
1779 }
1780 > contextkey.ts
1781 > public negate(): ContextKeyExpression {
1782 if (!this.negated) {
1783 const result: ContextKeyExpression[] = [];
1789 return this.negated;
1790 }
1791 > } contextkey.ts
1792 >
1793 > export class ContextKeyOrExpr implements IContextKeyExpression {
1794 >
1795 > public static create(_expr: ReadonlyArray<ContextKeyExpression | null | undefined>, negated: ContextKeyExpression | null, extraRedundantCheck: boolean): ContextKeyExpression | undefined {
1796 > return ContextKeyOrExpr._normalizeArr(_expr, negated, extraRedundantCheck);
1797 > }
1798 >
1799 > public readonly type = ContextKeyExprType.Or;
1800 >
1801 > private constructor(
1802 public readonly expr: ContextKeyExpression[],
1803 private negated: ContextKeyExpression | null
1804 ) {
1805 }
1806 > contextkey.ts
1807 > public cmp(other: ContextKeyExpression): number {
1808 if (other.type !== this.type) {
1809 return this.type - other.type;
1823 return 0;
1824 }
1825 > contextkey.ts
1826 > public equals(other: ContextKeyExpression): boolean {
1827 if (other.type === this.type) {
1828 if (this.expr.length !== other.expr.length) {
1838 return false;
1839 }
1840 > contextkey.ts
1841 > public substituteConstants(): ContextKeyExpression | undefined {
1842 const exprArr = eliminateConstantsInArray(this.expr);
1843 if (exprArr === this.expr) {
1847 return ContextKeyOrExpr.create(exprArr, this.negated, false);
1848 }
1849 > contextkey.ts
1850 > public evaluate(context: IContext): boolean {
1851 for (let i = 0, len = this.expr.length; i < len; i++) {
1852 if (this.expr[i].evaluate(context)) {
1856 return false;
1857 }
1858 > contextkey.ts
1859 > private static _normalizeArr(arr: ReadonlyArray<ContextKeyExpression | null | undefined>, negated: ContextKeyExpression | null, extraRedundantCheck: boolean): ContextKeyExpression | undefined {
1860 let expr: ContextKeyExpression[] = [];
1861 let hasFalse = false;
1932 return new ContextKeyOrExpr(expr, negated);
1933 }
1934 > contextkey.ts
1935 > public serialize(): string {
1936 return this.expr.map(e => e.serialize()).join(' || ');
1937 }
1938 > contextkey.ts
1939 > public keys(): string[] {
1940 const result: string[] = [];
1941 for (const expr of this.expr) {
1944 return result;
1945 }
1946 > contextkey.ts
1947 > public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1948 return new ContextKeyOrExpr(this.expr.map(expr => expr.map(mapFnc)), null);
1949 }
1950 > contextkey.ts
1951 > public negate(): ContextKeyExpression {
1952 if (!this.negated) {
1953 const result: ContextKeyExpression[] = [];
1976 return this.negated;
1977 }
1978 > } contextkey.ts
1979 >
1980 > export interface ContextKeyInfo {
1981 > readonly key: string;
1982 > readonly type?: string;
1983 > readonly description?: string;
1984 > }
1985 >
1986 > export class RawContextKey<T extends ContextKeyValue> extends ContextKeyDefinedExpr {
1987 >
1988 > private static _info: ContextKeyInfo[] = [];
1989 >
1990 > static all(): IterableIterator<ContextKeyInfo> {
1991 return RawContextKey._info.values();
1992 }
1993 > contextkey.ts
1994 > private readonly _defaultValue: T | undefined;
1995 >
1996 > constructor(key: string, defaultValue: T | undefined, metaOrHide?: string | true | { type: string; description: string }) {
1997 > super(key, null); contextkey.ts
1998 > this._defaultValue = defaultValue;
1999 >
2000 > // collect all context keys into a central place
2001 > if (typeof metaOrHide === 'object') {
2002 RawContextKey._info.push({ ...metaOrHide, key });
2003 > } else if (metaOrHide !== true) { contextkey.ts
2004 > RawContextKey._info.push({ key, description: metaOrHide, type: defaultValue !== null && defaultValue !== undefined ? typeof defaultValue : undefined }); contextkey.ts
2005 > }
2006 > } contextkey.ts
2007 > contextkey.ts
2008 > public bindTo(target: IContextKeyService): IContextKey<T> {
2009 return target.createKey(this.key, this._defaultValue);
2010 }
2011 > contextkey.ts
2012 > public getValue(target: IContextKeyService): T | undefined {
2013 return target.getContextKeyValue<T>(this.key);
2014 }
2015 > contextkey.ts
2016 > public toNegated(): ContextKeyExpression {
2017 return this.negate();
2018 }
2019 > contextkey.ts
2020 > public isEqualTo(value: any): ContextKeyExpression {
2021 return ContextKeyEqualsExpr.create(this.key, value);
2022 }
2023 > contextkey.ts
2024 > public notEqualsTo(value: any): ContextKeyExpression {
2025 return ContextKeyNotEqualsExpr.create(this.key, value);
2026 }
2027 > contextkey.ts
2028 > public greater(value: any): ContextKeyExpression {
2029 return ContextKeyGreaterExpr.create(this.key, value);
2030 }
2031 > } contextkey.ts
2032 >
2033 > export type ContextKeyValue = null | undefined | boolean | number | string
2034 > | Array<null | undefined | boolean | number | string>
2035 > | Record<string, null | undefined | boolean | number | string>;
2036 >
2037 > export interface IContext {
2038 > getValue<T extends ContextKeyValue = ContextKeyValue>(key: string): T | undefined;
2039 > }
2040 >
2041 > export interface IContextKey<T extends ContextKeyValue = ContextKeyValue> {
2042 > set(value: T): void;
2043 > reset(): void;
2044 > get(): T | undefined;
2045 > }
2046 >
2047 > export interface IContextKeyServiceTarget {
2048 > parentElement: IContextKeyServiceTarget | null;
2049 > setAttribute(attr: string, value: string): void;
2050 > removeAttribute(attr: string): void;
2051 > hasAttribute(attr: string): boolean;
2052 > getAttribute(attr: string): string | null;
2053 > }
2054 >
2055 > export const IContextKeyService = createDecorator<IContextKeyService>('contextKeyService');
2056 >
2057 > export interface IReadableSet<T> {
2058 > has(value: T): boolean;
2059 > }
2060 >
2061 > export interface IContextKeyChangeEvent {
2062 > affectsSome(keys: IReadableSet<string>): boolean;
2063 > allKeysContainedIn(keys: IReadableSet<string>): boolean;
2064 > }
2065 >
2066 > export type IScopedContextKeyService = IContextKeyService & IDisposable;
2067 >
2068 > export interface IContextKeyService {
2069 > readonly _serviceBrand: undefined;
2070 >
2071 > readonly onDidChangeContext: Event<IContextKeyChangeEvent>;
2072 > bufferChangeEvents(callback: Function): void;
2073 >
2074 > createKey<T extends ContextKeyValue>(key: string, defaultValue: T | undefined): IContextKey<T>;
2075 > contextMatchesRules(rules: ContextKeyExpression | undefined): boolean;
2076 > getContextKeyValue<T>(key: string): T | undefined;
2077 >
2078 > createScoped(target: IContextKeyServiceTarget): IScopedContextKeyService;
2079 > createOverlay(overlay: Iterable<[string, any]>): IContextKeyService;
2080 > getContext(target: IContextKeyServiceTarget | null): IContext;
2081 >
2082 > updateParent(parentContextKeyService: IContextKeyService): void;
2083 > }
2084 >
2085 function cmp1(key1: string, key2: string): number {
2086 if (key1 < key2) {
2092 return 0;
2093 }
2094 > contextkey.ts
2095 function cmp2(key1: string, value1: any, key2: string, value2: any): number {
2096 if (key1 < key2) {
2108 return 0;
2109 }
2110 > contextkey.ts
2111 > /**
2112 > * Returns true if it is provable `p` implies `q`.
2113 > */
2114 > export function implies(p: ContextKeyExpression, q: ContextKeyExpression): boolean {
2115
2116 if (p.type === ContextKeyExprType.False || q.type === ContextKeyExprType.True) {
2152 return p.equals(q);
2153 }
2154 > contextkey.ts
2155 > /**
2156 > * Returns true if all elements in `p` are also present in `q`.
2157 > * The two arrays are assumed to be sorted
2158 > */
2159 function allElementsIncluded(p: ContextKeyExpression[], q: ContextKeyExpression[]): boolean {
2160 let pIndex = 0;
2175 return (pIndex === p.length);
2176 }
2177 > contextkey.ts
2178 function getTerminals(node: ContextKeyExpression) {
2179 if (node.type === ContextKeyExprType.Or) {
src/vs/base/common/codiconsLibrary.ts 758 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- codiconsLibrary.ts
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 > import { register } from './codiconsUtil.js';
6 >
7 >
8 > // This file is automatically generated by (microsoft/vscode-codicons)/scripts/export-to-ts.js
9 > // Please don't edit it, as your changes will be overwritten.
10 > // Instead, add mappings to codiconsDerived in codicons.ts.
11 > export const codiconsLibrary = {
12 > add: register('add', 0xea60),
13 > plus: register('plus', 0xea60),
14 > gistNew: register('gist-new', 0xea60),
15 > repoCreate: register('repo-create', 0xea60),
16 > lightbulb: register('lightbulb', 0xea61),
17 > lightBulb: register('light-bulb', 0xea61),
18 > repo: register('repo', 0xea62),
19 > repoDelete: register('repo-delete', 0xea62),
20 > gistFork: register('gist-fork', 0xea63),
21 > repoForked: register('repo-forked', 0xea63),
22 > gitPullRequest: register('git-pull-request', 0xea64),
23 > gitPullRequestAbandoned: register('git-pull-request-abandoned', 0xea64),
24 > recordKeys: register('record-keys', 0xea65),
25 > keyboard: register('keyboard', 0xea65),
26 > tag: register('tag', 0xea66),
27 > gitPullRequestLabel: register('git-pull-request-label', 0xea66),
28 > tagAdd: register('tag-add', 0xea66),
29 > tagRemove: register('tag-remove', 0xea66),
30 > person: register('person', 0xea67),
31 > personFollow: register('person-follow', 0xea67),
32 > personOutline: register('person-outline', 0xea67),
33 > personFilled: register('person-filled', 0xea67),
34 > sourceControl: register('source-control', 0xea68),
35 > mirror: register('mirror', 0xea69),
36 > mirrorPublic: register('mirror-public', 0xea69),
37 > star: register('star', 0xea6a),
38 > starAdd: register('star-add', 0xea6a),
39 > starDelete: register('star-delete', 0xea6a),
40 > starEmpty: register('star-empty', 0xea6a),
41 > comment: register('comment', 0xea6b),
42 > commentAdd: register('comment-add', 0xea6b),
43 > alert: register('alert', 0xea6c),
44 > warning: register('warning', 0xea6c),
45 > search: register('search', 0xea6d),
46 > searchSave: register('search-save', 0xea6d),
47 > logOut: register('log-out', 0xea6e),
48 > signOut: register('sign-out', 0xea6e),
49 > logIn: register('log-in', 0xea6f),
50 > signIn: register('sign-in', 0xea6f),
51 > eye: register('eye', 0xea70),
52 > eyeUnwatch: register('eye-unwatch', 0xea70),
53 > eyeWatch: register('eye-watch', 0xea70),
54 > circleFilled: register('circle-filled', 0xea71),
55 > primitiveDot: register('primitive-dot', 0xea71),
56 > closeDirty: register('close-dirty', 0xea71),
57 > debugBreakpoint: register('debug-breakpoint', 0xea71),
58 > debugBreakpointDisabled: register('debug-breakpoint-disabled', 0xea71),
59 > debugHint: register('debug-hint', 0xea71),
60 > terminalDecorationSuccess: register('terminal-decoration-success', 0xea71),
61 > primitiveSquare: register('primitive-square', 0xea72),
62 > edit: register('edit', 0xea73),
63 > pencil: register('pencil', 0xea73),
64 > info: register('info', 0xea74),
65 > issueOpened: register('issue-opened', 0xea74),
66 > gistPrivate: register('gist-private', 0xea75),
67 > gitForkPrivate: register('git-fork-private', 0xea75),
68 > lock: register('lock', 0xea75),
69 > mirrorPrivate: register('mirror-private', 0xea75),
70 > close: register('close', 0xea76),
71 > removeClose: register('remove-close', 0xea76),
72 > x: register('x', 0xea76),
73 > repoSync: register('repo-sync', 0xea77),
74 > sync: register('sync', 0xea77),
75 > clone: register('clone', 0xea78),
76 > desktopDownload: register('desktop-download', 0xea78),
77 > beaker: register('beaker', 0xea79),
78 > microscope: register('microscope', 0xea79),
79 > vm: register('vm', 0xea7a),
80 > deviceDesktop: register('device-desktop', 0xea7a),
81 > file: register('file', 0xea7b),
82 > more: register('more', 0xea7c),
83 > ellipsis: register('ellipsis', 0xea7c),
84 > kebabHorizontal: register('kebab-horizontal', 0xea7c),
85 > mailReply: register('mail-reply', 0xea7d),
86 > reply: register('reply', 0xea7d),
87 > organization: register('organization', 0xea7e),
88 > organizationFilled: register('organization-filled', 0xea7e),
89 > organizationOutline: register('organization-outline', 0xea7e),
90 > newFile: register('new-file', 0xea7f),
91 > fileAdd: register('file-add', 0xea7f),
92 > newFolder: register('new-folder', 0xea80),
93 > fileDirectoryCreate: register('file-directory-create', 0xea80),
94 > trash: register('trash', 0xea81),
95 > trashcan: register('trashcan', 0xea81),
96 > history: register('history', 0xea82),
97 > clock: register('clock', 0xea82),
98 > folder: register('folder', 0xea83),
99 > fileDirectory: register('file-directory', 0xea83),
100 > symbolFolder: register('symbol-folder', 0xea83),
101 > logoGithub: register('logo-github', 0xea84),
102 > markGithub: register('mark-github', 0xea84),
103 > github: register('github', 0xea84),
104 > terminal: register('terminal', 0xea85),
105 > console: register('console', 0xea85),
106 > repl: register('repl', 0xea85),
107 > zap: register('zap', 0xea86),
108 > symbolEvent: register('symbol-event', 0xea86),
109 > error: register('error', 0xea87),
110 > stop: register('stop', 0xea87),
111 > variable: register('variable', 0xea88),
112 > symbolVariable: register('symbol-variable', 0xea88),
113 > array: register('array', 0xea8a),
114 > symbolArray: register('symbol-array', 0xea8a),
115 > symbolModule: register('symbol-module', 0xea8b),
116 > symbolPackage: register('symbol-package', 0xea8b),
117 > symbolNamespace: register('symbol-namespace', 0xea8b),
118 > symbolObject: register('symbol-object', 0xea8b),
119 > symbolMethod: register('symbol-method', 0xea8c),
120 > symbolFunction: register('symbol-function', 0xea8c),
121 > symbolConstructor: register('symbol-constructor', 0xea8c),
122 > symbolBoolean: register('symbol-boolean', 0xea8f),
123 > symbolNull: register('symbol-null', 0xea8f),
124 > symbolNumeric: register('symbol-numeric', 0xea90),
125 > symbolNumber: register('symbol-number', 0xea90),
126 > symbolStructure: register('symbol-structure', 0xea91),
127 > symbolStruct: register('symbol-struct', 0xea91),
128 > symbolParameter: register('symbol-parameter', 0xea92),
129 > symbolTypeParameter: register('symbol-type-parameter', 0xea92),
130 > symbolKey: register('symbol-key', 0xea93),
131 > symbolText: register('symbol-text', 0xea93),
132 > symbolReference: register('symbol-reference', 0xea94),
133 > goToFile: register('go-to-file', 0xea94),
134 > symbolEnum: register('symbol-enum', 0xea95),
135 > symbolValue: register('symbol-value', 0xea95),
136 > symbolRuler: register('symbol-ruler', 0xea96),
137 > symbolUnit: register('symbol-unit', 0xea96),
138 > activateBreakpoints: register('activate-breakpoints', 0xea97),
139 > archive: register('archive', 0xea98),
140 > arrowBoth: register('arrow-both', 0xea99),
141 > arrowDown: register('arrow-down', 0xea9a),
142 > arrowLeft: register('arrow-left', 0xea9b),
143 > arrowRight: register('arrow-right', 0xea9c),
144 > arrowSmallDown: register('arrow-small-down', 0xea9d),
145 > arrowSmallLeft: register('arrow-small-left', 0xea9e),
146 > arrowSmallRight: register('arrow-small-right', 0xea9f),
147 > arrowSmallUp: register('arrow-small-up', 0xeaa0),
148 > arrowUp: register('arrow-up', 0xeaa1),
149 > bell: register('bell', 0xeaa2),
150 > bold: register('bold', 0xeaa3),
151 > book: register('book', 0xeaa4),
152 > bookmark: register('bookmark', 0xeaa5),
153 > debugBreakpointConditionalUnverified: register('debug-breakpoint-conditional-unverified', 0xeaa6),
154 > debugBreakpointConditional: register('debug-breakpoint-conditional', 0xeaa7),
155 > debugBreakpointConditionalDisabled: register('debug-breakpoint-conditional-disabled', 0xeaa7),
156 > debugBreakpointDataUnverified: register('debug-breakpoint-data-unverified', 0xeaa8),
157 > debugBreakpointData: register('debug-breakpoint-data', 0xeaa9),
158 > debugBreakpointDataDisabled: register('debug-breakpoint-data-disabled', 0xeaa9),
159 > debugBreakpointLogUnverified: register('debug-breakpoint-log-unverified', 0xeaaa),
160 > debugBreakpointLog: register('debug-breakpoint-log', 0xeaab),
161 > debugBreakpointLogDisabled: register('debug-breakpoint-log-disabled', 0xeaab),
162 > briefcase: register('briefcase', 0xeaac),
163 > broadcast: register('broadcast', 0xeaad),
164 > browser: register('browser', 0xeaae),
165 > bug: register('bug', 0xeaaf),
166 > calendar: register('calendar', 0xeab0),
167 > caseSensitive: register('case-sensitive', 0xeab1),
168 > check: register('check', 0xeab2),
169 > checklist: register('checklist', 0xeab3),
170 > chevronDown: register('chevron-down', 0xeab4),
171 > chevronLeft: register('chevron-left', 0xeab5),
172 > chevronRight: register('chevron-right', 0xeab6),
173 > chevronUp: register('chevron-up', 0xeab7),
174 > chromeClose: register('chrome-close', 0xeab8),
175 > chromeMaximize: register('chrome-maximize', 0xeab9),
176 > chromeMinimize: register('chrome-minimize', 0xeaba),
177 > chromeRestore: register('chrome-restore', 0xeabb),
178 > circleOutline: register('circle-outline', 0xeabc),
179 > circle: register('circle', 0xeabc),
180 > debugBreakpointUnverified: register('debug-breakpoint-unverified', 0xeabc),
181 > terminalDecorationIncomplete: register('terminal-decoration-incomplete', 0xeabc),
182 > circleSlash: register('circle-slash', 0xeabd),
183 > circuitBoard: register('circuit-board', 0xeabe),
184 > clearAll: register('clear-all', 0xeabf),
185 > clippy: register('clippy', 0xeac0),
186 > closeAll: register('close-all', 0xeac1),
187 > cloudDownload: register('cloud-download', 0xeac2),
188 > cloudUpload: register('cloud-upload', 0xeac3),
189 > code: register('code', 0xeac4),
190 > collapseAll: register('collapse-all', 0xeac5),
191 > colorMode: register('color-mode', 0xeac6),
192 > commentDiscussion: register('comment-discussion', 0xeac7),
193 > creditCard: register('credit-card', 0xeac9),
194 > dash: register('dash', 0xeacc),
195 > dashboard: register('dashboard', 0xeacd),
196 > database: register('database', 0xeace),
197 > debugContinue: register('debug-continue', 0xeacf),
198 > debugDisconnect: register('debug-disconnect', 0xead0),
199 > debugPause: register('debug-pause', 0xead1),
200 > debugRestart: register('debug-restart', 0xead2),
201 > debugStart: register('debug-start', 0xead3),
202 > debugStepInto: register('debug-step-into', 0xead4),
203 > debugStepOut: register('debug-step-out', 0xead5),
204 > debugStepOver: register('debug-step-over', 0xead6),
205 > debugStop: register('debug-stop', 0xead7),
206 > debug: register('debug', 0xead8),
207 > deviceCameraVideo: register('device-camera-video', 0xead9),
208 > deviceCamera: register('device-camera', 0xeada),
209 > deviceMobile: register('device-mobile', 0xeadb),
210 > diffAdded: register('diff-added', 0xeadc),
211 > diffIgnored: register('diff-ignored', 0xeadd),
212 > diffModified: register('diff-modified', 0xeade),
213 > diffRemoved: register('diff-removed', 0xeadf),
214 > diffRenamed: register('diff-renamed', 0xeae0),
215 > diff: register('diff', 0xeae1),
216 > diffSidebyside: register('diff-sidebyside', 0xeae1),
217 > discard: register('discard', 0xeae2),
218 > editorLayout: register('editor-layout', 0xeae3),
219 > emptyWindow: register('empty-window', 0xeae4),
220 > exclude: register('exclude', 0xeae5),
221 > extensions: register('extensions', 0xeae6),
222 > eyeClosed: register('eye-closed', 0xeae7),
223 > fileBinary: register('file-binary', 0xeae8),
224 > fileCode: register('file-code', 0xeae9),
225 > fileMedia: register('file-media', 0xeaea),
226 > filePdf: register('file-pdf', 0xeaeb),
227 > fileSubmodule: register('file-submodule', 0xeaec),
228 > fileSymlinkDirectory: register('file-symlink-directory', 0xeaed),
229 > fileSymlinkFile: register('file-symlink-file', 0xeaee),
230 > fileZip: register('file-zip', 0xeaef),
231 > files: register('files', 0xeaf0),
232 > filter: register('filter', 0xeaf1),
233 > flame: register('flame', 0xeaf2),
234 > foldDown: register('fold-down', 0xeaf3),
235 > foldUp: register('fold-up', 0xeaf4),
236 > fold: register('fold', 0xeaf5),
237 > folderActive: register('folder-active', 0xeaf6),
238 > folderOpened: register('folder-opened', 0xeaf7),
239 > gear: register('gear', 0xeaf8),
240 > gift: register('gift', 0xeaf9),
241 > gistSecret: register('gist-secret', 0xeafa),
242 > gist: register('gist', 0xeafb),
243 > gitCommit: register('git-commit', 0xeafc),
244 > gitCompare: register('git-compare', 0xeafd),
245 > compareChanges: register('compare-changes', 0xeafd),
246 > gitMerge: register('git-merge', 0xeafe),
247 > githubAction: register('github-action', 0xeaff),
248 > githubAlt: register('github-alt', 0xeb00),
249 > globe: register('globe', 0xeb01),
250 > grabber: register('grabber', 0xeb02),
251 > graph: register('graph', 0xeb03),
252 > gripper: register('gripper', 0xeb04),
253 > heart: register('heart', 0xeb05),
254 > home: register('home', 0xeb06),
255 > horizontalRule: register('horizontal-rule', 0xeb07),
256 > hubot: register('hubot', 0xeb08),
257 > inbox: register('inbox', 0xeb09),
258 > issueReopened: register('issue-reopened', 0xeb0b),
259 > issues: register('issues', 0xeb0c),
260 > italic: register('italic', 0xeb0d),
261 > jersey: register('jersey', 0xeb0e),
262 > json: register('json', 0xeb0f),
263 > bracket: register('bracket', 0xeb0f),
264 > kebabVertical: register('kebab-vertical', 0xeb10),
265 > key: register('key', 0xeb11),
266 > law: register('law', 0xeb12),
267 > lightbulbAutofix: register('lightbulb-autofix', 0xeb13),
268 > linkExternal: register('link-external', 0xeb14),
269 > link: register('link', 0xeb15),
270 > listOrdered: register('list-ordered', 0xeb16),
271 > listUnordered: register('list-unordered', 0xeb17),
272 > liveShare: register('live-share', 0xeb18),
273 > loading: register('loading', 0xeb19),
274 > location: register('location', 0xeb1a),
275 > mailRead: register('mail-read', 0xeb1b),
276 > mail: register('mail', 0xeb1c),
277 > markdown: register('markdown', 0xeb1d),
278 > megaphone: register('megaphone', 0xeb1e),
279 > mention: register('mention', 0xeb1f),
280 > milestone: register('milestone', 0xeb20),
281 > gitPullRequestMilestone: register('git-pull-request-milestone', 0xeb20),
282 > mortarBoard: register('mortar-board', 0xeb21),
283 > move: register('move', 0xeb22),
284 > multipleWindows: register('multiple-windows', 0xeb23),
285 > mute: register('mute', 0xeb24),
286 > noNewline: register('no-newline', 0xeb25),
287 > note: register('note', 0xeb26),
288 > octoface: register('octoface', 0xeb27),
289 > openPreview: register('open-preview', 0xeb28),
290 > package: register('package', 0xeb29),
291 > paintcan: register('paintcan', 0xeb2a),
292 > pin: register('pin', 0xeb2b),
293 > play: register('play', 0xeb2c),
294 > run: register('run', 0xeb2c),
295 > plug: register('plug', 0xeb2d),
296 > preserveCase: register('preserve-case', 0xeb2e),
297 > preview: register('preview', 0xeb2f),
298 > project: register('project', 0xeb30),
299 > pulse: register('pulse', 0xeb31),
300 > question: register('question', 0xeb32),
301 > quote: register('quote', 0xeb33),
302 > radioTower: register('radio-tower', 0xeb34),
303 > reactions: register('reactions', 0xeb35),
304 > references: register('references', 0xeb36),
305 > refresh: register('refresh', 0xeb37),
306 > regex: register('regex', 0xeb38),
307 > remoteExplorer: register('remote-explorer', 0xeb39),
308 > remote: register('remote', 0xeb3a),
309 > remove: register('remove', 0xeb3b),
310 > replaceAll: register('replace-all', 0xeb3c),
311 > replace: register('replace', 0xeb3d),
312 > repoClone: register('repo-clone', 0xeb3e),
313 > repoForcePush: register('repo-force-push', 0xeb3f),
314 > repoPull: register('repo-pull', 0xeb40),
315 > repoPush: register('repo-push', 0xeb41),
316 > report: register('report', 0xeb42),
317 > requestChanges: register('request-changes', 0xeb43),
318 > rocket: register('rocket', 0xeb44),
319 > rootFolderOpened: register('root-folder-opened', 0xeb45),
320 > rootFolder: register('root-folder', 0xeb46),
321 > rss: register('rss', 0xeb47),
322 > ruby: register('ruby', 0xeb48),
323 > saveAll: register('save-all', 0xeb49),
324 > saveAs: register('save-as', 0xeb4a),
325 > save: register('save', 0xeb4b),
326 > screenFull: register('screen-full', 0xeb4c),
327 > screenNormal: register('screen-normal', 0xeb4d),
328 > searchStop: register('search-stop', 0xeb4e),
329 > server: register('server', 0xeb50),
330 > settingsGear: register('settings-gear', 0xeb51),
331 > settings: register('settings', 0xeb52),
332 > shield: register('shield', 0xeb53),
333 > smiley: register('smiley', 0xeb54),
334 > sortPrecedence: register('sort-precedence', 0xeb55),
335 > splitHorizontal: register('split-horizontal', 0xeb56),
336 > splitVertical: register('split-vertical', 0xeb57),
337 > squirrel: register('squirrel', 0xeb58),
338 > starFull: register('star-full', 0xeb59),
339 > starHalf: register('star-half', 0xeb5a),
340 > symbolClass: register('symbol-class', 0xeb5b),
341 > symbolColor: register('symbol-color', 0xeb5c),
342 > symbolConstant: register('symbol-constant', 0xeb5d),
343 > symbolEnumMember: register('symbol-enum-member', 0xeb5e),
344 > symbolField: register('symbol-field', 0xeb5f),
345 > symbolFile: register('symbol-file', 0xeb60),
346 > symbolInterface: register('symbol-interface', 0xeb61),
347 > symbolKeyword: register('symbol-keyword', 0xeb62),
348 > symbolMisc: register('symbol-misc', 0xeb63),
349 > symbolOperator: register('symbol-operator', 0xeb64),
350 > symbolProperty: register('symbol-property', 0xeb65),
351 > wrench: register('wrench', 0xeb65),
352 > wrenchSubaction: register('wrench-subaction', 0xeb65),
353 > symbolSnippet: register('symbol-snippet', 0xeb66),
354 > tasklist: register('tasklist', 0xeb67),
355 > telescope: register('telescope', 0xeb68),
356 > textSize: register('text-size', 0xeb69),
357 > threeBars: register('three-bars', 0xeb6a),
358 > thumbsdown: register('thumbsdown', 0xeb6b),
359 > thumbsup: register('thumbsup', 0xeb6c),
360 > tools: register('tools', 0xeb6d),
361 > triangleDown: register('triangle-down', 0xeb6e),
362 > triangleLeft: register('triangle-left', 0xeb6f),
363 > triangleRight: register('triangle-right', 0xeb70),
364 > triangleUp: register('triangle-up', 0xeb71),
365 > twitter: register('twitter', 0xeb72),
366 > unfold: register('unfold', 0xeb73),
367 > unlock: register('unlock', 0xeb74),
368 > unmute: register('unmute', 0xeb75),
369 > unverified: register('unverified', 0xeb76),
370 > verified: register('verified', 0xeb77),
371 > versions: register('versions', 0xeb78),
372 > vmActive: register('vm-active', 0xeb79),
373 > vmOutline: register('vm-outline', 0xeb7a),
374 > vmRunning: register('vm-running', 0xeb7b),
375 > watch: register('watch', 0xeb7c),
376 > whitespace: register('whitespace', 0xeb7d),
377 > wholeWord: register('whole-word', 0xeb7e),
378 > window: register('window', 0xeb7f),
379 > wordWrap: register('word-wrap', 0xeb80),
380 > zoomIn: register('zoom-in', 0xeb81),
381 > zoomOut: register('zoom-out', 0xeb82),
382 > listFilter: register('list-filter', 0xeb83),
383 > listFlat: register('list-flat', 0xeb84),
384 > listSelection: register('list-selection', 0xeb85),
385 > selection: register('selection', 0xeb85),
386 > listTree: register('list-tree', 0xeb86),
387 > debugBreakpointFunctionUnverified: register('debug-breakpoint-function-unverified', 0xeb87),
388 > debugBreakpointFunction: register('debug-breakpoint-function', 0xeb88),
389 > debugBreakpointFunctionDisabled: register('debug-breakpoint-function-disabled', 0xeb88),
390 > debugStackframeActive: register('debug-stackframe-active', 0xeb89),
391 > circleSmallFilled: register('circle-small-filled', 0xeb8a),
392 > debugStackframeDot: register('debug-stackframe-dot', 0xeb8a),
393 > terminalDecorationMark: register('terminal-decoration-mark', 0xeb8a),
394 > debugStackframe: register('debug-stackframe', 0xeb8b),
395 > debugStackframeFocused: register('debug-stackframe-focused', 0xeb8b),
396 > debugBreakpointUnsupported: register('debug-breakpoint-unsupported', 0xeb8c),
397 > symbolString: register('symbol-string', 0xeb8d),
398 > debugReverseContinue: register('debug-reverse-continue', 0xeb8e),
399 > debugStepBack: register('debug-step-back', 0xeb8f),
400 > debugRestartFrame: register('debug-restart-frame', 0xeb90),
401 > debugAlt: register('debug-alt', 0xeb91),
402 > callIncoming: register('call-incoming', 0xeb92),
403 > callOutgoing: register('call-outgoing', 0xeb93),
404 > menu: register('menu', 0xeb94),
405 > expandAll: register('expand-all', 0xeb95),
406 > feedback: register('feedback', 0xeb96),
407 > gitPullRequestReviewer: register('git-pull-request-reviewer', 0xeb96),
408 > groupByRefType: register('group-by-ref-type', 0xeb97),
409 > ungroupByRefType: register('ungroup-by-ref-type', 0xeb98),
410 > account: register('account', 0xeb99),
411 > gitPullRequestAssignee: register('git-pull-request-assignee', 0xeb99),
412 > bellDot: register('bell-dot', 0xeb9a),
413 > debugConsole: register('debug-console', 0xeb9b),
414 > library: register('library', 0xeb9c),
415 > output: register('output', 0xeb9d),
416 > runAll: register('run-all', 0xeb9e),
417 > syncIgnored: register('sync-ignored', 0xeb9f),
418 > pinned: register('pinned', 0xeba0),
419 > githubInverted: register('github-inverted', 0xeba1),
420 > serverProcess: register('server-process', 0xeba2),
421 > serverEnvironment: register('server-environment', 0xeba3),
422 > pass: register('pass', 0xeba4),
423 > issueClosed: register('issue-closed', 0xeba4),
424 > stopCircle: register('stop-circle', 0xeba5),
425 > playCircle: register('play-circle', 0xeba6),
426 > record: register('record', 0xeba7),
427 > debugAltSmall: register('debug-alt-small', 0xeba8),
428 > vmConnect: register('vm-connect', 0xeba9),
429 > cloud: register('cloud', 0xebaa),
430 > merge: register('merge', 0xebab),
431 > export: register('export', 0xebac),
432 > graphLeft: register('graph-left', 0xebad),
433 > magnet: register('magnet', 0xebae),
434 > notebook: register('notebook', 0xebaf),
435 > redo: register('redo', 0xebb0),
436 > checkAll: register('check-all', 0xebb1),
437 > pinnedDirty: register('pinned-dirty', 0xebb2),
438 > passFilled: register('pass-filled', 0xebb3),
439 > circleLargeFilled: register('circle-large-filled', 0xebb4),
440 > circleLarge: register('circle-large', 0xebb5),
441 > circleLargeOutline: register('circle-large-outline', 0xebb5),
442 > combine: register('combine', 0xebb6),
443 > gather: register('gather', 0xebb6),
444 > table: register('table', 0xebb7),
445 > variableGroup: register('variable-group', 0xebb8),
446 > typeHierarchy: register('type-hierarchy', 0xebb9),
447 > typeHierarchySub: register('type-hierarchy-sub', 0xebba),
448 > typeHierarchySuper: register('type-hierarchy-super', 0xebbb),
449 > gitPullRequestCreate: register('git-pull-request-create', 0xebbc),
450 > runAbove: register('run-above', 0xebbd),
451 > runBelow: register('run-below', 0xebbe),
452 > notebookTemplate: register('notebook-template', 0xebbf),
453 > debugRerun: register('debug-rerun', 0xebc0),
454 > workspaceTrusted: register('workspace-trusted', 0xebc1),
455 > workspaceUntrusted: register('workspace-untrusted', 0xebc2),
456 > workspaceUnknown: register('workspace-unknown', 0xebc3),
457 > terminalCmd: register('terminal-cmd', 0xebc4),
458 > terminalDebian: register('terminal-debian', 0xebc5),
459 > terminalLinux: register('terminal-linux', 0xebc6),
460 > terminalPowershell: register('terminal-powershell', 0xebc7),
461 > terminalTmux: register('terminal-tmux', 0xebc8),
462 > terminalUbuntu: register('terminal-ubuntu', 0xebc9),
463 > terminalBash: register('terminal-bash', 0xebca),
464 > arrowSwap: register('arrow-swap', 0xebcb),
465 > copy: register('copy', 0xebcc),
466 > personAdd: register('person-add', 0xebcd),
467 > filterFilled: register('filter-filled', 0xebce),
468 > wand: register('wand', 0xebcf),
469 > debugLineByLine: register('debug-line-by-line', 0xebd0),
470 > inspect: register('inspect', 0xebd1),
471 > layers: register('layers', 0xebd2),
472 > layersDot: register('layers-dot', 0xebd3),
473 > layersActive: register('layers-active', 0xebd4),
474 > compass: register('compass', 0xebd5),
475 > compassDot: register('compass-dot', 0xebd6),
476 > compassActive: register('compass-active', 0xebd7),
477 > azure: register('azure', 0xebd8),
478 > issueDraft: register('issue-draft', 0xebd9),
479 > gitPullRequestClosed: register('git-pull-request-closed', 0xebda),
480 > gitPullRequestDraft: register('git-pull-request-draft', 0xebdb),
481 > debugAll: register('debug-all', 0xebdc),
482 > debugCoverage: register('debug-coverage', 0xebdd),
483 > runErrors: register('run-errors', 0xebde),
484 > folderLibrary: register('folder-library', 0xebdf),
485 > debugContinueSmall: register('debug-continue-small', 0xebe0),
486 > beakerStop: register('beaker-stop', 0xebe1),
487 > graphLine: register('graph-line', 0xebe2),
488 > graphScatter: register('graph-scatter', 0xebe3),
489 > pieChart: register('pie-chart', 0xebe4),
490 > bracketDot: register('bracket-dot', 0xebe5),
491 > bracketError: register('bracket-error', 0xebe6),
492 > lockSmall: register('lock-small', 0xebe7),
493 > azureDevops: register('azure-devops', 0xebe8),
494 > verifiedFilled: register('verified-filled', 0xebe9),
495 > newline: register('newline', 0xebea),
496 > layout: register('layout', 0xebeb),
497 > layoutActivitybarLeft: register('layout-activitybar-left', 0xebec),
498 > layoutActivitybarRight: register('layout-activitybar-right', 0xebed),
499 > layoutPanelLeft: register('layout-panel-left', 0xebee),
500 > layoutPanelCenter: register('layout-panel-center', 0xebef),
501 > layoutPanelJustify: register('layout-panel-justify', 0xebf0),
502 > layoutPanelRight: register('layout-panel-right', 0xebf1),
503 > layoutPanel: register('layout-panel', 0xebf2),
504 > layoutSidebarLeft: register('layout-sidebar-left', 0xebf3),
505 > layoutSidebarRight: register('layout-sidebar-right', 0xebf4),
506 > layoutStatusbar: register('layout-statusbar', 0xebf5),
507 > layoutMenubar: register('layout-menubar', 0xebf6),
508 > layoutCentered: register('layout-centered', 0xebf7),
509 > target: register('target', 0xebf8),
510 > indent: register('indent', 0xebf9),
511 > recordSmall: register('record-small', 0xebfa),
512 > errorSmall: register('error-small', 0xebfb),
513 > terminalDecorationError: register('terminal-decoration-error', 0xebfb),
514 > arrowCircleDown: register('arrow-circle-down', 0xebfc),
515 > arrowCircleLeft: register('arrow-circle-left', 0xebfd),
516 > arrowCircleRight: register('arrow-circle-right', 0xebfe),
517 > arrowCircleUp: register('arrow-circle-up', 0xebff),
518 > layoutSidebarRightOff: register('layout-sidebar-right-off', 0xec00),
519 > layoutPanelOff: register('layout-panel-off', 0xec01),
520 > layoutSidebarLeftOff: register('layout-sidebar-left-off', 0xec02),
521 > blank: register('blank', 0xec03),
522 > heartFilled: register('heart-filled', 0xec04),
523 > map: register('map', 0xec05),
524 > mapHorizontal: register('map-horizontal', 0xec05),
525 > foldHorizontal: register('fold-horizontal', 0xec05),
526 > mapFilled: register('map-filled', 0xec06),
527 > mapHorizontalFilled: register('map-horizontal-filled', 0xec06),
528 > foldHorizontalFilled: register('fold-horizontal-filled', 0xec06),
529 > circleSmall: register('circle-small', 0xec07),
530 > bellSlash: register('bell-slash', 0xec08),
531 > bellSlashDot: register('bell-slash-dot', 0xec09),
532 > commentUnresolved: register('comment-unresolved', 0xec0a),
533 > gitPullRequestGoToChanges: register('git-pull-request-go-to-changes', 0xec0b),
534 > gitPullRequestNewChanges: register('git-pull-request-new-changes', 0xec0c),
535 > searchFuzzy: register('search-fuzzy', 0xec0d),
536 > commentDraft: register('comment-draft', 0xec0e),
537 > send: register('send', 0xec0f),
538 > sparkle: register('sparkle', 0xec10),
539 > insert: register('insert', 0xec11),
540 > mic: register('mic', 0xec12),
541 > thumbsdownFilled: register('thumbsdown-filled', 0xec13),
542 > thumbsupFilled: register('thumbsup-filled', 0xec14),
543 > coffee: register('coffee', 0xec15),
544 > snake: register('snake', 0xec16),
545 > game: register('game', 0xec17),
546 > vr: register('vr', 0xec18),
547 > chip: register('chip', 0xec19),
548 > piano: register('piano', 0xec1a),
549 > music: register('music', 0xec1b),
550 > micFilled: register('mic-filled', 0xec1c),
551 > repoFetch: register('repo-fetch', 0xec1d),
552 > copilot: register('copilot', 0xec1e),
553 > lightbulbSparkle: register('lightbulb-sparkle', 0xec1f),
554 > robot: register('robot', 0xec20),
555 > sparkleFilled: register('sparkle-filled', 0xec21),
556 > diffSingle: register('diff-single', 0xec22),
557 > diffMultiple: register('diff-multiple', 0xec23),
558 > surroundWith: register('surround-with', 0xec24),
559 > share: register('share', 0xec25),
560 > gitStash: register('git-stash', 0xec26),
561 > gitStashApply: register('git-stash-apply', 0xec27),
562 > gitStashPop: register('git-stash-pop', 0xec28),
563 > vscode: register('vscode', 0xec29),
564 > vscodeInsiders: register('vscode-insiders', 0xec2a),
565 > codeOss: register('code-oss', 0xec2b),
566 > runCoverage: register('run-coverage', 0xec2c),
567 > runAllCoverage: register('run-all-coverage', 0xec2d),
568 > coverage: register('coverage', 0xec2e),
569 > githubProject: register('github-project', 0xec2f),
570 > mapVertical: register('map-vertical', 0xec30),
571 > foldVertical: register('fold-vertical', 0xec30),
572 > mapVerticalFilled: register('map-vertical-filled', 0xec31),
573 > foldVerticalFilled: register('fold-vertical-filled', 0xec31),
574 > goToSearch: register('go-to-search', 0xec32),
575 > percentage: register('percentage', 0xec33),
576 > sortPercentage: register('sort-percentage', 0xec33),
577 > attach: register('attach', 0xec34),
578 > goToEditingSession: register('go-to-editing-session', 0xec35),
579 > editSession: register('edit-session', 0xec36),
580 > codeReview: register('code-review', 0xec37),
581 > copilotWarning: register('copilot-warning', 0xec38),
582 > python: register('python', 0xec39),
583 > copilotLarge: register('copilot-large', 0xec3a),
584 > copilotWarningLarge: register('copilot-warning-large', 0xec3b),
585 > keyboardTab: register('keyboard-tab', 0xec3c),
586 > copilotBlocked: register('copilot-blocked', 0xec3d),
587 > copilotNotConnected: register('copilot-not-connected', 0xec3e),
588 > flag: register('flag', 0xec3f),
589 > lightbulbEmpty: register('lightbulb-empty', 0xec40),
590 > symbolMethodArrow: register('symbol-method-arrow', 0xec41),
591 > copilotUnavailable: register('copilot-unavailable', 0xec42),
592 > repoPinned: register('repo-pinned', 0xec43),
593 > keyboardTabAbove: register('keyboard-tab-above', 0xec44),
594 > keyboardTabBelow: register('keyboard-tab-below', 0xec45),
595 > gitPullRequestDone: register('git-pull-request-done', 0xec46),
596 > mcp: register('mcp', 0xec47),
597 > extensionsLarge: register('extensions-large', 0xec48),
598 > layoutPanelDock: register('layout-panel-dock', 0xec49),
599 > layoutSidebarLeftDock: register('layout-sidebar-left-dock', 0xec4a),
600 > layoutSidebarRightDock: register('layout-sidebar-right-dock', 0xec4b),
601 > copilotInProgress: register('copilot-in-progress', 0xec4c),
602 > copilotError: register('copilot-error', 0xec4d),
603 > copilotSuccess: register('copilot-success', 0xec4e),
604 > chatSparkle: register('chat-sparkle', 0xec4f),
605 > searchSparkle: register('search-sparkle', 0xec50),
606 > editSparkle: register('edit-sparkle', 0xec51),
607 > copilotSnooze: register('copilot-snooze', 0xec52),
608 > sendToRemoteAgent: register('send-to-remote-agent', 0xec53),
609 > commentDiscussionSparkle: register('comment-discussion-sparkle', 0xec54),
610 > chatSparkleWarning: register('chat-sparkle-warning', 0xec55),
611 > chatSparkleError: register('chat-sparkle-error', 0xec56),
612 > collection: register('collection', 0xec57),
613 > newCollection: register('new-collection', 0xec58),
614 > thinking: register('thinking', 0xec59),
615 > build: register('build', 0xec5a),
616 > commentDiscussionQuote: register('comment-discussion-quote', 0xec5b),
617 > cursor: register('cursor', 0xec5c),
618 > eraser: register('eraser', 0xec5d),
619 > fileText: register('file-text', 0xec5e),
620 > quotes: register('quotes', 0xec60),
621 > rename: register('rename', 0xec61),
622 > runWithDeps: register('run-with-deps', 0xec62),
623 > debugConnected: register('debug-connected', 0xec63),
624 > strikethrough: register('strikethrough', 0xec64),
625 > openInProduct: register('open-in-product', 0xec65),
626 > indexZero: register('index-zero', 0xec66),
627 > agent: register('agent', 0xec67),
628 > editCode: register('edit-code', 0xec68),
629 > repoSelected: register('repo-selected', 0xec69),
630 > skip: register('skip', 0xec6a),
631 > mergeInto: register('merge-into', 0xec6b),
632 > gitBranchChanges: register('git-branch-changes', 0xec6c),
633 > gitBranchStagedChanges: register('git-branch-staged-changes', 0xec6d),
634 > gitBranchConflicts: register('git-branch-conflicts', 0xec6e),
635 > gitBranch: register('git-branch', 0xec6f),
636 > gitBranchCreate: register('git-branch-create', 0xec6f),
637 > gitBranchDelete: register('git-branch-delete', 0xec6f),
638 > searchLarge: register('search-large', 0xec70),
639 > terminalGitBash: register('terminal-git-bash', 0xec71),
640 > windowActive: register('window-active', 0xec72),
641 > forward: register('forward', 0xec73),
642 > download: register('download', 0xec74),
643 > clockface: register('clockface', 0xec75),
644 > unarchive: register('unarchive', 0xec76),
645 > sessionInProgress: register('session-in-progress', 0xec77),
646 > collectionSmall: register('collection-small', 0xec78),
647 > vmSmall: register('vm-small', 0xec79),
648 > cloudSmall: register('cloud-small', 0xec7a),
649 > addSmall: register('add-small', 0xec7b),
650 > removeSmall: register('remove-small', 0xec7c),
651 > worktreeSmall: register('worktree-small', 0xec7d),
652 > worktree: register('worktree', 0xec7e),
653 > screenCut: register('screen-cut', 0xec7f),
654 > ask: register('ask', 0xec80),
655 > openai: register('openai', 0xec81),
656 > claude: register('claude', 0xec82),
657 > openInWindow: register('open-in-window', 0xec83),
658 > newSession: register('new-session', 0xec84),
659 > terminalSecure: register('terminal-secure', 0xec85),
660 > chatImport: register('chat-import', 0xec86),
661 > chatExport: register('chat-export', 0xec87),
662 > shareWindow: register('share-window', 0xec88),
663 > circleSlashCompact: register('circle-slash-compact', 0xec89),
664 > copilotCompact: register('copilot-compact', 0xec8a),
665 > folderOpenedCompact: register('folder-opened-compact', 0xec8b),
666 > folderCompact: register('folder-compact', 0xec8c),
667 > gearCompact: register('gear-compact', 0xec8d),
668 > gitBranchCompact: register('git-branch-compact', 0xec8e),
669 > libraryCompact: register('library-compact', 0xec8f),
670 > recordKeysCompact: register('record-keys-compact', 0xec90),
671 > remoteCompact: register('remote-compact', 0xec91),
672 > repoForkedCompact: register('repo-forked-compact', 0xec92),
673 > repoCompact: register('repo-compact', 0xec93),
674 > shieldCompact: register('shield-compact', 0xec94),
675 > sparkleCompact: register('sparkle-compact', 0xec95),
676 > symbolColorCompact: register('symbol-color-compact', 0xec96),
677 > windowCompact: register('window-compact', 0xec97),
678 > errorCompact: register('error-compact', 0xec98),
679 > warningCompact: register('warning-compact', 0xec99),
680 > passCompact: register('pass-compact', 0xec9a),
681 > important: register('important', 0xec9b),
682 > importantCompact: register('important-compact', 0xec9c),
683 > rocketCompact: register('rocket-compact', 0xec9d),
684 > unpin: register('unpin', 0xec9e),
685 > addCompact: register('add-compact', 0xec9f),
686 > attachCompact: register('attach-compact', 0xeca0),
687 > beakerCompact: register('beaker-compact', 0xeca1),
688 > checkCompact: register('check-compact', 0xeca2),
689 > checklistCompact: register('checklist-compact', 0xeca3),
690 > chevronDownCompact: register('chevron-down-compact', 0xeca4),
691 > chevronLeftCompact: register('chevron-left-compact', 0xeca5),
692 > chevronRightCompact: register('chevron-right-compact', 0xeca6),
693 > chevronUpCompact: register('chevron-up-compact', 0xeca7),
694 > circleFilledCompact: register('circle-filled-compact', 0xeca8),
695 > circleSmallFilledCompact: register('circle-small-filled-compact', 0xeca9),
696 > closeCompact: register('close-compact', 0xecaa),
697 > collapseAllCompact: register('collapse-all-compact', 0xecab),
698 > commentCompact: register('comment-compact', 0xecac),
699 > commentUnresolvedCompact: register('comment-unresolved-compact', 0xecad),
700 > debugConnectedCompact: register('debug-connected-compact', 0xecae),
701 > debugDisconnectCompact: register('debug-disconnect-compact', 0xecaf),
702 > editCompact: register('edit-compact', 0xecb0),
703 > fileMediaCompact: register('file-media-compact', 0xecb1),
704 > gitFetch: register('git-fetch', 0xecb2),
705 > lightbulbCompact: register('lightbulb-compact', 0xecb3),
706 > loadingCompact: register('loading-compact', 0xecb4),
707 > passFilledCompact: register('pass-filled-compact', 0xecb5),
708 > projectCompact: register('project-compact', 0xecb6),
709 > refreshCompact: register('refresh-compact', 0xecb7),
710 > searchCompact: register('search-compact', 0xecb8),
711 > sessionInProgressCompact: register('session-in-progress-compact', 0xecb9),
712 > syncCompact: register('sync-compact', 0xecba),
713 > terminalCompact: register('terminal-compact', 0xecbb),
714 > vmPending: register('vm-pending', 0xecbc),
715 > worktreeCompact: register('worktree-compact', 0xecbd),
716 > developerTools: register('developer-tools', 0xecbe),
717 > cloudCompact: register('cloud-compact', 0xecbf),
718 > agentCompact: register('agent-compact', 0xecc0),
719 > askCompact: register('ask-compact', 0xecc1),
720 > settingsCompact: register('settings-compact', 0xecc2),
721 > vmCompact: register('vm-compact', 0xecc3),
722 > runCompact: register('run-compact', 0xecc4),
723 > gitPullRequestComment: register('git-pull-request-comment', 0xecc5),
724 > gitPullRequestError: register('git-pull-request-error', 0xecc6),
725 > rightPanelHide: register('right-panel-hide', 0xecc7),
726 > rightPanelShow: register('right-panel-show', 0xecc8),
727 > vscodeInsidersOutline: register('vscode-insiders-outline', 0xecc9),
728 > vscodeOutline: register('vscode-outline', 0xecca),
729 > voiceMode: register('voice-mode', 0xeccb),
730 > voiceModeCompact: register('voice-mode-compact', 0xeccc),
731 > micDownload: register('mic-download', 0xeccd),
732 > micDownloadCompact: register('mic-download-compact', 0xecce),
733 > voiceModeDownload: register('voice-mode-download', 0xeccf),
734 > voiceModeDownloadCompact: register('voice-mode-download-compact', 0xecd0),
735 > googleGemini: register('google-gemini', 0xecd1),
736 > kimi: register('kimi', 0xecd2),
737 > microsoft: register('microsoft', 0xecd3),
738 > fish1Happy: register('fish1-happy', 0xecd4),
739 > fish1Neutral: register('fish1-neutral', 0xecd5),
740 > fish1Sad: register('fish1-sad', 0xecd6),
741 > fish1VerySad: register('fish1-very-sad', 0xecd7),
742 > fish2Happy: register('fish2-happy', 0xecd8),
743 > fish2Neutral: register('fish2-neutral', 0xecd9),
744 > fish2Sad: register('fish2-sad', 0xecda),
745 > fish2VerySad: register('fish2-very-sad', 0xecdb),
746 > fish3Happy: register('fish3-happy', 0xecdc),
747 > fish3Neutral: register('fish3-neutral', 0xecdd),
748 > fish3Sad: register('fish3-sad', 0xecde),
749 > fish3VerySad: register('fish3-very-sad', 0xecdf),
750 > fish4Happy: register('fish4-happy', 0xece0),
751 > fish4Neutral: register('fish4-neutral', 0xece1),
752 > fish4Sad: register('fish4-sad', 0xece2),
753 > fish4VerySad: register('fish4-very-sad', 0xece3),
754 > personVoice: register('person-voice', 0xece4),
755 > personVoiceCompact: register('person-voice-compact', 0xece5),
756 > personVoiceFilled: register('person-voice-filled', 0xece6),
757 > personVoiceFilledCompact: register('person-voice-filled-compact', 0xece7),
758 > } as const;
src/vs/platform/agentHost/node/agentHostStateManager.ts 749 covered LOC · 75 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostStateManager.ts
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 > import { RunOnceScheduler } from '../../../base/common/async.js';
7 > import { Emitter, Event } from '../../../base/common/event.js';
8 > import { Disposable } from '../../../base/common/lifecycle.js';
9 > import { equals } from '../../../base/common/objects.js';
10 > import { ILogService } from '../../log/common/log.js';
11 > import { createDecorator } from '../../instantiation/common/instantiation.js';
12 > import { TelemetryLevel } from '../../telemetry/common/telemetry.js';
13 > import { ActionType, ActionEnvelope, ActionOrigin, INotification, IRootConfigChangedAction, SessionAction, ChatAction, RootAction, StateAction, TerminalAction, ChangesetAction, ClientChangesetAction, AnnotationsAction, ClientAnnotationsAction, isRootAction, isSessionAction, isChatAction, isChangesetAction, isAnnotationsAction, type AuthRequiredParams, type ProgressParams } from '../common/state/sessionActions.js';
14 > import type { IStateSnapshot } from '../common/state/sessionProtocol.js';
15 > import { rootReducer, sessionReducer, chatReducer, changesetReducer, annotationsReducer } from '../common/state/sessionReducers.js';
16 > import { createRootState, createSessionState, createChatState, createDefaultChatSummary, chatSummaryFromState, buildDefaultChatUri, parseDefaultChatUri, parseRequiredSessionUriFromChatUri, isAhpChatChannel, isDefaultChatUri, mergeSessionWithDefaultChat, isAhpRootChannel, SessionLifecycle, withHostBuildInfo, type Changeset, type ChangesetState, type AnnotationsState, type ChatState, type ChatSummary, type Customization, type ISessionWithDefaultChat, type Message, type RootState, type SessionConfigState, type SessionMeta, type SessionState, type SessionSummary, type Turn, type URI, ROOT_STATE_URI, ChangesetStatus, IHostBuildInfo, SessionStatus } from '../common/state/sessionState.js';
17 > import { AgentHostTelemetryLevelConfigKey, IPermissionsValue, platformRootSchema, telemetryLevelToAgentHostConfigValue } from '../common/agentHostSchema.js';
18 > import { SessionConfigKey } from '../common/sessionConfigKeys.js';
19 > import { parseChangesetUri } from '../common/changesetUri.js';
20 > import { buildAnnotationsUri, isAnnotationsUri } from '../common/annotationsUri.js';
21 > import { AgentHostChangesetStateCache, type IAgentHostChangesetStateRetentionOptions } from './agentHostChangesetStateCache.js';
22 > import { ChangesSummary, ChatInteractivity, type ChatOrigin } from '../common/state/protocol/state.js';
23 > import { arrayEquals, structuralEquals } from '../../../base/common/equals.js';
24 > import { preserveProviderBackedRootConfigValues } from '../common/agentCustomizationSettings.js';
25 >
26 > export interface IAgentHostStateManagerOptions {
27 > readonly changesetStateRetention?: IAgentHostChangesetStateRetentionOptions;
28 > /**
29 > * Build information about the program hosting the agent host. When
30 > * provided, it is published on {@link RootState._meta} so clients can see
31 > * which build is hosting them.
32 > */
33 > readonly hostBuildInfo?: IHostBuildInfo;
34 > }
35 >
36 > /**
37 > * Authoritative per-session record held by the state manager. Bundles the flat
38 > * {@link SessionState} with the {@link SessionSummary} catalog-only fields that
39 > * do not live on the state. The session URI (catalog `resource`) is the map
40 > * key, and the catalog `_meta` is the same object as {@link SessionState._meta},
41 > * so the only extra fields the record carries are the timestamps and the
42 > * aggregate change counts.
43 > */
44 > interface ISessionEntry {
45 > state: SessionState;
46 > /** Creation timestamp (ISO 8601). Catalog-only; immutable after creation. */
47 > readonly createdAt: string;
48 > /** Last modification timestamp (ISO 8601). Catalog-only; derived from chat aggregation. */
49 > modifiedAt: string;
50 > /** Aggregate file-change counts for the session-wide changeset. Catalog-only. */
51 > changes?: ChangesSummary;
52 > }
53 >
54 > /**
55 > * Encapsulates the root-channel summary-notification bookkeeping for the
56 > * {@link AgentHostStateManager}: the last {@link SessionSummary} announced to
57 > * clients per session (the diff baseline) and the set of sessions whose summary
58 > * changed since the last debounced flush. The snapshot map and the dirty set
59 > * are always mutated in lockstep, so keeping them together — rather than as two
60 > * loose fields on the manager — keeps the diffing state cohesive.
61 > *
62 > * The current summary for a session is sourced via the injected `getSummary`
63 > * callback; diff-based `root/sessionSummaryChanged` notifications are emitted
64 > * through `emit`.
65 > */
66 > class SessionSummaryNotifier extends Disposable {
67 >
68 > /** Last summary announced to clients (via sessionAdded or sessionSummaryChanged). */
69 > private readonly _lastNotified = new Map<string, SessionSummary>();
70 >
71 > /** Sessions whose summary changed since the last flush. */
72 > private readonly _dirty = new Set<string>();
73 >
74 > private readonly _scheduler = this._register(new RunOnceScheduler(() => this._flushAll(), 100));
75 >
76 > constructor(
77 > private readonly _getSummary: (session: string) => SessionSummary | undefined, agentHostStateManager.ts
78 > private readonly _emit: (session: string, changes: Partial<SessionSummary>) => void,
79 > ) {
80 > super();
81 > }
83 > /** Records `summary` as the last value announced to clients for `session`. */
84 > announce(session: string, summary: SessionSummary): void {
85 this._lastNotified.set(session, summary);
86 }
88 > /** Whether `session` has already been announced to clients. */
89 > isAnnounced(session: string): boolean {
90 return this._lastNotified.has(session);
91 }
93 > /** Marks `session` dirty and schedules a debounced flush. */
94 > markDirty(session: string): void {
95 this._dirty.add(session);
96 this._scheduler.schedule();
97 }
99 > /** Whether `session` has a pending (unflushed) summary change. */
100 > isDirty(session: string): boolean {
101 return this._dirty.has(session);
102 }
104 > /** Drops the pending dirty flag for `session` without flushing it. */
105 > clearDirty(session: string): void {
106 this._dirty.delete(session);
107 }
109 > /** Drops all notification bookkeeping for `session`. */
110 > remove(session: string): void {
111 this._lastNotified.delete(session);
112 this._dirty.delete(session);
113 }
115 > private _flushAll(): void {
116 for (const session of this._dirty) {
117 this.flush(session);
119 this._dirty.clear();
120 }
122 > /**
123 > * Emits a `root/sessionSummaryChanged` notification for `session` if its
124 > * current summary differs from the last announced one, then advances the
125 > * snapshot. Does NOT clear the dirty flag — callers own that bookkeeping.
126 > */
127 > flush(session: string): void {
128 const current = this._getSummary(session);
129 const lastNotified = this._lastNotified.get(session);
148 }
149 }
151 >
152 > /**
153 > * Server-side state manager for the sessions process protocol.
154 > *
155 > * Maintains the authoritative state tree (root + per-session), applies actions
156 > * through pure reducers, assigns monotonic sequence numbers, and emits
157 > * {@link ActionEnvelope}s for subscribed clients.
158 > */
159 > export const IAgentHostStateManager = createDecorator<AgentHostStateManager>('agentHostStateManager');
160 >
161 > export class AgentHostStateManager extends Disposable {
162 > declare readonly _serviceBrand: undefined;
163 >
164 > private _serverSeq = 0;
165 >
166 > private _rootState: RootState;
167 >
168 > /**
169 > * Authoritative per-session state, keyed by session URI string. Each entry
170 > * bundles the flat {@link SessionState} with the catalog-only fields that
171 > * are not part of the state (`createdAt`, `modifiedAt`, `changes`). The
172 > * root-channel {@link SessionSummary} catalog view is derived on demand from
173 > * an entry via {@link getSessionSummary} (its `_meta` is the same object as
174 > * {@link SessionState._meta}); the host streams catalog deltas via
175 > * `root/sessionSummaryChanged`.
176 > */
177 > private readonly _sessionStates = new Map<string, ISessionEntry>();
178 >
179 > /**
180 > * Authoritative per-chat conversation state, keyed by chat channel URI.
181 > * The protocol moved turns/activeTurn/pending state off the session and
182 > * onto a per-chat channel. VS Code currently models every session as
183 > * having exactly one chat — its default chat — whose URI is derived
184 > * deterministically from the session URI via {@link buildDefaultChatUri}.
185 > */
186 > private readonly _chatStates = new Map<string, ChatState>();
187 >
188 > /**
189 > * Opaque, agent-owned `providerData` blobs keyed by peer-chat channel URI.
190 > *
191 > * Each entry is the verbatim token the owning agent produced for a peer
192 > * chat (see {@link IAgentCreateChatResult.providerData}). The orchestrator
193 > * persists it with the session and hands it back to the agent on restore so
194 > * the agent can re-materialize its SDK conversation; the StateManager itself
195 > * **never parses, validates, or mutates it** — it stores and returns the
196 > * string as-is. The map is kept separate from the protocol-visible
197 > * {@link ChatState}/{@link ChatSummary} catalog so the private blob is not
198 > * streamed to clients. The default chat carries no `providerData`, so it
199 > * never appears here.
200 > */
201 > private readonly _chatProviderData = new Map<string, string>();
202 >
203 > /** Expanded changeset states, separated from protocol sequencing so cache policy stays local. */
204 > private readonly _changesets: AgentHostChangesetStateCache;
205 >
206 > /**
207 > * Per-channel annotation states for the `<session>/annotations` channel.
208 > * Unlike changesets (server-owned), annotation actions are
209 > * client-dispatchable and lazily create their state on first write.
210 > */
211 > private readonly _annotations = new Map<string, AnnotationsState>();
212 >
213 > /**
214 > * Active turns per session, keyed by session URI string with the value
215 > * being the set of that session's chat channel URIs that currently have an
216 > * active turn. A session is "active" while at least one of its chats is
217 > * streaming — this stays correct for multi-chat sessions whose chats can run
218 > * concurrent turns (e.g. agent-team / sub-agent workers), where the previous
219 > * single-flag-per-session model would clear too early. Active state is
220 > * derived from `state.activeTurn` (the source of truth maintained by the
221 > * session reducer) — never from raw action turn-ids — so that mismatched or
222 > * out-of-order turn lifecycle actions can't desync it from reality. The
223 > * session count (`size`) drives `RootActiveSessionsChanged` and
224 > * `hasActiveSessions`, which together gate `--enable-remote-auto-shutdown`.
225 > */
226 > private readonly _sessionsWithActiveTurn = new Map<string, Set<string>>();
227 >
228 > /**
229 > * Root-channel summary notification bookkeeping: the diff baseline (last
230 > * announced summary per session) and the dirty set, debounced into
231 > * `root/sessionSummaryChanged` notifications. Assigned in the constructor
232 > * since it closes over {@link _toSummary} and {@link _onDidEmitNotification}.
233 > */
234 > private readonly _summaryNotifier: SessionSummaryNotifier;
235 >
236 > private readonly _onDidEmitEnvelope = this._register(new Emitter<ActionEnvelope>());
237 > readonly onDidEmitEnvelope: Event<ActionEnvelope> = this._onDidEmitEnvelope.event;
238 >
239 > private readonly _onDidEmitNotification = this._register(new Emitter<INotification>());
240 > readonly onDidEmitNotification: Event<INotification> = this._onDidEmitNotification.event;
241 > private readonly _onDidChangeSessionActiveTurn = this._register(new Emitter<{ session: string; active: boolean }>());
242 > readonly onDidChangeSessionActiveTurn: Event<{ session: string; active: boolean }> = this._onDidChangeSessionActiveTurn.event;
243 >
244 > constructor(
245 > @ILogService private readonly _logService: ILogService, agentHostStateManager.ts
246 > options: IAgentHostStateManagerOptions = {},
247 > ) {
248 > super();
249 > this._changesets = new AgentHostChangesetStateCache(options.changesetStateRetention);
250 > this._rootState = createRootState();
251 > // Seed the host-level configuration schema + default values so that
252 > // RootConfigChanged actions can merge into it, and clients see the
253 > // schema immediately upon subscribing to `agenthost:/root`. See
254 > // `platformRootSchema` for the set of platform-owned properties.
255 > this._rootState = {
256 > ...this._rootState,
257 > config: {
258 > schema: platformRootSchema.toProtocol(),
259 > values: platformRootSchema.validateOrDefault({}, {
260 > [SessionConfigKey.Permissions]: { allow: [], deny: [] } satisfies IPermissionsValue,
261 > [AgentHostTelemetryLevelConfigKey]: telemetryLevelToAgentHostConfigValue(TelemetryLevel.USAGE),
262 > }),
263 > },
264 > _meta: withHostBuildInfo(this._rootState._meta, options.hostBuildInfo),
265 > };
266 > this._summaryNotifier = this._register(new SessionSummaryNotifier(
267 > session => {
268 const entry = this._sessionStates.get(session);
269 return entry ? this._toSummary(session, entry) : undefined;
270 },
271 > (session, changes) => this._onDidEmitNotification.fire({ agentHostStateManager.ts
272 type: 'root/sessionSummaryChanged',
273 channel: ROOT_STATE_URI,
275 changes,
276 }),
278 > }
279 > private readonly _log = (msg: string) => this._logService.warn(`[AgentHostStateManager] ${msg}`); agentHostStateManager.ts
280 >
281 > get hasActiveSessions(): boolean {
282 return this._sessionsWithActiveTurn.size > 0;
283 }
285 > /**
286 > * Whether the given session currently has an active turn — i.e. a request is
287 > * in progress on any of its chats. Stays `true` while at least one chat is
288 > * streaming, so it remains correct for multi-chat sessions running
289 > * concurrent turns.
290 > */
291 > hasActiveTurn(sessionKey: string): boolean {
292 return this._sessionsWithActiveTurn.has(sessionKey);
293 }
295 > // ---- State accessors ----------------------------------------------------
296 >
297 > get rootState(): RootState {
298 > return this._rootState; agentHostStateManager.ts
299 > }
301 > getSessionState(sessionOrChat: URI): ISessionWithDefaultChat | undefined {
302 // Accept either a session URI or one of its chat channel URIs. When a
303 // chat URI is given the conversation contents are taken from that chat,
315 return mergeSessionWithDefaultChat(entry.state, this._chatStates.get(chatUri));
316 }
318 > /**
319 > * Returns the root-channel {@link SessionSummary} catalog entry for a
320 > * session, or `undefined` when the session is unknown. The summary is
321 > * derived on demand from the session's {@link ISessionEntry}: its metadata
322 > * fields and `_meta` come straight off the live {@link SessionState}, while
323 > * the catalog-only `resource` / `createdAt` / `modifiedAt` / `changes` come
324 > * from the entry.
325 > */
326 > getSessionSummary(session: URI): SessionSummary | undefined {
327 const entry = this._sessionStates.get(session);
328 return entry ? this._toSummary(session, entry) : undefined;
329 }
331 > /**
332 > * Projects an {@link ISessionEntry} into its root-channel
333 > * {@link SessionSummary}. The summary's `_meta` is the same object as
334 > * {@link SessionState._meta} — the host treats the two as identical.
335 > */
336 > private _toSummary(session: string, entry: ISessionEntry): SessionSummary {
337 const { state } = entry;
338 const summary: SessionSummary = {
352 return summary;
353 }
355 > /**
356 > * Whether the {@link SessionSummary}-relevant fields of two session states
357 > * are field-equal. Used to decide whether a session action mutated anything
358 > * the root-channel catalog cares about.
359 > */
360 > private _summaryFieldsEqual(a: SessionState, b: SessionState): boolean {
361 return a.title === b.title
362 && a.status === b.status
367 && a._meta === b._meta;
368 }
370 > /**
371 > * Returns the authoritative {@link ChatState} for a session's default
372 > * chat, or `undefined` when the session is unknown. Use this when the
373 > * caller specifically needs conversation contents (turns, activeTurn,
374 > * pending/input state) rather than the session summary.
375 > */
376 > getDefaultChatState(session: URI): ChatState | undefined {
377 return this._chatStates.get(buildDefaultChatUri(session));
378 }
380 > /** Returns the authoritative {@link ChatState} for a chat channel URI. */
381 > getChatState(chat: URI): ChatState | undefined {
382 return this._chatStates.get(chat);
383 }
385 > /**
386 > * Returns the opaque, agent-owned `providerData` blob previously recorded
387 > * for a peer chat via {@link addChat} or {@link restoreChat}, or `undefined`
388 > * when none was stored (e.g. the default chat, or a peer chat the agent had
389 > * nothing resumable to persist for). The value is returned verbatim — the
390 > * StateManager never interprets it; callers persist it with the session and
391 > * hand it back to the owning agent on restore.
392 > */
393 > getChatProviderData(chat: URI): string | undefined {
394 return this._chatProviderData.get(chat);
395 }
397 > /**
398 > * Seeds the conversation contents (turns) of a session's default chat.
399 > * Used by the fork flow, which materializes a new session pre-populated
400 > * with a slice of the source session's turns.
401 > */
402 > seedDefaultChatTurns(session: URI, turns: Turn[]): void {
403 const chatState = this._chatStates.get(buildDefaultChatUri(session));
404 if (chatState) {
406 }
407 }
409 > get serverSeq(): number {
410 return this._serverSeq;
411 }
413 > getSessionUris(): string[] {
414 return [...this._sessionStates.keys()];
415 }
417 > /**
418 > * Summaries eligible to be overlaid onto a provider's `listSessions`
419 > * snapshot when that snapshot is missing them. A session qualifies if it
420 > * has materialized (lifecycle !== {@link SessionLifecycle.Creating}) — this
421 > * covers the transient-drop case where a provider briefly omits a
422 > * just-materialized session — or if it is still provisional but has had any
423 > * turn activity (an in-flight turn, or a completed turn whose materialize
424 > * event has not landed yet; the first turn can start before materialization
425 > * completes). Idle provisional sessions (created but not yet materialized
426 > * and with no turn activity, e.g. the new-session composer's eagerly-created
427 > * session before its first message) are excluded so they don't leak into
428 > * the session list (#321269).
429 > */
430 > getOverlaySessionSummaries(): SessionSummary[] {
431 const summaries: SessionSummary[] = [];
432 for (const [key, entry] of this._sessionStates) {
441 return summaries;
442 }
444 > /**
445 > * Returns all session URIs whose keys start with the given prefix.
446 > * Used to discover subagent sessions for a given parent.
447 > */
448 > getSessionUrisWithPrefix(prefix: string): string[] {
449 const result: string[] = [];
450 for (const key of this._sessionStates.keys()) {
455 return result;
456 }
458 > // ---- Snapshots ----------------------------------------------------------
459 >
460 > /**
461 > * Returns a state snapshot for a given resource URI.
462 > * The `fromSeq` in the snapshot is the current serverSeq at snapshot time;
463 > * the client should process subsequent envelopes with serverSeq > fromSeq.
464 > */
465 > getSnapshot(resource: URI): IStateSnapshot | undefined {
466 if (isAhpRootChannel(resource)) {
467 return {
519 };
520 }
522 > /** Read-only accessor for callers that only need to inspect a changeset (not subscribe). */
523 > getChangesetState(changeset: URI): ChangesetState | undefined {
524 return this._changesets.get(changeset);
525 }
527 > /** Reconsiders changeset state retention after subscribers or computes release their pins. */
528 > onChangesetLivenessChanged(): void {
529 this._changesets.trimEvictableEntries();
530 }
532 > // ---- Session lifecycle --------------------------------------------------
533 >
534 > /**
535 > * Creates a new session in state with `lifecycle: 'creating'`.
536 > * Returns the initial session state.
537 > *
538 > * By default a {@link NotificationType.SessionAdded} notification is
539 > * emitted so clients see the new session immediately. Pass
540 > * `options.emitNotification: false` to defer the notification — a typical
541 > * use is for **provisional** sessions that exist on the server but should
542 > * not appear in client session lists until they have been persisted by
543 > * the agent (e.g. on the first message that materializes an SDK session
544 > * and writes its on-disk metadata). Call {@link markSessionPersisted}
545 > * afterwards to fire the deferred notification.
546 > */
547 > createSession(summary: SessionSummary, options?: { readonly emitNotification?: boolean }): SessionState {
548 const key = summary.resource;
549 const existing = this._sessionStates.get(key);
574 return state;
575 }
577 > /** Builds the authoritative {@link ISessionEntry} for a freshly seeded state. */
578 > private _newEntry(state: SessionState, summary: SessionSummary): ISessionEntry {
579 return { state, createdAt: summary.createdAt, modifiedAt: summary.modifiedAt, changes: summary.changes };
580 }
582 > /**
583 > * Fire a {@link NotificationType.SessionAdded} notification for a session
584 > * whose creation was deferred via `createSession({ emitNotification: false })`.
585 > *
586 > * Propagates the materialization-resolved catalog fields (`project`,
587 > * `workingDirectory`, `modifiedAt`, `changes`) from the supplied summary
588 > * onto the session entry so subscribers see them. The reducer-owned metadata
589 > * (`title`, `status`, `activity`) is intentionally NOT copied back — the live
590 > * state is authoritative for those. No-ops for sessions that were already
591 > * announced (idempotent).
592 > */
593 > markSessionPersisted(session: URI, summary: SessionSummary): void {
594 const key = session.toString();
595 const entry = this._sessionStates.get(key);
621 });
622 }
624 > /**
625 > * Restores a session from a previous server lifetime into the state manager
626 > * with pre-populated turns. The session is created in `ready` lifecycle
627 > * state since it already exists on the backend.
628 > *
629 > * Unlike {@link createSession}, this does NOT emit a `sessionAdded`
630 > * notification because the session is already known to clients via
631 > * `listSessions`.
632 > */
633 > restoreSession(summary: SessionSummary, turns: Turn[], options?: { readonly draft?: Message; readonly defaultChatTitle?: string }): SessionState {
634 const key = summary.resource;
635 const existing = this._sessionStates.get(key);
651 return state;
652 }
654 > /**
655 > * Creates the default {@link ChatState} for a session and records it as
656 > * the session's single chat. VS Code models every session as having
657 > * exactly one chat — its default chat — whose URI is derived
658 > * deterministically from the session URI. The chat is seeded with any
659 > * pre-populated `turns` (used by {@link restoreSession}).
660 > *
661 > * The session's `chats` catalog and `defaultChat` pointer are updated
662 > * in place rather than via dispatched actions: there are no subscribers
663 > * at creation/restore time, so the snapshot a client later receives on
664 > * subscribe already reflects the default chat.
665 > */
666 > private _ensureDefaultChat(sessionKey: string, summary: SessionSummary, turns?: Turn[], draft?: Message, defaultChatTitle?: string): void {
667 const chatUri = buildDefaultChatUri(sessionKey);
668 // Empty title means "inherit the session title"; a persisted independent
682 }
683 }
685 > /**
686 > * Adds an additional (non-default) chat to an existing session. Creates
687 > * the chat's authoritative {@link ChatState}, registers it in the session's
688 > * catalog via a dispatched {@link ActionType.SessionChatAdded} action (so
689 > * live subscribers refresh), and returns the new chat's summary.
690 > *
691 > * The chat inherits the session's model/agent/working-directory scope. It
692 > * is a no-op (returning the existing summary) when a chat with the same URI
693 > * already exists.
694 > *
695 > * When `options.providerData` is supplied it is recorded verbatim as the
696 > * peer chat's opaque, agent-owned restore blob (see
697 > * {@link getChatProviderData}); the StateManager never parses it. The
698 > * default chat never carries `providerData`.
699 > */
700 > addChat(session: URI, chatUri: URI, options?: { readonly title?: string; readonly turns?: Turn[]; readonly origin?: ChatOrigin; readonly providerData?: string; readonly interactivity?: ChatInteractivity }): ChatSummary | undefined {
701 const entry = this._sessionStates.get(session);
702 if (!entry) {
735 return chatSummary;
736 }
738 > /**
739 > * Re-registers an additional (non-default) peer chat when a session is
740 > * restored from persistent storage, seeding its {@link ChatState} with the
741 > * supplied turns. Unlike {@link addChat} this does not snapshot the session
742 > * title onto the default chat (the default chat's persisted title is
743 > * restored independently) and it seeds history. The catalog entry is added
744 > * in place so the object identity returned by {@link restoreSession} stays
745 > * live; no {@link ActionType.SessionChatAdded} is dispatched because restore
746 > * runs before clients subscribe.
747 > *
748 > * When `options.providerData` is supplied it is recorded verbatim as the
749 > * peer chat's opaque, agent-owned restore blob (see
750 > * {@link getChatProviderData}); the StateManager never parses it.
751 > */
752 > restoreChat(session: URI, chatUri: URI, options: { readonly title?: string; readonly turns: Turn[]; readonly draft?: Message; readonly providerData?: string; readonly origin?: ChatOrigin }): void {
753 const entry = this._sessionStates.get(session);
754 if (!entry) {
772 sessionState.chats = [...sessionState.chats, chatSummary];
773 }
775 > /**
776 > * Removes an additional chat from a session. Deletes its
777 > * {@link ChatState}, dispatches {@link ActionType.SessionChatRemoved}, and
778 > * — if the removed chat was the default — repoints `defaultChat` to the
779 > * first remaining chat. The default chat itself cannot be removed in
780 > * isolation; it lives and dies with its session.
781 > */
782 > removeChat(session: URI, chatUri: URI): void {
783 const entry = this._sessionStates.get(session);
784 if (!entry || !entry.state.chats.some(c => c.resource === chatUri)) {
801 this.dispatchServerAction(session, { type: ActionType.SessionChatRemoved, chat: chatUri });
802 }
804 > /**
805 > * Renames a single chat within a session independently of the session
806 > * title. Updates the chat's authoritative {@link ChatState} title (so
807 > * later `chatSummaryFromState` projections stay consistent) and dispatches
808 > * a {@link ActionType.SessionChatUpdated} so the session's catalog entry and
809 > * live subscribers reflect the new title. Works for the default chat too —
810 > * giving it a non-empty title that no longer inherits the session title.
811 > */
812 > updateChatTitle(session: URI, chatUri: URI, title: string): void {
813 const chatState = this._chatStates.get(chatUri);
814 if (chatState) {
817 this.dispatchServerAction(session, { type: ActionType.SessionChatUpdated, chat: chatUri, changes: { title } });
818 }
820 > /**
821 > * Removes a session from in-memory state without emitting a
822 > * {@link NotificationType.SessionRemoved} notification.
823 > * Use {@link deleteSession} when the session is being permanently deleted
824 > * and clients need to be notified of its removal.
825 > *
826 > * Any pending summary change is flushed synchronously before the session is
827 > * torn down, so clients receive the final status (e.g. Idle after a turn
828 > * completes) even when the session is evicted before the scheduler fires.
829 > * A {@link NotificationType.SessionSummaryChanged} notification may therefore
830 > * be emitted as a side-effect of this call.
831 > *
832 > * Per-session changesets are intentionally NOT torn down here: this method
833 > * is also used as an idle-eviction (LRU) hook (see
834 > * `AgentService._maybeEvictIdleSession`) and the session list view keeps a
835 > * changeset subscription open per visible row to render the diff chip.
836 > * Tearing down on eviction would clear the chip on the list while the row
837 > * is still on screen. Permanent-delete paths (`deleteSession`,
838 > * `removeSubagentSessions`) call `disposeSessionChangesets` explicitly
839 > * before invoking `removeSession`.
840 > */
841 > removeSession(session: URI): void {
842 const entry = this._sessionStates.get(session);
843 if (!entry) {
874 this._logService.trace(`[AgentHostStateManager] Removed session: ${session}`);
875 }
877 > /**
878 > * Permanently deletes a session from state and emits a
879 > * {@link NotificationType.SessionRemoved} notification so that clients
880 > * know the session is no longer accessible.
881 > *
882 > * Sessions whose creation was deferred via
883 > * `createSession({ emitNotification: false })` and never persisted via
884 > * {@link markSessionPersisted} are removed silently — no client knows
885 > * about them, so a `SessionRemoved` would be noise (or worse, would
886 > * cause clients to drop a session URI they had eagerly subscribed to).
887 > */
888 > deleteSession(session: URI): void {
889 const wasAnnounced = this._summaryNotifier.isAnnounced(session);
890 // Drop any pending summary diff: the forthcoming SessionRemoved notification
908 }
909 }
911 > // ---- Session meta -------------------------------------------------------
912 >
913 > /**
914 > * Replaces `state._meta` on a session by dispatching a
915 > * {@link ActionType.SessionMetaChanged} action so the change flows
916 > * through the action envelope (and thus to all live subscribers).
917 > *
918 > * The full `_meta` object is replaced (not merged) so callers stay in
919 > * control of the convention for their own keys; use the `withSessionXxx`
920 > * helpers in `sessionState.ts` to combine slots.
921 > */
922 > setSessionMeta(session: URI, meta: SessionMeta | undefined): void {
923 this.dispatchServerAction(session, { type: ActionType.SessionMetaChanged, _meta: meta });
924 }
926 > /**
927 > * Seeds or replaces a session's resolved {@link SessionConfigState} on the
928 > * live session state. Unlike mid-session {@link ActionType.SessionConfigChanged}
929 > * updates (which merge values onto an existing config), this establishes
930 > * the initial config and is therefore an in-place mutation of the
931 > * authoritative state object so the value is present in the first snapshot
932 > * a subscriber receives. Use this from create/restore flows where the
933 > * config is resolved asynchronously after the session state already exists
934 > * in the map — reading back through {@link getSessionState} would return a
935 > * detached composite copy and stranding the mutation there.
936 > */
937 > setSessionConfig(session: URI, config: SessionConfigState | undefined): void {
938 const entry = this._sessionStates.get(session);
939 if (!entry) {
943 entry.state.config = config;
944 }
946 > /**
947 > * Seeds or replaces the session's effective customizations directly on the
948 > * authoritative in-memory state. Used by create/restore flows to ensure the
949 > * first snapshot already contains customizations.
950 > */
951 > setSessionCustomizations(session: URI, customizations: readonly Customization[] | undefined): void {
952 const entry = this._sessionStates.get(session);
953 if (!entry) {
957 entry.state.customizations = customizations ? [...customizations] : undefined;
958 }
960 > // ---- Changeset registry -------------------------------------------------
961 >
962 > /**
963 > * Registers a server-side changeset so that subscribers can attach to its
964 > * URI. The changeset is created with the supplied initial status (default
965 > * {@link ChangesetStatus.Computing}); subsequent file/operation/status
966 > * mutations flow through {@link dispatchChangesetAction} on the
967 > * canonical `<sessionUri>/changeset/<changesetId>` URI.
968 > *
969 > * Idempotent: a second call with the same URI is a no-op so producers
970 > * can safely re-register on session resume without double-creating
971 > * state.
972 > *
973 > * Callers construct `changesetUri` via {@link buildSessionChangesetUri}
974 > * for the session-wide entry, or {@link buildChangesetUri} for any
975 > * other catalogue entry.
976 > *
977 > * Returns the supplied changeset URI for caller convenience.
978 > */
979 > registerChangeset(changesetUri: URI, initialStatus: ChangesetStatus = ChangesetStatus.Computing): URI {
980 this._changesets.register(changesetUri, initialStatus);
981 return changesetUri;
982 }
984 > /**
985 > * Updates the aggregate `changes` for a session.
986 > *
987 > * There is no dedicated action for this field: the value is purely
988 > * informational (chip rendering on the session list), so the write
989 > * piggybacks on the existing `sessionSummaryChanged` notification
990 > * path. We update the session entry, mark the session dirty, and let
991 > * the summary notifier's flush pick the new value up via its
992 > * `current.changes !== lastNotified.changes` diff.
993 > */
994 > setSessionSummaryChanges(session: URI, changes: ChangesSummary | undefined): void {
995 const entry = this._sessionStates.get(session);
996 if (!entry) {
1006 this._summaryNotifier.markDirty(session);
1007 }
1009 > /**
1010 > * Replaces the catalogue entries on `state.changesets` for `session` by
1011 > * dispatching a {@link ActionType.SessionChangesetsChanged} action.
1012 > * Subscribers see the mutation in the standard session action stream —
1013 > * the catalogue lives on session state and is not its own subscribable
1014 > * resource. Aggregate `changes` counts (additions / deletions /
1015 > * files) are propagated separately via {@link setSessionSummaryChanges}.
1016 > *
1017 > * Producers call this after each compute pass to keep the list of
1018 > * available changesets (with their `changeKind`) in sync so observers
1019 > * can render the correct entries without subscribing to each one.
1020 > */
1021 > setSessionChangesets(session: URI, changesets: readonly Changeset[] | undefined): void {
1022 const entry = this._sessionStates.get(session);
1023 if (!entry) {
1042 });
1043 }
1045 > /**
1046 > * Tear down a changeset. Dispatches {@link ActionType.ChangesetCleared}
1047 > * so subscribers see an empty file list, then deletes the local state
1048 > * so a fresh `getChangesetState` returns `undefined` and forces the
1049 > * producer to re-create the changeset on next subscribe.
1050 > *
1051 > * Per the spec, the server SHOULD also unsubscribe its clients after
1052 > * dispatching this action; for VS Code-internal clients that happens
1053 > * via the `notify/sessionRemoved` notification, which the workbench-side
1054 > * provider correlates to release any held subscriptions.
1055 > *
1056 > * Safe to call for a URI that was never registered: producers typically
1057 > * iterate over a candidate set on session disposal and emit dispose
1058 > * actions defensively.
1059 > */
1060 > disposeChangeset(changeset: URI): void {
1061 if (!this._changesets.has(changeset)) {
1062 return;
1067 this._changesets.delete(changeset);
1068 }
1070 > /**
1071 > * Disposes every changeset whose URI is nested under `session` (i.e.
1072 > * matches `<session>/changeset/...`). Used to cascade cleanup when a
1073 > * session itself is removed.
1074 > */
1075 > disposeSessionChangesets(session: URI): void {
1076 // Collect first because `disposeChangeset` mutates the underlying
1077 // map via its envelope handler.
1087 }
1088 }
1090 > /**
1091 > * Drops the annotation state nested under `session` (i.e. the
1092 > * `<session>/annotations` channel). Used to cascade cleanup when a
1093 > * session itself is removed. Subscriptions are released via the
1094 > * forthcoming `sessionRemoved` notification.
1095 > */
1096 > disposeSessionAnnotations(session: URI): void {
1097 this._annotations.delete(buildAnnotationsUri(session));
1098 }
1100 > // ---- Turn tracking ------------------------------------------------------
1101 >
1102 > /**
1103 > * Registers a mapping from turnId to session URI so that incoming
1104 > * provider events (which carry only session URI) can be associated
1105 > * with the correct active turn.
1106 > */
1107 > getActiveTurnId(sessionOrChat: URI): string | undefined {
1108 const chatUri = isAhpChatChannel(sessionOrChat) ? sessionOrChat : buildDefaultChatUri(sessionOrChat);
1109 return this._chatStates.get(chatUri)?.activeTurn?.id;
1110 }
1112 > // ---- Action dispatch ----------------------------------------------------
1113 >
1114 > /**
1115 > * Dispatch a server-originated action (from the agent backend).
1116 > * The action is applied to state via the reducer and emitted as an
1117 > * envelope with no origin (server-produced).
1118 > *
1119 > * `channel` identifies the channel the action targets — `ROOT_STATE_URI`
1120 > * for root actions, a session URI for session actions, a terminal URI
1121 > * for terminal actions, an expanded changeset URI for changeset actions.
1122 > */
1123 > dispatchServerAction(channel: URI, action: StateAction): void {
1124 > this._applyAndEmit(channel, action, undefined); agentHostStateManager.ts
1125 > }
1127 > /**
1128 > * Dispatch a client-originated action (write-ahead from a renderer).
1129 > * The action is applied to state and emitted with the client's origin
1130 > * so the originating client can reconcile.
1131 > */
1132 > dispatchClientAction(channel: URI, action: SessionAction | ChatAction | TerminalAction | ClientChangesetAction | ClientAnnotationsAction | IRootConfigChangedAction, origin: ActionOrigin): unknown {
1133 return this._applyAndEmit(channel, action, origin);
1134 }
1136 > /**
1137 > * Reject a client-originated action without applying it to state. Emits an
1138 > * {@link ActionEnvelope} that carries the original {@link ActionOrigin} and a
1139 > * {@link ActionEnvelope.rejectionReason | rejectionReason} so the originating
1140 > * client can reconcile (roll back) its optimistic write-ahead action through
1141 > * the normal path instead of leaving it pending until reconnect. The reducer
1142 > * is deliberately NOT run, so no synchronized state changes.
1143 > */
1144 > rejectClientAction(channel: URI, action: StateAction, origin: ActionOrigin, reason: string): void {
1145 const envelope: ActionEnvelope = {
1146 channel,
1153 this._onDidEmitEnvelope.fire(envelope);
1154 }
1156 > // ---- Internal -----------------------------------------------------------
1157 >
1158 > private _applyAndEmit(channel: URI, action: StateAction, origin: ActionOrigin | undefined): unknown {
1159 > let resultingState: unknown = undefined; agentHostStateManager.ts
1160 > if (action.type === ActionType.RootConfigChanged && action.replace) {
1161 action = {
1162 ...action,
1164 };
1165 }
1166 > // Apply to state agentHostStateManager.ts
1167 > if (isRootAction(action)) {
1168 > // `RootConfigChanged` can be a true no-op: the reducer merges/replaces agentHostStateManager.ts
1169 > // values even when the patch matches the current state, and re-emitting
1170 > // it would cause clients observing rootState.onDidChange to react and
1171 > // potentially re-dispatch in a loop. Check the action's own patch
1172 > // against current values before running the reducer so we avoid
1173 > // allocating a new state object at all.
1174 > if (action.type === ActionType.RootConfigChanged && this._rootState.config) {
1175 const current = this._rootState.config.values;
1176 const patch = action.config;
1182 }
1183 }
1184 > this._rootState = rootReducer(this._rootState, action as RootAction, this._log); agentHostStateManager.ts
1185 > resultingState = this._rootState;
1186 > }
1188 > if (isSessionAction(action)) {
1189 const sessionAction = action as SessionAction;
1190 const key = channel;
1206 }
1207 }
1209 > if (isChatAction(action)) {
1210 if (!isAhpChatChannel(channel)) {
1211 throw new Error(`[AgentHostStateManager] Chat action dispatched to non-chat channel: ${channel}, type=${action.type}`);
1224 }
1225 }
1227 > if (isChangesetAction(action)) {
1228 const changesetAction = action as ChangesetAction;
1229 const key = channel;
1243 resultingState = newState;
1244 }
1246 > if (isAnnotationsAction(action)) {
1247 const annotationsAction = action as AnnotationsAction;
1248 const key = channel;
1256 resultingState = newState;
1257 }
1259 > // Emit envelope
1260 > const envelope: ActionEnvelope = {
1261 > channel,
1262 > action,
1263 > serverSeq: ++this._serverSeq,
1264 > origin,
1265 > };
1266 >
1267 > this._logService.trace(`[AgentHostStateManager] Emitting envelope: seq=${envelope.serverSeq}, channel=${envelope.channel}, type=${action.type}${origin ? `, origin=${origin.clientId}:${origin.clientSeq}` : ''}`);
1268 > this._onDidEmitEnvelope.fire(envelope);
1269 >
1270 > return resultingState;
1271 > }
1273 > /**
1274 > * Removes a single chat from its session's active-turn set, firing the
1275 > * session-level active flip ({@link onDidChangeSessionActiveTurn} +
1276 > * {@link ActionType.RootActiveSessionsChanged}) when this clears the
1277 > * session's last active chat. Safe to call for chats that aren't currently
1278 > * tracked as active — it is a no-op in that case. Used both when a turn
1279 > * ends and when a chat is removed mid-turn, so the session can't be
1280 > * stranded as permanently "active".
1281 > */
1282 > private _removeChatActiveTurn(sessionKey: string, chatUri: string): void {
1283 const activeChats = this._sessionsWithActiveTurn.get(sessionKey);
1284 if (!activeChats || !activeChats.delete(chatUri)) {
1292 }
1293 }
1295 > /**
1296 > * Bridges a default-chat state transition back onto its owning session.
1297 > *
1298 > * The protocol moved turn lifecycle (and therefore the derived
1299 > * activity status) onto the chat channel. To preserve VS Code's
1300 > * single-chat behaviour we:
1301 > * - track active-turn transitions (driving `RootActiveSessionsChanged`
1302 > * and `hasActiveSessions`, which gate `--enable-remote-auto-shutdown`),
1303 > * keyed by the owning session URI;
1304 > * - mirror the chat's denormalized `status`/`activity`/`modifiedAt`
1305 > * onto the session summary so the session list reflects progress;
1306 > * - forward the chat's own `status` to the session `chats` catalog (via a
1307 > * {@link ActionType.SessionChatUpdated}) so per-chat tabs reflect that
1308 > * chat's progress, not just the aggregated session summary; and
1309 > * - keep the session's `chats` catalog entry in sync.
1310 > */
1311 > private _onChatStateChanged(sessionKey: string, chatUri: string, prev: ChatState, next: ChatState): void {
1312 // Active turn tracking — derive from the reducer's view of state,
1313 // never from raw action turn-ids, so out-of-order lifecycle actions
1381 }
1382 }
1384 > /**
1385 > * Aggregates a session's chat catalog into the derived session-summary
1386 > * fields per the protocol rules: activity bits come from the default chat
1387 > * (else the most recently modified chat) with `InputNeeded`/`Error`/
1388 > * `InProgress` promoted whenever any chat raises them; the `activity` string
1389 > * follows the chat driving the resulting status; `modifiedAt` is the max
1390 > * across chats. Promotion precedence is `InputNeeded` > `Error` >
1391 > * `InProgress`, so a running peer (sub) chat surfaces as `InProgress` on the
1392 > * session even when the default chat is idle.
1393 > */
1394 > private _aggregateChatSummaries(chats: readonly ChatSummary[], defaultChat: URI | undefined): { status?: SessionStatus; activity?: string; modifiedAt?: number } {
1395 if (chats.length === 0) {
1396 return {};
1419 return { status, activity: driver.activity, modifiedAt };
1420 }
1422 > /**
1423 > * Combines the chat's activity status bits with the session summary's
1424 > * own metadata flags (IsRead / IsArchived) which live in the high bits
1425 > * of {@link SessionStatus} and are owned by the session, not the chat.
1426 > */
1427 > private _mergeSessionStatus(sessionStatus: SessionStatus, chatStatus: SessionStatus): SessionStatus {
1428 const metaFlags = sessionStatus & (SessionStatus.IsRead | SessionStatus.IsArchived);
1429 const activityBits = chatStatus & ~(SessionStatus.IsRead | SessionStatus.IsArchived);
1430 return activityBits | metaFlags;
1431 }
1433 > /**
1434 > * Emit a generic progress notification on the root channel, correlated to
1435 > * the originating request by {@link ProgressParams.progressToken}. Routed to
1436 > * clients through the same {@link onDidEmitNotification} path as session
1437 > * notifications, so both the local (IPC proxy) and remote (WebSocket
1438 > * {@link ProtocolServerHandler}) renderers receive it without any
1439 > * transport-specific special casing. Progress for host-level work (e.g. a
1440 > * shared SDK download) rides the root channel rather than a per-session one.
1441 > */
1442 > emitProgress(progress: Omit<ProgressParams, 'channel'>): void {
1443 this._onDidEmitNotification.fire({
1444 type: 'root/progress',
1447 });
1448 }
1450 > /**
1451 > * Emit an `auth/required` notification on the root channel, asking the
1452 > * client to obtain a fresh token and push it via `authenticate`. Rides the
1453 > * same {@link onDidEmitNotification} path as {@link emitProgress}, so both
1454 > * local (IPC proxy) and remote (WebSocket) renderers receive it. Used for
1455 > * host-level auth requirements (e.g. an agent whose transport flip makes a
1456 > * credential newly required) rather than a per-session one.
1457 > */
1458 > emitAuthRequired(params: Omit<AuthRequiredParams, 'channel'>): void {
1459 this._onDidEmitNotification.fire({
1460 type: 'auth/required',
src/vs/platform/configuration/common/configurationRegistry.ts 643 covered LOC · 100 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- configurationRegistry.ts
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 > import { distinct } from '../../../base/common/arrays.js';
7 > import { IStringDictionary } from '../../../base/common/collections.js';
8 > import { Emitter, Event } from '../../../base/common/event.js';
9 > import { IJSONSchema } from '../../../base/common/jsonSchema.js';
10 > import * as types from '../../../base/common/types.js';
11 > import * as nls from '../../../nls.js';
12 > import { getLanguageTagSettingPlainKey } from './configuration.js';
13 > import { Extensions as JSONExtensions, IJSONContributionRegistry } from '../../jsonschemas/common/jsonContributionRegistry.js';
14 > import { Registry } from '../../registry/common/platform.js';
15 > import { IPolicy, IPolicyReference, PolicyName } from '../../../base/common/policy.js';
16 > import { Disposable } from '../../../base/common/lifecycle.js';
17 > import product from '../../product/common/product.js';
18 >
19 > export enum EditPresentationTypes {
20 > Multiline = 'multilineText',
21 > Singleline = 'singlelineText'
22 > }
23 >
24 > export const Extensions = {
25 > Configuration: 'base.contributions.configuration'
26 > };
27 >
28 > export interface IConfigurationDelta {
29 > removedDefaults?: IConfigurationDefaults[];
30 > removedConfigurations?: IConfigurationNode[];
31 > addedDefaults?: IConfigurationDefaults[];
32 > addedConfigurations?: IConfigurationNode[];
33 > }
34 >
35 > export interface IConfigurationRegistry {
36 >
37 > /**
38 > * Register a configuration to the registry.
39 > */
40 > registerConfiguration(configuration: IConfigurationNode): IConfigurationNode;
41 >
42 > /**
43 > * Register multiple configurations to the registry.
44 > */
45 > registerConfigurations(configurations: IConfigurationNode[], validate?: boolean): void;
46 >
47 > /**
48 > * Deregister multiple configurations from the registry.
49 > */
50 > deregisterConfigurations(configurations: IConfigurationNode[]): void;
51 >
52 > /**
53 > * update the configuration registry by
54 > * - registering the configurations to add
55 > * - dereigstering the configurations to remove
56 > */
57 > updateConfigurations(configurations: { add: IConfigurationNode[]; remove: IConfigurationNode[] }): void;
58 >
59 > /**
60 > * Register multiple default configurations to the registry.
61 > */
62 > registerDefaultConfigurations(defaultConfigurations: IConfigurationDefaults[]): void;
63 >
64 > /**
65 > * Deregister multiple default configurations from the registry.
66 > */
67 > deregisterDefaultConfigurations(defaultConfigurations: IConfigurationDefaults[]): void;
68 >
69 > /**
70 > * Bulk update of the configuration registry (default and configurations, remove and add)
71 > * @param delta
72 > */
73 > deltaConfiguration(delta: IConfigurationDelta): void;
74 >
75 > /**
76 > * Return the registered default configurations
77 > */
78 > getRegisteredDefaultConfigurations(): IConfigurationDefaults[];
79 >
80 > /**
81 > * Return the registered configuration defaults overrides
82 > */
83 > getConfigurationDefaultsOverrides(): Map<string, IConfigurationDefaultOverrideValue>;
84 >
85 > /**
86 > * Signal that the schema of a configuration setting has changes. It is currently only supported to change enumeration values.
87 > * Property or default value changes are not allowed.
88 > */
89 > notifyConfigurationSchemaUpdated(...configurations: IConfigurationNode[]): void;
90 >
91 > /**
92 > * Event that fires whenever a configuration has been
93 > * registered.
94 > */
95 > readonly onDidSchemaChange: Event<void>;
96 >
97 > /**
98 > * Event that fires whenever a configuration has been
99 > * registered.
100 > */
101 > readonly onDidUpdateConfiguration: Event<{ properties: ReadonlySet<string>; defaultsOverrides?: boolean }>;
102 >
103 > /**
104 > * Returns all configuration nodes contributed to this registry.
105 > */
106 > getConfigurations(): IConfigurationNode[];
107 >
108 > /**
109 > * Returns all configurations settings of all configuration nodes contributed to this registry.
110 > */
111 > getConfigurationProperties(): IStringDictionary<IRegisteredConfigurationPropertySchema>;
112 >
113 > /**
114 > * Returns the owning setting key per policy name (at most one owner per name).
115 > */
116 > getPolicyConfigurations(): Map<PolicyName, string>;
117 >
118 > /**
119 > * Returns the referencing setting keys per policy name.
120 > */
121 > getPolicyReferenceConfigurations(): Map<PolicyName, Set<string>>;
122 >
123 > /**
124 > * Returns all excluded configurations settings of all configuration nodes contributed to this registry.
125 > */
126 > getExcludedConfigurationProperties(): IStringDictionary<IRegisteredConfigurationPropertySchema>;
127 >
128 > /**
129 > * Register the identifiers for editor configurations
130 > */
131 > registerOverrideIdentifiers(identifiers: string[]): void;
132 > }
133 >
134 > export const enum ConfigurationScope {
135 > /**
136 > * Application specific configuration, which can be configured only in default profile user settings.
137 > */
138 > APPLICATION = 1,
139 > /**
140 > * Machine specific configuration, which can be configured only in local and remote user settings.
141 > */
142 > MACHINE,
143 > /**
144 > * An application machine specific configuration, which can be configured only in default profile user settings and remote user settings.
145 > */
146 > APPLICATION_MACHINE,
147 > /**
148 > * Window specific configuration, which can be configured in the user or workspace settings.
149 > */
150 > WINDOW,
151 > /**
152 > * Resource specific configuration, which can be configured in the user, workspace or folder settings.
153 > */
154 > RESOURCE,
155 > /**
156 > * Resource specific configuration that can be configured in language specific settings
157 > */
158 > LANGUAGE_OVERRIDABLE,
159 > /**
160 > * Machine specific configuration that can also be configured in workspace or folder settings.
161 > */
162 > MACHINE_OVERRIDABLE,
163 > }
164 >
165 >
166 > export interface IConfigurationPropertySchema extends IJSONSchema {
167 >
168 > scope?: ConfigurationScope;
169 >
170 > /**
171 > * When restricted, value of this configuration will be read only from trusted sources.
172 > * For eg., If the workspace is not trusted, then the value of this configuration is not read from workspace settings file.
173 > */
174 > restricted?: boolean;
175 >
176 > /**
177 > * When `false` this property is excluded from the registry. Default is to include.
178 > */
179 > included?: boolean;
180 >
181 > /**
182 > * List of tags associated to the property.
183 > * - A tag can be used for filtering
184 > * - Use `experimental` tag for marking the setting as experimental.
185 > */
186 > tags?: string[];
187 >
188 > /**
189 > * When enabled this setting is ignored during sync and user can override this.
190 > */
191 > ignoreSync?: boolean;
192 >
193 > /**
194 > * When enabled this setting is ignored during sync and user cannot override this.
195 > */
196 > disallowSyncIgnore?: boolean;
197 >
198 > /**
199 > * Disallow extensions to contribute configuration default value for this setting.
200 > */
201 > disallowConfigurationDefault?: boolean;
202 >
203 > /**
204 > * Labels for enumeration items
205 > */
206 > enumItemLabels?: string[];
207 >
208 > /**
209 > * Optional keywords used for search purposes.
210 > */
211 > keywords?: string[];
212 >
213 > /**
214 > * When specified, controls the presentation format of string settings.
215 > * Otherwise, the presentation format defaults to `singleline`.
216 > */
217 > editPresentation?: EditPresentationTypes;
218 >
219 > /**
220 > * When specified, gives an order number for the setting
221 > * within the settings editor. Otherwise, the setting is placed at the end.
222 > */
223 > order?: number;
224 >
225 > /**
226 > * When specified, this setting's value can always be overwritten by
227 > * a system-wide policy. Exactly one setting may *own* a given policy name.
228 > */
229 > policy?: IPolicy;
230 >
231 > /**
232 > * When specified, this setting is governed by a policy owned by another setting.
233 > * A setting must not declare both `policy` and `policyReference`.
234 > * The type must match the owning setting (enforced when exporting the policy catalog).
235 > */
236 > policyReference?: IPolicyReference;
237 >
238 > /**
239 > * When specified, this setting's default value can always be overwritten by
240 > * an experiment.
241 > */
242 > experiment?: {
243 > /**
244 > * The mode of the experiment.
245 > * - `startup`: The setting value is updated to the experiment value only on startup.
246 > * - `auto`: The setting value is updated to the experiment value automatically (whenever the experiment value changes).
247 > */
248 > mode: 'startup' | 'auto';
249 >
250 > /**
251 > * The name of the experiment. By default, this is `config.${settingId}`
252 > */
253 > name?: string;
254 > };
255 >
256 > /**
257 > * When specified, provides configuration overrides for the Agents window.
258 > */
259 > agentsWindow?: {
260 > /**
261 > * Override default value for this setting in the Agents window.
262 > */
263 > default?: unknown;
264 >
265 > /**
266 > * When `true`, this setting is read-only in the Agents window
267 > * and cannot be changed by the user.
268 > */
269 > readOnly?: boolean;
270 > };
271 > }
272 >
273 > export interface IExtensionInfo {
274 > id: string;
275 > displayName?: string;
276 > }
277 >
278 > export interface IConfigurationNode {
279 > id?: string;
280 > order?: number;
281 > type?: string | string[];
282 > title?: string;
283 > description?: string;
284 > properties?: IStringDictionary<IConfigurationPropertySchema>;
285 > allOf?: IConfigurationNode[];
286 > scope?: ConfigurationScope;
287 > extensionInfo?: IExtensionInfo;
288 > restrictedProperties?: string[];
289 > }
290 >
291 > export type ConfigurationDefaultSource = IExtensionInfo | string;
292 >
293 > export function isConfigurationDefaultSourceEquals(a: ConfigurationDefaultSource | undefined, b: ConfigurationDefaultSource | undefined): boolean {
294 if (a === b) {
295 return true;
303 return a.id === b.id;
304 }
306 > export type ConfigurationDefaultValueSource = ConfigurationDefaultSource | Map<string, ConfigurationDefaultSource>;
307 >
308 > export interface IConfigurationDefaults {
309 > overrides: IStringDictionary<unknown>;
310 > source?: ConfigurationDefaultSource;
311 > donotCache?: boolean;
312 > preventExperimentOverride?: boolean;
313 > }
314 >
315 > export type IRegisteredConfigurationPropertySchema = IConfigurationPropertySchema & {
316 > section?: {
317 > id?: string;
318 > title?: string;
319 > order?: number;
320 > extensionInfo?: IExtensionInfo;
321 > };
322 > defaultDefaultValue?: unknown;
323 > source?: ConfigurationDefaultSource; // Source of the Property
324 > defaultValueSource?: ConfigurationDefaultValueSource; // Source of the Default Value
325 > };
326 >
327 > export interface IConfigurationDefaultOverride {
328 > readonly value: unknown;
329 > readonly source?: ConfigurationDefaultSource; // Source of the default override
330 > }
331 >
332 > export interface IConfigurationDefaultOverrideValue {
333 > readonly value: unknown;
334 > readonly source?: ConfigurationDefaultValueSource;
335 > }
336 >
337 > export const allSettings: { properties: IStringDictionary<IConfigurationPropertySchema>; patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
338 > export const applicationSettings: { properties: IStringDictionary<IConfigurationPropertySchema>; patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
339 > export const applicationMachineSettings: { properties: IStringDictionary<IConfigurationPropertySchema>; patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
340 > export const machineSettings: { properties: IStringDictionary<IConfigurationPropertySchema>; patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
341 > export const machineOverridableSettings: { properties: IStringDictionary<IConfigurationPropertySchema>; patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
342 > export const windowSettings: { properties: IStringDictionary<IConfigurationPropertySchema>; patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
343 > export const resourceSettings: { properties: IStringDictionary<IConfigurationPropertySchema>; patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
344 >
345 > export const resourceLanguageSettingsSchemaId = 'vscode://schemas/settings/resourceLanguage';
346 > export const configurationDefaultsSchemaId = 'vscode://schemas/settings/configurationDefaults';
347 >
348 > const contributionRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
349 >
350 > class ConfigurationRegistry extends Disposable implements IConfigurationRegistry {
351 >
352 > private readonly registeredConfigurationDefaults: IConfigurationDefaults[] = [];
353 > private readonly configurationDefaultsOverrides: Map<string, { configurationDefaultOverrides: IConfigurationDefaultOverride[]; configurationDefaultOverrideValue?: IConfigurationDefaultOverrideValue }>;
354 > private readonly defaultLanguageConfigurationOverridesNode: IConfigurationNode;
355 > private readonly configurationContributors: IConfigurationNode[];
356 > private readonly configurationProperties: IStringDictionary<IRegisteredConfigurationPropertySchema>;
357 > private readonly policyConfigurations: Map<PolicyName, string>;
358 > private readonly policyReferenceConfigurations: Map<PolicyName, Set<string>>;
359 > private readonly excludedConfigurationProperties: IStringDictionary<IRegisteredConfigurationPropertySchema>;
360 > private readonly resourceLanguageSettingsSchema: IJSONSchema;
361 > private readonly overrideIdentifiers = new Set<string>();
362 >
363 > private readonly _onDidSchemaChange = this._register(new Emitter<void>());
364 > readonly onDidSchemaChange: Event<void> = this._onDidSchemaChange.event;
365 >
366 > private readonly _onDidUpdateConfiguration = this._register(new Emitter<{ properties: ReadonlySet<string>; defaultsOverrides?: boolean }>());
367 > readonly onDidUpdateConfiguration = this._onDidUpdateConfiguration.event;
368 >
369 > constructor() {
370 > super();
371 > this.configurationDefaultsOverrides = new Map();
372 > this.defaultLanguageConfigurationOverridesNode = {
373 > id: 'defaultOverrides',
374 > title: nls.localize('defaultLanguageConfigurationOverrides.title', "Default Language Configuration Overrides"),
375 > properties: {}
376 > };
377 > this.configurationContributors = [this.defaultLanguageConfigurationOverridesNode];
378 > this.resourceLanguageSettingsSchema = {
379 > properties: {},
380 > patternProperties: {},
381 > additionalProperties: true,
382 > allowTrailingCommas: true,
383 > allowComments: true
384 > };
385 > this.configurationProperties = {};
386 > this.policyConfigurations = new Map<PolicyName, string>();
387 > this.policyReferenceConfigurations = new Map<PolicyName, Set<string>>();
388 > this.excludedConfigurationProperties = {};
389 >
390 > contributionRegistry.registerSchema(resourceLanguageSettingsSchemaId, this.resourceLanguageSettingsSchema);
391 > this.registerOverridePropertyPatternKey();
392 > }
393 >
394 > public registerConfiguration(configuration: IConfigurationNode, validate: boolean = true): IConfigurationNode {
395 > this.registerConfigurations([configuration], validate); configurationRegistry.ts
396 > return configuration;
397 > }
399 > public registerConfigurations(configurations: IConfigurationNode[], validate: boolean = true): void {
400 > const properties = new Set<string>(); configurationRegistry.ts
401 > this.doRegisterConfigurations(configurations, validate, properties);
402 >
403 > contributionRegistry.registerSchema(resourceLanguageSettingsSchemaId, this.resourceLanguageSettingsSchema);
404 > this._onDidSchemaChange.fire();
405 > this._onDidUpdateConfiguration.fire({ properties });
406 > }
408 > public deregisterConfigurations(configurations: IConfigurationNode[]): void {
409 const properties = new Set<string>();
410 this.doDeregisterConfigurations(configurations, properties);
414 this._onDidUpdateConfiguration.fire({ properties });
415 }
417 > public updateConfigurations({ add, remove }: { add: IConfigurationNode[]; remove: IConfigurationNode[] }): void {
418 > const properties = new Set<string>(); configurationRegistry.ts
419 > this.doDeregisterConfigurations(remove, properties);
420 > this.doRegisterConfigurations(add, false, properties);
421 >
422 > contributionRegistry.registerSchema(resourceLanguageSettingsSchemaId, this.resourceLanguageSettingsSchema);
423 > this._onDidSchemaChange.fire();
424 > this._onDidUpdateConfiguration.fire({ properties });
425 > }
427 > public registerDefaultConfigurations(configurationDefaults: IConfigurationDefaults[]): void {
428 const properties = new Set<string>();
429 this.doRegisterDefaultConfigurations(configurationDefaults, properties);
431 this._onDidUpdateConfiguration.fire({ properties, defaultsOverrides: true });
432 }
434 > private doRegisterDefaultConfigurations(configurationDefaults: IConfigurationDefaults[], bucket: Set<string>) {
435
436 this.registeredConfigurationDefaults.push(...configurationDefaults);
480 this.doRegisterOverrideIdentifiers(overrideIdentifiers);
481 }
483 > public deregisterDefaultConfigurations(defaultConfigurations: IConfigurationDefaults[]): void {
484 const properties = new Set<string>();
485 this.doDeregisterDefaultConfigurations(defaultConfigurations, properties);
487 this._onDidUpdateConfiguration.fire({ properties, defaultsOverrides: true });
488 }
490 > private doDeregisterDefaultConfigurations(defaultConfigurations: IConfigurationDefaults[], bucket: Set<string>): void {
491 for (const defaultConfiguration of defaultConfigurations) {
492 const index = this.registeredConfigurationDefaults.indexOf(defaultConfiguration);
544 this.updateOverridePropertyPatternKey();
545 }
547 > private updateDefaultOverrideProperty(key: string, newDefaultOverride: IConfigurationDefaultOverrideValue, source: ConfigurationDefaultSource | undefined): void {
548 const property: IRegisteredConfigurationPropertySchema = {
549 section: {
564 this.defaultLanguageConfigurationOverridesNode.properties![key] = property;
565 }
567 > private mergeDefaultConfigurationsForOverrideIdentifier(overrideIdentifier: string, configurationValueObject: IStringDictionary<unknown>, valueSource: ConfigurationDefaultSource | undefined, existingDefaultOverride: IConfigurationDefaultOverrideValue | undefined): IConfigurationDefaultOverrideValue | undefined {
568 const defaultValue = existingDefaultOverride?.value || {};
569 const source = existingDefaultOverride?.source ?? new Map<string, ConfigurationDefaultSource>();
605 return { value: defaultValue, source };
606 }
608 > private mergeDefaultConfigurationsForConfigurationProperty(propertyKey: string, value: unknown, valuesSource: ConfigurationDefaultSource | undefined, existingDefaultOverride: IConfigurationDefaultOverrideValue | undefined): IConfigurationDefaultOverrideValue | undefined {
609 const property = this.configurationProperties[propertyKey];
610 const existingDefaultValue = existingDefaultOverride?.value ?? property?.defaultDefaultValue;
637 return { value, source };
638 }
640 > public deltaConfiguration(delta: IConfigurationDelta): void {
641 // defaults: remove
642 let defaultsOverrides = false;
662 this._onDidUpdateConfiguration.fire({ properties, defaultsOverrides });
663 }
665 > public notifyConfigurationSchemaUpdated(...configurations: IConfigurationNode[]) {
666 this._onDidSchemaChange.fire();
667 }
669 > public registerOverrideIdentifiers(overrideIdentifiers: string[]): void {
670 this.doRegisterOverrideIdentifiers(overrideIdentifiers);
671 this._onDidSchemaChange.fire();
672 }
674 > private doRegisterOverrideIdentifiers(overrideIdentifiers: string[]) {
675 for (const overrideIdentifier of overrideIdentifiers) {
676 this.overrideIdentifiers.add(overrideIdentifier);
678 this.updateOverridePropertyPatternKey();
679 }
681 > private doRegisterConfigurations(configurations: IConfigurationNode[], validate: boolean, bucket: Set<string>): void {
683 > configurations.forEach(configuration => {
684 >
685 > this.validateAndRegisterProperties(configuration, validate, configuration.extensionInfo, configuration.restrictedProperties, undefined, bucket);
686 >
687 > this.configurationContributors.push(configuration);
688 > this.registerJSONConfiguration(configuration);
689 > });
690 > }
692 > private doDeregisterConfigurations(configurations: IConfigurationNode[], bucket: Set<string>): void {
694 > const deregisterConfiguration = (configuration: IConfigurationNode) => {
695 if (configuration.properties) {
696 for (const key in configuration.properties) {
715 configuration.allOf?.forEach(node => deregisterConfiguration(node));
716 };
717 > for (const configuration of configurations) { configurationRegistry.ts
718 deregisterConfiguration(configuration);
719 const index = this.configurationContributors.indexOf(configuration);
722 }
723 }
726 > private validateAndRegisterProperties(configuration: IConfigurationNode, validate: boolean = true, extensionInfo: IExtensionInfo | undefined, restrictedProperties: string[] | undefined, scope: ConfigurationScope = ConfigurationScope.WINDOW, bucket: Set<string>): void {
727 > scope = types.isUndefinedOrNull(configuration.scope) ? scope : configuration.scope; configurationRegistry.ts
728 > const properties = configuration.properties;
729 > if (properties) {
730 > for (const key in properties) {
731 > const property: IRegisteredConfigurationPropertySchema = properties[key];
732 > property.section = {
733 > id: configuration.id,
734 > title: configuration.title,
735 > order: configuration.order,
736 > extensionInfo: configuration.extensionInfo
737 > };
738 > if (validate && validateProperty(key, property, extensionInfo?.id)) {
739 delete properties[key];
740 continue;
741 }
743 > property.source = extensionInfo;
744 >
745 > // update default value
746 > property.defaultDefaultValue = properties[key].default;
747 > this.updatePropertyDefaultValue(key, property);
748 >
749 > // update scope
750 > if (OVERRIDE_PROPERTY_REGEX.test(key)) {
751 property.scope = undefined; // No scope for overridable properties `[${identifier}]`
752 > } else { configurationRegistry.ts
753 > property.scope = types.isUndefinedOrNull(property.scope) ? scope : property.scope;
754 > property.restricted = types.isUndefinedOrNull(property.restricted) ? !!restrictedProperties?.includes(key) : property.restricted;
755 > }
756 >
757 > if (property.experiment) {
758 > if (!property.tags?.some(tag => tag.toLowerCase() === 'onexp')) { configurationRegistry.ts
759 > property.tags = property.tags ?? [];
760 > property.tags.push('onExP');
761 > }
762 > } else if (property.tags?.some(tag => tag.toLowerCase() === 'onexp')) { configurationRegistry.ts
763 console.error(`Invalid tag 'onExP' found for property '${key}'. Please use 'experiment' property instead.`);
764 property.experiment = { mode: 'startup' };
765 }
767 > const excluded = properties[key].hasOwnProperty('included') && !properties[key].included;
768 > const policyName = properties[key].policy?.name; configurationRegistry.ts
769 > const policyReferenceName = properties[key].policyReference?.name;
770 >
771 > if (excluded) {
772 this.excludedConfigurationProperties[key] = properties[key];
773 if (policyName) {
780 }
781 delete properties[key];
782 > } else { configurationRegistry.ts
783 > bucket.add(key);
784 > if (policyName) {
785 > this.policyConfigurations.set(policyName, key); configurationRegistry.ts
786 > }
787 > if (policyReferenceName) { configurationRegistry.ts
788 this.addPolicyReferenceConfiguration(policyReferenceName, key);
789 }
790 > this.configurationProperties[key] = properties[key]; configurationRegistry.ts
791 > if (!properties[key].deprecationMessage && properties[key].markdownDeprecationMessage) {
792 > // If not set, default deprecationMessage to the markdown source configurationRegistry.ts
793 > properties[key].deprecationMessage = properties[key].markdownDeprecationMessage;
794 > }
797 >
798 > }
799 > }
800 > const subNodes = configuration.allOf;
801 > if (subNodes) {
802 for (const node of subNodes) {
803 this.validateAndRegisterProperties(node, validate, extensionInfo, restrictedProperties, scope, bucket);
804 }
805 }
808 > private addPolicyReferenceConfiguration(policyName: PolicyName, key: string): void {
809 let keys = this.policyReferenceConfigurations.get(policyName);
810 if (!keys) {
814 keys.add(key);
815 }
817 > // Only for tests
818 > getConfigurations(): IConfigurationNode[] {
819 return this.configurationContributors;
820 }
822 > getConfigurationProperties(): IStringDictionary<IRegisteredConfigurationPropertySchema> {
823 > return this.configurationProperties; configurationRegistry.ts
824 > }
826 > getPolicyConfigurations(): Map<PolicyName, string> {
827 > return this.policyConfigurations; configurationRegistry.ts
828 > }
830 > getPolicyReferenceConfigurations(): Map<PolicyName, Set<string>> {
831 return this.policyReferenceConfigurations;
832 }
834 > getExcludedConfigurationProperties(): IStringDictionary<IRegisteredConfigurationPropertySchema> {
835 return this.excludedConfigurationProperties;
836 }
838 > getRegisteredDefaultConfigurations(): IConfigurationDefaults[] {
839 return [...this.registeredConfigurationDefaults];
840 }
842 > getConfigurationDefaultsOverrides(): Map<string, IConfigurationDefaultOverrideValue> {
843 const configurationDefaultsOverrides = new Map<string, IConfigurationDefaultOverrideValue>();
844 for (const [key, value] of this.configurationDefaultsOverrides) {
849 return configurationDefaultsOverrides;
850 }
852 > private registerJSONConfiguration(configuration: IConfigurationNode) {
853 > const register = (configuration: IConfigurationNode) => { configurationRegistry.ts
854 > const properties = configuration.properties;
855 > if (properties) {
856 > for (const key in properties) {
857 > this.updateSchema(key, properties[key]); configurationRegistry.ts
858 > }
860 > const subNodes = configuration.allOf;
861 > subNodes?.forEach(register);
862 > };
863 > register(configuration);
864 > }
866 > private updateSchema(key: string, property: IConfigurationPropertySchema): void {
867 > allSettings.properties[key] = property; configurationRegistry.ts
868 > switch (property.scope) {
869 > case ConfigurationScope.APPLICATION:
870 > applicationSettings.properties[key] = property; configurationRegistry.ts
871 > break;
872 > case ConfigurationScope.MACHINE: configurationRegistry.ts
873 > machineSettings.properties[key] = property; configurationRegistry.ts
874 > break;
875 > case ConfigurationScope.APPLICATION_MACHINE: configurationRegistry.ts
876 applicationMachineSettings.properties[key] = property;
877 break;
878 > case ConfigurationScope.MACHINE_OVERRIDABLE: configurationRegistry.ts
879 machineOverridableSettings.properties[key] = property;
880 break;
881 > case ConfigurationScope.WINDOW: configurationRegistry.ts
882 > windowSettings.properties[key] = property; configurationRegistry.ts
883 > break;
884 > case ConfigurationScope.RESOURCE: configurationRegistry.ts
885 resourceSettings.properties[key] = property;
886 break;
887 > case ConfigurationScope.LANGUAGE_OVERRIDABLE: configurationRegistry.ts
888 resourceSettings.properties[key] = property;
889 this.resourceLanguageSettingsSchema.properties![key] = property;
890 break;
892 > }
894 > private removeFromSchema(key: string, property: IConfigurationPropertySchema): void {
895 delete allSettings.properties[key];
896 switch (property.scope) {
917 }
918 }
920 > private updateOverridePropertyPatternKey(): void {
921 for (const overrideIdentifier of this.overrideIdentifiers.values()) {
922 const overrideIdentifierProperty = `[${overrideIdentifier}]`;
937 }
938 }
940 > private registerOverridePropertyPatternKey(): void {
941 > const resourceLanguagePropertiesSchema: IJSONSchema = {
942 > type: 'object',
943 > description: nls.localize('overrideSettings.defaultDescription', "Configure editor settings to be overridden for a language."),
944 > errorMessage: nls.localize('overrideSettings.errorMessage', "This setting does not support per-language configuration."),
945 > $ref: resourceLanguageSettingsSchemaId,
946 > };
947 > allSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema;
948 > applicationSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema;
949 > applicationMachineSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema;
950 > machineSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema;
951 > machineOverridableSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema;
952 > windowSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema;
953 > resourceSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema;
954 > this._onDidSchemaChange.fire();
955 > }
956 >
957 > private updatePropertyDefaultValue(key: string, property: IRegisteredConfigurationPropertySchema): void {
958 > const configurationdefaultOverride = this.configurationDefaultsOverrides.get(key)?.configurationDefaultOverrideValue; configurationRegistry.ts
959 > let defaultValue = undefined;
960 > let defaultSource = undefined;
961 > if (configurationdefaultOverride
962 && (!property.disallowConfigurationDefault || !configurationdefaultOverride.source) // Prevent overriding the default value if the property is disallowed to be overridden by configuration defaults from extensions
964 defaultValue = configurationdefaultOverride.value;
965 defaultSource = configurationdefaultOverride.source;
966 }
967 > if (types.isUndefined(defaultValue)) { configurationRegistry.ts
968 > defaultValue = property.defaultDefaultValue; configurationRegistry.ts
969 > defaultSource = undefined;
970 > }
971 > if (types.isUndefined(defaultValue)) { configurationRegistry.ts
972 > defaultValue = getDefaultValue(property.type); configurationRegistry.ts
973 > }
974 > property.default = defaultValue; configurationRegistry.ts
975 > property.defaultValueSource = defaultSource;
976 > }
978 >
979 > const OVERRIDE_IDENTIFIER_PATTERN = `\\[([^\\]]+)\\]`;
980 > const OVERRIDE_IDENTIFIER_REGEX = new RegExp(OVERRIDE_IDENTIFIER_PATTERN, 'g');
981 > export const OVERRIDE_PROPERTY_PATTERN = `^(${OVERRIDE_IDENTIFIER_PATTERN})+$`;
982 > export const OVERRIDE_PROPERTY_REGEX = new RegExp(OVERRIDE_PROPERTY_PATTERN);
983 >
984 > export function overrideIdentifiersFromKey(key: string): string[] {
985 const identifiers: string[] = [];
986 if (OVERRIDE_PROPERTY_REGEX.test(key)) {
996 return distinct(identifiers);
997 }
999 > export function keyFromOverrideIdentifiers(overrideIdentifiers: string[]): string {
1000 return overrideIdentifiers.reduce((result, overrideIdentifier) => `${result}[${overrideIdentifier}]`, '');
1001 }
1003 > export function getDefaultValue(type: string | string[] | undefined) {
1004 > const t = Array.isArray(type) ? type[0] : <string>type; configurationRegistry.ts
1005 > switch (t) {
1006 > case 'boolean':
1007 return false;
1008 > case 'integer': configurationRegistry.ts
1009 > case 'number':
1010 return 0;
1011 > case 'string': configurationRegistry.ts
1012 > return ''; configurationRegistry.ts
1013 > case 'array': configurationRegistry.ts
1014 > return []; configurationRegistry.ts
1015 > case 'object': configurationRegistry.ts
1016 return {};
1017 > default: configurationRegistry.ts
1018 return null;
1020 > }
1022 > const configurationRegistry = new ConfigurationRegistry();
1023 > Registry.add(Extensions.Configuration, configurationRegistry);
1024 >
1025 > export function validateProperty(property: string, schema: IRegisteredConfigurationPropertySchema, extensionId?: string): string | null {
1026 > if (!property.trim()) { configurationRegistry.ts
1027 return nls.localize('config.property.empty', "Cannot register an empty property");
1028 }
1029 > if (OVERRIDE_PROPERTY_REGEX.test(property)) { configurationRegistry.ts
1030 return nls.localize('config.property.languageDefault', "Cannot register '{0}'. This matches property pattern '\\\\[.*\\\\]$' for describing language specific editor settings. Use 'configurationDefaults' contribution.", property);
1031 }
1032 > if (configurationRegistry.getConfigurationProperties()[property] !== undefined && (!extensionId || !EXTENSION_UNIFICATION_EXTENSION_IDS.has(extensionId.toLowerCase()))) { configurationRegistry.ts
1033 return nls.localize('config.property.duplicate', "Cannot register '{0}'. This property is already registered.", property);
1034 }
1035 > if (schema.policy && schema.policyReference) { configurationRegistry.ts
1036 return nls.localize('config.policy.bothPolicyAndReference', "Cannot register '{0}'. A setting must not declare both 'policy' and 'policyReference'.", property);
1037 }
1038 > if (schema.policy?.name && configurationRegistry.getPolicyConfigurations().get(schema.policy?.name) !== undefined) { configurationRegistry.ts
1039 return nls.localize('config.policy.duplicate', "Cannot register '{0}'. The associated policy {1} is already registered with {2}. To attach another setting to the same policy, use 'policyReference'.", property, schema.policy?.name, configurationRegistry.getPolicyConfigurations().get(schema.policy?.name));
1040 }
1041 > return null; configurationRegistry.ts
1042 > }
1044 > export function getScopes(): [string, ConfigurationScope | undefined][] {
1045 const scopes: [string, ConfigurationScope | undefined][] = [];
1046 const configurationProperties = configurationRegistry.getConfigurationProperties();
1052 return scopes;
1053 }
1055 > export function getAllConfigurationProperties(configurationNode: IConfigurationNode[]): IStringDictionary<IRegisteredConfigurationPropertySchema> {
1056 const result: IStringDictionary<IRegisteredConfigurationPropertySchema> = {};
1057 for (const configuration of configurationNode) {
1068 return result;
1069 }
1071 > export function parseScope(scope: string): ConfigurationScope {
1072 switch (scope) {
1073 case 'application':
1085 }
1086 }
1088 > // Used for extension unification. Should be removed when complete.
1089 > export const EXTENSION_UNIFICATION_EXTENSION_IDS: Set<string> = new Set(product.defaultChatAgent ? [product.defaultChatAgent.extensionId, product.defaultChatAgent.chatExtensionId].map(id => id.toLowerCase()) : []);
src/vs/platform/agentHost/node/shared/copilotApiService.ts 634 covered LOC · 31 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- copilotApiService.ts
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 > import type Anthropic from '@anthropic-ai/sdk';
7 > import { CAPIClient, RequestType, type CCAModel, type IExtensionInformation } from '@vscode/copilot-api';
8 > import { generateUuid } from '../../../../base/common/uuid.js';
9 > import { getDevDeviceId, getMachineId } from '../../../../base/node/id.js';
10 > import { createDecorator } from '../../../instantiation/common/instantiation.js';
11 > import { IAgentHostGitHubEndpointService } from '../agentHostGitHubEndpointService.js';
12 > import { ILogService } from '../../../log/common/log.js';
13 > import { IProductService } from '../../../product/common/productService.js';
14 > import { COPILOT_LICENSE_AGREEMENT } from '../../../endpoint/common/licenseAgreement.js';
15 > import { parseCopilotTokenFields } from '../copilot/copilotTokenFields.js';
16 >
17 > // #region Types
18 >
19 > /**
20 > * Per-call transport options for all {@link ICopilotApiService} methods.
21 > *
22 > * `headers` are merged into the outgoing CAPI request before security-
23 > * sensitive headers (`Authorization`, `Content-Type`, `X-Request-Id`,
24 > * `OpenAI-Intent`), so callers cannot override those.
25 > *
26 > * `signal` propagates to the outgoing API request but **not** to the
27 > * shared token mint. The mint is deduped across concurrent callers, so
28 > * a single caller's abort must not cancel it for everyone.
29 > */
30 > export interface ICopilotApiServiceRequestOptions {
31 > readonly headers?: Readonly<Record<string, string>>;
32 > readonly signal?: AbortSignal;
33 >
34 > /**
35 > * Suppress the `Copilot-Integration-Id` header on this request.
36 > *
37 > * When unset, `@vscode/copilot-api` derives the integration id from the
38 > * discovered Copilot SKU: a `no_auth_limited_copilot` SKU maps to
39 > * `vscode-nl`, which the CAPI backend treats as the limited/no-auth
40 > * integration and refuses premium models such as `claude-opus-4.7`.
41 > * Setting this to `true` omits the header so CAPI authorizes against the
42 > * token's real entitlement. Mirrors the Copilot Chat extension's
43 > * `ClaudeStreamingPassThroughEndpoint.getEndpointFetchOptions()`.
44 > */
45 > readonly suppressIntegrationId?: boolean;
46 > }
47 >
48 > /**
49 > * One chat message in a {@link ICopilotUtilityChatCompletionRequest}.
50 > * Mirrors the OpenAI Chat Completions message shape CAPI accepts.
51 > */
52 > export interface ICopilotUtilityChatMessage {
53 > readonly role: 'system' | 'user' | 'assistant';
54 > readonly content: string;
55 > }
56 >
57 > /**
58 > * Inputs for {@link ICopilotApiService.utilityChatCompletion}.
59 > *
60 > * Callers own prompt construction — typically a `'system'` rules message
61 > * followed by one or more `'user'` messages, matching the Copilot Chat
62 > * extension's `copilot-utility-small` prompts (see
63 > * `GitCommitMessagePrompt`'s `SystemMessage` + `UserMessage` pair). This
64 > * service forwards the messages and returns the assistant text.
65 > *
66 > * `temperature` defaults to `0.1` (matching the Copilot Chat extension's
67 > * default `IConversationOptions.temperature`). All other parameters
68 > * (`top_p`, model family) are fixed defaults inside the service — callers
69 > * should not need to tune them for utility flows. `max_tokens` is left
70 > * unset so CAPI applies its per-model default, matching what the
71 > * extension's `copilot-utility-small` endpoint sends today.
72 > */
73 > export interface ICopilotUtilityChatCompletionRequest {
74 > readonly messages: readonly ICopilotUtilityChatMessage[];
75 > readonly temperature?: number;
76 > }
77 >
78 > /**
79 > * Subset of the GitHub `copilot_internal/user` response we care about.
80 > * The full payload carries entitlement info; we only need `endpoints` (for
81 > * routing CAPI requests) and `access_type_sku` (which `CAPIClient.updateDomains`
82 > * stamps onto requests).
83 > */
84 > interface ICopilotUserResponse {
85 > readonly login?: string;
86 > readonly copilotignore_enabled?: boolean;
87 > readonly endpoints?: {
88 > readonly api?: string;
89 > readonly telemetry?: string;
90 > readonly proxy?: string;
91 > readonly 'origin-tracker'?: string;
92 > };
93 > readonly access_type_sku?: string;
94 > }
95 >
96 > interface ICachedClient {
97 > readonly capiClient: CAPIClient;
98 > readonly expiresAt: number;
99 > /** GitHub login returned by `/copilot_internal/user`, when present. */
100 > readonly login?: string;
101 > /** The CAPI `endpoints.telemetry` base URL discovered for this token, if any. */
102 > readonly telemetryEndpoint?: string;
103 > /** The CAPI `endpoints.api` base URL discovered (or overridden) for this token, if any. */
104 > readonly apiEndpoint?: string;
105 > readonly copilotIgnoreEnabled?: boolean;
106 > }
107 >
108 > /**
109 > * Subset of the `RequestType.CopilotToken` mint response we care about.
110 > */
111 > interface ICopilotTokenEnvelope {
112 > readonly token?: unknown;
113 > readonly expires_at?: unknown;
114 > readonly refresh_in?: unknown;
115 > readonly organization_list?: unknown;
116 > }
117 >
118 > /**
119 > * Per-GitHub-token Copilot session token cache entry, plus a per-family
120 > * resolved utility model id. The model id is bound to the same lifetime as
121 > * the Copilot token so the entry can be evicted atomically on 401/403.
122 > */
123 > interface ICachedCopilotToken {
124 > readonly token: string;
125 > readonly expiresAt: number;
126 > readonly modelIdsByFamily: Map<string, string>;
127 > readonly isInternal: boolean;
128 > readonly isVscodeTeamMember: boolean;
129 > }
130 >
131 > /**
132 > * Memoized parts of `CAPIClient` construction that don't depend on the user
133 > * token. Built once and reused by every per-token client.
134 > */
135 > interface ICapiBase {
136 > readonly extensionInfo: IExtensionInformation;
137 > readonly userUrl: string;
138 > }
139 >
140 > // #endregion
141 >
142 > // #region Constants
143 >
144 > /**
145 > * Sentinel {@link CopilotApiError.status} used when the error came from a
146 > * mid-stream SSE `event: error` frame rather than an HTTP non-2xx response.
147 > * The upstream HTTP status was 200 (the stream had already started); the
148 > * real HTTP status is no longer meaningful, so consumers that need an HTTP
149 > * status code (e.g. when re-emitting before headers are sent) should not
150 > * trust this value. Use `envelope.error.type` instead.
151 > */
152 > export const COPILOT_API_ERROR_STATUS_STREAMING = 520;
153 >
154 > /**
155 > * Re-resolve the CAPI endpoint discovery this many seconds before the cache
156 > * entry's notional expiry. The `/copilot_internal/user` response itself
157 > * carries no expiry, so we apply a fixed TTL and refresh ahead of it.
158 > */
159 > const CAPI_CONTEXT_REFRESH_BUFFER_SECONDS = 5 * 60;
160 >
161 > /** Conservative TTL for the `/copilot_internal/user` discovery result. */
162 > const CAPI_CONTEXT_TTL_SECONDS = 30 * 60;
163 >
164 > const USER_API_VERSION = '2025-04-01';
165 >
166 > /**
167 > * Test/debug override for the CAPI base URL. When set to a **loopback** URL,
168 > * {@link CopilotApiService} skips the `api.github.com/copilot_internal/user`
169 > * endpoint-discovery round-trip (which requires a real GitHub token) and routes
170 > * every CAPI request — `models`, `responses`, `messages` — straight at this URL
171 > * instead. Only ever set by the smoke-test harness (see `setupAgentHostSuite`)
172 > * so the agent host's shared CAPI client can talk to the mock LLM server; never
173 > * set in production, so normal per-token discovery is unchanged.
174 > *
175 > * The override is restricted to loopback hosts, plus the reserved
176 > * `vscode-smoke.test` host when the smoke proxy marker is present. Subsequent
177 > * CAPI calls carry the user's GitHub bearer token, so every other non-loopback
178 > * or unparseable value is ignored to prevent token exfiltration.
179 > */
180 > const CAPI_URL_OVERRIDE_ENV = 'VSCODE_AGENT_HOST_CAPI_URL_OVERRIDE';
181 > const CAPI_URL_OVERRIDE_SMOKE_TEST_HOST = 'vscode-smoke.test';
182 > const CAPI_URL_OVERRIDE_SMOKE_TEST_ENV = 'VSCODE_SMOKE_TEST_PROXY_HEADER';
183 >
184 > /** True iff `url` parses and its host is a loopback address (localhost / 127.0.0.0/8 / ::1). */
185 function isLoopbackUrl(url: string): boolean {
186 let hostname: string;
194 return host === 'localhost' || host === '::1' || /^127(?:\.\d{1,3}){3}$/.test(host);
195 }
197 function isAllowedCapiUrlOverride(url: string): boolean {
198 if (isLoopbackUrl(url)) {
208 }
209 }
211 > /**
212 > * Re-mint the Copilot session token this many seconds before its
213 > * server-reported `expires_at`, mirroring the Copilot Chat extension's
214 > * `RefreshableCopilotTokenManager` 5-minute refresh buffer.
215 > */
216 > const COPILOT_TOKEN_REFRESH_BUFFER_SECONDS = 5 * 60;
217 >
218 > /**
219 > * Default CAPI model family for {@link ICopilotApiService.utilityChatCompletion}.
220 > * Matches the Copilot Chat extension's `copilot-utility-small` resolver
221 > * (`CopilotUtilitySmallChatEndpoint.capiFamily === CHAT_MODEL.GPT4OMINI`).
222 > */
223 > const UTILITY_DEFAULT_MODEL_FAMILY = 'gpt-4o-mini';
224 >
225 > /**
226 > * Default `temperature` for utility chat completions. Matches the Copilot
227 > * Chat extension's default `IConversationOptions.temperature`.
228 > */
229 > const UTILITY_DEFAULT_TEMPERATURE = 0.1;
230 >
231 > /**
232 > * Default `top_p` for utility chat completions. Matches the Copilot Chat
233 > * extension's default `IConversationOptions.topP`.
234 > */
235 > const UTILITY_DEFAULT_TOP_P = 1;
236 >
237 > /**
238 > * `OpenAI-Intent` value for utility chat completions. Matches the extension
239 > * vocabulary `'conversation-background'` for non-user-initiated utility
240 > * calls (chat title generation, commit messages, branch names, etc.).
241 > */
242 > const UTILITY_INTENT = 'conversation-background';
243 >
244 > const INTERNAL_COPILOT_ORGANIZATIONS = new Set([
245 > '4535c7beffc844b46bb1ed4aa04d759a',
246 > 'a5db0bcaae94032fe715fb34a5e4bce2',
247 > '7184f66dfcee98cb5f08a1cb936d5225',
248 > '1cb18ac6eedd49b43d74a1c5beb0b955',
249 > 'ea9395b9a9248c05ee6847cbd24355ed',
250 > ]);
251 > const VSCODE_COPILOT_ORGANIZATIONS = new Set(['551cca60ce19654d894e786220822482']);
252 >
253 > // #endregion
254 >
255 > // #region Errors
256 >
257 > /**
258 > * Thrown by {@link ICopilotApiService} when CAPI returns an Anthropic-format
259 > * API error — either as a non-2xx HTTP response or as a mid-stream
260 > * `event: error` SSE frame. Carries enough information for the Phase 2
261 > * Claude proxy to re-emit the error passthrough without re-mapping.
262 > *
263 > * Network/transport failures (connection reset, DNS failure, etc.) are
264 > * **not** wrapped as `CopilotApiError` — they propagate as raw `fetch`
265 > * rejections so consumers can distinguish API errors from transport errors.
266 > */
267 > export class CopilotApiError extends Error {
268 >
269 > /**
270 > * @param status HTTP status from the originating CAPI response, or
271 > * {@link COPILOT_API_ERROR_STATUS_STREAMING} for mid-stream SSE errors.
272 > * @param envelope Anthropic-format error envelope. For HTTP errors with a
273 > * non-conforming body (plain text, malformed JSON, missing fields) this
274 > * is synthesized; for conforming bodies and SSE frames it is the
275 > * server's envelope verbatim.
276 > * @param message Optional override for `Error.message`. Defaults to
277 > * `envelope.error.message`. **Never includes auth tokens.**
278 > */
279 > constructor(
280 readonly status: number,
281 readonly envelope: Anthropic.ErrorResponse,
285 this.name = 'CopilotApiError';
286 }
288 >
289 > /**
290 > * Build a {@link CopilotApiError} from a CAPI HTTP response body. If the
291 > * body parses as a conforming Anthropic envelope, it is used verbatim;
292 > * otherwise a synthetic envelope is constructed with `error.type:
293 > * 'api_error'` and the response body as `error.message` (or status text
294 > * when the body is empty). The returned error's `message` deliberately
295 > * mirrors the original `"<prefix>: <status> <statusText>"` format so
296 > * existing log-line consumers continue to read identifiably. `prefix`
297 > * defaults to `"CAPI request failed"` (the historical wording for
298 > * `messages`); pass `"CAPI models request failed"` for the `models()` path.
299 > */
300 function buildCopilotApiHttpError(status: number, statusText: string, bodyText: string, prefix = 'CAPI request failed'): CopilotApiError {
301 let envelope: Anthropic.ErrorResponse | undefined;
336 );
337 }
339 > // #endregion
340 >
341 > export type FetchFunction = typeof globalThis.fetch;
342 >
343 > export const ICopilotApiService = createDecorator<ICopilotApiService>('copilotApiService');
344 >
345 > /**
346 > * Foundational gateway between the agent host and GitHub Copilot's CAPI proxy
347 > * for Anthropic-style chat completions and model discovery.
348 > *
349 > * ## Goals
350 > *
351 > * 1. **Single source of truth for CAPI auth.** Callers pass a raw GitHub token
352 > * and never deal with endpoint discovery or routing themselves.
353 > * 2. **Stable surface for chat agents.** A small, typed API that abstracts the
354 > * underlying `CAPIClient`, SSE framing, and Anthropic event taxonomy so
355 > * feature code can focus on prompting.
356 > * 3. **Resource-safe streaming.** Async-generator output that fully releases
357 > * the underlying HTTP connection regardless of how the consumer terminates
358 > * iteration (early `break`, thrown error, abort, or natural end-of-stream).
359 > * 4. **Skew- and revocation-tolerant context cache.** Endpoint/sku discovery
360 > * stays cached as long as it's usable and is invalidated immediately on
361 > * `401`/`403` so callers self-heal without restarting the host.
362 > *
363 > * ## Auth strategy
364 > *
365 > * The GitHub user token IS the credential. There is no Copilot session-token
366 > * mint; we send `Authorization: Bearer <github-token>` directly to CAPI's
367 > * `/v1/messages` and `/models` endpoints. This mirrors what the
368 > * `@github/copilot` CLI does (see `fetchCopilotUser` and
369 > * `CopilotAnthropicClient.createWithOAuthToken` in `github/copilot-agent-runtime`).
370 > *
371 > * The `endpoints.api` URL CAPI requests are routed to is discovered per-token
372 > * by calling `GET /copilot_internal/user` once and caching the result. This
373 > * works for both consumer (`api.githubcopilot.com`) and Enterprise
374 > * (`api.enterprise.githubcopilot.com`) accounts without configuration.
375 > *
376 > * {@link utilityChatCompletion} is the one exception to the
377 > * GitHub-token-IS-the-credential rule: CAPI's `/chat/completions` endpoint
378 > * expects a Copilot session token (the same one the Copilot Chat extension
379 > * mints via `RequestType.CopilotToken`). The service mints it internally
380 > * from the supplied GitHub token, caches it per-token alongside the
381 > * resolved utility model id, and refreshes ahead of expiry.
382 > *
383 > * ## Non-goals
384 > *
385 > * - Per-conversation history, retry/backoff, or rate-limit handling. Callers
386 > * own request orchestration.
387 > *
388 > * ## Concurrency model
389 > *
390 > * - Each cached entry is a **distinct {@link CAPIClient} instance** with its
391 > * own discovered domain state. Concurrent in-flight requests for two
392 > * different GitHub tokens cannot trample each other's `endpoints.api` —
393 > * token A's request will always route through the client built for A.
394 > * - Multiple in-flight requests for the **same** GitHub token share a single
395 > * endpoint-discovery call via the per-token cache map (no thundering herd
396 > * on cold start).
397 > * - `AbortSignal` is forwarded to the outgoing API request (messages, models)
398 > * but **not** to the shared discovery call, so cancellation propagates to
399 > * the caller's own request without affecting concurrent callers sharing the
400 > * discovery.
401 > *
402 > * ## Error semantics
403 > *
404 > * - Network/transport errors propagate as raw `fetch` rejections (e.g.
405 > * connection reset, DNS failure). Consumers can distinguish them from
406 > * API errors by `instanceof CopilotApiError`.
407 > * - Non-2xx responses from CAPI's `messages` and `models` endpoints throw
408 > * {@link CopilotApiError} carrying the HTTP `status` and the parsed
409 > * Anthropic error `envelope` (synthesized if the response body isn't a
410 > * conforming envelope). **Tokens are never embedded in error messages.**
411 > * - Streaming `event: error` SSE frames throw {@link CopilotApiError} with
412 > * `status` set to {@link COPILOT_API_ERROR_STATUS_STREAMING} (the upstream
413 > * HTTP status was 200 and is no longer meaningful) and the server-supplied
414 > * error envelope preserved verbatim.
415 > * - Failures of the `/copilot_internal/user` discovery call throw plain
416 > * `Error` (not `CopilotApiError`) with a `"Copilot endpoint discovery
417 > * failed: ..."` prefix — it is an implementation detail of this service
418 > * and is not part of the Anthropic-shaped CAPI surface.
419 > * - Malformed JSON in an SSE `data:` line is logged and skipped, not thrown.
420 > */
421 > /**
422 > * Restricted/enhanced telemetry context derived from a user's minted CAPI Copilot session token,
423 > * mirroring what the Copilot extension reads off its `CopilotToken` (`rt` opt-in, `tid` tracking id)
424 > * plus the CAPI `endpoints.telemetry` host.
425 > */
426 > export interface IRestrictedTelemetryContext {
427 > /** Whether the token opts into enhanced/restricted telemetry (the `rt=1` claim). */
428 > readonly restrictedTelemetryEnabled: boolean;
429 > /** The Copilot user tracking id (`tid` claim), or `undefined` when absent. */
430 > readonly trackingId: string | undefined;
431 > /** The CAPI `endpoints.telemetry` base URL, resolved only when enabled; `undefined` otherwise. */
432 > readonly telemetryEndpoint: string | undefined;
433 > /** Whether the token belongs to a GitHub or Microsoft internal organization. */
434 > readonly isInternal?: boolean;
435 > /** GitHub login returned by `/copilot_internal/user`. */
436 > readonly userName?: string;
437 > /** Whether the token identifies a VS Code team member. */
438 > readonly isVscodeTeamMember?: boolean;
439 > /** Whether content exclusion is enabled; undefined when discovery could not determine it. */
440 > readonly copilotIgnoreEnabled?: boolean;
441 > }
442 >
443 > export interface ICopilotApiService {
444 >
445 > readonly _serviceBrand: undefined;
446 >
447 > /**
448 > * Stream a chat completion as raw Anthropic stream events.
449 > *
450 > * Yields every `Anthropic.MessageStreamEvent` in the order the server
451 > * emits them, **including `message_stop` as the last event** before the
452 > * generator returns. Phase 2 proxy relies on receiving a complete,
453 > * replayable event stream.
454 > *
455 > * @throws on non-2xx status or SSE `error` event.
456 > */
457 > messages(
458 > githubToken: string,
459 > request: Anthropic.MessageCreateParamsStreaming,
460 > options?: ICopilotApiServiceRequestOptions,
461 > ): AsyncGenerator<Anthropic.MessageStreamEvent>;
462 >
463 > /**
464 > * Send a chat completion and return the full aggregated response.
465 > * @throws on non-2xx status.
466 > */
467 > messages(
468 > githubToken: string,
469 > request: Anthropic.MessageCreateParamsNonStreaming,
470 > options?: ICopilotApiServiceRequestOptions,
471 > ): Promise<Anthropic.Message>;
472 >
473 > /**
474 > * Count tokens for a hypothetical request.
475 > *
476 > * @throws always — `countTokens` is not supported by CAPI in Phase 1.5.
477 > * Phase 2 proxy maps this to HTTP 501.
478 > */
479 > countTokens(
480 > githubToken: string,
481 > req: Anthropic.MessageCountTokensParams,
482 > options?: ICopilotApiServiceRequestOptions,
483 > ): Promise<Anthropic.MessageTokensCount>;
484 >
485 > /**
486 > * List models available to the GitHub user.
487 > *
488 > * Each {@link CCAModel} carries a `vendor` (e.g. `'Anthropic'`) and
489 > * `supported_endpoints` (e.g. `['/v1/messages']`). Callers filtering for
490 > * Anthropic-format models should match on both fields.
491 > *
492 > * Known CAPI values as of 2026-04-30:
493 > * - `vendor`: `'Anthropic'` (capitalized)
494 > * - `supported_endpoints`: `'/v1/messages'` for Anthropic chat models
495 > */
496 > models(githubToken: string, options?: ICopilotApiServiceRequestOptions): Promise<CCAModel[]>;
497 >
498 > /**
499 > * Pass-through to CAPI's OpenAI-shaped Responses endpoint
500 > * (`{capiBaseUrl}/responses`). Used by `CodexProxyService` to forward
501 > * `/v1/responses` requests from the Codex CLI without deserializing
502 > * the body. The caller owns the returned `Response` (its body and any
503 > * streaming) and is responsible for consuming or aborting it.
504 > *
505 > * @throws on non-2xx upstream response.
506 > */
507 > responses(
508 > githubToken: string,
509 > body: string,
510 > options?: ICopilotApiServiceRequestOptions,
511 > ): Promise<Response>;
512 >
513 > /**
514 > * Send arbitrary user chat messages through CAPI's `/chat/completions`
515 > * endpoint and return the assistant text.
516 > *
517 > * Internally mints (and caches) a Copilot session token from the
518 > * supplied GitHub token — the same flow the Copilot Chat extension
519 > * uses for its `copilot-utility-small` endpoint (PR title/description,
520 > * commit messages, branch names, chat titles, etc.). Uses the
521 > * `gpt-4o-mini` model family with `top_p = 1` and `temperature = 0.1`
522 > * by default (override via `request.temperature`).
523 > *
524 > * Non-streaming. Callers own prompt construction and any
525 > * domain-specific parsing of the returned text.
526 > *
527 > * @throws {@link CopilotApiError} on non-2xx CAPI response.
528 > * @throws plain `Error` when no model in the requested family is
529 > * available or when the response contains no text content.
530 > */
531 > utilityChatCompletion(
532 > githubToken: string,
533 > request: ICopilotUtilityChatCompletionRequest,
534 > options?: ICopilotApiServiceRequestOptions,
535 > ): Promise<string>;
536 >
537 > /**
538 > * Resolve this user's restricted-telemetry context from the minted CAPI Copilot session token —
539 > * the `rt` opt-in and `tid` tracking id — plus the CAPI `endpoints.telemetry` host. The GitHub
540 > * token itself carries none of these claims; they live in the Copilot session token (minted via
541 > * `RequestType.CopilotToken`), exactly as the Copilot extension reads them off its `CopilotToken`.
542 > * The telemetry endpoint is resolved only when enabled, so public users incur no extra discovery.
543 > */
544 > resolveRestrictedTelemetryContext(githubToken: string): Promise<IRestrictedTelemetryContext>;
545 >
546 > /**
547 > * Resolve the CAPI `endpoints.api` base URL discovered for this GitHub token
548 > * (or the loopback test override), or `undefined` when discovery hasn't run
549 > * or failed. The effective CAPI host varies by account (consumer
550 > * `api.githubcopilot.com` vs. Enterprise / proxy), so callers that need the
551 > * real host — e.g. to resolve the correct proxy — should prefer this over the
552 > * hardcoded default.
553 > */
554 > resolveApiEndpoint(githubToken: string): Promise<string | undefined>;
555 >
556 > /** Resolve the GitHub login cached from `/copilot_internal/user`. */
557 > resolveUserLogin?(githubToken: string): Promise<string | undefined>;
558 > }
559 >
560 > export class CopilotApiService implements ICopilotApiService {
561 >
562 > declare readonly _serviceBrand: undefined;
563 >
564 > private _capiBasePromise: Promise<ICapiBase> | null = null;
565 > private readonly _clientsByToken = new Map<string, Promise<ICachedClient>>();
566 > private readonly _copilotTokensByGithub = new Map<string, Promise<ICachedCopilotToken>>();
567 > private readonly _fetch: FetchFunction;
568 >
569 > constructor(
570 > fetchFn: FetchFunction | undefined, copilotApiService.ts
571 > @ILogService private readonly _logService: ILogService,
572 > @IProductService private readonly _productService: IProductService,
573 > @IAgentHostGitHubEndpointService private readonly _gitHubEndpointService: IAgentHostGitHubEndpointService,
574 > ) {
575 > this._fetch = fetchFn ?? globalThis.fetch;
576 > }
578 > // #region Public API
579 >
580 > messages(
581 > githubToken: string,
582 > request: Anthropic.MessageCreateParamsStreaming,
583 > options?: ICopilotApiServiceRequestOptions,
584 > ): AsyncGenerator<Anthropic.MessageStreamEvent>;
585 > messages(
586 > githubToken: string,
587 > request: Anthropic.MessageCreateParamsNonStreaming,
588 > options?: ICopilotApiServiceRequestOptions,
589 > ): Promise<Anthropic.Message>;
590 > messages(
591 githubToken: string,
592 request: Anthropic.MessageCreateParams,
598 return this._messagesNonStreaming(githubToken, request, options);
599 }
601 > async countTokens(
602 _githubToken: string,
603 _req: Anthropic.MessageCountTokensParams,
606 throw new Error('countTokens not supported by CAPI');
607 }
609 > async models(githubToken: string, options?: ICopilotApiServiceRequestOptions): Promise<CCAModel[]> {
610 const capiClient = await this._getClientForToken(githubToken);
611
638 return json.data ?? [];
639 }
641 > async responses(
642 githubToken: string,
643 body: string,
686 return response;
687 }
689 > async utilityChatCompletion(
690 githubToken: string,
691 request: ICopilotUtilityChatCompletionRequest,
738 return content;
739 }
741 > // #endregion
742 >
743 > // #region Lazy Init
744 >
745 > private _getCapiBase(): Promise<ICapiBase> {
746 if (!this._capiBasePromise) {
747 this._capiBasePromise = this._buildCapiBase().catch(err => {
752 return this._capiBasePromise;
753 }
755 > private async _buildCapiBase(): Promise<ICapiBase> {
756 const [machineId, deviceId] = await Promise.all([
757 getMachineId(err => this._logService.warn('[CopilotApiService] getMachineId failed', err)),
779 return { extensionInfo, userUrl };
780 }
782 > // #endregion
783 >
784 > // #region Streaming
785 >
786 > private async *_messagesStreaming(
787 githubToken: string,
788 request: Anthropic.MessageCreateParams,
797 yield* this._readSSE(response.body);
798 }
800 > // #endregion
801 >
802 > // #region Non-Streaming
803 >
804 > private async _messagesNonStreaming(
805 githubToken: string,
806 request: Anthropic.MessageCreateParams,
810 return response.json() as Promise<Anthropic.Message>;
811 }
813 > // #endregion
814 >
815 > // #region Shared Request
816 >
817 > private async _sendRequest(
818 githubToken: string,
819 request: Anthropic.MessageCreateParams,
872 return response;
873 }
875 > // #endregion
876 >
877 > // #region Per-Token Client
878 >
879 > /**
880 > * Resolve a {@link CAPIClient} that has had its domains updated for the
881 > * supplied user. Concurrent callers for the same token share one
882 > * `/copilot_internal/user` discovery via the cache map; callers with
883 > * different tokens get their **own** `CAPIClient` instance, so the
884 > * `updateDomains` mutation for token A can never affect a request being
885 > * dispatched for token B.
886 > */
887 > private _getClientForToken(githubToken: string): Promise<CAPIClient> {
888 return this._getEntryForToken(githubToken).then(entry => entry.capiClient);
889 }
891 > /**
892 > * Resolve this user's restricted-telemetry context. Reads the `rt`/`tid` claims from the minted
893 > * CAPI Copilot session token (the GitHub token has neither), and resolves the CAPI
894 > * `endpoints.telemetry` host from the cached `/copilot_internal/user` discovery only when the
895 > * user is opted in, so public users pay no extra discovery call.
896 > */
897 > async resolveRestrictedTelemetryContext(githubToken: string): Promise<IRestrictedTelemetryContext> {
898 const token = await this._getCopilotTokenEntry(githubToken);
899 const client = await this._getEntryForToken(githubToken);
914 };
915 }
917 > async resolveApiEndpoint(githubToken: string): Promise<string | undefined> {
918 return (await this._getEntryForToken(githubToken)).apiEndpoint;
919 }
921 > async resolveUserLogin(githubToken: string): Promise<string | undefined> {
922 return (await this._getEntryForToken(githubToken)).login;
923 }
925 > private _getEntryForToken(githubToken: string): Promise<ICachedClient> {
926 const nowSeconds = Date.now() / 1000;
927 const existing = this._clientsByToken.get(githubToken);
951 return pending;
952 }
954 > private _invalidateClientForToken(githubToken: string): void {
955 this._clientsByToken.delete(githubToken);
956 }
958 > private async _buildClientForToken(githubToken: string): Promise<ICachedClient> {
959 const { extensionInfo, userUrl } = await this._getCapiBase();
960 const fetch = this._fetch;
1024 };
1025 }
1027 > // #endregion
1028 >
1029 > // #region Per-Token Copilot Session Token
1030 >
1031 > /**
1032 > * Resolve the Copilot session token for a GitHub token, minting and
1033 > * caching one if needed. Concurrent callers for the same GitHub token
1034 > * share a single in-flight mint; the caller's `AbortSignal` is
1035 > * deliberately NOT forwarded so cancelling one caller does not poison
1036 > * the shared mint for the others.
1037 > */
1038 > private _getCopilotToken(githubToken: string): Promise<string> {
1039 return this._getCopilotTokenEntry(githubToken).then(entry => entry.token);
1040 }
1042 > private _getCopilotTokenEntry(githubToken: string): Promise<ICachedCopilotToken> {
1043 const nowSeconds = Date.now() / 1000;
1044 const existing = this._copilotTokensByGithub.get(githubToken);
1073 return pending;
1074 }
1076 > private _invalidateCopilotTokenForGithub(githubToken: string): void {
1077 this._copilotTokensByGithub.delete(githubToken);
1078 }
1080 > private async _buildCopilotToken(githubToken: string): Promise<ICachedCopilotToken> {
1081 const capiClient = await this._getClientForToken(githubToken);
1082
1128 };
1129 }
1131 > /**
1132 > * Resolve the concrete CAPI model id for the supplied family (e.g.
1133 > * `gpt-4o-mini`). Cached per GitHub token + family alongside the
1134 > * Copilot session token so eviction on 401/403 also clears the cached
1135 > * model id.
1136 > */
1137 > private async _resolveUtilityModelId(githubToken: string, modelFamily: string): Promise<string> {
1138 const pendingEntry = this._copilotTokensByGithub.get(githubToken);
1139 const entry = pendingEntry ? await pendingEntry : undefined;
1152 return match.id;
1153 }
1155 > // #endregion
1156 >
1157 > // #region SSE Parsing
1158 >
1159 > private async *_readSSE(body: ReadableStream<Uint8Array>): AsyncGenerator<Anthropic.MessageStreamEvent> {
1160 const reader = body.getReader();
1161 const decoder = new TextDecoder();
1206 }
1207 }
1209 > /**
1210 > * @returns the parsed stream event, or `undefined` to skip the line.
1211 > * @throws on `error` events from the server.
1212 > */
1213 > private _parseDataLine(line: string): Anthropic.MessageStreamEvent | undefined {
1214 if (!line.startsWith('data: ')) {
1215 return undefined;
1273 return parsed as Anthropic.MessageStreamEvent;
1274 }
1276 > // #endregion
1277 > }
1278 >
1279 > const KNOWN_SSE_EVENT_TYPES = new Set([
1280 > 'message_start', 'message_delta', 'message_stop',
1281 > 'content_block_start', 'content_block_delta', 'content_block_stop',
1282 > ]);
src/vs/base/common/lifecycle.ts 602 covered LOC · 145 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- lifecycle.ts
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 > import { compareBy, numberComparator } from './arrays.js';
7 > import { groupBy } from './collections.js';
8 > import { SetMap, ResourceMap } from './map.js';
9 > import { URI } from './uri.js';
10 > import { createSingleCallFunction } from './functional.js';
11 > import { Iterable } from './iterator.js';
12 > import { BugIndicatingError, onUnexpectedError } from './errors.js';
13 >
14 > // #region Disposable Tracking
15 >
16 > /**
17 > * Enables logging of potentially leaked disposables.
18 > *
19 > * A disposable is considered leaked if it is not disposed or not registered as the child of
20 > * another disposable. This tracking is very simple an only works for classes that either
21 > * extend Disposable or use a DisposableStore. This means there are a lot of false positives.
22 > */
23 > const TRACK_DISPOSABLES = false;
24 > let disposableTracker: IDisposableTracker | null = null;
25 >
26 > export interface IDisposableTracker {
27 > /**
28 > * Is called on construction of a disposable.
29 > */
30 > trackDisposable(disposable: IDisposable): void;
31 >
32 > /**
33 > * Is called when a disposable is registered as child of another disposable (e.g. {@link DisposableStore}).
34 > * If parent is `null`, the disposable is removed from its former parent.
35 > */
36 > setParent(child: IDisposable, parent: IDisposable | null): void;
37 >
38 > /**
39 > * Is called after a disposable is disposed.
40 > */
41 > markAsDisposed(disposable: IDisposable): void;
42 >
43 > /**
44 > * Indicates that the given object is a singleton which does not need to be disposed.
45 > */
46 > markAsSingleton(disposable: IDisposable): void;
47 > }
48 >
49 > export class GCBasedDisposableTracker implements IDisposableTracker {
50
51 private readonly _registry = new FinalizationRegistry<string>(heldValue => {
52 console.warn(`[LEAKED DISPOSABLE] ${heldValue}`);
53 });
55 > trackDisposable(disposable: IDisposable): void {
56 const stack = new Error('CREATED via:').stack!;
57 this._registry.register(disposable, stack, disposable);
58 }
60 > setParent(child: IDisposable, parent: IDisposable | null): void {
61 if (parent) {
62 this._registry.unregister(child);
65 }
66 }
68 > markAsDisposed(disposable: IDisposable): void {
69 this._registry.unregister(disposable);
70 }
72 > markAsSingleton(disposable: IDisposable): void {
73 this._registry.unregister(disposable);
74 }
75 > } lifecycle.ts
76 >
77 > export interface DisposableInfo {
78 > value: IDisposable;
79 > source: string | null;
80 > parent: IDisposable | null;
81 > isSingleton: boolean;
82 > idx: number;
83 > }
84 >
85 > export class DisposableTracker implements IDisposableTracker {
86 > private static idx = 0; lifecycle.ts
87 >
88 > private readonly livingDisposables = new Map<IDisposable, DisposableInfo>();
90 > private getDisposableData(d: IDisposable): DisposableInfo {
91 > let val = this.livingDisposables.get(d); lifecycle.ts
92 > if (!val) {
93 > val = { parent: null, source: null, isSingleton: false, value: d, idx: DisposableTracker.idx++ };
94 > this.livingDisposables.set(d, val);
95 > }
96 > return val;
97 > }
99 > trackDisposable(d: IDisposable): void {
100 > const data = this.getDisposableData(d); lifecycle.ts
101 > if (!data.source) {
102 > data.source =
103 > new Error().stack!;
104 > }
105 > }
106 > lifecycle.ts
107 > setParent(child: IDisposable, parent: IDisposable | null): void {
108 > const data = this.getDisposableData(child); lifecycle.ts
109 > data.parent = parent;
110 > }
111 > lifecycle.ts
112 > markAsDisposed(x: IDisposable): void {
113 > this.livingDisposables.delete(x); lifecycle.ts
114 > }
115 > lifecycle.ts
116 > markAsSingleton(disposable: IDisposable): void {
117 this.getDisposableData(disposable).isSingleton = true;
118 }
119 > lifecycle.ts
120 > private getRootParent(data: DisposableInfo, cache: Map<DisposableInfo, DisposableInfo>): DisposableInfo {
121 const cacheValue = cache.get(data);
122 if (cacheValue) {
128 return result;
129 }
130 > lifecycle.ts
131 > getTrackedDisposables(): IDisposable[] {
132 const rootParentCache = new Map<DisposableInfo, DisposableInfo>();
133
138 return leaking;
139 }
140 > lifecycle.ts
141 > computeLeakingDisposables(maxReported = 10, preComputedLeaks?: DisposableInfo[]): { leaks: DisposableInfo[]; details: string } | undefined {
142 > let uncoveredLeakingObjs: DisposableInfo[] | undefined; lifecycle.ts
143 > if (preComputedLeaks) {
144 uncoveredLeakingObjs = preComputedLeaks;
145 > } else { lifecycle.ts
146 > const rootParentCache = new Map<DisposableInfo, DisposableInfo>();
147 >
148 > const leakingObjects = [...this.livingDisposables.values()]
149 > .filter((info) => info.source !== null && !this.getRootParent(info, rootParentCache).isSingleton);
150 >
151 > if (leakingObjects.length === 0) {
152 > return; lifecycle.ts
153 > }
154 const leakingObjsSet = new Set(leakingObjects.map(o => o.value));
155
162 throw new Error('There are cyclic diposable chains!');
163 }
164 > } lifecycle.ts
165
166 if (!uncoveredLeakingObjs) {
224
225 return { leaks: uncoveredLeakingObjs, details: message };
226 > } lifecycle.ts
227 > } lifecycle.ts
228 >
229 > export function setDisposableTracker(tracker: IDisposableTracker | null): void {
230 > disposableTracker = tracker; lifecycle.ts
231 > }
232 > lifecycle.ts
233 > if (TRACK_DISPOSABLES) {
234 const __is_disposable_tracked__ = '__is_disposable_tracked__';
235 setDisposableTracker(new class implements IDisposableTracker {
268 });
269 }
270 > lifecycle.ts
271 > export function trackDisposable<T extends IDisposable>(x: T): T {
272 > disposableTracker?.trackDisposable(x); lifecycle.ts
273 > return x;
274 > }
275 > lifecycle.ts
276 > export function markAsDisposed(disposable: IDisposable): void {
277 > disposableTracker?.markAsDisposed(disposable); lifecycle.ts
278 > }
279 > lifecycle.ts
280 > function setParentOfDisposable(child: IDisposable, parent: IDisposable | null): void { lifecycle.ts
281 > disposableTracker?.setParent(child, parent);
282 > }
283 > lifecycle.ts
284 function setParentOfDisposables(children: IDisposable[], parent: IDisposable | null): void {
285 if (!disposableTracker) {
290 }
291 }
292 > lifecycle.ts
293 > /**
294 > * Indicates that the given object is a singleton which does not need to be disposed.
295 > */
296 > export function markAsSingleton<T extends IDisposable>(singleton: T): T {
297 disposableTracker?.markAsSingleton(singleton);
298 return singleton;
299 }
300 > lifecycle.ts
301 > // #endregion
302 >
303 > /**
304 > * An object that performs a cleanup operation when `.dispose()` is called.
305 > *
306 > * Some examples of how disposables are used:
307 > *
308 > * - An event listener that removes itself when `.dispose()` is called.
309 > * - A resource such as a file system watcher that cleans up the resource when `.dispose()` is called.
310 > * - The return value from registering a provider. When `.dispose()` is called, the provider is unregistered.
311 > */
312 > export interface IDisposable {
313 > dispose(): void;
314 > }
315 >
316 > /**
317 > * Check if `thing` is {@link IDisposable disposable}.
318 > */
319 > export function isDisposable<E>(thing: E): thing is E & IDisposable {
320 // eslint-disable-next-line local/code-no-any-casts
321 return typeof thing === 'object' && thing !== null && typeof (<IDisposable><any>thing).dispose === 'function' && (<IDisposable><any>thing).dispose.length === 0;
322 }
323 > lifecycle.ts
324 > /**
325 > * Disposes of the value(s) passed in.
326 > */
327 > export function dispose<T extends IDisposable>(disposable: T): T;
328 > export function dispose<T extends IDisposable>(disposable: T | undefined): T | undefined;
329 > export function dispose<T extends IDisposable, A extends Iterable<T> = Iterable<T>>(disposables: A): A;
330 > export function dispose<T extends IDisposable>(disposables: Array<T>): Array<T>;
331 > export function dispose<T extends IDisposable>(disposables: ReadonlyArray<T>): ReadonlyArray<T>;
332 > export function dispose<T extends IDisposable>(arg: T | Iterable<T> | undefined): any {
333 > if (Iterable.is(arg)) { lifecycle.ts
334 > const errors: any[] = []; lifecycle.ts
335 >
336 > for (const d of arg) {
337 > if (d) { lifecycle.ts
338 > try {
339 > d.dispose();
340 > } catch (e) {
341 errors.push(e);
342 }
343 > } lifecycle.ts
344 > }
345 > lifecycle.ts
346 > if (errors.length === 1) {
347 throw errors[0];
348 > } else if (errors.length > 1) { lifecycle.ts
349 throw new AggregateError(errors, 'Encountered errors while disposing of store');
350 }
351 > lifecycle.ts
352 > return Array.isArray(arg) ? [] : arg; lifecycle.ts
353 > } else if (arg) { lifecycle.ts
354 > arg.dispose(); lifecycle.ts
355 > return arg;
356 > }
357 > } lifecycle.ts
358 > lifecycle.ts
359 > export function disposeIfDisposable<T extends IDisposable | object>(disposables: Array<T>): Array<T> {
360 for (const d of disposables) {
361 if (isDisposable(d)) {
365 return [];
366 }
367 > lifecycle.ts
368 > /**
369 > * Combine multiple disposable values into a single {@link IDisposable}.
370 > */
371 > export function combinedDisposable(...disposables: IDisposable[]): IDisposable {
372 const parent = toDisposable(() => dispose(disposables));
373 setParentOfDisposables(disposables, parent);
374 return parent;
375 }
376 > lifecycle.ts
377 > class FunctionDisposable implements IDisposable {
378 > private _isDisposed: boolean;
379 > private readonly _fn: () => void;
380 >
381 > constructor(fn: () => void) {
382 > this._isDisposed = false; lifecycle.ts
383 > this._fn = fn;
384 > trackDisposable(this);
385 > }
386 > lifecycle.ts
387 > dispose() {
388 > if (this._isDisposed) { lifecycle.ts
389 > return; lifecycle.ts
390 > }
391 > if (!this._fn) { lifecycle.ts
392 throw new Error(`Unbound disposable context: Need to use an arrow function to preserve the value of this`);
393 }
394 > this._isDisposed = true; lifecycle.ts
395 > markAsDisposed(this);
396 > this._fn();
397 > }
398 > } lifecycle.ts
399 >
400 > /**
401 > * Turn a function that implements dispose into an {@link IDisposable}.
402 > *
403 > * @param fn Clean up function, guaranteed to be called only **once**.
404 > */
405 > export function toDisposable(fn: () => void): IDisposable {
406 > return new FunctionDisposable(fn); lifecycle.ts
407 > }
408 > lifecycle.ts
409 > /**
410 > * Manages a collection of disposable values.
411 > *
412 > * This is the preferred way to manage multiple disposables. A `DisposableStore` is safer to work with than an
413 > * `IDisposable[]` as it considers edge cases, such as registering the same value multiple times or adding an item to a
414 > * store that has already been disposed of.
415 > */
416 > export class DisposableStore implements IDisposable {
417 >
418 > static DISABLE_DISPOSED_WARNING = false;
419 >
420 > private readonly _toDispose = new Set<IDisposable>();
421 > private _isDisposed = false;
422 >
423 > constructor() {
424 > trackDisposable(this); lifecycle.ts
425 > }
426 > lifecycle.ts
427 > /**
428 > * Dispose of all registered disposables and mark this object as disposed.
429 > *
430 > * Any future disposables added to this object will be disposed of on `add`.
431 > */
432 > public dispose(): void {
433 > if (this._isDisposed) { lifecycle.ts
434 return;
435 }
436 > lifecycle.ts
437 > markAsDisposed(this);
438 > this._isDisposed = true;
439 > this.clear();
440 > }
441 > lifecycle.ts
442 > /**
443 > * @return `true` if this object has been disposed of.
444 > */
445 > public get isDisposed(): boolean {
446 > return this._isDisposed; lifecycle.ts
447 > }
448 > lifecycle.ts
449 > /**
450 > * Dispose of all registered disposables but do not mark this object as disposed.
451 > */
452 > public clear(): void {
453 > if (this._toDispose.size === 0) { lifecycle.ts
454 > return; lifecycle.ts
455 > }
456 > lifecycle.ts
457 > try {
458 > dispose(this._toDispose);
459 > } finally {
460 > this._toDispose.clear();
461 > }
462 > } lifecycle.ts
463 > lifecycle.ts
464 > /**
465 > * Add a new {@link IDisposable disposable} to the collection.
466 > */
467 > public add<T extends IDisposable>(o: T): T {
468 > if (!o || o === Disposable.None) { lifecycle.ts
469 > return o; lifecycle.ts
470 > }
471 > if ((o as unknown as DisposableStore) === this) { lifecycle.ts
472 throw new Error('Cannot register a disposable on itself!');
473 }
474 > lifecycle.ts
475 > setParentOfDisposable(o, this);
476 > if (this._isDisposed) {
477 if (!DisposableStore.DISABLE_DISPOSED_WARNING) {
478 console.warn(new Error('Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!').stack);
479 }
480 > } else { lifecycle.ts
481 > this._toDispose.add(o);
482 > }
483 >
484 > return o;
485 > } lifecycle.ts
486 > lifecycle.ts
487 > /**
488 > * Deletes a disposable from store and disposes of it. This will not throw or warn and proceed to dispose the
489 > * disposable even when the disposable is not part in the store.
490 > */
491 > public delete<T extends IDisposable>(o: T): void {
492 if (!o) {
493 return;
499 o.dispose();
500 }
501 > lifecycle.ts
502 > /**
503 > * Deletes the value from the store, but does not dispose it.
504 > */
505 > public deleteAndLeak<T extends IDisposable>(o: T): void {
506 if (!o) {
507 return;
511 }
512 }
513 > lifecycle.ts
514 > public assertNotDisposed(): void {
515 if (this._isDisposed) {
516 onUnexpectedError(new BugIndicatingError('Object disposed'));
517 }
518 }
519 > } lifecycle.ts
520 >
521 > /**
522 > * Abstract base class for a {@link IDisposable disposable} object.
523 > *
524 > * Subclasses can {@linkcode _register} disposables that will be automatically cleaned up when this object is disposed of.
525 > */
526 > export abstract class Disposable implements IDisposable {
527 >
528 > /**
529 > * A disposable that does nothing when it is disposed of.
530 > *
531 > * TODO: This should not be a static property.
532 > */
533 > static readonly None = Object.freeze<IDisposable>({ dispose() { } });
534 >
535 > protected readonly _store = new DisposableStore();
536 >
537 > constructor() {
538 > trackDisposable(this); lifecycle.ts
539 > setParentOfDisposable(this._store, this);
540 > }
541 > lifecycle.ts
542 > public dispose(): void {
543 > markAsDisposed(this); lifecycle.ts
544 >
545 > this._store.dispose();
546 > }
547 > lifecycle.ts
548 > /**
549 > * Adds `o` to the collection of disposables managed by this object.
550 > */
551 > protected _register<T extends IDisposable>(o: T): T {
552 > if ((o as unknown as Disposable) === this) { lifecycle.ts
553 throw new Error('Cannot register a disposable on itself!');
554 }
555 > return this._store.add(o); lifecycle.ts
556 > }
557 > } lifecycle.ts
558 >
559 > /**
560 > * Manages the lifecycle of a disposable value that may be changed.
561 > *
562 > * This ensures that when the disposable value is changed, the previously held disposable is disposed of. You can
563 > * also register a `MutableDisposable` on a `Disposable` to ensure it is automatically cleaned up.
564 > */
565 > export class MutableDisposable<T extends IDisposable> implements IDisposable {
566 > private _value?: T;
567 > private _isDisposed = false;
568 >
569 > constructor() {
570 trackDisposable(this);
571 }
572 > lifecycle.ts
573 > /**
574 > * Get the currently held disposable value, or `undefined` if this MutableDisposable has been disposed
575 > */
576 > get value(): T | undefined {
577 return this._isDisposed ? undefined : this._value;
578 }
579 > lifecycle.ts
580 > /**
581 > * Set a new disposable value.
582 > *
583 > * Behaviour:
584 > * - If the MutableDisposable has been disposed, the setter is a no-op.
585 > * - If the new value is strictly equal to the current value, the setter is a no-op.
586 > * - Otherwise the previous value (if any) is disposed and the new value is stored.
587 > *
588 > * Related helpers:
589 > * - clear() resets the value to `undefined` (and disposes the previous value).
590 > * - clearAndLeak() returns the old value without disposing it and removes its parent.
591 > */
592 > set value(value: T | undefined) {
593 if (this._isDisposed || value === this._value) {
594 return;
601 this._value = value;
602 }
603 > lifecycle.ts
604 > /**
605 > * Resets the stored value and disposed of the previously stored value.
606 > */
607 > clear(): void {
608 this.value = undefined;
609 }
610 > lifecycle.ts
611 > dispose(): void {
612 this._isDisposed = true;
613 markAsDisposed(this);
615 this._value = undefined;
616 }
617 > lifecycle.ts
618 > /**
619 > * Clears the value, but does not dispose it.
620 > * The old value is returned.
621 > */
622 > clearAndLeak(): T | undefined {
623 const oldValue = this._value;
624 this._value = undefined;
628 return oldValue;
629 }
630 > } lifecycle.ts
631 >
632 > /**
633 > * Manages the lifecycle of a disposable value that may be changed like {@link MutableDisposable}, but the value must
634 > * exist and cannot be undefined.
635 > */
636 > export class MandatoryMutableDisposable<T extends IDisposable> implements IDisposable {
637 > private readonly _disposable = new MutableDisposable<T>();
638 > private _isDisposed = false;
639 >
640 > constructor(initialValue: T) {
641 this._disposable.value = initialValue;
642 }
643 > lifecycle.ts
644 > get value(): T {
645 return this._disposable.value!;
646 }
647 > lifecycle.ts
648 > set value(value: T) {
649 if (this._isDisposed || value === this._disposable.value) {
650 return;
652 this._disposable.value = value;
653 }
654 > lifecycle.ts
655 > dispose() {
656 this._isDisposed = true;
657 this._disposable.dispose();
658 }
659 > } lifecycle.ts
660 >
661 > export class RefCountedDisposable {
662 >
663 > private _counter: number = 1;
664 >
665 > constructor(
666 private readonly _disposable: IDisposable,
667 ) { }
668 > lifecycle.ts
669 > acquire() {
670 this._counter++;
671 return this;
672 }
673 > lifecycle.ts
674 > release() {
675 if (--this._counter === 0) {
676 this._disposable.dispose();
678 return this;
679 }
680 > } lifecycle.ts
681 >
682 > export interface IReference<T> extends IDisposable {
683 > readonly object: T;
684 > }
685 >
686 > export abstract class ReferenceCollection<T> {
687 > lifecycle.ts
688 > private readonly references: Map<string, { readonly object: T; counter: number }> = new Map();
689 > lifecycle.ts
690 > acquire(key: string, ...args: unknown[]): IReference<T> {
691 let reference = this.references.get(key);
692
708 return { object, dispose };
709 }
710 > lifecycle.ts
711 > protected abstract createReferencedObject(key: string, ...args: unknown[]): T;
712 > protected abstract destroyReferencedObject(key: string, object: T): void;
713 > }
714 >
715 > /**
716 > * Unwraps a reference collection of promised values. Makes sure
717 > * references are disposed whenever promises get rejected.
718 > */
719 > export class AsyncReferenceCollection<T> {
720 >
721 > constructor(private referenceCollection: ReferenceCollection<Promise<T>>) { }
722 >
723 > async acquire(key: string, ...args: unknown[]): Promise<IReference<T>> {
724 const ref = this.referenceCollection.acquire(key, ...args);
725
736 }
737 }
738 > } lifecycle.ts
739 >
740 > export class ImmortalReference<T> implements IReference<T> {
741 > constructor(public object: T) { }
742 > dispose(): void { /* noop */ }
743 > }
744 >
745 > export function disposeOnReturn(fn: (store: DisposableStore) => void): void {
746 const store = new DisposableStore();
747 try {
751 }
752 }
753 > lifecycle.ts
754 > /**
755 > * A map the manages the lifecycle of the values that it stores.
756 > */
757 > export class DisposableMap<K, V extends IDisposable = IDisposable> implements IDisposable {
758 >
759 > private readonly _store: Map<K, V>;
760 > private _isDisposed = false;
761 >
762 > constructor(store: Map<K, V> = new Map<K, V>()) {
763 > this._store = store; lifecycle.ts
764 > trackDisposable(this);
765 > }
766 > lifecycle.ts
767 > /**
768 > * Disposes of all stored values and mark this object as disposed.
769 > *
770 > * Trying to use this object after it has been disposed of is an error.
771 > */
772 > dispose(): void {
773 > markAsDisposed(this); lifecycle.ts
774 > this._isDisposed = true;
775 > this.clearAndDisposeAll();
776 > }
777 > lifecycle.ts
778 > /**
779 > * Disposes of all stored values and clear the map, but DO NOT mark this object as disposed.
780 > */
781 > clearAndDisposeAll(): void {
782 > if (!this._store.size) { lifecycle.ts
783 > return; lifecycle.ts
784 > }
785 > lifecycle.ts
786 > try {
787 > dispose(this._store.values());
788 > } finally {
789 > this._store.clear();
790 > }
791 > } lifecycle.ts
792 > lifecycle.ts
793 > has(key: K): boolean {
794 > return this._store.has(key); lifecycle.ts
795 > }
796 > lifecycle.ts
797 > get size(): number {
798 > return this._store.size; lifecycle.ts
799 > }
800 > lifecycle.ts
801 > get(key: K): V | undefined {
802 return this._store.get(key);
803 }
804 > lifecycle.ts
805 > set(key: K, value: V, skipDisposeOnOverwrite = false): void {
806 > if (this._isDisposed) { lifecycle.ts
807 console.warn(new Error('Trying to add a disposable to a DisposableMap that has already been disposed of. The added object will be leaked!').stack);
808 }
809 > lifecycle.ts
810 > if (!skipDisposeOnOverwrite) {
811 > this._store.get(key)?.dispose();
812 > }
813 >
814 > this._store.set(key, value);
815 > setParentOfDisposable(value, this);
816 > }
817 > lifecycle.ts
818 > /**
819 > * Delete the value stored for `key` from this map and also dispose of it.
820 > */
821 > deleteAndDispose(key: K): void {
822 > this._store.get(key)?.dispose(); lifecycle.ts
823 > this._store.delete(key);
824 > }
825 > lifecycle.ts
826 > /**
827 > * Delete the value stored for `key` from this map but return it. The caller is
828 > * responsible for disposing of the value.
829 > */
830 > deleteAndLeak(key: K): V | undefined {
831 const value = this._store.get(key);
832 if (value) {
836 return value;
837 }
838 > lifecycle.ts
839 > keys(): IterableIterator<K> {
840 > return this._store.keys(); lifecycle.ts
841 > }
842 > lifecycle.ts
843 > values(): IterableIterator<V> {
844 return this._store.values();
845 }
846 > lifecycle.ts
847 > [Symbol.iterator](): IterableIterator<[K, V]> {
848 return this._store[Symbol.iterator]();
849 }
850 > } lifecycle.ts
851 >
852 > /**
853 > * A set that manages the lifecycle of the values that it stores.
854 > */
855 > export class DisposableSet<V extends IDisposable = IDisposable> implements IDisposable {
856 >
857 > private readonly _store: Set<V>;
858 > private _isDisposed = false;
859 >
860 > constructor(store: Set<V> = new Set<V>()) {
861 this._store = store;
862 trackDisposable(this);
863 }
864 > lifecycle.ts
865 > /**
866 > * Disposes of all stored values and mark this object as disposed.
867 > *
868 > * Trying to use this object after it has been disposed of is an error.
869 > */
870 > dispose(): void {
871 markAsDisposed(this);
872 this._isDisposed = true;
873 this.clearAndDisposeAll();
874 }
875 > lifecycle.ts
876 > /**
877 > * Disposes of all stored values and clear the set, but DO NOT mark this object as disposed.
878 > */
879 > clearAndDisposeAll(): void {
880 if (!this._store.size) {
881 return;
888 }
889 }
890 > lifecycle.ts
891 > has(value: V): boolean {
892 return this._store.has(value);
893 }
894 > lifecycle.ts
895 > get size(): number {
896 return this._store.size;
897 }
898 > lifecycle.ts
899 > add(value: V): void {
900 if (this._isDisposed) {
901 console.warn(new Error('Trying to add a disposable to a DisposableSet that has already been disposed of. The added object will be leaked!').stack);
905 setParentOfDisposable(value, this);
906 }
907 > lifecycle.ts
908 > /**
909 > * Delete the value from this set and also dispose of it.
910 > */
911 > deleteAndDispose(value: V): void {
912 if (this._store.delete(value)) {
913 value.dispose();
914 }
915 }
916 > lifecycle.ts
917 > /**
918 > * Delete the value from this set but return it. The caller is
919 > * responsible for disposing of the value.
920 > */
921 > deleteAndLeak(value: V): V | undefined {
922 if (this._store.delete(value)) {
923 setParentOfDisposable(value, null);
926 return undefined;
927 }
928 > lifecycle.ts
929 > values(): IterableIterator<V> {
930 return this._store.values();
931 }
932 > lifecycle.ts
933 > [Symbol.iterator](): IterableIterator<V> {
934 return this._store[Symbol.iterator]();
935 }
936 > } lifecycle.ts
937 >
938 > /**
939 > * Call `then` on a Promise, unless the returned disposable is disposed.
940 > */
941 > export function thenIfNotDisposed<T>(promise: Promise<T>, then: (result: T) => void): IDisposable {
942 let disposed = false;
943 promise.then(result => {
951 });
952 }
953 > lifecycle.ts
954 > /**
955 > * Call `then` on a promise that resolves to a {@link IDisposable}, then either register the
956 > * disposable or register it to the {@link DisposableStore}, depending on whether the store is
957 > * disposed or not.
958 > */
959 > export function thenRegisterOrDispose<T extends IDisposable>(promise: Promise<T>, store: DisposableStore): Promise<T> {
960 return promise.then(disposable => {
961 if (store.isDisposed) {
967 });
968 }
969 > lifecycle.ts
970 > export class DisposableResourceMap<V extends IDisposable = IDisposable> extends DisposableMap<URI, V> {
971 > constructor() {
972 > super(new ResourceMap()); lifecycle.ts
973 > }
974 > } lifecycle.ts
src/vs/platform/agentHost/common/agentHostSchema.ts 583 covered LOC · 39 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostSchema.ts
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 > import { localize } from '../../../nls.js';
7 > import { structuralEquals } from '../../../base/common/equals.js';
8 > import { ConfigurationTarget, type IConfigurationService, type IConfigurationValue } from '../../configuration/common/configuration.js';
9 > import type { IMcpServerConfiguration } from '../../mcp/common/mcpPlatformTypes.js';
10 > import { TelemetryConfiguration, TelemetryLevel } from '../../telemetry/common/telemetry.js';
11 > import { SessionConfigKey } from './sessionConfigKeys.js';
12 > import type { SessionConfigPropertySchema, SessionConfigSchema } from './state/protocol/commands.js';
13 > import { JsonRpcErrorCodes, ProtocolError } from './state/sessionProtocol.js';
14 >
15 > // ---- Schema builder --------------------------------------------------------
16 >
17 > /**
18 > * A schema property with a phantom TypeScript type and a precomputed
19 > * runtime validator.
20 > *
21 > * The `<T>` type parameter is the developer's assertion about the
22 > * property's runtime shape; the validator derived from `protocol`
23 > * (`type`, `enum`, `items`, `properties`, `required`) enforces it at
24 > * runtime.
25 > */
26 > export interface ISchemaProperty<T> {
27 > readonly protocol: SessionConfigPropertySchema;
28 > /**
29 > * Returns `true` iff `value` conforms to {@link protocol}. Narrows
30 > * the type to `T` for callers. The boolean form is preferred for
31 > * control flow; use {@link assertValid} when you want a descriptive
32 > * error for the offending path.
33 > */
34 > validate(value: unknown): value is T;
35 > /**
36 > * Throws a {@link ProtocolError} with `JsonRpcErrorCodes.InvalidParams`
37 > * describing the offending path (e.g. `'permissions.allow[2]'`) when
38 > * `value` does not conform to {@link protocol}. Otherwise returns and
39 > * narrows the type to `T`.
40 > *
41 > * @param path Dotted path prefix to embed in error messages. Defaults
42 > * to empty (the value itself).
43 > */
44 > assertValid(value: unknown, path?: string): asserts value is T;
45 > }
46 >
47 > /**
48 > * Defines a strongly-typed schema property whose runtime validator is
49 > * derived from the supplied JSON-schema descriptor.
50 > */
51 > export function schemaProperty<T>(protocol: SessionConfigPropertySchema): ISchemaProperty<T> {
52 > const assertFn = buildAssert(protocol);
53 > const assertValid = (value: unknown, path: string = ''): asserts value is T => assertFn(value, path);
54 > const validate = (value: unknown): value is T => {
55 try {
56 assertFn(value, '');
60 }
61 };
62 > return { protocol, validate, assertValid }; agentHostSchema.ts
63 > }
64 >
65 > // eslint-disable-next-line @typescript-eslint/no-explicit-any
66 > export type SchemaDefinition = Record<string, ISchemaProperty<any>>;
67 >
68 > export type SchemaValue<P> = P extends ISchemaProperty<infer T> ? T : never;
69 >
70 > export type SchemaValues<D extends SchemaDefinition> = {
71 > [K in keyof D]?: SchemaValue<D[K]>;
72 > };
73 >
74 > /**
75 > * A bundle of named schema properties plus helpers for serializing to the
76 > * protocol shape, validating a values bag at write sites, and validating
77 > * a single key at read sites.
78 > */
79 > export interface ISchema<D extends SchemaDefinition> {
80 > readonly definition: D;
81 > /** Returns the protocol-serializable schema for this bundle. */
82 > toProtocol(): SessionConfigSchema;
83 > /**
84 > * Validates each known key in `values` against its schema and returns
85 > * a new plain record. Throws a {@link ProtocolError} with a path like
86 > * `'permissions.allow[2]'` when any supplied value fails validation.
87 > * Unknown keys are passed through untouched for forward-compatibility.
88 > */
89 > values(values: SchemaValues<D>): Record<string, unknown>;
90 > /**
91 > * Returns `true` iff `value` validates against the schema for `key`.
92 > * Unknown keys return `false`.
93 > */
94 > validate<K extends keyof D & string>(key: K, value: unknown): value is SchemaValue<D[K]>;
95 > /**
96 > * Throws a {@link ProtocolError} describing the offending path when
97 > * `value` does not validate against the schema for `key`, or when
98 > * `key` is not defined in the schema.
99 > */
100 > assertValid<K extends keyof D & string>(key: K, value: unknown): asserts value is SchemaValue<D[K]>;
101 > /**
102 > * Returns a fully-typed values bag by validating each key of the
103 > * schema against `values` and falling back to the default when
104 > * the incoming value is missing or fails validation.
105 > *
106 > * Semantics: for every key declared in the schema `definition`:
107 > * - if `values[key]` validates, it is kept;
108 > * - else if `key` is present in `defaults`, the default is used;
109 > * - else the key is omitted from the result.
110 > *
111 > * This means callers MAY supply defaults for only a subset of the
112 > * schema — keys not present in `defaults` are simply left unset
113 > * when the incoming value is missing or invalid. This is useful
114 > * when some properties (e.g. per-session `permissions`) should be
115 > * inherited from a higher scope rather than materialized on every
116 > * new session.
117 > *
118 > * Intended for sanitizing untrusted input at protocol boundaries
119 > * (e.g. `resolveSessionConfig`). Keys that fail validation are
120 > * silently replaced with their default or dropped; use
121 > * {@link values} or {@link assertValid} when you want a descriptive
122 > * {@link ProtocolError} instead.
123 > */
124 > validateOrDefault<T extends Partial<{ [K in keyof D]: SchemaValue<D[K]> }>>(values: { [K in keyof T]?: unknown } | undefined, defaults: T): T;
125 > }
126 >
127 > export function createSchema<D extends SchemaDefinition>(definition: D): ISchema<D> {
128 > return {
129 > definition,
130 > toProtocol(): SessionConfigSchema {
131 > const properties: Record<string, SessionConfigPropertySchema> = {}; agentHostSchema.ts
132 > for (const key of Object.keys(definition)) {
133 > properties[key] = definition[key].protocol;
134 > }
135 > return { type: 'object', properties };
136 > },
137 > values(values) { agentHostSchema.ts
138 const raw = values as Record<string, unknown>;
139 for (const key of Object.keys(definition)) {
149 return { ...raw };
150 },
151 > validate<K extends keyof D & string>(key: K, value: unknown): value is SchemaValue<D[K]> { agentHostSchema.ts
152 const prop = definition[key];
153 return prop ? prop.validate(value) : false;
154 },
155 > assertValid<K extends keyof D & string>(key: K, value: unknown): asserts value is SchemaValue<D[K]> { agentHostSchema.ts
156 const prop: ISchemaProperty<unknown> | undefined = definition[key];
157 if (!prop) {
163 narrowed.assertValid(value, key);
164 },
165 > validateOrDefault<T extends Partial<{ [K in keyof D]: SchemaValue<D[K]> }>>(values: { [K in keyof T]?: unknown } | undefined, defaults: T): T { agentHostSchema.ts
166 > const result: Record<string, unknown> = {}; agentHostSchema.ts
167 > const raw: { [K in keyof T]?: unknown } = values ?? {};
168 > for (const key of Object.keys(definition)) {
169 > const prop = definition[key];
170 > const candidate = raw[key];
171 > if (candidate !== undefined && prop.validate(candidate)) {
172 result[key] = candidate;
173 > } else if (Object.prototype.hasOwnProperty.call(defaults, key)) { agentHostSchema.ts
174 > result[key] = (defaults as Record<string, unknown>)[key]; agentHostSchema.ts
175 > }
176 > // else: key not in defaults and incoming value missing/invalid agentHostSchema.ts
177 > // → leave unset so higher-scope defaults can fill in.
178 > }
179 > return result as T;
180 > },
182 > }
183 >
184 > // ---- Validator derivation --------------------------------------------------
185 >
186 > /**
187 > * A validator that throws a {@link ProtocolError} annotated with the
188 > * offending path when `value` does not conform, or returns normally
189 > * when it does.
190 > */
191 > type AssertValidator = (value: unknown, path: string) => void;
192 >
193 > function buildAssert(schema: SessionConfigPropertySchema): AssertValidator {
194 > if (schema.type === 'object' && schema.properties) {
195 > const propAsserts: Record<string, AssertValidator> = {};
196 > for (const key of Object.keys(schema.properties)) {
197 > propAsserts[key] = buildAssert(schema.properties[key] as SessionConfigPropertySchema);
198 > }
199 > const required = new Set(schema.required ?? []);
200 > return (value, path) => {
201 if (typeof value !== 'object' || value === null || Array.isArray(value)) {
202 throw invalidParams(path, 'object', value);
214 }
215 };
217 > if (schema.type === 'array' && schema.items) {
218 > const itemAssert = buildAssert(schema.items as SessionConfigPropertySchema);
219 > return (value, path) => {
220 if (!Array.isArray(value)) {
221 throw invalidParams(path, 'array', value);
225 }
226 };
228 > return buildPrimitiveAssert(schema);
229 > }
230 >
231 > function buildPrimitiveAssert(schema: SessionConfigPropertySchema): AssertValidator {
232 > const enumDynamic = schema.enumDynamic === true;
233 > return (value, path) => {
234 switch (schema.type) {
235 case 'string': if (typeof value !== 'string') { throw invalidParams(path, 'string', value); } break;
243 }
244 };
246 >
247 function invalidParams(path: string, expected: string, value: unknown): ProtocolError {
248 return new ProtocolError(JsonRpcErrorCodes.InvalidParams, `Invalid value at '${path || '<root>'}': expected ${expected}, got ${safeStringify(value)}`);
249 }
251 function joinPath(parent: string, key: string): string {
252 return parent ? `${parent}.${key}` : key;
253 }
255 function safeStringify(value: unknown): string {
256 try {
260 }
261 }
263 > // ---- Platform-owned schema -------------------------------------------------
264 >
265 > export type AutoApproveLevel = 'default' | 'assisted' | 'autoApprove';
266 >
267 > export type SessionMode = 'interactive' | 'plan' | 'autopilot';
268 >
269 > export interface IPermissionsValue {
270 > readonly allow: readonly string[];
271 > readonly deny: readonly string[];
272 > }
273 >
274 > const permissionsProperty = schemaProperty<IPermissionsValue>({
275 > type: 'object',
276 > title: localize('agentHost.sessionConfig.permissions', "Permissions"),
277 > description: localize('agentHost.sessionConfig.permissionsDescription', "Per-tool session permissions. Updated automatically when approving a tool \"in this Session\"."),
278 > properties: {
279 > allow: {
280 > type: 'array',
281 > title: localize('agentHost.sessionConfig.permissions.allow', "Allowed tools"),
282 > items: {
283 > type: 'string',
284 > title: localize('agentHost.sessionConfig.permissions.toolName', "Tool name"),
285 > },
286 > },
287 > deny: {
288 > type: 'array',
289 > title: localize('agentHost.sessionConfig.permissions.deny', "Denied tools"),
290 > items: {
291 > type: 'string',
292 > title: localize('agentHost.sessionConfig.permissions.toolName', "Tool name"),
293 > },
294 > },
295 > },
296 > default: { allow: [], deny: [] },
297 > sessionMutable: true,
298 > });
299 >
300 > /**
301 > * Session-config properties owned by the platform itself — i.e. consumed
302 > * by the agent host rather than by any particular agent.
303 > *
304 > * Agents extend this schema by spreading `platformSessionSchema.definition`
305 > * into their own {@link createSchema} call together with any
306 > * provider-specific properties.
307 > */
308 > export const platformSessionSchema = createSchema({
309 > [SessionConfigKey.AutoApprove]: schemaProperty<AutoApproveLevel>({
310 > type: 'string',
311 > title: localize('agentHost.sessionConfig.autoApprove', "Approvals"),
312 > description: localize('agentHost.sessionConfig.autoApproveDescription', "Tool approval behavior for this session"),
313 > enum: ['default', 'assisted', 'autoApprove'],
314 > enumLabels: [
315 > localize('agentHost.sessionConfig.autoApprove.default', "Default approvals"),
316 > localize('agentHost.sessionConfig.autoApprove.assisted', "Assisted permissions"),
317 > localize('agentHost.sessionConfig.autoApprove.bypass', "Allow all"),
318 > ],
319 > enumDescriptions: [
320 > localize('agentHost.sessionConfig.autoApprove.defaultDescription', "Asks when approval settings don't apply"),
321 > localize('agentHost.sessionConfig.autoApprove.assistedDescription', "Evaluates risk before running tools"),
322 > localize('agentHost.sessionConfig.autoApprove.bypassDescription', "Runs tool calls without asking"),
323 > ],
324 > default: 'default',
325 > sessionMutable: true,
326 > }),
327 > [SessionConfigKey.Permissions]: permissionsProperty,
328 > [SessionConfigKey.Mode]: schemaProperty<SessionMode>({
329 > type: 'string',
330 > title: localize('agentHost.sessionConfig.mode', "Agent Mode"),
331 > description: localize('agentHost.sessionConfig.modeDescription', "How the agent should approach this turn"),
332 > enum: ['interactive', 'plan', 'autopilot'],
333 > enumLabels: [
334 > localize('agentHost.sessionConfig.mode.interactive', "Interactive"),
335 > localize('agentHost.sessionConfig.mode.plan', "Plan"),
336 > localize('agentHost.sessionConfig.mode.autopilot', "Autopilot"),
337 > ],
338 > enumDescriptions: [
339 > localize('agentHost.sessionConfig.mode.interactiveDescription', "Step-by-step collaboration"),
340 > localize('agentHost.sessionConfig.mode.planDescription', "Plan first, execute when ready"),
341 > localize('agentHost.sessionConfig.mode.autopilotDescription', "Autonomously iterates from start to finish"),
342 > ],
343 > default: 'interactive',
344 > sessionMutable: true,
345 > }),
346 > });
347 >
348 > /**
349 > * Rewrites a legacy `autoApprove='autopilot'` config value — used before
350 > * Autopilot moved from the `autoApprove` axis onto the orthogonal `mode`
351 > * axis — into the current two-axis shape:
352 > *
353 > * - `autoApprove='autopilot'` + `mode='plan'` → `mode='plan'`, `autoApprove='default'`
354 > * (legacy `plan` took precedence over autopilot when resolving the SDK mode).
355 > * - `autoApprove='autopilot'` + any other mode → `mode='autopilot'`, `autoApprove='default'`.
356 > *
357 > * Returns a shallow copy with the migration applied, or the original
358 > * reference unchanged when no legacy value is present. Safe to call on
359 > * `undefined`.
360 > *
361 > * Without this, a session persisted (or a "remembered" picker value seeded)
362 > * with `autoApprove='autopilot'` would fail the new schema's enum validation
363 > * and silently fall back to `default`, downgrading the session from
364 > * autonomous Autopilot to manual per-tool confirmation.
365 > */
366 > export function migrateLegacyAutopilotConfig<T extends Record<string, unknown> | undefined>(config: T): T {
367 if (!config || config[SessionConfigKey.AutoApprove] !== 'autopilot') {
368 return config;
375 return migrated as T;
376 }
378 > /**
379 > * Root (agent host) config properties owned by the platform itself.
380 > *
381 > * Root config acts as the baseline that applies to every session:
382 > *
383 > * - {@link SessionConfigKey.Permissions} — host-wide allow/deny lists
384 > * unioned with each session's own permissions when evaluating tool
385 > * auto-approval. See `SessionPermissionManager` for the evaluation
386 > * rules.
387 > */
388 > export const AgentHostTelemetryLevelConfigKey = 'telemetryLevel';
389 >
390 > /** Legacy Copilot Chat debug switch that disables `request.repoInfo` collection. */
391 > export const AgentHostDisableRepoInfoTelemetryConfigKey = 'disableRepoInfoTelemetry';
392 >
393 > /** VS Code setting forwarded into {@link AgentHostDisableRepoInfoTelemetryConfigKey}. */
394 > export const DISABLE_REPO_INFO_TELEMETRY_SETTING_ID = 'chat.advanced.debug.disableRepoInfoTelemetry';
395 >
396 > /**
397 > * Root config key forwarded from the renderer when VS Code's
398 > * `chat.sessionSync.enabled` setting changes. Controls the `remote` flag
399 > * passed to the copilot-sdk `CopilotClientOptions`.
400 > */
401 > export const AgentHostSessionSyncEnabledConfigKey = 'sessionSyncEnabled';
402 >
403 > /**
404 > * Root config key forwarded from the renderer carrying the experiment-aware
405 > * value of `chat.agentHost.codexAgent.enabled`. The host registers the Codex
406 > * provider when this is `true`; disabling requires an agent host restart.
407 > */
408 > export const AgentHostCodexEnabledConfigKey = 'codexAgentEnabled';
409 >
410 > /**
411 > * Root config key forwarded from the renderer when VS Code's
412 > * `chat.tools.terminal.enableAutoApprove` setting changes. Controls whether
413 > * agent-host shell permission checks may apply terminal auto-approve rules.
414 > */
415 > export const AgentHostTerminalAutoApproveEnabledConfigKey = 'terminalAutoApproveEnabled';
416 >
417 > /**
418 > * The VS Code setting ID for terminal auto approve enablement. Defined here so
419 > * renderer-side agent-host clients can forward it without importing from
420 > * workbench terminal contributions.
421 > */
422 > export const TERMINAL_AUTO_APPROVE_ENABLED_SETTING_ID = 'chat.tools.terminal.enableAutoApprove';
423 >
424 > /**
425 > * Root config key forwarded from the renderer when VS Code's
426 > * `chat.tools.global.autoApprove` setting changes. When `true`, the global
427 > * auto-approve ("approve everything") setting is enabled and the agent host
428 > * treats every tool call as auto-approved — equivalent to a session running
429 > * with Allow all.
430 > */
431 > export const AgentHostGlobalAutoApproveEnabledConfigKey = 'globalAutoApproveEnabled';
432 >
433 > /**
434 > * The VS Code setting ID for global auto approve. Defined here so renderer-side
435 > * agent-host clients can forward it without importing from `workbench/contrib/chat`.
436 > */
437 > export const GLOBAL_AUTO_APPROVE_SETTING_ID = 'chat.tools.global.autoApprove';
438 >
439 > /**
440 > * Root config key forwarded from the renderer when VS Code's `chat.autoReply`
441 > * setting changes. When `true`, the agent host auto-answers `ask_user`
442 > * questions instead of blocking on the user — the user is treated as
443 > * unavailable and the agent is told to use its best judgment, mirroring the
444 > * behavior of `autopilot` mode.
445 > */
446 > export const AgentHostAutoReplyEnabledConfigKey = 'autoReplyEnabled';
447 >
448 > /**
449 > * The VS Code setting ID for auto-reply. Defined here so renderer-side
450 > * agent-host clients can forward it without importing from `workbench/contrib/chat`.
451 > */
452 > export const AUTO_REPLY_SETTING_ID = 'chat.autoReply';
453 >
454 > // Root config key forwarded from the renderer when Copilot Chat's `github.copilot.chat.preferLongContext.enabled` setting changes.
455 > export const AgentHostPreferLongContextEnabledConfigKey = 'preferLongContextEnabled';
456 >
457 > // The Copilot Chat setting ID for preferring long context, forwarded into the agent host root config.
458 > export const PREFER_LONG_CONTEXT_SETTING_ID = 'github.copilot.chat.preferLongContext.enabled';
459 >
460 > /** Root config key forwarded from the renderer for automatic OS system proxy discovery. */
461 > export const AgentHostSystemProxyEnabledConfigKey = 'systemProxyEnabled';
462 >
463 > /**
464 > * Root config key forwarded from the renderer when VS Code's
465 > * `chat.tools.terminal.autoApprove` setting changes. Holds the effective
466 > * terminal auto-approve rule object for agent-host shell permission checks.
467 > */
468 > export const AgentHostTerminalAutoApproveRulesConfigKey = 'terminalAutoApproveRules';
469 >
470 > export interface IAgentHostTerminalAutoApproveRule {
471 > readonly approve: boolean;
472 > readonly matchCommandLine?: boolean;
473 > }
474 >
475 > export type AgentHostTerminalAutoApproveRuleValue = boolean | null | IAgentHostTerminalAutoApproveRule;
476 > export type AgentHostTerminalAutoApproveRules = Record<string, AgentHostTerminalAutoApproveRuleValue>;
477 >
478 > /**
479 > * The VS Code setting IDs for terminal auto approve rules. Defined here so
480 > * renderer-side agent-host clients can forward them without importing from
481 > * workbench terminal contributions.
482 > */
483 > export const TERMINAL_AUTO_APPROVE_SETTING_ID = 'chat.tools.terminal.autoApprove';
484 > export const TERMINAL_IGNORE_DEFAULT_AUTO_APPROVE_RULES_SETTING_ID = 'chat.tools.terminal.ignoreDefaultAutoApproveRules';
485 >
486 > export function getAgentHostTerminalAutoApproveRulesConfig(configurationService: IConfigurationService): AgentHostTerminalAutoApproveRules {
487 const config = configurationService.getValue<AgentHostTerminalAutoApproveRules | undefined>(TERMINAL_AUTO_APPROVE_SETTING_ID);
488 const configInspectValue = configurationService.inspect<Readonly<AgentHostTerminalAutoApproveRules>>(TERMINAL_AUTO_APPROVE_SETTING_ID);
490 return normalizeAgentHostTerminalAutoApproveRulesConfig(config, configInspectValue, ignoreDefaults);
491 }
493 > export function normalizeAgentHostTerminalAutoApproveRulesConfig(config: AgentHostTerminalAutoApproveRules | undefined, configInspectValue: IConfigurationValue<Readonly<AgentHostTerminalAutoApproveRules>>, ignoreDefaults: boolean): AgentHostTerminalAutoApproveRules {
494 if (!config) {
495 return {};
505 return rules;
506 }
508 function isDefaultOnlyAutoApproveRule(key: string, value: AgentHostTerminalAutoApproveRuleValue, configInspectValue: IConfigurationValue<Readonly<AgentHostTerminalAutoApproveRules>>): boolean {
509 const defaultValue = configInspectValue.default?.value;
517 return sourceTarget === ConfigurationTarget.DEFAULT;
518 }
520 function getAutoApproveRuleSourceTarget(key: string, value: AgentHostTerminalAutoApproveRuleValue, configInspectValue: IConfigurationValue<Readonly<AgentHostTerminalAutoApproveRules>>): ConfigurationTarget {
521 if (hasMatchingRule(configInspectValue.workspaceFolderValue, key, value)) {
539 return ConfigurationTarget.DEFAULT;
540 }
542 function hasMatchingRule(config: Readonly<AgentHostTerminalAutoApproveRules> | undefined, key: string, value: AgentHostTerminalAutoApproveRuleValue): boolean {
543 return !!config && Object.prototype.hasOwnProperty.call(config, key) && structuralEquals(config[key], value);
544 }
546 > /**
547 > * Root config key holding agent-host-level MCP server definitions.
548 > *
549 > * The value is a map of server name → {@link IMcpServerConfiguration}
550 > * (the same `servers` shape used by `mcp.json`). These servers are
551 > * exposed to every session created by the host, merged with any
552 > * plugin-provided MCP servers when launching the copilot-sdk client.
553 > */
554 > export const AgentHostMcpServersConfigKey = 'mcpServers';
555 >
556 > /**
557 > * Map of server name → MCP server configuration, as stored in the
558 > * {@link AgentHostMcpServersConfigKey} root config value.
559 > */
560 > export type AgentHostMcpServers = Record<string, IMcpServerConfiguration>;
561 >
562 > /**
563 > * The VS Code setting ID for session sync. Defined here so the platform
564 > * layer (renderer-side forwarding) can reference it without importing from
565 > * `workbench/contrib/chat`.
566 > */
567 > export const SESSION_SYNC_ENABLED_SETTING_ID = 'chat.sessionSync.enabled';
568 >
569 > export function telemetryLevelToAgentHostConfigValue(telemetryLevel: TelemetryLevel): TelemetryConfiguration {
570 > switch (telemetryLevel) { agentHostSchema.ts
571 > case TelemetryLevel.NONE:
572 return TelemetryConfiguration.OFF;
573 > case TelemetryLevel.CRASH: agentHostSchema.ts
574 return TelemetryConfiguration.CRASH;
575 > case TelemetryLevel.ERROR: agentHostSchema.ts
576 return TelemetryConfiguration.ERROR;
577 > case TelemetryLevel.USAGE: agentHostSchema.ts
578 > return TelemetryConfiguration.ON; agentHostSchema.ts
580 > }
582 > export function agentHostConfigValueToTelemetryLevel(value: unknown): TelemetryLevel | undefined {
583 > switch (value) { agentHostSchema.ts
584 > case TelemetryConfiguration.OFF:
585 return TelemetryLevel.NONE;
586 > case TelemetryConfiguration.CRASH: agentHostSchema.ts
587 return TelemetryLevel.CRASH;
588 > case TelemetryConfiguration.ERROR: agentHostSchema.ts
589 return TelemetryLevel.ERROR;
590 > case TelemetryConfiguration.ON: agentHostSchema.ts
591 > return TelemetryLevel.USAGE; agentHostSchema.ts
592 > default: agentHostSchema.ts
593 return undefined;
595 > }
597 > /**
598 > * Field descriptors for a single MCP server entry, shared by the stdio and
599 > * http shapes. The agent-host config schema has no `oneOf`, so both variants'
600 > * fields are described together; `type` selects which fields apply
601 > * (`stdio` uses `command`/`args`/`env`/`cwd`, `http` uses `url`/`headers`).
602 > */
603 > const mcpServerConfigProperties: Record<string, SessionConfigPropertySchema> = {
604 > type: {
605 > type: 'string',
606 > title: localize('agentHost.config.mcpServers.type.title', "Server Type"),
607 > description: localize('agentHost.config.mcpServers.type.description', "The transport used to reach the server: `stdio` for a local command, `http` for a remote endpoint."),
608 > enum: ['stdio', 'http'],
609 > },
610 > command: {
611 > type: 'string',
612 > title: localize('agentHost.config.mcpServers.command.title', "Command"),
613 > description: localize('agentHost.config.mcpServers.command.description', "For `stdio` servers, the executable to spawn."),
614 > },
615 > args: {
616 > type: 'array',
617 > title: localize('agentHost.config.mcpServers.args.title', "Arguments"),
618 > description: localize('agentHost.config.mcpServers.args.description', "For `stdio` servers, the arguments passed to the command."),
619 > items: { type: 'string', title: localize('agentHost.config.mcpServers.arg.title', "Argument") },
620 > },
621 > env: {
622 > type: 'object',
623 > title: localize('agentHost.config.mcpServers.env.title', "Environment"),
624 > description: localize('agentHost.config.mcpServers.env.description', "For `stdio` servers, environment variables set on the spawned process."),
625 > },
626 > cwd: {
627 > type: 'string',
628 > title: localize('agentHost.config.mcpServers.cwd.title', "Working Directory"),
629 > description: localize('agentHost.config.mcpServers.cwd.description', "For `stdio` servers, the working directory the command runs in."),
630 > },
631 > url: {
632 > type: 'string',
633 > title: localize('agentHost.config.mcpServers.url.title', "URL"),
634 > description: localize('agentHost.config.mcpServers.url.description', "For `http` servers, the endpoint URL of the MCP server."),
635 > },
636 > headers: {
637 > type: 'object',
638 > title: localize('agentHost.config.mcpServers.headers.title', "Headers"),
639 > description: localize('agentHost.config.mcpServers.headers.description', "For `http` servers, HTTP headers sent with every request."),
640 > },
641 > };
642 >
643 > /**
644 > * Documents the value shape of the {@link AgentHostMcpServersConfigKey} map.
645 > *
646 > * The config value is a map of server name → server config. The schema
647 > * language has no `additionalProperties`, so the per-entry shape is attached
648 > * under a placeholder key (`<serverName>`) rather than at the map level —
649 > * this keeps the field descriptions discoverable without the runtime
650 > * validator mistaking a real server named e.g. `command` for the `command`
651 > * field. Real entries (keyed by actual server names) are passed through.
652 > */
653 > const mcpServersValueProperties: Record<string, SessionConfigPropertySchema> = {
654 > '<serverName>': {
655 > type: 'object',
656 > title: localize('agentHost.config.mcpServers.entry.title', "MCP Server"),
657 > description: localize('agentHost.config.mcpServers.entry.description', "A single MCP server entry. The property key is the server name."),
658 > properties: mcpServerConfigProperties,
659 > },
660 > };
661 >
662 > export const platformRootSchema = createSchema({
663 > [SessionConfigKey.Permissions]: permissionsProperty,
664 > [AgentHostDisableRepoInfoTelemetryConfigKey]: schemaProperty<boolean>({
665 > type: 'boolean',
666 > title: localize('agentHost.config.disableRepoInfoTelemetry.title', "Disable Repository Information Telemetry"),
667 > description: localize('agentHost.config.disableRepoInfoTelemetry.description', "Whether repository information telemetry is disabled for Agent Host sessions."),
668 > default: false,
669 > }),
670 > [AgentHostTelemetryLevelConfigKey]: schemaProperty<TelemetryConfiguration>({
671 > type: 'string',
672 > title: localize('agentHost.config.telemetryLevel.title', "Telemetry Level"),
673 > description: localize('agentHost.config.telemetryLevel.description', "Most restrictive telemetry level requested by connected clients."),
674 > enum: [TelemetryConfiguration.ON, TelemetryConfiguration.ERROR, TelemetryConfiguration.CRASH, TelemetryConfiguration.OFF],
675 > default: TelemetryConfiguration.ON,
676 > }),
677 > [AgentHostSessionSyncEnabledConfigKey]: schemaProperty<boolean>({
678 > type: 'boolean',
679 > title: localize('agentHost.config.sessionSyncEnabled.title', "Session Sync"),
680 > description: localize('agentHost.config.sessionSyncEnabled.description', "Whether remote session sync is enabled for the copilot-sdk CLI."),
681 > default: false,
682 > }),
683 > [AgentHostCodexEnabledConfigKey]: schemaProperty<boolean>({
684 > type: 'boolean',
685 > title: localize('agentHost.config.codexAgentEnabled.title', "Codex Agent"),
686 > description: localize('agentHost.config.codexAgentEnabled.description', "Whether the Codex provider is enabled."),
687 > default: false,
688 > }),
689 > [AgentHostTerminalAutoApproveEnabledConfigKey]: schemaProperty<boolean>({
690 > type: 'boolean',
691 > title: localize('agentHost.config.terminalAutoApproveEnabled.title', "Terminal Auto Approve"),
692 > description: localize('agentHost.config.terminalAutoApproveEnabled.description', "Whether terminal auto-approve rules forwarded by the connected client are allowed to apply to agent-host shell permission requests."),
693 > default: true,
694 > }),
695 > [AgentHostGlobalAutoApproveEnabledConfigKey]: schemaProperty<boolean>({
696 > type: 'boolean',
697 > title: localize('agentHost.config.globalAutoApproveEnabled.title', "Global Auto Approve"),
698 > description: localize('agentHost.config.globalAutoApproveEnabled.description', "Whether VS Code's global auto-approve setting is enabled. When `true`, every tool call is auto-approved, equivalent to a session using Allow all."),
699 > default: false,
700 > }),
701 > [AgentHostAutoReplyEnabledConfigKey]: schemaProperty<boolean>({
702 > type: 'boolean',
703 > title: localize('agentHost.config.autoReplyEnabled.title', "Auto Reply"),
704 > description: localize('agentHost.config.autoReplyEnabled.description', "Whether VS Code's auto-reply setting is enabled. When `true`, `ask_user` questions are auto-answered instead of blocking on the user, mirroring autopilot mode."),
705 > default: false,
706 > }),
707 > [AgentHostPreferLongContextEnabledConfigKey]: schemaProperty<boolean>({
708 > type: 'boolean',
709 > title: localize('agentHost.config.preferLongContextEnabled.title', "Prefer Long Context"),
710 > description: localize('agentHost.config.preferLongContextEnabled.description', "Whether Copilot Chat's prefer-long-context setting is enabled. When `true`, models with a free long context window only show the long context option in the picker. When `false` (default), the smaller default context option stays selectable."),
711 > default: false,
712 > }),
713 > [AgentHostSystemProxyEnabledConfigKey]: schemaProperty<boolean>({
714 > type: 'boolean',
715 > title: localize('agentHost.config.systemProxyEnabled.title', "System Proxy Discovery"),
716 > description: localize('agentHost.config.systemProxyEnabled.description', "Whether Copilot sessions automatically discover and use the operating system's proxy configuration."),
717 > default: true,
718 > }),
719 > [AgentHostTerminalAutoApproveRulesConfigKey]: schemaProperty<AgentHostTerminalAutoApproveRules>({
720 > type: 'object',
721 > title: localize('agentHost.config.terminalAutoApproveRules.title', "Terminal Auto Approve Rules"),
722 > description: localize('agentHost.config.terminalAutoApproveRules.description', "Terminal auto-approve rules forwarded by the connected client for agent-host shell permission checks."),
723 > default: {},
724 > }),
725 > [AgentHostMcpServersConfigKey]: schemaProperty<AgentHostMcpServers>({
726 > type: 'object',
727 > title: localize('agentHost.config.mcpServers.title', "MCP Servers"),
728 > description: localize('agentHost.config.mcpServers.description', "Agent-host-level MCP servers exposed to every session, keyed by server name. Each value is a server configuration (see `<serverName>`)."),
729 > properties: mcpServersValueProperties,
730 > default: {},
731 > }),
732 > });
src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts 533 covered LOC · 31 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- copilotToolDisplay.ts
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 > import type { PermissionRequest } from '@github/copilot-sdk';
7 > import { hasKey } from '../../../../base/common/types.js';
8 > import { URI } from '../../../../base/common/uri.js';
9 > import { appendEscapedMarkdownInlineCode, escapeMarkdownLinkLabel, MarkdownString } from '../../../../base/common/htmlContent.js';
10 > import { hash } from '../../../../base/common/hash.js';
11 > import { localize } from '../../../../nls.js';
12 > import type { IAgentToolPendingConfirmationSignal } from '../../common/agentService.js';
13 > import { stripRedundantCdPrefix } from '../../common/commandLineHelpers.js';
14 > import { StringOrMarkdown } from '../../common/state/protocol/state.js';
15 > import { basename } from '../../../../base/common/resources.js';
16 > import { getServerToolDisplay } from '../shared/serverToolGroups.js';
17 >
18 > // =============================================================================
19 > // Copilot CLI built-in tool interfaces
20 > //
21 > // The Copilot CLI (via @github/copilot-sdk) exposes these built-in tools. Tool names
22 > // and parameter shapes are not typed in the SDK -- they come from the CLI server
23 > // as plain strings. These interfaces are derived from observing the CLI's actual
24 > // tool events and the Copilot Chat extension's CLI display table.
25 > //
26 > // Shell tool names follow a pattern per ShellConfig:
27 > // shellToolName, readShellToolName, writeShellToolName,
28 > // stopShellToolName, listShellsToolName
29 > // For bash: bash, read_bash, write_bash, stop_bash/bash_shutdown, list_bash
30 > // For powershell: powershell, read_powershell, write_powershell, stop_powershell/powershell_shutdown, list_powershell
31 > // =============================================================================
32 >
33 > /**
34 > * Known Copilot CLI tool names. These are the `toolName` values that appear
35 > * in `tool.execution_start` events from the SDK.
36 > */
37 > const enum CopilotToolName {
38 > StrReplaceEditor = 'str_replace_editor',
39 > StrReplace = 'str_replace',
40 > Insert = 'insert',
41 >
42 > Bash = 'bash',
43 > ReadBash = 'read_bash',
44 > WriteBash = 'write_bash',
45 > StopBash = 'stop_bash',
46 > BashShutdown = 'bash_shutdown',
47 > ListBash = 'list_bash',
48 >
49 > PowerShell = 'powershell',
50 > ReadPowerShell = 'read_powershell',
51 > WritePowerShell = 'write_powershell',
52 > StopPowerShell = 'stop_powershell',
53 > PowerShellShutdown = 'powershell_shutdown',
54 > ListPowerShell = 'list_powershell',
55 >
56 > View = 'view',
57 > Edit = 'edit',
58 > Create = 'create',
59 > Grep = 'grep',
60 > Rg = 'rg',
61 > Glob = 'glob',
62 > SearchCodeSubagent = 'search_code_subagent',
63 > ReplyToComment = 'reply_to_comment',
64 > CodeReview = 'code_review',
65 > ApplyPatch = 'apply_patch',
66 > GitApplyPatch = 'git_apply_patch',
67 > WebSearch = 'web_search',
68 > WebFetch = 'web_fetch',
69 > AskUser = 'ask_user',
70 > ReportIntent = 'report_intent',
71 > Think = 'think',
72 > ReportProgress = 'report_progress',
73 > UpdateTodo = 'update_todo',
74 > ShowFile = 'show_file',
75 > FetchCopilotCliDocumentation = 'fetch_copilot_cli_documentation',
76 > ProposeWork = 'propose_work',
77 > TaskComplete = 'task_complete',
78 > Skill = 'skill',
79 > Task = 'task',
80 > ListAgents = 'list_agents',
81 > ReadAgent = 'read_agent',
82 > ExitPlanMode = 'exit_plan_mode',
83 > Sql = 'sql',
84 > Lsp = 'lsp',
85 > CreatePullRequest = 'create_pull_request',
86 > GhAdvisoryDatabase = 'gh-advisory-database',
87 > StoreMemory = 'store_memory',
88 > ParallelValidation = 'parallel_validation',
89 > WriteAgent = 'write_agent',
90 > McpReload = 'mcp_reload',
91 > McpValidate = 'mcp_validate',
92 > ToolSearchToolRegex = 'tool_search_tool_regex',
93 > CodeqlChecker = 'codeql_checker',
94 > }
95 >
96 > /** Parameters for the `bash` / `powershell` shell tools. */
97 > interface ICopilotShellToolArgs {
98 > command: string;
99 > timeout?: number;
100 > }
101 >
102 > /** Parameters for file tools (`view`, `edit`, `create`). */
103 > interface ICopilotFileToolArgs {
104 > path: string;
105 > }
106 >
107 > /**
108 > * Parameters for the `view` tool. The Copilot CLI accepts an optional
109 > * `view_range: [startLine, endLine]` (1-based, inclusive). `endLine` may be
110 > * `-1` to mean "to end of file".
111 > */
112 > interface ICopilotViewToolArgs extends ICopilotFileToolArgs {
113 > view_range?: number[];
114 > }
115 >
116 > /**
117 > * Normalizes a `view_range` array. Returns `undefined` unless the array has
118 > * exactly two integer elements with `startLine >= 0`. `endLine === -1` is
119 > * preserved as the "to end of file" sentinel; otherwise `endLine` must be
120 > * `>= startLine`.
121 > */
122 function formatViewRange(view_range: number[] | undefined): { startLine: number; endLine: number } | undefined {
123 if (!Array.isArray(view_range) || view_range.length !== 2) {
136 return { startLine, endLine };
137 }
139 > /**
140 > * Parameters for the `grep` tool. The Copilot CLI's `grep` accepts the same
141 > * rich rg-flag schema as `rg`; the older narrower shape (e.g. `include`) is
142 > * no longer used.
143 > */
144 > interface ICopilotGrepToolArgs {
145 > pattern: string;
146 > path?: string;
147 > output_mode?: 'content' | 'files_with_matches' | 'count';
148 > glob?: string;
149 > type?: string;
150 > '-i'?: boolean;
151 > '-A'?: number;
152 > '-B'?: number;
153 > '-C'?: number;
154 > '-n'?: boolean;
155 > head_limit?: number;
156 > multiline?: boolean;
157 > }
158 >
159 > /**
160 > * Parameters for the `rg` tool. Mirrors {@link ICopilotGrepToolArgs} today but
161 > * is kept as a distinct interface so the two tools can drift independently if
162 > * the SDK ever differentiates them.
163 > */
164 > interface ICopilotRgToolArgs {
165 > pattern: string;
166 > path?: string;
167 > output_mode?: 'content' | 'files_with_matches' | 'count';
168 > glob?: string;
169 > type?: string;
170 > '-i'?: boolean;
171 > '-A'?: number;
172 > '-B'?: number;
173 > '-C'?: number;
174 > '-n'?: boolean;
175 > head_limit?: number;
176 > multiline?: boolean;
177 > }
178 >
179 > /** Parameters for the `glob` tool. */
180 > interface ICopilotGlobToolArgs {
181 > pattern: string;
182 > path?: string;
183 > }
184 >
185 > /** Parameters for the `sql` tool. */
186 > interface ICopilotSqlToolArgs {
187 > description?: string;
188 > query?: string;
189 > }
190 >
191 > /** Parameters for the `web_fetch` tool. */
192 > interface ICopilotWebFetchToolArgs {
193 > url: string;
194 > }
195 >
196 > /**
197 > * Parameters shared by the agent-coordination tools (`read_agent`,
198 > * `write_agent`). The Copilot CLI identifies the target agent by its
199 > * human-readable `agent_id` (e.g. `math-helper`).
200 > */
201 > interface ICopilotAgentToolArgs {
202 > agent_id?: string;
203 > }
204 >
205 > /**
206 > * Reads a well-formed `agent_id` from untrusted tool parameters. Since these are
207 > * parsed from JSON they may not match the expected shape, so the id is returned
208 > * only when it is a non-empty string and is therefore safe to render as inline
209 > * markdown code.
210 > */
211 function getAgentId(parameters: Record<string, unknown> | undefined): string | undefined {
212 const agentId = (parameters as ICopilotAgentToolArgs | undefined)?.agent_id;
213 return typeof agentId === 'string' && agentId.length > 0 ? agentId : undefined;
214 }
216 > /**
217 > * Parameters for the `apply_patch` / `git_apply_patch` tools. The patch text
218 > * itself lives in `input` using the V4A diff format (file headers like
219 > * `*** Update File: <path>`), so file paths must be parsed out of the body
220 > * rather than read from a top-level field.
221 > */
222 > interface ICopilotApplyPatchToolArgs {
223 > input?: string;
224 > /** Some SDK callers send the patch under `patch` instead of `input`. */
225 > patch?: string;
226 > explanation?: string;
227 > }
228 >
229 > /**
230 > * Headers of the V4A patch format the `apply_patch` tool accepts. Tolerates
231 > * leading whitespace; trims the captured path.
232 > */
233 > const APPLY_PATCH_FILE_HEADERS = [
234 > /^\s*\*\*\*\s+Update File:\s*(.+?)\s*$/,
235 > /^\s*\*\*\*\s+Add File:\s*(.+?)\s*$/,
236 > /^\s*\*\*\*\s+Delete File:\s*(.+?)\s*$/,
237 > /^\s*\*\*\*\s+Move to:\s*(.+?)\s*$/,
238 > ];
239 >
240 > /**
241 > * Extracts the set of file paths affected by an `apply_patch` payload. Reads
242 > * the `*** Update File:` / `*** Add File:` / `*** Delete File:` / `*** Move to:`
243 > * headers from the V4A diff body. Returns paths in document order with
244 > * duplicates removed.
245 > *
246 > * Accepts either a structured args object ({@link ICopilotApplyPatchToolArgs})
247 > * or a bare patch string. The Copilot SDK delivers `apply_patch` with
248 > * `arguments` as a raw V4A patch string (custom tool format), not as a JSON
249 > * object, so the string fallback is the common case for apply_patch.
250 > */
251 function getApplyPatchFiles(args: string | ICopilotApplyPatchToolArgs | undefined): string[] {
252 const text = typeof args === 'string' ? args : (args?.input ?? args?.patch);
271 return out;
272 }
274 > /** Set of tool names that perform file edits. */
275 > const EDIT_TOOL_NAMES: ReadonlySet<string> = new Set([
276 > CopilotToolName.Edit,
277 > CopilotToolName.StrReplace,
278 > CopilotToolName.Insert,
279 > CopilotToolName.Create,
280 > CopilotToolName.ApplyPatch,
281 > CopilotToolName.GitApplyPatch,
282 > ]);
283 >
284 > const STR_REPLACE_EDITOR_EDIT_COMMANDS: ReadonlySet<string> = new Set([
285 > CopilotToolName.Edit,
286 > CopilotToolName.StrReplace,
287 > CopilotToolName.Insert,
288 > CopilotToolName.Create,
289 > ]);
290 >
291 > /**
292 > * Returns true if the tool modifies files on disk.
293 > */
294 > export function isEditTool(toolName: string, command?: string): boolean {
295 if (EDIT_TOOL_NAMES.has(toolName)) {
296 return true;
301 return false;
302 }
304 > /**
305 > * Extracts the target file path from an edit tool's parameters, if available.
306 > * For `apply_patch` / `git_apply_patch` the first file in the V4A patch body
307 > * is returned. Callers that need every affected file (for snapshotting all
308 > * edits in a multi-file patch) should use {@link getEditFilePaths} instead.
309 > */
310 > export function getEditFilePath(parameters: unknown): string | undefined {
311 return getEditFilePaths(parameters)[0];
312 }
314 > /**
315 > * Extracts every file path an edit tool will touch. For `edit` / `create` this
316 > * is the single `path` parameter; for `apply_patch` / `git_apply_patch` this
317 > * is the unique set of files declared in the V4A patch body, in document
318 > * order. Returns an empty array if no paths can be determined.
319 > */
320 > export function getEditFilePaths(parameters: unknown): string[] {
321 if (typeof parameters === 'string') {
322 // Could be either a JSON-encoded args object or a raw V4A patch
348 return typeof args.path === 'string' ? [args.path] : [];
349 }
351 > /** Set of tool names that execute shell commands (bash or powershell). */
352 > const SHELL_TOOL_NAMES: ReadonlySet<string> = new Set([
353 > CopilotToolName.Bash,
354 > CopilotToolName.PowerShell,
355 > ]);
356 >
357 > /** Set of tool names that write input to an interactive shell session. */
358 > const WRITE_SHELL_TOOL_NAMES: ReadonlySet<string> = new Set([
359 > CopilotToolName.WriteBash,
360 > CopilotToolName.WritePowerShell,
361 > ]);
362 >
363 > /** Set of tool names that read output from an interactive shell session. */
364 > const READ_SHELL_TOOL_NAMES: ReadonlySet<string> = new Set([
365 > CopilotToolName.ReadBash,
366 > CopilotToolName.ReadPowerShell,
367 > ]);
368 >
369 > /** Set of tool names that spawn subagent sessions. */
370 > const SUBAGENT_TOOL_NAMES: ReadonlySet<string> = new Set([
371 > 'task',
372 > ]);
373 >
374 > /** Set of tool names that perform file/text search. */
375 > const SEARCH_TOOL_NAMES: ReadonlySet<string> = new Set([
376 > CopilotToolName.Grep,
377 > CopilotToolName.Rg,
378 > CopilotToolName.Glob,
379 > ]);
380 >
381 > /**
382 > * Tools that should not be shown to the user. These are internal tools
383 > * used by the CLI for its own purposes (e.g., reporting intent to the model).
384 > *
385 > * `skill` is hidden because the SDK already emits a richer `skill.invoked`
386 > * lifecycle event with the resolved skill file path; the agent session
387 > * synthesizes a tool-start/complete pair from that event so the UI can
388 > * render a clickable file link instead of just the skill name. See
389 > * {@link synthesizeSkillToolCall}.
390 > */
391 > const HIDDEN_TOOL_NAMES: ReadonlySet<string> = new Set([
392 > CopilotToolName.ReportIntent,
393 > CopilotToolName.Skill,
394 > ]);
395 >
396 > /**
397 > * Returns true if the tool should be hidden from the UI.
398 > */
399 > export function isHiddenTool(toolName: string): boolean {
400 return HIDDEN_TOOL_NAMES.has(toolName);
401 }
403 > /**
404 > * Returns true for the auto-approved agent-coordination tools (list/read/write
405 > * agents). These are client-contributed tools that never go through the
406 > * permission flow, so the agent host auto-readies them at start to surface a
407 > * tailored invocation message instead of the generic fallback.
408 > */
409 > export function isAgentCoordinationTool(toolName: string): boolean {
410 return toolName === CopilotToolName.ListAgents
411 || toolName === CopilotToolName.ReadAgent
412 || toolName === CopilotToolName.WriteAgent;
413 }
415 > /**
416 > * Returns true when the tool is Copilot's internal Autopilot completion signal.
417 > */
418 > export function isTaskCompleteTool(toolName: string): boolean {
419 return toolName === CopilotToolName.TaskComplete;
420 }
422 > /**
423 > * Extracts the user-facing Autopilot completion summary from the tool output,
424 > * falling back to the original `summary` argument for older/incomplete events.
425 > */
426 > export function getTaskCompleteSummary(parameters: Record<string, unknown> | undefined, toolOutput: string | undefined): string | undefined {
427 if (toolOutput && toolOutput.trim().length > 0) {
428 return toolOutput;
431 return typeof summary === 'string' && summary.trim().length > 0 ? summary : undefined;
432 }
434 > /**
435 > * Formats the Autopilot completion summary as the markdown response part
436 > * content, including the localized prefix.
437 > */
438 > export function getTaskCompleteMarkdown(parameters: Record<string, unknown> | undefined, toolOutput: string | undefined): string | undefined {
439 const summary = getTaskCompleteSummary(parameters, toolOutput);
440 if (!summary) {
443 return '\n\n' + localize('toolMarkdown.taskComplete', "**Task completed:** {0}", summary);
444 }
446 > /**
447 > * Returns true if the tool should render as a markdown response part instead
448 > * of a tool-call entry.
449 > */
450 > export function isMarkdownRenderedTool(toolName: string): boolean {
451 return isTaskCompleteTool(toolName);
452 }
454 > /**
455 > * Returns markdown content for tools rendered as inline markdown response
456 > * parts.
457 > */
458 > export function getToolMarkdownContent(toolName: string, parameters: Record<string, unknown> | undefined): string | undefined {
459 if (!isMarkdownRenderedTool(toolName)) {
460 return undefined;
466 return getTaskCompleteMarkdown(parameters, undefined);
467 }
469 > /**
470 > * Returns true if the tool executes shell commands.
471 > */
472 > export function isShellTool(toolName: string): boolean {
473 return SHELL_TOOL_NAMES.has(toolName);
474 }
476 > /**
477 > * Extracts the intention for a shell tool call from its `description`
478 > * argument. The Copilot shell tools (`bash`/`powershell`) carry a short
479 > * human-readable description of what the command does, which matches the
480 > * model's intention summary. Non-shell tools have no such argument, so this
481 > * returns `undefined` for them.
482 > */
483 > export function getShellIntention(toolName: string, parameters: Record<string, unknown> | undefined): string | undefined {
484 if (isShellTool(toolName) && typeof parameters?.description === 'string' && parameters.description.length > 0) {
485 return parameters.description;
487 return undefined;
488 }
490 > // =============================================================================
491 > // Display helpers
492 > //
493 > // These functions translate Copilot CLI tool names and arguments into
494 > // human-readable display strings. This logic lives here -- in the agent-host
495 > // process -- so the IPC protocol stays agent-agnostic; the renderer never needs
496 > // to know about specific tool names.
497 > // =============================================================================
498 >
499 function truncate(text: string, maxLength: number): string {
500 return text.length > maxLength ? text.substring(0, maxLength - 3) + '...' : text;
501 }
503 > /**
504 > * Formats a file path as a markdown link `[](file-uri)` so it renders
505 > * as a clickable file widget in the chat UI.
506 > */
507 function formatPathAsMarkdownLink(path: string): string {
508 const uri = URI.file(path);
509 return `[${escapeMarkdownLinkLabel(basename(uri))}](${uri})`;
510 }
512 function formatUrlAsMarkdownLink(url: string): string {
513 return new MarkdownString().appendLink(url, truncate(url, 80)).value;
514 }
516 > /**
517 > * Wraps a localized message containing a markdown file link into a
518 > * `StringOrMarkdown` object so the renderer treats it as markdown.
519 > */
520 function md(value: string): StringOrMarkdown {
521 return { markdown: value };
522 }
524 > export function getToolDisplayName(toolName: string): string {
525 const serverDisplay = getServerToolDisplay(toolName, undefined)?.displayName;
526 if (serverDisplay !== undefined) {
584 }
585 }
587 > export function getInvocationMessage(toolName: string, displayName: string, parameters: Record<string, unknown> | undefined): StringOrMarkdown {
588 const serverDisplay = getServerToolDisplay(toolName, parameters)?.invocationMessage;
589 if (serverDisplay !== undefined) {
704 }
705 }
707 > export function getPastTenseMessage(toolName: string, displayName: string, parameters: Record<string, unknown> | undefined, success: boolean, resultText?: string): StringOrMarkdown {
708 if (!success) {
709 return localize('toolComplete.failed', "\"{0}\" failed", displayName);
837 }
838 }
840 > // =============================================================================
841 > // Skill event synthesis
842 > //
843 > // The Copilot SDK emits a `skill` tool call (which we hide) and, separately, a
844 > // `skill.invoked` lifecycle event with the resolved skill file path. We turn
845 > // the latter into a synthesized tool-start/complete pair so clients can render
846 > // a clickable file link to the SKILL.md the agent loaded -- matching the
847 > // existing `view`-tool display style. Live and replay paths share this helper
848 > // so they stay in lock-step (see also the mirrored-pair gotcha for tool-call
849 > // display in this file).
850 > // =============================================================================
851 >
852 > /** Subset of the SDK's `skill.invoked` payload that the synth helper needs. */
853 > export interface ICopilotSkillInvokedData {
854 > readonly name: string;
855 > readonly path?: string;
856 > readonly description?: string;
857 > }
858 >
859 > /**
860 > * Builds a stable synthetic tool call id for a `skill.invoked` event so
861 > * reconnect/replay produces the same id as the original live emit. The id
862 > * is used unencoded as a path segment (e.g. by `ChatResponseResource.createUri`),
863 > * so it must not contain characters like `/` -- we hash any fallback values
864 > * that could carry filesystem paths or arbitrary text.
865 > */
866 > export function getSkillSyntheticToolCallId(eventId: string | undefined, data: ICopilotSkillInvokedData): string {
867 if (eventId) {
868 return `synth-skill-${eventId}`;
871 return `synth-skill-${hash(seed).toString(16)}`;
872 }
874 > /**
875 > * Synthesized data for a `skill.invoked` tool call. Used by both the live
876 > * session handler and the history-replay mapper so the two paths render
877 > * identically. Callers wrap this into protocol actions or {@link Turn}
878 > * data; this helper avoids any agent-protocol coupling.
879 > */
880 > export interface ISynthesizedSkillToolCall {
881 > readonly toolCallId: string;
882 > readonly toolName: string;
883 > readonly displayName: string;
884 > readonly invocationMessage: StringOrMarkdown;
885 > readonly pastTenseMessage: StringOrMarkdown;
886 > }
887 >
888 > /**
889 > * Synthesizes the data for a `skill.invoked` tool call (a tool-start /
890 > * tool-complete pair). Returns the constituent fields without coupling to
891 > * any specific event or action shape — callers compose them into protocol
892 > * actions or {@link Turn} entries as needed.
893 > */
894 > export function synthesizeSkillToolCall(
895 data: ICopilotSkillInvokedData,
896 eventId: string | undefined,
924 };
925 }
927 > export function getToolInputString(toolName: string, parameters: Record<string, unknown> | undefined, rawArguments: string | undefined): string | undefined {
928 if (!parameters && !rawArguments) {
929 return undefined;
968 }
969 }
971 > /**
972 > * Returns a rendering hint for the given tool. Currently 'terminal', 'subagent',
973 > * and 'search' are supported, which tell the renderer to display the tool with
974 > * a terminal command block, a subagent widget, or a search icon respectively.
975 > */
976 > export function getToolKind(toolName: string): 'terminal' | 'subagent' | 'search' | undefined {
977 if (SHELL_TOOL_NAMES.has(toolName)) {
978 return 'terminal';
986 return undefined;
987 }
989 > /**
990 > * Extracts subagent metadata (agent name, description) from the parsed
991 > * arguments of a Copilot SDK subagent tool call. The Copilot `task` tool
992 > * uses `agent_type` (snake_case), which this normalizes into the generic
993 > * `subagentAgentName` / `subagentDescription` shape used by the rest of the
994 > * agent host code.
995 > *
996 > * Only call this for tools where {@link getToolKind} returned `'subagent'`.
997 > */
998 > export function getSubagentMetadata(parameters: Record<string, unknown> | undefined): { agentName?: string; description?: string } {
999 if (!parameters) {
1000 return {};
1008 return { agentName, description };
1009 }
1011 > /**
1012 > * Returns the shell language identifier for syntax highlighting.
1013 > * Used when creating terminal tool-specific data for the renderer.
1014 > */
1015 > export function getShellLanguage(toolName: string): string {
1016 switch (toolName) {
1017 case CopilotToolName.PowerShell:
1021 }
1022 }
1024 > // =============================================================================
1025 > // Permission display
1026 > //
1027 > // Derives display fields from SDK permission requests for the tool
1028 > // confirmation UI. Colocated with the tool-start display helpers above so
1029 > // that formatting utilities (formatPathAsMarkdownLink, md, etc.) are shared.
1030 > // =============================================================================
1031 >
1032 > export function tryStringify(value: unknown): string | undefined {
1033 try {
1034 return JSON.stringify(value);
1037 }
1038 }
1040 > /**
1041 > * Loose, optional-field projection of the SDK's {@link PermissionRequest}
1042 > * discriminated union. Lets the rest of the agent host read the well-known
1043 > * fields without `switch (request.kind)` narrowing at every access site.
1044 > *
1045 > * The SDK's `PermissionRequest` (a union with required per-variant fields) is
1046 > * structurally assignable to this interface — every variant carries `kind`
1047 > * and `toolCallId?`, and the variant-specific fields are listed here as
1048 > * optional. Use this type at the agent-host boundary so call sites and tests
1049 > * can rely on a single shape.
1050 > */
1051 > export interface ITypedPermissionRequest {
1052 > /** Permission kind discriminator from the SDK. */
1053 > kind: PermissionRequest['kind'];
1054 > /** Tool call ID that triggered this permission request, when available. */
1055 > toolCallId?: string;
1056 > /** File path — set for `read` permission requests. */
1057 > path?: string;
1058 > /** File path — set for `write` permission requests. */
1059 > fileName?: string;
1060 > /** Full shell command text — set for `shell` permission requests. */
1061 > fullCommandText?: string;
1062 > /**
1063 > * True when the model requested this `shell` command run outside the
1064 > * sandbox (via `requestSandboxBypass`) and the host opted in via
1065 > * `sandbox.allowBypass`.
1066 > */
1067 > requestSandboxBypass?: boolean;
1068 > /** Human-readable intention describing the operation. */
1069 > intention?: string;
1070 > /** MCP server name — set for `mcp` permission requests. */
1071 > serverName?: string;
1072 > /** Tool name — set for `mcp` and `custom-tool` permission requests. */
1073 > toolName?: string;
1074 > /** Tool arguments — set for `custom-tool` permission requests. */
1075 > args?: Record<string, unknown>;
1076 > /** URL — set for `url` permission requests. */
1077 > url?: string;
1078 > /** Unified diff of the proposed change — set for `write` permission requests. */
1079 > diff?: string;
1080 > /** New file contents that will be written — set for `write` permission requests. */
1081 > newFileContents?: string;
1082 > }
1083 >
1084 > /** Safely extract a string value from an SDK field that may be `unknown` at runtime. */
1085 function str(value: unknown): string | undefined {
1086 return typeof value === 'string' ? value : undefined;
1087 }
1089 > /**
1090 > * Derives display fields from a permission request for the tool confirmation UI.
1091 > */
1092 > export function getPermissionDisplay(request: ITypedPermissionRequest, workingDirectory?: URI, isNewFile?: boolean): {
1093 confirmationTitle: string;
1094 invocationMessage: StringOrMarkdown;
src/vs/platform/files/common/fileService.ts 523 covered LOC · 144 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- fileService.ts
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 > import { coalesce } from '../../../base/common/arrays.js';
7 > import { Promises, ResourceQueue } from '../../../base/common/async.js';
8 > import { bufferedStreamToBuffer, bufferToReadable, newWriteableBufferStream, readableToBuffer, streamToBuffer, VSBuffer, VSBufferReadable, VSBufferReadableBufferedStream, VSBufferReadableStream } from '../../../base/common/buffer.js';
9 > import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
10 > import { Emitter } from '../../../base/common/event.js';
11 > import { hash } from '../../../base/common/hash.js';
12 > import { Iterable } from '../../../base/common/iterator.js';
13 > import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
14 > import { TernarySearchTree } from '../../../base/common/ternarySearchTree.js';
15 > import { Schemas } from '../../../base/common/network.js';
16 > import { mark } from '../../../base/common/performance.js';
17 > import { extUri, extUriIgnorePathCase, IExtUri, isAbsolutePath } from '../../../base/common/resources.js';
18 > import { consumeStream, isReadableBufferedStream, isReadableStream, listenStream, newWriteableStream, peekReadable, peekStream, transform } from '../../../base/common/stream.js';
19 > import { URI } from '../../../base/common/uri.js';
20 > import { localize } from '../../../nls.js';
21 > import { ensureFileSystemProviderError, etag, ETAG_DISABLED, FileChangesEvent, IFileDeleteOptions, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, hasFileAppendCapability, hasFileAtomicReadCapability, hasFileFolderCopyCapability, hasFileReadStreamCapability, hasOpenReadWriteCloseCapability, hasReadWriteCapability, ICreateFileOptions, IFileContent, IFileService, IFileStat, IFileStatWithMetadata, IFileStreamContent, IFileSystemProvider, IFileSystemProviderActivationEvent, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IReadFileOptions, IReadFileStreamOptions, IResolveFileOptions, IFileStatResult, IFileStatResultWithMetadata, IResolveMetadataFileOptions, IStat, IFileStatWithPartialMetadata, IWatchOptions, IWriteFileOptions, NotModifiedSinceFileOperationError, toFileOperationResult, toFileSystemProviderErrorCode, hasFileCloneCapability, TooLargeFileOperationError, hasFileAtomicDeleteCapability, hasFileAtomicWriteCapability, IWatchOptionsWithCorrelation, IFileSystemWatcher, IWatchOptionsWithoutCorrelation, hasFileRealpathCapability } from './files.js';
22 > import { readFileIntoStream } from './io.js';
23 > import { ILogService } from '../../log/common/log.js';
24 > import { ErrorNoTelemetry } from '../../../base/common/errors.js';
25 >
26 > export class FileService extends Disposable implements IFileService {
27 >
28 > declare readonly _serviceBrand: undefined;
29 >
30 > // Choose a buffer size that is a balance between memory needs and
31 > // manageable IPC overhead. The larger the buffer size, the less
32 > // roundtrips we have to do for reading/writing data.
33 > private readonly BUFFER_SIZE = 256 * 1024;
34 >
35 > constructor(@ILogService private readonly logService: ILogService) {
36 > super(); fileService.ts
37 > }
38 >
39 > //#region File System Provider
40 >
41 > private readonly _onDidChangeFileSystemProviderRegistrations = this._register(new Emitter<IFileSystemProviderRegistrationEvent>());
42 > readonly onDidChangeFileSystemProviderRegistrations = this._onDidChangeFileSystemProviderRegistrations.event;
43 >
44 > private readonly _onWillActivateFileSystemProvider = this._register(new Emitter<IFileSystemProviderActivationEvent>());
45 > readonly onWillActivateFileSystemProvider = this._onWillActivateFileSystemProvider.event;
46 >
47 > private readonly _onDidChangeFileSystemProviderCapabilities = this._register(new Emitter<IFileSystemProviderCapabilitiesChangeEvent>());
48 > readonly onDidChangeFileSystemProviderCapabilities = this._onDidChangeFileSystemProviderCapabilities.event;
49 >
50 > private readonly provider = new Map<string, IFileSystemProvider>();
51 >
52 > registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable {
53 > if (this.provider.has(scheme)) { fileService.ts
54 throw new Error(`A filesystem provider for the scheme '${scheme}' is already registered.`);
55 }
57 > mark(`code/registerFilesystem/${scheme}`);
58 >
59 > const providerDisposables = new DisposableStore();
60 >
61 > // Add provider with event
62 > this.provider.set(scheme, provider);
63 > this._onDidChangeFileSystemProviderRegistrations.fire({ added: true, scheme, provider });
64 >
65 > // Forward events from provider
66 > providerDisposables.add(provider.onDidChangeFile(changes => {
67 > const event = new FileChangesEvent(changes, !this.isPathCaseSensitive(provider)); fileService.ts
68 >
69 > // Always emit any event internally
70 > this.internalOnDidFilesChange.fire(event);
71 >
72 > // Only emit uncorrelated events in the global `onDidFilesChange` event
73 > if (!event.hasCorrelation()) {
74 > this._onDidUncorrelatedFilesChange.fire(event);
75 > }
76 > })); fileService.ts
77 > if (typeof provider.onDidWatchError === 'function') {
78 providerDisposables.add(provider.onDidWatchError(error => this._onDidWatchError.fire(new Error(error))));
79 }
80 > providerDisposables.add(provider.onDidChangeCapabilities(() => this._onDidChangeFileSystemProviderCapabilities.fire({ provider, scheme }))); fileService.ts
81 >
82 > return toDisposable(() => {
83 > this._onDidChangeFileSystemProviderRegistrations.fire({ added: false, scheme, provider }); fileService.ts
84 > this.provider.delete(scheme);
85 >
86 > dispose(providerDisposables);
87 > }); fileService.ts
88 > }
90 > getProvider(scheme: string): IFileSystemProvider | undefined {
91 return this.provider.get(scheme);
92 }
94 > async activateProvider(scheme: string): Promise<void> {
96 > // Emit an event that we are about to activate a provider with the given scheme.
97 > // Listeners can participate in the activation by registering a provider for it.
98 > const joiners: Promise<void>[] = [];
99 > this._onWillActivateFileSystemProvider.fire({
100 > scheme,
101 > join(promise) {
102 joiners.push(promise);
103 },
104 > }); fileService.ts
105 >
106 > if (this.provider.has(scheme)) {
107 > return; // provider is already here so we can return directly fileService.ts
108 > }
109
110 // If the provider is not yet there, make sure to join on the listeners assuming
111 // that it takes a bit longer to register the file system provider.
112 await Promises.settled(joiners);
113 > } fileService.ts
115 > async canHandleResource(resource: URI): Promise<boolean> {
116
117 // Await activation of potentially extension contributed providers
120 return this.hasProvider(resource);
121 }
123 > hasProvider(resource: URI): boolean {
124 return this.provider.has(resource.scheme);
125 }
127 > hasCapability(resource: URI, capability: FileSystemProviderCapabilities): boolean {
128 const provider = this.provider.get(resource.scheme);
129
130 return !!(provider && (provider.capabilities & capability));
131 }
133 > listCapabilities(): Iterable<{ scheme: string; capabilities: FileSystemProviderCapabilities }> {
134 return Iterable.map(this.provider, ([scheme, provider]) => ({ scheme, capabilities: provider.capabilities }));
135 }
137 > protected async withProvider(resource: URI): Promise<IFileSystemProvider> {
139 > // Assert path is absolute
140 > if (!isAbsolutePath(resource)) {
141 throw new FileOperationError(localize('invalidPath', "Unable to resolve filesystem provider with relative file path '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_INVALID_PATH);
142 }
144 > // Activate provider
145 > await this.activateProvider(resource.scheme);
146 >
147 > // Assert provider
148 > const provider = this.provider.get(resource.scheme);
149 > if (!provider) {
150 const error = new ErrorNoTelemetry();
151 error.message = localize('noProviderFound', "ENOPRO: No file system provider found for resource '{0}'", resource.toString());
153 throw error;
154 }
156 > return provider;
157 > } fileService.ts
159 > private async withReadProvider(resource: URI): Promise<IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability> {
160 const provider = await this.withProvider(resource);
161
166 throw new Error(`Filesystem provider for scheme '${resource.scheme}' neither has FileReadWrite, FileReadStream nor FileOpenReadWriteClose capability which is needed for the read operation.`);
167 }
169 > private async withWriteProvider(resource: URI): Promise<IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability> {
170 > const provider = await this.withProvider(resource); fileService.ts
171 >
172 > if (hasOpenReadWriteCloseCapability(provider) || hasReadWriteCapability(provider)) {
173 > return provider;
174 > }
175
176 throw new Error(`Filesystem provider for scheme '${resource.scheme}' neither has FileReadWrite nor FileOpenReadWriteClose capability which is needed for the write operation.`);
177 > } fileService.ts
179 > //#endregion
180 >
181 > //#region Operation events
182 >
183 > private readonly _onDidRunOperation = this._register(new Emitter<FileOperationEvent>());
184 > readonly onDidRunOperation = this._onDidRunOperation.event;
185 >
186 > //#endregion
187 >
188 > //#region File Metadata Resolving
189 >
190 > async resolve(resource: URI, options: IResolveMetadataFileOptions): Promise<IFileStatWithMetadata>;
191 > async resolve(resource: URI, options?: IResolveFileOptions): Promise<IFileStat>;
192 > async resolve(resource: URI, options?: IResolveFileOptions): Promise<IFileStat> {
193 > try { fileService.ts
194 > return await this.doResolveFile(resource, options);
195 > } catch (error) {
196
197 // Specially handle file not found case as file operation result
203 throw ensureFileSystemProviderError(error);
204 }
205 > } fileService.ts
207 > private async doResolveFile(resource: URI, options: IResolveMetadataFileOptions): Promise<IFileStatWithMetadata>;
208 > private async doResolveFile(resource: URI, options?: IResolveFileOptions): Promise<IFileStat>;
209 > private async doResolveFile(resource: URI, options?: IResolveFileOptions): Promise<IFileStat> {
210 > const provider = await this.withProvider(resource); fileService.ts
211 > const isPathCaseSensitive = this.isPathCaseSensitive(provider); fileService.ts
212 >
213 > const resolveTo = options?.resolveTo;
214 > const resolveSingleChildDescendants = options?.resolveSingleChildDescendants; fileService.ts
215 > const resolveMetadata = options?.resolveMetadata;
216 >
217 > const stat = await provider.stat(resource);
219 > let trie: TernarySearchTree<URI, boolean> | undefined;
220 >
221 > return this.toFileStat(provider, resource, stat, undefined, !!resolveMetadata, (stat, siblings) => {
223 > // lazy trie to check for recursive resolving
224 > if (!trie) {
225 > trie = TernarySearchTree.forUris<true>(() => !isPathCaseSensitive);
226 > trie.set(resource, true);
227 > if (resolveTo) {
228 trie.fill(true, resolveTo);
229 }
230 > } fileService.ts
231 >
232 > // check for recursive resolving
233 > if (trie.get(stat.resource) || trie.findSuperstr(stat.resource.with({ query: null, fragment: null } /* required for https://github.com/microsoft/vscode/issues/128151 */))) {
234 > return true;
235 > }
236
237 // check for resolving single child folders
238 > if (stat.isDirectory && resolveSingleChildDescendants) { fileService.ts
239 return siblings === 1;
240 }
241
242 return false;
243 > }); fileService.ts
244 > } fileService.ts
246 > private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType } & Partial<IStat>, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise<IFileStat>;
247 > private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat, siblings: number | undefined, resolveMetadata: true, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise<IFileStatWithMetadata>;
248 > private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType } & Partial<IStat>, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise<IFileStat> {
249 > const { providerExtUri } = this.getExtUri(provider); fileService.ts
250 >
251 > // convert to file stat
252 > const fileStat: IFileStat = {
253 > resource,
254 > name: providerExtUri.basename(resource),
255 > isFile: (stat.type & FileType.File) !== 0,
256 > isDirectory: (stat.type & FileType.Directory) !== 0,
257 > isSymbolicLink: (stat.type & FileType.SymbolicLink) !== 0,
258 > mtime: stat.mtime,
259 > ctime: stat.ctime,
260 > size: stat.size,
261 > readonly: Boolean((stat.permissions ?? 0) & FilePermission.Readonly) || Boolean(provider.capabilities & FileSystemProviderCapabilities.Readonly),
262 > locked: Boolean((stat.permissions ?? 0) & FilePermission.Locked),
263 > executable: Boolean((stat.permissions ?? 0) & FilePermission.Executable),
264 > etag: etag({ mtime: stat.mtime, size: stat.size }),
265 > children: undefined
266 > };
267 >
268 > // check to recurse for directories
269 > if (fileStat.isDirectory && recurse(fileStat, siblings)) {
270 > try { fileService.ts
271 > const entries = await provider.readdir(resource);
272 > const resolvedEntries = await Promises.settled(entries.map(async ([name, type]) => {
273 try {
274 const childResource = providerExtUri.joinPath(resource, name);
281 return null; // can happen e.g. due to permission errors
282 }
283 > })); fileService.ts
284 >
285 > // make sure to get rid of null values that signal a failure to resolve a particular entry
286 > fileStat.children = coalesce(resolvedEntries);
287 > } catch (error) {
288 this.logService.trace(error);
289
290 fileStat.children = []; // gracefully handle errors, we may not have permissions to read
291 }
293 > return fileStat;
294 > }
296 > return fileStat;
297 > } fileService.ts
299 > async resolveAll(toResolve: { resource: URI; options?: IResolveFileOptions }[]): Promise<IFileStatResult[]>;
300 > async resolveAll(toResolve: { resource: URI; options: IResolveMetadataFileOptions }[]): Promise<IFileStatResultWithMetadata[]>;
301 > async resolveAll(toResolve: { resource: URI; options?: IResolveFileOptions }[]): Promise<IFileStatResult[]> {
302 return Promises.settled(toResolve.map(async entry => {
303 try {
310 }));
311 }
313 > async stat(resource: URI): Promise<IFileStatWithPartialMetadata> {
314 const provider = await this.withProvider(resource);
315
318 return this.toFileStat(provider, resource, stat, undefined, true, () => false /* Do not resolve any children */);
319 }
321 > async realpath(resource: URI): Promise<URI | undefined> {
322 const provider = await this.withProvider(resource);
323
330 return undefined;
331 }
333 > async exists(resource: URI): Promise<boolean> {
334 const provider = await this.withProvider(resource);
335
342 }
343 }
345 > //#endregion
346 >
347 > //#region File Reading/Writing
348 >
349 > async canCreateFile(resource: URI, options?: ICreateFileOptions): Promise<Error | true> {
350 try {
351 await this.doValidateCreateFile(resource, options);
356 return true;
357 }
359 > private async doValidateCreateFile(resource: URI, options?: ICreateFileOptions): Promise<void> {
360
361 // validate overwrite
364 }
365 }
367 > async createFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream = VSBuffer.fromString(''), options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
368
369 // validate
378 return fileStat;
379 }
381 > async writeFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise<IFileStatWithMetadata> {
382 > const provider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(resource), resource); fileService.ts
383 > const { providerExtUri } = this.getExtUri(provider);
384 >
385 > let writeFileOptions = options;
386 > if (hasFileAtomicWriteCapability(provider) && !writeFileOptions?.atomic) {
387 const enforcedAtomicWrite = provider.enforceAtomicWriteFile?.(resource);
388 if (enforcedAtomicWrite) {
390 }
391 }
393 > try {
394 >
395 > // validate write (this may already return a peeked-at buffer)
396 > let { stat, buffer: bufferOrReadableOrStreamOrBufferedStream } = await this.validateWriteFile(provider, resource, bufferOrReadableOrStream, writeFileOptions);
397 >
398 > // mkdir recursively as needed
399 > if (!stat) {
400 > await this.mkdirp(provider, providerExtUri.dirname(resource)); fileService.ts
401 > }
403 > // optimization: if the provider has unbuffered write capability and the data
404 > // to write is not a buffer, we consume up to 3 chunks and try to write the data
405 > // unbuffered to reduce the overhead. If the stream or readable has more data
406 > // to provide we continue to write buffered.
407 > if (!bufferOrReadableOrStreamOrBufferedStream) {
408 > bufferOrReadableOrStreamOrBufferedStream = await this.peekBufferForWriting(provider, bufferOrReadableOrStream);
409 > }
410 >
411 > // write file: unbuffered
412 > if (
413 > !hasOpenReadWriteCloseCapability(provider) || // buffered writing is unsupported
414 > (hasReadWriteCapability(provider) && bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer) || // data is a full buffer already
415 (hasReadWriteCapability(provider) && hasFileAtomicWriteCapability(provider) && writeFileOptions?.atomic) // atomic write forces unbuffered write if the provider supports it
416 > ) { fileService.ts
417 > await this.doWriteUnbuffered(provider, resource, writeFileOptions, bufferOrReadableOrStreamOrBufferedStream);
418 > }
419
420 // write file: buffered
422 await this.doWriteBuffered(provider, resource, writeFileOptions, bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStreamOrBufferedStream) : bufferOrReadableOrStreamOrBufferedStream);
423 }
425 > // events
426 > this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.WRITE));
427 > } catch (error) {
428 throw new FileOperationError(localize('err.write', "Unable to write file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), writeFileOptions);
429 }
431 > return this.resolve(resource, { resolveMetadata: true });
432 > }
434 >
435 > private async peekBufferForWriting(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream): Promise<VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream> {
436 > let peekResult: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream; fileService.ts
437 > if (hasReadWriteCapability(provider) && !(bufferOrReadableOrStream instanceof VSBuffer)) {
438 if (isReadableStream(bufferOrReadableOrStream)) {
439 const bufferedStream = await peekStream(bufferOrReadableOrStream, 3);
446 peekResult = peekReadable(bufferOrReadableOrStream, data => VSBuffer.concat(data), 3);
447 }
448 > } else { fileService.ts
449 > peekResult = bufferOrReadableOrStream;
450 > }
451 >
452 > return peekResult;
453 > }
455 > private async validateWriteFile(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise<{ stat: IStat | undefined; buffer: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream | undefined }> {
457 > // Validate unlock support
458 > const unlock = !!options?.unlock;
459 > if (unlock && !(provider.capabilities & FileSystemProviderCapabilities.FileWriteUnlock)) {
460 throw new Error(localize('writeFailedUnlockUnsupported', "Unable to unlock file '{0}' because provider does not support it.", this.resourceForError(resource)));
461 }
463 > // Validate append support
464 > if (options?.append && !hasFileAppendCapability(provider)) {
465 throw new FileOperationError(localize('err.noAppend', "Filesystem provider for scheme '{0}' does not does not support append", this.resourceForError(resource)), FileOperationResult.FILE_PERMISSION_DENIED);
466 }
468 > // Validate atomic support
469 > const atomic = !!options?.atomic;
470 > if (atomic) {
471 if (!(provider.capabilities & FileSystemProviderCapabilities.FileAtomicWrite)) {
472 throw new Error(localize('writeFailedAtomicUnsupported1', "Unable to atomically write file '{0}' because provider does not support it.", this.resourceForError(resource)));
481 }
482 }
484 > // Validate via file stat meta data
485 > let stat: IStat | undefined = undefined;
486 > try {
487 > stat = await provider.stat(resource);
488 > } catch (error) {
489 > return Object.create(null); // file might not exist fileService.ts
490 > }
491
492 // File cannot be directory
516 let buffer: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream | undefined;
517 if (
518 > typeof options?.mtime === 'number' && typeof options.etag === 'string' && options.etag !== ETAG_DISABLED && fileService.ts
519 > typeof stat.mtime === 'number' && typeof stat.size === 'number' &&
520 > options.mtime < stat.mtime && options.etag !== etag({ mtime: options.mtime /* not using stat.mtime for a reason, see above */, size: stat.size })
521 > ) {
522 buffer = await this.peekBufferForWriting(provider, bufferOrReadableOrStream);
523 if (buffer instanceof VSBuffer && buffer.byteLength === stat.size) {
536
537 return { stat, buffer };
538 > } fileService.ts
540 > async readFile(resource: URI, options?: IReadFileOptions, token?: CancellationToken): Promise<IFileContent> {
541 const provider = await this.withReadProvider(resource);
542
547 return this.doReadFile(provider, resource, options, token);
548 }
550 > private async doReadFileAtomic(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions, token?: CancellationToken): Promise<IFileContent> {
551 return new Promise<IFileContent>((resolve, reject) => {
552 this.writeQueue.queueFor(resource, async () => {
560 });
561 }
563 > private async doReadFile(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions, token?: CancellationToken): Promise<IFileContent> {
564 const stream = await this.doReadFileStream(provider, resource, {
565 ...options,
577 };
578 }
580 > async readFileStream(resource: URI, options?: IReadFileStreamOptions, token?: CancellationToken): Promise<IFileStreamContent> {
581 const provider = await this.withReadProvider(resource);
582
583 return this.doReadFileStream(provider, resource, options, token);
584 }
586 > private async doReadFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions & IReadFileStreamOptions & { preferUnbuffered?: boolean }, token?: CancellationToken): Promise<IFileStreamContent> {
587
588 // install a cancellation token that gets cancelled
661 }
662 }
664 > private restoreReadError(error: Error, resource: URI, options?: IReadFileStreamOptions): FileOperationError {
665 const message = localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString());
666
675 return new FileOperationError(message, toFileOperationResult(error), options);
676 }
678 > private readFileStreamed(provider: IFileSystemProviderWithFileReadStreamCapability, resource: URI, token: CancellationToken, options: IReadFileStreamOptions = Object.create(null)): VSBufferReadableStream {
679 const fileStream = provider.readFileStream(resource, options, token);
680
684 }, data => VSBuffer.concat(data));
685 }
687 > private readFileBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, token: CancellationToken, options: IReadFileStreamOptions = Object.create(null)): VSBufferReadableStream {
688 const stream = newWriteableBufferStream();
689
696 return stream;
697 }
699 > private readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithFileAtomicReadCapability, resource: URI, options?: IReadFileOptions & IReadFileStreamOptions): VSBufferReadableStream {
700 const stream = newWriteableStream<VSBuffer>(data => VSBuffer.concat(data));
701
734 return stream;
735 }
737 > private async validateReadFile(resource: URI, options?: IReadFileStreamOptions): Promise<IFileStatWithMetadata> {
738 const stat = await this.resolve(resource, { resolveMetadata: true });
739
753 return stat;
754 }
756 > private validateReadFileLimits(resource: URI, size: number, options?: IReadFileStreamOptions): void {
757 if (typeof options?.limits?.size === 'number' && size > options.limits.size) {
758 throw new TooLargeFileOperationError(localize('fileTooLargeError', "Unable to read file '{0}' that is too large to open", this.resourceForError(resource)), FileOperationResult.FILE_TOO_LARGE, size, options);
759 }
760 }
762 > //#endregion
763 >
764 > //#region Move/Copy/Delete/Create Folder
765 >
766 > async canMove(source: URI, target: URI, overwrite?: boolean): Promise<Error | true> {
767 return this.doCanMoveCopy(source, target, 'move', overwrite);
768 }
770 > async canCopy(source: URI, target: URI, overwrite?: boolean): Promise<Error | true> {
771 return this.doCanMoveCopy(source, target, 'copy', overwrite);
772 }
774 > private async doCanMoveCopy(source: URI, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<Error | true> {
775 if (source.toString() !== target.toString()) {
776 try {
786 return true;
787 }
789 > async move(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
790 const sourceProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(source), source);
791 const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target);
800 return fileStat;
801 }
803 > async copy(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
804 const sourceProvider = await this.withReadProvider(source);
805 const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target);
814 return fileStat;
815 }
817 > private async doMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite: boolean): Promise<'move' | 'copy'> {
818 if (source.toString() === target.toString()) {
819 return mode; // simulate node.js behaviour here and do a no-op if paths match
872 }
873 }
875 > private async doCopyFile(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI): Promise<void> {
876
877 // copy: source (buffered) => target (buffered)
895 }
896 }
898 > private async doCopyFolder(sourceProvider: IFileSystemProvider, sourceFolder: IFileStat, targetProvider: IFileSystemProvider, targetFolder: URI): Promise<void> {
899
900 // create folder in target
913 }
914 }
916 > private async doValidateMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<{ exists: boolean; isSameResourceWithDifferentPathCase: boolean }> {
917 let isSameResourceWithDifferentPathCase = false;
918
954 return { exists, isSameResourceWithDifferentPathCase };
955 }
957 > private getExtUri(provider: IFileSystemProvider): { providerExtUri: IExtUri; isPathCaseSensitive: boolean } {
958 > const isPathCaseSensitive = this.isPathCaseSensitive(provider); fileService.ts
959 >
960 > return {
961 > providerExtUri: isPathCaseSensitive ? extUri : extUriIgnorePathCase,
962 > isPathCaseSensitive
963 > };
964 > }
966 > private isPathCaseSensitive(provider: IFileSystemProvider): boolean {
967 > return !!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); fileService.ts
968 > }
970 > async createFolder(resource: URI): Promise<IFileStatWithMetadata> {
971 > const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource); fileService.ts
972 >
973 > // mkdir recursively
974 > await this.mkdirp(provider, resource);
975 >
976 > // events
977 > const fileStat = await this.resolve(resource, { resolveMetadata: true });
978 > this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat));
979 >
980 > return fileStat;
981 > }
983 > private async mkdirp(provider: IFileSystemProvider, directory: URI): Promise<void> {
984 > const directoriesToCreate: string[] = []; fileService.ts
985 >
986 > // mkdir until we reach root
987 > const { providerExtUri } = this.getExtUri(provider);
988 > while (!providerExtUri.isEqual(directory, providerExtUri.dirname(directory))) {
989 > try { fileService.ts
990 > const stat = await provider.stat(directory);
991 > if ((stat.type & FileType.Directory) === 0) { fileService.ts
992 throw new Error(localize('mkdirExistsError', "Unable to create folder '{0}' that already exists but is not a directory", this.resourceForError(directory)));
993 }
995 > break; // we have hit a directory that exists -> good
996 > } catch (error) { fileService.ts
997 >
998 > // Bubble up any other error that is not file not found
999 > if (toFileSystemProviderErrorCode(error) !== FileSystemProviderErrorCode.FileNotFound) {
1000 throw error;
1001 }
1003 > // Upon error, remember directories that need to be created
1004 > directoriesToCreate.push(providerExtUri.basename(directory));
1005 >
1006 > // Continue up
1007 > directory = providerExtUri.dirname(directory);
1008 > }
1009 > }
1011 > // Create directories as needed
1012 > for (let i = directoriesToCreate.length - 1; i >= 0; i--) {
1013 > directory = providerExtUri.joinPath(directory, directoriesToCreate[i]); fileService.ts
1014 >
1015 > try {
1016 > await provider.mkdir(directory);
1017 > } catch (error) {
1018 if (toFileSystemProviderErrorCode(error) !== FileSystemProviderErrorCode.FileExists) {
1019 // For mkdirp() we tolerate that the mkdir() call fails
1028 }
1029 }
1030 > } fileService.ts
1031 > } fileService.ts
1033 > async canDelete(resource: URI, options?: Partial<IFileDeleteOptions>): Promise<Error | true> {
1034 try {
1035 await this.doValidateDelete(resource, options);
1040 return true;
1041 }
1043 > private async doValidateDelete(resource: URI, options?: Partial<IFileDeleteOptions>): Promise<IFileSystemProvider> {
1044 const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource);
1045
1085 return provider;
1086 }
1088 > async del(resource: URI, options?: Partial<IFileDeleteOptions>): Promise<void> {
1089 const provider = await this.doValidateDelete(resource, options);
1090
1107 this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE));
1108 }
1110 > //#endregion
1111 >
1112 > //#region Clone File
1113 >
1114 > async cloneFile(source: URI, target: URI): Promise<void> {
1115 const sourceProvider = await this.withProvider(source);
1116 const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target);
1142 return this.writeQueue.queueFor(source, () => this.doCopyFile(sourceProvider, source, targetProvider, target), this.getExtUri(sourceProvider).providerExtUri);
1143 }
1145 > //#endregion
1146 >
1147 > //#region File Watching
1148 >
1149 > private readonly internalOnDidFilesChange = this._register(new Emitter<FileChangesEvent>());
1150 >
1151 > private readonly _onDidUncorrelatedFilesChange = this._register(new Emitter<FileChangesEvent>());
1152 > readonly onDidFilesChange = this._onDidUncorrelatedFilesChange.event; // global `onDidFilesChange` skips correlated events
1153 >
1154 > private readonly _onDidWatchError = this._register(new Emitter<Error>());
1155 > readonly onDidWatchError = this._onDidWatchError.event;
1156 >
1157 > private readonly activeWatchers = new Map<number /* watch request hash */, { disposable: IDisposable; count: number }>();
1158 >
1159 > private static WATCHER_CORRELATION_IDS = 0;
1160 >
1161 > createWatcher(resource: URI, options: IWatchOptionsWithoutCorrelation & { recursive: false }): IFileSystemWatcher {
1162 return this.watch(resource, {
1163 ...options,
1168 });
1169 }
1171 > watch(resource: URI, options: IWatchOptionsWithCorrelation): IFileSystemWatcher;
1172 > watch(resource: URI, options?: IWatchOptionsWithoutCorrelation): IDisposable;
1173 > watch(resource: URI, options: IWatchOptions = { recursive: false, excludes: [] }): IFileSystemWatcher | IDisposable {
1174 const disposables = new DisposableStore();
1175
1215 return disposables;
1216 }
1218 > private async doWatch(resource: URI, options: IWatchOptions): Promise<IDisposable> {
1219 const provider = await this.withProvider(resource);
1220
1248 });
1249 }
1251 > override dispose(): void {
1252 > super.dispose(); fileService.ts
1253 >
1254 > for (const [, watcher] of this.activeWatchers) {
1255 dispose(watcher.disposable);
1256 }
1258 > this.activeWatchers.clear();
1259 > }
1261 > //#endregion
1262 >
1263 > //#region Helpers
1264 >
1265 > private readonly writeQueue = this._register(new ResourceQueue());
1267 > private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, options: IWriteFileOptions | undefined, readableOrStreamOrBufferedStream: VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
1268 return this.writeQueue.queueFor(resource, async () => {
1269
1287 }, this.getExtUri(provider).providerExtUri);
1288 }
1290 > private async doWriteStreamBufferedQueued(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, streamOrBufferedStream: VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
1291 let posInFile = 0;
1292 let stream: VSBufferReadableStream;
1341 });
1342 }
1344 > private async doWriteReadableBufferedQueued(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, readable: VSBufferReadable): Promise<void> {
1345 let posInFile = 0;
1346
1352 }
1353 }
1355 > private async doWriteBuffer(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, buffer: VSBuffer, length: number, posInFile: number, posInBuffer: number): Promise<void> {
1356 let totalBytesWritten = 0;
1357 while (totalBytesWritten < length) {
1362 }
1363 }
1365 > private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options: IWriteFileOptions | undefined, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
1366 > return this.writeQueue.queueFor(resource, () => this.doWriteUnbufferedQueued(provider, resource, options, bufferOrReadableOrStreamOrBufferedStream), this.getExtUri(provider).providerExtUri); fileService.ts
1367 > }
1369 > private async doWriteUnbufferedQueued(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options: IWriteFileOptions | undefined, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
1370 > let buffer: VSBuffer; fileService.ts
1371 > if (bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer) {
1372 > buffer = bufferOrReadableOrStreamOrBufferedStream;
1373 > } else if (isReadableStream(bufferOrReadableOrStreamOrBufferedStream)) {
1374 buffer = await streamToBuffer(bufferOrReadableOrStreamOrBufferedStream);
1375 } else if (isReadableBufferedStream(bufferOrReadableOrStreamOrBufferedStream)) {
1378 buffer = readableToBuffer(bufferOrReadableOrStreamOrBufferedStream);
1379 }
1381 > // Write through the provider
1382 > await provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true, unlock: options?.unlock ?? false, atomic: options?.atomic ?? false, append: options?.append ?? false });
1383 > }
1385 > private async doPipeBuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
1386 return this.writeQueue.queueFor(target, () => this.doPipeBufferedQueued(sourceProvider, source, targetProvider, target), this.getExtUri(targetProvider).providerExtUri);
1387 }
1389 > private async doPipeBufferedQueued(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
1390 let sourceHandle: number | undefined = undefined;
1391 let targetHandle: number | undefined = undefined;
1428 }
1429 }
1431 > private async doPipeUnbuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {
1432 return this.writeQueue.queueFor(target, () => this.doPipeUnbufferedQueued(sourceProvider, source, targetProvider, target), this.getExtUri(targetProvider).providerExtUri);
1433 }
1435 > private async doPipeUnbufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {
1436 return targetProvider.writeFile(target, await sourceProvider.readFile(source), { create: true, overwrite: true, unlock: false, atomic: false });
1437 }
1439 > private async doPipeUnbufferedToBuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
1440 return this.writeQueue.queueFor(target, () => this.doPipeUnbufferedToBufferedQueued(sourceProvider, source, targetProvider, target), this.getExtUri(targetProvider).providerExtUri);
1441 }
1443 > private async doPipeUnbufferedToBufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
1444
1445 // Open handle
1456 }
1457 }
1459 > private async doPipeBufferedToUnbuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {
1460
1461 // Read buffer via stream buffered
1465 await this.doWriteUnbuffered(targetProvider, target, undefined, buffer);
1466 }
1468 > protected throwIfFileSystemIsReadonly<T extends IFileSystemProvider>(provider: T, resource: URI): T {
1469 > if (provider.capabilities & FileSystemProviderCapabilities.Readonly) { fileService.ts
1470 throw new FileOperationError(localize('err.readonly', "Unable to modify read-only file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_PERMISSION_DENIED);
1471 }
1473 > return provider;
1474 > }
1476 > private throwIfFileIsReadonly(resource: URI, stat: IStat): void {
1477 if ((stat.permissions ?? 0) & FilePermission.Readonly) {
1478 throw new FileOperationError(localize('err.readonly', "Unable to modify read-only file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_PERMISSION_DENIED);
1479 }
1480 }
1482 > private resourceForError(resource: URI): string {
1483 if (resource.scheme === Schemas.file) {
1484 return resource.fsPath;
1487 return resource.toString(true);
1488 }
1490 > //#endregion
1491 > }
src/vs/platform/agentHost/node/shared/sessionServerTools.ts 506 covered LOC · 50 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- sessionServerTools.ts
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 > import { URI } from '../../../../base/common/uri.js';
7 > import type { Mutable } from '../../../../base/common/types.js';
8 > import { localize } from '../../../../nls.js';
9 > import type { IAgentCreateSessionConfig, IAgentModelInfo, IAgentSessionMetadata } from '../../common/agentService.js';
10 > import { SessionStatus } from '../../common/state/protocol/channels-session/state.js';
11 > import { buildChatUri, buildDefaultChatUri, parseChatUri, readSessionGitState, readSessionGitHubState, ResponsePartKind, ToolCallStatus, TurnState, type Message, type ResponsePart, type ToolCallState, type ToolDefinition, type StringOrMarkdown, type Turn, type URI as ProtocolURI } from '../../common/state/sessionState.js';
12 > import { buildOpenSessionLinkUri, CREATE_CHAT_TOOL_NAME, CREATE_SESSION_TOOL_NAME, parseOpenSessionLinkChatId, parseOpenSessionLinkUri, SEND_MESSAGE_TOOL_NAME } from '../../common/openSessionLink.js';
13 > import { generateUuid } from '../../../../base/common/uuid.js';
14 > import type { AgentHostStateManager } from '../agentHostStateManager.js';
15 > import type { IServerToolDisplay, IServerToolDisplayResult, IServerToolGroup } from './agentServerToolHost.js';
16 >
17 > export const listSessionsToolName = 'list_sessions';
18 > export const getCurrentSessionToolName = 'get_current_session';
19 > export const createSessionToolName = CREATE_SESSION_TOOL_NAME;
20 > export const createChatToolName = CREATE_CHAT_TOOL_NAME;
21 > export const sendMessageToolName = SEND_MESSAGE_TOOL_NAME;
22 > export const getSessionContextToolName = 'get_session_context';
23 > export const deleteSessionToolName = 'delete_session';
24 >
25 > /**
26 > * Maximum `create_session` recursion depth. A user/top-level session is depth 0;
27 > * a session created by `create_session` from within a depth-N session is depth
28 > * N+1. Once a session reaches this depth, its agent may not create further
29 > * sessions — this bounds recursive spawn *chains* (A→B→C→…). Breadth is bounded
30 > * separately by {@link maxCreatedSessions} plus the per-call user confirmation.
31 > */
32 > const maxSessionSpawnDepth = 3;
33 >
34 > /** Process-wide backstop against runaway spawning (breadth), independent of depth. */
35 > const maxCreatedSessions = 25;
36 > const maxCreatedChats = 25;
37 >
38 > /** Process-wide backstop against runaway `send_message` fan-out. */
39 > const maxSentMessages = 50;
40 >
41 > const sessionConfirmationToolNames: ReadonlySet<string> = new Set([createSessionToolName, createChatToolName, sendMessageToolName, deleteSessionToolName]);
42 >
43 > /** Whether the given session server tool requires user confirmation before it runs. */
44 > export function sessionToolRequiresConfirmation(toolName: string): boolean {
45 return sessionConfirmationToolNames.has(toolName);
46 }
48 > const listSessionsStatusValues = ['idle', 'inProgress', 'inputNeeded', 'error', 'archived'] as const;
49 >
50 > const listSessionsInputSchema: ToolDefinition['inputSchema'] = {
51 > type: 'object',
52 > properties: {
53 > session: { type: 'string', description: 'Return only the session with this URI or `agent-host-session://` link (a direct lookup that ignores the other filters). Use this to fetch one known session\'s metadata.' },
54 > status: {
55 > type: 'array',
56 > items: { type: 'string', enum: [...listSessionsStatusValues] },
57 > description: 'Only return sessions whose status matches one of these (e.g. `inputNeeded` for sessions awaiting a reply, `inProgress` for running ones, `archived` for sessions marked Done/completed — implies `includeArchived`). Omit to return every status.',
58 > },
59 > workspace: { type: 'string', description: 'Only return sessions whose working directory is this folder — an absolute path or a workspace URI.' },
60 > withChanges: { type: 'boolean', description: 'When true, only return sessions that have pending worktree changes.' },
61 > unread: { type: 'boolean', description: 'When true, only return sessions with updates the user has not seen yet.' },
62 > withPullRequest: { type: 'boolean', description: 'When true, only return sessions that have a linked GitHub pull request.' },
63 > includeArchived: { type: 'boolean', description: 'Whether to include archived sessions. Defaults to false; set true to also return archived sessions.' },
64 > createdAfter: { type: 'string', description: 'Only return sessions created at or after this time (ISO-8601 timestamp, e.g. `2025-01-31T00:00:00Z`).' },
65 > createdBefore: { type: 'string', description: 'Only return sessions created at or before this time (ISO-8601 timestamp).' },
66 > },
67 > };
68 >
69 > const createSessionInputSchema: ToolDefinition['inputSchema'] = {
70 > type: 'object',
71 > properties: {
72 > workspace: { type: 'string', description: 'Absolute folder path, workspace URI, or a working directory from an existing session.' },
73 > prompt: { type: 'string', description: 'Initial prompt to send to the new session.' },
74 > model: { type: 'string', description: 'Optional model ID or display name.' },
75 > },
76 > required: ['workspace', 'prompt'],
77 > };
78 >
79 > const getCurrentSessionInputSchema: ToolDefinition['inputSchema'] = {
80 > type: 'object',
81 > properties: {},
82 > };
83 >
84 > const createChatInputSchema: ToolDefinition['inputSchema'] = {
85 > type: 'object',
86 > properties: {
87 > session: { type: 'string', description: 'Optional session to add the chat to: a session URI from `list_sessions` or an `agent-host-session://` link. Defaults to the current session when omitted.' },
88 > prompt: { type: 'string', description: 'Initial prompt to send to the new chat.' },
89 > title: { type: 'string', description: 'Optional title for the new chat.' },
90 > model: { type: 'string', description: 'Optional model ID or display name. Defaults to the session\'s model.' },
91 > },
92 > required: ['prompt'],
93 > };
94 >
95 > const deleteSessionInputSchema: ToolDefinition['inputSchema'] = {
96 > type: 'object',
97 > properties: {
98 > session: { type: 'string', description: 'The session to delete: a session URI from `list_sessions` or an `agent-host-session://` link (e.g. from `create_session`).' },
99 > },
100 > required: ['session'],
101 > };
102 >
103 > const sendMessageInputSchema: ToolDefinition['inputSchema'] = {
104 > type: 'object',
105 > properties: {
106 > session: { type: 'string', description: 'The session or chat to message: a session URI from `list_sessions`, or an `agent-host-session://` link (from `create_session`/`create_chat`; a `create_chat` link targets that specific chat).' },
107 > message: { type: 'string', description: 'The message to send.' },
108 > },
109 > required: ['session', 'message'],
110 > };
111 >
112 > const sessionContextDetailValues = ['summary', 'digest', 'full'] as const;
113 >
114 > const getSessionContextInputSchema: ToolDefinition['inputSchema'] = {
115 > type: 'object',
116 > properties: {
117 > session: { type: 'string', description: 'The session or chat to read: a session URI from `list_sessions`, or an `agent-host-session://` link (a `create_chat` link targets that specific chat).' },
118 > detail: {
119 > type: 'string',
120 > enum: [...sessionContextDetailValues],
121 > description: 'How much conversation detail to return. `summary` (default): status and a short per-turn gist (the message plus a compact snippet of the reply). `digest`: adds the full assistant reply text and tool-call names. `full`: adds tool-call inputs. Higher levels return more tokens.',
122 > },
123 > transcriptLimit: { type: 'number', description: 'Maximum number of most-recent turns to include. Defaults to 10; capped at 50.' },
124 > },
125 > required: ['session'],
126 > };
127 >
128 > /** Protocol tool definitions for the session-management server tools. */
129 > export const sessionServerToolDefinitions: ToolDefinition[] = [
130 > {
131 > name: listSessionsToolName,
132 > title: 'List Sessions',
133 > description: 'List sessions and their compact metadata (status, activity, working directory, project, worktree changes, git/GitHub info, timestamps). Pass `session` to fetch a single known session by URI. By default archived sessions are omitted. Optionally filter by `status`, `workspace`, `withChanges`, `unread`, `withPullRequest`, `includeArchived`, `createdAfter`, or `createdBefore`.',
134 > inputSchema: listSessionsInputSchema,
135 > annotations: { readOnlyHint: true },
136 > },
137 > {
138 > name: getCurrentSessionToolName,
139 > title: 'Get Current Session',
140 > description: 'Get metadata and the open link for the session this conversation is running in. Use this to reference the current session (for example before adding a chat to it).',
141 > inputSchema: getCurrentSessionInputSchema,
142 > annotations: { readOnlyHint: true },
143 > },
144 > {
145 > name: createSessionToolName,
146 > title: 'Create Session',
147 > description: 'Create a session in a workspace and start it with an initial prompt. The UI shows a "Session Created" confirmation with a button to open it, so reply with a single short sentence confirming the session was created and do NOT print the session URL or tell the user to click a button.',
148 > inputSchema: createSessionInputSchema,
149 > annotations: { readOnlyHint: false },
150 > },
151 > {
152 > name: createChatToolName,
153 > title: 'Create Chat',
154 > description: 'Add a new chat to an existing session and start it with an initial prompt. Omit `session` to add the chat to the current session; otherwise pass a session URI from `list_sessions`. Optionally pass a `model` to use for the chat (defaults to the session\'s model). The UI shows a "Chat Created" confirmation with a button to open the session, so reply with a single short sentence and do NOT print the session URL or tell the user to click a button.',
155 > inputSchema: createChatInputSchema,
156 > annotations: { readOnlyHint: false },
157 > },
158 > {
159 > name: sendMessageToolName,
160 > title: 'Send Message',
161 > description: 'Send a message to an existing session or chat, starting a new turn there. Provide a session URI from `list_sessions` or an `agent-host-session://` link (a `create_chat` link targets that specific chat). The message is delivered asynchronously — this tool does not wait for or return the reply. The UI shows a confirmation with a button to open the target, so reply with a single short sentence and do NOT print the URL or tell the user to click a button.',
162 > inputSchema: sendMessageInputSchema,
163 > annotations: { readOnlyHint: false },
164 > },
165 > {
166 > name: getSessionContextToolName,
167 > title: 'Get Session Context',
168 > description: 'Read the recent conversation of an existing session or chat: a compacted transcript of its turns (messages, replies, and tool calls). Use this to see what a session you created is doing, or to gather context before sending it a message. Returns a compacted summary by default (`detail: "summary"`); request `digest` or `full` for more detail. For session metadata (status, working directory, changes, …) use `list_sessions` with the `session` argument.',
169 > inputSchema: getSessionContextInputSchema,
170 > annotations: { readOnlyHint: true },
171 > },
172 > {
173 > name: deleteSessionToolName,
174 > title: 'Delete Session',
175 > description: 'Permanently delete a session (identified by a session URI from `list_sessions`), including its stored data. This cannot be undone. Refuses to delete the current session.',
176 > inputSchema: deleteSessionInputSchema,
177 > annotations: { readOnlyHint: false, destructiveHint: true },
178 > },
179 > ];
180 >
181 > /** Resolves the owning backend session URI for the channel a tool call runs on. */
182 > export function currentSessionUri(toolCallChannel: ProtocolURI): URI {
183 const owning = parseChatUri(toolCallChannel) ?? undefined;
184 return URI.parse(owning?.session ?? toolCallChannel);
185 }
187 > interface ICreateSessionArgs {
188 > readonly workspace?: unknown;
189 > readonly prompt?: unknown;
190 > readonly model?: unknown;
191 > }
192 >
193 > export interface IResolvedCreateSessionArgs {
194 > readonly workspace: URI;
195 > readonly prompt: string;
196 > readonly model?: IAgentModelInfo;
197 > }
198 >
199 > /** Minimal dependency surface needed by the session server-tool group. */
200 > export interface ISessionServerToolAccessor {
201 > readonly listSessions: () => Promise<readonly IAgentSessionMetadata[]>;
202 > readonly createSession: (config: IAgentCreateSessionConfig) => Promise<URI>;
203 > readonly getModels: () => readonly IAgentModelInfo[];
204 > readonly startPrompt: (session: URI, chat: URI, prompt: string) => Promise<void>;
205 > readonly createChat: (session: URI, chat: URI, options?: { title?: string; model?: IAgentModelInfo }) => Promise<void>;
206 > readonly deleteSession: (session: URI) => Promise<void>;
207 > /** Reads a point-in-time snapshot of a session's chat conversation (default chat, or a specific chat by id). */
208 > readonly getChatContext: (session: URI, chatId?: string) => IChatContextSnapshot | undefined;
209 > /** The spawn depth of a session (0 for a user/top-level session, N for one created N levels deep by `create_session`). */
210 > readonly getSessionSpawnDepth: (session: URI) => number;
211 > /** Records the spawn depth of a freshly-created session so its own `create_session` calls can enforce the recursion limit. */
212 > readonly setSessionSpawnDepth: (session: URI, depth: number) => void;
213 > }
214 >
215 > /** Point-in-time snapshot of a chat's conversation, read from the host state. */
216 > export interface IChatContextSnapshot {
217 > /** Completed turns, oldest first. */
218 > readonly turns: readonly Turn[];
219 > /** The in-progress turn, if the chat is mid-response. */
220 > readonly activeTurn?: Pick<Turn, 'message' | 'responseParts'>;
221 > /** `true` when older completed turns exist beyond the in-memory window. */
222 > readonly hasMoreHistory: boolean;
223 > }
224 >
225 > interface ISerializedGitState {
226 > readonly branch?: string;
227 > readonly baseBranch?: string;
228 > readonly upstreamBranch?: string;
229 > readonly ahead?: number;
230 > readonly behind?: number;
231 > readonly uncommittedChanges?: number;
232 > }
233 >
234 > interface ISerializedGitHubState {
235 > readonly owner?: string;
236 > readonly repo?: string;
237 > readonly pullRequestUrl?: string;
238 > }
239 >
240 > interface ISerializedSession {
241 > readonly session: string;
242 > readonly title?: string;
243 > readonly status?: string;
244 > /** Human-readable description of what the session is currently doing. */
245 > readonly activity?: string;
246 > readonly workingDirectory?: string;
247 > /** Display name of the session's project/workspace. */
248 > readonly project?: string;
249 > /** `true` when the session has updates the user has not yet seen. */
250 > readonly unread?: boolean;
251 > /** ISO-8601 timestamp of when the session was created. */
252 > readonly createdAt?: string;
253 > /** ISO-8601 timestamp of the session's last activity. */
254 > readonly modifiedAt?: string;
255 > readonly changes?: IAgentSessionMetadata['changes'];
256 > readonly changesets?: readonly {
257 > readonly label: string;
258 > readonly changeKind: string;
259 > readonly uriTemplate: string;
260 > readonly description?: string;
261 > }[];
262 > readonly git?: ISerializedGitState;
263 > readonly github?: ISerializedGitHubState;
264 > }
265 >
266 function getRequiredString(value: unknown, field: string, toolName: string): string {
267 if (typeof value !== 'string' || value.length === 0) {
270 return value;
271 }
273 function getOptionalString(value: unknown, field: string, toolName: string): string | undefined {
274 if (value === undefined) {
280 return value;
281 }
283 function parseWorkspaceUri(workspace: string): URI | undefined {
284 // Absolute filesystem path (POSIX `/…` or Windows `C:\…` / `\\share`).
293 }
294 }
296 function resolveWorkspace(workspace: string, sessions: readonly IAgentSessionMetadata[]): URI {
297 const matchingSession = sessions.find(session =>
306 return parsed;
307 }
309 function resolveModel(modelName: string | undefined, models: readonly IAgentModelInfo[]): IAgentModelInfo | undefined {
310 if (modelName === undefined) {
317 return model;
318 }
320 > /** Validates and resolves create-session arguments against current sessions and models. */
321 > export function getCreateSessionArgs(rawArgs: unknown, sessions: readonly IAgentSessionMetadata[], models: readonly IAgentModelInfo[]): IResolvedCreateSessionArgs {
322 const args = (rawArgs ?? {}) as ICreateSessionArgs;
323 const workspace = getRequiredString(args.workspace, 'workspace', createSessionToolName);
330 };
331 }
333 > /** Decodes the {@link SessionStatus} bit-flags into readable names for the agent. */
334 function describeSessionStatusBits(status: SessionStatus): string[] {
335 const names: string[] = [];
351 return names;
352 }
354 > /**
355 > * Decodes a session's status into readable names, used by both filtering and
356 > * serialization so they agree on which sessions are considered `archived`.
357 > * This combines the {@link SessionStatus} bit-flags with the `isArchived`
358 > * metadata flag (see {@link sessionIsArchived}), since a session can be
359 > * archived through either mechanism.
360 > */
361 function describeSessionStatusNames(session: IAgentSessionMetadata): string[] {
362 const names = session.status !== undefined ? describeSessionStatusBits(session.status) : [];
366 return names;
367 }
369 > /** Renders a session's status names as the compact string used in tool results. */
370 function describeSessionStatus(session: IAgentSessionMetadata): string | undefined {
371 const names = describeSessionStatusNames(session);
375 return session.status !== undefined ? 'unknown' : undefined;
376 }
378 >
379 > /** Filters accepted by `list_sessions` to narrow the returned set. */
380 > export interface IListSessionsArgs {
381 > /** Direct lookup: return only the session with this URI / open link, ignoring all other filters. */
382 > readonly session?: string;
383 > readonly status?: ReadonlySet<string>;
384 > readonly workspace?: string;
385 > readonly withChanges?: boolean;
386 > readonly unread?: boolean;
387 > readonly withPullRequest?: boolean;
388 > readonly includeArchived?: boolean;
389 > /** Lower bound on session creation time, in epoch milliseconds. */
390 > readonly createdAfter?: number;
391 > /** Upper bound on session creation time, in epoch milliseconds. */
392 > readonly createdBefore?: number;
393 > }
394 >
395 function getOptionalBoolean(value: unknown, field: string, toolName: string): boolean | undefined {
396 if (value === undefined) {
402 return value;
403 }
405 function getOptionalTimestamp(value: unknown, field: string, toolName: string): number | undefined {
406 if (value === undefined) {
416 return parsed;
417 }
419 > /** Validates and normalizes the optional `list_sessions` filter arguments. */
420 > export function getListSessionsArgs(rawArgs: unknown): IListSessionsArgs {
421 const args = (rawArgs ?? {}) as { session?: unknown; status?: unknown; workspace?: unknown; withChanges?: unknown; unread?: unknown; withPullRequest?: unknown; includeArchived?: unknown; createdAfter?: unknown; createdBefore?: unknown };
422
445 };
446 }
448 > /** Whether a session has any pending worktree changes (insertions, deletions, or changed files). */
449 function sessionHasChanges(session: IAgentSessionMetadata): boolean {
450 const changes = session.changes;
451 return !!changes && ((changes.files ?? 0) > 0 || (changes.additions ?? 0) > 0 || (changes.deletions ?? 0) > 0);
452 }
454 > /** Whether a session is archived (either the metadata flag or the status bit). */
455 function sessionIsArchived(session: IAgentSessionMetadata): boolean {
456 return session.isArchived === true || (session.status !== undefined && (session.status & SessionStatus.IsArchived) !== 0);
457 }
459 > /** Whether a session's working directory matches the given folder (absolute path or URI). */
460 function sessionMatchesWorkspace(session: IAgentSessionMetadata, workspace: string): boolean {
461 const dir = session.workingDirectory;
469 return !!parsed && parsed.toString() === dir.toString();
470 }
472 > /** Applies the {@link IListSessionsArgs} filters to a set of sessions. */
473 > export function filterSessions(sessions: readonly IAgentSessionMetadata[], args: IListSessionsArgs): readonly IAgentSessionMetadata[] {
474 // A direct `session` lookup returns just that session, bypassing the other
475 // filters (including the default archived exclusion).
511 });
512 }
514 function serializeGitState(session: IAgentSessionMetadata): ISerializedGitState | undefined {
515 const git = readSessionGitState(session._meta);
526 return Object.keys(result).length > 0 ? result : undefined;
527 }
529 function serializeGitHubState(session: IAgentSessionMetadata): ISerializedGitHubState | undefined {
530 const github = readSessionGitHubState(session._meta);
538 return Object.keys(result).length > 0 ? result : undefined;
539 }
541 function serializeSession(session: IAgentSessionMetadata): ISerializedSession {
542 const git = serializeGitState(session);
566 };
567 }
569 > /** Serializes session metadata into the compact tool-result JSON payload. */
570 > export function serializeSessions(sessions: readonly IAgentSessionMetadata[]): string {
571 return JSON.stringify({ sessions: sessions.map(serializeSession) });
572 }
574 > export interface ICreateSessionResult {
575 > readonly session: string;
576 > readonly chat: string;
577 > /** Clickable {@link AGENT_HOST_SESSION_LINK_SCHEME} URI that opens the session in the Agents window. */
578 > readonly openLink: string;
579 > }
580 >
581 > /**
582 > * Creates a session, sends its initial prompt, and returns the created channels.
583 > * Enforces the {@link maxSessionSpawnDepth recursion limit} against
584 > * {@link currentSession} (the session the tool runs in) and stamps the new
585 > * session one level deeper so its own `create_session` calls are bounded too.
586 > */
587 export async function applyCreateSessionTool(accessor: ISessionServerToolAccessor, rawArgs: unknown, currentSession?: URI): Promise<ICreateSessionResult> {
588 const parentDepth = currentSession ? accessor.getSessionSpawnDepth(currentSession) : 0;
602 return { session: session.toString(), chat: chat.toString(), openLink: buildOpenSessionLinkUri(session) };
603 }
605 > /**
606 > * Builds the model-facing `create_session` result. Keeps the machine-readable
607 > * `agent-host-session://` link (parsed client-side to render the deterministic
608 > * "Session Created" confirmation + button) but omits the raw backend session
609 > * URI so the model has nothing ugly to echo, and tells it to reply briefly.
610 > */
611 > export function formatCreateSessionResult(result: ICreateSessionResult): string {
612 return `Session created (${result.openLink}). Reply with one short sentence confirming the session was created; do not print the URL or mention a button.`;
613 }
615 > interface ICreateChatArgs {
616 > readonly session?: unknown;
617 > readonly prompt?: unknown;
618 > readonly title?: unknown;
619 > readonly model?: unknown;
620 > }
621 >
622 > export interface ICreateChatResult {
623 > readonly session: string;
624 > readonly chat: string;
625 > /** Clickable {@link AGENT_HOST_SESSION_LINK_SCHEME} URI that opens the created chat. */
626 > readonly openLink: string;
627 > }
628 >
629 > /**
630 > * Resolves a session identifier — accepting either a backend session URI
631 > * (`copilotcli:/…` from `list_sessions`) or an `agent-host-session://…` open
632 > * link (as returned by `create_session`/`get_current_session`) — against the
633 > * set of known sessions. Returns `undefined` when it matches no known session.
634 > */
635 function resolveKnownSession(sessionInput: string, sessions: readonly IAgentSessionMetadata[]): URI | undefined {
636 // Normalize an open-session link back to its backend session URI.
640 return match?.session;
641 }
643 > /** Resolves the target session URI for `create_chat` against the known sessions. */
644 function resolveChatSession(sessionInput: string, sessions: readonly IAgentSessionMetadata[]): URI {
645 const session = resolveKnownSession(sessionInput, sessions);
649 return session;
650 }
652 > /** Validates and resolves create-chat arguments; defaults the session to {@link currentSession} when omitted. */
653 > export function getCreateChatArgs(rawArgs: unknown, sessions: readonly IAgentSessionMetadata[], models: readonly IAgentModelInfo[], currentSession?: URI): { session: URI; prompt: string; title?: string; model?: IAgentModelInfo } {
654 const args = (rawArgs ?? {}) as ICreateChatArgs;
655 const prompt = getRequiredString(args.prompt, 'prompt', createChatToolName);
668 return { session, prompt, ...(title !== undefined ? { title } : {}), ...(model !== undefined ? { model } : {}) };
669 }
671 > /** Adds a chat to a session, sends its initial prompt, and returns the created channels. */
672 export async function applyCreateChatTool(accessor: ISessionServerToolAccessor, rawArgs: unknown, currentSession?: URI): Promise<ICreateChatResult> {
673 const sessions = await accessor.listSessions();
679 return { session: args.session.toString(), chat: chat.toString(), openLink: buildOpenSessionLinkUri(args.session, chatId) };
680 }
682 > /** Builds the model-facing `create_chat` result. */
683 > export function formatCreateChatResult(result: ICreateChatResult): string {
684 return `Chat created (${result.openLink}). Reply with one short sentence confirming the chat was created; do not print the URL or mention a button.`;
685 }
687 > interface ISendMessageArgs {
688 > readonly session?: unknown;
689 > readonly message?: unknown;
690 > }
691 >
692 > export interface IResolvedSendMessageArgs {
693 > /** The owning backend session URI of the target chat. */
694 > readonly session: URI;
695 > /** The chat channel to deliver the message on (default chat, or a specific chat when the link carried one). */
696 > readonly chat: URI;
697 > /** The chat id when a specific chat was targeted (from a `create_chat` link). */
698 > readonly chatId?: string;
699 > readonly message: string;
700 > }
701 >
702 > /**
703 > * Validates and resolves send-message arguments. When the `session` input is a
704 > * `create_chat` open link (carrying a chat id), the message is targeted at that
705 > * specific chat rather than the session's default chat.
706 > */
707 > export function getSendMessageArgs(rawArgs: unknown, sessions: readonly IAgentSessionMetadata[]): IResolvedSendMessageArgs {
708 const args = (rawArgs ?? {}) as ISendMessageArgs;
709 const message = getRequiredString(args.message, 'message', sendMessageToolName);
717 return { session, chat, message, ...(chatId !== undefined ? { chatId } : {}) };
718 }
720 > /**
721 > * Sends a message to an existing session/chat, starting a new turn there.
722 > * Refuses to target {@link currentChannel} (the chat channel the tool runs on)
723 > * to avoid a session trivially messaging itself in a loop.
724 > */
725 export async function applySendMessageTool(accessor: ISessionServerToolAccessor, rawArgs: unknown, currentChannel?: ProtocolURI): Promise<string> {
726 const sessions = await accessor.listSessions();
732 return formatSendMessageResult(buildOpenSessionLinkUri(session, chatId));
733 }
735 > /** Builds the model-facing `send_message` result. */
736 > export function formatSendMessageResult(openLink: string): string {
737 return `Message sent (${openLink}). Reply with one short sentence confirming the message was sent; do not print the URL or mention a button.`;
738 }
740 > // --- get_session_context -----------------------------------------------------
741 >
742 > type SessionContextDetail = (typeof sessionContextDetailValues)[number];
743 >
744 > const defaultTranscriptLimit = 10;
745 > const maxTranscriptLimit = 50;
746 >
747 > /** Per-detail truncation caps (characters); a value of 0 omits the field. */
748 > const contextCaps: Record<SessionContextDetail, { user: number; assistant: number; toolInput: number }> = {
749 > // `summary` still carries a short assistant gist per turn so the reader sees
750 > // what each turn actually did, not just what was asked.
751 > summary: { user: 160, assistant: 140, toolInput: 0 },
752 > digest: { user: 300, assistant: 800, toolInput: 0 },
753 > full: { user: 1000, assistant: 2000, toolInput: 200 },
754 > };
755 >
756 > interface ISessionContextArgs {
757 > readonly session?: unknown;
758 > readonly detail?: unknown;
759 > readonly transcriptLimit?: unknown;
760 > }
761 >
762 > export interface IResolvedSessionContextArgs {
763 > readonly session: URI;
764 > readonly chatId?: string;
765 > readonly detail: SessionContextDetail;
766 > readonly transcriptLimit: number;
767 > }
768 >
769 > /** Validates and resolves get-session-context arguments against the known sessions. */
770 > export function getSessionContextArgs(rawArgs: unknown, sessions: readonly IAgentSessionMetadata[]): IResolvedSessionContextArgs {
771 const args = (rawArgs ?? {}) as ISessionContextArgs;
772 const sessionInput = getRequiredString(args.session, 'session', getSessionContextToolName);
792 return { session, detail, transcriptLimit, ...(chatId !== undefined ? { chatId } : {}) };
793 }
795 > /** Truncates {@link text} to {@link max} characters, appending an ellipsis when cut. */
796 function truncateText(text: string, max: number): { text: string; truncated: boolean } {
797 const trimmed = text.trim();
801 return { text: `${trimmed.slice(0, Math.max(0, max - 1))}…`, truncated: true };
802 }
804 > /** Reads the tool-call parts of a turn, newest-emitted last. */
805 function toolCallsOf(parts: readonly ResponsePart[]): ToolCallState[] {
806 return parts.filter((p): p is Extract<ResponsePart, { kind: ResponsePartKind.ToolCall }> => p.kind === ResponsePartKind.ToolCall).map(p => p.toolCall);
807 }
809 > /** Concatenated markdown text of a turn's response, in stream order. */
810 function assistantTextOf(parts: readonly ResponsePart[]): string {
811 return parts.filter((p): p is Extract<ResponsePart, { kind: ResponsePartKind.Markdown }> => p.kind === ResponsePartKind.Markdown).map(p => p.content).join('').trim();
812 }
814 > /** Reads a tool call's JSON input string, which is absent while still streaming. */
815 function readToolInput(tc: ToolCallState): string | undefined {
816 return tc.status === ToolCallStatus.Streaming ? undefined : tc.toolInput;
817 }
819 > interface ISerializedContextTurn {
820 > readonly turn: number;
821 > readonly state: string;
822 > readonly user?: string;
823 > readonly assistant?: string;
824 > readonly toolCalls?: readonly (string | { readonly name: string; readonly input?: string })[];
825 > }
826 >
827 > /** Maps a {@link TurnState} (or the in-progress active turn) to a display string. */
828 function describeTurnState(state: TurnState | 'inProgress'): string {
829 switch (state) {
834 }
835 }
837 > interface ISerializedSessionContext {
838 > readonly session: string;
839 > readonly openLink: string;
840 > readonly detail: SessionContextDetail;
841 > readonly transcript: readonly ISerializedContextTurn[];
842 > readonly hasMoreHistory: boolean;
843 > /** `true` when turns were dropped from the window or any field was shortened. */
844 > readonly truncated: boolean;
845 > }
846 >
847 > /** Builds the compacted, model-facing session-context payload from a snapshot. */
848 > export function serializeSessionContext(session: URI, chatId: string | undefined, snapshot: IChatContextSnapshot, detail: SessionContextDetail, transcriptLimit: number): string {
849 const caps = contextCaps[detail];
850 let truncated = false;
902 return JSON.stringify(payload);
903 }
905 > /** Reads and serializes the context of an existing session/chat. */
906 export async function applyGetSessionContextTool(accessor: ISessionServerToolAccessor, rawArgs: unknown): Promise<string> {
907 const sessions = await accessor.listSessions();
922 return serializeSessionContext(session, chatId, snapshot, detail, transcriptLimit);
923 }
925 >
926 > /** Serializes the current session's metadata + open link as the `get_current_session` result. */
927 > export function serializeCurrentSession(currentSession: URI, sessions: readonly IAgentSessionMetadata[]): string {
928 const meta = sessions.find(s => s.session.toString() === currentSession.toString());
929 return JSON.stringify({
933 });
934 }
936 function parseListedSessionCount(resultText: string | undefined): number | undefined {
937 if (!resultText) {
945 }
946 }
948 > interface IDeleteSessionArgs {
949 > readonly session?: unknown;
950 > }
951 >
952 > /**
953 > * Validates delete-session arguments against current sessions and refuses to
954 > * delete {@link currentSession} (deleting the session the tool runs in would
955 > * tear down its own conversation).
956 > */
957 > export function getDeleteSessionArgs(rawArgs: unknown, sessions: readonly IAgentSessionMetadata[], currentSession?: URI): URI {
958 const args = (rawArgs ?? {}) as IDeleteSessionArgs;
959 const sessionInput = getRequiredString(args.session, 'session', deleteSessionToolName);
967 return session;
968 }
970 > /** Deletes a session and returns the model-facing confirmation. */
971 export async function applyDeleteSessionTool(accessor: ISessionServerToolAccessor, rawArgs: unknown, currentSession?: URI): Promise<string> {
972 const sessions = await accessor.listSessions();
975 return `Deleted session ${session.toString()}. Reply with one short sentence confirming the session was deleted.`;
976 }
978 function getSessionToolDisplay(toolName: string, _args: unknown, result?: IServerToolDisplayResult): IServerToolDisplay | undefined {
979 switch (toolName) {
1034 }
1035 }
1037 > /**
1038 > * Creates the session server-tool group with process-local recursion protection.
1039 > *
1040 > * The {@link accessor} is optional so the group can also back the pure display
1041 > * path (`getServerToolDisplay`), which only needs {@link IServerToolGroup.definitions},
1042 > * {@link IServerToolGroup.getDisplay} and {@link IServerToolGroup.requiresConfirmation}
1043 > * and never invokes {@link IServerToolGroup.execute}. `execute` throws when no
1044 > * accessor was provided.
1045 > */
1046 > export function createSessionServerToolGroup(accessor?: ISessionServerToolAccessor): IServerToolGroup {
1047 > let createdSessionCount = 0; sessionServerTools.ts
1048 > let createdChatCount = 0;
1049 > let sentMessageCount = 0;
1050 > const group: IServerToolGroup = {
1051 > definitions: sessionServerToolDefinitions,
1052 > requiresConfirmation(toolName: string): boolean {
1053 return sessionToolRequiresConfirmation(toolName);
1054 },
1055 > getDisplay(toolName: string, args: unknown, result?: IServerToolDisplayResult): IServerToolDisplay | undefined { sessionServerTools.ts
1056 return getSessionToolDisplay(toolName, args, result);
1057 },
1058 > async execute(_stateManager: AgentHostStateManager, sessionUri: ProtocolURI, toolName: string, rawArgs: unknown): Promise<string> { sessionServerTools.ts
1059 if (!accessor) {
1060 throw new Error(`Session server tool "${toolName}" cannot run: the group was built without a session accessor.`);
src/vs/base/common/uri.ts 503 covered LOC · 117 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- uri.ts
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 > import { CharCode } from './charCode.js';
7 > import { MarshalledId } from './marshallingIds.js';
8 > import * as paths from './path.js';
9 > import { isWindows } from './platform.js';
10 >
11 > const _schemePattern = /^\w[\w\d+.-]*$/;
12 > const _singleSlashStart = /^\//;
13 > const _doubleSlashStart = /^\/\//;
14 >
15 > function _validateUri(ret: URI, _strict?: boolean): void { uri.ts
16 >
17 > // scheme, must be set
18 > if (!ret.scheme && _strict) {
19 throw new Error(`[UriError]: Scheme is missing: {scheme: "", authority: "${ret.authority}", path: "${ret.path}", query: "${ret.query}", fragment: "${ret.fragment}"}`);
20 }
21 > uri.ts
22 > // scheme, https://tools.ietf.org/html/rfc3986#section-3.1
23 > // ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
24 > if (ret.scheme && !_schemePattern.test(ret.scheme)) { uri.ts
25 const matches = [...ret.scheme.matchAll(/[^\w\d+.-]/gu)];
26 const detail = matches.length > 0
29 throw new Error(`[UriError]: Scheme contains illegal characters.${detail} (len:${ret.scheme.length})`);
30 }
31 > uri.ts
32 > // path, http://tools.ietf.org/html/rfc3986#section-3.3
33 > // If a URI contains an authority component, then the path component
34 > // must either be empty or begin with a slash ("/") character. If a URI
35 > // does not contain an authority component, then the path cannot begin
36 > // with two slash characters ("//").
37 > if (ret.path) {
38 > if (ret.authority) { uri.ts
39 if (!_singleSlashStart.test(ret.path)) {
40 throw new Error('[UriError]: If a URI contains an authority component, then the path component must either be empty or begin with a slash ("/") character');
41 }
42 > } else { uri.ts
43 > if (_doubleSlashStart.test(ret.path)) { uri.ts
44 throw new Error('[UriError]: If a URI does not contain an authority component, then the path cannot begin with two slash characters ("//")');
45 }
46 > } uri.ts
47 > } uri.ts
48 > } uri.ts
49 > uri.ts
50 > // for a while we allowed uris *without* schemes and this is the migration
51 > // for them, e.g. an uri without scheme and without strict-mode warns and falls
52 > // back to the file-scheme. that should cause the least carnage and still be a
53 > // clear warning
54 > function _schemeFix(scheme: string, _strict: boolean): string { uri.ts
55 > if (!scheme && !_strict) {
56 return 'file';
57 }
58 > return scheme; uri.ts
59 > }
60 > uri.ts
61 > // implements a bit of https://tools.ietf.org/html/rfc3986#section-5
62 > function _referenceResolution(scheme: string, path: string): string { uri.ts
63 >
64 > // the slash-character is our 'default base' as we don't
65 > // support constructing URIs relative to other URIs. This
66 > // also means that we alter and potentially break paths.
67 > // see https://tools.ietf.org/html/rfc3986#section-5.1.4
68 > switch (scheme) {
69 > case 'https':
70 > case 'http':
71 > case 'file':
72 > if (!path) { uri.ts
73 path = _slash;
74 > } else if (path[0] !== _slash) { uri.ts
75 path = _slash + path;
76 }
77 > break; uri.ts
78 > } uri.ts
79 > return path;
80 > }
81 > uri.ts
82 > const _empty = '';
83 > const _slash = '/';
84 > const _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
85 >
86 > /**
87 > * Uniform Resource Identifier (URI) http://tools.ietf.org/html/rfc3986.
88 > * This class is a simple parser which creates the basic component parts
89 > * (http://tools.ietf.org/html/rfc3986#section-3) with minimal validation
90 > * and encoding.
91 > *
92 > * ```txt
93 > * foo://example.com:8042/over/there?name=ferret#nose
94 > * \_/ \______________/\_________/ \_________/ \__/
95 > * | | | | |
96 > * scheme authority path query fragment
97 > * | _____________________|__
98 > * / \ / \
99 > * urn:example:animal:ferret:nose
100 > * ```
101 > */
102 > export class URI implements UriComponents {
103 >
104 > static isUri(thing: unknown): thing is URI {
105 > if (thing instanceof URI) { uri.ts
106 return true;
107 }
108 > if (!thing || typeof thing !== 'object') { uri.ts
109 > return false; uri.ts
110 > }
111 return typeof (<URI>thing).authority === 'string'
112 && typeof (<URI>thing).fragment === 'string'
117 && typeof (<URI>thing).with === 'function'
118 && typeof (<URI>thing).toString === 'function';
119 > } uri.ts
120 > uri.ts
121 > /**
122 > * scheme is the 'http' part of 'http://www.example.com/some/path?query#fragment'.
123 > * The part before the first colon.
124 > */
125 > readonly scheme: string;
126 >
127 > /**
128 > * authority is the 'www.example.com' part of 'http://www.example.com/some/path?query#fragment'.
129 > * The part between the first double slashes and the next slash.
130 > */
131 > readonly authority: string;
132 >
133 > /**
134 > * path is the '/some/path' part of 'http://www.example.com/some/path?query#fragment'.
135 > */
136 > readonly path: string;
137 >
138 > /**
139 > * query is the 'query' part of 'http://www.example.com/some/path?query#fragment'.
140 > */
141 > readonly query: string;
142 >
143 > /**
144 > * fragment is the 'fragment' part of 'http://www.example.com/some/path?query#fragment'.
145 > */
146 > readonly fragment: string;
147 >
148 > /**
149 > * @internal
150 > */
151 > protected constructor(scheme: string, authority?: string, path?: string, query?: string, fragment?: string, _strict?: boolean);
152 >
153 > /**
154 > * @internal
155 > */
156 > protected constructor(components: UriComponents);
157 >
158 > /**
159 > * @internal
160 > */
161 > protected constructor(schemeOrData: string | UriComponents, authority?: string, path?: string, query?: string, fragment?: string, _strict: boolean = false) {
162 > uri.ts
163 > if (typeof schemeOrData === 'object') {
164 this.scheme = schemeOrData.scheme || _empty;
165 this.authority = schemeOrData.authority || _empty;
170 // that creates uri components.
171 // _validateUri(this);
172 > } else { uri.ts
173 > this.scheme = _schemeFix(schemeOrData, _strict);
174 > this.authority = authority || _empty;
175 > this.path = _referenceResolution(this.scheme, path || _empty);
176 > this.query = query || _empty;
177 > this.fragment = fragment || _empty;
178 >
179 > _validateUri(this, _strict);
180 > }
181 > }
182 > uri.ts
183 > // ---- filesystem path -----------------------
184 >
185 > /**
186 > * Returns a string representing the corresponding file system path of this URI.
187 > * Will handle UNC paths, normalizes windows drive letters to lower-case, and uses the
188 > * platform specific path separator.
189 > *
190 > * * Will *not* validate the path for invalid characters and semantics.
191 > * * Will *not* look at the scheme of this URI.
192 > * * The result shall *not* be used for display purposes but for accessing a file on disk.
193 > *
194 > *
195 > * The *difference* to `URI#path` is the use of the platform specific separator and the handling
196 > * of UNC paths. See the below sample of a file-uri with an authority (UNC path).
197 > *
198 > * ```ts
199 > const u = URI.parse('file://server/c$/folder/file.txt')
200 > u.authority === 'server'
201 > u.path === '/shares/c$/file.txt'
202 > u.fsPath === '\\server\c$\folder\file.txt'
203 > ```
204 > *
205 > * Using `URI#path` to read a file (using fs-apis) would not be enough because parts of the path,
206 > * namely the server name, would be missing. Therefore `URI#fsPath` exists - it's sugar to ease working
207 > * with URIs that represent files on disk (`file` scheme).
208 > */
209 > get fsPath(): string {
210 // if (this.scheme !== 'file') {
211 // console.warn(`[UriError] calling fsPath with scheme ${this.scheme}`);
213 return uriToFsPath(this, false);
214 }
215 > uri.ts
216 > // ---- modify to new -------------------------
217 >
218 > with(change: { scheme?: string; authority?: string | null; path?: string | null; query?: string | null; fragment?: string | null }): URI {
219 > uri.ts
220 > if (!change) {
221 return this;
222 }
223 > uri.ts
224 > let { scheme, authority, path, query, fragment } = change;
225 > if (scheme === undefined) {
226 > scheme = this.scheme; uri.ts
227 > } else if (scheme === null) { uri.ts
228 scheme = _empty;
229 }
230 > if (authority === undefined) { uri.ts
231 > authority = this.authority; uri.ts
232 > } else if (authority === null) { uri.ts
233 authority = _empty;
234 }
235 > if (path === undefined) { uri.ts
236 > path = this.path; uri.ts
237 > } else if (path === null) { uri.ts
238 path = _empty;
239 }
240 > if (query === undefined) { uri.ts
241 > query = this.query; uri.ts
242 > } else if (query === null) { uri.ts
243 query = _empty;
244 }
245 > if (fragment === undefined) { uri.ts
246 > fragment = this.fragment; uri.ts
247 > } else if (fragment === null) { uri.ts
248 fragment = _empty;
249 }
250 > uri.ts
251 > if (scheme === this.scheme
252 > && authority === this.authority uri.ts
253 > && path === this.path
254 > && query === this.query uri.ts
255 > && fragment === this.fragment) { uri.ts
256 > uri.ts
257 > return this;
258 > }
259 > uri.ts
260 > return new Uri(scheme, authority, path, query, fragment);
261 > } uri.ts
262 > uri.ts
263 > // ---- parse & validate ------------------------
264 >
265 > /**
266 > * Creates a new URI from a string, e.g. `http://www.example.com/some/path`,
267 > * `file:///usr/home`, or `scheme:with/path`.
268 > *
269 > * @param value A string which represents an URI (see `URI#toString`).
270 > */
271 > static parse(value: string, _strict: boolean = false): URI {
272 > const match = _regexp.exec(value); uri.ts
273 > if (!match) {
274 return new Uri(_empty, _empty, _empty, _empty, _empty);
275 }
276 > return new Uri( uri.ts
277 > match[2] || _empty,
278 > percentDecode(match[4] || _empty),
279 > percentDecode(match[5] || _empty),
280 > percentDecode(match[7] || _empty),
281 > percentDecode(match[9] || _empty),
282 > _strict
283 > );
284 > }
285 > uri.ts
286 > /**
287 > * Creates a new URI from a file system path, e.g. `c:\my\files`,
288 > * `/usr/home`, or `\\server\share\some\path`.
289 > *
290 > * The *difference* between `URI#parse` and `URI#file` is that the latter treats the argument
291 > * as path, not as stringified-uri. E.g. `URI.file(path)` is **not the same as**
292 > * `URI.parse('file://' + path)` because the path might contain characters that are
293 > * interpreted (# and ?). See the following sample:
294 > * ```ts
295 > const good = URI.file('/coding/c#/project1');
296 > good.scheme === 'file';
297 > good.path === '/coding/c#/project1';
298 > good.fragment === '';
299 > const bad = URI.parse('file://' + '/coding/c#/project1');
300 > bad.scheme === 'file';
301 > bad.path === '/coding/c'; // path is now broken
302 > bad.fragment === '/project1';
303 > ```
304 > *
305 > * @param path A file system path (see `URI#fsPath`)
306 > */
307 > static file(path: string): URI {
308 > uri.ts
309 > let authority = _empty;
310 >
311 > // normalize to fwd-slashes on windows,
312 > // on other systems bwd-slashes are valid
313 > // filename character, eg /f\oo/ba\r.txt
314 > if (isWindows) {
315 path = path.replace(/\\/g, _slash);
316 }
317 > uri.ts
318 > // check for authority as used in UNC shares
319 > // or use the path as given
320 > if (path[0] === _slash && path[1] === _slash) {
321 const idx = path.indexOf(_slash, 2);
322 if (idx === -1) {
328 }
329 }
330 > uri.ts
331 > return new Uri('file', authority, path, _empty, _empty);
332 > }
333 > uri.ts
334 > /**
335 > * Creates new URI from uri components.
336 > *
337 > * Unless `strict` is `true` the scheme is defaults to be `file`. This function performs
338 > * validation and should be used for untrusted uri components retrieved from storage,
339 > * user input, command arguments etc
340 > */
341 > static from(components: UriComponents, strict?: boolean): URI {
342 > const result = new Uri( uri.ts
343 > components.scheme,
344 > components.authority,
345 > components.path,
346 > components.query,
347 > components.fragment,
348 > strict
349 > );
350 > return result;
351 > }
352 > uri.ts
353 > /**
354 > * Join a URI path with path fragments and normalizes the resulting path.
355 > *
356 > * @param uri The input URI.
357 > * @param pathFragment The path fragment to add to the URI path.
358 > * @returns The resulting URI.
359 > */
360 > static joinPath(uri: URI, ...pathFragment: string[]): URI {
361 > if (!uri.path) { uri.ts
362 throw new Error(`[UriError]: cannot call joinPath on URI without path: ${uri.toString()}`);
363 }
364 > let newPath: string; uri.ts
365 > if (isWindows && uri.scheme === 'file') {
366 newPath = URI.file(paths.win32.join(uriToFsPath(uri, true), ...pathFragment)).path;
367 > } else { uri.ts
368 > newPath = paths.posix.join(uri.path, ...pathFragment);
369 > }
370 > return uri.with({ path: newPath });
371 > }
372 > uri.ts
373 > // ---- printing/externalize ---------------------------
374 >
375 > /**
376 > * Creates a string representation for this URI. It's guaranteed that calling
377 > * `URI.parse` with the result of this function creates an URI which is equal
378 > * to this URI.
379 > *
380 > * * The result shall *not* be used for display purposes but for externalization or transport.
381 > * * The result will be encoded using the percentage encoding and encoding happens mostly
382 > * ignore the scheme-specific encoding rules.
383 > *
384 > * @param skipEncoding Do not encode the result, default is `false`
385 > */
386 > toString(skipEncoding: boolean = false): string {
387 return _asFormatted(this, skipEncoding);
388 }
389 > uri.ts
390 > toJSON(): UriComponents {
391 return this;
392 }
393 > uri.ts
394 > /**
395 > * A helper function to revive URIs.
396 > *
397 > * **Note** that this function should only be used when receiving URI#toJSON generated data
398 > * and that it doesn't do any validation. Use {@link URI.from} when received "untrusted"
399 > * uri components such as command arguments or data from storage.
400 > *
401 > * @param data The URI components or URI to revive.
402 > * @returns The revived URI or undefined or null.
403 > */
404 > static revive(data: UriComponents | URI): URI;
405 > static revive(data: UriComponents | URI | undefined): URI | undefined;
406 > static revive(data: UriComponents | URI | null): URI | null;
407 > static revive(data: UriComponents | URI | undefined | null): URI | undefined | null;
408 > static revive(data: UriComponents | URI | undefined | null): URI | undefined | null {
409 if (!data) {
410 return data;
418 }
419 }
420 > uri.ts
421 > [Symbol.for('debug.description')]() {
422 return `URI(${this.toString()})`;
423 }
424 > } uri.ts
425 >
426 > export interface UriComponents {
427 > scheme: string;
428 > authority?: string;
429 > path?: string;
430 > query?: string;
431 > fragment?: string;
432 > }
433 >
434 > export function isUriComponents(thing: unknown): thing is UriComponents {
435 if (!thing || typeof thing !== 'object') {
436 return false;
442 && (typeof (<UriComponents>thing).fragment === 'string' || typeof (<UriComponents>thing).fragment === 'undefined');
443 }
444 > uri.ts
445 > interface UriState extends UriComponents {
446 > $mid: MarshalledId.Uri;
447 > external?: string;
448 > fsPath?: string;
449 > _sep?: 1;
450 > }
451 >
452 > const _pathSepMarker = isWindows ? 1 : undefined;
453 >
454 > // This class exists so that URI is compatible with vscode.Uri (API).
455 > class Uri extends URI { uri.ts
456 >
457 > _formatted: string | null = null;
458 > _fsPath: string | null = null;
459 > uri.ts
460 > override get fsPath(): string {
461 > if (!this._fsPath) { uri.ts
462 > this._fsPath = uriToFsPath(this, false);
463 > }
464 > return this._fsPath;
465 > }
466 > uri.ts
467 > override toString(skipEncoding: boolean = false): string {
468 > if (!skipEncoding) { uri.ts
469 > if (!this._formatted) { uri.ts
470 > this._formatted = _asFormatted(this, false);
471 > }
472 > return this._formatted;
473 > } else { uri.ts
474 // we don't cache that
475 return _asFormatted(this, true);
476 }
477 > } uri.ts
478 > uri.ts
479 > override toJSON(): UriComponents {
480 // eslint-disable-next-line local/code-no-dangerous-type-assertions
481 const res = <UriState>{
512 return res;
513 }
514 > } uri.ts
515 >
516 > // reserved characters: https://tools.ietf.org/html/rfc3986#section-2.2
517 > const encodeTable: { [ch: number]: string } = {
518 > [CharCode.Colon]: '%3A', // gen-delims
519 > [CharCode.Slash]: '%2F',
520 > [CharCode.QuestionMark]: '%3F',
521 > [CharCode.Hash]: '%23',
522 > [CharCode.OpenSquareBracket]: '%5B',
523 > [CharCode.CloseSquareBracket]: '%5D',
524 > [CharCode.AtSign]: '%40',
525 >
526 > [CharCode.ExclamationMark]: '%21', // sub-delims
527 > [CharCode.DollarSign]: '%24',
528 > [CharCode.Ampersand]: '%26',
529 > [CharCode.SingleQuote]: '%27',
530 > [CharCode.OpenParen]: '%28',
531 > [CharCode.CloseParen]: '%29',
532 > [CharCode.Asterisk]: '%2A',
533 > [CharCode.Plus]: '%2B',
534 > [CharCode.Comma]: '%2C',
535 > [CharCode.Semicolon]: '%3B',
536 > [CharCode.Equals]: '%3D',
537 >
538 > [CharCode.Space]: '%20',
539 > };
540 >
541 > function encodeURIComponentFast(uriComponent: string, isPath: boolean, isAuthority: boolean): string { uri.ts
542 > let res: string | undefined = undefined;
543 > let nativeEncodePos = -1;
544 >
545 > for (let pos = 0; pos < uriComponent.length; pos++) {
546 > const code = uriComponent.charCodeAt(pos);
547 >
548 > // unreserved characters: https://tools.ietf.org/html/rfc3986#section-2.3
549 > if (
550 > (code >= CharCode.a && code <= CharCode.z)
551 > || (code >= CharCode.A && code <= CharCode.Z) uri.ts
552 > || (code >= CharCode.Digit0 && code <= CharCode.Digit9)
553 > || code === CharCode.Dash uri.ts
554 > || code === CharCode.Period uri.ts
555 > || code === CharCode.Underline
556 > || code === CharCode.Tilde
557 > || (isPath && code === CharCode.Slash)
558 || (isAuthority && code === CharCode.OpenSquareBracket)
559 || (isAuthority && code === CharCode.CloseSquareBracket)
560 || (isAuthority && code === CharCode.Colon)
561 > ) { uri.ts
562 > // check if we are delaying native encode
563 > if (nativeEncodePos !== -1) {
564 res += encodeURIComponent(uriComponent.substring(nativeEncodePos, pos));
565 nativeEncodePos = -1;
566 }
567 > // check if we write into a new string (by default we try to return the param) uri.ts
568 > if (res !== undefined) {
569 res += uriComponent.charAt(pos);
570 }
571 > uri.ts
572 > } else {
573 // encoding needed, we need to allocate a new string
574 if (res === undefined) {
594 }
595 }
596 > } uri.ts
597 >
598 > if (nativeEncodePos !== -1) {
599 res += encodeURIComponent(uriComponent.substring(nativeEncodePos));
600 }
601 > uri.ts
602 > return res !== undefined ? res : uriComponent;
603 > }
604 > uri.ts
605 function encodeURIComponentMinimal(path: string): string {
606 let res: string | undefined = undefined;
620 return res !== undefined ? res : path;
621 }
622 > uri.ts
623 > /**
624 > * Compute `fsPath` for the given uri
625 > */
626 > export function uriToFsPath(uri: URI, keepDriveLetterCasing: boolean): string {
627 > uri.ts
628 > let value: string;
629 > if (uri.authority && uri.path.length > 1 && uri.scheme === 'file') {
630 // unc path: file://shares/c$/far/boo
631 value = `//${uri.authority}${uri.path}`;
632 } else if (
633 > uri.path.charCodeAt(0) === CharCode.Slash uri.ts
634 > && (uri.path.charCodeAt(1) >= CharCode.A && uri.path.charCodeAt(1) <= CharCode.Z || uri.path.charCodeAt(1) >= CharCode.a && uri.path.charCodeAt(1) <= CharCode.z)
635 > && uri.path.charCodeAt(2) === CharCode.Colon uri.ts
636 > ) { uri.ts
637 if (!keepDriveLetterCasing) {
638 // windows drive letter: file:///c:/far/boo
641 value = uri.path.substr(1);
642 }
643 > } else { uri.ts
644 > // other path uri.ts
645 > value = uri.path;
646 > }
647 > if (isWindows) { uri.ts
648 value = value.replace(/\//g, '\\');
649 }
650 > return value; uri.ts
651 > }
652 > uri.ts
653 > /**
654 > * Create the external version of a uri
655 > */
656 > function _asFormatted(uri: URI, skipEncoding: boolean): string { uri.ts
657 >
658 > const encoder = !skipEncoding
659 > ? encodeURIComponentFast uri.ts
660 : encodeURIComponentMinimal;
661 > uri.ts
662 > let res = '';
663 > let { scheme, authority, path, query, fragment } = uri;
664 > if (scheme) {
665 > res += scheme;
666 > res += ':';
667 > }
668 > if (authority || scheme === 'file') {
669 res += _slash;
670 res += _slash;
671 }
672 > if (authority) { uri.ts
673 let idx = authority.indexOf('@');
674 if (idx !== -1) {
697 }
698 }
699 > if (path) { uri.ts
700 > // lower-case windows drive letters in /C:/fff or C:/fff uri.ts
701 > if (path.length >= 3 && path.charCodeAt(0) === CharCode.Slash && path.charCodeAt(2) === CharCode.Colon) {
702 const code = path.charCodeAt(1);
703 if (code >= CharCode.A && code <= CharCode.Z) {
704 path = `/${String.fromCharCode(code + 32)}:${path.substr(3)}`; // "/c:".length === 3
705 }
706 > } else if (path.length >= 2 && path.charCodeAt(1) === CharCode.Colon) { uri.ts
707 const code = path.charCodeAt(0);
708 if (code >= CharCode.A && code <= CharCode.Z) {
710 }
711 }
712 > // encode the rest of the path uri.ts
713 > res += encoder(path, true, false);
714 > }
715 > if (query) { uri.ts
716 res += '?';
717 res += encoder(query, false, false);
718 }
719 > if (fragment) { uri.ts
720 res += '#';
721 res += !skipEncoding ? encodeURIComponentFast(fragment, false, false) : fragment;
722 }
723 > return res; uri.ts
724 > }
725 > uri.ts
726 > // --- decode
727 >
728 function decodeURIComponentGraceful(str: string): string {
729 try {
737 }
738 }
739 > uri.ts
740 > const _rEncodedAsHex = /(%[0-9A-Za-z][0-9A-Za-z])+/g;
741 >
742 > function percentDecode(str: string): string { uri.ts
743 > if (!str.match(_rEncodedAsHex)) {
744 > return str;
745 > }
746 return str.replace(_rEncodedAsHex, (match) => decodeURIComponentGraceful(match));
747 }
748 > uri.ts
749 > /**
750 > * Mapped-type that replaces all occurrences of URI with UriComponents
751 > */
752 > export type UriDto<T> = { [K in keyof T]: T[K] extends URI
753 > ? UriComponents
754 > : UriDto<T[K]> };
src/vs/platform/agentHost/node/agentSideEffects.ts 499 covered LOC · 68 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentSideEffects.ts
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 > import { getErrorCode } from '../../../base/common/errors.js';
7 > import { Disposable, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';
8 > import { NKeyMap } from '../../../base/common/map.js';
9 > import { equals } from '../../../base/common/objects.js';
10 > import { autorun, IObservable, IReader } from '../../../base/common/observable.js';
11 > import { StopWatch } from '../../../base/common/stopwatch.js';
12 > import { hasKey } from '../../../base/common/types.js';
13 > import { URI } from '../../../base/common/uri.js';
14 > import { generateUuid } from '../../../base/common/uuid.js';
15 > import { IInstantiationService } from '../../instantiation/common/instantiation.js';
16 > import { ILogService } from '../../log/common/log.js';
17 > import { IAgentHostChangesetService } from '../common/agentHostChangesetService.js';
18 > import { IAgentHostCheckpointService } from '../common/agentHostCheckpointService.js';
19 > import { readAgentModelByokIdentifier } from '../common/agentModelByokMeta.js';
20 > import { AgentSession, AgentSignal, IAgent, IAgentToolPendingConfirmationSignal } from '../common/agentService.js';
21 > import { readToolCallMeta, toToolCallMeta } from '../common/meta/agentToolCallMeta.js';
22 >
23 > import { ITelemetryService } from '../../telemetry/common/telemetry.js';
24 > import { ISessionDataService } from '../common/sessionDataService.js';
25 > import { SessionConfigKey } from '../common/sessionConfigKeys.js';
26 > import { resolveChatAttachment } from '../common/state/chatAttachmentContext.js';
27 > import { SessionInputRequestKind, ToolCallContributorKind, type AgentInfo, type SessionInputRequest } from '../common/state/protocol/state.js';
28 > import { ActionType, isChatAction, StateAction, type ChatAction, type ChatToolCallCompleteAction } from '../common/state/sessionActions.js';
29 > import {
30 > buildSubagentChatUri,
31 > getToolFileEdits,
32 > isAhpChatChannel,
33 > isDefaultChatUri,
34 > isSubagentChatUri,
35 > isChatReadOnly,
36 > AH_META_IS_ARCHIVED_DB_KEY,
37 > MessageAttachmentKind,
38 > MessageKind,
39 > parseChatUri,
40 > parseRequiredSessionUriFromChatUri,
41 > PendingMessageKind,
42 > ResponsePartKind,
43 > ROOT_STATE_URI,
44 > SessionLifecycle,
45 > SessionStatus,
46 > ToolCallStatus,
47 > ToolResultContentType,
48 > type ErrorInfo,
49 > type ISessionWithDefaultChat,
50 > type Message,
51 > type MessageAttachment,
52 > type URI as ProtocolURI,
53 > type SessionState,
54 > type ToolCallState,
55 > type ToolCallResult,
56 > type ToolResultContent,
57 > type Turn
58 > } from '../common/state/sessionState.js';
59 > import { AgentHostLocalTurns } from './agentHostLocalTurns.js';
60 > import { AgentHostSessionTitleController } from './agentHostSessionTitleController.js';
61 > import { AgentHostStateManager } from './agentHostStateManager.js';
62 > import { AgentHostTelemetryReporter, type AgentHostModelTelemetryKind, type AgentHostTurnFailureStage, type IAgentHostTurnFailure } from './agentHostTelemetryReporter.js';
63 > import { AgentHostToolCallTracker } from './agentHostToolCallTracker.js';
64 > import { updateAgentHostTelemetryLevelFromConfig } from './agentHostTelemetryService.js';
65 > import { AgentHostTurnTracker } from './agentHostTurnTracker.js';
66 > import { AgentHostLocalCommands } from './localCommands/localChatCommand.js';
67 > import './localCommands/localChatCommands.contribution.js';
68 > import { SessionPermissionManager } from './sessionPermissions.js';
69 > import type { ICopilotApiService } from './shared/copilotApiService.js';
70 > import { stripProxyErrorMarker, toChatErrorMeta, tryParseForwardedChatError } from './shared/forwardedChatError.js';
71 > import { persistSessionMetadata } from './shared/persistSessionMetadata.js';
72 > import type { WorktreeIsolation } from './shared/worktreeIsolation.js';
73 >
74 > /**
75 > * Options for constructing an {@link AgentSideEffects} instance.
76 > */
77 > export interface IAgentSideEffectsOptions {
78 > /** Resolve the agent responsible for a given session URI. */
79 > readonly getAgent: (session: ProtocolURI) => IAgent | undefined;
80 > /** Observable set of registered agents. Triggers `root/agentsChanged` when it changes. */
81 > readonly agents: IObservable<readonly IAgent[]>;
82 > /** Session data service for cleaning up per-session data on disposal. */
83 > readonly sessionDataService: ISessionDataService;
84 > /** Registry that persists host-injected `/rename` and `!command` turns. */
85 > readonly localTurns: AgentHostLocalTurns;
86 > /** Get the GitHub token used for Copilot utility title generation. */
87 > readonly getGitHubCopilotToken?: () => string | undefined;
88 > /** CAPI service used for Copilot utility title generation. */
89 > readonly copilotApiService?: ICopilotApiService;
90 > /**
91 > * Host-owned working-directory resolution hook, awaited before the agent's
92 > * first send so the session's working directory (an isolated worktree created
93 > * on the first send, or the picked folder) is resolved before the agent
94 > * materializes and its cwd is locked. Resolves to the working directory to
95 > * hand the agent, or `undefined` for workspace-less sessions. Provided by
96 > * {@link AgentService}.
97 > */
98 > readonly resolveWorkingDirectoryBeforeSend?: (params: { session: ProtocolURI; chat: ProtocolURI; turnId: string; prompt: string }) => Promise<URI | undefined>;
99 > /** Resolves a referenced chat's turns, hydrating its owning session when needed. */
100 > readonly resolveChatAttachmentTurns?: (resource: ProtocolURI) => Promise<readonly Turn[]>;
101 > /**
102 > * Called after each top-level session turn completes so git state can be
103 > * refreshed and published via `SessionMetaChanged`. Subagent turns are
104 > * excluded — only the parent session URI is passed.
105 > */
106 > readonly onTurnComplete: (session: ProtocolURI) => void;
107 > }
108 >
109 > /** A signal that was deferred because its subagent session does not exist yet. */
110 > interface IPendingSubagentSignal {
111 > readonly signal: AgentSignal;
112 > readonly agent: IAgent;
113 > }
114 >
115 > interface ISubagentSessionRef {
116 > readonly parentChatUri: ProtocolURI;
117 > readonly toolCallId: string;
118 > readonly sessionUri: ProtocolURI;
119 > readonly chatUri: ProtocolURI;
120 > readonly turnStopWatch: StopWatch;
121 > }
122 >
123 > type AgentSignalTurnIdRouting = 'preserve' | 'remap';
124 >
125 > /**
126 > * Shared implementation of agent side-effect handling.
127 > *
128 > * Routes client-dispatched actions to the correct agent backend,
129 > * restores sessions from previous lifetimes, handles filesystem
130 > * operations (browse/fetch/write), tracks pending permission requests,
131 > * and wires up agent progress events to the state manager.
132 > *
133 > * Session create/dispose/list and auth are handled by {@link AgentService}.
134 > */
135 > export class AgentSideEffects extends Disposable {
136 >
137 > /** Maps tool call IDs to the agent that owns them, for routing confirmations. */
138 > private readonly _toolCallAgents = new Map<string, string>();
139 > private _lastAgentInfos: readonly AgentInfo[] = [];
140 >
141 > private readonly _permissionManager: SessionPermissionManager;
142 >
143 > /** Registry-driven dispatcher for host-handled `/rename` / `!command` etc. */
144 > private readonly _localCommands: AgentHostLocalCommands;
145 >
146 > private readonly _subagentChats = new NKeyMap<ISubagentSessionRef, [ProtocolURI, string]>();
147 > private readonly _cancelledTurnIds = new Map<ProtocolURI, Set<string>>();
148 >
149 > /**
150 > * Buffers signals whose `parentToolCallId` references a subagent
151 > * whose `subagent_started` signal has not yet been processed. The SDK is
152 > * not strict about ordering: an inner `tool_start` can arrive before the
153 > * `subagent_started` that creates the child session. Without buffering,
154 > * those signals would be dispatched against the parent session and the
155 > * UI would render the inner tool calls flat at the top level rather than
156 > * grouping them under the subagent. Drained by `_handleSubagentStarted`.
157 > *
158 > */
159 > private readonly _pendingSubagentSignals = new NKeyMap<IPendingSubagentSignal[], [ProtocolURI, string]>();
160 > private readonly _telemetryReporter: AgentHostTelemetryReporter;
161 > private readonly _turnTracker: AgentHostTurnTracker;
162 > private readonly _toolCallTracker: AgentHostToolCallTracker;
163 > private readonly _titleController: AgentHostSessionTitleController;
164 > /** Host-owned worktree isolation controller; injected post-construction. */
165 > private _worktree: WorktreeIsolation | undefined;
166 >
167 > constructor(
168 > private readonly _stateManager: AgentHostStateManager, agentSideEffects.ts
169 > private readonly _options: IAgentSideEffectsOptions,
170 > @IInstantiationService instantiationService: IInstantiationService,
171 > @ILogService private readonly _logService: ILogService,
172 > @IAgentHostChangesetService private readonly _changesets: IAgentHostChangesetService,
173 > @ITelemetryService private readonly _telemetryService: ITelemetryService,
174 > @IAgentHostCheckpointService private readonly _checkpointService: IAgentHostCheckpointService,
175 > ) {
176 > super();
177 > this._telemetryReporter = new AgentHostTelemetryReporter(this._telemetryService);
178 > this._turnTracker = new AgentHostTurnTracker(this._telemetryReporter);
179 > this._toolCallTracker = this._register(new AgentHostToolCallTracker(this._telemetryReporter));
180 > this._permissionManager = this._register(instantiationService.createInstance(SessionPermissionManager, this._stateManager, {}));
181 > this._localCommands = this._register(instantiationService.createInstance(
182 > AgentHostLocalCommands,
183 > this._stateManager,
184 > this._options.localTurns,
185 > // Draining the queue re-enters agent lookup / telemetry / sendMessage,
186 > // which is this class's responsibility, so the dispatcher hands the
187 > // turn back here once it has completed a host-handled command.
188 > (turnChannel: ProtocolURI) => this._tryConsumeNextQueuedMessage(turnChannel),
189 > ));
190 > this._titleController = this._register(instantiationService.createInstance(AgentHostSessionTitleController, this._stateManager, {
191 > sessionDataService: this._options.sessionDataService,
192 > getGitHubCopilotToken: this._options.getGitHubCopilotToken,
193 > copilotApiService: this._options.copilotApiService,
194 > }));
195 >
196 > // Whenever the agents observable changes, publish to root state.
197 > this._register(autorun(reader => {
198 > const agents = this._options.agents.read(reader);
199 > this._publishAgentInfos(agents, reader);
200 > }));
201 >
202 > // Server-dispatched ChatToolCallComplete actions (e.g. from
203 > // the disconnect timeout in ProtocolServerHandler) bypass
204 > // handleAction, so the agent's SDK deferred never resolves.
205 > // Listen for these envelopes and notify the agent directly.
206 > this._register(this._stateManager.onDidEmitEnvelope(envelope => {
207 > if (isAhpChatChannel(envelope.channel) && isChatAction(envelope.action)) { agentSideEffects.ts
208 if (envelope.action.type === ActionType.ChatTurnCancelled) {
209 let turnIds = this._cancelledTurnIds.get(envelope.channel);
216 this._syncSessionInputNeededForChatAction(envelope.channel, envelope.action);
217 }
218 > if (!envelope.origin && envelope.action.type === ActionType.ChatToolCallComplete) { agentSideEffects.ts
219 const action = envelope.action;
220 // Chat-action envelopes are emitted on the chat channel URI;
228 this._notifyClientToolCallComplete(sessionChannel, envelope.channel, action.toolCallId, action.result, 'server-envelope');
229 }
230 > if (envelope.action.type === ActionType.ChatDraftChanged) { agentSideEffects.ts
231 this._persistChatDraft(envelope.channel, envelope.action.draft);
232 }
233 > })); agentSideEffects.ts
234 > }
236 > /**
237 > * Publishes agent descriptors using the last known model lists.
238 > */
239 > private _publishAgentInfos(agents: readonly IAgent[], reader?: IReader): void {
240 > const infos: AgentInfo[] = agents.map(a => { agentSideEffects.ts
241 > const d = a.getDescriptor(); agentSideEffects.ts
242 > const protectedResources = a.getProtectedResources();
243 > const models = reader ? a.models.read(reader) : a.models.get();
244 > const customizations = a.getCustomizations?.();
245 > return {
246 > provider: d.provider, displayName: d.displayName, description: d.description, models: models.map(m => ({
247 id: m.id,
248 provider: m.provider,
255 configSchema: m.configSchema,
256 _meta: m._meta,
257 > })), agentSideEffects.ts
258 > customizations: customizations?.length ? [...customizations] : undefined,
259 > protectedResources: protectedResources.length > 0 ? protectedResources : undefined,
260 > capabilities: d.capabilities ? { ...d.capabilities } : undefined,
261 > };
263 > if (equals(this._lastAgentInfos, infos)) {
264 > return; agentSideEffects.ts
265 > }
266 > this._lastAgentInfos = infos; agentSideEffects.ts
267 > this._stateManager.dispatchServerAction(ROOT_STATE_URI, { type: ActionType.RootAgentsChanged, agents: infos });
270 > private async _publishSessionCustomizations(agent: IAgent, session: ProtocolURI): Promise<void> {
271 if (!agent.getSessionCustomizations) {
272 return;
298 });
299 }
301 > private _publishSessionCustomizationsSoon(agent: IAgent, session: ProtocolURI): void {
302 void this._publishSessionCustomizations(agent, session).catch(err => {
303 this._logService.error('[AgentSideEffects] getSessionCustomizations failed', err);
304 });
305 }
307 > private _publishSessionCustomizationsForAgent(agent: IAgent): void {
308 for (const session of this._stateManager.getSessionUris()) {
309 if (this._options.getAgent(session) === agent) {
312 }
313 }
315 > private _publishAllSessionCustomizations(): void {
316 for (const session of this._stateManager.getSessionUris()) {
317 const agent = this._options.getAgent(session);
321 }
322 }
324 > // ---- Session input-needed aggregation ----------------------------------
325 > //
326 > // Mirrors per-chat blockers (user-input elicitations, tool confirmations,
327 > // client-tool executions, and MCP authentication) into the owning session's
328 > // `inputNeeded` list so clients subscribed only to the session channel can
329 > // discover and answer them without subscribing to each chat. This handler
330 > // only produces the state; it does not consume it.
331 >
332 > private _syncSessionInputNeededForChatAction(chatUri: ProtocolURI, action: ChatAction): void {
333 switch (action.type) {
334 case ActionType.ChatInputRequested:
358 }
359 }
361 > private _syncChatInputNeeded(chatUri: ProtocolURI, requestId: string): void {
362 const state = this._stateManager.getSessionState(chatUri);
363 const part = state?.activeTurn?.responseParts.find(part =>
378 });
379 }
381 > private _syncToolInputNeeded(chatUri: ProtocolURI, turnId: string, toolCallId: string): void {
382 const confirmationId = this._toolConfirmationNeededId(chatUri, turnId, toolCallId);
383 const clientExecutionId = this._toolClientExecutionNeededId(chatUri, turnId, toolCallId);
433 }
434 }
436 > private _findToolCall(chatUri: ProtocolURI, turnId: string, toolCallId: string): ToolCallState | undefined {
437 const state = this._stateManager.getSessionState(chatUri);
438 const turn = state?.activeTurn?.id === turnId ? state.activeTurn : state?.turns.find(t => t.id === turnId);
440 return part?.kind === ResponsePartKind.ToolCall ? part.toolCall : undefined;
441 }
443 > private _setSessionInputNeeded(chatUri: ProtocolURI, request: SessionInputRequest): void {
444 const sessionUri = parseRequiredSessionUriFromChatUri(chatUri);
445 const existing = this._stateManager.getSessionState(sessionUri)?.inputNeeded?.find(r => r.id === request.id);
455 }
456 }
458 > private _removeSessionInputNeeded(chatUri: ProtocolURI, id: string): void {
459 const sessionUri = parseRequiredSessionUriFromChatUri(chatUri);
460 this._toolCallTracker.toolCallUnblocked(chatUri, id);
464 this._stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionInputNeededRemoved, id });
465 }
467 > private _removeSessionInputNeededForChat(chatUri: ProtocolURI): void {
468 const sessionUri = parseRequiredSessionUriFromChatUri(chatUri);
469 for (const request of this._stateManager.getSessionState(sessionUri)?.inputNeeded ?? []) {
473 }
474 }
476 > private _chatInputNeededId(chatUri: ProtocolURI, requestId: string): string {
477 return `chatInput:${chatUri}:${requestId}`;
478 }
480 > private _toolConfirmationNeededId(chatUri: ProtocolURI, turnId: string, toolCallId: string): string {
481 return `toolConfirmation:${chatUri}:${turnId}:${toolCallId}`;
482 }
484 > private _toolClientExecutionNeededId(chatUri: ProtocolURI, turnId: string, toolCallId: string): string {
485 return `toolClientExecution:${chatUri}:${turnId}:${toolCallId}`;
486 }
488 > private _toolAuthenticationNeededId(chatUri: ProtocolURI, turnId: string, toolCallId: string): string {
489 return `toolAuthentication:${chatUri}:${turnId}:${toolCallId}`;
490 }
492 > // ---- Initialization ----------------------------------------------------
493 >
494 > /**
495 > * Initializes async resources (tree-sitter WASM) used for command
496 > * auto-approval. Await this before any session events can arrive to
497 > * guarantee that auto-approval checks are fully synchronous.
498 > */
499 > initialize(): Promise<void> {
500 return this._permissionManager.initialize();
501 }
503 > // ---- Agent registration -------------------------------------------------
504 >
505 > /**
506 > * Registers a progress-signal listener on the given agent so that
507 > * {@link AgentSignal}s are routed/dispatched through the state manager.
508 > * Returns a disposable that removes the listener.
509 > */
510 > registerProgressListener(agent: IAgent): IDisposable {
511 > const disposables = new DisposableStore(); agentSideEffects.ts
512 > disposables.add(agent.onDidSessionProgress(signal => {
513 this._handleAgentSignal(agent, signal);
514 > })); agentSideEffects.ts
515 > if (agent.onDidCustomizationsChange) {
516 > disposables.add(agent.onDidCustomizationsChange(() => {
517 this._publishAgentInfos(this._options.agents.get());
518 this._publishSessionCustomizationsForAgent(agent);
519 > })); agentSideEffects.ts
520 > }
521 > if (agent.onDidRequireAuth) {
522 disposables.add(agent.onDidRequireAuth(e => this._stateManager.emitAuthRequired(e)));
523 }
524 > return disposables; agentSideEffects.ts
525 > }
527 > /**
528 > * Routes a single signal from `agent` to the correct session.
529 > *
530 > * Action signals with a `parentToolCallId` are routed to the matching
531 > * subagent session. If the subagent session does not exist yet (the SDK
532 > * can emit an inner `tool_start` before its `subagent_started`), the
533 > * signal is buffered in {@link _pendingSubagentSignals} and replayed
534 > * once the `subagent_started` arrives.
535 > */
536 > private _handleAgentSignal(agent: IAgent, signal: AgentSignal): void {
537 if (signal.kind === 'subagent_started') {
538 this._handleSubagentStarted(signal.chat.toString(), signal.toolCallId, signal.agentName, signal.agentDisplayName, signal.agentDescription, signal.taskPrompt, signal.parentToolCallId);
637 }
638 }
640 > /**
641 > * Dispatches a signal to a resolved chat, preserving top-level turn identity or remapping cross-channel subagent actions.
642 > */
643 > private _dispatchActionForSession(signal: AgentSignal, sessionKey: ProtocolURI, turnId: string, turnIdRouting: AgentSignalTurnIdRouting, agent?: IAgent): void {
644 if (signal.kind === 'pending_confirmation') {
645 if (agent) {
750 }
751 }
753 > /**
754 > * Post-turn side effects: flush any pending debounced diff computation,
755 > * compute final diffs immediately, drain the next queued message, and
756 > * notify the host so it can refresh git state.
757 > */
758 > private _runTurnCompleteSideEffects(sessionKey: ProtocolURI, turnId: string | undefined): void {
759 // Checkpoints, changesets and the host git-refresh notification are
760 // scoped to the owning session's working tree, which peer chats
801 this._markSessionUnread(sessionUri);
802 }
804 > private _markSessionUnread(session: ProtocolURI): void {
805 const status = this._stateManager.getSessionSummary(session)?.status ?? 0;
806 if (!(status & SessionStatus.IsRead)) {
810 this._persistSessionFlag(session, 'isRead', '');
811 }
813 > private _describeSignal(signal: AgentSignal): string {
814 return signal.kind === 'action' ? `action(${signal.action.type})` : signal.kind;
815 }
817 > /**
818 > * Replays any signals that were buffered while waiting for
819 > * `subagent_started` to create the subagent session. Called immediately
820 > * after `_handleSubagentStarted`.
821 > */
822 > private _drainPendingSubagentSignals(parentChatURI: ProtocolURI, parentToolCallId: string): void {
823 const buffer = this._pendingSubagentSignals.get(parentChatURI, parentToolCallId);
824 if (!buffer) {
831 }
832 }
834 > // ---- Subagent session management ----------------------------------------
835 >
836 > /**
837 > * Starts the subagent turn in response to a `subagent_started` event and
838 > * wires the parent tool call to the subagent chat. The subagent chat's
839 > * catalog membership is owned by the spawn channel
840 > * ({@link AgentService._onChatSpawned}), which the orchestrator applies
841 > * before this runs, so this only drives the turn/tracking/parent content
842 > * — it does not add the chat.
843 > *
844 > * `chatURI` is always the agent's top-level chat: the subagent is
845 > * registered (and inner events routed) under it because inner-tool
846 > * signals carry the top-level chat as their resource. `spawningToolParentId`,
847 > * when set, is the tool call one level up from the spawning `toolCallId`
848 > * — the tool call in whose (subagent) chat the spawning tool lives — and
849 > * is used to route the discovery content block to that immediate parent
850 > * chat. Since subagent chats are flat (keyed off the root session), this
851 > * one-hop reference resolves the parent chat at any nesting depth.
852 > */
853 > private _handleSubagentStarted(
854 chatURI: ProtocolURI,
855 toolCallId: string,
909 }
910 }
912 > /**
913 > * Gets the current content array from a running tool call, if any.
914 > */
915 > private _getRunningToolCallContent(
916 state: ISessionWithDefaultChat | undefined,
917 turnId: string,
928 return [];
929 }
931 > private _turnDuration(stopWatch: StopWatch | undefined): number {
932 const elapsed = stopWatch?.elapsed();
933 return typeof elapsed === 'number' && Number.isFinite(elapsed) ? Math.max(0, elapsed) : 0;
934 }
936 > /**
937 > * Cancels all active subagent sessions for a given parent session.
938 > */
939 > cancelSubagentSessions(parentChatURI: ProtocolURI): void {
940 for (const subagent of this._subagentChats.getAll(parentChatURI)) {
941 const turnId = this._stateManager.getActiveTurnId(subagent.chatUri);
954 this._pendingSubagentSignals.deleteAll(parentChatURI);
955 }
957 > /**
958 > * Completes the subagent session associated with a parent tool call.
959 > * Driven by the `subagent_completed` signal from the agent (which the
960 > * SDK fires on both `subagent.completed` and `subagent.failed`), not by
961 > * parent tool call completion — background subagents keep running after
962 > * their parent tool returns.
963 > */
964 > completeSubagentSession(parentChatURI: ProtocolURI, toolCallId: string): void {
965 // Drop any events that were buffered waiting for a `subagent_started`
966 // that never arrived (e.g. the parent tool failed before the subagent
984 this._subagentChats.delete(parentChatURI, toolCallId);
985 }
987 > /**
988 > * Removes all subagent chats for a given parent session from the state manager.
989 > */
990 > removeSubagentSessions(parentSession: ProtocolURI): void {
991 for (const chatUri of this._cancelledTurnIds.keys()) {
992 if (parseRequiredSessionUriFromChatUri(chatUri) === parentSession) {
1007 }
1008 }
1010 > /**
1011 > * Finds the subagent session that owns a given tool call by checking
1012 > * whether the tool call was previously registered under a subagent
1013 > * session key in `_toolCallAgents`. Scoped to subagent sessions owned
1014 > * by the given parent to avoid cross-session collisions.
1015 > */
1016 > private _findSubagentChatForToolCall(parentChatURI: ProtocolURI, toolCallId: string): ProtocolURI | undefined {
1017 for (const subagent of this._subagentChats.getAll(parentChatURI)) {
1018 if (this._toolCallAgents.has(`${subagent.chatUri}:${toolCallId}`)) {
1022 return undefined;
1023 }
1025 > private _toolCallCompletionChat(chatChannel: ProtocolURI): ProtocolURI {
1026 if (!isSubagentChatUri(chatChannel)) {
1027 return chatChannel;
1037 return chatChannel;
1038 }
1040 > private _notifyClientToolCallComplete(sessionChannel: ProtocolURI, chatChannel: ProtocolURI, toolCallId: string, result: ToolCallResult, source: 'client-dispatch' | 'server-envelope'): void {
1041 const completionChat = this._toolCallCompletionChat(chatChannel);
1042 const agent = this._options.getAgent(sessionChannel);
1048 agent.onClientToolCallComplete(URI.parse(sessionChannel), URI.parse(completionChat), toolCallId, result);
1049 }
1051 > // ---- Side-effect handlers --------------------------------------------------
1052 >
1053 > /**
1054 > * Handles a `pending_confirmation` signal end-to-end: checks for
1055 > * auto-approval via the permission manager, and if not auto-approved,
1056 > * dispatches the `ChatToolCallReady` action with confirmation options
1057 > * for the client.
1058 > */
1059 > private async _handleToolReady(e: IAgentToolPendingConfirmationSignal, sessionKey: ProtocolURI, turnId: string, agent: IAgent): Promise<void> {
1060 const approvalEvent = {
1061 toolCallId: e.state.toolCallId,
1100 );
1101 }
1103 > handleAction(channel: ProtocolURI, action: StateAction, clientId?: string): void {
1104 const chatChannel = isAhpChatChannel(channel) ? channel : undefined;
1105 const sessionChannel = chatChannel ? parseRequiredSessionUriFromChatUri(chatChannel) : channel;
1355 }
1356 }
1358 > /** Injects the host-owned worktree isolation controller (see {@link AgentService.setWorktreeIsolation}). */
1359 > setWorktreeIsolation(worktree: WorktreeIsolation): void {
1360 this._worktree = worktree;
1361 }
1363 > cancelSessionTitleGeneration(session: ProtocolURI): void {
1364 this._titleController.cancelTitleGeneration(session);
1365 }
1367 > /**
1368 > * Generates a content-derived title for a freshly forked session
1369 > * (`chatChannel` undefined) or peer chat from its inherited chat
1370 > * turns, replacing the placeholder `Forked: …` title once ready.
1371 > */
1372 > generateForkedTitle(channel: ProtocolURI, chatChannel: ProtocolURI | undefined, turns: readonly Turn[], fallbackTitle: string, sourceTitle?: string): void {
1373 this._titleController.generateForkedTitle(channel, chatChannel, turns, fallbackTitle, sourceTitle);
1374 }
1376 > /**
1377 > * Persists a session metadata key/value pair to the session database.
1378 > * Used for fields the host needs to remember across restarts (custom
1379 > * title, isRead/isArchived flags, merged config values).
1380 > */
1381 > private _persistSessionFlag(session: ProtocolURI, key: string, value: string): void {
1382 persistSessionMetadata(this._options.sessionDataService, this._logService, session, key, value);
1383 }
1385 > private _persistChatDraft(channel: ProtocolURI, draft: Message | undefined): void {
1386 if (!isAhpChatChannel(channel)) {
1387 return;
1401 });
1402 }
1404 > /**
1405 > * Pushes the current pending message state from the chat to the agent.
1406 > * The server controls queued message consumption; only steering messages
1407 > * are forwarded to the agent for mid-turn injection.
1408 > */
1409 > private _syncPendingMessages(chatChannel: ProtocolURI): void {
1410 const sessionChannel = parseRequiredSessionUriFromChatUri(chatChannel);
1411 const state = this._stateManager.getSessionState(chatChannel);
1427 this._tryConsumeNextQueuedMessage(chatChannel);
1428 }
1430 > /**
1431 > * Consumes the next queued message by dispatching a server-initiated
1432 > * `ChatTurnStarted` action with `queuedMessageId` set. The reducer
1433 > * atomically creates the active turn and removes the message from the
1434 > * queue. Only consumes one message at a time; subsequent messages are
1435 > * consumed when the next `idle` event fires.
1436 > */
1437 > private _tryConsumeNextQueuedMessage(session: ProtocolURI): void {
1438 const sessionChannel = parseRequiredSessionUriFromChatUri(session);
1439 // Bail if there's already an active turn
1508 });
1509 }
1511 >
1512 > private _getTurnTelemetryContext(agent: IAgent, state: SessionState | undefined, modelId: string | undefined): { model: string | undefined; modelTelemetryKind: AgentHostModelTelemetryKind | undefined; permissionLevel: string | undefined } {
1513 const permissionValue = state?.config?.values[SessionConfigKey.AutoApprove];
1514 const permissionLevel = typeof permissionValue === 'string' ? permissionValue : undefined;
1526 return { model: modelId, modelTelemetryKind, permissionLevel };
1527 }
1529 > /**
1530 > * Applies a turn message's model/agent selection (see
1531 > * {@link _applyMessageSelection}) and forwards it to the agent's
1532 > * `sendMessage`. A rejected send is wired to fail the turn: it logs,
1533 > * dispatches {@link ActionType.ChatError} on the turn channel, and marks the
1534 > * turn errored.
1535 > */
1536 > private async _sendTurnMessage(options: {
1537 agent: IAgent;
1538 /** The agent/session URI the chat lives on (the send target). */
1615 }
1616 }
1618 > private async _resolveChatAttachments(sessionChannel: ProtocolURI, attachments: readonly MessageAttachment[] | undefined): Promise<readonly MessageAttachment[] | undefined> {
1619 if (!attachments?.some(attachment => attachment.type === MessageAttachmentKind.Chat)) {
1620 return attachments;
1640 }));
1641 }
1643 > private _resolveSourceChatState(sourceUri: string) {
1644 const peerState = this._stateManager.getChatState(sourceUri);
1645 if (peerState) {
1654 return undefined;
1655 }
1657 > /**
1658 > * Surfaces a failed first turn on a not-yet-materialized session as a
1659 > * terminal creation failure.
1660 > *
1661 > * Provisional sessions defer both their root-catalog `SessionAdded`
1662 > * notification and their `Creating -> Ready` lifecycle transition until the
1663 > * agent materializes them (worktree setup, SDK session init, …) on the
1664 > * first `sendMessage`. When that first send rejects — e.g. worktree/branch
1665 > * creation throws — the session never entered the catalog and its lifecycle
1666 > * is stuck at `Creating`, so clients that optimistically rendered it as
1667 > * in-progress keep spinning forever.
1668 > *
1669 > * When the failing session is still `Creating`, dispatch
1670 > * {@link ActionType.SessionCreationFailed} to move it to a terminal
1671 > * `CreationFailed` lifecycle, then announce its catalog entry via
1672 > * {@link AgentHostStateManager.markSessionPersisted}. The summary's status
1673 > * was already aggregated to `Error` by the preceding `ChatError` dispatch,
1674 > * so subscribers render the session as failed immediately rather than
1675 > * waiting on a client-side timeout. The provisional session survives on the
1676 > * agent, so resending re-attempts materialization.
1677 > */
1678 > private _failSessionCreationIfStillCreating(sessionChannel: ProtocolURI, error: ErrorInfo): void {
1679 const state = this._stateManager.getSessionState(sessionChannel);
1680 if (state?.lifecycle !== SessionLifecycle.Creating) {
1690 }
1691 }
1693 >
1694 > override dispose(): void {
1695 > this._toolCallAgents.clear(); agentSideEffects.ts
1696 > this._toolCallTracker.clear();
1697 > super.dispose();
1698 > }
1700 >
1701 > /**
1702 > * Builds the {@link ErrorInfo} for a failed `sendMessage` rejection. When the
1703 > * rejection text carries a `VSCODE_PROXY_ERROR` marker (embedded by a model
1704 > * proxy and echoed back through the agent SDK), the decoded structured chat
1705 > * error is attached to `_meta.chatError` so core can render a rich, localized
1706 > * message. Otherwise the raw error message is used as-is.
1707 > */
1708 function buildTurnFailure(stage: AgentHostTurnFailureStage, err: unknown): IAgentHostTurnFailure {
1709 const error = buildTurnFailureError(stage, err);
1716 };
1717 }
1719 function buildTurnFailureError(stage: AgentHostTurnFailureStage, err: unknown): ErrorInfo {
1720 const message = String(err);
src/vs/base/common/strings.ts 488 covered LOC · 114 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- strings.ts
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 > import { LRUCachedFunction } from './cache.js';
7 > import { CharCode } from './charCode.js';
8 > import { Lazy } from './lazy.js';
9 > import { Constants } from './uint.js';
10 >
11 > export function isFalsyOrWhitespace(str: string | undefined): boolean {
12 if (!str || typeof str !== 'string') {
13 return true;
15 return str.trim().length === 0;
16 }
17 > strings.ts
18 > const _formatRegexp = /{(\d+)}/g;
19 >
20 > /**
21 > * Helper to produce a string with a variable number of arguments. Insert variable segments
22 > * into the string using the {n} notation where N is the index of the argument following the string.
23 > * @param value string to which formatting is applied
24 > * @param args replacements for {n}-entries
25 > */
26 > // eslint-disable-next-line @typescript-eslint/no-explicit-any
27 > export function format(value: string, ...args: any[]): string {
28 if (args.length === 0) {
29 return value;
36 });
37 }
38 > strings.ts
39 > const _format2Regexp = /{([^}]+)}/g;
40 >
41 > /**
42 > * Helper to create a string from a template and a string record.
43 > * Similar to `format` but with objects instead of positional arguments.
44 > */
45 > export function format2(template: string, values: Record<string, unknown>): string {
46 if (Object.keys(values).length === 0) {
47 return template;
49 return template.replace(_format2Regexp, (match, group) => (values[group] ?? match) as string);
50 }
51 > strings.ts
52 > /**
53 > * Encodes the given value so that it can be used as literal value in html attributes.
54 > *
55 > * In other words, computes `$val`, such that `attr` in `<div attr="$val" />` has the runtime value `value`.
56 > * This prevents XSS injection.
57 > */
58 > export function htmlAttributeEncodeValue(value: string): string {
59 return value.replace(/[<>"'&]/g, ch => {
60 switch (ch) {
68 });
69 }
70 > strings.ts
71 > /**
72 > * Converts HTML characters inside the string to use entities instead. Makes the string safe from
73 > * being used e.g. in HTMLElement.innerHTML.
74 > */
75 > export function escape(html: string): string {
76 return html.replace(/[<>&]/g, function (match) {
77 switch (match) {
83 });
84 }
85 > strings.ts
86 > /**
87 > * Escapes regular expression characters in a given string
88 > */
89 > export function escapeRegExpCharacters(value: string): string {
90 return value.replace(/[\\\{\}\*\+\?\|\^\$\.\[\]\(\)]/g, '\\$&');
91 }
92 > strings.ts
93 > /**
94 > * Counts how often `substr` occurs inside `value`.
95 > */
96 > export function count(value: string, substr: string): number {
97 let result = 0;
98 let index = value.indexOf(substr);
103 return result;
104 }
105 > strings.ts
106 > export function truncate(value: string, maxLength: number, suffix = Ellipsis): string {
107 if (value.length <= maxLength) {
108 return value;
111 return `${value.substr(0, maxLength)}${suffix}`;
112 }
113 > strings.ts
114 > export function truncateMiddle(value: string, maxLength: number, suffix = Ellipsis): string {
115 if (value.length <= maxLength) {
116 return value;
122 return `${value.substr(0, prefixLength)}${suffix}${value.substr(value.length - suffixLength)}`;
123 }
124 > strings.ts
125 > /**
126 > * Removes all occurrences of needle from the beginning and end of haystack.
127 > * @param haystack string to trim
128 > * @param needle the thing to trim (default is a blank)
129 > */
130 > export function trim(haystack: string, needle: string = ' '): string {
131 const trimmed = ltrim(haystack, needle);
132 return rtrim(trimmed, needle);
133 }
134 > strings.ts
135 > /**
136 > * Removes all occurrences of needle from the beginning of haystack.
137 > * @param haystack string to trim
138 > * @param needle the thing to trim
139 > */
140 > export function ltrim(haystack: string, needle: string): string {
141 if (!haystack || !needle) {
142 return haystack;
157 return haystack.substring(offset);
158 }
159 > strings.ts
160 > /**
161 > * Removes all occurrences of needle from the end of haystack.
162 > * @param haystack string to trim
163 > * @param needle the thing to trim
164 > */
165 > export function rtrim(haystack: string, needle: string): string {
166 if (!haystack || !needle) {
167 return haystack;
187 return haystack.substring(0, offset);
188 }
189 > strings.ts
190 > export function convertSimple2RegExpPattern(pattern: string): string {
191 return pattern.replace(/[\-\\\{\}\+\?\|\^\$\.\,\[\]\(\)\#\s]/g, '\\$&').replace(/[\*]/g, '.*');
192 }
193 > strings.ts
194 > export interface RegExpOptions {
195 > matchCase?: boolean;
196 > wholeWord?: boolean;
197 > multiline?: boolean;
198 > global?: boolean;
199 > unicode?: boolean;
200 > }
201 >
202 > export function createRegExp(searchString: string, isRegex: boolean, options: RegExpOptions = {}): RegExp {
203 if (!searchString) {
204 throw new Error('Cannot create regex from empty string');
231 return new RegExp(searchString, modifiers);
232 }
233 > strings.ts
234 > export function regExpLeadsToEndlessLoop(regexp: RegExp): boolean {
235 // Exit early if it's one of these special cases which are meant to match
236 // against an empty string
244 return !!(match && regexp.lastIndex === 0);
245 }
246 > strings.ts
247 > export function joinStrings(items: (string | undefined | null | false)[], separator: string): string {
248 return items.filter(item => item !== undefined && item !== null && item !== false).join(separator);
249 }
250 > strings.ts
251 > export function splitLines(str: string): string[] {
252 return str.split(/\r\n|\r|\n/);
253 }
254 > strings.ts
255 > export function splitLinesIncludeSeparators(str: string): string[] {
256 const linesWithSeparators: string[] = [];
257 const splitLinesAndSeparators = str.split(/(\r\n|\r|\n)/);
261 return linesWithSeparators;
262 }
263 > strings.ts
264 > export function indexOfPattern(str: string, re: RegExp) {
265 const match = re.exec(str);
266 if (match) {
269 return -1;
270 }
271 > strings.ts
272 > /**
273 > * Returns first index of the string that is not whitespace.
274 > * If string is empty or contains only whitespaces, returns -1
275 > */
276 > export function firstNonWhitespaceIndex(str: string): number {
277 for (let i = 0, len = str.length; i < len; i++) {
278 const chCode = str.charCodeAt(i);
283 return -1;
284 }
285 > strings.ts
286 > /**
287 > * Returns the leading whitespace of the string.
288 > * If the string contains only whitespaces, returns entire string
289 > */
290 > export function getLeadingWhitespace(str: string, start: number = 0, end: number = str.length): string {
291 for (let i = start; i < end; i++) {
292 const chCode = str.charCodeAt(i);
297 return str.substring(start, end);
298 }
299 > strings.ts
300 > /**
301 > * Returns last index of the string that is not whitespace.
302 > * If string is empty or contains only whitespaces, returns -1
303 > */
304 > export function lastNonWhitespaceIndex(str: string, startIndex: number = str.length - 1): number {
305 for (let i = startIndex; i >= 0; i--) {
306 const chCode = str.charCodeAt(i);
311 return -1;
312 }
313 > strings.ts
314 > export function getIndentationLength(str: string): number {
315 const idx = firstNonWhitespaceIndex(str);
316 if (idx === -1) { return str.length; }
317 return idx;
318 }
319 > strings.ts
320 > /**
321 > * Function that works identically to String.prototype.replace, except, the
322 > * replace function is allowed to be async and return a Promise.
323 > */
324 > export function replaceAsync(str: string, search: RegExp, replacer: (match: string, ...args: unknown[]) => Promise<string>): Promise<string> {
325 const parts: (string | Promise<string>)[] = [];
326
340 return Promise.all(parts).then(p => p.join(''));
341 }
342 > strings.ts
343 > export function compare(a: string, b: string): number {
344 if (a < b) {
345 return -1;
350 }
351 }
352 > strings.ts
353 > export function compareSubstring(a: string, b: string, aStart: number = 0, aEnd: number = a.length, bStart: number = 0, bEnd: number = b.length): number {
354 > for (; aStart < aEnd && bStart < bEnd; aStart++, bStart++) { strings.ts
355 > const codeA = a.charCodeAt(aStart);
356 > const codeB = b.charCodeAt(bStart);
357 > if (codeA < codeB) {
358 return -1;
359 > } else if (codeA > codeB) { strings.ts
360 return 1;
361 }
362 > } strings.ts
363 > const aLen = aEnd - aStart; strings.ts
364 > const bLen = bEnd - bStart;
365 > if (aLen < bLen) {
366 return -1;
367 > } else if (aLen > bLen) { strings.ts
368 return 1;
369 }
370 > return 0; strings.ts
371 > }
372 > strings.ts
373 > export function compareIgnoreCase(a: string, b: string): number {
374 > return compareSubstringIgnoreCase(a, b, 0, a.length, 0, b.length); strings.ts
375 > }
376 > strings.ts
377 > export function compareSubstringIgnoreCase(a: string, b: string, aStart: number = 0, aEnd: number = a.length, bStart: number = 0, bEnd: number = b.length): number {
378 > strings.ts
379 > for (; aStart < aEnd && bStart < bEnd; aStart++, bStart++) {
380 >
381 > let codeA = a.charCodeAt(aStart);
382 > let codeB = b.charCodeAt(bStart);
383 >
384 > if (codeA === codeB) {
385 > // equal strings.ts
386 > continue;
387 > }
388
389 > if (codeA >= 128 || codeB >= 128) { strings.ts
390 // not ASCII letters -> fallback to lower-casing strings
391 return compareSubstring(a.toLowerCase(), b.toLowerCase(), aStart, aEnd, bStart, bEnd);
409 return diff;
410 }
411 > strings.ts
412 > const aLen = aEnd - aStart;
413 > const bLen = bEnd - bStart;
414 >
415 > if (aLen < bLen) {
416 return -1;
417 > } else if (aLen > bLen) { strings.ts
418 return 1;
419 }
420 > strings.ts
421 > return 0;
422 > }
423 > strings.ts
424 > export function isAsciiDigit(code: number): boolean {
425 return code >= CharCode.Digit0 && code <= CharCode.Digit9;
426 }
427 > strings.ts
428 > export function isLowerAsciiLetter(code: number): boolean {
429 return code >= CharCode.a && code <= CharCode.z;
430 }
431 > strings.ts
432 > export function isUpperAsciiLetter(code: number): boolean {
433 return code >= CharCode.A && code <= CharCode.Z;
434 }
435 > strings.ts
436 > export function equalsIgnoreCase(a: string, b: string): boolean {
437 return a.length === b.length && compareSubstringIgnoreCase(a, b) === 0;
438 }
439 > strings.ts
440 > export function equals(a: string | undefined, b: string | undefined, ignoreCase?: boolean): boolean {
441 return a === b || (!!ignoreCase && a !== undefined && b !== undefined && equalsIgnoreCase(a, b));
442 }
443 > strings.ts
444 > export function startsWithIgnoreCase(str: string, candidate: string): boolean {
445 const len = candidate.length;
446 return len <= str.length && compareSubstringIgnoreCase(str, candidate, 0, len) === 0;
447 }
448 > strings.ts
449 > export function endsWithIgnoreCase(str: string, candidate: string): boolean {
450 const len = str.length;
451 const start = len - candidate.length;
452 return start >= 0 && compareSubstringIgnoreCase(str, candidate, start, len) === 0;
453 }
454 > strings.ts
455 > /**
456 > * @returns the length of the common prefix of the two strings.
457 > */
458 > export function commonPrefixLength(a: string, b: string): number {
459
460 const len = Math.min(a.length, b.length);
469 return len;
470 }
471 > strings.ts
472 > /**
473 > * @returns the length of the common suffix of the two strings.
474 > */
475 > export function commonSuffixLength(a: string, b: string): number {
476
477 const len = Math.min(a.length, b.length);
489 return len;
490 }
491 > strings.ts
492 > /**
493 > * See http://en.wikipedia.org/wiki/Surrogate_pair
494 > */
495 > export function isHighSurrogate(charCode: number): boolean {
496 return (0xD800 <= charCode && charCode <= 0xDBFF);
497 }
498 > strings.ts
499 > /**
500 > * See http://en.wikipedia.org/wiki/Surrogate_pair
501 > */
502 > export function isLowSurrogate(charCode: number): boolean {
503 return (0xDC00 <= charCode && charCode <= 0xDFFF);
504 }
505 > strings.ts
506 > /**
507 > * See http://en.wikipedia.org/wiki/Surrogate_pair
508 > */
509 > export function computeCodePoint(highSurrogate: number, lowSurrogate: number): number {
510 return ((highSurrogate - 0xD800) << 10) + (lowSurrogate - 0xDC00) + 0x10000;
511 }
512 > strings.ts
513 > /**
514 > * get the code point that begins at offset `offset`
515 > */
516 > export function getNextCodePoint(str: string, len: number, offset: number): number {
517 const charCode = str.charCodeAt(offset);
518 if (isHighSurrogate(charCode) && offset + 1 < len) {
524 return charCode;
525 }
526 > strings.ts
527 > /**
528 > * get the code point that ends right before offset `offset`
529 > */
530 function getPrevCodePoint(str: string, offset: number): number {
531 const charCode = str.charCodeAt(offset - 1);
538 return charCode;
539 }
540 > strings.ts
541 > export class CodePointIterator {
542 >
543 > private readonly _str: string;
544 > private readonly _len: number;
545 > private _offset: number;
546 >
547 > public get offset(): number {
548 return this._offset;
549 }
550 > strings.ts
551 > constructor(str: string, offset: number = 0) {
552 this._str = str;
553 this._len = str.length;
554 this._offset = offset;
555 }
556 > strings.ts
557 > public setOffset(offset: number): void {
558 this._offset = offset;
559 }
560 > strings.ts
561 > public prevCodePoint(): number {
562 const codePoint = getPrevCodePoint(this._str, this._offset);
563 this._offset -= (codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1);
564 return codePoint;
565 }
566 > strings.ts
567 > public nextCodePoint(): number {
568 const codePoint = getNextCodePoint(this._str, this._len, this._offset);
569 this._offset += (codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1);
570 return codePoint;
571 }
572 > strings.ts
573 > public eol(): boolean {
574 return (this._offset >= this._len);
575 }
576 > } strings.ts
577 >
578 > export class GraphemeIterator {
579 >
580 > private readonly _iterator: CodePointIterator;
581 >
582 > public get offset(): number {
583 return this._iterator.offset;
584 }
585 > strings.ts
586 > constructor(str: string, offset: number = 0) {
587 this._iterator = new CodePointIterator(str, offset);
588 }
589 > strings.ts
590 > public nextGraphemeLength(): number {
591 const graphemeBreakTree = GraphemeBreakTree.getInstance();
592 const iterator = this._iterator;
606 return (iterator.offset - initialOffset);
607 }
608 > strings.ts
609 > public prevGraphemeLength(): number {
610 const graphemeBreakTree = GraphemeBreakTree.getInstance();
611 const iterator = this._iterator;
625 return (initialOffset - iterator.offset);
626 }
627 > strings.ts
628 > public eol(): boolean {
629 return this._iterator.eol();
630 }
631 > } strings.ts
632 >
633 > export function nextCharLength(str: string, initialOffset: number): number {
634 const iterator = new GraphemeIterator(str, initialOffset);
635 return iterator.nextGraphemeLength();
636 }
637 > strings.ts
638 > export function prevCharLength(str: string, initialOffset: number): number {
639 const iterator = new GraphemeIterator(str, initialOffset);
640 return iterator.prevGraphemeLength();
641 }
642 > strings.ts
643 > export function getCharContainingOffset(str: string, offset: number): [number, number] {
644 if (offset > 0 && isLowSurrogate(str.charCodeAt(offset))) {
645 offset--;
649 return [startOffset, endOffset];
650 }
651 > strings.ts
652 > export function charCount(str: string): number {
653 const iterator = new GraphemeIterator(str);
654 let length = 0;
659 return length;
660 }
661 > strings.ts
662 > let CONTAINS_RTL: RegExp | undefined = undefined;
663 >
664 function makeContainsRtl() {
665 // Generated using https://github.com/alexdima/unicode-utils/blob/main/rtl-test.js
666 return /(?:[\u05BE\u05C0\u05C3\u05C6\u05D0-\u05F4\u0608\u060B\u060D\u061B-\u064A\u066D-\u066F\u0671-\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u0710\u0712-\u072F\u074D-\u07A5\u07B1-\u07EA\u07F4\u07F5\u07FA\u07FE-\u0815\u081A\u0824\u0828\u0830-\u0858\u085E-\u088E\u08A0-\u08C9\u200F\uFB1D\uFB1F-\uFB28\uFB2A-\uFD3D\uFD50-\uFDC7\uFDF0-\uFDFC\uFE70-\uFEFC]|\uD802[\uDC00-\uDD1B\uDD20-\uDE00\uDE10-\uDE35\uDE40-\uDEE4\uDEEB-\uDF35\uDF40-\uDFFF]|\uD803[\uDC00-\uDD23\uDE80-\uDEA9\uDEAD-\uDF45\uDF51-\uDF81\uDF86-\uDFF6]|\uD83A[\uDC00-\uDCCF\uDD00-\uDD43\uDD4B-\uDFFF]|\uD83B[\uDC00-\uDEBB])/;
667 }
668 > strings.ts
669 > /**
670 > * Returns true if `str` contains any Unicode character that is classified as "R" or "AL".
671 > */
672 > export function containsRTL(str: string): boolean {
673 if (!CONTAINS_RTL) {
674 CONTAINS_RTL = makeContainsRtl();
677 return CONTAINS_RTL.test(str);
678 }
679 > strings.ts
680 > const IS_BASIC_ASCII = /^[\t\n\r\x20-\x7E]*$/;
681 > /**
682 > * Returns true if `str` contains only basic ASCII characters in the range 32 - 126 (including 32 and 126) or \n, \r, \t
683 > */
684 > export function isBasicASCII(str: string): boolean {
685 return IS_BASIC_ASCII.test(str);
686 }
687 > strings.ts
688 > export const UNUSUAL_LINE_TERMINATORS = /[\u2028\u2029]/; // LINE SEPARATOR (LS) or PARAGRAPH SEPARATOR (PS)
689 > /**
690 > * Returns true if `str` contains unusual line terminators, like LS or PS
691 > */
692 > export function containsUnusualLineTerminators(str: string): boolean {
693 return UNUSUAL_LINE_TERMINATORS.test(str);
694 }
695 > strings.ts
696 > export function isFullWidthCharacter(charCode: number): boolean {
697 // Do a cheap trick to better support wrapping of wide characters, treat them as 2 columns
698 // http://jrgraphix.net/research/unicode_blocks.php
741 );
742 }
743 > strings.ts
744 > /**
745 > * A fast function (therefore imprecise) to check if code points are emojis.
746 > * Generated using https://github.com/alexdima/unicode-utils/blob/main/emoji-test.js
747 > */
748 > export function isEmojiImprecise(x: number): boolean {
749 return (
750 (x >= 0x1F1E6 && x <= 0x1F1FF) || (x === 8986) || (x === 8987) || (x === 9200)
755 );
756 }
757 > strings.ts
758 > /**
759 > * Given a string and a max length returns a shorted version. Shorting
760 > * happens at favorable positions - such as whitespace or punctuation characters.
761 > * The return value can be longer than the given value of `n`. Leading whitespace is always trimmed.
762 > */
763 > export function lcut(text: string, n: number, prefix = ''): string {
764 const trimmed = text.trimStart();
765
785 return prefix + trimmed.substring(i).trimStart();
786 }
787 > strings.ts
788 > /**
789 > * Given a string and a max length returns a shortened version keeping the beginning.
790 > * Shortening happens at favorable positions - such as whitespace or punctuation characters.
791 > * Trailing whitespace is always trimmed.
792 > */
793 > export function rcut(text: string, n: number, suffix = ''): string {
794 const trimmed = text.trimEnd();
795
832 return result + suffix;
833 }
834 > strings.ts
835 > // Defacto standard: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
836 > const CSI_SEQUENCE = /(?:\x1b\[|\x9b)[=?>!]?[\d;:]*["$#'* ]?[a-zA-Z@^`{}|~]/;
837 > const OSC_SEQUENCE = /(?:\x1b\]|\x9d).*?(?:\x1b\\|\x07|\x9c)/;
838 > const ESC_SEQUENCE = /\x1b(?:[ #%\(\)\*\+\-\.\/]?[a-zA-Z0-9\|}~@])/;
839 > const CONTROL_SEQUENCES = new RegExp('(?:' + [
840 > CSI_SEQUENCE.source,
841 > OSC_SEQUENCE.source,
842 > ESC_SEQUENCE.source,
843 > ].join('|') + ')', 'g');
844 >
845 > /** Iterates over parts of a string with CSI sequences */
846 > export function* forAnsiStringParts(str: string) {
847 let last = 0;
848 for (const match of str.matchAll(CONTROL_SEQUENCES)) {
859 }
860 }
861 > strings.ts
862 > /**
863 > * Strips ANSI escape sequences from a string.
864 > * @param str The dastringa stringo strip the ANSI escape sequences from.
865 > *
866 > * @example
867 > * removeAnsiEscapeCodes('\u001b[31mHello, World!\u001b[0m');
868 > * // 'Hello, World!'
869 > */
870 > export function removeAnsiEscapeCodes(str: string): string {
871 if (str) {
872 str = str.replace(CONTROL_SEQUENCES, '');
875 return str;
876 }
877 > strings.ts
878 > const PROMPT_NON_PRINTABLE = /\\\[.*?\\\]/g;
879 >
880 > /**
881 > * Strips ANSI escape sequences from a UNIX-style prompt string (eg. `$PS1`).
882 > * @param str The string to strip the ANSI escape sequences from.
883 > *
884 > * @example
885 > * removeAnsiEscapeCodesFromPrompt('\n\\[\u001b[01;34m\\]\\w\\[\u001b[00m\\]\n\\[\u001b[1;32m\\]> \\[\u001b[0m\\]');
886 > * // '\n\\w\n> '
887 > */
888 > export function removeAnsiEscapeCodesFromPrompt(str: string): string {
889 return removeAnsiEscapeCodes(str).replace(PROMPT_NON_PRINTABLE, '');
890 }
891 > strings.ts
892 >
893 > // -- UTF-8 BOM
894 >
895 > export const UTF8_BOM_CHARACTER = String.fromCharCode(CharCode.UTF8_BOM);
896 >
897 > export function startsWithUTF8BOM(str: string): boolean {
898 return !!(str && str.length > 0 && str.charCodeAt(0) === CharCode.UTF8_BOM);
899 }
900 > strings.ts
901 > export function stripUTF8BOM(str: string): string {
902 return startsWithUTF8BOM(str) ? str.substr(1) : str;
903 }
904 > strings.ts
905 > /**
906 > * Checks if the characters of the provided query string are included in the
907 > * target string. The characters do not have to be contiguous within the string.
908 > */
909 > export function fuzzyContains(target: string, query: string): boolean {
910 if (!target || !query) {
911 return false; // return early if target or query are undefined
936 return true;
937 }
938 > strings.ts
939 > export function containsUppercaseCharacter(target: string, ignoreEscapedChars = false): boolean {
940 if (!target) {
941 return false;
948 return target.toLowerCase() !== target;
949 }
950 > strings.ts
951 > export function uppercaseFirstLetter(str: string): string {
952 return str.charAt(0).toUpperCase() + str.slice(1);
953 }
954 > strings.ts
955 > export function getNLines(str: string, n = 1): string {
956 if (n === 0) {
957 return '';
974 return str.substr(0, idx);
975 }
976 > strings.ts
977 > /**
978 > * Produces 'a'-'z', followed by 'A'-'Z'... followed by 'a'-'z', etc.
979 > */
980 > export function singleLetterHash(n: number): string {
981 const LETTERS_CNT = (CharCode.Z - CharCode.A + 1);
982
989 return String.fromCharCode(CharCode.A + n - LETTERS_CNT);
990 }
991 > strings.ts
992 > //#region Unicode Grapheme Break
993 >
994 > export function getGraphemeBreakType(codePoint: number): GraphemeBreakType {
995 const graphemeBreakTree = GraphemeBreakTree.getInstance();
996 return graphemeBreakTree.getGraphemeBreakType(codePoint);
997 }
998 > strings.ts
999 function breakBetweenGraphemeBreakType(breakTypeA: GraphemeBreakType, breakTypeB: GraphemeBreakType): boolean {
1000 // http://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules
1076 return true;
1077 }
1078 > strings.ts
1079 > export const enum GraphemeBreakType {
1080 > Other = 0,
1081 > Prepend = 1,
1082 > CR = 2,
1083 > LF = 3,
1084 > Control = 4,
1085 > Extend = 5,
1086 > Regional_Indicator = 6,
1087 > SpacingMark = 7,
1088 > L = 8,
1089 > V = 9,
1090 > T = 10,
1091 > LV = 11,
1092 > LVT = 12,
1093 > ZWJ = 13,
1094 > Extended_Pictographic = 14
1095 > }
1096 >
1097 > class GraphemeBreakTree {
1098 >
1099 > private static _INSTANCE: GraphemeBreakTree | null = null;
1100 > public static getInstance(): GraphemeBreakTree {
1101 if (!GraphemeBreakTree._INSTANCE) {
1102 GraphemeBreakTree._INSTANCE = new GraphemeBreakTree();
1104 return GraphemeBreakTree._INSTANCE;
1105 }
1106 > strings.ts
1107 > private readonly _data: number[];
1108 >
1109 > constructor() {
1110 this._data = getGraphemeBreakRawData();
1111 }
1112 > strings.ts
1113 > public getGraphemeBreakType(codePoint: number): GraphemeBreakType {
1114 // !!! Let's make 7bit ASCII a bit faster: 0..31
1115 if (codePoint < 32) {
1145 return GraphemeBreakType.Other;
1146 }
1147 > } strings.ts
1148 >
1149 function getGraphemeBreakRawData(): number[] {
1150 // generated using https://github.com/alexdima/unicode-utils/blob/main/grapheme-break.js
1151 return JSON.parse('[0,0,0,51229,51255,12,44061,44087,12,127462,127487,6,7083,7085,5,47645,47671,12,54813,54839,12,128678,128678,14,3270,3270,5,9919,9923,14,45853,45879,12,49437,49463,12,53021,53047,12,71216,71218,7,128398,128399,14,129360,129374,14,2519,2519,5,4448,4519,9,9742,9742,14,12336,12336,14,44957,44983,12,46749,46775,12,48541,48567,12,50333,50359,12,52125,52151,12,53917,53943,12,69888,69890,5,73018,73018,5,127990,127990,14,128558,128559,14,128759,128760,14,129653,129655,14,2027,2035,5,2891,2892,7,3761,3761,5,6683,6683,5,8293,8293,4,9825,9826,14,9999,9999,14,43452,43453,5,44509,44535,12,45405,45431,12,46301,46327,12,47197,47223,12,48093,48119,12,48989,49015,12,49885,49911,12,50781,50807,12,51677,51703,12,52573,52599,12,53469,53495,12,54365,54391,12,65279,65279,4,70471,70472,7,72145,72147,7,119173,119179,5,127799,127818,14,128240,128244,14,128512,128512,14,128652,128652,14,128721,128722,14,129292,129292,14,129445,129450,14,129734,129743,14,1476,1477,5,2366,2368,7,2750,2752,7,3076,3076,5,3415,3415,5,4141,4144,5,6109,6109,5,6964,6964,5,7394,7400,5,9197,9198,14,9770,9770,14,9877,9877,14,9968,9969,14,10084,10084,14,43052,43052,5,43713,43713,5,44285,44311,12,44733,44759,12,45181,45207,12,45629,45655,12,46077,46103,12,46525,46551,12,46973,46999,12,47421,47447,12,47869,47895,12,48317,48343,12,48765,48791,12,49213,49239,12,49661,49687,12,50109,50135,12,50557,50583,12,51005,51031,12,51453,51479,12,51901,51927,12,52349,52375,12,52797,52823,12,53245,53271,12,53693,53719,12,54141,54167,12,54589,54615,12,55037,55063,12,69506,69509,5,70191,70193,5,70841,70841,7,71463,71467,5,72330,72342,5,94031,94031,5,123628,123631,5,127763,127765,14,127941,127941,14,128043,128062,14,128302,128317,14,128465,128467,14,128539,128539,14,128640,128640,14,128662,128662,14,128703,128703,14,128745,128745,14,129004,129007,14,129329,129330,14,129402,129402,14,129483,129483,14,129686,129704,14,130048,131069,14,173,173,4,1757,1757,1,2200,2207,5,2434,2435,7,2631,2632,5,2817,2817,5,3008,3008,5,3201,3201,5,3387,3388,5,3542,3542,5,3902,3903,7,4190,4192,5,6002,6003,5,6439,6440,5,6765,6770,7,7019,7027,5,7154,7155,7,8205,8205,13,8505,8505,14,9654,9654,14,9757,9757,14,9792,9792,14,9852,9853,14,9890,9894,14,9937,9937,14,9981,9981,14,10035,10036,14,11035,11036,14,42654,42655,5,43346,43347,7,43587,43587,5,44006,44007,7,44173,44199,12,44397,44423,12,44621,44647,12,44845,44871,12,45069,45095,12,45293,45319,12,45517,45543,12,45741,45767,12,45965,45991,12,46189,46215,12,46413,46439,12,46637,46663,12,46861,46887,12,47085,47111,12,47309,47335,12,47533,47559,12,47757,47783,12,47981,48007,12,48205,48231,12,48429,48455,12,48653,48679,12,48877,48903,12,49101,49127,12,49325,49351,12,49549,49575,12,49773,49799,12,49997,50023,12,50221,50247,12,50445,50471,12,50669,50695,12,50893,50919,12,51117,51143,12,51341,51367,12,51565,51591,12,51789,51815,12,52013,52039,12,52237,52263,12,52461,52487,12,52685,52711,12,52909,52935,12,53133,53159,12,53357,53383,12,53581,53607,12,53805,53831,12,54029,54055,12,54253,54279,12,54477,54503,12,54701,54727,12,54925,54951,12,55149,55175,12,68101,68102,5,69762,69762,7,70067,70069,7,70371,70378,5,70720,70721,7,71087,71087,5,71341,71341,5,71995,71996,5,72249,72249,7,72850,72871,5,73109,73109,5,118576,118598,5,121505,121519,5,127245,127247,14,127568,127569,14,127777,127777,14,127872,127891,14,127956,127967,14,128015,128016,14,128110,128172,14,128259,128259,14,128367,128368,14,128424,128424,14,128488,128488,14,128530,128532,14,128550,128551,14,128566,128566,14,128647,128647,14,128656,128656,14,128667,128673,14,128691,128693,14,128715,128715,14,128728,128732,14,128752,128752,14,128765,128767,14,129096,129103,14,129311,129311,14,129344,129349,14,129394,129394,14,129413,129425,14,129466,129471,14,129511,129535,14,129664,129666,14,129719,129722,14,129760,129767,14,917536,917631,5,13,13,2,1160,1161,5,1564,1564,4,1807,1807,1,2085,2087,5,2307,2307,7,2382,2383,7,2497,2500,5,2563,2563,7,2677,2677,5,2763,2764,7,2879,2879,5,2914,2915,5,3021,3021,5,3142,3144,5,3263,3263,5,3285,3286,5,3398,3400,7,3530,3530,5,3633,3633,5,3864,3865,5,3974,3975,5,4155,4156,7,4229,4230,5,5909,5909,7,6078,6085,7,6277,6278,5,6451,6456,7,6744,6750,5,6846,6846,5,6972,6972,5,7074,7077,5,7146,7148,7,7222,7223,5,7416,7417,5,8234,8238,4,8417,8417,5,9000,9000,14,9203,9203,14,9730,9731,14,9748,9749,14,9762,9763,14,9776,9783,14,9800,9811,14,9831,9831,14,9872,9873,14,9882,9882,14,9900,9903,14,9929,9933,14,9941,9960,14,9974,9974,14,9989,9989,14,10006,10006,14,10062,10062,14,10160,10160,14,11647,11647,5,12953,12953,14,43019,43019,5,43232,43249,5,43443,43443,5,43567,43568,7,43696,43696,5,43765,43765,7,44013,44013,5,44117,44143,12,44229,44255,12,44341,44367,12,44453,44479,12,44565,44591,12,44677,44703,12,44789,44815,12,44901,44927,12,45013,45039,12,45125,45151,12,45237,45263,12,45349,45375,12,45461,45487,12,45573,45599,12,45685,45711,12,45797,45823,12,45909,45935,12,46021,46047,12,46133,46159,12,46245,46271,12,46357,46383,12,46469,46495,12,46581,46607,12,46693,46719,12,46805,46831,12,46917,46943,12,47029,47055,12,47141,47167,12,47253,47279,12,47365,47391,12,47477,47503,12,47589,47615,12,47701,47727,12,47813,47839,12,47925,47951,12,48037,48063,12,48149,48175,12,48261,48287,12,48373,48399,12,48485,48511,12,48597,48623,12,48709,48735,12,48821,48847,12,48933,48959,12,49045,49071,12,49157,49183,12,49269,49295,12,49381,49407,12,49493,49519,12,49605,49631,12,49717,49743,12,49829,49855,12,49941,49967,12,50053,50079,12,50165,50191,12,50277,50303,12,50389,50415,12,50501,50527,12,50613,50639,12,50725,50751,12,50837,50863,12,50949,50975,12,51061,51087,12,51173,51199,12,51285,51311,12,51397,51423,12,51509,51535,12,51621,51647,12,51733,51759,12,51845,51871,12,51957,51983,12,52069,52095,12,52181,52207,12,52293,52319,12,52405,52431,12,52517,52543,12,52629,52655,12,52741,52767,12,52853,52879,12,52965,52991,12,53077,53103,12,53189,53215,12,53301,53327,12,53413,53439,12,53525,53551,12,53637,53663,12,53749,53775,12,53861,53887,12,53973,53999,12,54085,54111,12,54197,54223,12,54309,54335,12,54421,54447,12,54533,54559,12,54645,54671,12,54757,54783,12,54869,54895,12,54981,55007,12,55093,55119,12,55243,55291,10,66045,66045,5,68325,68326,5,69688,69702,5,69817,69818,5,69957,69958,7,70089,70092,5,70198,70199,5,70462,70462,5,70502,70508,5,70750,70750,5,70846,70846,7,71100,71101,5,71230,71230,7,71351,71351,5,71737,71738,5,72000,72000,7,72160,72160,5,72273,72278,5,72752,72758,5,72882,72883,5,73031,73031,5,73461,73462,7,94192,94193,7,119149,119149,7,121403,121452,5,122915,122916,5,126980,126980,14,127358,127359,14,127535,127535,14,127759,127759,14,127771,127771,14,127792,127793,14,127825,127867,14,127897,127899,14,127945,127945,14,127985,127986,14,128000,128007,14,128021,128021,14,128066,128100,14,128184,128235,14,128249,128252,14,128266,128276,14,128335,128335,14,128379,128390,14,128407,128419,14,128444,128444,14,128481,128481,14,128499,128499,14,128526,128526,14,128536,128536,14,128543,128543,14,128556,128556,14,128564,128564,14,128577,128580,14,128643,128645,14,128649,128649,14,128654,128654,14,128660,128660,14,128664,128664,14,128675,128675,14,128686,128689,14,128695,128696,14,128705,128709,14,128717,128719,14,128725,128725,14,128736,128741,14,128747,128748,14,128755,128755,14,128762,128762,14,128981,128991,14,129009,129023,14,129160,129167,14,129296,129304,14,129320,129327,14,129340,129342,14,129356,129356,14,129388,129392,14,129399,129400,14,129404,129407,14,129432,129442,14,129454,129455,14,129473,129474,14,129485,129487,14,129648,129651,14,129659,129660,14,129671,129679,14,129709,129711,14,129728,129730,14,129751,129753,14,129776,129782,14,917505,917505,4,917760,917999,5,10,10,3,127,159,4,768,879,5,1471,1471,5,1536,1541,1,1648,1648,5,1767,1768,5,1840,1866,5,2070,2073,5,2137,2139,5,2274,2274,1,2363,2363,7,2377,2380,7,2402,2403,5,2494,2494,5,2507,2508,7,2558,2558,5,2622,2624,7,2641,2641,5,2691,2691,7,2759,2760,5,2786,2787,5,2876,2876,5,2881,2884,5,2901,2902,5,3006,3006,5,3014,3016,7,3072,3072,5,3134,3136,5,3157,3158,5,3260,3260,5,3266,3266,5,3274,3275,7,3328,3329,5,3391,3392,7,3405,3405,5,3457,3457,5,3536,3537,7,3551,3551,5,3636,3642,5,3764,3772,5,3895,3895,5,3967,3967,7,3993,4028,5,4146,4151,5,4182,4183,7,4226,4226,5,4253,4253,5,4957,4959,5,5940,5940,7,6070,6070,7,6087,6088,7,6158,6158,4,6432,6434,5,6448,6449,7,6679,6680,5,6742,6742,5,6754,6754,5,6783,6783,5,6912,6915,5,6966,6970,5,6978,6978,5,7042,7042,7,7080,7081,5,7143,7143,7,7150,7150,7,7212,7219,5,7380,7392,5,7412,7412,5,8203,8203,4,8232,8232,4,8265,8265,14,8400,8412,5,8421,8432,5,8617,8618,14,9167,9167,14,9200,9200,14,9410,9410,14,9723,9726,14,9733,9733,14,9745,9745,14,9752,9752,14,9760,9760,14,9766,9766,14,9774,9774,14,9786,9786,14,9794,9794,14,9823,9823,14,9828,9828,14,9833,9850,14,9855,9855,14,9875,9875,14,9880,9880,14,9885,9887,14,9896,9897,14,9906,9916,14,9926,9927,14,9935,9935,14,9939,9939,14,9962,9962,14,9972,9972,14,9978,9978,14,9986,9986,14,9997,9997,14,10002,10002,14,10017,10017,14,10055,10055,14,10071,10071,14,10133,10135,14,10548,10549,14,11093,11093,14,12330,12333,5,12441,12442,5,42608,42610,5,43010,43010,5,43045,43046,5,43188,43203,7,43302,43309,5,43392,43394,5,43446,43449,5,43493,43493,5,43571,43572,7,43597,43597,7,43703,43704,5,43756,43757,5,44003,44004,7,44009,44010,7,44033,44059,12,44089,44115,12,44145,44171,12,44201,44227,12,44257,44283,12,44313,44339,12,44369,44395,12,44425,44451,12,44481,44507,12,44537,44563,12,44593,44619,12,44649,44675,12,44705,44731,12,44761,44787,12,44817,44843,12,44873,44899,12,44929,44955,12,44985,45011,12,45041,45067,12,45097,45123,12,45153,45179,12,45209,45235,12,45265,45291,12,45321,45347,12,45377,45403,12,45433,45459,12,45489,45515,12,45545,45571,12,45601,45627,12,45657,45683,12,45713,45739,12,45769,45795,12,45825,45851,12,45881,45907,12,45937,45963,12,45993,46019,12,46049,46075,12,46105,46131,12,46161,46187,12,46217,46243,12,46273,46299,12,46329,46355,12,46385,46411,12,46441,46467,12,46497,46523,12,46553,46579,12,46609,46635,12,46665,46691,12,46721,46747,12,46777,46803,12,46833,46859,12,46889,46915,12,46945,46971,12,47001,47027,12,47057,47083,12,47113,47139,12,47169,47195,12,47225,47251,12,47281,47307,12,47337,47363,12,47393,47419,12,47449,47475,12,47505,47531,12,47561,47587,12,47617,47643,12,47673,47699,12,47729,47755,12,47785,47811,12,47841,47867,12,47897,47923,12,47953,47979,12,48009,48035,12,48065,48091,12,48121,48147,12,48177,48203,12,48233,48259,12,48289,48315,12,48345,48371,12,48401,48427,12,48457,48483,12,48513,48539,12,48569,48595,12,48625,48651,12,48681,48707,12,48737,48763,12,48793,48819,12,48849,48875,12,48905,48931,12,48961,48987,12,49017,49043,12,49073,49099,12,49129,49155,12,49185,49211,12,49241,49267,12,49297,49323,12,49353,49379,12,49409,49435,12,49465,49491,12,49521,49547,12,49577,49603,12,49633,49659,12,49689,49715,12,49745,49771,12,49801,49827,12,49857,49883,12,49913,49939,12,49969,49995,12,50025,50051,12,50081,50107,12,50137,50163,12,50193,50219,12,50249,50275,12,50305,50331,12,50361,50387,12,50417,50443,12,50473,50499,12,50529,50555,12,50585,50611,12,50641,50667,12,50697,50723,12,50753,50779,12,50809,50835,12,50865,50891,12,50921,50947,12,50977,51003,12,51033,51059,12,51089,51115,12,51145,51171,12,51201,51227,12,51257,51283,12,51313,51339,12,51369,51395,12,51425,51451,12,51481,51507,12,51537,51563,12,51593,51619,12,51649,51675,12,51705,51731,12,51761,51787,12,51817,51843,12,51873,51899,12,51929,51955,12,51985,52011,12,52041,52067,12,52097,52123,12,52153,52179,12,52209,52235,12,52265,52291,12,52321,52347,12,52377,52403,12,52433,52459,12,52489,52515,12,52545,52571,12,52601,52627,12,52657,52683,12,52713,52739,12,52769,52795,12,52825,52851,12,52881,52907,12,52937,52963,12,52993,53019,12,53049,53075,12,53105,53131,12,53161,53187,12,53217,53243,12,53273,53299,12,53329,53355,12,53385,53411,12,53441,53467,12,53497,53523,12,53553,53579,12,53609,53635,12,53665,53691,12,53721,53747,12,53777,53803,12,53833,53859,12,53889,53915,12,53945,53971,12,54001,54027,12,54057,54083,12,54113,54139,12,54169,54195,12,54225,54251,12,54281,54307,12,54337,54363,12,54393,54419,12,54449,54475,12,54505,54531,12,54561,54587,12,54617,54643,12,54673,54699,12,54729,54755,12,54785,54811,12,54841,54867,12,54897,54923,12,54953,54979,12,55009,55035,12,55065,55091,12,55121,55147,12,55177,55203,12,65024,65039,5,65520,65528,4,66422,66426,5,68152,68154,5,69291,69292,5,69633,69633,5,69747,69748,5,69811,69814,5,69826,69826,5,69932,69932,7,70016,70017,5,70079,70080,7,70095,70095,5,70196,70196,5,70367,70367,5,70402,70403,7,70464,70464,5,70487,70487,5,70709,70711,7,70725,70725,7,70833,70834,7,70843,70844,7,70849,70849,7,71090,71093,5,71103,71104,5,71227,71228,7,71339,71339,5,71344,71349,5,71458,71461,5,71727,71735,5,71985,71989,7,71998,71998,5,72002,72002,7,72154,72155,5,72193,72202,5,72251,72254,5,72281,72283,5,72344,72345,5,72766,72766,7,72874,72880,5,72885,72886,5,73023,73029,5,73104,73105,5,73111,73111,5,92912,92916,5,94095,94098,5,113824,113827,4,119142,119142,7,119155,119162,4,119362,119364,5,121476,121476,5,122888,122904,5,123184,123190,5,125252,125258,5,127183,127183,14,127340,127343,14,127377,127386,14,127491,127503,14,127548,127551,14,127744,127756,14,127761,127761,14,127769,127769,14,127773,127774,14,127780,127788,14,127796,127797,14,127820,127823,14,127869,127869,14,127894,127895,14,127902,127903,14,127943,127943,14,127947,127950,14,127972,127972,14,127988,127988,14,127992,127994,14,128009,128011,14,128019,128019,14,128023,128041,14,128064,128064,14,128102,128107,14,128174,128181,14,128238,128238,14,128246,128247,14,128254,128254,14,128264,128264,14,128278,128299,14,128329,128330,14,128348,128359,14,128371,128377,14,128392,128393,14,128401,128404,14,128421,128421,14,128433,128434,14,128450,128452,14,128476,128478,14,128483,128483,14,128495,128495,14,128506,128506,14,128519,128520,14,128528,128528,14,128534,128534,14,128538,128538,14,128540,128542,14,128544,128549,14,128552,128555,14,128557,128557,14,128560,128563,14,128565,128565,14,128567,128576,14,128581,128591,14,128641,128642,14,128646,128646,14,128648,128648,14,128650,128651,14,128653,128653,14,128655,128655,14,128657,128659,14,128661,128661,14,128663,128663,14,128665,128666,14,128674,128674,14,128676,128677,14,128679,128685,14,128690,128690,14,128694,128694,14,128697,128702,14,128704,128704,14,128710,128714,14,128716,128716,14,128720,128720,14,128723,128724,14,128726,128727,14,128733,128735,14,128742,128744,14,128746,128746,14,128749,128751,14,128753,128754,14,128756,128758,14,128761,128761,14,128763,128764,14,128884,128895,14,128992,129003,14,129008,129008,14,129036,129039,14,129114,129119,14,129198,129279,14,129293,129295,14,129305,129310,14,129312,129319,14,129328,129328,14,129331,129338,14,129343,129343,14,129351,129355,14,129357,129359,14,129375,129387,14,129393,129393,14,129395,129398,14,129401,129401,14,129403,129403,14,129408,129412,14,129426,129431,14,129443,129444,14,129451,129453,14,129456,129465,14,129472,129472,14,129475,129482,14,129484,129484,14,129488,129510,14,129536,129647,14,129652,129652,14,129656,129658,14,129661,129663,14,129667,129670,14,129680,129685,14,129705,129708,14,129712,129718,14,129723,129727,14,129731,129733,14,129744,129750,14,129754,129759,14,129768,129775,14,129783,129791,14,917504,917504,4,917506,917535,4,917632,917759,4,918000,921599,4,0,9,4,11,12,4,14,31,4,169,169,14,174,174,14,1155,1159,5,1425,1469,5,1473,1474,5,1479,1479,5,1552,1562,5,1611,1631,5,1750,1756,5,1759,1764,5,1770,1773,5,1809,1809,5,1958,1968,5,2045,2045,5,2075,2083,5,2089,2093,5,2192,2193,1,2250,2273,5,2275,2306,5,2362,2362,5,2364,2364,5,2369,2376,5,2381,2381,5,2385,2391,5,2433,2433,5,2492,2492,5,2495,2496,7,2503,2504,7,2509,2509,5,2530,2531,5,2561,2562,5,2620,2620,5,2625,2626,5,2635,2637,5,2672,2673,5,2689,2690,5,2748,2748,5,2753,2757,5,2761,2761,7,2765,2765,5,2810,2815,5,2818,2819,7,2878,2878,5,2880,2880,7,2887,2888,7,2893,2893,5,2903,2903,5,2946,2946,5,3007,3007,7,3009,3010,7,3018,3020,7,3031,3031,5,3073,3075,7,3132,3132,5,3137,3140,7,3146,3149,5,3170,3171,5,3202,3203,7,3262,3262,7,3264,3265,7,3267,3268,7,3271,3272,7,3276,3277,5,3298,3299,5,3330,3331,7,3390,3390,5,3393,3396,5,3402,3404,7,3406,3406,1,3426,3427,5,3458,3459,7,3535,3535,5,3538,3540,5,3544,3550,7,3570,3571,7,3635,3635,7,3655,3662,5,3763,3763,7,3784,3789,5,3893,3893,5,3897,3897,5,3953,3966,5,3968,3972,5,3981,3991,5,4038,4038,5,4145,4145,7,4153,4154,5,4157,4158,5,4184,4185,5,4209,4212,5,4228,4228,7,4237,4237,5,4352,4447,8,4520,4607,10,5906,5908,5,5938,5939,5,5970,5971,5,6068,6069,5,6071,6077,5,6086,6086,5,6089,6099,5,6155,6157,5,6159,6159,5,6313,6313,5,6435,6438,7,6441,6443,7,6450,6450,5,6457,6459,5,6681,6682,7,6741,6741,7,6743,6743,7,6752,6752,5,6757,6764,5,6771,6780,5,6832,6845,5,6847,6862,5,6916,6916,7,6965,6965,5,6971,6971,7,6973,6977,7,6979,6980,7,7040,7041,5,7073,7073,7,7078,7079,7,7082,7082,7,7142,7142,5,7144,7145,5,7149,7149,5,7151,7153,5,7204,7211,7,7220,7221,7,7376,7378,5,7393,7393,7,7405,7405,5,7415,7415,7,7616,7679,5,8204,8204,5,8206,8207,4,8233,8233,4,8252,8252,14,8288,8292,4,8294,8303,4,8413,8416,5,8418,8420,5,8482,8482,14,8596,8601,14,8986,8987,14,9096,9096,14,9193,9196,14,9199,9199,14,9201,9202,14,9208,9210,14,9642,9643,14,9664,9664,14,9728,9729,14,9732,9732,14,9735,9741,14,9743,9744,14,9746,9746,14,9750,9751,14,9753,9756,14,9758,9759,14,9761,9761,14,9764,9765,14,9767,9769,14,9771,9773,14,9775,9775,14,9784,9785,14,9787,9791,14,9793,9793,14,9795,9799,14,9812,9822,14,9824,9824,14,9827,9827,14,9829,9830,14,9832,9832,14,9851,9851,14,9854,9854,14,9856,9861,14,9874,9874,14,9876,9876,14,9878,9879,14,9881,9881,14,9883,9884,14,9888,9889,14,9895,9895,14,9898,9899,14,9904,9905,14,9917,9918,14,9924,9925,14,9928,9928,14,9934,9934,14,9936,9936,14,9938,9938,14,9940,9940,14,9961,9961,14,9963,9967,14,9970,9971,14,9973,9973,14,9975,9977,14,9979,9980,14,9982,9985,14,9987,9988,14,9992,9996,14,9998,9998,14,10000,10001,14,10004,10004,14,10013,10013,14,10024,10024,14,10052,10052,14,10060,10060,14,10067,10069,14,10083,10083,14,10085,10087,14,10145,10145,14,10175,10175,14,11013,11015,14,11088,11088,14,11503,11505,5,11744,11775,5,12334,12335,5,12349,12349,14,12951,12951,14,42607,42607,5,42612,42621,5,42736,42737,5,43014,43014,5,43043,43044,7,43047,43047,7,43136,43137,7,43204,43205,5,43263,43263,5,43335,43345,5,43360,43388,8,43395,43395,7,43444,43445,7,43450,43451,7,43454,43456,7,43561,43566,5,43569,43570,5,43573,43574,5,43596,43596,5,43644,43644,5,43698,43700,5,43710,43711,5,43755,43755,7,43758,43759,7,43766,43766,5,44005,44005,5,44008,44008,5,44012,44012,7,44032,44032,11,44060,44060,11,44088,44088,11,44116,44116,11,44144,44144,11,44172,44172,11,44200,44200,11,44228,44228,11,44256,44256,11,44284,44284,11,44312,44312,11,44340,44340,11,44368,44368,11,44396,44396,11,44424,44424,11,44452,44452,11,44480,44480,11,44508,44508,11,44536,44536,11,44564,44564,11,44592,44592,11,44620,44620,11,44648,44648,11,44676,44676,11,44704,44704,11,44732,44732,11,44760,44760,11,44788,44788,11,44816,44816,11,44844,44844,11,44872,44872,11,44900,44900,11,44928,44928,11,44956,44956,11,44984,44984,11,45012,45012,11,45040,45040,11,45068,45068,11,45096,45096,11,45124,45124,11,45152,45152,11,45180,45180,11,45208,45208,11,45236,45236,11,45264,45264,11,45292,45292,11,45320,45320,11,45348,45348,11,45376,45376,11,45404,45404,11,45432,45432,11,45460,45460,11,45488,45488,11,45516,45516,11,45544,45544,11,45572,45572,11,45600,45600,11,45628,45628,11,45656,45656,11,45684,45684,11,45712,45712,11,45740,45740,11,45768,45768,11,45796,45796,11,45824,45824,11,45852,45852,11,45880,45880,11,45908,45908,11,45936,45936,11,45964,45964,11,45992,45992,11,46020,46020,11,46048,46048,11,46076,46076,11,46104,46104,11,46132,46132,11,46160,46160,11,46188,46188,11,46216,46216,11,46244,46244,11,46272,46272,11,46300,46300,11,46328,46328,11,46356,46356,11,46384,46384,11,46412,46412,11,46440,46440,11,46468,46468,11,46496,46496,11,46524,46524,11,46552,46552,11,46580,46580,11,46608,46608,11,46636,46636,11,46664,46664,11,46692,46692,11,46720,46720,11,46748,46748,11,46776,46776,11,46804,46804,11,46832,46832,11,46860,46860,11,46888,46888,11,46916,46916,11,46944,46944,11,46972,46972,11,47000,47000,11,47028,47028,11,47056,47056,11,47084,47084,11,47112,47112,11,47140,47140,11,47168,47168,11,47196,47196,11,47224,47224,11,47252,47252,11,47280,47280,11,47308,47308,11,47336,47336,11,47364,47364,11,47392,47392,11,47420,47420,11,47448,47448,11,47476,47476,11,47504,47504,11,47532,47532,11,47560,47560,11,47588,47588,11,47616,47616,11,47644,47644,11,47672,47672,11,47700,47700,11,47728,47728,11,47756,47756,11,47784,47784,11,47812,47812,11,47840,47840,11,47868,47868,11,47896,47896,11,47924,47924,11,47952,47952,11,47980,47980,11,48008,48008,11,48036,48036,11,48064,48064,11,48092,48092,11,48120,48120,11,48148,48148,11,48176,48176,11,48204,48204,11,48232,48232,11,48260,48260,11,48288,48288,11,48316,48316,11,48344,48344,11,48372,48372,11,48400,48400,11,48428,48428,11,48456,48456,11,48484,48484,11,48512,48512,11,48540,48540,11,48568,48568,11,48596,48596,11,48624,48624,11,48652,48652,11,48680,48680,11,48708,48708,11,48736,48736,11,48764,48764,11,48792,48792,11,48820,48820,11,48848,48848,11,48876,48876,11,48904,48904,11,48932,48932,11,48960,48960,11,48988,48988,11,49016,49016,11,49044,49044,11,49072,49072,11,49100,49100,11,49128,49128,11,49156,49156,11,49184,49184,11,49212,49212,11,49240,49240,11,49268,49268,11,49296,49296,11,49324,49324,11,49352,49352,11,49380,49380,11,49408,49408,11,49436,49436,11,49464,49464,11,49492,49492,11,49520,49520,11,49548,49548,11,49576,49576,11,49604,49604,11,49632,49632,11,49660,49660,11,49688,49688,11,49716,49716,11,49744,49744,11,49772,49772,11,49800,49800,11,49828,49828,11,49856,49856,11,49884,49884,11,49912,49912,11,49940,49940,11,49968,49968,11,49996,49996,11,50024,50024,11,50052,50052,11,50080,50080,11,50108,50108,11,50136,50136,11,50164,50164,11,50192,50192,11,50220,50220,11,50248,50248,11,50276,50276,11,50304,50304,11,50332,50332,11,50360,50360,11,50388,50388,11,50416,50416,11,50444,50444,11,50472,50472,11,50500,50500,11,50528,50528,11,50556,50556,11,50584,50584,11,50612,50612,11,50640,50640,11,50668,50668,11,50696,50696,11,50724,50724,11,50752,50752,11,50780,50780,11,50808,50808,11,50836,50836,11,50864,50864,11,50892,50892,11,50920,50920,11,50948,50948,11,50976,50976,11,51004,51004,11,51032,51032,11,51060,51060,11,51088,51088,11,51116,51116,11,51144,51144,11,51172,51172,11,51200,51200,11,51228,51228,11,51256,51256,11,51284,51284,11,51312,51312,11,51340,51340,11,51368,51368,11,51396,51396,11,51424,51424,11,51452,51452,11,51480,51480,11,51508,51508,11,51536,51536,11,51564,51564,11,51592,51592,11,51620,51620,11,51648,51648,11,51676,51676,11,51704,51704,11,51732,51732,11,51760,51760,11,51788,51788,11,51816,51816,11,51844,51844,11,51872,51872,11,51900,51900,11,51928,51928,11,51956,51956,11,51984,51984,11,52012,52012,11,52040,52040,11,52068,52068,11,52096,52096,11,52124,52124,11,52152,52152,11,52180,52180,11,52208,52208,11,52236,52236,11,52264,52264,11,52292,52292,11,52320,52320,11,52348,52348,11,52376,52376,11,52404,52404,11,52432,52432,11,52460,52460,11,52488,52488,11,52516,52516,11,52544,52544,11,52572,52572,11,52600,52600,11,52628,52628,11,52656,52656,11,52684,52684,11,52712,52712,11,52740,52740,11,52768,52768,11,52796,52796,11,52824,52824,11,52852,52852,11,52880,52880,11,52908,52908,11,52936,52936,11,52964,52964,11,52992,52992,11,53020,53020,11,53048,53048,11,53076,53076,11,53104,53104,11,53132,53132,11,53160,53160,11,53188,53188,11,53216,53216,11,53244,53244,11,53272,53272,11,53300,53300,11,53328,53328,11,53356,53356,11,53384,53384,11,53412,53412,11,53440,53440,11,53468,53468,11,53496,53496,11,53524,53524,11,53552,53552,11,53580,53580,11,53608,53608,11,53636,53636,11,53664,53664,11,53692,53692,11,53720,53720,11,53748,53748,11,53776,53776,11,53804,53804,11,53832,53832,11,53860,53860,11,53888,53888,11,53916,53916,11,53944,53944,11,53972,53972,11,54000,54000,11,54028,54028,11,54056,54056,11,54084,54084,11,54112,54112,11,54140,54140,11,54168,54168,11,54196,54196,11,54224,54224,11,54252,54252,11,54280,54280,11,54308,54308,11,54336,54336,11,54364,54364,11,54392,54392,11,54420,54420,11,54448,54448,11,54476,54476,11,54504,54504,11,54532,54532,11,54560,54560,11,54588,54588,11,54616,54616,11,54644,54644,11,54672,54672,11,54700,54700,11,54728,54728,11,54756,54756,11,54784,54784,11,54812,54812,11,54840,54840,11,54868,54868,11,54896,54896,11,54924,54924,11,54952,54952,11,54980,54980,11,55008,55008,11,55036,55036,11,55064,55064,11,55092,55092,11,55120,55120,11,55148,55148,11,55176,55176,11,55216,55238,9,64286,64286,5,65056,65071,5,65438,65439,5,65529,65531,4,66272,66272,5,68097,68099,5,68108,68111,5,68159,68159,5,68900,68903,5,69446,69456,5,69632,69632,7,69634,69634,7,69744,69744,5,69759,69761,5,69808,69810,7,69815,69816,7,69821,69821,1,69837,69837,1,69927,69931,5,69933,69940,5,70003,70003,5,70018,70018,7,70070,70078,5,70082,70083,1,70094,70094,7,70188,70190,7,70194,70195,7,70197,70197,7,70206,70206,5,70368,70370,7,70400,70401,5,70459,70460,5,70463,70463,7,70465,70468,7,70475,70477,7,70498,70499,7,70512,70516,5,70712,70719,5,70722,70724,5,70726,70726,5,70832,70832,5,70835,70840,5,70842,70842,5,70845,70845,5,70847,70848,5,70850,70851,5,71088,71089,7,71096,71099,7,71102,71102,7,71132,71133,5,71219,71226,5,71229,71229,5,71231,71232,5,71340,71340,7,71342,71343,7,71350,71350,7,71453,71455,5,71462,71462,7,71724,71726,7,71736,71736,7,71984,71984,5,71991,71992,7,71997,71997,7,71999,71999,1,72001,72001,1,72003,72003,5,72148,72151,5,72156,72159,7,72164,72164,7,72243,72248,5,72250,72250,1,72263,72263,5,72279,72280,7,72324,72329,1,72343,72343,7,72751,72751,7,72760,72765,5,72767,72767,5,72873,72873,7,72881,72881,7,72884,72884,7,73009,73014,5,73020,73021,5,73030,73030,1,73098,73102,7,73107,73108,7,73110,73110,7,73459,73460,5,78896,78904,4,92976,92982,5,94033,94087,7,94180,94180,5,113821,113822,5,118528,118573,5,119141,119141,5,119143,119145,5,119150,119154,5,119163,119170,5,119210,119213,5,121344,121398,5,121461,121461,5,121499,121503,5,122880,122886,5,122907,122913,5,122918,122922,5,123566,123566,5,125136,125142,5,126976,126979,14,126981,127182,14,127184,127231,14,127279,127279,14,127344,127345,14,127374,127374,14,127405,127461,14,127489,127490,14,127514,127514,14,127538,127546,14,127561,127567,14,127570,127743,14,127757,127758,14,127760,127760,14,127762,127762,14,127766,127768,14,127770,127770,14,127772,127772,14,127775,127776,14,127778,127779,14,127789,127791,14,127794,127795,14,127798,127798,14,127819,127819,14,127824,127824,14,127868,127868,14,127870,127871,14,127892,127893,14,127896,127896,14,127900,127901,14,127904,127940,14,127942,127942,14,127944,127944,14,127946,127946,14,127951,127955,14,127968,127971,14,127973,127984,14,127987,127987,14,127989,127989,14,127991,127991,14,127995,127999,5,128008,128008,14,128012,128014,14,128017,128018,14,128020,128020,14,128022,128022,14,128042,128042,14,128063,128063,14,128065,128065,14,128101,128101,14,128108,128109,14,128173,128173,14,128182,128183,14,128236,128237,14,128239,128239,14,128245,128245,14,128248,128248,14,128253,128253,14,128255,128258,14,128260,128263,14,128265,128265,14,128277,128277,14,128300,128301,14,128326,128328,14,128331,128334,14,128336,128347,14,128360,128366,14,128369,128370,14,128378,128378,14,128391,128391,14,128394,128397,14,128400,128400,14,128405,128406,14,128420,128420,14,128422,128423,14,128425,128432,14,128435,128443,14,128445,128449,14,128453,128464,14,128468,128475,14,128479,128480,14,128482,128482,14,128484,128487,14,128489,128494,14,128496,128498,14,128500,128505,14,128507,128511,14,128513,128518,14,128521,128525,14,128527,128527,14,128529,128529,14,128533,128533,14,128535,128535,14,128537,128537,14]');
1152 }
1153 > strings.ts
1154 > //#endregion
1155 >
1156 > /**
1157 > * Computes the offset after performing a left delete on the given string,
1158 > * while considering unicode grapheme/emoji rules.
1159 > */
1160 > export function getLeftDeleteOffset(offset: number, str: string): number {
1161 if (offset === 0) {
1162 return 0;
1174 return iterator.offset;
1175 }
1176 > strings.ts
1177 function getOffsetBeforeLastEmojiComponent(initialOffset: number, str: string): number | undefined {
1178 // See https://www.unicode.org/reports/tr51/tr51-14.html#EBNF_and_Regex for the
1210 return resultOffset;
1211 }
1212 > strings.ts
1213 function isEmojiModifier(codePoint: number): boolean {
1214 return 0x1F3FB <= codePoint && codePoint <= 0x1F3FF;
1215 }
1216 > strings.ts
1217 > const enum CodePoint {
1218 > zwj = 0x200D,
1219 >
1220 > /**
1221 > * Variation Selector-16 (VS16)
1222 > */
1223 > emojiVariantSelector = 0xFE0F,
1224 >
1225 > /**
1226 > * Combining Enclosing Keycap
1227 > */
1228 > enclosingKeyCap = 0x20E3,
1229 >
1230 > space = 0x0020,
1231 > }
1232 >
1233 > export const noBreakWhitespace = '\xa0';
1234 >
1235 > export class AmbiguousCharacters {
1236 > private static readonly ambiguousCharacterData = new Lazy<
1237 > Record<
1238 > string | '_common' | '_default',
1239 > /* code point -> ascii code point */ number[]
1240 > >
1241 > >(() => {
1242 // Generated using https://github.com/hediet/vscode-unicode-data
1243 // Stored as key1, value1, key2, value2, ...
1245 '{\"_common\":[8232,32,8233,32,5760,32,8192,32,8193,32,8194,32,8195,32,8196,32,8197,32,8198,32,8200,32,8201,32,8202,32,8287,32,8199,32,8239,32,2042,95,65101,95,65102,95,65103,95,8208,45,8209,45,8210,45,65112,45,1748,45,8259,45,727,45,8722,45,10134,45,11450,45,1549,44,1643,44,184,44,42233,44,894,59,2307,58,2691,58,1417,58,1795,58,1796,58,5868,58,65072,58,6147,58,6153,58,8282,58,1475,58,760,58,42889,58,8758,58,720,58,42237,58,451,33,11601,33,660,63,577,63,2429,63,5038,63,42731,63,119149,46,8228,46,1793,46,1794,46,42510,46,68176,46,1632,46,1776,46,42232,46,1373,96,65287,96,8219,96,1523,96,8242,96,1370,96,8175,96,65344,96,900,96,8189,96,8125,96,8127,96,8190,96,697,96,884,96,712,96,714,96,715,96,756,96,699,96,701,96,700,96,702,96,42892,96,1497,96,2036,96,2037,96,5194,96,5836,96,94033,96,94034,96,65339,91,10088,40,10098,40,12308,40,64830,40,65341,93,10089,41,10099,41,12309,41,64831,41,10100,123,119060,123,10101,125,65342,94,8270,42,1645,42,8727,42,66335,42,5941,47,8257,47,8725,47,8260,47,9585,47,10187,47,10744,47,119354,47,12755,47,12339,47,11462,47,20031,47,12035,47,65340,92,65128,92,8726,92,10189,92,10741,92,10745,92,119311,92,119355,92,12756,92,20022,92,12034,92,42872,38,708,94,710,94,5869,43,10133,43,66203,43,8249,60,10094,60,706,60,119350,60,5176,60,5810,60,5120,61,11840,61,12448,61,42239,61,8250,62,10095,62,707,62,119351,62,5171,62,94015,62,8275,126,732,126,8128,126,8764,126,65372,124,65293,45,118002,50,120784,50,120794,50,120804,50,120814,50,120824,50,130034,50,42842,50,423,50,1000,50,42564,50,5311,50,42735,50,119302,51,118003,51,120785,51,120795,51,120805,51,120815,51,120825,51,130035,51,42923,51,540,51,439,51,42858,51,11468,51,1248,51,94011,51,71882,51,118004,52,120786,52,120796,52,120806,52,120816,52,120826,52,130036,52,5070,52,71855,52,118005,53,120787,53,120797,53,120807,53,120817,53,120827,53,130037,53,444,53,71867,53,118006,54,120788,54,120798,54,120808,54,120818,54,120828,54,130038,54,11474,54,5102,54,71893,54,119314,55,118007,55,120789,55,120799,55,120809,55,120819,55,120829,55,130039,55,66770,55,71878,55,2819,56,2538,56,2666,56,125131,56,118008,56,120790,56,120800,56,120810,56,120820,56,120830,56,130040,56,547,56,546,56,66330,56,2663,57,2920,57,2541,57,3437,57,118009,57,120791,57,120801,57,120811,57,120821,57,120831,57,130041,57,42862,57,11466,57,71884,57,71852,57,71894,57,9082,97,65345,97,119834,97,119886,97,119938,97,119990,97,120042,97,120094,97,120146,97,120198,97,120250,97,120302,97,120354,97,120406,97,120458,97,593,97,945,97,120514,97,120572,97,120630,97,120688,97,120746,97,65313,65,117974,65,119808,65,119860,65,119912,65,119964,65,120016,65,120068,65,120120,65,120172,65,120224,65,120276,65,120328,65,120380,65,120432,65,913,65,120488,65,120546,65,120604,65,120662,65,120720,65,5034,65,5573,65,42222,65,94016,65,66208,65,119835,98,119887,98,119939,98,119991,98,120043,98,120095,98,120147,98,120199,98,120251,98,120303,98,120355,98,120407,98,120459,98,388,98,5071,98,5234,98,5551,98,65314,66,8492,66,117975,66,119809,66,119861,66,119913,66,120017,66,120069,66,120121,66,120173,66,120225,66,120277,66,120329,66,120381,66,120433,66,42932,66,914,66,120489,66,120547,66,120605,66,120663,66,120721,66,5108,66,5623,66,42192,66,66178,66,66209,66,66305,66,65347,99,8573,99,119836,99,119888,99,119940,99,119992,99,120044,99,120096,99,120148,99,120200,99,120252,99,120304,99,120356,99,120408,99,120460,99,7428,99,1010,99,11429,99,43951,99,66621,99,128844,67,71913,67,71922,67,65315,67,8557,67,8450,67,8493,67,117976,67,119810,67,119862,67,119914,67,119966,67,120018,67,120174,67,120226,67,120278,67,120330,67,120382,67,120434,67,1017,67,11428,67,5087,67,42202,67,66210,67,66306,67,66581,67,66844,67,8574,100,8518,100,119837,100,119889,100,119941,100,119993,100,120045,100,120097,100,120149,100,120201,100,120253,100,120305,100,120357,100,120409,100,120461,100,1281,100,5095,100,5231,100,42194,100,8558,68,8517,68,117977,68,119811,68,119863,68,119915,68,119967,68,120019,68,120071,68,120123,68,120175,68,120227,68,120279,68,120331,68,120383,68,120435,68,5024,68,5598,68,5610,68,42195,68,8494,101,65349,101,8495,101,8519,101,119838,101,119890,101,119942,101,120046,101,120098,101,120150,101,120202,101,120254,101,120306,101,120358,101,120410,101,120462,101,43826,101,1213,101,8959,69,65317,69,8496,69,117978,69,119812,69,119864,69,119916,69,120020,69,120072,69,120124,69,120176,69,120228,69,120280,69,120332,69,120384,69,120436,69,917,69,120492,69,120550,69,120608,69,120666,69,120724,69,11577,69,5036,69,42224,69,71846,69,71854,69,66182,69,119839,102,119891,102,119943,102,119995,102,120047,102,120099,102,120151,102,120203,102,120255,102,120307,102,120359,102,120411,102,120463,102,43829,102,42905,102,383,102,7837,102,1412,102,119315,70,8497,70,117979,70,119813,70,119865,70,119917,70,120021,70,120073,70,120125,70,120177,70,120229,70,120281,70,120333,70,120385,70,120437,70,42904,70,988,70,120778,70,5556,70,42205,70,71874,70,71842,70,66183,70,66213,70,66853,70,65351,103,8458,103,119840,103,119892,103,119944,103,120048,103,120100,103,120152,103,120204,103,120256,103,120308,103,120360,103,120412,103,120464,103,609,103,7555,103,397,103,1409,103,117980,71,119814,71,119866,71,119918,71,119970,71,120022,71,120074,71,120126,71,120178,71,120230,71,120282,71,120334,71,120386,71,120438,71,1292,71,5056,71,5107,71,42198,71,65352,104,8462,104,119841,104,119945,104,119997,104,120049,104,120101,104,120153,104,120205,104,120257,104,120309,104,120361,104,120413,104,120465,104,1211,104,1392,104,5058,104,65320,72,8459,72,8460,72,8461,72,117981,72,119815,72,119867,72,119919,72,120023,72,120179,72,120231,72,120283,72,120335,72,120387,72,120439,72,919,72,120494,72,120552,72,120610,72,120668,72,120726,72,11406,72,5051,72,5500,72,42215,72,66255,72,731,105,9075,105,65353,105,8560,105,8505,105,8520,105,119842,105,119894,105,119946,105,119998,105,120050,105,120102,105,120154,105,120206,105,120258,105,120310,105,120362,105,120414,105,120466,105,120484,105,618,105,617,105,953,105,8126,105,890,105,120522,105,120580,105,120638,105,120696,105,120754,105,1110,105,42567,105,1231,105,43893,105,5029,105,71875,105,65354,106,8521,106,119843,106,119895,106,119947,106,119999,106,120051,106,120103,106,120155,106,120207,106,120259,106,120311,106,120363,106,120415,106,120467,106,1011,106,1112,106,65322,74,117983,74,119817,74,119869,74,119921,74,119973,74,120025,74,120077,74,120129,74,120181,74,120233,74,120285,74,120337,74,120389,74,120441,74,42930,74,895,74,1032,74,5035,74,5261,74,42201,74,119844,107,119896,107,119948,107,120000,107,120052,107,120104,107,120156,107,120208,107,120260,107,120312,107,120364,107,120416,107,120468,107,8490,75,65323,75,117984,75,119818,75,119870,75,119922,75,119974,75,120026,75,120078,75,120130,75,120182,75,120234,75,120286,75,120338,75,120390,75,120442,75,922,75,120497,75,120555,75,120613,75,120671,75,120729,75,11412,75,5094,75,5845,75,42199,75,66840,75,1472,108,8739,73,9213,73,65512,73,1633,108,1777,73,66336,108,125127,108,118001,108,120783,73,120793,73,120803,73,120813,73,120823,73,130033,73,65321,73,8544,73,8464,73,8465,73,117982,108,119816,73,119868,73,119920,73,120024,73,120128,73,120180,73,120232,73,120284,73,120336,73,120388,73,120440,73,65356,108,8572,73,8467,108,119845,108,119897,108,119949,108,120001,108,120053,108,120105,73,120157,73,120209,73,120261,73,120313,73,120365,73,120417,73,120469,73,448,73,120496,73,120554,73,120612,73,120670,73,120728,73,11410,73,1030,73,1216,73,1493,108,1503,108,1575,108,126464,108,126592,108,65166,108,65165,108,1994,108,11599,73,5825,73,42226,73,93992,73,66186,124,66313,124,119338,76,8556,76,8466,76,117985,76,119819,76,119871,76,119923,76,120027,76,120079,76,120131,76,120183,76,120235,76,120287,76,120339,76,120391,76,120443,76,11472,76,5086,76,5290,76,42209,76,93974,76,71843,76,71858,76,66587,76,66854,76,65325,77,8559,77,8499,77,117986,77,119820,77,119872,77,119924,77,120028,77,120080,77,120132,77,120184,77,120236,77,120288,77,120340,77,120392,77,120444,77,924,77,120499,77,120557,77,120615,77,120673,77,120731,77,1018,77,11416,77,5047,77,5616,77,5846,77,42207,77,66224,77,66321,77,119847,110,119899,110,119951,110,120003,110,120055,110,120107,110,120159,110,120211,110,120263,110,120315,110,120367,110,120419,110,120471,110,1400,110,1404,110,65326,78,8469,78,117987,78,119821,78,119873,78,119925,78,119977,78,120029,78,120081,78,120185,78,120237,78,120289,78,120341,78,120393,78,120445,78,925,78,120500,78,120558,78,120616,78,120674,78,120732,78,11418,78,42208,78,66835,78,3074,111,3202,111,3330,111,3458,111,2406,111,2662,111,2790,111,3046,111,3174,111,3302,111,3430,111,3664,111,3792,111,4160,111,1637,111,1781,111,65359,111,8500,111,119848,111,119900,111,119952,111,120056,111,120108,111,120160,111,120212,111,120264,111,120316,111,120368,111,120420,111,120472,111,7439,111,7441,111,43837,111,959,111,120528,111,120586,111,120644,111,120702,111,120760,111,963,111,120532,111,120590,111,120648,111,120706,111,120764,111,11423,111,4351,111,1413,111,1505,111,1607,111,126500,111,126564,111,126596,111,65259,111,65260,111,65258,111,65257,111,1726,111,64428,111,64429,111,64427,111,64426,111,1729,111,64424,111,64425,111,64423,111,64422,111,1749,111,3360,111,4125,111,66794,111,71880,111,71895,111,66604,111,1984,79,2534,79,2918,79,12295,79,70864,79,71904,79,118000,79,120782,79,120792,79,120802,79,120812,79,120822,79,130032,79,65327,79,117988,79,119822,79,119874,79,119926,79,119978,79,120030,79,120082,79,120134,79,120186,79,120238,79,120290,79,120342,79,120394,79,120446,79,927,79,120502,79,120560,79,120618,79,120676,79,120734,79,11422,79,1365,79,11604,79,4816,79,2848,79,66754,79,42227,79,71861,79,66194,79,66219,79,66564,79,66838,79,9076,112,65360,112,119849,112,119901,112,119953,112,120005,112,120057,112,120109,112,120161,112,120213,112,120265,112,120317,112,120369,112,120421,112,120473,112,961,112,120530,112,120544,112,120588,112,120602,112,120646,112,120660,112,120704,112,120718,112,120762,112,120776,112,11427,112,65328,80,8473,80,117989,80,119823,80,119875,80,119927,80,119979,80,120031,80,120083,80,120187,80,120239,80,120291,80,120343,80,120395,80,120447,80,929,80,120504,80,120562,80,120620,80,120678,80,120736,80,11426,80,5090,80,5229,80,42193,80,66197,80,119850,113,119902,113,119954,113,120006,113,120058,113,120110,113,120162,113,120214,113,120266,113,120318,113,120370,113,120422,113,120474,113,1307,113,1379,113,1382,113,8474,81,117990,81,119824,81,119876,81,119928,81,119980,81,120032,81,120084,81,120188,81,120240,81,120292,81,120344,81,120396,81,120448,81,11605,81,119851,114,119903,114,119955,114,120007,114,120059,114,120111,114,120163,114,120215,114,120267,114,120319,114,120371,114,120423,114,120475,114,43847,114,43848,114,7462,114,11397,114,43905,114,119318,82,8475,82,8476,82,8477,82,117991,82,119825,82,119877,82,119929,82,120033,82,120189,82,120241,82,120293,82,120345,82,120397,82,120449,82,422,82,5025,82,5074,82,66740,82,5511,82,42211,82,94005,82,65363,115,119852,115,119904,115,119956,115,120008,115,120060,115,120112,115,120164,115,120216,115,120268,115,120320,115,120372,115,120424,115,120476,115,42801,115,445,115,1109,115,43946,115,71873,115,66632,115,65331,83,117992,83,119826,83,119878,83,119930,83,119982,83,120034,83,120086,83,120138,83,120190,83,120242,83,120294,83,120346,83,120398,83,120450,83,1029,83,1359,83,5077,83,5082,83,42210,83,94010,83,66198,83,66592,83,119853,116,119905,116,119957,116,120009,116,120061,116,120113,116,120165,116,120217,116,120269,116,120321,116,120373,116,120425,116,120477,116,8868,84,10201,84,128872,84,65332,84,117993,84,119827,84,119879,84,119931,84,119983,84,120035,84,120087,84,120139,84,120191,84,120243,84,120295,84,120347,84,120399,84,120451,84,932,84,120507,84,120565,84,120623,84,120681,84,120739,84,11430,84,5026,84,42196,84,93962,84,71868,84,66199,84,66225,84,66325,84,119854,117,119906,117,119958,117,120010,117,120062,117,120114,117,120166,117,120218,117,120270,117,120322,117,120374,117,120426,117,120478,117,42911,117,7452,117,43854,117,43858,117,651,117,965,117,120534,117,120592,117,120650,117,120708,117,120766,117,1405,117,66806,117,71896,117,8746,85,8899,85,117994,85,119828,85,119880,85,119932,85,119984,85,120036,85,120088,85,120140,85,120192,85,120244,85,120296,85,120348,85,120400,85,120452,85,1357,85,4608,85,66766,85,5196,85,42228,85,94018,85,71864,85,8744,118,8897,118,65366,118,8564,118,119855,118,119907,118,119959,118,120011,118,120063,118,120115,118,120167,118,120219,118,120271,118,120323,118,120375,118,120427,118,120479,118,7456,118,957,118,120526,118,120584,118,120642,118,120700,118,120758,118,1141,118,1496,118,71430,118,43945,118,71872,118,119309,86,1639,86,1783,86,8548,86,117995,86,119829,86,119881,86,119933,86,119985,86,120037,86,120089,86,120141,86,120193,86,120245,86,120297,86,120349,86,120401,86,120453,86,1140,86,11576,86,5081,86,5167,86,42719,86,42214,86,93960,86,71840,86,66845,86,623,119,119856,119,119908,119,119960,119,120012,119,120064,119,120116,119,120168,119,120220,119,120272,119,120324,119,120376,119,120428,119,120480,119,7457,119,1121,119,1309,119,1377,119,71434,119,71438,119,71439,119,43907,119,71910,87,71919,87,117996,87,119830,87,119882,87,119934,87,119986,87,120038,87,120090,87,120142,87,120194,87,120246,87,120298,87,120350,87,120402,87,120454,87,1308,87,5043,87,5076,87,42218,87,5742,120,10539,120,10540,120,10799,120,65368,120,8569,120,119857,120,119909,120,119961,120,120013,120,120065,120,120117,120,120169,120,120221,120,120273,120,120325,120,120377,120,120429,120,120481,120,5441,120,5501,120,5741,88,9587,88,66338,88,71916,88,65336,88,8553,88,117997,88,119831,88,119883,88,119935,88,119987,88,120039,88,120091,88,120143,88,120195,88,120247,88,120299,88,120351,88,120403,88,120455,88,42931,88,935,88,120510,88,120568,88,120626,88,120684,88,120742,88,11436,88,11613,88,5815,88,42219,88,66192,88,66228,88,66327,88,66855,88,611,121,7564,121,65369,121,119858,121,119910,121,119962,121,120014,121,120066,121,120118,121,120170,121,120222,121,120274,121,120326,121,120378,121,120430,121,120482,121,655,121,7935,121,43866,121,947,121,8509,121,120516,121,120574,121,120632,121,120690,121,120748,121,1199,121,4327,121,71900,121,65337,89,117998,89,119832,89,119884,89,119936,89,119988,89,120040,89,120092,89,120144,89,120196,89,120248,89,120300,89,120352,89,120404,89,120456,89,933,89,978,89,120508,89,120566,89,120624,89,120682,89,120740,89,11432,89,1198,89,5033,89,5053,89,42220,89,94019,89,71844,89,66226,89,119859,122,119911,122,119963,122,120015,122,120067,122,120119,122,120171,122,120223,122,120275,122,120327,122,120379,122,120431,122,120483,122,7458,122,43923,122,71876,122,71909,90,66293,90,65338,90,8484,90,8488,90,117999,90,119833,90,119885,90,119937,90,119989,90,120041,90,120197,90,120249,90,120301,90,120353,90,120405,90,120457,90,918,90,120493,90,120551,90,120609,90,120667,90,120725,90,5059,90,42204,90,71849,90,65282,34,65283,35,65284,36,65285,37,65286,38,65290,42,65291,43,65294,46,65295,47,65296,48,65298,50,65299,51,65300,52,65301,53,65302,54,65303,55,65304,56,65305,57,65308,60,65309,61,65310,62,65312,64,65316,68,65318,70,65319,71,65324,76,65329,81,65330,82,65333,85,65334,86,65335,87,65343,95,65346,98,65348,100,65350,102,65355,107,65357,109,65358,110,65361,113,65362,114,65364,116,65365,117,65367,119,65370,122,65371,123,65373,125,119846,109],\"_default\":[160,32,8211,45,65374,126,8218,44,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"cs\":[65374,126,8218,44,65306,58,65281,33,8216,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"de\":[65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"es\":[8211,45,65374,126,8218,44,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"fr\":[65374,126,8218,44,65306,58,65281,33,8216,96,8245,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"it\":[160,32,8211,45,65374,126,8218,44,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"ja\":[8211,45,8218,44,65281,33,8216,96,8245,96,180,96,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65292,44,65297,49,65307,59],\"ko\":[8211,45,65374,126,8218,44,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"pl\":[65374,126,65306,58,65281,33,8216,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"pt-BR\":[65374,126,8218,44,65306,58,65281,33,8216,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"qps-ploc\":[160,32,8211,45,65374,126,8218,44,65306,58,65281,33,8216,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"ru\":[65374,126,8218,44,65306,58,65281,33,8216,96,8245,96,180,96,12494,47,305,105,921,73,1009,112,215,120,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"tr\":[160,32,8211,45,65374,126,8218,44,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"zh-hans\":[160,32,65374,126,8218,44,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65297,49],\"zh-hant\":[8211,45,65374,126,8218,44,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89]}'
1246 );
1247 > }); strings.ts
1248 >
1249 > private static readonly cache = new LRUCachedFunction<string, AmbiguousCharacters>((localesStr) => {
1250 const locales = localesStr.split(',');
1251
1304
1305 return new AmbiguousCharacters(map);
1306 > }); strings.ts
1307 >
1308 > public static getInstance(locales: Iterable<string>): AmbiguousCharacters {
1309 return AmbiguousCharacters.cache.get(Array.from(locales).join(','));
1310 }
1311 > strings.ts
1312 > private static _locales = new Lazy<string[]>(() =>
1313 Object.keys(AmbiguousCharacters.ambiguousCharacterData.value).filter(
1314 (k) => !k.startsWith('_')
1315 )
1316 > ); strings.ts
1317 > public static getLocales(): string[] {
1318 return AmbiguousCharacters._locales.value;
1319 }
1320 > strings.ts
1321 > private constructor(
1322 private readonly confusableDictionary: Map<number, number>
1323 ) { }
1324 > strings.ts
1325 > public isAmbiguous(codePoint: number): boolean {
1326 return this.confusableDictionary.has(codePoint);
1327 }
1328 > strings.ts
1329 > public containsAmbiguousCharacter(str: string): boolean {
1330 for (let i = 0; i < str.length; i++) {
1331 const codePoint = str.codePointAt(i);
1336 return false;
1337 }
1338 > strings.ts
1339 > /**
1340 > * Returns the non basic ASCII code point that the given code point can be confused,
1341 > * or undefined if such code point does note exist.
1342 > */
1343 > public getPrimaryConfusable(codePoint: number): number | undefined {
1344 return this.confusableDictionary.get(codePoint);
1345 }
1346 > strings.ts
1347 > public getConfusableCodePoints(): ReadonlySet<number> {
1348 return new Set(this.confusableDictionary.keys());
1349 }
1350 > } strings.ts
1351 >
1352 > export class InvisibleCharacters {
1353 > private static getRawData(): Record<string | '_common', number[]> {
1354 // Generated using https://github.com/hediet/vscode-unicode-data
1355 return JSON.parse('{\"_common\":[11,12,13,127,847,1564,4447,4448,6068,6069,6155,6156,6157,6158,7355,7356,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8204,8205,8206,8207,8234,8235,8236,8237,8238,8239,8287,8288,8289,8290,8291,8292,8293,8294,8295,8296,8297,8298,8299,8300,8301,8302,8303,10240,12644,65024,65025,65026,65027,65028,65029,65030,65031,65032,65033,65034,65035,65036,65037,65038,65039,65279,65440,65520,65521,65522,65523,65524,65525,65526,65527,65528,65532,78844,119155,119156,119157,119158,119159,119160,119161,119162,917504,917505,917506,917507,917508,917509,917510,917511,917512,917513,917514,917515,917516,917517,917518,917519,917520,917521,917522,917523,917524,917525,917526,917527,917528,917529,917530,917531,917532,917533,917534,917535,917536,917537,917538,917539,917540,917541,917542,917543,917544,917545,917546,917547,917548,917549,917550,917551,917552,917553,917554,917555,917556,917557,917558,917559,917560,917561,917562,917563,917564,917565,917566,917567,917568,917569,917570,917571,917572,917573,917574,917575,917576,917577,917578,917579,917580,917581,917582,917583,917584,917585,917586,917587,917588,917589,917590,917591,917592,917593,917594,917595,917596,917597,917598,917599,917600,917601,917602,917603,917604,917605,917606,917607,917608,917609,917610,917611,917612,917613,917614,917615,917616,917617,917618,917619,917620,917621,917622,917623,917624,917625,917626,917627,917628,917629,917630,917631,917760,917761,917762,917763,917764,917765,917766,917767,917768,917769,917770,917771,917772,917773,917774,917775,917776,917777,917778,917779,917780,917781,917782,917783,917784,917785,917786,917787,917788,917789,917790,917791,917792,917793,917794,917795,917796,917797,917798,917799,917800,917801,917802,917803,917804,917805,917806,917807,917808,917809,917810,917811,917812,917813,917814,917815,917816,917817,917818,917819,917820,917821,917822,917823,917824,917825,917826,917827,917828,917829,917830,917831,917832,917833,917834,917835,917836,917837,917838,917839,917840,917841,917842,917843,917844,917845,917846,917847,917848,917849,917850,917851,917852,917853,917854,917855,917856,917857,917858,917859,917860,917861,917862,917863,917864,917865,917866,917867,917868,917869,917870,917871,917872,917873,917874,917875,917876,917877,917878,917879,917880,917881,917882,917883,917884,917885,917886,917887,917888,917889,917890,917891,917892,917893,917894,917895,917896,917897,917898,917899,917900,917901,917902,917903,917904,917905,917906,917907,917908,917909,917910,917911,917912,917913,917914,917915,917916,917917,917918,917919,917920,917921,917922,917923,917924,917925,917926,917927,917928,917929,917930,917931,917932,917933,917934,917935,917936,917937,917938,917939,917940,917941,917942,917943,917944,917945,917946,917947,917948,917949,917950,917951,917952,917953,917954,917955,917956,917957,917958,917959,917960,917961,917962,917963,917964,917965,917966,917967,917968,917969,917970,917971,917972,917973,917974,917975,917976,917977,917978,917979,917980,917981,917982,917983,917984,917985,917986,917987,917988,917989,917990,917991,917992,917993,917994,917995,917996,917997,917998,917999],\"cs\":[173,8203,12288],\"de\":[173,8203,12288],\"es\":[8203,12288],\"fr\":[173,8203,12288],\"it\":[160,173,12288],\"ja\":[173],\"ko\":[173,12288],\"pl\":[173,8203,12288],\"pt-BR\":[173,8203,12288],\"qps-ploc\":[160,173,8203,12288],\"ru\":[173,12288],\"tr\":[160,173,8203,12288],\"zh-hans\":[160,173,8203,12288],\"zh-hant\":[173,12288]}');
1356 }
1357 > strings.ts
1358 > private static _data: Set<number> | undefined = undefined;
1359 >
1360 > private static getData() {
1361 if (!this._data) {
1362 this._data = new Set([...Object.values(InvisibleCharacters.getRawData())].flat());
1364 return this._data;
1365 }
1366 > strings.ts
1367 > public static isInvisibleCharacter(codePoint: number): boolean {
1368 return InvisibleCharacters.getData().has(codePoint);
1369 }
1370 > strings.ts
1371 > public static containsInvisibleCharacter(str: string): boolean {
1372 for (let i = 0; i < str.length; i++) {
1373 const codePoint = str.codePointAt(i);
1378 return false;
1379 }
1380 > strings.ts
1381 > public static get codePoints(): ReadonlySet<number> {
1382 return InvisibleCharacters.getData();
1383 }
1384 > } strings.ts
1385 >
1386 > export const Ellipsis = '\u2026';
1387 >
1388 > /**
1389 > * Convert a Unicode string to a string in which each 16-bit unit occupies only one byte
1390 > *
1391 > * From https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa
1392 > */
1393 function toBinary(str: string): string {
1394 const codeUnits = new Uint16Array(str.length);
1403 return binary;
1404 }
1405 > strings.ts
1406 > /**
1407 > * Version of the global `btoa` function that handles multi-byte characters instead
1408 > * of throwing an exception.
1409 > */
1410 >
1411 > export function multibyteAwareBtoa(str: string): string {
1412 return btoa(toBinary(str));
1413 }
src/vs/base/common/charCode.ts 450 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- charCode.ts
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 > // Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/
7 >
8 > /**
9 > * An inlined enum containing useful character codes (to be used with String.charCodeAt).
10 > * Please leave the const keyword such that it gets inlined when compiled to JavaScript!
11 > */
12 > export const enum CharCode {
13 > Null = 0,
14 > /**
15 > * The `\b` character.
16 > */
17 > Backspace = 8,
18 > /**
19 > * The `\t` character.
20 > */
21 > Tab = 9,
22 > /**
23 > * The `\n` character.
24 > */
25 > LineFeed = 10,
26 > /**
27 > * The `\r` character.
28 > */
29 > CarriageReturn = 13,
30 > Space = 32,
31 > /**
32 > * The `!` character.
33 > */
34 > ExclamationMark = 33,
35 > /**
36 > * The `"` character.
37 > */
38 > DoubleQuote = 34,
39 > /**
40 > * The `#` character.
41 > */
42 > Hash = 35,
43 > /**
44 > * The `$` character.
45 > */
46 > DollarSign = 36,
47 > /**
48 > * The `%` character.
49 > */
50 > PercentSign = 37,
51 > /**
52 > * The `&` character.
53 > */
54 > Ampersand = 38,
55 > /**
56 > * The `'` character.
57 > */
58 > SingleQuote = 39,
59 > /**
60 > * The `(` character.
61 > */
62 > OpenParen = 40,
63 > /**
64 > * The `)` character.
65 > */
66 > CloseParen = 41,
67 > /**
68 > * The `*` character.
69 > */
70 > Asterisk = 42,
71 > /**
72 > * The `+` character.
73 > */
74 > Plus = 43,
75 > /**
76 > * The `,` character.
77 > */
78 > Comma = 44,
79 > /**
80 > * The `-` character.
81 > */
82 > Dash = 45,
83 > /**
84 > * The `.` character.
85 > */
86 > Period = 46,
87 > /**
88 > * The `/` character.
89 > */
90 > Slash = 47,
91 >
92 > Digit0 = 48,
93 > Digit1 = 49,
94 > Digit2 = 50,
95 > Digit3 = 51,
96 > Digit4 = 52,
97 > Digit5 = 53,
98 > Digit6 = 54,
99 > Digit7 = 55,
100 > Digit8 = 56,
101 > Digit9 = 57,
102 >
103 > /**
104 > * The `:` character.
105 > */
106 > Colon = 58,
107 > /**
108 > * The `;` character.
109 > */
110 > Semicolon = 59,
111 > /**
112 > * The `<` character.
113 > */
114 > LessThan = 60,
115 > /**
116 > * The `=` character.
117 > */
118 > Equals = 61,
119 > /**
120 > * The `>` character.
121 > */
122 > GreaterThan = 62,
123 > /**
124 > * The `?` character.
125 > */
126 > QuestionMark = 63,
127 > /**
128 > * The `@` character.
129 > */
130 > AtSign = 64,
131 >
132 > A = 65,
133 > B = 66,
134 > C = 67,
135 > D = 68,
136 > E = 69,
137 > F = 70,
138 > G = 71,
139 > H = 72,
140 > I = 73,
141 > J = 74,
142 > K = 75,
143 > L = 76,
144 > M = 77,
145 > N = 78,
146 > O = 79,
147 > P = 80,
148 > Q = 81,
149 > R = 82,
150 > S = 83,
151 > T = 84,
152 > U = 85,
153 > V = 86,
154 > W = 87,
155 > X = 88,
156 > Y = 89,
157 > Z = 90,
158 >
159 > /**
160 > * The `[` character.
161 > */
162 > OpenSquareBracket = 91,
163 > /**
164 > * The `\` character.
165 > */
166 > Backslash = 92,
167 > /**
168 > * The `]` character.
169 > */
170 > CloseSquareBracket = 93,
171 > /**
172 > * The `^` character.
173 > */
174 > Caret = 94,
175 > /**
176 > * The `_` character.
177 > */
178 > Underline = 95,
179 > /**
180 > * The ``(`)`` character.
181 > */
182 > BackTick = 96,
183 >
184 > a = 97,
185 > b = 98,
186 > c = 99,
187 > d = 100,
188 > e = 101,
189 > f = 102,
190 > g = 103,
191 > h = 104,
192 > i = 105,
193 > j = 106,
194 > k = 107,
195 > l = 108,
196 > m = 109,
197 > n = 110,
198 > o = 111,
199 > p = 112,
200 > q = 113,
201 > r = 114,
202 > s = 115,
203 > t = 116,
204 > u = 117,
205 > v = 118,
206 > w = 119,
207 > x = 120,
208 > y = 121,
209 > z = 122,
210 >
211 > /**
212 > * The `{` character.
213 > */
214 > OpenCurlyBrace = 123,
215 > /**
216 > * The `|` character.
217 > */
218 > Pipe = 124,
219 > /**
220 > * The `}` character.
221 > */
222 > CloseCurlyBrace = 125,
223 > /**
224 > * The `~` character.
225 > */
226 > Tilde = 126,
227 >
228 > /**
229 > * The &nbsp; (no-break space) character.
230 > * Unicode Character 'NO-BREAK SPACE' (U+00A0)
231 > */
232 > NoBreakSpace = 160,
233 >
234 > U_Combining_Grave_Accent = 0x0300, // U+0300 Combining Grave Accent
235 > U_Combining_Acute_Accent = 0x0301, // U+0301 Combining Acute Accent
236 > U_Combining_Circumflex_Accent = 0x0302, // U+0302 Combining Circumflex Accent
237 > U_Combining_Tilde = 0x0303, // U+0303 Combining Tilde
238 > U_Combining_Macron = 0x0304, // U+0304 Combining Macron
239 > U_Combining_Overline = 0x0305, // U+0305 Combining Overline
240 > U_Combining_Breve = 0x0306, // U+0306 Combining Breve
241 > U_Combining_Dot_Above = 0x0307, // U+0307 Combining Dot Above
242 > U_Combining_Diaeresis = 0x0308, // U+0308 Combining Diaeresis
243 > U_Combining_Hook_Above = 0x0309, // U+0309 Combining Hook Above
244 > U_Combining_Ring_Above = 0x030A, // U+030A Combining Ring Above
245 > U_Combining_Double_Acute_Accent = 0x030B, // U+030B Combining Double Acute Accent
246 > U_Combining_Caron = 0x030C, // U+030C Combining Caron
247 > U_Combining_Vertical_Line_Above = 0x030D, // U+030D Combining Vertical Line Above
248 > U_Combining_Double_Vertical_Line_Above = 0x030E, // U+030E Combining Double Vertical Line Above
249 > U_Combining_Double_Grave_Accent = 0x030F, // U+030F Combining Double Grave Accent
250 > U_Combining_Candrabindu = 0x0310, // U+0310 Combining Candrabindu
251 > U_Combining_Inverted_Breve = 0x0311, // U+0311 Combining Inverted Breve
252 > U_Combining_Turned_Comma_Above = 0x0312, // U+0312 Combining Turned Comma Above
253 > U_Combining_Comma_Above = 0x0313, // U+0313 Combining Comma Above
254 > U_Combining_Reversed_Comma_Above = 0x0314, // U+0314 Combining Reversed Comma Above
255 > U_Combining_Comma_Above_Right = 0x0315, // U+0315 Combining Comma Above Right
256 > U_Combining_Grave_Accent_Below = 0x0316, // U+0316 Combining Grave Accent Below
257 > U_Combining_Acute_Accent_Below = 0x0317, // U+0317 Combining Acute Accent Below
258 > U_Combining_Left_Tack_Below = 0x0318, // U+0318 Combining Left Tack Below
259 > U_Combining_Right_Tack_Below = 0x0319, // U+0319 Combining Right Tack Below
260 > U_Combining_Left_Angle_Above = 0x031A, // U+031A Combining Left Angle Above
261 > U_Combining_Horn = 0x031B, // U+031B Combining Horn
262 > U_Combining_Left_Half_Ring_Below = 0x031C, // U+031C Combining Left Half Ring Below
263 > U_Combining_Up_Tack_Below = 0x031D, // U+031D Combining Up Tack Below
264 > U_Combining_Down_Tack_Below = 0x031E, // U+031E Combining Down Tack Below
265 > U_Combining_Plus_Sign_Below = 0x031F, // U+031F Combining Plus Sign Below
266 > U_Combining_Minus_Sign_Below = 0x0320, // U+0320 Combining Minus Sign Below
267 > U_Combining_Palatalized_Hook_Below = 0x0321, // U+0321 Combining Palatalized Hook Below
268 > U_Combining_Retroflex_Hook_Below = 0x0322, // U+0322 Combining Retroflex Hook Below
269 > U_Combining_Dot_Below = 0x0323, // U+0323 Combining Dot Below
270 > U_Combining_Diaeresis_Below = 0x0324, // U+0324 Combining Diaeresis Below
271 > U_Combining_Ring_Below = 0x0325, // U+0325 Combining Ring Below
272 > U_Combining_Comma_Below = 0x0326, // U+0326 Combining Comma Below
273 > U_Combining_Cedilla = 0x0327, // U+0327 Combining Cedilla
274 > U_Combining_Ogonek = 0x0328, // U+0328 Combining Ogonek
275 > U_Combining_Vertical_Line_Below = 0x0329, // U+0329 Combining Vertical Line Below
276 > U_Combining_Bridge_Below = 0x032A, // U+032A Combining Bridge Below
277 > U_Combining_Inverted_Double_Arch_Below = 0x032B, // U+032B Combining Inverted Double Arch Below
278 > U_Combining_Caron_Below = 0x032C, // U+032C Combining Caron Below
279 > U_Combining_Circumflex_Accent_Below = 0x032D, // U+032D Combining Circumflex Accent Below
280 > U_Combining_Breve_Below = 0x032E, // U+032E Combining Breve Below
281 > U_Combining_Inverted_Breve_Below = 0x032F, // U+032F Combining Inverted Breve Below
282 > U_Combining_Tilde_Below = 0x0330, // U+0330 Combining Tilde Below
283 > U_Combining_Macron_Below = 0x0331, // U+0331 Combining Macron Below
284 > U_Combining_Low_Line = 0x0332, // U+0332 Combining Low Line
285 > U_Combining_Double_Low_Line = 0x0333, // U+0333 Combining Double Low Line
286 > U_Combining_Tilde_Overlay = 0x0334, // U+0334 Combining Tilde Overlay
287 > U_Combining_Short_Stroke_Overlay = 0x0335, // U+0335 Combining Short Stroke Overlay
288 > U_Combining_Long_Stroke_Overlay = 0x0336, // U+0336 Combining Long Stroke Overlay
289 > U_Combining_Short_Solidus_Overlay = 0x0337, // U+0337 Combining Short Solidus Overlay
290 > U_Combining_Long_Solidus_Overlay = 0x0338, // U+0338 Combining Long Solidus Overlay
291 > U_Combining_Right_Half_Ring_Below = 0x0339, // U+0339 Combining Right Half Ring Below
292 > U_Combining_Inverted_Bridge_Below = 0x033A, // U+033A Combining Inverted Bridge Below
293 > U_Combining_Square_Below = 0x033B, // U+033B Combining Square Below
294 > U_Combining_Seagull_Below = 0x033C, // U+033C Combining Seagull Below
295 > U_Combining_X_Above = 0x033D, // U+033D Combining X Above
296 > U_Combining_Vertical_Tilde = 0x033E, // U+033E Combining Vertical Tilde
297 > U_Combining_Double_Overline = 0x033F, // U+033F Combining Double Overline
298 > U_Combining_Grave_Tone_Mark = 0x0340, // U+0340 Combining Grave Tone Mark
299 > U_Combining_Acute_Tone_Mark = 0x0341, // U+0341 Combining Acute Tone Mark
300 > U_Combining_Greek_Perispomeni = 0x0342, // U+0342 Combining Greek Perispomeni
301 > U_Combining_Greek_Koronis = 0x0343, // U+0343 Combining Greek Koronis
302 > U_Combining_Greek_Dialytika_Tonos = 0x0344, // U+0344 Combining Greek Dialytika Tonos
303 > U_Combining_Greek_Ypogegrammeni = 0x0345, // U+0345 Combining Greek Ypogegrammeni
304 > U_Combining_Bridge_Above = 0x0346, // U+0346 Combining Bridge Above
305 > U_Combining_Equals_Sign_Below = 0x0347, // U+0347 Combining Equals Sign Below
306 > U_Combining_Double_Vertical_Line_Below = 0x0348, // U+0348 Combining Double Vertical Line Below
307 > U_Combining_Left_Angle_Below = 0x0349, // U+0349 Combining Left Angle Below
308 > U_Combining_Not_Tilde_Above = 0x034A, // U+034A Combining Not Tilde Above
309 > U_Combining_Homothetic_Above = 0x034B, // U+034B Combining Homothetic Above
310 > U_Combining_Almost_Equal_To_Above = 0x034C, // U+034C Combining Almost Equal To Above
311 > U_Combining_Left_Right_Arrow_Below = 0x034D, // U+034D Combining Left Right Arrow Below
312 > U_Combining_Upwards_Arrow_Below = 0x034E, // U+034E Combining Upwards Arrow Below
313 > U_Combining_Grapheme_Joiner = 0x034F, // U+034F Combining Grapheme Joiner
314 > U_Combining_Right_Arrowhead_Above = 0x0350, // U+0350 Combining Right Arrowhead Above
315 > U_Combining_Left_Half_Ring_Above = 0x0351, // U+0351 Combining Left Half Ring Above
316 > U_Combining_Fermata = 0x0352, // U+0352 Combining Fermata
317 > U_Combining_X_Below = 0x0353, // U+0353 Combining X Below
318 > U_Combining_Left_Arrowhead_Below = 0x0354, // U+0354 Combining Left Arrowhead Below
319 > U_Combining_Right_Arrowhead_Below = 0x0355, // U+0355 Combining Right Arrowhead Below
320 > U_Combining_Right_Arrowhead_And_Up_Arrowhead_Below = 0x0356, // U+0356 Combining Right Arrowhead And Up Arrowhead Below
321 > U_Combining_Right_Half_Ring_Above = 0x0357, // U+0357 Combining Right Half Ring Above
322 > U_Combining_Dot_Above_Right = 0x0358, // U+0358 Combining Dot Above Right
323 > U_Combining_Asterisk_Below = 0x0359, // U+0359 Combining Asterisk Below
324 > U_Combining_Double_Ring_Below = 0x035A, // U+035A Combining Double Ring Below
325 > U_Combining_Zigzag_Above = 0x035B, // U+035B Combining Zigzag Above
326 > U_Combining_Double_Breve_Below = 0x035C, // U+035C Combining Double Breve Below
327 > U_Combining_Double_Breve = 0x035D, // U+035D Combining Double Breve
328 > U_Combining_Double_Macron = 0x035E, // U+035E Combining Double Macron
329 > U_Combining_Double_Macron_Below = 0x035F, // U+035F Combining Double Macron Below
330 > U_Combining_Double_Tilde = 0x0360, // U+0360 Combining Double Tilde
331 > U_Combining_Double_Inverted_Breve = 0x0361, // U+0361 Combining Double Inverted Breve
332 > U_Combining_Double_Rightwards_Arrow_Below = 0x0362, // U+0362 Combining Double Rightwards Arrow Below
333 > U_Combining_Latin_Small_Letter_A = 0x0363, // U+0363 Combining Latin Small Letter A
334 > U_Combining_Latin_Small_Letter_E = 0x0364, // U+0364 Combining Latin Small Letter E
335 > U_Combining_Latin_Small_Letter_I = 0x0365, // U+0365 Combining Latin Small Letter I
336 > U_Combining_Latin_Small_Letter_O = 0x0366, // U+0366 Combining Latin Small Letter O
337 > U_Combining_Latin_Small_Letter_U = 0x0367, // U+0367 Combining Latin Small Letter U
338 > U_Combining_Latin_Small_Letter_C = 0x0368, // U+0368 Combining Latin Small Letter C
339 > U_Combining_Latin_Small_Letter_D = 0x0369, // U+0369 Combining Latin Small Letter D
340 > U_Combining_Latin_Small_Letter_H = 0x036A, // U+036A Combining Latin Small Letter H
341 > U_Combining_Latin_Small_Letter_M = 0x036B, // U+036B Combining Latin Small Letter M
342 > U_Combining_Latin_Small_Letter_R = 0x036C, // U+036C Combining Latin Small Letter R
343 > U_Combining_Latin_Small_Letter_T = 0x036D, // U+036D Combining Latin Small Letter T
344 > U_Combining_Latin_Small_Letter_V = 0x036E, // U+036E Combining Latin Small Letter V
345 > U_Combining_Latin_Small_Letter_X = 0x036F, // U+036F Combining Latin Small Letter X
346 >
347 > /**
348 > * Unicode Character 'LINE SEPARATOR' (U+2028)
349 > * http://www.fileformat.info/info/unicode/char/2028/index.htm
350 > */
351 > LINE_SEPARATOR = 0x2028,
352 > /**
353 > * Unicode Character 'PARAGRAPH SEPARATOR' (U+2029)
354 > * http://www.fileformat.info/info/unicode/char/2029/index.htm
355 > */
356 > PARAGRAPH_SEPARATOR = 0x2029,
357 > /**
358 > * Unicode Character 'NEXT LINE' (U+0085)
359 > * http://www.fileformat.info/info/unicode/char/0085/index.htm
360 > */
361 > NEXT_LINE = 0x0085,
362 >
363 > // http://www.fileformat.info/info/unicode/category/Sk/list.htm
364 > U_CIRCUMFLEX = 0x005E, // U+005E CIRCUMFLEX
365 > U_GRAVE_ACCENT = 0x0060, // U+0060 GRAVE ACCENT
366 > U_DIAERESIS = 0x00A8, // U+00A8 DIAERESIS
367 > U_MACRON = 0x00AF, // U+00AF MACRON
368 > U_ACUTE_ACCENT = 0x00B4, // U+00B4 ACUTE ACCENT
369 > U_CEDILLA = 0x00B8, // U+00B8 CEDILLA
370 > U_MODIFIER_LETTER_LEFT_ARROWHEAD = 0x02C2, // U+02C2 MODIFIER LETTER LEFT ARROWHEAD
371 > U_MODIFIER_LETTER_RIGHT_ARROWHEAD = 0x02C3, // U+02C3 MODIFIER LETTER RIGHT ARROWHEAD
372 > U_MODIFIER_LETTER_UP_ARROWHEAD = 0x02C4, // U+02C4 MODIFIER LETTER UP ARROWHEAD
373 > U_MODIFIER_LETTER_DOWN_ARROWHEAD = 0x02C5, // U+02C5 MODIFIER LETTER DOWN ARROWHEAD
374 > U_MODIFIER_LETTER_CENTRED_RIGHT_HALF_RING = 0x02D2, // U+02D2 MODIFIER LETTER CENTRED RIGHT HALF RING
375 > U_MODIFIER_LETTER_CENTRED_LEFT_HALF_RING = 0x02D3, // U+02D3 MODIFIER LETTER CENTRED LEFT HALF RING
376 > U_MODIFIER_LETTER_UP_TACK = 0x02D4, // U+02D4 MODIFIER LETTER UP TACK
377 > U_MODIFIER_LETTER_DOWN_TACK = 0x02D5, // U+02D5 MODIFIER LETTER DOWN TACK
378 > U_MODIFIER_LETTER_PLUS_SIGN = 0x02D6, // U+02D6 MODIFIER LETTER PLUS SIGN
379 > U_MODIFIER_LETTER_MINUS_SIGN = 0x02D7, // U+02D7 MODIFIER LETTER MINUS SIGN
380 > U_BREVE = 0x02D8, // U+02D8 BREVE
381 > U_DOT_ABOVE = 0x02D9, // U+02D9 DOT ABOVE
382 > U_RING_ABOVE = 0x02DA, // U+02DA RING ABOVE
383 > U_OGONEK = 0x02DB, // U+02DB OGONEK
384 > U_SMALL_TILDE = 0x02DC, // U+02DC SMALL TILDE
385 > U_DOUBLE_ACUTE_ACCENT = 0x02DD, // U+02DD DOUBLE ACUTE ACCENT
386 > U_MODIFIER_LETTER_RHOTIC_HOOK = 0x02DE, // U+02DE MODIFIER LETTER RHOTIC HOOK
387 > U_MODIFIER_LETTER_CROSS_ACCENT = 0x02DF, // U+02DF MODIFIER LETTER CROSS ACCENT
388 > U_MODIFIER_LETTER_EXTRA_HIGH_TONE_BAR = 0x02E5, // U+02E5 MODIFIER LETTER EXTRA-HIGH TONE BAR
389 > U_MODIFIER_LETTER_HIGH_TONE_BAR = 0x02E6, // U+02E6 MODIFIER LETTER HIGH TONE BAR
390 > U_MODIFIER_LETTER_MID_TONE_BAR = 0x02E7, // U+02E7 MODIFIER LETTER MID TONE BAR
391 > U_MODIFIER_LETTER_LOW_TONE_BAR = 0x02E8, // U+02E8 MODIFIER LETTER LOW TONE BAR
392 > U_MODIFIER_LETTER_EXTRA_LOW_TONE_BAR = 0x02E9, // U+02E9 MODIFIER LETTER EXTRA-LOW TONE BAR
393 > U_MODIFIER_LETTER_YIN_DEPARTING_TONE_MARK = 0x02EA, // U+02EA MODIFIER LETTER YIN DEPARTING TONE MARK
394 > U_MODIFIER_LETTER_YANG_DEPARTING_TONE_MARK = 0x02EB, // U+02EB MODIFIER LETTER YANG DEPARTING TONE MARK
395 > U_MODIFIER_LETTER_UNASPIRATED = 0x02ED, // U+02ED MODIFIER LETTER UNASPIRATED
396 > U_MODIFIER_LETTER_LOW_DOWN_ARROWHEAD = 0x02EF, // U+02EF MODIFIER LETTER LOW DOWN ARROWHEAD
397 > U_MODIFIER_LETTER_LOW_UP_ARROWHEAD = 0x02F0, // U+02F0 MODIFIER LETTER LOW UP ARROWHEAD
398 > U_MODIFIER_LETTER_LOW_LEFT_ARROWHEAD = 0x02F1, // U+02F1 MODIFIER LETTER LOW LEFT ARROWHEAD
399 > U_MODIFIER_LETTER_LOW_RIGHT_ARROWHEAD = 0x02F2, // U+02F2 MODIFIER LETTER LOW RIGHT ARROWHEAD
400 > U_MODIFIER_LETTER_LOW_RING = 0x02F3, // U+02F3 MODIFIER LETTER LOW RING
401 > U_MODIFIER_LETTER_MIDDLE_GRAVE_ACCENT = 0x02F4, // U+02F4 MODIFIER LETTER MIDDLE GRAVE ACCENT
402 > U_MODIFIER_LETTER_MIDDLE_DOUBLE_GRAVE_ACCENT = 0x02F5, // U+02F5 MODIFIER LETTER MIDDLE DOUBLE GRAVE ACCENT
403 > U_MODIFIER_LETTER_MIDDLE_DOUBLE_ACUTE_ACCENT = 0x02F6, // U+02F6 MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT
404 > U_MODIFIER_LETTER_LOW_TILDE = 0x02F7, // U+02F7 MODIFIER LETTER LOW TILDE
405 > U_MODIFIER_LETTER_RAISED_COLON = 0x02F8, // U+02F8 MODIFIER LETTER RAISED COLON
406 > U_MODIFIER_LETTER_BEGIN_HIGH_TONE = 0x02F9, // U+02F9 MODIFIER LETTER BEGIN HIGH TONE
407 > U_MODIFIER_LETTER_END_HIGH_TONE = 0x02FA, // U+02FA MODIFIER LETTER END HIGH TONE
408 > U_MODIFIER_LETTER_BEGIN_LOW_TONE = 0x02FB, // U+02FB MODIFIER LETTER BEGIN LOW TONE
409 > U_MODIFIER_LETTER_END_LOW_TONE = 0x02FC, // U+02FC MODIFIER LETTER END LOW TONE
410 > U_MODIFIER_LETTER_SHELF = 0x02FD, // U+02FD MODIFIER LETTER SHELF
411 > U_MODIFIER_LETTER_OPEN_SHELF = 0x02FE, // U+02FE MODIFIER LETTER OPEN SHELF
412 > U_MODIFIER_LETTER_LOW_LEFT_ARROW = 0x02FF, // U+02FF MODIFIER LETTER LOW LEFT ARROW
413 > U_GREEK_LOWER_NUMERAL_SIGN = 0x0375, // U+0375 GREEK LOWER NUMERAL SIGN
414 > U_GREEK_TONOS = 0x0384, // U+0384 GREEK TONOS
415 > U_GREEK_DIALYTIKA_TONOS = 0x0385, // U+0385 GREEK DIALYTIKA TONOS
416 > U_GREEK_KORONIS = 0x1FBD, // U+1FBD GREEK KORONIS
417 > U_GREEK_PSILI = 0x1FBF, // U+1FBF GREEK PSILI
418 > U_GREEK_PERISPOMENI = 0x1FC0, // U+1FC0 GREEK PERISPOMENI
419 > U_GREEK_DIALYTIKA_AND_PERISPOMENI = 0x1FC1, // U+1FC1 GREEK DIALYTIKA AND PERISPOMENI
420 > U_GREEK_PSILI_AND_VARIA = 0x1FCD, // U+1FCD GREEK PSILI AND VARIA
421 > U_GREEK_PSILI_AND_OXIA = 0x1FCE, // U+1FCE GREEK PSILI AND OXIA
422 > U_GREEK_PSILI_AND_PERISPOMENI = 0x1FCF, // U+1FCF GREEK PSILI AND PERISPOMENI
423 > U_GREEK_DASIA_AND_VARIA = 0x1FDD, // U+1FDD GREEK DASIA AND VARIA
424 > U_GREEK_DASIA_AND_OXIA = 0x1FDE, // U+1FDE GREEK DASIA AND OXIA
425 > U_GREEK_DASIA_AND_PERISPOMENI = 0x1FDF, // U+1FDF GREEK DASIA AND PERISPOMENI
426 > U_GREEK_DIALYTIKA_AND_VARIA = 0x1FED, // U+1FED GREEK DIALYTIKA AND VARIA
427 > U_GREEK_DIALYTIKA_AND_OXIA = 0x1FEE, // U+1FEE GREEK DIALYTIKA AND OXIA
428 > U_GREEK_VARIA = 0x1FEF, // U+1FEF GREEK VARIA
429 > U_GREEK_OXIA = 0x1FFD, // U+1FFD GREEK OXIA
430 > U_GREEK_DASIA = 0x1FFE, // U+1FFE GREEK DASIA
431 >
432 > U_IDEOGRAPHIC_FULL_STOP = 0x3002, // U+3002 IDEOGRAPHIC FULL STOP
433 > U_LEFT_CORNER_BRACKET = 0x300C, // U+300C LEFT CORNER BRACKET
434 > U_RIGHT_CORNER_BRACKET = 0x300D, // U+300D RIGHT CORNER BRACKET
435 > U_LEFT_BLACK_LENTICULAR_BRACKET = 0x3010, // U+3010 LEFT BLACK LENTICULAR BRACKET
436 > U_RIGHT_BLACK_LENTICULAR_BRACKET = 0x3011, // U+3011 RIGHT BLACK LENTICULAR BRACKET
437 >
438 >
439 > U_OVERLINE = 0x203E, // Unicode Character 'OVERLINE'
440 >
441 > /**
442 > * UTF-8 BOM
443 > * Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF)
444 > * http://www.fileformat.info/info/unicode/char/feff/index.htm
445 > */
446 > UTF8_BOM = 65279,
447 >
448 > U_FULLWIDTH_SEMICOLON = 0xFF1B, // U+FF1B FULLWIDTH SEMICOLON
449 > U_FULLWIDTH_COMMA = 0xFF0C, // U+FF0C FULLWIDTH COMMA
450 > }
src/vs/platform/log/common/log.ts 447 covered LOC · 73 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- log.ts
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 > import * as nls from '../../../nls.js';
7 > import { toErrorMessage } from '../../../base/common/errorMessage.js';
8 > import { Emitter, Event } from '../../../base/common/event.js';
9 > import { hash } from '../../../base/common/hash.js';
10 > import { Disposable, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
11 > import { ResourceMap } from '../../../base/common/map.js';
12 > import { isWindows } from '../../../base/common/platform.js';
13 > import { joinPath } from '../../../base/common/resources.js';
14 > import { Mutable, isNumber, isString } from '../../../base/common/types.js';
15 > import { URI } from '../../../base/common/uri.js';
16 > import { ILocalizedString } from '../../action/common/action.js';
17 > import { RawContextKey } from '../../contextkey/common/contextkey.js';
18 > import { IEnvironmentService } from '../../environment/common/environment.js';
19 > import { createDecorator } from '../../instantiation/common/instantiation.js';
20 >
21 > export const ILogService = createDecorator<ILogService>('logService');
22 > export const ILoggerService = createDecorator<ILoggerService>('loggerService');
23 >
24 function now(): string {
25 return new Date().toISOString();
26 }
27 > log.ts
28 > export function isLogLevel(thing: unknown): thing is LogLevel {
29 return isNumber(thing);
30 }
31 > log.ts
32 > export enum LogLevel {
33 > Off,
34 > Trace,
35 > Debug,
36 > Info,
37 > Warning,
38 > Error
39 > }
40 >
41 > export const DEFAULT_LOG_LEVEL: LogLevel = LogLevel.Info;
42 >
43 > export interface ILogger extends IDisposable {
44 > readonly onDidChangeLogLevel: Event<LogLevel>;
45 > getLevel(): LogLevel;
46 > setLevel(level: LogLevel): void;
47 >
48 > trace(message: string, ...args: unknown[]): void;
49 > debug(message: string, ...args: unknown[]): void;
50 > info(message: string, ...args: unknown[]): void;
51 > warn(message: string, ...args: unknown[]): void;
52 > error(message: string | Error, ...args: unknown[]): void;
53 >
54 > /**
55 > * An operation to flush the contents. Can be synchronous.
56 > */
57 > flush(): void;
58 > }
59 >
60 > export function canLog(loggerLevel: LogLevel, messageLevel: LogLevel): boolean {
61 return loggerLevel !== LogLevel.Off && loggerLevel <= messageLevel;
62 }
63 > log.ts
64 > export function log(logger: ILogger, level: LogLevel, message: string): void {
65 switch (level) {
66 case LogLevel.Trace: logger.trace(message); break;
73 }
74 }
75 > log.ts
76 > type ConsoleMethod = 'debug' | 'error' | 'info' | 'log' | 'warn';
77 > type ConsoleMethodFn = (...args: unknown[]) => void;
78 >
79 > /**
80 > * Flag to enable forwarding of console.* calls to the log service in development.
81 > * This is intended for the use of agents to quickly instrument the code with console.logs
82 > * which will end up in the log service's file outputs.
83 > */
84 > export const isDevConsoleLogForwardingEnabled = false
85 > // || Boolean("true") // done "weirdly" so that a lint warning prevents you from pushing this
86 > ;
87 >
88 > let isConsoleForwarding = false;
89 > let isLogServiceConsoleEcho = false;
90 >
91 function getConsoleMethod(method: ConsoleMethod): ConsoleMethodFn {
92 switch (method) {
98 }
99 }
100 > log.ts
101 function setConsoleMethod(method: ConsoleMethod, fn: ConsoleMethodFn): void {
102 switch (method) {
108 }
109 }
110 > log.ts
111 function logToConsole(method: ConsoleMethod, ...args: unknown[]): void {
112 if (isConsoleForwarding) {
120 }
121 }
122 > log.ts
123 > export function registerDevConsoleLogForwarder(logService: ILogService): IDisposable {
124 const originalConsoleMethods: Record<ConsoleMethod, ConsoleMethodFn> = {
125 debug: console.debug,
177 });
178 }
179 > log.ts
180 > export function format(args: any, verbose: boolean = false): string {
181 let result = '';
182
199 return result;
200 }
201 > log.ts
202 > export type LoggerGroup = {
203 > readonly id: string;
204 > readonly name: string;
205 > };
206 >
207 > export interface ILogService extends ILogger {
208 > readonly _serviceBrand: undefined;
209 > }
210 >
211 > export interface ILoggerOptions {
212 >
213 > /**
214 > * Id of the logger.
215 > */
216 > id?: string;
217 >
218 > /**
219 > * Name of the logger.
220 > */
221 > name?: string;
222 >
223 > /**
224 > * Do not create rotating files if max size exceeds.
225 > */
226 > donotRotate?: boolean;
227 >
228 > /**
229 > * Do not use formatters.
230 > */
231 > donotUseFormatters?: boolean;
232 >
233 > /**
234 > * When to log. Set to `always` to log always.
235 > */
236 > logLevel?: 'always' | LogLevel;
237 >
238 > /**
239 > * Whether the log should be hidden from the user.
240 > */
241 > hidden?: boolean;
242 >
243 > /**
244 > * Condition which must be true to show this logger
245 > */
246 > when?: string;
247 >
248 > /**
249 > * Id of the extension that created this logger.
250 > */
251 > extensionId?: string;
252 >
253 > /**
254 > * Group of the logger.
255 > */
256 > group?: LoggerGroup;
257 > }
258 >
259 > export interface ILoggerResource {
260 > readonly resource: URI;
261 > readonly id: string;
262 > readonly name?: string;
263 > readonly logLevel?: LogLevel;
264 > readonly hidden?: boolean;
265 > readonly when?: string;
266 > readonly extensionId?: string;
267 > readonly group?: LoggerGroup;
268 > }
269 >
270 > export type DidChangeLoggersEvent = {
271 > readonly added: Iterable<ILoggerResource>;
272 > readonly removed: Iterable<ILoggerResource>;
273 > };
274 >
275 > export interface ILoggerService {
276 >
277 > readonly _serviceBrand: undefined;
278 >
279 > /**
280 > * Creates a logger for the given resource, or gets one if it already exists.
281 > *
282 > * This will also register the logger with the logger service.
283 > */
284 > createLogger(resource: URI, options?: ILoggerOptions): ILogger;
285 >
286 > /**
287 > * Creates a logger with the given id in the logs folder, or gets one if it already exists.
288 > *
289 > * This will also register the logger with the logger service.
290 > */
291 > createLogger(id: string, options?: Omit<ILoggerOptions, 'id'>): ILogger;
292 >
293 > /**
294 > * Gets an existing logger, if any.
295 > */
296 > getLogger(resourceOrId: URI | string): ILogger | undefined;
297 >
298 > /**
299 > * An event which fires when the log level of a logger has changed
300 > */
301 > readonly onDidChangeLogLevel: Event<LogLevel | [URI, LogLevel]>;
302 >
303 > /**
304 > * Set default log level.
305 > */
306 > setLogLevel(level: LogLevel): void;
307 >
308 > /**
309 > * Set log level for a logger.
310 > */
311 > setLogLevel(resource: URI, level: LogLevel): void;
312 >
313 > /**
314 > * Get log level for a logger or the default log level.
315 > */
316 > getLogLevel(resource?: URI): LogLevel;
317 >
318 > /**
319 > * An event which fires when the visibility of a logger has changed
320 > */
321 > readonly onDidChangeVisibility: Event<[URI, boolean]>;
322 >
323 > /**
324 > * Set the visibility of a logger.
325 > */
326 > setVisibility(resourceOrId: URI | string, visible: boolean): void;
327 >
328 > /**
329 > * An event which fires when the logger resources are changed
330 > */
331 > readonly onDidChangeLoggers: Event<DidChangeLoggersEvent>;
332 >
333 > /**
334 > * Register a logger with the logger service.
335 > *
336 > * Note that this will not create a logger, but only register it.
337 > *
338 > * Use `createLogger` to create a logger and register it.
339 > *
340 > * Use it when you want to register a logger that is not created by the logger service.
341 > */
342 > registerLogger(resource: ILoggerResource): void;
343 >
344 > /**
345 > * Deregister the logger for the given resource.
346 > */
347 > deregisterLogger(idOrResource: URI | string): void;
348 >
349 > /**
350 > * Get all registered loggers
351 > */
352 > getRegisteredLoggers(): Iterable<ILoggerResource>;
353 >
354 > /**
355 > * Get the registered logger for the given resource.
356 > */
357 > getRegisteredLogger(resource: URI): ILoggerResource | undefined;
358 > }
359 >
360 > export abstract class AbstractLogger extends Disposable implements ILogger {
361
362 private level: LogLevel = DEFAULT_LOG_LEVEL;
363 private readonly _onDidChangeLogLevel: Emitter<LogLevel> = this._register(new Emitter<LogLevel>());
364 > get onDidChangeLogLevel(): Event<LogLevel> { return this._onDidChangeLogLevel.event; } log.ts
365 >
366 > setLevel(level: LogLevel): void {
367 if (this.level !== level) {
368 this.level = level;
370 }
371 }
372 > log.ts
373 > getLevel(): LogLevel {
374 return this.level;
375 }
376 > log.ts
377 > protected checkLogLevel(level: LogLevel): boolean {
378 return canLog(this.level, level);
379 }
380 > log.ts
381 > protected canLog(level: LogLevel): boolean {
382 if (this._store.isDisposed) {
383 return false;
385 return this.checkLogLevel(level);
386 }
387 > log.ts
388 > abstract trace(message: string, ...args: unknown[]): void;
389 > abstract debug(message: string, ...args: unknown[]): void;
390 > abstract info(message: string, ...args: unknown[]): void;
391 > abstract warn(message: string, ...args: unknown[]): void;
392 > abstract error(message: string | Error, ...args: unknown[]): void;
393 > abstract flush(): void;
394 > }
395 >
396 > export abstract class AbstractMessageLogger extends AbstractLogger implements ILogger {
397 >
398 > constructor(private readonly logAlways?: boolean) {
399 super();
400 }
401 > log.ts
402 > protected override checkLogLevel(level: LogLevel): boolean {
403 return this.logAlways || super.checkLogLevel(level);
404 }
405 > log.ts
406 > trace(message: string, ...args: unknown[]): void {
407 if (this.canLog(LogLevel.Trace)) {
408 this.log(LogLevel.Trace, format([message, ...args], true));
409 }
410 }
411 > log.ts
412 > debug(message: string, ...args: unknown[]): void {
413 if (this.canLog(LogLevel.Debug)) {
414 this.log(LogLevel.Debug, format([message, ...args]));
415 }
416 }
417 > log.ts
418 > info(message: string, ...args: unknown[]): void {
419 if (this.canLog(LogLevel.Info)) {
420 this.log(LogLevel.Info, format([message, ...args]));
421 }
422 }
423 > log.ts
424 > warn(message: string, ...args: unknown[]): void {
425 if (this.canLog(LogLevel.Warning)) {
426 this.log(LogLevel.Warning, format([message, ...args]));
427 }
428 }
429 > log.ts
430 > error(message: string | Error, ...args: unknown[]): void {
431 if (this.canLog(LogLevel.Error)) {
432 if (message instanceof Error) {
439 }
440 }
441 > log.ts
442 > flush(): void { }
443 >
444 > protected abstract log(level: LogLevel, message: string): void;
445 > }
446 >
447 >
448 > export class ConsoleMainLogger extends AbstractLogger implements ILogger {
449 >
450 > private useColors: boolean;
451 >
452 > constructor(logLevel: LogLevel = DEFAULT_LOG_LEVEL) {
453 super();
454 this.setLevel(logLevel);
455 this.useColors = !isWindows;
456 }
457 > log.ts
458 > trace(message: string, ...args: unknown[]): void {
459 if (this.canLog(LogLevel.Trace)) {
460 if (this.useColors) {
465 }
466 }
467 > log.ts
468 > debug(message: string, ...args: unknown[]): void {
469 if (this.canLog(LogLevel.Debug)) {
470 if (this.useColors) {
475 }
476 }
477 > log.ts
478 > info(message: string, ...args: unknown[]): void {
479 if (this.canLog(LogLevel.Info)) {
480 if (this.useColors) {
485 }
486 }
487 > log.ts
488 > warn(message: string | Error, ...args: unknown[]): void {
489 if (this.canLog(LogLevel.Warning)) {
490 if (this.useColors) {
495 }
496 }
497 > log.ts
498 > error(message: string, ...args: unknown[]): void {
499 if (this.canLog(LogLevel.Error)) {
500 if (this.useColors) {
505 }
506 }
507 > log.ts
508 > flush(): void {
509 // noop
510 }
511 > log.ts
512 > }
513 >
514 > export class ConsoleLogger extends AbstractLogger implements ILogger {
515 >
516 > constructor(logLevel: LogLevel = DEFAULT_LOG_LEVEL, private readonly useColors: boolean = true) {
517 super();
518 this.setLevel(logLevel);
519 }
520 > log.ts
521 > trace(message: string, ...args: unknown[]): void {
522 if (this.canLog(LogLevel.Trace)) {
523 if (this.useColors) {
528 }
529 }
530 > log.ts
531 > debug(message: string, ...args: unknown[]): void {
532 if (this.canLog(LogLevel.Debug)) {
533 if (this.useColors) {
538 }
539 }
540 > log.ts
541 > info(message: string, ...args: unknown[]): void {
542 if (this.canLog(LogLevel.Info)) {
543 if (this.useColors) {
548 }
549 }
550 > log.ts
551 > warn(message: string | Error, ...args: unknown[]): void {
552 if (this.canLog(LogLevel.Warning)) {
553 if (this.useColors) {
558 }
559 }
560 > log.ts
561 > error(message: string, ...args: unknown[]): void {
562 if (this.canLog(LogLevel.Error)) {
563 if (this.useColors) {
568 }
569 }
570 > log.ts
571 >
572 > flush(): void {
573 // noop
574 }
575 > } log.ts
576 >
577 > export class AdapterLogger extends AbstractLogger implements ILogger {
578 >
579 > constructor(private readonly adapter: { log: (logLevel: LogLevel, args: any[]) => void }, logLevel: LogLevel = DEFAULT_LOG_LEVEL) {
580 super();
581 this.setLevel(logLevel);
582 }
583 > log.ts
584 > trace(message: string, ...args: unknown[]): void {
585 if (this.canLog(LogLevel.Trace)) {
586 this.adapter.log(LogLevel.Trace, [this.extractMessage(message), ...args]);
587 }
588 }
589 > log.ts
590 > debug(message: string, ...args: unknown[]): void {
591 if (this.canLog(LogLevel.Debug)) {
592 this.adapter.log(LogLevel.Debug, [this.extractMessage(message), ...args]);
593 }
594 }
595 > log.ts
596 > info(message: string, ...args: unknown[]): void {
597 if (this.canLog(LogLevel.Info)) {
598 this.adapter.log(LogLevel.Info, [this.extractMessage(message), ...args]);
599 }
600 }
601 > log.ts
602 > warn(message: string | Error, ...args: unknown[]): void {
603 if (this.canLog(LogLevel.Warning)) {
604 this.adapter.log(LogLevel.Warning, [this.extractMessage(message), ...args]);
605 }
606 }
607 > log.ts
608 > error(message: string | Error, ...args: unknown[]): void {
609 if (this.canLog(LogLevel.Error)) {
610 this.adapter.log(LogLevel.Error, [this.extractMessage(message), ...args]);
611 }
612 }
613 > log.ts
614 > private extractMessage(msg: string | Error): string {
615 if (typeof msg === 'string') {
616 return msg;
619 return toErrorMessage(msg, this.canLog(LogLevel.Trace));
620 }
621 > log.ts
622 > flush(): void {
623 // noop
624 }
625 > } log.ts
626 >
627 > export class MultiplexLogger extends AbstractLogger implements ILogger {
628 >
629 > constructor(private readonly loggers: ReadonlyArray<ILogger>) {
630 super();
631 if (loggers.length) {
633 }
634 }
635 > log.ts
636 > override setLevel(level: LogLevel): void {
637 for (const logger of this.loggers) {
638 logger.setLevel(level);
640 super.setLevel(level);
641 }
642 > log.ts
643 > trace(message: string, ...args: unknown[]): void {
644 for (const logger of this.loggers) {
645 logger.trace(message, ...args);
646 }
647 }
648 > log.ts
649 > debug(message: string, ...args: unknown[]): void {
650 for (const logger of this.loggers) {
651 logger.debug(message, ...args);
652 }
653 }
654 > log.ts
655 > info(message: string, ...args: unknown[]): void {
656 for (const logger of this.loggers) {
657 logger.info(message, ...args);
658 }
659 }
660 > log.ts
661 > warn(message: string, ...args: unknown[]): void {
662 for (const logger of this.loggers) {
663 logger.warn(message, ...args);
664 }
665 }
666 > log.ts
667 > error(message: string | Error, ...args: unknown[]): void {
668 for (const logger of this.loggers) {
669 logger.error(message, ...args);
670 }
671 }
672 > log.ts
673 > flush(): void {
674 for (const logger of this.loggers) {
675 logger.flush();
676 }
677 }
678 > log.ts
679 > override dispose(): void {
680 for (const logger of this.loggers) {
681 logger.dispose();
683 super.dispose();
684 }
685 > } log.ts
686 >
687 > type LoggerEntry = { logger: ILogger | undefined; info: Mutable<ILoggerResource> };
688 >
689 > export abstract class AbstractLoggerService extends Disposable implements ILoggerService {
690 >
691 > declare readonly _serviceBrand: undefined;
692 >
693 > private readonly _loggers = new ResourceMap<LoggerEntry>();
694 >
695 > private _onDidChangeLoggers = this._register(new Emitter<{ added: ILoggerResource[]; removed: ILoggerResource[] }>);
696 > readonly onDidChangeLoggers = this._onDidChangeLoggers.event;
697 >
698 > private _onDidChangeLogLevel = this._register(new Emitter<LogLevel | [URI, LogLevel]>);
699 > readonly onDidChangeLogLevel = this._onDidChangeLogLevel.event;
700 >
701 > private _onDidChangeVisibility = this._register(new Emitter<[URI, boolean]>);
702 > readonly onDidChangeVisibility = this._onDidChangeVisibility.event;
703 >
704 > constructor(
705 protected logLevel: LogLevel,
706 private readonly logsHome: URI,
714 }
715 }
716 > log.ts
717 > private getLoggerEntry(resourceOrId: URI | string): LoggerEntry | undefined {
718 if (isString(resourceOrId)) {
719 return [...this._loggers.values()].find(logger => logger.info.id === resourceOrId);
721 return this._loggers.get(resourceOrId);
722 }
723 > log.ts
724 > getLogger(resourceOrId: URI | string): ILogger | undefined {
725 return this.getLoggerEntry(resourceOrId)?.logger;
726 }
727 > log.ts
728 > createLogger(idOrResource: URI | string, options?: ILoggerOptions): ILogger {
729 const resource = this.toResource(idOrResource);
730 const id = isString(idOrResource) ? idOrResource : (options?.id ?? hash(resource.toString()).toString(16));
752 return logger;
753 }
754 > log.ts
755 > protected toResource(idOrResource: string | URI): URI {
756 return isString(idOrResource) ? joinPath(this.logsHome, `${idOrResource.replace(/[\\/:\*\?"<>\|]/g, '')}.log`) : idOrResource;
757 }
758 > log.ts
759 > setLogLevel(logLevel: LogLevel): void;
760 > setLogLevel(resource: URI, logLevel: LogLevel): void;
761 > setLogLevel(arg1: any, arg2?: any): void {
762 if (URI.isUri(arg1)) {
763 const resource = arg1;
780 }
781 }
782 > log.ts
783 > setVisibility(resourceOrId: URI | string, visibility: boolean): void {
784 const logger = this.getLoggerEntry(resourceOrId);
785 if (logger && visibility !== !logger.info.hidden) {
789 }
790 }
791 > log.ts
792 > getLogLevel(resource?: URI): LogLevel {
793 let logLevel;
794 if (resource) {
797 return logLevel ?? this.logLevel;
798 }
799 > log.ts
800 > registerLogger(resource: ILoggerResource): void {
801 const existing = this._loggers.get(resource.resource);
802 if (existing) {
809 }
810 }
811 > log.ts
812 > deregisterLogger(idOrResource: URI | string): void {
813 const resource = this.toResource(idOrResource);
814 const existing = this._loggers.get(resource);
821 }
822 }
823 > log.ts
824 > *getRegisteredLoggers(): Iterable<ILoggerResource> {
825 for (const entry of this._loggers.values()) {
826 yield entry.info;
827 }
828 }
829 > log.ts
830 > getRegisteredLogger(resource: URI): ILoggerResource | undefined {
831 return this._loggers.get(resource)?.info;
832 }
833 > log.ts
834 > override dispose(): void {
835 this._loggers.forEach(logger => logger.logger?.dispose());
836 this._loggers.clear();
837 super.dispose();
838 }
839 > log.ts
840 > protected abstract doCreateLogger(resource: URI, logLevel: LogLevel, options?: ILoggerOptions): ILogger;
841 > }
842 >
843 > export class NullLogger implements ILogger {
844 > readonly onDidChangeLogLevel: Event<LogLevel> = new Emitter<LogLevel>().event; log.ts
845 > setLevel(level: LogLevel): void { } log.ts
846 > getLevel(): LogLevel { return LogLevel.Info; }
847 > trace(message: string, ...args: unknown[]): void { }
848 > debug(message: string, ...args: unknown[]): void { }
849 > info(message: string, ...args: unknown[]): void { }
850 > warn(message: string, ...args: unknown[]): void { }
851 > error(message: string | Error, ...args: unknown[]): void { }
852 > critical(message: string | Error, ...args: unknown[]): void { }
853 > dispose(): void { }
854 > flush(): void { }
855 > }
856 >
857 > export class NullLogService extends NullLogger implements ILogService {
858 > declare readonly _serviceBrand: undefined;
859 > }
860 >
861 > export class NullLoggerService extends AbstractLoggerService {
862 > constructor() {
863 super(LogLevel.Off, URI.parse('log:///log'));
864 }
865 > protected override doCreateLogger(resource: URI, logLevel: LogLevel, options?: ILoggerOptions): ILogger { log.ts
866 return new NullLogger();
867 }
868 > } log.ts
869 >
870 > export function getLogLevel(environmentService: IEnvironmentService): LogLevel {
871 if (environmentService.verbose) {
872 return LogLevel.Trace;
880 return DEFAULT_LOG_LEVEL;
881 }
882 > log.ts
883 > export function LogLevelToString(logLevel: LogLevel): string {
884 > switch (logLevel) {
885 > case LogLevel.Trace: return 'trace';
886 > case LogLevel.Debug: return 'debug';
887 > case LogLevel.Info: return 'info';
888 > case LogLevel.Warning: return 'warn';
889 > case LogLevel.Error: return 'error';
890 > case LogLevel.Off: return 'off';
891 > }
892 > }
893 >
894 > export function LogLevelToLocalizedString(logLevel: LogLevel): ILocalizedString {
895 switch (logLevel) {
896 case LogLevel.Trace: return { original: 'Trace', value: nls.localize('trace', "Trace") };
902 }
903 }
904 > log.ts
905 > export function parseLogLevel(logLevel: string): LogLevel | undefined {
906 switch (logLevel) {
907 case 'trace':
922 return undefined;
923 }
924 > log.ts
925 > // Contexts
926 > export const CONTEXT_LOG_LEVEL = new RawContextKey<string>('logLevel', LogLevelToString(LogLevel.Info));
src/vs/platform/agentHost/common/sessionDataService.ts 421 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- sessionDataService.ts
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 > import { IDisposable, IReference } from '../../../base/common/lifecycle.js';
7 > import { URI } from '../../../base/common/uri.js';
8 > import { createDecorator } from '../../instantiation/common/instantiation.js';
9 > import { Event } from '../../../base/common/event.js';
10 > import type { FileEditKind, Message } from './state/sessionState.js';
11 >
12 > export const ISessionDataService = createDecorator<ISessionDataService>('sessionDataService');
13 >
14 > /** Filename of the per-session SQLite database. */
15 > export const SESSION_DB_FILENAME = 'session.db';
16 >
17 > /**
18 > * Subdirectory under a session's data directory that holds snapshotted
19 > * user-message attachments (e.g. pasted images, fetched file references).
20 > * The agent host writes these on dispatch so large blobs stay out of the
21 > * in-memory state tree, and reads of files under this directory are
22 > * auto-approved by the agent's permission flow.
23 > */
24 > export const SESSION_ATTACHMENTS_DIRNAME = 'attachments';
25 >
26 > // ---- File-edit types ----------------------------------------------------
27 >
28 > /**
29 > * Lightweight metadata for a file edit. Returned by {@link ISessionDatabase.getFileEdits}
30 > * without the (potentially large) file content blobs.
31 > */
32 > export interface IFileEditRecord {
33 > /** The turn that owns this file edit. */
34 > turnId: string;
35 > /** The tool call that produced this edit. */
36 > toolCallId: string;
37 > /** Primary file path (after-path for edits/creates/renames, before-path for deletes). */
38 > filePath: string;
39 > /** The kind of file operation. */
40 > kind: FileEditKind;
41 > /** For renames, the original file path before the move. */
42 > originalPath?: string;
43 > /** Number of lines added (informational, for diff metadata). */
44 > addedLines: number | undefined;
45 > /** Number of lines removed (informational, for diff metadata). */
46 > removedLines: number | undefined;
47 > }
48 >
49 > /**
50 > * The before/after content blobs for a single file edit.
51 > * Retrieved on demand via {@link ISessionDatabase.readFileEditContent}.
52 > *
53 > * For creates, `beforeContent` is absent.
54 > * For deletes, `afterContent` is absent.
55 > */
56 > export interface IFileEditContent {
57 > /** File content before the edit. Absent for file creations. */
58 > beforeContent?: Uint8Array;
59 > /** File content after the edit. Absent for file deletions. */
60 > afterContent?: Uint8Array;
61 > }
62 >
63 > // ---- Reviewed-file types ------------------------------------------------
64 >
65 > /**
66 > * A record of a file having been reviewed by the user at a specific content
67 > * nonce. Returned by {@link ISessionDatabase.getReviewedFiles} and
68 > * {@link ISessionDatabase.getReviewedFilesForUri}.
69 > */
70 > export interface IReviewedFileRecord {
71 > /** The reviewed file. */
72 > uri: URI;
73 > /** Content version/hash captured at review time. */
74 > nonce: string;
75 > }
76 >
77 > // ---- Session database ---------------------------------------------------
78 >
79 > /**
80 > * A host-injected ("local") turn: a completed protocol `Turn` the agent SDK
81 > * never saw — e.g. the `/rename` acknowledgement or a `!command` terminal run.
82 > * These are persisted separately from SDK turns so they survive reload, and are
83 > * interleaved back into the SDK-derived turns on restore.
84 > */
85 > export interface ILocalTurnRecord {
86 > /** The local turn's id (matches the payload `Turn.id`). */
87 > turnId: string;
88 > /** The chat this local turn belongs to (its channel URI string). */
89 > chatUri: string;
90 > /**
91 > * Id of the preceding concrete (SDK-backed) turn this local turn is
92 > * anchored after, or `undefined` when it precedes any real turn.
93 > */
94 > anchorTurnId: string | undefined;
95 > /** Monotonic ordering among local turns (used to interleave on restore). */
96 > seq: number;
97 > /** JSON-serialized protocol `Turn`. */
98 > payload: string;
99 > }
100 >
101 >
102 > /**
103 > * A disposable handle to a per-session SQLite database backed by
104 > * `@vscode/sqlite3`.
105 > *
106 > * Callers obtain an instance via {@link ISessionDataService.openDatabase} and
107 > * **must** dispose it when finished to close the underlying database connection.
108 > */
109 > export interface ISessionDatabase extends IDisposable {
110 > /**
111 > * Create a turn record. Must be called before storing file edits that
112 > * reference this turn.
113 > */
114 > createTurn(turnId: string): Promise<void>;
115 >
116 > /**
117 > * Delete a turn and all of its associated file edits (cascade).
118 > */
119 > deleteTurn(turnId: string): Promise<void>;
120 >
121 > /**
122 > * Associates a Copilot SDK event ID with a turn. The event ID corresponds
123 > * to the `user.message` event in the SDK event stream and is used by
124 > * the SDK's `history.truncate` and `sessions.fork` RPCs.
125 > */
126 > setTurnEventId(turnId: string, eventId: string): Promise<void>;
127 >
128 > /**
129 > * Retrieves the SDK event ID previously stored for a turn.
130 > * Returns `undefined` if no event ID has been set.
131 > */
132 > getTurnEventId(turnId: string): Promise<string | undefined>;
133 >
134 > /**
135 > * Returns the SDK event ID of the turn inserted immediately after the
136 > * given turn, or `undefined` if the given turn is the last one.
137 > */
138 > getNextTurnEventId(turnId: string): Promise<string | undefined>;
139 >
140 > /**
141 > * Returns the SDK event ID of the earliest turn in insertion order,
142 > * or `undefined` if there are no turns.
143 > */
144 > getFirstTurnEventId(): Promise<string | undefined>;
145 >
146 > /**
147 > * Associates a git checkpoint ref (e.g. `refs/agents/<sid>/checkpoints/turn/N`)
148 > * with a turn. Idempotent — last writer wins per turn.
149 > */
150 > setTurnCheckpointRef(turnId: string, ref: string): Promise<void>;
151 >
152 > /**
153 > * Retrieves the checkpoint ref previously stored for a turn, or
154 > * `undefined` if none.
155 > */
156 > getTurnCheckpointRef(turnId: string): Promise<string | undefined>;
157 >
158 > /**
159 > * Returns the checkpoint ref of the most recent turn (in insertion
160 > * order) prior to `turnId` that has a non-null `checkpoint_ref`.
161 > * Used to resolve the parent checkpoint for end-of-turn diffs without
162 > * persisting an explicit parent column.
163 > */
164 > getPreviousCheckpointRef(turnId: string): Promise<string | undefined>;
165 >
166 > /**
167 > * Returns every non-null `checkpoint_ref` recorded against any turn in
168 > * this session. Used by checkpoint cleanup to enumerate refs precisely
169 > * (rather than scanning `for-each-ref` on the underlying repo).
170 > */
171 > getAllCheckpointRefs(): Promise<string[]>;
172 >
173 > /**
174 > * Deletes the given turn and all turns inserted after it, along
175 > * with their associated file edits (cascade).
176 > */
177 > truncateFromTurn(turnId: string): Promise<void>;
178 >
179 > /**
180 > * Deletes all turns inserted after the given turn (but keeps the
181 > * given turn itself). Associated file edits cascade-delete.
182 > */
183 > deleteTurnsAfter(turnId: string): Promise<void>;
184 >
185 > /**
186 > * Deletes all turns and their associated file edits.
187 > */
188 > deleteAllTurns(): Promise<void>;
189 >
190 > // ---- Local (host-injected) turns -------------------------------------
191 >
192 > /**
193 > * Persist a host-injected local turn (e.g. `/rename` or `!command`).
194 > * Replaces any existing record with the same `turnId`.
195 > */
196 > insertLocalTurn(record: ILocalTurnRecord): Promise<void>;
197 >
198 > /**
199 > * Retrieve all persisted local turns in this session, in `seq` order.
200 > * Callers filter by {@link ILocalTurnRecord.chatUri} for a given chat.
201 > */
202 > getLocalTurns(): Promise<ILocalTurnRecord[]>;
203 >
204 > /**
205 > * Delete the local turns with the given ids. Ids not present are ignored.
206 > */
207 > deleteLocalTurns(turnIds: readonly string[]): Promise<void>;
208 >
209 > /**
210 > * Store a file-edit snapshot (metadata + content) for a tool invocation
211 > * within a turn.
212 > *
213 > * If a record for the same `toolCallId` and `filePath` already exists
214 > * it is replaced.
215 > */
216 > storeFileEdit(edit: IFileEditRecord & IFileEditContent): Promise<void>;
217 >
218 > /**
219 > * Retrieve file-edit metadata for the given tool call IDs.
220 > * Content blobs are **not** included — use {@link readFileEditContent}
221 > * to fetch them on demand. Results are returned in insertion order.
222 > */
223 > getFileEdits(toolCallIds: string[]): Promise<IFileEditRecord[]>;
224 >
225 > /**
226 > * Retrieve file-edit metadata for all edits in this session.
227 > * Content blobs are **not** included — use {@link readFileEditContent}
228 > * to fetch them on demand. Results are returned in insertion order.
229 > */
230 > getAllFileEdits(): Promise<IFileEditRecord[]>;
231 >
232 > /**
233 > * Retrieve file-edit metadata for all edits belonging to a specific turn.
234 > * Content blobs are **not** included — use {@link readFileEditContent}
235 > * to fetch them on demand. Results are returned in insertion order.
236 > */
237 > getFileEditsByTurn(turnId: string): Promise<IFileEditRecord[]>;
238 >
239 > /**
240 > * Read the before/after content blobs for a single file edit.
241 > * Returns `undefined` if no edit exists for the given key.
242 > */
243 > readFileEditContent(toolCallId: string, filePath: string): Promise<IFileEditContent | undefined>;
244 >
245 > // ---- Session metadata ------------------------------------------------
246 >
247 > /**
248 > * Read a metadata value by key.
249 > * Returns `undefined` if no value has been stored for the key.
250 > */
251 > getMetadata(key: string): Promise<string | undefined>;
252 >
253 > /**
254 > * Gets a bulk of metadata. For example `getMetadataObject({ foo: true }) -> { foo: 'data' }`
255 > */
256 > getMetadataObject<T extends Record<string, unknown>>(obj: T): Promise<{ [K in keyof T]: string | undefined }>;
257 >
258 > /**
259 > * Store a metadata key-value pair. Overwrites any existing value for the key.
260 > */
261 > setMetadata(key: string, value: string): Promise<void>;
262 >
263 > /**
264 > * Store or clear the draft for a chat in this session.
265 > */
266 > setChatDraft(chat: URI, draft: Message | undefined): Promise<void>;
267 >
268 > /**
269 > * Read the stored draft for a chat in this session.
270 > */
271 > getChatDraft(chat: URI): Promise<Message | undefined>;
272 >
273 > /**
274 > * Bulk-remaps turn IDs using the provided old→new mapping.
275 > * Used after copying a database file for a forked session.
276 > */
277 > remapTurnIds(mapping: ReadonlyMap<string, string>): Promise<void>;
278 >
279 > // ---- Reviewed files --------------------------------------------------
280 >
281 > /**
282 > * Mark a file (identified by URI + content nonce) as reviewed by the user.
283 > * Idempotent — re-marking the same `(uri, nonce)` pair is a no-op.
284 > */
285 > markFileReviewed(uri: URI, nonce: string): Promise<void>;
286 >
287 > /**
288 > * Remove the reviewed-file entry for the given URI + content nonce.
289 > * No-op if no such entry exists.
290 > */
291 > unmarkFileReviewed(uri: URI, nonce: string): Promise<void>;
292 >
293 > /**
294 > * Return every reviewed-file entry in this session, in insertion order.
295 > */
296 > getReviewedFiles(): Promise<IReviewedFileRecord[]>;
297 >
298 > /**
299 > * Return all reviewed-file entries for a specific URI (one per reviewed
300 > * content nonce), in insertion order.
301 > */
302 > getReviewedFilesForUri(uri: URI): Promise<IReviewedFileRecord[]>;
303 >
304 > /**
305 > * Return whether the given file has been reviewed at the given content nonce.
306 > */
307 > isFileReviewed(uri: URI, nonce: string): Promise<boolean>;
308 >
309 > /**
310 > * Creates a safe, consistent copy of the database at the given path
311 > * using SQLite's `VACUUM INTO` command.
312 > */
313 > vacuumInto(targetPath: string): Promise<void>;
314 >
315 > /**
316 > * Resolves once all in-flight write operations on this database have
317 > * settled. Used by graceful shutdown to flush fire-and-forget writes
318 > * before the process exits.
319 > */
320 > whenIdle(): Promise<void>;
321 >
322 > /**
323 > * Close the database connection. After calling this method, the object is
324 > * considered disposed and all other methods will reject with an error.
325 > */
326 > close(): Promise<void>;
327 > }
328 >
329 > /**
330 > * Provides persistent, per-session data directories on disk.
331 > *
332 > * Each session gets a directory under `{userDataPath}/agentSessionData/{sessionId}/`
333 > * where internal agent-host code can store arbitrary files (e.g. file snapshots).
334 > *
335 > * Directories are created lazily — callers should use {@link IFileService.createFolder}
336 > * before writing files. Cleanup happens eagerly on session removal and via startup
337 > * garbage collection for orphaned directories.
338 > */
339 > export interface ISessionDataService {
340 > readonly _serviceBrand: undefined;
341 >
342 > /**
343 > * Returns the root data directory URI for a session.
344 > * Does **not** create the directory on disk; callers use
345 > * `IFileService.createFolder()` as needed.
346 > */
347 > getSessionDataDir(session: URI): URI;
348 >
349 > /**
350 > * Returns the root data directory URI for a session given its raw ID.
351 > * Equivalent to {@link getSessionDataDir} but without requiring a full URI.
352 > */
353 > getSessionDataDirById(sessionId: string): URI;
354 >
355 > /**
356 > * Opens (or creates) a per-session SQLite database. The database file is
357 > * stored at `{sessionDataDir}/session.db`. Migrations are applied
358 > * automatically on first use.
359 > *
360 > * Returns a ref-counted reference. Multiple callers for the same session
361 > * share the same underlying connection. The connection is closed when
362 > * the last reference is disposed.
363 > */
364 > openDatabase(session: URI): IReference<ISessionDatabase>;
365 >
366 > /**
367 > * Opens an existing per-session database **only if the database file
368 > * already exists on disk**. Returns `undefined` when no database has
369 > * been created yet, avoiding the side effect of materializing empty
370 > * database files during read-only operations like listing sessions.
371 > */
372 > tryOpenDatabase(session: URI): Promise<IReference<ISessionDatabase> | undefined>;
373 >
374 > /**
375 > * Recursively deletes the data directory for a session, if it exists.
376 > */
377 > deleteSessionData(session: URI): Promise<void>;
378 >
379 > /**
380 > * Fires immediately before a session's data directory (and the
381 > * SQLite database within it) is deleted by {@link deleteSessionData}.
382 > *
383 > * Subscribers can register asynchronous cleanup work via
384 > * {@link IWillDeleteSessionDataEvent.waitUntil}; the deletion is
385 > * blocked until all registered promises settle. Used by
386 > * `IAgentHostCheckpointService.disposeSessionData` to read the exact
387 > * list of checkpoint refs from the (still-readable) database and
388 > * delete them before the directory is removed.
389 > *
390 > * Subscribers must own their own error handling — exceptions
391 > * propagated out of `waitUntil` promises are logged and ignored;
392 > * deletion proceeds regardless.
393 > */
394 > readonly onWillDeleteSessionData: Event<IWillDeleteSessionDataEvent>;
395 >
396 > /**
397 > * Deletes data directories that do not correspond to any known session.
398 > * Called at startup; safe to call multiple times.
399 > */
400 > cleanupOrphanedData(knownSessionIds: Set<string>): Promise<void>;
401 >
402 > /**
403 > * Resolves once all in-flight write operations across every currently
404 > * open per-session database have settled. Intended for graceful
405 > * shutdown — fire-and-forget writes (e.g. metadata persistence) would
406 > * otherwise be lost when the process exits.
407 > */
408 > whenIdle(): Promise<void>;
409 > }
410 >
411 > /**
412 > * Payload of {@link ISessionDataService.onWillDeleteSessionData}.
413 > */
414 > export interface IWillDeleteSessionDataEvent {
415 > readonly session: URI;
416 > /**
417 > * Register an asynchronous task that must settle before the session's
418 > * data directory is removed.
419 > */
420 > waitUntil(promise: Promise<unknown>): void;
421 > }
src/vs/base/common/arrays.ts 410 covered LOC · 76 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- arrays.ts
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 > import { findFirstIdxMonotonousOrArrLen } from './arraysFind.js';
7 > import { CancellationToken } from './cancellation.js';
8 > import { CancellationError } from './errors.js';
9 > import { ISplice } from './sequence.js';
10 >
11 > /**
12 > * Returns the last entry and the initial N-1 entries of the array, as a tuple of [rest, last].
13 > *
14 > * The array must have at least one element.
15 > *
16 > * @param arr The input array
17 > * @returns A tuple of [rest, last] where rest is all but the last element and last is the last element
18 > * @throws Error if the array is empty
19 > */
20 > export function tail<T>(arr: T[]): [T[], T] {
21 if (arr.length === 0) {
22 throw new Error('Invalid tail call');
25 return [arr.slice(0, arr.length - 1), arr[arr.length - 1]];
26 }
27 > arrays.ts
28 > export function equals<T>(one: ReadonlyArray<T> | undefined, other: ReadonlyArray<T> | undefined, itemEquals: (a: T, b: T) => boolean = (a, b) => a === b): boolean {
29 if (one === other) {
30 return true;
47 return true;
48 }
49 > arrays.ts
50 > /**
51 > * Remove the element at `index` by replacing it with the last element. This is faster than `splice`
52 > * but changes the order of the array
53 > */
54 > export function removeFastWithoutKeepingOrder<T>(array: T[], index: number) {
55 const last = array.length - 1;
56 if (index < last) {
59 array.pop();
60 }
61 > arrays.ts
62 > /**
63 > * Performs a binary search algorithm over a sorted array.
64 > *
65 > * @param array The array being searched.
66 > * @param key The value we search for.
67 > * @param comparator A function that takes two array elements and returns zero
68 > * if they are equal, a negative number if the first element precedes the
69 > * second one in the sorting order, or a positive number if the second element
70 > * precedes the first one.
71 > * @return See {@link binarySearch2}
72 > */
73 > export function binarySearch<T>(array: ReadonlyArray<T>, key: T, comparator: (op1: T, op2: T) => number): number {
74 return binarySearch2(array.length, i => comparator(array[i], key));
75 }
76 > arrays.ts
77 > /**
78 > * Performs a binary search algorithm over a sorted collection. Useful for cases
79 > * when we need to perform a binary search over something that isn't actually an
80 > * array, and converting data to an array would defeat the use of binary search
81 > * in the first place.
82 > *
83 > * @param length The collection length.
84 > * @param compareToKey A function that takes an index of an element in the
85 > * collection and returns zero if the value at this index is equal to the
86 > * search key, a negative number if the value precedes the search key in the
87 > * sorting order, or a positive number if the search key precedes the value.
88 > * @return A non-negative index of an element, if found. If not found, the
89 > * result is -(n+1) (or ~n, using bitwise notation), where n is the index
90 > * where the key should be inserted to maintain the sorting order.
91 > */
92 > export function binarySearch2(length: number, compareToKey: (index: number) => number): number {
93 let low = 0,
94 high = length - 1;
107 return -(low + 1);
108 }
109 > arrays.ts
110 > type Compare<T> = (a: T, b: T) => number;
111 >
112 > /**
113 > * Finds the nth smallest element in the array using quickselect algorithm.
114 > * The data does not need to be sorted.
115 > *
116 > * @param nth The zero-based index of the element to find (0 = smallest, 1 = second smallest, etc.)
117 > * @param data The unsorted array
118 > * @param compare A comparator function that defines the sort order
119 > * @returns The nth smallest element
120 > * @throws TypeError if nth is >= data.length
121 > */
122 > export function quickSelect<T>(nth: number, data: T[], compare: Compare<T>): T {
123
124 nth = nth | 0;
152 }
153 }
154 > arrays.ts
155 > export function groupBy<T>(data: ReadonlyArray<T>, compare: (a: T, b: T) => number): T[][] {
156 const result: T[][] = [];
157 let currentGroup: T[] | undefined = undefined;
166 return result;
167 }
168 > arrays.ts
169 > /**
170 > * Splits the given items into a list of (non-empty) groups.
171 > * `shouldBeGrouped` is used to decide if two consecutive items should be in the same group.
172 > * The order of the items is preserved.
173 > */
174 > export function* groupAdjacentBy<T>(items: Iterable<T>, shouldBeGrouped: (item1: T, item2: T) => boolean): Iterable<T[]> {
175 let currentGroup: T[] | undefined;
176 let last: T | undefined;
190 }
191 }
192 > arrays.ts
193 > export function forEachAdjacent<T>(arr: T[], f: (item1: T | undefined, item2: T | undefined) => void): void {
194 for (let i = 0; i <= arr.length; i++) {
195 f(i === 0 ? undefined : arr[i - 1], i === arr.length ? undefined : arr[i]);
196 }
197 }
198 > arrays.ts
199 > export function forEachWithNeighbors<T>(arr: T[], f: (before: T | undefined, element: T, after: T | undefined) => void): void {
200 for (let i = 0; i < arr.length; i++) {
201 f(i === 0 ? undefined : arr[i - 1], arr[i], i + 1 === arr.length ? undefined : arr[i + 1]);
202 }
203 }
204 > arrays.ts
205 > export function concatArrays<T extends any[]>(...arrays: T): T[number][number][] {
206 return [].concat(...arrays);
207 }
208 > arrays.ts
209 > interface IMutableSplice<T> extends ISplice<T> {
210 > readonly toInsert: T[];
211 > deleteCount: number;
212 > }
213 >
214 > /**
215 > * Diffs two *sorted* arrays and computes the splices which apply the diff.
216 > */
217 > export function sortedDiff<T>(before: ReadonlyArray<T>, after: ReadonlyArray<T>, compare: (a: T, b: T) => number): ISplice<T>[] {
218 const result: IMutableSplice<T>[] = [];
219
266 return result;
267 }
268 > arrays.ts
269 > /**
270 > * Takes two *sorted* arrays and computes their delta (removed, added elements).
271 > * Finishes in `Math.min(before.length, after.length)` steps.
272 > */
273 > export function delta<T>(before: ReadonlyArray<T>, after: ReadonlyArray<T>, compare: (a: T, b: T) => number): { removed: T[]; added: T[] } {
274 const splices = sortedDiff(before, after, compare);
275 const removed: T[] = [];
283 return { removed, added };
284 }
285 > arrays.ts
286 > /**
287 > * Returns the top N elements from the array.
288 > *
289 > * Faster than sorting the entire array when the array is a lot larger than N.
290 > *
291 > * @param array The unsorted array.
292 > * @param compare A sort function for the elements.
293 > * @param n The number of elements to return.
294 > * @return The first n elements from array when sorted with compare.
295 > */
296 > export function top<T>(array: ReadonlyArray<T>, compare: (a: T, b: T) => number, n: number): T[] {
297 if (n === 0) {
298 return [];
302 return result;
303 }
304 > arrays.ts
305 > /**
306 > * Asynchronous variant of `top()` allowing for splitting up work in batches between which the event loop can run.
307 > *
308 > * Returns the top N elements from the array.
309 > *
310 > * Faster than sorting the entire array when the array is a lot larger than N.
311 > *
312 > * @param array The unsorted array.
313 > * @param compare A sort function for the elements.
314 > * @param n The number of elements to return.
315 > * @param batch The number of elements to examine before yielding to the event loop.
316 > * @return The first n elements from array when sorted with compare.
317 > */
318 > export function topAsync<T>(array: T[], compare: (a: T, b: T) => number, n: number, batch: number, token?: CancellationToken): Promise<T[]> {
319 if (n === 0) {
320 return Promise.resolve([]);
339 });
340 }
341 > arrays.ts
342 function topStep<T>(array: ReadonlyArray<T>, compare: (a: T, b: T) => number, result: T[], i: number, m: number): void {
343 for (const n = result.length; i < m; i++) {
350 }
351 }
352 > arrays.ts
353 > /**
354 > * @returns New array with all falsy values removed. The original array IS NOT modified.
355 > */
356 > export function coalesce<T>(array: ReadonlyArray<T | undefined | null>): T[] {
357 > return array.filter((e): e is T => !!e); arrays.ts
358 > }
359 > arrays.ts
360 > /**
361 > * Remove all falsy values from `array`. The original array IS modified.
362 > */
363 > export function coalesceInPlace<T>(array: Array<T | undefined | null>): asserts array is Array<T> {
364 let to = 0;
365 for (let i = 0; i < array.length; i++) {
371 array.length = to;
372 }
373 > arrays.ts
374 > /**
375 > * @deprecated Use `Array.copyWithin` instead
376 > */
377 > export function move(array: unknown[], from: number, to: number): void {
378 array.splice(to, 0, array.splice(from, 1)[0]);
379 }
380 > arrays.ts
381 > /**
382 > * @returns false if the provided object is an array and not empty.
383 > */
384 > export function isFalsyOrEmpty(obj: unknown): boolean {
385 return !Array.isArray(obj) || obj.length === 0;
386 }
387 > arrays.ts
388 > /**
389 > * @returns True if the provided object is an array and has at least one element.
390 > */
391 > export function isNonEmptyArray<T>(obj: T[] | undefined | null): obj is T[];
392 > export function isNonEmptyArray<T>(obj: readonly T[] | undefined | null): obj is readonly T[];
393 > export function isNonEmptyArray<T>(obj: T[] | readonly T[] | undefined | null): obj is T[] | readonly T[] {
394 return Array.isArray(obj) && obj.length > 0;
395 }
396 > arrays.ts
397 > /**
398 > * Removes duplicates from the given array. The optional keyFn allows to specify
399 > * how elements are checked for equality by returning an alternate value for each.
400 > */
401 > export function distinct<T>(array: ReadonlyArray<T>, keyFn: (value: T) => unknown = value => value): T[] {
402 const seen = new Set<any>();
403
411 });
412 }
413 > arrays.ts
414 > export function uniqueFilter<T, R>(keyFn: (t: T) => R): (t: T) => boolean {
415 const seen = new Set<R>();
416
426 };
427 }
428 > arrays.ts
429 > export function commonPrefixLength<T>(one: ReadonlyArray<T>, other: ReadonlyArray<T>, equals: (a: T, b: T) => boolean = (a, b) => a === b): number {
430 let result = 0;
431
436 return result;
437 }
438 > arrays.ts
439 > export function range(to: number): number[];
440 > export function range(from: number, to: number): number[];
441 > export function range(arg: number, to?: number): number[] {
442 let from = typeof to === 'number' ? arg : 0;
443
463 return result;
464 }
465 > arrays.ts
466 > export function index<T>(array: ReadonlyArray<T>, indexer: (t: T) => string): { [key: string]: T };
467 > export function index<T, R>(array: ReadonlyArray<T>, indexer: (t: T) => string, mapper: (t: T) => R): { [key: string]: R };
468 > export function index<T, R>(array: ReadonlyArray<T>, indexer: (t: T) => string, mapper?: (t: T) => R): { [key: string]: R } {
469 return array.reduce((r, t) => {
470 r[indexer(t)] = mapper ? mapper(t) : t;
472 }, Object.create(null));
473 }
474 > arrays.ts
475 > /**
476 > * Inserts an element into an array. Returns a function which, when
477 > * called, will remove that element from the array.
478 > *
479 > * @deprecated In almost all cases, use a `Set<T>` instead.
480 > */
481 > export function insert<T>(array: T[], element: T): () => void {
482 array.push(element);
483
484 return () => remove(array, element);
485 }
486 > arrays.ts
487 > /**
488 > * Removes an element from an array if it can be found.
489 > *
490 > * @deprecated In almost all cases, use a `Set<T>` instead.
491 > */
492 > export function remove<T>(array: T[], element: T): T | undefined {
493 const index = array.indexOf(element);
494 if (index > -1) {
500 return undefined;
501 }
502 > arrays.ts
503 > /**
504 > * Insert `insertArr` inside `target` at `insertIndex`.
505 > * Please don't touch unless you understand https://jsperf.com/inserting-an-array-within-an-array
506 > */
507 > export function arrayInsert<T>(target: T[], insertIndex: number, insertArr: T[]): T[] {
508 const before = target.slice(0, insertIndex);
509 const after = target.slice(insertIndex);
510 return before.concat(insertArr, after);
511 }
512 > arrays.ts
513 > /**
514 > * Uses Fisher-Yates shuffle to shuffle the given array
515 > */
516 > export function shuffle<T>(array: T[], _seed?: number): void {
517 let rand: () => number;
518
536 }
537 }
538 > arrays.ts
539 > /**
540 > * Pushes an element to the start of the array, if found.
541 > */
542 > export function pushToStart<T>(arr: T[], value: T): void {
543 const index = arr.indexOf(value);
544
548 }
549 }
550 > arrays.ts
551 > /**
552 > * Pushes an element to the end of the array, if found.
553 > */
554 > export function pushToEnd<T>(arr: T[], value: T): void {
555 const index = arr.indexOf(value);
556
560 }
561 }
562 > arrays.ts
563 > export function pushMany<T>(arr: T[], items: ReadonlyArray<T>): void {
564 for (const item of items) {
565 arr.push(item);
566 }
567 }
568 > arrays.ts
569 > export function mapArrayOrNot<T, U>(items: T | T[], fn: (_: T) => U): U | U[] {
570 return Array.isArray(items) ?
571 items.map(fn) :
572 fn(items);
573 }
574 > arrays.ts
575 > export function mapFilter<T, U>(array: ReadonlyArray<T>, fn: (t: T) => U | undefined): U[] {
576 const result: U[] = [];
577 for (const item of array) {
583 return result;
584 }
585 > arrays.ts
586 > export function withoutDuplicates<T>(array: ReadonlyArray<T>): T[] {
587 const s = new Set(array);
588 return Array.from(s);
589 }
590 > arrays.ts
591 > export function asArray<T>(x: T | T[]): T[];
592 > export function asArray<T>(x: T | readonly T[]): readonly T[];
593 > export function asArray<T>(x: T | T[]): T[] {
594 return Array.isArray(x) ? x : [x];
595 }
596 > arrays.ts
597 > export function getRandomElement<T>(arr: T[]): T | undefined {
598 return arr[Math.floor(Math.random() * arr.length)];
599 }
600 > arrays.ts
601 > /**
602 > * Insert the new items in the array.
603 > * @param array The original array.
604 > * @param start The zero-based location in the array from which to start inserting elements.
605 > * @param newItems The items to be inserted
606 > */
607 > export function insertInto<T>(array: T[], start: number, newItems: T[]): void {
608 const startIdx = getActualStartIndex(array, start);
609 const originalLength = array.length;
619 }
620 }
621 > arrays.ts
622 > /**
623 > * Removes elements from an array and inserts new elements in their place, returning the deleted elements. Alternative to the native Array.splice method, it
624 > * can only support limited number of items due to the maximum call stack size limit.
625 > * @param array The original array.
626 > * @param start The zero-based location in the array from which to start removing elements.
627 > * @param deleteCount The number of elements to remove.
628 > * @returns An array containing the elements that were deleted.
629 > */
630 > export function splice<T>(array: T[], start: number, deleteCount: number, newItems: T[]): T[] {
631 const index = getActualStartIndex(array, start);
632 let result = array.splice(index, deleteCount);
638 return result;
639 }
640 > arrays.ts
641 > /**
642 > * Determine the actual start index (same logic as the native splice() or slice())
643 > * If greater than the length of the array, start will be set to the length of the array. In this case, no element will be deleted but the method will behave as an adding function, adding as many element as item[n*] provided.
644 > * If negative, it will begin that many elements from the end of the array. (In this case, the origin -1, meaning -n is the index of the nth last element, and is therefore equivalent to the index of array.length - n.) If array.length + start is less than 0, it will begin from index 0.
645 > * @param array The target array.
646 > * @param start The operation index.
647 > */
648 function getActualStartIndex<T>(array: T[], start: number): number {
649 return start < 0 ? Math.max(start + array.length, 0) : Math.min(start, array.length);
650 }
651 > arrays.ts
652 >
653 >
654 > /**
655 > * When comparing two values,
656 > * a negative number indicates that the first value is less than the second,
657 > * a positive number indicates that the first value is greater than the second,
658 > * and zero indicates that neither is the case.
659 > */
660 > export type CompareResult = number;
661 >
662 > export namespace CompareResult {
663 > export function isLessThan(result: CompareResult): boolean {
664 return result < 0;
665 }
666 > arrays.ts
667 > export function isLessThanOrEqual(result: CompareResult): boolean {
668 return result <= 0;
669 }
670 > arrays.ts
671 > export function isGreaterThan(result: CompareResult): boolean {
672 return result > 0;
673 }
674 > arrays.ts
675 > export function isNeitherLessOrGreaterThan(result: CompareResult): boolean {
676 return result === 0;
677 }
678 > arrays.ts
679 > export const greaterThan = 1;
680 > export const lessThan = -1;
681 > export const neitherLessOrGreaterThan = 0;
682 > }
683 >
684 > /**
685 > * A comparator `c` defines a total order `<=` on `T` as following:
686 > * `c(a, b) <= 0` iff `a` <= `b`.
687 > * We also have `c(a, b) == 0` iff `c(b, a) == 0`.
688 > */
689 > export type Comparator<T> = (a: T, b: T) => CompareResult;
690 >
691 > export function compareBy<TItem, TCompareBy>(selector: (item: TItem) => TCompareBy, comparator: Comparator<TCompareBy>): Comparator<TItem> {
692 > return (a, b) => comparator(selector(a), selector(b)); arrays.ts
693 > }
694 > arrays.ts
695 > export function tieBreakComparators<TItem>(...comparators: Comparator<TItem>[]): Comparator<TItem> {
696 > return (item1, item2) => { arrays.ts
697 for (const comparator of comparators) {
698 const result = comparator(item1, item2);
703 return CompareResult.neitherLessOrGreaterThan;
704 };
705 > } arrays.ts
706 > arrays.ts
707 > /**
708 > * The natural order on numbers.
709 > */
710 > export const numberComparator: Comparator<number> = (a, b) => a - b;
711 >
712 > export const booleanComparator: Comparator<boolean> = (a, b) => numberComparator(a ? 1 : 0, b ? 1 : 0);
713 >
714 > export function reverseOrder<TItem>(comparator: Comparator<TItem>): Comparator<TItem> {
715 return (a, b) => -comparator(a, b);
716 }
717 > arrays.ts
718 > /**
719 > * Returns a new comparator that treats `undefined` as the smallest value.
720 > * All other values are compared using the given comparator.
721 > */
722 > export function compareUndefinedSmallest<T>(comparator: Comparator<T>): Comparator<T | undefined> {
723 return (a, b) => {
724 if (a === undefined) {
731 };
732 }
733 > arrays.ts
734 > export class ArrayQueue<T> {
735 > private readonly items: readonly T[];
736 > private firstIdx = 0;
737 > private lastIdx: number;
738 >
739 > /**
740 > * Constructs a queue that is backed by the given array. Runtime is O(1).
741 > */
742 > constructor(items: readonly T[]) {
743 this.items = items;
744 this.lastIdx = this.items.length - 1;
745 }
746 > arrays.ts
747 > get length(): number {
748 return this.lastIdx - this.firstIdx + 1;
749 }
750 > arrays.ts
751 > /**
752 > * Consumes elements from the beginning of the queue as long as the predicate returns true.
753 > * If no elements were consumed, `null` is returned. Has a runtime of O(result.length).
754 > */
755 > takeWhile(predicate: (value: T) => boolean): T[] | null {
756 // P(k) := k <= this.lastIdx && predicate(this.items[k])
757 // Find s := min { k | k >= this.firstIdx && !P(k) } and return this.data[this.firstIdx...s)
765 return result;
766 }
767 > arrays.ts
768 > /**
769 > * Consumes elements from the end of the queue as long as the predicate returns true.
770 > * If no elements were consumed, `null` is returned.
771 > * The result has the same order as the underlying array!
772 > */
773 > takeFromEndWhile(predicate: (value: T) => boolean): T[] | null {
774 // P(k) := this.firstIdx >= k && predicate(this.items[k])
775 // Find s := max { k | k <= this.lastIdx && !P(k) } and return this.data(s...this.lastIdx]
783 return result;
784 }
785 > arrays.ts
786 > peek(): T | undefined {
787 if (this.length === 0) {
788 return undefined;
790 return this.items[this.firstIdx];
791 }
792 > arrays.ts
793 > peekLast(): T | undefined {
794 if (this.length === 0) {
795 return undefined;
797 return this.items[this.lastIdx];
798 }
799 > arrays.ts
800 > dequeue(): T | undefined {
801 const result = this.items[this.firstIdx];
802 this.firstIdx++;
803 return result;
804 }
805 > arrays.ts
806 > removeLast(): T | undefined {
807 const result = this.items[this.lastIdx];
808 this.lastIdx--;
809 return result;
810 }
811 > arrays.ts
812 > takeCount(count: number): T[] {
813 const result = this.items.slice(this.firstIdx, this.firstIdx + count);
814 this.firstIdx += count;
815 return result;
816 }
817 > } arrays.ts
818 >
819 > /**
820 > * This class is faster than an iterator and array for lazy computed data.
821 > */
822 > export class CallbackIterable<T> {
823 > public static readonly empty = new CallbackIterable<never>(_callback => { });
824 >
825 > constructor(
826 > /**
827 > * Calls the callback for every item.
828 > * Stops when the callback returns false.
829 > */
830 > public readonly iterate: (callback: (item: T) => boolean) => void
831 > ) {
832 > }
833 >
834 > forEach(handler: (item: T) => void) {
835 this.iterate(item => { handler(item); return true; });
836 }
837 > arrays.ts
838 > toArray(): T[] {
839 const result: T[] = [];
840 this.iterate(item => { result.push(item); return true; });
841 return result;
842 }
843 > arrays.ts
844 > filter(predicate: (item: T) => boolean): CallbackIterable<T> {
845 return new CallbackIterable(cb => this.iterate(item => predicate(item) ? cb(item) : true));
846 }
847 > arrays.ts
848 > map<TResult>(mapFn: (item: T) => TResult): CallbackIterable<TResult> {
849 return new CallbackIterable<TResult>(cb => this.iterate(item => cb(mapFn(item))));
850 }
851 > arrays.ts
852 > some(predicate: (item: T) => boolean): boolean {
853 let result = false;
854 this.iterate(item => { result = predicate(item); return !result; });
855 return result;
856 }
857 > arrays.ts
858 > findFirst(predicate: (item: T) => boolean): T | undefined {
859 let result: T | undefined;
860 this.iterate(item => {
867 return result;
868 }
869 > arrays.ts
870 > findLast(predicate: (item: T) => boolean): T | undefined {
871 let result: T | undefined;
872 this.iterate(item => {
878 return result;
879 }
880 > arrays.ts
881 > findLastMaxBy(comparator: Comparator<T>): T | undefined {
882 let result: T | undefined;
883 let first = true;
891 return result;
892 }
893 > } arrays.ts
894 >
895 > /**
896 > * Represents a re-arrangement of items in an array.
897 > */
898 > export class Permutation {
899 > constructor(private readonly _indexMap: readonly number[]) { }
900 >
901 > /**
902 > * Returns a permutation that sorts the given array according to the given compare function.
903 > */
904 > public static createSortPermutation<T>(arr: readonly T[], compareFn: (a: T, b: T) => number): Permutation {
905 const sortIndices = Array.from(arr.keys()).sort((index1, index2) => compareFn(arr[index1], arr[index2]));
906 return new Permutation(sortIndices);
907 }
908 > arrays.ts
909 > /**
910 > * Returns a new array with the elements of the given array re-arranged according to this permutation.
911 > */
912 > apply<T>(arr: readonly T[]): T[] {
913 return arr.map((_, index) => arr[this._indexMap[index]]);
914 }
915 > arrays.ts
916 > /**
917 > * Returns a new permutation that undoes the re-arrangement of this permutation.
918 > */
919 > inverse(): Permutation {
920 const inverseIndexMap = this._indexMap.slice();
921 for (let i = 0; i < this._indexMap.length; i++) {
924 return new Permutation(inverseIndexMap);
925 }
926 > } arrays.ts
927 >
928 > /**
929 > * Asynchronous variant of `Array.find()`, returning the first element in
930 > * the array for which the predicate returns true.
931 > *
932 > * This implementation does not bail early and waits for all promises to
933 > * resolve before returning.
934 > */
935 export async function findAsync<T>(array: readonly T[], predicate: (element: T, index: number) => Promise<boolean>): Promise<T | undefined> {
936 const results = await Promise.all(array.map(
940 return results.find(r => r.ok)?.element;
941 }
942 > arrays.ts
943 > export function sum(array: readonly number[]): number {
944 return array.reduce((acc, value) => acc + value, 0);
945 }
946 > arrays.ts
947 > export function sumBy<T>(array: readonly T[], selector: (value: T) => number): number {
948 return array.reduce((acc, value) => acc + selector(value), 0);
949 }
src/vs/base/common/json.ts 399 covered LOC · 14 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- json.ts
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 > export const enum ScanError {
7 > None = 0,
8 > UnexpectedEndOfComment = 1,
9 > UnexpectedEndOfString = 2,
10 > UnexpectedEndOfNumber = 3,
11 > InvalidUnicode = 4,
12 > InvalidEscapeCharacter = 5,
13 > InvalidCharacter = 6
14 > }
15 >
16 > export const enum SyntaxKind {
17 > OpenBraceToken = 1,
18 > CloseBraceToken = 2,
19 > OpenBracketToken = 3,
20 > CloseBracketToken = 4,
21 > CommaToken = 5,
22 > ColonToken = 6,
23 > NullKeyword = 7,
24 > TrueKeyword = 8,
25 > FalseKeyword = 9,
26 > StringLiteral = 10,
27 > NumericLiteral = 11,
28 > LineCommentTrivia = 12,
29 > BlockCommentTrivia = 13,
30 > LineBreakTrivia = 14,
31 > Trivia = 15,
32 > Unknown = 16,
33 > EOF = 17
34 > }
35 >
36 > /**
37 > * The scanner object, representing a JSON scanner at a position in the input string.
38 > */
39 > export interface JSONScanner {
40 > /**
41 > * Sets the scan position to a new offset. A call to 'scan' is needed to get the first token.
42 > */
43 > setPosition(pos: number): void;
44 > /**
45 > * Read the next token. Returns the token code.
46 > */
47 > scan(): SyntaxKind;
48 > /**
49 > * Returns the current scan position, which is after the last read token.
50 > */
51 > getPosition(): number;
52 > /**
53 > * Returns the last read token.
54 > */
55 > getToken(): SyntaxKind;
56 > /**
57 > * Returns the last read token value. The value for strings is the decoded string content. For numbers its of type number, for boolean it's true or false.
58 > */
59 > getTokenValue(): string;
60 > /**
61 > * The start offset of the last read token.
62 > */
63 > getTokenOffset(): number;
64 > /**
65 > * The length of the last read token.
66 > */
67 > getTokenLength(): number;
68 > /**
69 > * An error code of the last scan.
70 > */
71 > getTokenError(): ScanError;
72 > }
73 >
74 >
75 >
76 > export interface ParseError {
77 > error: ParseErrorCode;
78 > offset: number;
79 > length: number;
80 > }
81 >
82 > export const enum ParseErrorCode {
83 > InvalidSymbol = 1,
84 > InvalidNumberFormat = 2,
85 > PropertyNameExpected = 3,
86 > ValueExpected = 4,
87 > ColonExpected = 5,
88 > CommaExpected = 6,
89 > CloseBraceExpected = 7,
90 > CloseBracketExpected = 8,
91 > EndOfFileExpected = 9,
92 > InvalidCommentToken = 10,
93 > UnexpectedEndOfComment = 11,
94 > UnexpectedEndOfString = 12,
95 > UnexpectedEndOfNumber = 13,
96 > InvalidUnicode = 14,
97 > InvalidEscapeCharacter = 15,
98 > InvalidCharacter = 16
99 > }
100 >
101 > export type NodeType = 'object' | 'array' | 'property' | 'string' | 'number' | 'boolean' | 'null';
102 >
103 > export interface Node {
104 > readonly type: NodeType;
105 > readonly value?: any;
106 > readonly offset: number;
107 > readonly length: number;
108 > readonly colonOffset?: number;
109 > readonly parent?: Node;
110 > readonly children?: Node[];
111 > }
112 >
113 > export type Segment = string | number;
114 > export type JSONPath = Segment[];
115 >
116 > export interface Location {
117 > /**
118 > * The previous property key or literal value (string, number, boolean or null) or undefined.
119 > */
120 > previousNode?: Node;
121 > /**
122 > * The path describing the location in the JSON document. The path consists of a sequence strings
123 > * representing an object property or numbers for array indices.
124 > */
125 > path: JSONPath;
126 > /**
127 > * Matches the locations path against a pattern consisting of strings (for properties) and numbers (for array indices).
128 > * '*' will match a single segment, of any property name or index.
129 > * '**' will match a sequence of segments or no segment, of any property name or index.
130 > */
131 > matches: (patterns: JSONPath) => boolean;
132 > /**
133 > * If set, the location's offset is at a property key.
134 > */
135 > isAtPropertyKey: boolean;
136 > }
137 >
138 > export interface ParseOptions {
139 > disallowComments?: boolean;
140 > allowTrailingComma?: boolean;
141 > allowEmptyContent?: boolean;
142 > }
143 >
144 > export namespace ParseOptions {
145 > export const DEFAULT = {
146 > allowTrailingComma: true
147 > };
148 > }
149 >
150 > export interface JSONVisitor {
151 > /**
152 > * Invoked when an open brace is encountered and an object is started. The offset and length represent the location of the open brace.
153 > */
154 > onObjectBegin?: (offset: number, length: number) => void;
155 >
156 > /**
157 > * Invoked when a property is encountered. The offset and length represent the location of the property name.
158 > */
159 > onObjectProperty?: (property: string, offset: number, length: number) => void;
160 >
161 > /**
162 > * Invoked when a closing brace is encountered and an object is completed. The offset and length represent the location of the closing brace.
163 > */
164 > onObjectEnd?: (offset: number, length: number) => void;
165 >
166 > /**
167 > * Invoked when an open bracket is encountered. The offset and length represent the location of the open bracket.
168 > */
169 > onArrayBegin?: (offset: number, length: number) => void;
170 >
171 > /**
172 > * Invoked when a closing bracket is encountered. The offset and length represent the location of the closing bracket.
173 > */
174 > onArrayEnd?: (offset: number, length: number) => void;
175 >
176 > /**
177 > * Invoked when a literal value is encountered. The offset and length represent the location of the literal value.
178 > */
179 > onLiteralValue?: (value: any, offset: number, length: number) => void;
180 >
181 > /**
182 > * Invoked when a comma or colon separator is encountered. The offset and length represent the location of the separator.
183 > */
184 > onSeparator?: (character: string, offset: number, length: number) => void;
185 >
186 > /**
187 > * When comments are allowed, invoked when a line or block comment is encountered. The offset and length represent the location of the comment.
188 > */
189 > onComment?: (offset: number, length: number) => void;
190 >
191 > /**
192 > * Invoked on an error.
193 > */
194 > onError?: (error: ParseErrorCode, offset: number, length: number) => void;
195 > }
196 >
197 > /**
198 > * Creates a JSON scanner on the given text.
199 > * If ignoreTrivia is set, whitespaces or comments are ignored.
200 > */
201 > export function createScanner(text: string, ignoreTrivia: boolean = false): JSONScanner {
202
203 let pos = 0;
558 };
559 }
560 > json.ts
561 function isWhitespace(ch: number): boolean {
562 return ch === CharacterCodes.space || ch === CharacterCodes.tab || ch === CharacterCodes.verticalTab || ch === CharacterCodes.formFeed ||
564 ch === CharacterCodes.narrowNoBreakSpace || ch === CharacterCodes.mathematicalSpace || ch === CharacterCodes.ideographicSpace || ch === CharacterCodes.byteOrderMark;
565 }
566 > json.ts
567 function isLineBreak(ch: number): boolean {
568 return ch === CharacterCodes.lineFeed || ch === CharacterCodes.carriageReturn || ch === CharacterCodes.lineSeparator || ch === CharacterCodes.paragraphSeparator;
569 }
570 > json.ts
571 function isDigit(ch: number): boolean {
572 return ch >= CharacterCodes._0 && ch <= CharacterCodes._9;
573 }
574 > json.ts
575 > const enum CharacterCodes {
576 > nullCharacter = 0,
577 > maxAsciiCharacter = 0x7F,
578 >
579 > lineFeed = 0x0A, // \n
580 > carriageReturn = 0x0D, // \r
581 > lineSeparator = 0x2028,
582 > paragraphSeparator = 0x2029,
583 >
584 > // REVIEW: do we need to support this? The scanner doesn't, but our IText does. This seems
585 > // like an odd disparity? (Or maybe it's completely fine for them to be different).
586 > nextLine = 0x0085,
587 >
588 > // Unicode 3.0 space characters
589 > space = 0x0020, // " "
590 > nonBreakingSpace = 0x00A0, //
591 > enQuad = 0x2000,
592 > emQuad = 0x2001,
593 > enSpace = 0x2002,
594 > emSpace = 0x2003,
595 > threePerEmSpace = 0x2004,
596 > fourPerEmSpace = 0x2005,
597 > sixPerEmSpace = 0x2006,
598 > figureSpace = 0x2007,
599 > punctuationSpace = 0x2008,
600 > thinSpace = 0x2009,
601 > hairSpace = 0x200A,
602 > zeroWidthSpace = 0x200B,
603 > narrowNoBreakSpace = 0x202F,
604 > ideographicSpace = 0x3000,
605 > mathematicalSpace = 0x205F,
606 > ogham = 0x1680,
607 >
608 > _ = 0x5F,
609 > $ = 0x24,
610 >
611 > _0 = 0x30,
612 > _1 = 0x31,
613 > _2 = 0x32,
614 > _3 = 0x33,
615 > _4 = 0x34,
616 > _5 = 0x35,
617 > _6 = 0x36,
618 > _7 = 0x37,
619 > _8 = 0x38,
620 > _9 = 0x39,
621 >
622 > a = 0x61,
623 > b = 0x62,
624 > c = 0x63,
625 > d = 0x64,
626 > e = 0x65,
627 > f = 0x66,
628 > g = 0x67,
629 > h = 0x68,
630 > i = 0x69,
631 > j = 0x6A,
632 > k = 0x6B,
633 > l = 0x6C,
634 > m = 0x6D,
635 > n = 0x6E,
636 > o = 0x6F,
637 > p = 0x70,
638 > q = 0x71,
639 > r = 0x72,
640 > s = 0x73,
641 > t = 0x74,
642 > u = 0x75,
643 > v = 0x76,
644 > w = 0x77,
645 > x = 0x78,
646 > y = 0x79,
647 > z = 0x7A,
648 >
649 > A = 0x41,
650 > B = 0x42,
651 > C = 0x43,
652 > D = 0x44,
653 > E = 0x45,
654 > F = 0x46,
655 > G = 0x47,
656 > H = 0x48,
657 > I = 0x49,
658 > J = 0x4A,
659 > K = 0x4B,
660 > L = 0x4C,
661 > M = 0x4D,
662 > N = 0x4E,
663 > O = 0x4F,
664 > P = 0x50,
665 > Q = 0x51,
666 > R = 0x52,
667 > S = 0x53,
668 > T = 0x54,
669 > U = 0x55,
670 > V = 0x56,
671 > W = 0x57,
672 > X = 0x58,
673 > Y = 0x59,
674 > Z = 0x5A,
675 >
676 > ampersand = 0x26, // &
677 > asterisk = 0x2A, // *
678 > at = 0x40, // @
679 > backslash = 0x5C, // \
680 > bar = 0x7C, // |
681 > caret = 0x5E, // ^
682 > closeBrace = 0x7D, // }
683 > closeBracket = 0x5D, // ]
684 > closeParen = 0x29, // )
685 > colon = 0x3A, // :
686 > comma = 0x2C, // ,
687 > dot = 0x2E, // .
688 > doubleQuote = 0x22, // "
689 > equals = 0x3D, // =
690 > exclamation = 0x21, // !
691 > greaterThan = 0x3E, // >
692 > lessThan = 0x3C, // <
693 > minus = 0x2D, // -
694 > openBrace = 0x7B, // {
695 > openBracket = 0x5B, // [
696 > openParen = 0x28, // (
697 > percent = 0x25, // %
698 > plus = 0x2B, // +
699 > question = 0x3F, // ?
700 > semicolon = 0x3B, // ;
701 > singleQuote = 0x27, // '
702 > slash = 0x2F, // /
703 > tilde = 0x7E, // ~
704 >
705 > backspace = 0x08, // \b
706 > formFeed = 0x0C, // \f
707 > byteOrderMark = 0xFEFF,
708 > tab = 0x09, // \t
709 > verticalTab = 0x0B, // \v
710 > }
711 >
712 > interface NodeImpl extends Node {
713 > type: NodeType;
714 > value?: any;
715 > offset: number;
716 > length: number;
717 > colonOffset?: number;
718 > parent?: NodeImpl;
719 > children?: NodeImpl[];
720 > }
721 >
722 > /**
723 > * For a given offset, evaluate the location in the JSON document. Each segment in the location path is either a property name or an array index.
724 > */
725 > export function getLocation(text: string, position: number): Location {
726 const segments: Segment[] = []; // strings or numbers
727 const earlyReturnException = new Object();
838 };
839 }
840 > json.ts
841 >
842 > /**
843 > * Parses the given text and returns the object the JSON content represents. On invalid input, the parser tries to be as fault tolerant as possible, but still return a result.
844 > * Therefore always check the errors list to find out if the input was valid.
845 > */
846 > export function parse(text: string, errors: ParseError[] = [], options: ParseOptions = ParseOptions.DEFAULT): any {
847 let currentProperty: string | null = null;
848 let currentParent: any = [];
889 return currentParent[0];
890 }
891 > json.ts
892 >
893 > /**
894 > * Parses the given text and returns a tree representation the JSON content. On invalid input, the parser tries to be as fault tolerant as possible, but still return a result.
895 > */
896 > export function parseTree(text: string, errors: ParseError[] = [], options: ParseOptions = ParseOptions.DEFAULT): Node {
897 let currentParent: NodeImpl = { type: 'array', offset: -1, length: -1, children: [], parent: undefined }; // artificial root
898
955 return result;
956 }
957 > json.ts
958 > /**
959 > * Finds the node at the given path in a JSON DOM.
960 > */
961 > export function findNodeAtLocation(root: Node, path: JSONPath): Node | undefined {
962 if (!root) {
963 return undefined;
990 return node;
991 }
992 > json.ts
993 > /**
994 > * Gets the JSON path of the given JSON DOM node
995 > */
996 > export function getNodePath(node: Node): JSONPath {
997 if (!node.parent || !node.parent.children) {
998 return [];
1010 return path;
1011 }
1012 > json.ts
1013 > /**
1014 > * Evaluates the JavaScript object of the given JSON DOM node
1015 > */
1016 > export function getNodeValue(node: Node): any {
1017 switch (node.type) {
1018 case 'array':
1038
1039 }
1040 > json.ts
1041 > export function contains(node: Node, offset: number, includeRightBound = false): boolean {
1042 return (offset >= node.offset && offset < (node.offset + node.length)) || includeRightBound && (offset === (node.offset + node.length));
1043 }
1044 > json.ts
1045 > /**
1046 > * Finds the most inner node at the given offset. If includeRightBound is set, also finds nodes that end at the given offset.
1047 > */
1048 > export function findNodeAtOffset(node: Node, offset: number, includeRightBound = false): Node | undefined {
1049 if (contains(node, offset, includeRightBound)) {
1050 const children = node.children;
1062 return undefined;
1063 }
1064 > json.ts
1065 >
1066 > /**
1067 > * Parses the given text and invokes the visitor functions for each object, array and literal reached.
1068 > */
1069 > export function visit(text: string, visitor: JSONVisitor, options: ParseOptions = ParseOptions.DEFAULT): any {
1070
1071 const _scanner = createScanner(text, false);
1308 return true;
1309 }
1310 > json.ts
1311 > export function getNodeType(value: unknown): NodeType {
1312 switch (typeof value) {
1313 case 'boolean': return 'boolean';
src/vs/platform/agentHost/common/state/protocol/action-origin.generated.ts 365 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- action-origin.generated.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > // Generated from types/actions.ts — do not edit
10 > // Run `npm run generate` to regenerate.
11 >
12 > 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';
13 >
14 >
15 > // ─── Root vs Session vs Chat vs Terminal vs Changeset Action Unions ─────────────────
16 >
17 > /** Union of all root-scoped actions. */
18 > export type RootAction =
19 > | RootAgentsChangedAction
20 > | RootActiveSessionsChangedAction
21 > | RootTerminalsChangedAction
22 > | RootConfigChangedAction
23 > ;
24 >
25 > /** Union of root actions that clients may dispatch. */
26 > export type ClientRootAction =
27 > | RootConfigChangedAction
28 > ;
29 >
30 > /** Union of root actions that only the server may produce. */
31 > export type ServerRootAction =
32 > | RootAgentsChangedAction
33 > | RootActiveSessionsChangedAction
34 > | RootTerminalsChangedAction
35 > ;
36 >
37 > /** Union of all session-scoped actions. */
38 > export type SessionAction =
39 > | SessionReadyAction
40 > | SessionCreationFailedAction
41 > | SessionChatAddedAction
42 > | SessionChatRemovedAction
43 > | SessionChatUpdatedAction
44 > | SessionDefaultChatChangedAction
45 > | SessionTitleChangedAction
46 > | SessionServerToolsChangedAction
47 > | SessionActiveClientSetAction
48 > | SessionActiveClientRemovedAction
49 > | SessionWorkingDirectorySetAction
50 > | SessionWorkingDirectoryRemovedAction
51 > | SessionInputNeededSetAction
52 > | SessionInputNeededRemovedAction
53 > | SessionCustomizationsChangedAction
54 > | SessionCustomizationToggledAction
55 > | SessionCustomizationUpdatedAction
56 > | SessionCustomizationRemovedAction
57 > | SessionMcpServerStateChangedAction
58 > | SessionMcpServerStartRequestedAction
59 > | SessionMcpServerStopRequestedAction
60 > | SessionIsReadChangedAction
61 > | SessionIsArchivedChangedAction
62 > | SessionActivityChangedAction
63 > | SessionChangesetsChangedAction
64 > | SessionConfigChangedAction
65 > | SessionMetaChangedAction
66 > ;
67 >
68 > /** Union of session actions that clients may dispatch. */
69 > export type ClientSessionAction =
70 > | SessionTitleChangedAction
71 > | SessionActiveClientSetAction
72 > | SessionActiveClientRemovedAction
73 > | SessionWorkingDirectorySetAction
74 > | SessionWorkingDirectoryRemovedAction
75 > | SessionCustomizationToggledAction
76 > | SessionMcpServerStartRequestedAction
77 > | SessionMcpServerStopRequestedAction
78 > | SessionIsReadChangedAction
79 > | SessionIsArchivedChangedAction
80 > | SessionConfigChangedAction
81 > ;
82 >
83 > /** Union of session actions that only the server may produce. */
84 > export type ServerSessionAction =
85 > | SessionReadyAction
86 > | SessionCreationFailedAction
87 > | SessionChatAddedAction
88 > | SessionChatRemovedAction
89 > | SessionChatUpdatedAction
90 > | SessionDefaultChatChangedAction
91 > | SessionServerToolsChangedAction
92 > | SessionInputNeededSetAction
93 > | SessionInputNeededRemovedAction
94 > | SessionCustomizationsChangedAction
95 > | SessionCustomizationUpdatedAction
96 > | SessionCustomizationRemovedAction
97 > | SessionMcpServerStateChangedAction
98 > | SessionActivityChangedAction
99 > | SessionChangesetsChangedAction
100 > | SessionMetaChangedAction
101 > ;
102 >
103 > /** Union of all chat-scoped actions. */
104 > export type ChatAction =
105 > | ChatTurnStartedAction
106 > | ChatDeltaAction
107 > | ChatResponsePartAction
108 > | ChatToolCallStartAction
109 > | ChatToolCallDeltaAction
110 > | ChatToolCallReadyAction
111 > | ChatToolCallConfirmedAction
112 > | ChatToolCallCompleteAction
113 > | ChatToolCallResultConfirmedAction
114 > | ChatToolCallContentChangedAction
115 > | ChatToolCallAuthRequiredAction
116 > | ChatToolCallAuthResolvedAction
117 > | ChatTurnCompleteAction
118 > | ChatTurnCancelledAction
119 > | ChatErrorAction
120 > | ChatActivityChangedAction
121 > | ChatWorkingDirectorySetAction
122 > | ChatWorkingDirectoryRemovedAction
123 > | ChatUsageAction
124 > | ChatReasoningAction
125 > | ChatPendingMessageSetAction
126 > | ChatPendingMessageRemovedAction
127 > | ChatQueuedMessagesReorderedAction
128 > | ChatDraftChangedAction
129 > | ChatInputRequestedAction
130 > | ChatInputAnswerChangedAction
131 > | ChatInputCompletedAction
132 > | ChatTruncatedAction
133 > | ChatTurnsLoadedAction
134 > ;
135 >
136 > /** Union of chat actions that clients may dispatch. */
137 > export type ClientChatAction =
138 > | ChatTurnStartedAction
139 > | ChatToolCallConfirmedAction
140 > | ChatToolCallCompleteAction
141 > | ChatToolCallResultConfirmedAction
142 > | ChatToolCallContentChangedAction
143 > | ChatTurnCancelledAction
144 > | ChatWorkingDirectorySetAction
145 > | ChatWorkingDirectoryRemovedAction
146 > | ChatPendingMessageSetAction
147 > | ChatPendingMessageRemovedAction
148 > | ChatQueuedMessagesReorderedAction
149 > | ChatDraftChangedAction
150 > | ChatInputAnswerChangedAction
151 > | ChatInputCompletedAction
152 > | ChatTruncatedAction
153 > ;
154 >
155 > /** Union of chat actions that only the server may produce. */
156 > export type ServerChatAction =
157 > | ChatDeltaAction
158 > | ChatResponsePartAction
159 > | ChatToolCallStartAction
160 > | ChatToolCallDeltaAction
161 > | ChatToolCallReadyAction
162 > | ChatToolCallAuthRequiredAction
163 > | ChatToolCallAuthResolvedAction
164 > | ChatTurnCompleteAction
165 > | ChatErrorAction
166 > | ChatActivityChangedAction
167 > | ChatUsageAction
168 > | ChatReasoningAction
169 > | ChatInputRequestedAction
170 > | ChatTurnsLoadedAction
171 > ;
172 >
173 > /** Union of all terminal-scoped actions. */
174 > export type TerminalAction =
175 > | TerminalDataAction
176 > | TerminalInputAction
177 > | TerminalResizedAction
178 > | TerminalClaimedAction
179 > | TerminalTitleChangedAction
180 > | TerminalCwdChangedAction
181 > | TerminalExitedAction
182 > | TerminalClearedAction
183 > | TerminalCommandDetectionAvailableAction
184 > | TerminalCommandExecutedAction
185 > | TerminalCommandFinishedAction
186 > ;
187 >
188 > /** Union of terminal actions that clients may dispatch. */
189 > export type ClientTerminalAction =
190 > | TerminalInputAction
191 > | TerminalResizedAction
192 > | TerminalClaimedAction
193 > | TerminalTitleChangedAction
194 > | TerminalClearedAction
195 > ;
196 >
197 > /** Union of terminal actions that only the server may produce. */
198 > export type ServerTerminalAction =
199 > | TerminalDataAction
200 > | TerminalCwdChangedAction
201 > | TerminalExitedAction
202 > | TerminalCommandDetectionAvailableAction
203 > | TerminalCommandExecutedAction
204 > | TerminalCommandFinishedAction
205 > ;
206 >
207 > /** Union of all changeset-scoped actions. */
208 > export type ChangesetAction =
209 > | ChangesetStatusChangedAction
210 > | ChangesetFileSetAction
211 > | ChangesetFileRemovedAction
212 > | ChangesetFilesReviewChangedAction
213 > | ChangesetContentChangedAction
214 > | ChangesetOperationsChangedAction
215 > | ChangesetOperationStatusChangedAction
216 > | ChangesetClearedAction
217 > ;
218 >
219 > /** Union of changeset actions that clients may dispatch. */
220 > export type ClientChangesetAction =
221 > | ChangesetFilesReviewChangedAction
222 > ;
223 >
224 > /** Union of changeset actions that only the server may produce. */
225 > export type ServerChangesetAction =
226 > | ChangesetStatusChangedAction
227 > | ChangesetFileSetAction
228 > | ChangesetFileRemovedAction
229 > | ChangesetContentChangedAction
230 > | ChangesetOperationsChangedAction
231 > | ChangesetOperationStatusChangedAction
232 > | ChangesetClearedAction
233 > ;
234 >
235 > /** Union of all annotations-scoped actions. */
236 > export type AnnotationsAction =
237 > | AnnotationsSetAction
238 > | AnnotationsUpdatedAction
239 > | AnnotationsRemovedAction
240 > | AnnotationsEntrySetAction
241 > | AnnotationsEntryRemovedAction
242 > ;
243 >
244 > /** Union of annotations actions that clients may dispatch. */
245 > export type ClientAnnotationsAction =
246 > | AnnotationsSetAction
247 > | AnnotationsUpdatedAction
248 > | AnnotationsRemovedAction
249 > | AnnotationsEntrySetAction
250 > | AnnotationsEntryRemovedAction
251 > ;
252 >
253 > /** Union of annotations actions that only the server may produce. */
254 > export type ServerAnnotationsAction =
255 > never
256 > ;
257 >
258 > /** Union of all resource-watch-scoped actions. */
259 > export type ResourceWatchAction =
260 > | ResourceWatchChangedAction
261 > ;
262 >
263 > /** Union of resource-watch actions that clients may dispatch. */
264 > export type ClientResourceWatchAction =
265 > never
266 > ;
267 >
268 > /** Union of resource-watch actions that only the server may produce. */
269 > export type ServerResourceWatchAction =
270 > | ResourceWatchChangedAction
271 > ;
272 >
273 > // ─── Client-Dispatchable Map ─────────────────────────────────────────────────
274 >
275 > /**
276 > * Exhaustive map indicating which action types may be dispatched by clients.
277 > * Adding a new action to StateAction without adding it here is a compile error.
278 > */
279 > export const IS_CLIENT_DISPATCHABLE: { readonly [K in StateAction['type']]: boolean } = {
280 > [ActionType.RootAgentsChanged]: false,
281 > [ActionType.RootActiveSessionsChanged]: false,
282 > [ActionType.RootTerminalsChanged]: false,
283 > [ActionType.RootConfigChanged]: true,
284 > [ActionType.SessionReady]: false,
285 > [ActionType.SessionCreationFailed]: false,
286 > [ActionType.SessionChatAdded]: false,
287 > [ActionType.SessionChatRemoved]: false,
288 > [ActionType.SessionChatUpdated]: false,
289 > [ActionType.SessionDefaultChatChanged]: false,
290 > [ActionType.SessionTitleChanged]: true,
291 > [ActionType.SessionServerToolsChanged]: false,
292 > [ActionType.SessionActiveClientSet]: true,
293 > [ActionType.SessionActiveClientRemoved]: true,
294 > [ActionType.SessionWorkingDirectorySet]: true,
295 > [ActionType.SessionWorkingDirectoryRemoved]: true,
296 > [ActionType.SessionInputNeededSet]: false,
297 > [ActionType.SessionInputNeededRemoved]: false,
298 > [ActionType.SessionCustomizationsChanged]: false,
299 > [ActionType.SessionCustomizationToggled]: true,
300 > [ActionType.SessionCustomizationUpdated]: false,
301 > [ActionType.SessionCustomizationRemoved]: false,
302 > [ActionType.SessionMcpServerStateChanged]: false,
303 > [ActionType.SessionMcpServerStartRequested]: true,
304 > [ActionType.SessionMcpServerStopRequested]: true,
305 > [ActionType.SessionIsReadChanged]: true,
306 > [ActionType.SessionIsArchivedChanged]: true,
307 > [ActionType.SessionActivityChanged]: false,
308 > [ActionType.SessionChangesetsChanged]: false,
309 > [ActionType.SessionConfigChanged]: true,
310 > [ActionType.SessionMetaChanged]: false,
311 > [ActionType.ChatTurnStarted]: true,
312 > [ActionType.ChatDelta]: false,
313 > [ActionType.ChatResponsePart]: false,
314 > [ActionType.ChatToolCallStart]: false,
315 > [ActionType.ChatToolCallDelta]: false,
316 > [ActionType.ChatToolCallReady]: false,
317 > [ActionType.ChatToolCallConfirmed]: true,
318 > [ActionType.ChatToolCallComplete]: true,
319 > [ActionType.ChatToolCallResultConfirmed]: true,
320 > [ActionType.ChatToolCallContentChanged]: true,
321 > [ActionType.ChatToolCallAuthRequired]: false,
322 > [ActionType.ChatToolCallAuthResolved]: false,
323 > [ActionType.ChatTurnComplete]: false,
324 > [ActionType.ChatTurnCancelled]: true,
325 > [ActionType.ChatError]: false,
326 > [ActionType.ChatActivityChanged]: false,
327 > [ActionType.ChatWorkingDirectorySet]: true,
328 > [ActionType.ChatWorkingDirectoryRemoved]: true,
329 > [ActionType.ChatUsage]: false,
330 > [ActionType.ChatReasoning]: false,
331 > [ActionType.ChatPendingMessageSet]: true,
332 > [ActionType.ChatPendingMessageRemoved]: true,
333 > [ActionType.ChatQueuedMessagesReordered]: true,
334 > [ActionType.ChatDraftChanged]: true,
335 > [ActionType.ChatInputRequested]: false,
336 > [ActionType.ChatInputAnswerChanged]: true,
337 > [ActionType.ChatInputCompleted]: true,
338 > [ActionType.ChatTruncated]: true,
339 > [ActionType.ChatTurnsLoaded]: false,
340 > [ActionType.ChangesetStatusChanged]: false,
341 > [ActionType.ChangesetFileSet]: false,
342 > [ActionType.ChangesetFileRemoved]: false,
343 > [ActionType.ChangesetFilesReviewChanged]: true,
344 > [ActionType.ChangesetContentChanged]: false,
345 > [ActionType.ChangesetOperationsChanged]: false,
346 > [ActionType.ChangesetOperationStatusChanged]: false,
347 > [ActionType.ChangesetCleared]: false,
348 > [ActionType.AnnotationsSet]: true,
349 > [ActionType.AnnotationsUpdated]: true,
350 > [ActionType.AnnotationsRemoved]: true,
351 > [ActionType.AnnotationsEntrySet]: true,
352 > [ActionType.AnnotationsEntryRemoved]: true,
353 > [ActionType.TerminalData]: false,
354 > [ActionType.TerminalInput]: true,
355 > [ActionType.TerminalResized]: true,
356 > [ActionType.TerminalClaimed]: true,
357 > [ActionType.TerminalTitleChanged]: true,
358 > [ActionType.TerminalCwdChanged]: false,
359 > [ActionType.TerminalExited]: false,
360 > [ActionType.TerminalCleared]: true,
361 > [ActionType.TerminalCommandDetectionAvailable]: false,
362 > [ActionType.TerminalCommandExecuted]: false,
363 > [ActionType.TerminalCommandFinished]: false,
364 > [ActionType.ResourceWatchChanged]: false,
365 > };
src/vs/base/common/ternarySearchTree.ts 364 covered LOC · 115 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- ternarySearchTree.ts
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 > import { shuffle } from './arrays.js';
7 > import { assert } from './assert.js';
8 > import { CharCode } from './charCode.js';
9 > import { compare, compareIgnoreCase, compareSubstring, compareSubstringIgnoreCase } from './strings.js';
10 > import { URI } from './uri.js';
11 >
12 > export interface IKeyIterator<K> {
13 > reset(key: K): this;
14 > next(): this;
15 >
16 > hasNext(): boolean;
17 > cmp(a: string): number;
18 > value(): string;
19 > }
20 >
21 > export class StringIterator implements IKeyIterator<string> {
22
23 private _value: string = '';
24 private _pos: number = 0;
26 > reset(key: string): this {
27 this._value = key;
28 this._pos = 0;
29 return this;
30 }
32 > next(): this {
33 this._pos += 1;
34 return this;
35 }
37 > hasNext(): boolean {
38 return this._pos < this._value.length - 1;
39 }
41 > cmp(a: string): number {
42 const aCode = a.charCodeAt(0);
43 const thisCode = this._value.charCodeAt(this._pos);
44 return aCode - thisCode;
45 }
47 > value(): string {
48 return this._value[this._pos];
49 }
51 >
52 > export class ConfigKeysIterator implements IKeyIterator<string> {
53 >
54 > private _value!: string;
55 > private _from!: number;
56 > private _to!: number;
57 >
58 > constructor(
59 private readonly _caseSensitive: boolean = true
60 ) { }
62 > reset(key: string): this {
63 this._value = key;
64 this._from = 0;
66 return this.next();
67 }
69 > hasNext(): boolean {
70 return this._to < this._value.length;
71 }
73 > next(): this {
74 // this._data = key.split(/[\\/]/).filter(s => !!s);
75 this._from = this._to;
89 return this;
90 }
92 > cmp(a: string): number {
93 return this._caseSensitive
94 ? compareSubstring(a, this._value, 0, a.length, this._from, this._to)
95 : compareSubstringIgnoreCase(a, this._value, 0, a.length, this._from, this._to);
96 }
98 > value(): string {
99 return this._value.substring(this._from, this._to);
100 }
102 >
103 > export class PathIterator implements IKeyIterator<string> {
104 >
105 > private _value!: string;
106 > private _valueLen!: number;
107 > private _from!: number;
108 > private _to!: number;
109 >
110 > constructor(
111 > private readonly _splitOnBackslash: boolean = true, ternarySearchTree.ts
112 > private readonly _caseSensitive: boolean = true
113 > ) { }
115 > reset(key: string): this {
116 > this._from = 0; ternarySearchTree.ts
117 > this._to = 0;
118 > this._value = key;
119 > this._valueLen = key.length;
120 > for (let pos = key.length - 1; pos >= 0; pos--, this._valueLen--) {
121 > const ch = this._value.charCodeAt(pos);
122 > if (!(ch === CharCode.Slash || this._splitOnBackslash && ch === CharCode.Backslash)) {
123 > break;
124 > }
125 > }
126 >
127 > return this.next();
128 > }
130 > hasNext(): boolean {
131 > return this._to < this._valueLen; ternarySearchTree.ts
132 > }
134 > next(): this {
135 > // this._data = key.split(/[\\/]/).filter(s => !!s); ternarySearchTree.ts
136 > this._from = this._to;
137 > let justSeps = true;
138 > for (; this._to < this._valueLen; this._to++) {
139 > const ch = this._value.charCodeAt(this._to);
140 > if (ch === CharCode.Slash || this._splitOnBackslash && ch === CharCode.Backslash) {
141 > if (justSeps) { ternarySearchTree.ts
142 > this._from++;
143 > } else {
144 break;
145 }
146 > } else { ternarySearchTree.ts
147 > justSeps = false;
148 > }
149 > }
150 > return this;
151 > }
153 > cmp(a: string): number {
154 > return this._caseSensitive ternarySearchTree.ts
155 > ? compareSubstring(a, this._value, 0, a.length, this._from, this._to) ternarySearchTree.ts
156 : compareSubstringIgnoreCase(a, this._value, 0, a.length, this._from, this._to);
159 > value(): string {
160 > return this._value.substring(this._from, this._to); ternarySearchTree.ts
161 > }
163 >
164 > const enum UriIteratorState {
165 > Scheme = 1, Authority = 2, Path = 3, Query = 4, Fragment = 5
166 > }
167 >
168 > export class UriIterator implements IKeyIterator<URI> {
169 >
170 > private _pathIterator!: PathIterator;
171 > private _value!: URI;
172 > private _states: UriIteratorState[] = [];
173 > private _stateIdx: number = 0;
174 >
175 > constructor(
176 > private readonly _ignorePathCasing: (uri: URI) => boolean, ternarySearchTree.ts
177 > private readonly _ignoreQueryAndFragment: (uri: URI) => boolean) { }
179 > reset(key: URI): this {
180 > this._value = key; ternarySearchTree.ts
181 > this._states = [];
182 > if (this._value.scheme) {
183 > this._states.push(UriIteratorState.Scheme);
184 > }
185 > if (this._value.authority) {
186 this._states.push(UriIteratorState.Authority);
187 }
188 > if (this._value.path) { ternarySearchTree.ts
189 > this._pathIterator = new PathIterator(false, !this._ignorePathCasing(key));
190 > this._pathIterator.reset(key.path);
191 > if (this._pathIterator.value()) {
192 > this._states.push(UriIteratorState.Path);
193 > }
194 > }
195 > if (!this._ignoreQueryAndFragment(key)) {
196 > if (this._value.query) { ternarySearchTree.ts
197 this._states.push(UriIteratorState.Query);
198 }
199 > if (this._value.fragment) { ternarySearchTree.ts
200 this._states.push(UriIteratorState.Fragment);
201 }
203 > this._stateIdx = 0; ternarySearchTree.ts
204 > return this;
205 > }
207 > next(): this {
208 > if (this._states[this._stateIdx] === UriIteratorState.Path && this._pathIterator.hasNext()) { ternarySearchTree.ts
209 this._pathIterator.next();
210 > } else { ternarySearchTree.ts
211 > this._stateIdx += 1;
212 > }
213 > return this;
214 > }
216 > hasNext(): boolean {
217 > return (this._states[this._stateIdx] === UriIteratorState.Path && this._pathIterator.hasNext()) ternarySearchTree.ts
218 > || this._stateIdx < this._states.length - 1;
219 > }
221 > cmp(a: string): number {
222 > if (this._states[this._stateIdx] === UriIteratorState.Scheme) { ternarySearchTree.ts
223 > return compareIgnoreCase(a, this._value.scheme);
224 > } else if (this._states[this._stateIdx] === UriIteratorState.Authority) {
225 return compareIgnoreCase(a, this._value.authority);
226 > } else if (this._states[this._stateIdx] === UriIteratorState.Path) { ternarySearchTree.ts
227 > return this._pathIterator.cmp(a); ternarySearchTree.ts
228 > } else if (this._states[this._stateIdx] === UriIteratorState.Query) { ternarySearchTree.ts
229 return compare(a, this._value.query);
230 } else if (this._states[this._stateIdx] === UriIteratorState.Fragment) {
232 }
233 throw new Error();
236 > value(): string {
237 > if (this._states[this._stateIdx] === UriIteratorState.Scheme) { ternarySearchTree.ts
238 > return this._value.scheme;
239 > } else if (this._states[this._stateIdx] === UriIteratorState.Authority) {
240 return this._value.authority;
241 > } else if (this._states[this._stateIdx] === UriIteratorState.Path) { ternarySearchTree.ts
242 > return this._pathIterator.value();
243 > } else if (this._states[this._stateIdx] === UriIteratorState.Query) {
244 return this._value.query;
245 } else if (this._states[this._stateIdx] === UriIteratorState.Fragment) {
247 }
248 throw new Error();
251 >
252 > abstract class Undef {
253 >
254 > static readonly Val: unique symbol = Symbol('undefined_placeholder');
255 >
256 > static wrap<V>(value: V | undefined): V | typeof Undef.Val {
257 > return value === undefined ? Undef.Val : value; ternarySearchTree.ts
258 > }
260 > static unwrap<V>(value: V | typeof Undef.Val): V | undefined {
261 > return value === Undef.Val ? undefined : value; ternarySearchTree.ts
262 > }
264 >
265 > class TernarySearchTreeNode<K, V> { ternarySearchTree.ts
266 > height: number = 1;
267 > segment!: string;
268 > value: V | typeof Undef.Val | undefined = undefined;
269 > key: K | undefined = undefined;
270 > left: TernarySearchTreeNode<K, V> | undefined = undefined;
271 > mid: TernarySearchTreeNode<K, V> | undefined = undefined;
272 > right: TernarySearchTreeNode<K, V> | undefined = undefined;
274 > isEmpty(): boolean {
275 return !this.left && !this.mid && !this.right && this.value === undefined;
276 }
278 > rotateLeft() {
279 const tmp = this.right!;
280 this.right = tmp.left;
284 return tmp;
285 }
287 > rotateRight() {
288 const tmp = this.left!;
289 this.left = tmp.right;
293 return tmp;
294 }
296 > updateHeight() {
297 > this.height = 1 + Math.max(this.heightLeft, this.heightRight); ternarySearchTree.ts
298 > }
300 > balanceFactor() {
301 > return this.heightRight - this.heightLeft; ternarySearchTree.ts
302 > }
304 > get heightLeft() {
305 > return this.left?.height ?? 0; ternarySearchTree.ts
306 > }
308 > get heightRight() {
309 > return this.right?.height ?? 0; ternarySearchTree.ts
310 > }
312 >
313 > const enum Dir {
314 > Left = -1,
315 > Mid = 0,
316 > Right = 1
317 > }
318 >
319 > export class TernarySearchTree<K, V> {
320 >
321 > static forUris<E>(ignorePathCasing: (key: URI) => boolean = () => false, ignoreQueryAndFragment: (key: URI) => boolean = () => false): TernarySearchTree<URI, E> {
322 > return new TernarySearchTree<URI, E>(new UriIterator(ignorePathCasing, ignoreQueryAndFragment)); ternarySearchTree.ts
323 > }
325 > static forPaths<E>(ignorePathCasing = false): TernarySearchTree<string, E> {
326 return new TernarySearchTree<string, E>(new PathIterator(undefined, !ignorePathCasing));
327 }
329 > static forStrings<E>(): TernarySearchTree<string, E> {
330 return new TernarySearchTree<string, E>(new StringIterator());
331 }
333 > static forConfigKeys<E>(): TernarySearchTree<string, E> {
334 return new TernarySearchTree<string, E>(new ConfigKeysIterator());
335 }
337 > private _iter: IKeyIterator<K>;
338 > private _root: TernarySearchTreeNode<K, V> | undefined;
339 >
340 > constructor(segments: IKeyIterator<K>) {
341 > this._iter = segments; ternarySearchTree.ts
342 > }
344 > clear(): void {
345 this._root = undefined;
346 }
348 > /**
349 > * Fill the tree with the same value of the given keys
350 > */
351 > fill(element: V, keys: readonly K[]): void;
352 > /**
353 > * Fill the tree with given [key,value]-tuples
354 > */
355 > fill(values: readonly [K, V][]): void;
356 > fill(values: readonly [K, V][] | V, keys?: readonly K[]): void {
357 if (keys) {
358 const arr = keys.slice(0);
369 }
370 }
372 > set(key: K, element: V): V | undefined {
373 > const iter = this._iter.reset(key); ternarySearchTree.ts
374 > let node: TernarySearchTreeNode<K, V>;
375 >
376 > if (!this._root) {
377 > this._root = new TernarySearchTreeNode<K, V>();
378 > this._root.segment = iter.value();
379 > }
380 > const stack: [Dir, TernarySearchTreeNode<K, V>][] = [];
381 >
382 > // find insert_node
383 > node = this._root;
384 > while (true) {
385 > const val = iter.cmp(node.segment);
386 > if (val > 0) {
387 // left
388 if (!node.left) {
393 node = node.left;
394
395 > } else if (val < 0) { ternarySearchTree.ts
396 // right
397 if (!node.right) {
402 node = node.right;
403
404 > } else if (iter.hasNext()) { ternarySearchTree.ts
405 > // mid ternarySearchTree.ts
406 > iter.next();
407 > if (!node.mid) {
408 > node.mid = new TernarySearchTreeNode<K, V>();
409 > node.mid.segment = iter.value();
410 > }
411 > stack.push([Dir.Mid, node]);
412 > node = node.mid;
413 > } else { ternarySearchTree.ts
414 > break;
415 > }
416 > }
417 >
418 > // set value
419 > const oldElement = Undef.unwrap(node.value);
420 > node.value = Undef.wrap(element);
421 > node.key = key;
422 >
423 > // balance
424 > for (let i = stack.length - 1; i >= 0; i--) {
425 > const node = stack[i][1]; ternarySearchTree.ts
426 >
427 > node.updateHeight();
428 > const bf = node.balanceFactor();
429 >
430 > if (bf < -1 || bf > 1) {
431 // needs rotate
432 const d1 = stack[i][0];
472 }
473 }
476 > return oldElement;
477 > }
479 > get(key: K): V | undefined {
480 > return Undef.unwrap(this._getNode(key)?.value); ternarySearchTree.ts
481 > }
483 > private _getNode(key: K) {
484 > const iter = this._iter.reset(key); ternarySearchTree.ts
485 > let node = this._root;
486 > while (node) {
487 > const val = iter.cmp(node.segment); ternarySearchTree.ts
488 > if (val > 0) {
489 // left
490 node = node.left;
491 > } else if (val < 0) { ternarySearchTree.ts
492 // right
493 node = node.right;
494 > } else if (iter.hasNext()) { ternarySearchTree.ts
495 > // mid ternarySearchTree.ts
496 > iter.next();
497 > node = node.mid;
498 > } else { ternarySearchTree.ts
499 > break; ternarySearchTree.ts
500 > }
502 > return node; ternarySearchTree.ts
503 > }
505 > has(key: K): boolean {
506 const node = this._getNode(key);
507 return !(node?.value === undefined && node?.mid === undefined);
508 }
510 > delete(key: K): void {
511 return this._delete(key, false);
512 }
514 > deleteSuperstr(key: K): void {
515 return this._delete(key, true);
516 }
518 > private _delete(key: K, superStr: boolean): void {
519 const iter = this._iter.reset(key);
520 const stack: [Dir, TernarySearchTreeNode<K, V>][] = [];
620 this._root = this._balanceByStack(stack) ?? this._root;
621 }
623 > private _min(node: TernarySearchTreeNode<K, V>, stack: [Dir, TernarySearchTreeNode<K, V>][]): TernarySearchTreeNode<K, V> {
624 while (node.left) {
625 stack.push([Dir.Left, node]);
628 return node;
629 }
631 > private _balanceByStack(stack: [Dir, TernarySearchTreeNode<K, V>][]) {
632
633 for (let i = stack.length - 1; i >= 0; i--) {
679 return undefined;
680 }
682 > findSubstr(key: K): V | undefined {
683 const iter = this._iter.reset(key);
684 let node = this._root;
703 return node && Undef.unwrap(node.value) || candidate;
704 }
706 > findSuperstr(key: K): IterableIterator<[K, V]> | undefined {
707 return this._findSuperstrOrElement(key, false);
708 }
710 > private _findSuperstrOrElement(key: K, allowValue: true): IterableIterator<[K, V]> | V | undefined;
711 > private _findSuperstrOrElement(key: K, allowValue: false): IterableIterator<[K, V]> | undefined;
712 > private _findSuperstrOrElement(key: K, allowValue: boolean): IterableIterator<[K, V]> | V | undefined {
713 const iter = this._iter.reset(key);
714 let node = this._root;
740 return undefined;
741 }
743 > hasElementOrSubtree(key: K): boolean {
744 return this._findSuperstrOrElement(key, true) !== undefined;
745 }
747 > forEach(callback: (value: V, index: K) => unknown): void {
748 for (const [key, value] of this) {
749 callback(value, key);
750 }
751 }
753 > *[Symbol.iterator](): IterableIterator<[K, V]> {
754 yield* this._entries(this._root);
755 }
757 > private _entries(node: TernarySearchTreeNode<K, V> | undefined): IterableIterator<[K, V]> {
758 const result: [K, V][] = [];
759 this._dfsEntries(node, result);
760 return result[Symbol.iterator]();
761 }
763 > private _dfsEntries(node: TernarySearchTreeNode<K, V> | undefined, bucket: [K, V][]) {
764 // DFS
765 if (!node) {
779 }
780 }
782 > // for debug/testing
783 > _isBalanced(): boolean {
784 const nodeIsBalanced = (node: TernarySearchTreeNode<unknown, unknown> | undefined): boolean => {
785 if (!node) {
src/vs/platform/agentHost/test/node/mockAgent.ts 364 covered LOC · 97 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- mockAgent.ts
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 > import { timeout } from '../../../../base/common/async.js';
7 > import { Emitter } from '../../../../base/common/event.js';
8 > import { observableValue } from '../../../../base/common/observable.js';
9 > import type { IAuthorizationProtectedResourceMetadata } from '../../../../base/common/oauth.js';
10 > import { URI } from '../../../../base/common/uri.js';
11 > import { type ISyncedCustomization } from '../../common/agentPluginManager.js';
12 > import { AgentSession, type AgentProvider, type AgentSignal, type IActiveClient, type IAgent, type IAgentActionSignal, type IAgentChats, type IAgentCreateChatForkSource, type IAgentCreateChatOptions, type IAgentCreateChatResult, type IAgentCreateSessionConfig, type IAgentCreateSessionResult, type IAgentDescriptor, type IAgentModelInfo, type IAgentResolveSessionConfigParams, type IAgentSessionConfigCompletionsParams, type IAgentSessionMetadata, type IAgentToolPendingConfirmationSignal } from '../../common/agentService.js';
13 > import { buildSubagentTurnsFromHistory, buildTurnsFromHistory, type IHistoryRecord } from './historyRecordFixtures.js';
14 > import { ProtectedResourceMetadata, ToolCallContributorKind, type AgentSelection, type MessageAttachment, type ModelSelection, type ToolDefinition } from '../../common/state/protocol/state.js';
15 > import type { ResolveSessionConfigResult, SessionConfigCompletionsResult } from '../../common/state/protocol/commands.js';
16 > import { ActionType } from '../../common/state/sessionActions.js';
17 > import { ResponsePartKind, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, CustomizationLoadStatus, buildDefaultChatUri, isAhpChatChannel, parseChatUri, parseSubagentSessionUri, type ClientPluginCustomization, type Customization, type PendingMessage, type StringOrMarkdown, type ToolCallResult, type Turn, type UsageInfo } from '../../common/state/sessionState.js';
18 > import { hasKey } from '../../../../base/common/types.js';
19 >
20 > /** Well-known auto-generated title used by the 'with-title' prompt. */
21 > export const MOCK_AUTO_TITLE = 'Automatically generated title';
22 >
23 function uriKey(session: URI): string {
24 // Build a stable key from raw URI fields without invoking `toString()`,
28 return `${session.scheme}://${session.authority}${session.path}${session.query ? '?' + session.query : ''}${session.fragment ? '#' + session.fragment : ''}`;
29 }
31 function mockProject(provider: AgentProvider) {
32 return { uri: URI.from({ scheme: 'mock-project', path: `/${provider}` }), displayName: `Agent ${provider}` };
33 }
35 > interface IMockSendMessageCall {
36 > readonly session: URI;
37 > readonly prompt: string;
38 > readonly attachments?: readonly MessageAttachment[];
39 > readonly chat?: URI;
40 > readonly senderClientId?: string;
41 > }
42 >
43 > /**
44 > * General-purpose mock agent for unit tests. Tracks all method calls
45 > * for assertion and exposes {@link fireProgress} to inject progress events.
46 > */
47 > export class MockAgent implements IAgent {
48 > private readonly _onDidSessionProgress = new Emitter<AgentSignal>();
49 > readonly onDidSessionProgress = this._onDidSessionProgress.event;
50 > private readonly _onDidSendMessage = new Emitter<IMockSendMessageCall>();
51 > readonly onDidSendMessage = this._onDidSendMessage.event;
52 > private readonly _models = observableValue<readonly IAgentModelInfo[]>(this, []);
53 > readonly models = this._models;
54 >
55 > private readonly _sessions = new Map<string, URI>();
56 > private _nextId = 1;
57 > /** Active turn IDs per session, captured from sendMessage(). */
58 > private readonly _activeTurnIds = new Map<string, string>();
59 >
60 >
61 > readonly sendMessageCalls: IMockSendMessageCall[] = [];
62 > readonly setPendingMessagesCalls: { chat: URI; steeringMessage: PendingMessage | undefined; queuedMessages: readonly PendingMessage[] }[] = [];
63 > readonly disposeSessionCalls: URI[] = [];
64 > readonly releaseSessionCalls: URI[] = [];
65 > readonly abortSessionCalls: URI[] = [];
66 > readonly respondToPermissionCalls: { requestId: string; approved: boolean }[] = [];
67 > readonly changeModelCalls: { session: URI; model: ModelSelection; chat?: URI }[] = [];
68 > readonly changeAgentCalls: { session: URI; agent: AgentSelection | undefined; chat?: URI }[] = [];
69 > readonly authenticateCalls: { resource: string; token: string }[] = [];
70 > readonly setClientCustomizationsCalls: { clientId: string; customizations: ClientPluginCustomization[] }[] = [];
71 > readonly setClientToolsCalls: { clientId: string; tools: readonly ToolDefinition[] }[] = [];
72 > readonly removeActiveClientCalls: { clientId: string }[] = [];
73 > readonly clientToolCallCompleteCalls: { session: URI; chat: URI; toolCallId: string; result: ToolCallResult }[] = [];
74 > readonly truncateSessionCalls: { session: URI; turnId: string | undefined; chat: URI | undefined }[] = [];
75 > /** Configurable return value for getCustomizations. */
76 > customizations: Customization[] = [];
77 > private readonly _onDidCustomizationsChange = new Emitter<void>();
78 > readonly onDidCustomizationsChange = this._onDidCustomizationsChange.event;
79 > getSessionCustomizations?: (session: URI) => Promise<readonly Customization[]>;
80 >
81 > /**
82 > * Configurable session history. Tests construct {@link IHistoryRecord}
83 > * entries (the agent-internal intermediate shape) and the mock converts
84 > * them to {@link Turn}s on demand. Subagent URIs are routed to filtered
85 > * subagent turns via {@link buildSubagentTurnsFromHistory}.
86 > */
87 > sessionMessages: IHistoryRecord[] = [];
88 >
89 > /** Optional overrides applied to session metadata from listSessions. */
90 > sessionMetadataOverrides: Partial<Omit<IAgentSessionMetadata, 'session'>> = {};
91 >
92 > constructor(readonly id: AgentProvider = 'mock') { }
94 > getDescriptor(): IAgentDescriptor {
95 > return { provider: this.id, displayName: `Agent ${this.id}`, description: `Test ${this.id} agent` }; mockAgent.ts
96 > }
98 > getProtectedResources(): ProtectedResourceMetadata[] {
99 if (this.id === 'copilot') {
100 return [{ resource: 'https://api.github.com', authorization_servers: ['https://github.com/login/oauth'], required: true }];
102 return [];
103 }
104 > mockAgent.ts
105 > setModels(models: readonly IAgentModelInfo[]): void {
106 this._models.set(models, undefined);
107 }
108 > mockAgent.ts
109 > async listSessions(): Promise<IAgentSessionMetadata[]> {
110 return [...this._sessions.values()].map(s => ({ session: s, startTime: Date.now(), modifiedTime: Date.now(), project: mockProject(this.id), ...this.sessionMetadataOverrides }));
111 }
112 > mockAgent.ts
113 > async getSessionMetadata(session: URI): Promise<IAgentSessionMetadata | undefined> {
114 if (!this._sessions.has(AgentSession.id(session))) {
115 return undefined;
117 return { session, startTime: Date.now(), modifiedTime: Date.now(), project: mockProject(this.id), ...this.sessionMetadataOverrides };
118 }
119 > mockAgent.ts
120 > /** Optional override for the working directory returned by createSession. */
121 > resolvedWorkingDirectory: URI | undefined;
122 >
123 > /**
124 > * When set, {@link sendMessage} rejects with this error after recording the
125 > * call — used to simulate a failed first-turn materialization (e.g. worktree
126 > * or branch setup throwing).
127 > */
128 > sendMessageError: Error | undefined;
129 > async createSession(config?: IAgentCreateSessionConfig): Promise<IAgentCreateSessionResult> {
130 const session = config?.session ?? AgentSession.uri(this.id, `${this.id}-session-${this._nextId++}`);
131 const rawId = AgentSession.id(session);
133 return { session, project: mockProject(this.id), workingDirectory: this.resolvedWorkingDirectory };
134 }
135 > mockAgent.ts
136 > async resolveSessionConfig(params: IAgentResolveSessionConfigParams): Promise<ResolveSessionConfigResult> {
137 return { schema: { type: 'object', properties: {} }, values: params.config ?? {} };
138 }
139 > mockAgent.ts
140 > async sessionConfigCompletions(_params: IAgentSessionConfigCompletionsParams): Promise<SessionConfigCompletionsResult> {
141 return { items: [] };
142 }
143 > mockAgent.ts
144 > async sendMessage(session: URI, chat: URI, prompt: string, attachments?: readonly MessageAttachment[], turnId?: string, senderClientId?: string): Promise<void> {
145 const call = { session, prompt, attachments, chat, ...(senderClientId ? { senderClientId } : {}) };
146 this.sendMessageCalls.push(call);
153 }
154 }
155 > mockAgent.ts
156 > setPendingMessages(chat: URI, steeringMessage: PendingMessage | undefined, queuedMessages: readonly PendingMessage[]): void {
157 this.setPendingMessagesCalls.push({ chat, steeringMessage, queuedMessages });
158 }
159 > mockAgent.ts
160 > readonly onSessionConfigChangedCalls: { session: URI; values: Record<string, unknown> }[] = [];
161 > onSessionConfigChanged(session: URI, values: Record<string, unknown>): void {
162 this.onSessionConfigChangedCalls.push({ session, values });
163 }
164 > mockAgent.ts
165 > async getSessionMessages(session: URI): Promise<readonly Turn[]> {
166 const subagentInfo = parseSubagentSessionUri(session);
167 if (subagentInfo) {
170 return buildTurnsFromHistory(this.sessionMessages);
171 }
172 > mockAgent.ts
173 > async disposeSession(session: URI): Promise<void> {
174 this.disposeSessionCalls.push(session);
175 this._sessions.delete(AgentSession.id(session));
176 }
177 > mockAgent.ts
178 > async releaseSession(session: URI): Promise<void> {
179 // Non-destructive: record the call but keep the session in the catalog
180 // so a later restore/resume still finds its durable data.
181 this.releaseSessionCalls.push(session);
182 }
183 > mockAgent.ts
184 > async abortSession(session: URI): Promise<void> {
185 this.abortSessionCalls.push(session);
186 }
187 > mockAgent.ts
188 > async truncateSession(session: URI, turnId?: string, chat?: URI): Promise<void> {
189 this.truncateSessionCalls.push({ session, turnId, chat });
190 }
191 > mockAgent.ts
192 > respondToPermissionRequest(requestId: string, approved: boolean): void {
193 this.respondToPermissionCalls.push({ requestId, approved });
194 }
195 > mockAgent.ts
196 > respondToUserInputRequest(): void {
197 // no-op for tests
198 }
199 > mockAgent.ts
200 > async changeModel(session: URI, model: ModelSelection, chat?: URI): Promise<void> {
201 this.changeModelCalls.push({ session, model, chat });
202 }
203 > mockAgent.ts
204 > async changeAgent(session: URI, agent: AgentSelection | undefined, chat?: URI): Promise<void> {
205 this.changeAgentCalls.push({ session, agent, chat });
206 }
207 > mockAgent.ts
208 > /**
209 > * Create an additional (peer) chat. The base mock is single-chat and
210 > * rejects; multi-chat test subclasses override this.
211 > */
212 > async createChat(_session: URI, _chat: URI, _options?: IAgentCreateChatOptions): Promise<IAgentCreateChatResult | void> {
213 throw new Error(`Agent ${this.id} does not support multiple chats`);
214 }
215 > mockAgent.ts
216 > /** Dispose an additional (peer) chat. Overridden by multi-chat subclasses. */
217 > async disposeChat(_session: URI, _chat: URI): Promise<void> { }
218 >
219 > /**
220 > * Map an already-resolved chat URI to the `(session, chat)` pair the
221 > * mock records calls against (mirroring the real agents).
222 > */
223 > private _resolveChatTarget(chat: URI): { session: URI; chat: URI } {
224 const parsed = parseChatUri(chat);
225 if (!parsed) {
228 return { session: URI.parse(parsed.session), chat: URI.parse(chat.toString()) };
229 }
230 > mockAgent.ts
231 > readonly chats: IAgentChats = {
232 > createChat: (chatUri: URI, options?: IAgentCreateChatOptions): Promise<IAgentCreateChatResult | void> => {
233 const { session, chat } = this._resolveChatTarget(chatUri);
234 return this.createChat(session, chat, options);
235 },
236 > fork: (chatUri: URI, source: IAgentCreateChatForkSource, options?: IAgentCreateChatOptions): Promise<IAgentCreateChatResult | void> => { mockAgent.ts
237 const { session, chat } = this._resolveChatTarget(chatUri);
238 return this.createChat(session, chat, { ...options, fork: source });
239 },
240 > disposeChat: (chatUri: URI): Promise<void> => { mockAgent.ts
241 const { session, chat } = this._resolveChatTarget(chatUri);
242 return this.disposeChat(session, chat);
243 },
244 > sendMessage: (chatUri: URI, prompt: string, _workingDirectory: URI | undefined, attachments?: readonly MessageAttachment[], turnId?: string, senderClientId?: string): Promise<void> => { mockAgent.ts
245 const { session, chat } = this._resolveChatTarget(chatUri);
246 return this.sendMessage(session, chat, prompt, attachments, turnId, senderClientId);
247 },
248 > abort: (chat: URI): Promise<void> => { mockAgent.ts
249 const { session } = this._resolveChatTarget(chat);
250 return this.abortSession(session);
251 },
252 > changeModel: (chatUri: URI, model: ModelSelection): Promise<void> => { mockAgent.ts
253 const { session, chat } = this._resolveChatTarget(chatUri);
254 return this.changeModel(session, model, chat);
255 },
256 > changeAgent: (chatUri: URI, agent: AgentSelection | undefined): Promise<void> => { mockAgent.ts
257 const { session, chat } = this._resolveChatTarget(chatUri);
258 return this.changeAgent(session, agent, chat);
259 },
260 > getMessages: (chat: URI): Promise<readonly Turn[]> => { mockAgent.ts
261 return this.getSessionMessages(chat);
262 },
263 > }; mockAgent.ts
264 > mockAgent.ts
265 > async authenticate(resource: string, token: string): Promise<boolean> {
266 this.authenticateCalls.push({ resource, token });
267 return true;
268 }
269 > mockAgent.ts
270 > getCustomizations(): Customization[] {
271 > return this.customizations; mockAgent.ts
272 > }
273 > mockAgent.ts
274 > syncClientCustomizations(session: URI, clientId: string, customizations: ClientPluginCustomization[]): ISyncedCustomization[] {
275 this.setClientCustomizationsCalls.push({ clientId, customizations });
276 const results: ISyncedCustomization[] = customizations.map(c => ({
290 return results;
291 }
292 > mockAgent.ts
293 > getOrCreateActiveClient(session: URI, client: { readonly clientId: string; readonly displayName?: string }): IActiveClient {
294 const self = this;
295 let tools: readonly ToolDefinition[] = [];
310 };
311 }
312 > mockAgent.ts
313 > removeActiveClient(_session: URI, clientId: string): void {
314 this.removeActiveClientCalls.push({ clientId });
315 }
316 > mockAgent.ts
317 > onClientToolCallComplete(session: URI, chat: URI, toolCallId: string, result: ToolCallResult): void {
318 this.clientToolCallCompleteCalls.push({ session, chat, toolCallId, result });
319 }
320 > mockAgent.ts
321 > async shutdown(): Promise<void> { }
322 >
323 > /**
324 > * Fires an {@link AgentSignal} on this agent.
325 > */
326 > fireProgress(signal: AgentSignal): void {
327 this._onDidSessionProgress.fire(signal);
328 }
329 > mockAgent.ts
330 > /**
331 > * Looks up the active turn id captured from the most recent
332 > * {@link sendMessage} call for a given session. Returns `undefined` if
333 > * the session has no active turn yet (e.g. tests that fire progress
334 > * without first calling sendMessage).
335 > */
336 > getActiveTurnId(session: URI): string | undefined {
337 return this._activeTurnIds.get(uriKey(session));
338 }
339 > mockAgent.ts
340 > fireCustomizationsChange(): void {
341 this._onDidCustomizationsChange.fire();
342 }
343 > mockAgent.ts
344 > dispose(): void {
345 > this._onDidSessionProgress.dispose(); mockAgent.ts
346 > this._onDidSendMessage.dispose();
347 > this._onDidCustomizationsChange.dispose();
348 > }
349 > } mockAgent.ts
350 >
351 > /**
352 > * Well-known URI of a pre-existing session seeded in {@link ScriptedMockAgent}.
353 > * This session appears in `listSessions()` and has message history via
354 > * `getSessionMessages()`, but was never created through the server's
355 > * `handleCreateSession`. It simulates a session from a previous server
356 > * lifetime for testing the restore-on-subscribe path.
357 > */
358 > export const PRE_EXISTING_SESSION_URI = AgentSession.uri('mock', 'pre-existing-session');
359 >
360 > export class ScriptedMockAgent implements IAgent {
361 > readonly id: AgentProvider = 'mock';
362 >
363 > private readonly _onDidSessionProgress = new Emitter<AgentSignal>();
364 > readonly onDidSessionProgress = this._onDidSessionProgress.event;
365 > private readonly _models = observableValue<readonly IAgentModelInfo[]>(this, [{ provider: 'mock', id: 'mock-model', name: 'Mock Model', maxContextWindow: 128000, supportsVision: false }]);
366 > readonly models = this._models;
367 >
368 > private readonly _sessions = new Map<string, URI>();
369 > private _nextId = 1;
370 >
371 > /**
372 > * Message history for the pre-existing session: a single user→assistant
373 > * turn with a tool call.
374 > */
375 > private readonly _preExistingMessages: IHistoryRecord[] = [
376 > { type: 'message', role: 'user', session: PRE_EXISTING_SESSION_URI, messageId: 'h-msg-1', content: 'What files are here?' },
377 > { type: 'tool_start', session: PRE_EXISTING_SESSION_URI, toolCallId: 'h-tc-1', toolName: 'list_files', displayName: 'List Files', invocationMessage: 'Listing files...' },
378 > { type: 'tool_complete', session: PRE_EXISTING_SESSION_URI, toolCallId: 'h-tc-1', result: { pastTenseMessage: 'Listed files', content: [{ type: ToolResultContentType.Text, text: 'file1.ts\nfile2.ts' }], success: true } satisfies ToolCallResult },
379 > { type: 'message', role: 'assistant', session: PRE_EXISTING_SESSION_URI, messageId: 'h-msg-2', content: 'Here are the files: file1.ts and file2.ts' },
380 > ];
381 >
382 > // Track pending permission requests
383 > private readonly _pendingPermissions = new Map<string, (approved: boolean) => void>();
384 > // Track the active turn ID per session, captured from sendMessage().
385 > private readonly _activeTurnIds = new Map<string, string>();
386 > // Track pending abort callbacks for slow responses
387 > private readonly _pendingAborts = new Map<string, () => void>();
388 >
389 > constructor() {
390 // Seed the pre-existing session so it appears in listSessions()
391 this._sessions.set(AgentSession.id(PRE_EXISTING_SESSION_URI), PRE_EXISTING_SESSION_URI);
406 }
407 }
408 > mockAgent.ts
409 > getDescriptor(): IAgentDescriptor {
410 return { provider: 'mock', displayName: 'Mock Agent', description: 'Scripted test agent' };
411 }
412 > mockAgent.ts
413 > getProtectedResources(): IAuthorizationProtectedResourceMetadata[] {
414 return [];
415 }
416 > mockAgent.ts
417 > async listSessions(): Promise<IAgentSessionMetadata[]> {
418 return [...this._sessions.values()].map(s => ({
419 session: s,
424 }));
425 }
426 > mockAgent.ts
427 > async getSessionMetadata(session: URI): Promise<IAgentSessionMetadata | undefined> {
428 if (!this._sessions.has(AgentSession.id(session))) {
429 return undefined;
437 };
438 }
439 > mockAgent.ts
440 > async createSession(config?: IAgentCreateSessionConfig): Promise<IAgentCreateSessionResult> {
441 const session = config?.session ?? AgentSession.uri('mock', `mock-session-${this._nextId++}`);
442 const rawId = AgentSession.id(session);
444 return { session, project: mockProject(this.id) };
445 }
446 > mockAgent.ts
447 > async resolveSessionConfig(params: IAgentResolveSessionConfigParams): Promise<ResolveSessionConfigResult> {
448 const isolation = params.config?.isolation === 'folder' || params.config?.isolation === 'worktree' ? params.config.isolation : 'worktree';
449 const branch = isolation === 'worktree' && typeof params.config?.branch === 'string' ? params.config.branch : 'main';
475 };
476 }
477 > mockAgent.ts
478 > async sessionConfigCompletions(params: IAgentSessionConfigCompletionsParams): Promise<SessionConfigCompletionsResult> {
479 if (params.property !== 'branch') {
480 return { items: [] };
484 return { items: branches.map(branch => ({ value: branch, label: branch })) };
485 }
486 > mockAgent.ts
487 > async sendMessage(session: URI, chat: URI, prompt: string, _attachments?: readonly MessageAttachment[], turnId?: string): Promise<void> {
488 if (turnId) {
489 this._activeTurnIds.set(uriKey(session), turnId);
821 }
822 }
823 > mockAgent.ts
824 > setPendingMessages(chat: URI, steeringMessage: PendingMessage | undefined, _queuedMessages: readonly PendingMessage[]): void {
825 // When steering is set, consume it on the next tick
826 if (steeringMessage) {
830 }
831 }
832 > mockAgent.ts
833 > getOrCreateActiveClient(_session: URI, client: { readonly clientId: string; readonly displayName?: string }): IActiveClient {
834 let tools: readonly ToolDefinition[] = [];
835 let customizations: readonly ClientPluginCustomization[] = [];
843 };
844 }
845 > mockAgent.ts
846 > removeActiveClient(): void { }
847 >
848 > private didCompleteToolCalls = new Set<string>();
849 >
850 > onClientToolCallComplete(session: URI, chat: URI, toolCallId: string, result: ToolCallResult): void {
851 // The mock's event model is chat-channel oriented (sendMessage fires
852 // every turn signal on the chat URI). Emit the completion on the chat
867 }
868 }
869 > mockAgent.ts
870 > async getSessionMessages(session: URI): Promise<readonly Turn[]> {
871 const subagentInfo = parseSubagentSessionUri(session);
872 if (subagentInfo) {
882 return [];
883 }
884 > mockAgent.ts
885 > async disposeSession(session: URI): Promise<void> {
886 this._sessions.delete(AgentSession.id(session));
887 }
888 > mockAgent.ts
889 > async abortSession(session: URI): Promise<void> {
890 const callback = this._pendingAborts.get(session.toString());
891 if (callback) {
894 }
895 }
896 > mockAgent.ts
897 > async changeModel(_session: URI, _model: ModelSelection): Promise<void> {
898 // Mock agent doesn't track model state
899 }
900 > mockAgent.ts
901 > /**
902 > * Map an already-resolved chat URI to the `(session, chat)` pair the
903 > * scripted mock's per-chat context is keyed by.
904 > */
905 > private _resolveChatTarget(chat: URI): { session: URI; chat: URI } {
906 const parsed = parseChatUri(chat);
907 if (!parsed) {
910 return { session: URI.parse(parsed.session), chat: URI.parse(chat.toString()) };
911 }
912 > mockAgent.ts
913 > readonly chats: IAgentChats = {
914 > createChat: (_chat: URI, _options?: IAgentCreateChatOptions): Promise<IAgentCreateChatResult | void> => {
915 > throw new Error('Scripted mock agent does not support multiple chats'); mockAgent.ts
916 > },
917 > fork: (_chat: URI, _source: IAgentCreateChatForkSource, _options?: IAgentCreateChatOptions): Promise<IAgentCreateChatResult | void> => { mockAgent.ts
918 > throw new Error('Scripted mock agent does not support chat forking'); mockAgent.ts
919 > },
920 > disposeChat: (_chat: URI): Promise<void> => { mockAgent.ts
921 > return Promise.resolve(); mockAgent.ts
922 > },
923 > sendMessage: (chatUri: URI, prompt: string, _workingDirectory: URI | undefined, attachments?: readonly MessageAttachment[], turnId?: string, _senderClientId?: string): Promise<void> => { mockAgent.ts
924 > const { session, chat } = this._resolveChatTarget(chatUri); mockAgent.ts
925 > return this.sendMessage(session, chat, prompt, attachments, turnId);
926 > },
927 > abort: (chat: URI): Promise<void> => { mockAgent.ts
928 > const { session } = this._resolveChatTarget(chat); mockAgent.ts
929 > return this.abortSession(session);
930 > },
931 > changeModel: (chat: URI, model: ModelSelection): Promise<void> => { mockAgent.ts
932 > const { session } = this._resolveChatTarget(chat); mockAgent.ts
933 > return this.changeModel(session, model);
934 > },
935 > changeAgent: (_chat: URI, _agent: AgentSelection | undefined): Promise<void> => { mockAgent.ts
936 > // Scripted mock does not track agent selection. mockAgent.ts
937 > return Promise.resolve();
938 > },
939 > getMessages: (chat: URI): Promise<readonly Turn[]> => { mockAgent.ts
940 > return this.getSessionMessages(chat); mockAgent.ts
941 > },
942 > }; mockAgent.ts
943 >
944 > async truncateSession(_session: URI, _turnId?: string): Promise<void> {
945 // Mock agent accepts truncation without side effects
946 }
947 > mockAgent.ts
948 > respondToPermissionRequest(toolCallId: string, approved: boolean): void {
949 const callback = this._pendingPermissions.get(toolCallId);
950 if (callback) {
953 }
954 }
955 > mockAgent.ts
956 > respondToUserInputRequest(): void {
957 // no-op for tests
958 }
959 > mockAgent.ts
960 > async authenticate(_resource: string, _token: string): Promise<boolean> {
961 return true;
962 }
963 > mockAgent.ts
964 > async shutdown(): Promise<void> { }
965 >
966 > dispose(): void {
967 this._onDidSessionProgress.dispose();
968 }
969 > mockAgent.ts
970 > /**
971 > * Fires a sequence of {@link AgentSignal}s with staggered 10 ms delays
972 > * so the state manager processes them in order.
973 > */
974 > private _fireSequence(signals: AgentSignal[]): void {
975 let delay = 0;
976 for (const signal of signals) {
979 }
980 }
981 > mockAgent.ts
982 > /** Builds the session-string + turnId context for signal construction. */
983 > private _ctx(session: URI): { sessionStr: string; turnId: string } {
984 return {
985 sessionStr: session.toString(),
987 };
988 }
989 > } mockAgent.ts
990 >
991 > // =============================================================================
992 > // Test-event helpers
993 > // =============================================================================
994 >
995 > // =============================================================================
996 > // Signal factory helpers
997 > // =============================================================================
998 >
999 > let _mockPartIdCounter = 0;
1000 >
1001 > /** Wraps a session action into an {@link IAgentActionSignal}. */
1002 function _action(session: URI, action: import('../../common/state/sessionActions.js').SessionAction | import('../../common/state/sessionActions.js').ChatAction, parentToolCallId?: string): IAgentActionSignal {
1003 return { kind: 'action', resource: session, action, parentToolCallId };
1004 }
1005 > mockAgent.ts
1006 > /** Creates a markdown {@link ResponsePartKind.Markdown} response part signal. */
1007 function _markdown(session: URI, sessionStr: string, turnId: string, content: string, parentToolCallId?: string): IAgentActionSignal {
1008 return _action(session, {
1012 }, parentToolCallId);
1013 }
1014 > mockAgent.ts
1015 > /** Creates a reasoning {@link ResponsePartKind.Reasoning} response part signal. */
1016 function _reasoning(session: URI, sessionStr: string, turnId: string, content: string): IAgentActionSignal {
1017 return _action(session, {
1021 });
1022 }
1023 > mockAgent.ts
1024 > /** Creates a {@link ActionType.ChatTurnComplete} signal. */
1025 function _idle(session: URI, sessionStr: string, turnId: string): IAgentActionSignal {
1026 return _action(session, { type: ActionType.ChatTurnComplete, turnId, duration: 1 });
1027 }
1028 > mockAgent.ts
1029 > /** Creates a {@link ActionType.ChatError} signal. */
1030 function _error(session: URI, sessionStr: string, turnId: string, errorType: string, message: string, stack?: string): IAgentActionSignal {
1031 return _action(session, { type: ActionType.ChatError, turnId, duration: 1, error: { errorType, message, stack } });
1032 }
1033 > mockAgent.ts
1034 > /** Creates a {@link ActionType.SessionTitleChanged} signal. */
1035 function _titleChanged(session: URI, sessionStr: string, title: string): IAgentActionSignal {
1036 return _action(session, { type: ActionType.SessionTitleChanged, title });
1037 }
1038 > mockAgent.ts
1039 > /** Creates a {@link ActionType.ChatUsage} signal. */
1040 function _usage(session: URI, sessionStr: string, turnId: string, usage: UsageInfo): IAgentActionSignal {
1041 return _action(session, { type: ActionType.ChatUsage, turnId, usage });
1042 }
1043 > mockAgent.ts
1044 > /**
1045 > * Creates tool-start signals: a {@link ActionType.ChatToolCallStart} and,
1046 > * for non-client tools, an auto-ready {@link ActionType.ChatToolCallReady}.
1047 > */
1048 function _toolStart(session: URI, sessionStr: string, turnId: string, toolCallId: string, toolName: string, displayName: string, invocationMessage: StringOrMarkdown, opts?: {
1049 toolInput?: string;
1085 return signals;
1086 }
1087 > mockAgent.ts
1088 > /** Creates a {@link ActionType.ChatToolCallComplete} signal. */
1089 function _toolComplete(session: URI, sessionStr: string, turnId: string, toolCallId: string, result: ToolCallResult, parentToolCallId?: string): IAgentActionSignal {
1090 return _action(session, { type: ActionType.ChatToolCallComplete, turnId, toolCallId, result }, parentToolCallId);
1091 }
1092 > mockAgent.ts
1093 > /** Creates a {@link IAgentToolPendingConfirmationSignal}. */
1094 function _pendingConfirmation(session: URI, toolCallId: string, invocationMessage: StringOrMarkdown, opts?: {
1095 toolInput?: string;
src/vs/platform/agentHost/node/shared/worktreeIsolation.ts 362 covered LOC · 39 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- worktreeIsolation.ts
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 > import * as fs from 'fs/promises';
7 > import { SequencerByKey } from '../../../../base/common/async.js';
8 > import { appendEscapedMarkdownInlineCode } from '../../../../base/common/htmlContent.js';
9 > import { Disposable } from '../../../../base/common/lifecycle.js';
10 > import { Schemas } from '../../../../base/common/network.js';
11 > import { basename } from '../../../../base/common/path.js';
12 > import { URI } from '../../../../base/common/uri.js';
13 > import { generateUuid } from '../../../../base/common/uuid.js';
14 > import { localize } from '../../../../nls.js';
15 > import { ILogService } from '../../../log/common/log.js';
16 > import { IAgentSessionProjectInfo } from '../../common/agentService.js';
17 > import { getBranchCompletions, IAgentHostGitService, IDefaultBranch, META_DIFF_BASE_BRANCH } from '../../common/agentHostGitService.js';
18 > import { ISchemaProperty, schemaProperty } from '../../common/agentHostSchema.js';
19 > import { ISessionDataService } from '../../common/sessionDataService.js';
20 > import { SessionConfigKey } from '../../common/sessionConfigKeys.js';
21 > import { AH_META_IS_ARCHIVED_DB_KEY, AH_META_IS_DONE_DB_KEY, ResponsePart, ResponsePartKind, Turn } from '../../common/state/sessionState.js';
22 > import { AGENT_BRANCH_PREFIX, AgentBranchNameGenerator, IAgentBranchNameGenerator } from './agentBranchNameGenerator.js';
23 > import { ICopilotApiService } from './copilotApiService.js';
24 >
25 > /**
26 > * Per-session-database metadata keys under which the worktree an agent
27 > * created for an isolated session is recorded. The string values keep the
28 > * historical `copilot.worktree.*` prefix so sessions materialized by earlier
29 > * Copilot builds keep resolving their worktree on archive / unarchive /
30 > * restore after this logic was unified across agents. All agents (Copilot,
31 > * Codex, Claude) now write and read these same keys; the per-session database
32 > * is already scoped by session, so there is no cross-agent collision.
33 > */
34 > const WORKTREE_META_BRANCH = 'copilot.worktree.branchName';
35 > const WORKTREE_META_PATH = 'copilot.worktree.path';
36 > export const WORKTREE_META_REPOSITORY_ROOT = 'copilot.worktree.repositoryRoot';
37 >
38 > /** Thrown when a persisted session working directory is missing and cannot be repaired. */
39 > export class SessionWorkingDirectoryMissingError extends Error {
40 > constructor(readonly workingDirectory: URI, readonly reason?: string) {
41 super(reason
42 ? localize('sessionWorkingDirectoryMissingWithReason', "This session couldn't be loaded because its worktree is missing and could not be recreated: {0}", reason)
44 this.name = 'SessionWorkingDirectoryMissingError';
45 }
47 >
48 > /** Default upper bound on branch names returned for the branch picker. */
49 > const BRANCH_COMPLETION_LIMIT = 25;
50 >
51 > interface ICreatedWorktree {
52 > readonly repositoryRoot: URI;
53 > readonly worktree: URI;
54 > }
55 >
56 > /**
57 > * The `<repo>.worktrees` sibling directory where per-session isolated
58 > * worktrees are created, e.g. `/src/vscode` → `/src/vscode.worktrees`.
59 > */
60 > export function getWorktreesRoot(repositoryRoot: URI): URI {
61 return URI.joinPath(repositoryRoot, '..', `${basename(repositoryRoot.fsPath)}.worktrees`);
62 }
64 > /**
65 > * Derives the on-disk worktree directory name from a branch name: strips the
66 > * caller-supplied prefix (e.g. the user's `git.branchPrefix`) and the built-in
67 > * `agents/` prefix so the directory stays concise, then flattens any remaining
68 > * path separators.
69 > */
70 > export function getWorktreeName(branchName: string, branchPrefix: string = ''): string {
71 let name = branchName;
72 if (branchPrefix && name.startsWith(branchPrefix)) {
78 return name.replace(/\//g, '-');
79 }
81 > /**
82 > * Builds the localized "Created isolated worktree for branch X" markdown shown
83 > * at the top of the first response in worktree-isolated sessions. The branch
84 > * name is wrapped as inline code so the localized template doesn't have to
85 > * embed markdown punctuation. The trailing blank line keeps the announcement
86 > * visually separated when it gets merged into the same markdown part as the
87 > * model's reply.
88 > */
89 > export function buildWorktreeAnnouncementText(branchName: string): string {
90 return localize(
91 'agentHost.worktreeCreated',
94 ) + '\n\n';
95 }
97 > /**
98 > * Returns a copy of `turns` where `announcement` has been prepended to the
99 > * first top-level assistant turn's first markdown response part. Used on
100 > * session restore so the worktree announcement remains visible after the
101 > * session is reopened. If no assistant content exists yet, a fresh markdown
102 > * part is inserted at the top of the first turn.
103 > */
104 > export function prependAnnouncementToFirstTurn(turns: readonly Turn[], announcement: string): readonly Turn[] {
105 if (turns.length === 0) {
106 return turns;
122 return result;
123 }
125 > /** Parameters for {@link WorktreeIsolation.resolveIsolationConfig}. */
126 > export interface IResolveIsolationConfigRequest {
127 > readonly workingDirectory: URI | undefined;
128 > readonly config: Record<string, unknown> | undefined;
129 > }
130 >
131 > /**
132 > * The isolation + branch schema contribution for an agent's
133 > * `resolveSessionConfig`. Callers merge {@link isolationProperty} (and
134 > * {@link branchProperty} / {@link worktreeBranchPrefixProperty} when present)
135 > * into their own schema and merge the default values ({@link isolationValue} /
136 > * {@link branchDefault}) into the defaults bag they pass to `validateOrDefault`.
137 > */
138 > export interface IIsolationConfigContribution {
139 > readonly isolationProperty: ISchemaProperty<'folder' | 'worktree'>;
140 > readonly branchProperty: ISchemaProperty<string> | undefined;
141 > /**
142 > * Read-only carrier for the client's `git.branchPrefix`. Declared for both
143 > * isolations (like `branch`) so the value rides `_config.values` and
144 > * survives isolation toggles; the host only consumes it for worktree
145 > * isolation (see {@link WorktreeIsolation.resolveWorkingDirectory}).
146 > */
147 > readonly worktreeBranchPrefixProperty: ISchemaProperty<string> | undefined;
148 > /** Read-only carrier for the client's `git.worktreeIncludeFiles`. */
149 > readonly worktreeIncludeFilesProperty: ISchemaProperty<readonly string[]> | undefined;
150 > readonly isolationValue: 'folder' | 'worktree';
151 > readonly branchDefault: string | undefined;
152 > readonly branchValue: string | undefined;
153 > }
154 >
155 > /** Parameters for {@link WorktreeIsolation.resolveWorkingDirectory}. */
156 > export interface IResolveWorkingDirectoryRequest {
157 > readonly sessionUri: URI;
158 > readonly sessionId: string;
159 > readonly workingDirectory: URI | undefined;
160 > readonly config: Record<string, unknown> | undefined;
161 > readonly prompt?: string;
162 > readonly githubToken?: string;
163 > }
164 >
165 > /**
166 > * Shared, per-agent controller for git-worktree session isolation. Owns the
167 > * full machinery Copilot pioneered so Codex and Claude get identical behavior:
168 > *
169 > * - advertising the `isolation` (`folder` / `worktree`) and `branch` session
170 > * config properties from `resolveSessionConfig` ({@link resolveIsolationConfig});
171 > * - completing branch names for the branch picker ({@link branchCompletions});
172 > * - creating the worktree on materialization and persisting its metadata
173 > * ({@link resolveWorkingDirectory});
174 > * - surfacing the "Created isolated worktree" announcement live on the first
175 > * turn ({@link takePendingAnnouncement}) and on restore
176 > * ({@link applyRestoreAnnouncement});
177 > * - cleaning up / recreating the worktree on dispose, archive, and unarchive.
178 > *
179 > * A single host-owned instance serves every agent: the orchestrator
180 > * ({@link AgentService}) creates it and drives the lifecycle so individual
181 > * agents stay unaware of the folder-vs-worktree distinction. Session state
182 > * (`_createdWorktrees`, pending markers, pending announcements) is keyed by the
183 > * globally-unique sessionId, so sharing one instance across agents is safe.
184 > */
185 > export class WorktreeIsolation extends Disposable {
186 >
187 > /**
188 > * Worktrees created by this agent in the current process, keyed by
189 > * sessionId. Used to remove the worktree on dispose / error and to
190 > * enumerate live worktrees during shutdown.
191 > */
192 > private readonly _createdWorktrees = new Map<string, ICreatedWorktree>();
193 >
194 > /**
195 > * Per-session announcement (markdown) emitted as a synthetic streaming
196 > * markdown part the first time the session sends a message. Surfaces the
197 > * "Created isolated worktree for branch X" message live during the first
198 > * turn; the same announcement is re-injected on restore via
199 > * {@link applyRestoreAnnouncement}.
200 > */
201 > private readonly _pendingFirstTurnAnnouncements = new Map<string, string>();
202 >
203 > /**
204 > * SessionIds of freshly-created worktree-isolation sessions whose worktree
205 > * has not yet been created (creation is deferred to the first send so the
206 > * user's prompt can drive branch naming). While a session is in this set the
207 > * host reports its working directory as "pending" ({@link isWorkingDirectoryPending})
208 > * so agents defer prewarming / materializing until {@link resolveOnFirstSend}
209 > * runs. Never populated for restored sessions — their worktree already exists
210 > * on disk and their persisted working directory already points at it.
211 > */
212 > private readonly _pending = new Set<string>();
213 >
214 > /** Fixed log label; one host-owned instance serves every agent. */
215 > private readonly _logLabel = 'AgentHost';
216 >
217 > /**
218 > * Serializes the worktree lifecycle per session so a first-send creation
219 > * ({@link resolveOnFirstSend}) never interleaves with archive/unarchive
220 > * cleanup ({@link cleanupWorktreeOnArchive} / {@link recreateWorktreeOnUnarchive})
221 > * or dispose ({@link removeCreatedWorktree}) for the same session — the
222 > * guarantee each agent previously enforced with its own sequencer.
223 > */
224 > private readonly _sequencer = new SequencerByKey<string>();
225 > private readonly _worktreeCreationSequencer = new SequencerByKey<string>();
226 >
227 > /** Branch-name generator for worktree sessions; created from {@link ICopilotApiService} unless a test supplies an override. */
228 > private readonly _branchNameGenerator: IAgentBranchNameGenerator;
229 >
230 > constructor(
231 branchNameGenerator: IAgentBranchNameGenerator | undefined,
232 @IAgentHostGitService private readonly _gitService: IAgentHostGitService,
238 this._branchNameGenerator = branchNameGenerator ?? new AgentBranchNameGenerator(copilotApiService, this._logService);
239 }
241 > /** SessionIds with a worktree created by this agent in the current process. */
242 > get createdWorktreeSessionIds(): readonly string[] {
243 return [...this._createdWorktrees.keys()];
244 }
246 > /**
247 > * Marks a fresh worktree-isolation session as pending — its worktree is
248 > * deferred to the first send. Called by the host while a creating session's
249 > * resolved config selects `worktree` isolation.
250 > */
251 > notePending(sessionId: string): void {
252 this._pending.add(sessionId);
253 }
255 > /** Clears a pending marker when a session will not materialize a worktree. */
256 > clearPending(sessionId: string): void {
257 this._pending.delete(sessionId);
258 }
260 > /**
261 > * Whether a session's worktree is still pending creation. The host exposes
262 > * this through {@link IAgentConfigurationService.isWorkingDirectoryPending} so
263 > * agents defer materialization until the host has resolved the worktree.
264 > */
265 > isWorkingDirectoryPending(sessionId: string): boolean {
266 return this._pending.has(sessionId);
267 }
269 > /** The worktree created for a session in this process, if any. */
270 > getResolvedWorktree(sessionId: string): URI | undefined {
271 return this._createdWorktrees.get(sessionId)?.worktree;
272 }
274 > /**
275 > * First-send worktree resolution: creates the worktree (when the session
276 > * selected `worktree` isolation on a git repo) and clears the pending marker
277 > * regardless of outcome, so a failed creation falls back to folder isolation
278 > * instead of leaving the session permanently "pending". Delegates to
279 > * {@link resolveWorkingDirectory}, which is idempotent per session.
280 > */
281 > async resolveOnFirstSend(request: IResolveWorkingDirectoryRequest): Promise<URI | undefined> {
282 return this._sequencer.queue(request.sessionId, async () => {
283 try {
288 });
289 }
291 > /**
292 > * Builds the `isolation` / `branch` schema contribution for
293 > * `resolveSessionConfig`. When {@link IResolveIsolationConfigRequest.workingDirectory}
294 > * is not a git repository (or has no commits yet) isolation is forced to
295 > * `folder` and no branch property is offered.
296 > */
297 > async resolveIsolationConfig(request: IResolveIsolationConfigRequest): Promise<IIsolationConfigContribution> {
298 const gitInfo = request.workingDirectory ? await this._getGitInfo(request.workingDirectory) : undefined;
299
373 return { isolationProperty, branchProperty, worktreeBranchPrefixProperty, worktreeIncludeFilesProperty, isolationValue, branchDefault, branchValue };
374 }
376 > /**
377 > * Branch-name completions for the branch picker. Callers forward this from
378 > * their `sessionConfigCompletions` when the requested property is
379 > * {@link SessionConfigKey.Branch}.
380 > */
381 > async branchCompletions(workingDirectory: URI | undefined, query?: string): Promise<{ items: { value: string; label: string }[] }> {
382 if (!workingDirectory) {
383 return { items: [] };
388 return { items: branchCompletions.map(branch => ({ value: branch, label: branch })) };
389 }
391 > /**
392 > * Resolves the effective working directory for a session that is about to
393 > * be materialized. When the session config selects `worktree` isolation on
394 > * a git repository, creates a fresh branch + worktree, records it for
395 > * cleanup, queues the first-turn announcement, persists the worktree
396 > * metadata, and returns the worktree URI. Otherwise returns the requested
397 > * working directory unchanged.
398 > */
399 > async resolveWorkingDirectory(request: IResolveWorkingDirectoryRequest): Promise<URI | undefined> {
400 const { config, workingDirectory, sessionId, sessionUri, prompt, githubToken } = request;
401 if (config?.[SessionConfigKey.Isolation] !== 'worktree' || !workingDirectory || typeof config[SessionConfigKey.Branch] !== 'string') {
467 return worktree;
468 }
470 > /** Resolves a persisted working directory, repairing a removed worktree when possible. */
471 > async resolveWorkingDirectoryForResume(sessionUri: URI, sessionId: string, workingDirectory: URI): Promise<URI> {
472 return this._sequencer.queue(sessionId, () => this._resolveWorkingDirectoryForResume(sessionUri, sessionId, workingDirectory));
473 }
475 > private async _resolveWorkingDirectoryForResume(sessionUri: URI, sessionId: string, workingDirectory: URI): Promise<URI> {
476 if (workingDirectory.scheme !== Schemas.file) {
477 return workingDirectory;
514 throw new SessionWorkingDirectoryMissingError(workingDirectory, recreateFailureReason);
515 }
517 > /**
518 > * Takes (and clears) the pending "worktree created" announcement for a
519 > * session so callers can emit it live as the first response part on the
520 > * first turn. Returns `undefined` when the session has no pending
521 > * announcement.
522 > */
523 > takePendingAnnouncement(sessionId: string): string | undefined {
524 const announcement = this._pendingFirstTurnAnnouncements.get(sessionId);
525 if (announcement !== undefined) {
528 return announcement;
529 }
531 > /**
532 > * Re-injects the worktree announcement into a restored transcript by
533 > * prepending it to the first turn. No-op when the session was not worktree
534 > * isolated. Callers forward the turns returned from their history-read path.
535 > *
536 > * The live path ({@link takePendingAnnouncement}) handles the very first
537 > * turn while the session is fresh; this path takes over on subsequent loads
538 > * (where the synthetic announcement is not part of the agent transcript).
539 > */
540 > async applyRestoreAnnouncement(sessionUri: URI, turns: readonly Turn[]): Promise<readonly Turn[]> {
541 const worktreeMeta = await this._readWorktreeMetadata(sessionUri).catch(() => undefined);
542 if (!worktreeMeta?.branchName) {
545 return prependAnnouncementToFirstTurn(turns, buildWorktreeAnnouncementText(worktreeMeta.branchName));
546 }
548 > /**
549 > * Removes the worktree created for a session in the current process (if
550 > * any). Used on session dispose and on materialization failure.
551 > */
552 > async removeCreatedWorktree(sessionId: string): Promise<void> {
553 return this._sequencer.queue(sessionId, () => this._removeCreatedWorktree(sessionId));
554 }
556 > private async _removeCreatedWorktree(sessionId: string): Promise<void> {
557 this.clearPending(sessionId);
558 const worktree = this._createdWorktrees.get(sessionId);
568 }
569 }
571 > /**
572 > * Removes every worktree created by this agent in the current process.
573 > * Called from the agent's `shutdown` so no isolated worktree is leaked when
574 > * the provider is torn down, matching Copilot's shutdown drain.
575 > */
576 > async removeAllCreatedWorktrees(): Promise<void> {
577 await Promise.all(this.createdWorktreeSessionIds.map(sessionId => this.removeCreatedWorktree(sessionId)));
578 }
580 > /**
581 > * On archive, removes the worktree directory when its branch is preserved
582 > * and the working tree is clean, so the worktree can be recreated on
583 > * unarchive without losing work. Skips the removal when the branch is
584 > * missing or the tree is dirty.
585 > */
586 > async cleanupWorktreeOnArchive(sessionUri: URI, sessionId: string): Promise<void> {
587 return this._sequencer.queue(sessionId, () => this._cleanupWorktreeOnArchive(sessionUri, sessionId));
588 }
590 > private async _cleanupWorktreeOnArchive(sessionUri: URI, sessionId: string): Promise<void> {
591 const meta = await this._readWorktreeMetadata(sessionUri).catch(() => undefined);
592 if (!meta?.worktreePath || !meta.repositoryRoot) {
631 }
632 }
634 > /**
635 > * On unarchive, recreates a previously cleaned-up worktree against its
636 > * preserved branch. No-op when the directory still exists or the branch is
637 > * missing.
638 > */
639 > async recreateWorktreeOnUnarchive(sessionUri: URI, sessionId: string): Promise<void> {
640 return this._sequencer.queue(sessionId, () => this._recreateWorktreeOnUnarchive(sessionUri, sessionId));
641 }
643 > private async _recreateWorktreeOnUnarchive(sessionUri: URI, sessionId: string): Promise<void> {
644 const meta = await this._readWorktreeMetadata(sessionUri).catch(() => undefined);
645 if (!meta?.worktreePath || !meta.repositoryRoot) {
657 await this._recreateWorktree(sessionId, { branchName, worktreePath, repositoryRoot });
658 }
660 > private async _recreateWorktree(sessionId: string, meta: { readonly branchName: string; readonly worktreePath: URI; readonly repositoryRoot: URI }): Promise<{ readonly ok: true } | { readonly ok: false; readonly reason: string }> {
661 const { branchName, worktreePath, repositoryRoot } = meta;
662 const branchPresent = await this._gitService.branchExists(repositoryRoot, branchName).catch(() => false);
678 }
679 }
681 > /** Reads the persisted worktree metadata for a session, if any. */
682 > async readWorktreeMetadata(sessionUri: URI): Promise<{ branchName: string; worktreePath?: URI; repositoryRoot?: URI } | undefined> {
683 return this._readWorktreeMetadata(sessionUri);
684 }
686 > /**
687 > * Resolves the repository "project" for a worktree-isolated session from its
688 > * persisted worktree metadata. Worktree sessions run out of a
689 > * `<repo>.worktrees/<name>` directory, but in the sessions UI they must group
690 > * under the *repository* (e.g. `vscode`) — not the worktree folder — exactly
691 > * like Copilot. Returns the repository root as the project so agents can merge
692 > * it into the `project` field of the `IAgentSessionMetadata` reported from
693 > * `listSessions` / `getSessionMetadata`; without it a list refresh clears the
694 > * transient project set by the materialize event and the workspace reverts to
695 > * the worktree directory name. Returns `undefined` for sessions that were never
696 > * worktree-isolated, leaving the caller's own folder-based project untouched.
697 > */
698 > async resolveWorktreeProject(sessionUri: URI): Promise<IAgentSessionProjectInfo | undefined> {
699 const meta = await this._readWorktreeMetadata(sessionUri).catch(() => undefined);
700 return meta?.repositoryRoot ? projectFromRepositoryRoot(meta.repositoryRoot) : undefined;
701 }
703 > /**
704 > * Synchronous companion to {@link resolveWorktreeProject} for the
705 > * materialize-event path: the repository project for a worktree this agent
706 > * created in the current process, or `undefined` when the session has none.
707 > * Lets an agent supply the materialize event's `project` without an async
708 > * metadata read so a fresh worktree groups under the repository the moment it
709 > * materializes.
710 > */
711 > createdWorktreeProject(sessionId: string): IAgentSessionProjectInfo | undefined {
712 const worktree = this._createdWorktrees.get(sessionId);
713 return worktree ? projectFromRepositoryRoot(worktree.repositoryRoot) : undefined;
714 }
716 > private async _getGitInfo(workingDirectory: URI): Promise<{ currentBranch: string; defaultBranch: IDefaultBranch } | undefined> {
717 const repositoryRoot = await this._gitService.getRepositoryRoot(workingDirectory);
718 if (!repositoryRoot) {
730 return { currentBranch, defaultBranch };
731 }
733 > private async _resolveBranchStartPoint(repositoryRoot: URI, selectedBranch: string): Promise<string> {
734 const defaultBranch = await this._gitService.getDefaultBranch(repositoryRoot);
735 return defaultBranch?.name === selectedBranch
737 : selectedBranch;
738 }
740 > private async _writeWorktreeMetadata(sessionUri: URI, metadata: { branchName: string; baseBranch: string | undefined; worktreePath: URI; repositoryRoot: URI }): Promise<void> {
741 const dbRef = this._sessionDataService.openDatabase(sessionUri);
742 try {
754 }
755 }
757 > private async _readWorktreeMetadata(sessionUri: URI): Promise<{ branchName: string; worktreePath?: URI; repositoryRoot?: URI } | undefined> {
758 const ref = await this._sessionDataService.tryOpenDatabase(sessionUri);
759 if (!ref) {
776 }
777 }
779 > private async _isSessionArchived(sessionUri: URI): Promise<boolean> {
780 const ref = await this._sessionDataService.tryOpenDatabase(sessionUri);
781 if (!ref) {
792 }
793 }
795 >
796 > /**
797 > * Derives the repository {@link IAgentSessionProjectInfo} from a repository
798 > * root URI. The display name is the repo directory's basename (falling back to
799 > * the URI string for pathological roots), matching how Copilot names the
800 > * project via `resolveGitProject`.
801 > */
802 function projectFromRepositoryRoot(repositoryRoot: URI): IAgentSessionProjectInfo {
803 return { uri: repositoryRoot, displayName: basename(repositoryRoot.fsPath) || repositoryRoot.toString() };
804 }
806 > /**
807 > * Builds the repository {@link IAgentSessionProjectInfo} from a persisted
808 > * {@link WORKTREE_META_REPOSITORY_ROOT} value (a URI string), or `undefined`
809 > * when absent. Lets the host merge the repository project into a session's
810 > * catalog entry directly from a metadata batch it already read, without a
811 > * second database open.
812 > */
813 > export function worktreeProjectFromRepositoryRoot(repositoryRootRaw: string | undefined): IAgentSessionProjectInfo | undefined {
814 return repositoryRootRaw ? projectFromRepositoryRoot(URI.parse(repositoryRootRaw)) : undefined;
815 }
817 function errorMessage(error: unknown): string {
818 return error instanceof Error ? error.message : String(error);
819 }
821 async function fileExists(path: string): Promise<boolean> {
822 try {
src/vs/platform/agentHost/node/agentHostTelemetryReporter.ts 345 covered LOC · 13 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostTelemetryReporter.ts
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 > import type { LanguageModelToolInvokedClassification, LanguageModelToolInvokedEvent } from '../../telemetry/common/languageModelToolTelemetry.js';
7 > import type { ITelemetryService } from '../../telemetry/common/telemetry.js';
8 > import { TelemetryTrustedValue } from '../../telemetry/common/telemetryUtils.js';
9 > import { hash } from '../../../base/common/hash.js';
10 > import { AgentSession } from '../common/agentService.js';
11 > import type { ErrorInfo, MessageAttachment, SessionInputRequestKind, ToolDefinition } from '../common/state/protocol/state.js';
12 > import { isAhpChatChannel, isSubagentChatUri, isSubagentSession, parseRequiredSessionUriFromChatUri, type ISessionWithDefaultChat } from '../common/state/sessionState.js';
13 > import type { ToolInvokedResult } from './agentHostToolCallTracker.js';
14 > import { multiplexProperties, type IAgentHostRestrictedTelemetry, type IAgentHostRestrictedTelemetryContext } from './agentHostRestrictedTelemetry.js';
15 >
16 > export type AgentHostUserMessageSentSource = 'direct' | 'queued';
17 >
18 > export interface IAgentHostUserMessageSentEvent {
19 > provider: string;
20 > agentSessionId: string;
21 > source: AgentHostUserMessageSentSource;
22 > isSubagentSession: boolean;
23 > turnCount: number;
24 > activeClientId?: string;
25 > activeClientToolCount?: number;
26 > activeClientCustomizationCount?: number;
27 > attachmentCount: number;
28 > }
29 >
30 > export type IAgentHostUserMessageSentClassification = {
31 > provider: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider handling the agent host session.' };
32 > agentSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The agent host session identifier.' };
33 > source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user message was sent directly or from the queued-message flow.' };
34 > isSubagentSession: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the message was sent to a subagent session.' };
35 > turnCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of completed turns in the session when the message was sent.' };
36 > activeClientId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the first active client for the session, if any.' };
37 > activeClientToolCount?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The total number of tools provided by the active clients, if any.' };
38 > activeClientCustomizationCount?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The total number of customizations provided by the active clients, if any.' };
39 > attachmentCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of attachments included with the user message.' };
40 > owner: 'roblourens';
41 > comment: 'Tracks user messages sent from the agent host process to an agent provider.';
42 > };
43 >
44 > export type AgentHostTurnResult = 'success' | 'error' | 'cancelled';
45 > export type AgentHostModelTelemetryKind = 'trusted' | 'byok' | 'unknown';
46 > type AgentHostModelSelectionKind = 'default' | 'auto' | 'explicit';
47 > export type AgentHostTurnFailureStage = 'validation' | 'workingDirectory' | 'modelSelection' | 'sendMessage' | 'provider';
48 >
49 > export interface IAgentHostTurnCompletedEvent {
50 > provider: string;
51 > agentSessionId: string;
52 > turnId: string;
53 > timeToFirstProgress: number | undefined;
54 > totalTime: number;
55 > result: AgentHostTurnResult;
56 > model: string | TelemetryTrustedValue<string> | undefined;
57 > modelSelectionKind: AgentHostModelSelectionKind;
58 > permissionLevel: string | undefined;
59 > errorType: string | undefined;
60 > failureStage: AgentHostTurnFailureStage | undefined;
61 > }
62 >
63 > export type IAgentHostTurnCompletedClassification = {
64 > provider: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider handling the agent host session.' };
65 > agentSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The agent host session identifier.' };
66 > turnId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the turn within the agent host session.' };
67 > timeToFirstProgress: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time in milliseconds from turn start to the first visible progress (text delta, response part, tool call start, or reasoning).' };
68 > totalTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Total time in milliseconds from turn start to turn completion.' };
69 > result: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the turn completed successfully, with an error, or was cancelled.' };
70 > model: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The trusted provider model identifier selected at turn start, or a generic value for BYOK and unknown models.' };
71 > modelSelectionKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the client used the provider default, Auto, or an explicit model.' };
72 > permissionLevel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The tool auto-approval level configured for the session at turn start (e.g. default, autoApprove, autopilot).' };
73 > errorType: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The structured agent host or provider error type when the turn fails.' };
74 > failureStage: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The bounded stage at which the agent host turn failed.' };
75 > owner: 'roblourens';
76 > comment: 'Tracks agent host turn performance including time to first visible progress and total turn duration.';
77 > };
78 >
79 > export interface IAgentHostTurnFailedEvent {
80 > provider: string;
81 > agentSessionId: string;
82 > turnId: string;
83 > failureStage: AgentHostTurnFailureStage;
84 > errorType: string;
85 > errorName: string | undefined;
86 > errorCode: string | undefined;
87 > msg: string;
88 > callstack: string | undefined;
89 > }
90 >
91 > export type IAgentHostTurnFailedClassification = {
92 > provider: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The provider handling the failed agent host turn.' };
93 > agentSessionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The agent host session identifier.' };
94 > turnId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The identifier of the failed turn within the agent host session.' };
95 > failureStage: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The bounded stage at which the agent host turn failed.' };
96 > errorType: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The structured agent host or provider error type.' };
97 > errorName: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'The name of the exception, when available.' };
98 > errorCode: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'The exception or protocol error code, when available.' };
99 > msg: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'The error message. VS Code telemetry scrubs file paths and likely secrets before transmission.' };
100 > callstack: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'The error stack. VS Code telemetry scrubs file paths and likely secrets before transmission.' };
101 > owner: 'roblourens';
102 > comment: 'Captures diagnostic details for failed agent host turns.';
103 > };
104 >
105 > export interface IAgentHostTurnFailure {
106 > stage: AgentHostTurnFailureStage;
107 > error: ErrorInfo;
108 > errorName?: string;
109 > errorCode?: string;
110 > errorStack?: string;
111 > }
112 >
113 > export interface IAgentHostTurnCompletedReport {
114 > provider: string;
115 > session: string;
116 > turnId: string;
117 > timeToFirstProgress: number | undefined;
118 > totalTime: number;
119 > result: AgentHostTurnResult;
120 > model: string | undefined;
121 > modelTelemetryKind: AgentHostModelTelemetryKind | undefined;
122 > permissionLevel: string | undefined;
123 > failure: IAgentHostTurnFailure | undefined;
124 > }
125 >
126 > export interface IAgentHostToolInvokedReport {
127 > provider: string;
128 > session: string;
129 > toolId: string;
130 > toolSourceKind: string;
131 > result: ToolInvokedResult;
132 > invocationTimeMs: number;
133 > }
134 >
135 > export interface IAgentHostToolCallDetailsReport {
136 > session: string;
137 > turnId: string;
138 > model: string | undefined;
139 > responseType: string;
140 > /** Count of invocations keyed by tool name, across all rounds in the turn. */
141 > toolCounts: Record<string, number>;
142 > /** Names of the tools offered to the model for this turn. */
143 > availableTools: readonly string[];
144 > /** Number of model-call rounds in the turn, including the final tool-free response round (matches the extension's `toolCallRounds.length`). */
145 > numRequests: number;
146 > totalToolCalls: number;
147 > parallelToolCallRounds: number;
148 > parallelToolCallsTotal: number;
149 > }
150 >
151 > export interface IAgentHostSkillContentReadReport {
152 > /** The skill name. */
153 > name: string;
154 > /** Path to the SKILL.md file. */
155 > path: string;
156 > /** Full skill content; hashed (never sent raw), matching the extension. */
157 > content: string;
158 > /** Where the skill was discovered (project, personal-copilot, plugin, builtin, …). */
159 > source: string | undefined;
160 > /** Name of the plugin the skill came from, when applicable (AH-native analog of the extension's skill extension id). */
161 > pluginName: string | undefined;
162 > /** Version of the plugin the skill came from, when applicable. */
163 > pluginVersion: string | undefined;
164 > }
165 >
166 > export type AgentHostRepoInfoResult = 'success' | 'filesChanged' | 'diffTooLarge' | 'noChanges' | 'tooManyChanges' | 'mergeBaseTooOld' | 'virtualFileSystem' | 'tooManyCommits';
167 >
168 > export interface IAgentHostRepoInfoReport {
169 > telemetryMessageId: string;
170 > location: 'begin' | 'end';
171 > remoteUrl: string;
172 > repoId: string;
173 > repoType: 'github' | 'ado';
174 > headCommitHash: string;
175 > headBranchName: string | undefined;
176 > fileRelativePaths: string | undefined;
177 > diffsJSON: string | undefined;
178 > result: AgentHostRepoInfoResult;
179 > isActiveRepository: 'true';
180 > workspaceFileCount: number;
181 > changedFileCount: number;
182 > diffSizeBytes: number;
183 > }
184 >
185 > export interface IAgentHostToolCallStalledEvent {
186 > provider: string;
187 > agentSessionId: string;
188 > isSubagentSession: boolean;
189 > blockerKind: SessionInputRequestKind.ToolConfirmation | SessionInputRequestKind.ToolClientExecution | SessionInputRequestKind.ToolAuthentication;
190 > toolId: string;
191 > toolSourceKind: string;
192 > stalledTimeMs: number;
193 > }
194 >
195 > export type IAgentHostToolCallStalledClassification = {
196 > provider: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The provider handling the stalled agent host tool call.' };
197 > agentSessionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The agent host session identifier.' };
198 > isSubagentSession: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Whether the stalled tool call belongs to a subagent session.' };
199 > blockerKind: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the tool call is waiting for confirmation or client execution.' };
200 > toolId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The identifier of the stalled tool.' };
201 > toolSourceKind: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the stalled tool is provided by the agent host, an MCP server, or a client.' };
202 > stalledTimeMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time in milliseconds that the tool call has remained blocked.' };
203 > owner: 'roblourens';
204 > comment: 'Tracks agent host tool calls that remain blocked beyond the stall threshold.';
205 > };
206 >
207 > export interface IAgentHostToolCallStalledReport {
208 > provider: string;
209 > session: string;
210 > blockerKind: SessionInputRequestKind.ToolConfirmation | SessionInputRequestKind.ToolClientExecution | SessionInputRequestKind.ToolAuthentication;
211 > toolId: string;
212 > toolSourceKind: string;
213 > stalledTimeMs: number;
214 > }
215 >
216 > export interface IAgentHostStalledToolCallCompletedEvent {
217 > provider: string;
218 > agentSessionId: string;
219 > isSubagentSession: boolean;
220 > blockerKind: SessionInputRequestKind.ToolConfirmation | SessionInputRequestKind.ToolClientExecution | SessionInputRequestKind.ToolAuthentication;
221 > toolId: string;
222 > toolSourceKind: string;
223 > result: ToolInvokedResult;
224 > totalTimeMs: number;
225 > timeAfterStallMs: number;
226 > }
227 >
228 > export type IAgentHostStalledToolCallCompletedClassification = {
229 > provider: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The provider handling the completed agent host tool call.' };
230 > agentSessionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The agent host session identifier.' };
231 > isSubagentSession: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Whether the completed tool call belongs to a subagent session.' };
232 > blockerKind: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the tool call had stalled waiting for confirmation or client execution.' };
233 > toolId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The identifier of the completed tool.' };
234 > toolSourceKind: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the completed tool is provided by the agent host, an MCP server, or a client.' };
235 > result: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the stalled tool call eventually completed successfully, with an error, or through user cancellation.' };
236 > totalTimeMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Total time in milliseconds from tool call start to completion.' };
237 > timeAfterStallMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time in milliseconds from the stall report to tool call completion.' };
238 > owner: 'roblourens';
239 > comment: 'Tracks agent host tool calls that complete after previously exceeding the stall threshold.';
240 > };
241 >
242 > export interface IAgentHostStalledToolCallCompletedReport {
243 > provider: string;
244 > session: string;
245 > blockerKind: SessionInputRequestKind.ToolConfirmation | SessionInputRequestKind.ToolClientExecution | SessionInputRequestKind.ToolAuthentication;
246 > toolId: string;
247 > toolSourceKind: string;
248 > result: ToolInvokedResult;
249 > totalTimeMs: number;
250 > timeAfterStallMs: number;
251 > }
252 >
253 > export class AgentHostTelemetryReporter {
254 >
255 > constructor(private readonly _telemetryService: ITelemetryService) { }
256 >
257 > /** The restricted GH/MSFT telemetry surface, present when the agent-host telemetry service is wired. */
258 > private get _restricted(): IAgentHostRestrictedTelemetry | undefined {
259 const ts = this._telemetryService as Partial<IAgentHostRestrictedTelemetry>;
260 return typeof ts.sendEnhancedGHTelemetryEvent === 'function' ? ts as IAgentHostRestrictedTelemetry : undefined;
261 }
263 > userMessageSent(provider: string, session: string, sessionState: ISessionWithDefaultChat | undefined, source: AgentHostUserMessageSentSource, attachments: readonly MessageAttachment[] | undefined): void {
264 const attachmentCount = attachments?.length ?? 0;
265 const activeClients = sessionState?.activeClients ?? [];
279 });
280 }
282 > /**
283 > * Mirrors the Copilot extension's enhanced GH `request.options.tools` event for the agent-host
284 > * flow. The extension emits it per LLM request from its model fetcher; the agent host observes
285 > * the equivalent boundary when an `assistant.message` arrives (one per model call). The
286 > * extension populates `headerRequestId` with the client-minted `x-request-id`, which the SDK
287 > * does not surface on success; we keep the same field name (so science queries are undisturbed)
288 > * but fill it with the model call's `x-copilot-service-request-id`, the per-call id the SDK does
289 > * expose. `messagesJson` is the raw tool definitions offered for the call, multiplexed across
290 > * ~8192-char chunks like the extension, so it lands identically downstream.
291 > *
292 > * @param session Session URI string; its id becomes `conversationId`.
293 > * @param serviceRequestId The model call's `x-copilot-service-request-id`, mapped to the extension's `headerRequestId`. No-ops when absent (e.g. providers that don't surface it).
294 > * @param tools The tool definitions offered to the model for this call.
295 > */
296 > assistantMessageReceived(session: string, serviceRequestId: string | undefined, tools: readonly ToolDefinition[]): void {
297 const restricted = this._restricted;
298 if (!restricted || !serviceRequestId || tools.length === 0) {
305 }));
306 }
308 > /**
309 > * Mirrors the Copilot extension's restricted `conversation.messageText` event (the panel-chat
310 > * prefix of `sendConversationalMessageTelemetry`) for the user's prompt. The extension emits it
311 > * for every user and model message, carrying the raw message text to the enhanced GH
312 > * (`copilot_v0_restricted_copilot_event`) and internal MSFT pipelines; the agent host observes
313 > * the same boundary at the SDK `user.message` event. The text is multiplexed across ~8192-char
314 > * chunks (`messageText`, `messageText_02`, …) so long prompts land untruncated, matching the
315 > * extension's `multiplexProperties`.
316 > *
317 > * @param session Session URI string; its id becomes `conversationId`.
318 > * @param content The user's prompt text. No-ops when empty.
319 > * @param turnIndex The 0-based ordinal of the turn this message belongs to, matching the extension's numeric `turnIndex` (`conversation.turns.length`). CTS parses `turn_index` as an integer, so a numeric ordinal is required here (a non-numeric id lands empty).
320 > */
321 > userMessageText(session: string, content: string, turnIndex: number): void {
322 const restricted = this._restricted;
323 if (!restricted || !content) {
334 restricted.sendInternalMSFTTelemetryEvent('conversation.messageText', properties, measurements);
335 }
337 > /**
338 > * The model-message counterpart to {@link userMessageText}. Emitted when an `assistant.message`
339 > * arrives (the agent host's per-model-call boundary), carrying the assistant's response text.
340 > * `headerRequestId` is filled with the model call's `x-copilot-service-request-id` (the id the
341 > * SDK exposes), mirroring the field the extension populates from the client-minted request id.
342 > * VS Code-only enrichment dims (code-block languages/counts) are not reconstructed here.
343 > *
344 > * @param session Session URI string; its id becomes `conversationId`.
345 > * @param content The assistant's response text. No-ops when empty.
346 > * @param turnIndex The 0-based ordinal of the turn this message belongs to, matching the extension's numeric `turnIndex` (`conversation.turns.length`). CTS parses `turn_index` as an integer, so a numeric ordinal is required here.
347 > * @param serviceRequestId The model call's `x-copilot-service-request-id`, mapped to `headerRequestId`.
348 > */
349 > modelMessageText(session: string, content: string, turnIndex: number, serviceRequestId: string | undefined): void {
350 const restricted = this._restricted;
351 if (!restricted || !content) {
363 restricted.sendInternalMSFTTelemetryEvent('conversation.messageText', properties, measurements);
364 }
366 > /**
367 > * Mirrors the Copilot extension's restricted `toolCallDetailsExternal` / `toolCallDetailsInternal`
368 > * events (`chatParticipantTelemetry.ts` -> `sendToolCallingTelemetry`) — the per-turn tool-call
369 > * aggregate. The extension emits it once at the end of a turn's tool-calling loop; the agent host
370 > * accumulates the same counts across the turn's `assistant.message` rounds and emits on turn
371 > * completion. The tool-definition token count, per-round token/char counts, invalid-round count,
372 > * and turn index (the extension emits it only as a non-landing measurement) are not surfaced at the
373 > * AH turn boundary and are omitted. Like the extension, this fires for every turn that had tools
374 > * available — even one that made no tool calls (empty `toolCounts`) — and no-ops only when no tools
375 > * were offered.
376 > *
377 > * @param report The per-turn tool-call aggregate.
378 > */
379 > toolCallDetails(report: IAgentHostToolCallDetailsReport): void {
380 const restricted = this._restricted;
381 if (!restricted || report.availableTools.length === 0) {
402 restricted.sendInternalMSFTTelemetryEvent('toolCallDetailsInternal', properties, measurements);
403 }
405 > /**
406 > * Mirrors the Copilot extension's restricted `skillContentRead` event (`skillTelemetry.ts` ->
407 > * `sendSkillContentReadTelemetry`) — records which skill file was loaded into the conversation.
408 > * The extension emits it from the skill/readFile tools; the agent host observes the equivalent
409 > * boundary at the SDK `skill.invoked` event, whose payload already carries the content (hashed
410 > * here, never sent raw), the discovery `source`, and the plugin identity. The extension's
411 > * `skillExtensionId` / `skillExtensionVersion` encode the contributing *VS Code extension*, which
412 > * does not exist in the agent host; the AH-native provenance is the plugin, so `pluginName` /
413 > * `pluginVersion` fill those columns. No-ops when the skill name is empty.
414 > *
415 > * @param report The invoked skill's metadata (from the SDK `skill.invoked` payload).
416 > */
417 > skillContentRead(report: IAgentHostSkillContentReadReport): void {
418 const restricted = this._restricted;
419 if (!restricted || !report.name) {
443 restricted.sendInternalMSFTTelemetryEvent('skillContentRead', plaintextProps);
444 }
446 > reportRepoInfo(context: IAgentHostRestrictedTelemetryContext, report: IAgentHostRepoInfoReport): void {
447 const restricted = this._restricted;
448 if (!restricted) {
473 restricted.sendInternalMSFTTelemetryEventForContext(context, 'request.repoInfo', multiplexProperties(internalProperties), measurements);
474 }
476 > turnCompleted(report: IAgentHostTurnCompletedReport): void {
477 const session = isAhpChatChannel(report.session) ? parseRequiredSessionUriFromChatUri(report.session) : report.session;
478 const model = report.model === undefined
508 }
509 }
511 > toolInvoked(report: IAgentHostToolInvokedReport): void {
512 // `chatSessionId` is the full session URI string (matching the value
513 // previously emitted by `CopilotAgentSession`). Action signals are keyed
524 });
525 }
527 > toolCallStalled(report: IAgentHostToolCallStalledReport): void {
528 const session = isAhpChatChannel(report.session) ? parseRequiredSessionUriFromChatUri(report.session) : report.session;
529 this._telemetryService.publicLog2<IAgentHostToolCallStalledEvent, IAgentHostToolCallStalledClassification>('agentHost.toolCallStalled', {
537 });
538 }
540 > stalledToolCallCompleted(report: IAgentHostStalledToolCallCompletedReport): void {
541 const session = isAhpChatChannel(report.session) ? parseRequiredSessionUriFromChatUri(report.session) : report.session;
542 this._telemetryService.publicLog2<IAgentHostStalledToolCallCompletedEvent, IAgentHostStalledToolCallCompletedClassification>('agentHost.stalledToolCallCompleted', {
src/vs/platform/agentHost/common/agentHostGitService.ts 342 covered LOC · 3 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostGitService.ts
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 > import { VSBuffer } from '../../../base/common/buffer.js';
7 > import { URI } from '../../../base/common/uri.js';
8 > import { createDecorator } from '../../instantiation/common/instantiation.js';
9 > import { ISessionFileDiff, ISessionGitState } from './state/sessionState.js';
10 >
11 > /**
12 > * Provider-agnostic session-database metadata key under which agents
13 > * persist the branch they want git-driven diffs anchored to. Read by
14 > * {@link IAgentHostChangesetService} when computing per-session file diffs; absent
15 > * value means the diff falls back to anchoring at HEAD.
16 > */
17 > export const META_DIFF_BASE_BRANCH = 'agentHost.diffBaseBranch';
18 >
19 > /**
20 > * Resolves the Branch Changes base-branch **name** from its two sources, in
21 > * precedence order: the agent-persisted {@link META_DIFF_BASE_BRANCH} metadata
22 > * value, then the session git state's detected base branch. Returns `undefined`
23 > * when neither is available (callers then anchor the diff at `HEAD`).
24 > *
25 > * Shared by {@link IAgentHostChangesetService} and the review service so both
26 > * pick the same base branch.
27 > */
28 > export function resolveDiffBaseBranchName(persistedBaseBranch: string | undefined, sessionGitStateBaseBranch: string | undefined): string | undefined {
29 return persistedBaseBranch ?? sessionGitStateBaseBranch;
30 }
32 > /**
33 > * The well-known SHA-1 of git's empty tree, used as a fallback when a
34 > * repository has no commits (no `HEAD` to read into the temp index).
35 > */
36 > export const EMPTY_TREE_OBJECT = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
37 >
38 > /** Options for {@link IAgentHostGitService.computeSessionFileDiffs}. */
39 > export interface IComputeSessionFileDiffsOptions {
40 > /**
41 > * The session URI, used as the authority of the produced
42 > * `git-blob:` URIs so the resolver can find the session's working
43 > * directory.
44 > */
45 > readonly sessionUri: string;
46 > /**
47 > * The branch to diff against. Typically the worktree's start-point
48 > * branch (for worktree sessions) or the repository's default branch.
49 > * When undefined or unresolvable, the diff is taken against `HEAD`,
50 > * which surfaces uncommitted work but no committed-on-branch work.
51 > */
52 > readonly baseBranch?: string;
53 > }
54 >
55 > /** Cheap repository facts used to decide whether a branch diff is safe to compute. */
56 > export interface IBranchDiffSafetyInfo {
57 > readonly hasVirtualFileSystem: boolean;
58 > readonly baselineCommitTimestamp: number | undefined;
59 > readonly commitCount: number | undefined;
60 > readonly workspaceFileCount: number;
61 > }
62 >
63 > /** A bounded unified-diff result. */
64 > export interface IDiffPatchResult {
65 > readonly patch: string | undefined;
66 > readonly tooLarge: boolean;
67 > }
68 >
69 > /** Options for {@link IAgentHostGitService.push}. */
70 > export interface IPushOptions {
71 > /** The branch or refspec to push. Defaults to the current branch. */
72 > readonly ref?: string;
73 > /** The remote to push to. Defaults to `origin`. */
74 > readonly remote?: string;
75 > /**
76 > * When true, the push uses `-u` so the pushed branch tracks the remote
77 > * branch for subsequent fetch/push commands.
78 > */
79 > readonly setUpstream?: boolean;
80 > }
81 >
82 > /** Options for {@link IAgentHostGitService.pull}. */
83 > export interface IPullOptions {
84 > /** The branch or ref to pull. Defaults to the configured upstream. */
85 > readonly ref?: string;
86 > /** The remote to pull from. Defaults to `origin`. */
87 > readonly remote?: string;
88 > /** When true, local commits are rebased onto the fetched ref (`-r`) instead of merged. */
89 > readonly rebase?: boolean;
90 > }
91 >
92 > export const IAgentHostGitService = createDecorator<IAgentHostGitService>('agentHostGitService');
93 >
94 > export interface IRefQuery {
95 > readonly count?: number;
96 > readonly pattern?: string | string[];
97 > readonly sort?: 'alphabetically' | 'committerdate' | 'creatordate';
98 > }
99 >
100 > export type Branch = IBranch | IRemoteBranch;
101 > export type GitRef = IBranch | IRemoteBranch | ITag;
102 >
103 > export const enum GitRefType {
104 > Head,
105 > RemoteHead,
106 > DetachedHead,
107 > Tag
108 > }
109 >
110 > export interface IBranch {
111 > readonly ref: string;
112 > readonly name: string;
113 > readonly upstream?: {
114 > readonly ref: string;
115 > readonly name: string;
116 > readonly remote: string;
117 > };
118 > readonly kind: GitRefType.Head;
119 > }
120 >
121 > export interface IRemoteBranch {
122 > readonly ref: string;
123 > readonly name: string;
124 > readonly remote: string;
125 > readonly kind: GitRefType.RemoteHead;
126 > }
127 >
128 > export interface ITag {
129 > readonly ref: string;
130 > readonly name: string;
131 > readonly kind: GitRefType.Tag;
132 > }
133 >
134 > export interface IDetachedHead {
135 > readonly name: string;
136 > readonly kind: GitRefType.DetachedHead;
137 > }
138 >
139 > export interface IDefaultBranch {
140 > readonly name: string;
141 > readonly startPoint: string;
142 > }
143 >
144 > export interface IAgentHostGitService {
145 > readonly _serviceBrand: undefined;
146 > getCurrentBranch(workingDirectory: URI): Promise<string | undefined>;
147 > getDefaultBranch(workingDirectory: URI): Promise<IDefaultBranch | undefined>;
148 > getRefs(workingDirectory: URI, query?: IRefQuery): Promise<GitRef[]>;
149 > getBranches(workingDirectory: URI, query?: IRefQuery): Promise<Branch[]>;
150 > getBranch(workingDirectory: URI, name: string): Promise<Branch | undefined>;
151 > getRepositoryRoot(workingDirectory: URI): Promise<URI | undefined>;
152 > getWorktreeRoots(workingDirectory: URI): Promise<URI[]>;
153 > addWorktree(repositoryRoot: URI, worktree: URI, branchName: string, startPoint: string): Promise<void>;
154 > copyWorktreeIncludeFiles(repositoryRoot: URI, worktree: URI, globs: readonly string[]): Promise<void>;
155 > /**
156 > * Adds a worktree for an existing branch (no `-b`). Used when restoring
157 > * a worktree whose branch was preserved (e.g. unarchiving a session
158 > * whose worktree was previously cleaned up on archive).
159 > */
160 > addExistingWorktree(repositoryRoot: URI, worktree: URI, branchName: string): Promise<void>;
161 > removeWorktree(repositoryRoot: URI, worktree: URI): Promise<void>;
162 > /**
163 > * Returns true when the named branch exists in the repository
164 > * (`refs/heads/<branchName>` resolves). Used by archive cleanup to
165 > * confirm the branch is preserved before deleting the worktree, and by
166 > * the unarchive path to confirm the branch is still around before
167 > * recreating the worktree.
168 > */
169 > branchExists(repositoryRoot: URI, branchName: string): Promise<boolean>;
170 > /**
171 > * Returns true when the working tree has any tracked, staged, or
172 > * untracked changes. Used by archive cleanup to skip removing a
173 > * worktree that still contains uncommitted work.
174 > */
175 > hasUncommittedChanges(workingDirectory: URI): Promise<boolean>;
176 >
177 > /**
178 > * Stages and commits all tracked, staged, and untracked changes in the
179 > * working tree. Mirrors the Copilot CLI session PR path, which commits
180 > * uncommitted work before creating a pull request.
181 > */
182 > commitAll(workingDirectory: URI, message: string): Promise<void>;
183 >
184 > /**
185 > * Restores files in the working tree via `git restore`. When
186 > * {@link options.staged} is true, restores the index instead of the
187 > * working tree. When {@link options.ref} is provided, the contents are
188 > * taken from that ref (`--source`). An empty {@link paths} array
189 > * restores everything (`.`).
190 > */
191 > restore(workingDirectory: URI, paths: readonly string[], options?: { readonly staged?: boolean; readonly ref?: string }): Promise<void>;
192 >
193 > /**
194 > * Returns true when the named branch has an upstream tracking ref
195 > * (i.e. `<branch>@{upstream}` resolves). Used before {@link push}
196 > * to decide whether `--set-upstream` is needed.
197 > */
198 > hasUpstream(workingDirectory: URI, branchName: string): Promise<boolean>;
199 >
200 > /**
201 > * Fetches the latest changes from the remote (`origin` unless
202 > * {@link IPullOptions.remote} overrides it) and integrates them into the
203 > * current branch. When {@link IPullOptions.rebase} is true, local commits
204 > * are rebased onto the fetched ref instead of merged. When
205 > * {@link IPullOptions.ref} is provided, that ref is pulled instead of the
206 > * branch's configured upstream.
207 > */
208 > pull(workingDirectory: URI, options?: IPullOptions): Promise<void>;
209 >
210 > /**
211 > * Pushes the current branch (or {@link IPushOptions.ref}) to the remote
212 > * (`origin` unless {@link IPushOptions.remote} overrides it). When
213 > * {@link IPushOptions.setUpstream} is true, the push uses `-u` so
214 > * subsequent fetch/push commands track the remote branch.
215 > */
216 > push(workingDirectory: URI, options?: IPushOptions): Promise<void>;
217 >
218 > /**
219 > * Computes the {@link ISessionGitState} for the working directory by
220 > * shelling out to `git`. Returns undefined if the directory is not a
221 > * git work tree. Called on session open and after each turn completes
222 > * so the UI always reflects current branch/remote/change state.
223 > */
224 > getSessionGitState(workingDirectory: URI): Promise<ISessionGitState | undefined>;
225 > /** Returns fetch remote URLs with the preferred remote, then `origin`, first. */
226 > getFetchRemoteUrls(workingDirectory: URI, preferredRemote?: string): Promise<readonly string[] | undefined>;
227 > /** Returns repo-relative untracked file paths. */
228 > getUntrackedPaths(workingDirectory: URI): Promise<readonly string[] | undefined>;
229 >
230 > /**
231 > * Computes per-file diffs for the session by shelling out to `git
232 > * diff --raw --numstat --diff-filter=ADMR -z` against the merge base of
233 > * the current branch and {@link IComputeSessionFileDiffsOptions.baseBranch}
234 > * (or `HEAD` if no base branch is available). When the working tree has
235 > * untracked files, the diff is computed via a temp index so the
236 > * untracked content is included.
237 > *
238 > * Returns `undefined` when {@link workingDirectory} is not a git work
239 > * tree, so callers can fall back to other diff sources.
240 > *
241 > * Each returned {@link ISessionFileDiff} has its `before.content` set to
242 > * a `git-blob:` URI ({@link buildGitBlobUri}); `after.content` is a
243 > * `file:` URI on the working-tree path. Adds and deletes drop the
244 > * missing side.
245 > */
246 > computeSessionFileDiffs(workingDirectory: URI, options: IComputeSessionFileDiffsOptions): Promise<readonly ISessionFileDiff[] | undefined>;
247 >
248 > /**
249 > * Resolves the commit-ish the **Branch Changes** baseline is measured from:
250 > * the merge-base of `HEAD` and `baseBranch` (preferring the
251 > * `origin/<baseBranch>` remote-tracking ref when it exists), falling back to
252 > * `HEAD`, then to the empty-tree object for a repo with no commits. Returns
253 > * `undefined` only when {@link workingDirectory} is not a git work tree.
254 > *
255 > * Shared by {@link computeSessionFileDiffs} (which anchors the Branch Changes
256 > * diff here) and the review service, so both agree on the exact baseline.
257 > */
258 > resolveBranchBaselineCommit(workingDirectory: URI, baseBranch?: string): Promise<string | undefined>;
259 >
260 > /**
261 > * Reads a single git blob via `git show <ref>:<repoRelativePath>` from
262 > * the given working directory. Returns `undefined` when the blob does
263 > * not exist or the directory is not a git work tree.
264 > */
265 > showBlob(workingDirectory: URI, ref: string, repoRelativePath: string): Promise<VSBuffer | undefined>;
266 >
267 > // ---- Checkpoint plumbing (used by IAgentHostCheckpointService) -------
268 >
269 > /**
270 > * Captures the current working tree (including untracked files) as a
271 > * tree object, returning the tree OID. Uses a throwaway `GIT_INDEX_FILE`
272 > * so the user's real index is untouched. Returns `undefined` when the
273 > * directory is not a git work tree.
274 > */
275 > captureWorkingTreeAsTree(workingDirectory: URI): Promise<string | undefined>;
276 >
277 > /**
278 > * Creates a commit object from a tree (optionally chained to a parent)
279 > * and returns its OID. Does NOT update any ref.
280 > */
281 > commitTree(repositoryRoot: URI, treeOid: string, parentOid: string | undefined, message: string): Promise<string | undefined>;
282 >
283 > /**
284 > * Updates a ref to point at `newOid`. Creates the ref if missing.
285 > */
286 > updateRef(repositoryRoot: URI, ref: string, newOid: string): Promise<void>;
287 >
288 > /**
289 > * Batch-deletes the given refs via `git update-ref --stdin -z`.
290 > * Missing refs are tolerated.
291 > */
292 > deleteRefs(repositoryRoot: URI, refs: readonly string[]): Promise<void>;
293 >
294 > /**
295 > * Resolves a ref/object expression to its OID, e.g. `revParse(repo, 'refs/agents/abc/...')`
296 > * or `revParse(repo, '<commit>^{tree}')`. Returns `undefined` when the
297 > * ref does not exist.
298 > */
299 > revParse(repositoryRoot: URI, expression: string): Promise<string | undefined>;
300 >
301 > /**
302 > * Builds a new tree from `baseTreeOid` in which the single repo-relative
303 > * `path` is replaced by its content (blob + mode) from `sourceTreeOid`, or
304 > * removed when the path is absent in `sourceTreeOid`. All other paths are
305 > * copied verbatim from `baseTreeOid`. Uses a throwaway `GIT_INDEX_FILE` so
306 > * the user's real index is untouched. Returns the new tree OID, or
307 > * `undefined` on git failure.
308 > *
309 > * File-level building block for review (see `IAgentHostReviewService`): to
310 > * mark a file reviewed, overlay it from the working-tree snapshot tree; to
311 > * unmark, overlay it from the baseline tree.
312 > */
313 > overlayPathIntoTree(repositoryRoot: URI, baseTreeOid: string, path: string, sourceTreeOid: string): Promise<string | undefined>;
314 >
315 > /**
316 > * Returns the repo-relative paths that differ between two tree-ish (commit
317 > * or tree) objects via `git diff --name-only --no-renames -z`. Rename
318 > * detection is off so a rename shows as delete(old) + add(new). Returns
319 > * `undefined` on git failure (e.g. not a git work tree).
320 > */
321 > diffTreePaths(repositoryRoot: URI, fromTreeish: string, toTreeish: string): Promise<string[] | undefined>;
322 >
323 > /**
324 > * Computes per-file diffs between two refs (typically two consecutive
325 > * checkpoint refs) by shelling out to
326 > * `git diff --raw --numstat --diff-filter=ADMR -z <fromRef> <toRef>`.
327 > * Returns the same {@link ISessionFileDiff} shape as
328 > * {@link computeSessionFileDiffs}: `before.content` is a `git-blob:`
329 > * URI anchored on `fromRef`, `after.content` is a `git-blob:` URI
330 > * anchored on `toRef`. Returns `undefined` on git failure.
331 > *
332 > * Used by the changeset service to materialise per-turn diffs from
333 > * checkpoint refs when they are available — that path captures
334 > * terminal-tool edits the FileEditTracker pipeline misses.
335 > */
336 > computeFileDiffsBetweenRefs(workingDirectory: URI, options: { readonly sessionUri: string; readonly fromRef: string; readonly toRef: string }): Promise<readonly ISessionFileDiff[] | undefined>;
337 > /** Reads bounded facts needed before computing an expensive branch diff. */
338 > getBranchDiffSafetyInfo(workingDirectory: URI, baselineCommit: string): Promise<IBranchDiffSafetyInfo | undefined>;
339 > /** Computes a unified patch for paths between immutable tree-ish values. */
340 > getDiffPatchBetweenRefs(workingDirectory: URI, options: { readonly fromRef: string; readonly toRef: string; readonly paths: readonly string[]; readonly maxBuffer: number }): Promise<IDiffPatchResult | undefined>;
341 > }
342 >
343 function getCommonBranchPriority(branch: string): number {
344 if (branch === 'main') {
350 return 2;
351 }
353 > export function getBranchCompletions(branches: readonly string[], options?: { readonly query?: string; readonly limit?: number }): string[] {
354 const normalizedQuery = options?.query?.toLowerCase();
355 const filtered = normalizedQuery
src/vs/platform/agentHost/node/agentHostChangesetService.ts 340 covered LOC · 54 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostChangesetService.ts
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 > import { disposableTimeout, SequencerByKey } from '../../../base/common/async.js';
7 > import { toErrorMessage } from '../../../base/common/errorMessage.js';
8 > import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js';
9 > import { URI } from '../../../base/common/uri.js';
10 > import { ILogService } from '../../log/common/log.js';
11 > import {
12 > buildBranchChangesetUri,
13 > buildCompareTurnsChangesetUri,
14 > buildSessionChangesetUri,
15 > buildTurnChangesetUri,
16 > buildUncommittedChangesetUri,
17 > parseChangesetUri,
18 > ChangesetKind,
19 > buildDefaultChangesetCatalog,
20 > } from '../common/changesetUri.js';
21 > import { IDiffComputeService } from '../common/diffComputeService.js';
22 > import { ISessionDatabase, ISessionDataService } from '../common/sessionDataService.js';
23 > import type { ChangesetState, ChangesSummary } from '../common/state/protocol/state.js';
24 > import { ActionType } from '../common/state/sessionActions.js';
25 > import {
26 > ChangesetStatus,
27 > type ChangesetFile,
28 > type ISessionFileDiff,
29 > type URI as ProtocolURI,
30 > readSessionGitState,
31 > isDefaultChatUri,
32 > SessionLifecycle,
33 > } from '../common/state/sessionState.js';
34 > import { AgentHostStateManager, IAgentHostStateManager } from './agentHostStateManager.js';
35 > import { IAgentConfigurationService } from './agentConfigurationService.js';
36 > import { IAgentHostGitService, META_DIFF_BASE_BRANCH, resolveDiffBaseBranchName } from '../common/agentHostGitService.js';
37 > import { IAgentHostCheckpointService } from '../common/agentHostCheckpointService.js';
38 > import { NodeWorkerDiffComputeService } from './diffComputeService.js';
39 > import { computeSessionDiffs, computeTurnDiffs, computeUnionedDiffs, type IIncrementalDiffOptions, type ISessionDiffSource } from './sessionDiffAggregator.js';
40 > import { META_CHECKPOINT_WORKING_DIR } from './agentHostCheckpointService.js';
41 > import { IAgentHostChangesetService, IPersistedChangesetMetadata, IRestoredChangesetDiffs, CHANGESET_DB_METADATA_KEYS, META_CHANGES_SUMMARY, META_CHANGESET_BRANCH, META_CHANGESET_SESSION, META_LEGACY_DIFFS, StaticChangesetKind } from '../common/agentHostChangesetService.js';
42 > import { IAgentHostChangesetSubscriptionService } from '../common/agentHostChangesetSubscriptionService.js';
43 > import { IAgentHostChangesetOperationService } from '../common/agentHostChangesetOperationService.js';
44 > import { IAgentHostReviewService } from '../common/agentHostReviewService.js';
45 > import { relativePath } from '../../../base/common/resources.js';
46 >
47 function staticChangesetUri(session: ProtocolURI, kind: StaticChangesetKind): ProtocolURI {
48 return kind === 'branch'
50 : buildSessionChangesetUri(session);
51 }
53 function persistKeyFor(kind: StaticChangesetKind): string {
54 return kind === 'branch'
56 : META_CHANGESET_SESSION;
57 }
59 > /**
60 > * Sums the per-file diff counts into the {@link ChangesSummary} shape
61 > * that lives on `summary.changes`. Returns `undefined` for an undefined
62 > * input so callers can distinguish "no data yet" from "data, zero changes".
63 > */
64 function summariseDiffs(diffs: readonly ISessionFileDiff[] | undefined): ChangesSummary | undefined {
65 if (!diffs) {
74 return { additions, deletions, files: diffs.length };
75 }
77 > /**
78 > * Derives the `summary.changes` aggregate for an unopened session from
79 > * the ready live {@link ChangesetState} of the catalogue entry whose
80 > * `changeKind === 'session'` — typically because a previous
81 > * `restoreStaticChangeset` warmed the cache before the session itself
82 > * was attached.
83 > *
84 > * Returns `undefined` when no live session-wide state is ready, so
85 > * `listSessions` leaves the `changes` field unset for sessions without
86 > * usable counts — preserving the long-standing contract that unopened
87 > * sessions without live or persisted data advertise no aggregate.
88 > *
89 > * Only the `changeKind: 'session'` entry feeds the summary; other kinds
90 > * (`'uncommitted'`, `'turn'`, `'compare-turns'`) describe slices, not
91 > * the session-level footprint. The static catalogue itself (built by
92 > * {@link buildDefaultChangesetCatalog}) is independent of counts and
93 > * is seeded once at session creation.
94 > */
95 function computeChangesSummaryFromLiveState(
96 session: ChangesetState | undefined,
99 return summariseDiffs(sessionDiffs);
100 }
102 > /**
103 > * Derives the `summary.changes` aggregate for an unopened session from
104 > * parsed persisted diffs for the `changeKind: 'session'` catalogue
105 > * entry. Returns `undefined` when the session-wide blob is absent so
106 > * malformed metadata leaves `summary.changes` unset.
107 > */
108 function computeChangesSummaryFromPersistedDiffs(
109 sessionDiffs: readonly ISessionFileDiff[] | undefined,
111 return summariseDiffs(sessionDiffs);
112 }
114 > /**
115 > * Parses a JSON-serialised {@link ISessionFileDiff}[] blob from session
116 > * metadata. Returns `undefined` for missing or malformed input, logging a
117 > * warning that names `sessionUri` and `kind` so operators can correlate the
118 > * failure with a specific session/changeset slot. Never throws.
119 > */
120 function tryParsePersistedDiffs(raw: string | undefined, sessionUri: string, kind: string, log: ILogService): ISessionFileDiff[] | undefined {
121 if (!raw) {
129 }
130 }
132 > export class AgentHostChangesetService extends Disposable implements IAgentHostChangesetService {
133 > declare readonly _serviceBrand: undefined;
134 >
135 > /** Shared diff compute service for calculating line-level diffs in a worker thread. */
136 > private readonly _diffComputeService: IDiffComputeService;
137 > /** Serializes per-session diff computations to avoid races with stale previousDiffs. */
138 > private readonly _diffComputationSequencer = new SequencerByKey<string>();
139 > /** Per-session debounce timers for mid-turn diff computation. */
140 > private readonly _debouncedDiffTimers = this._register(new DisposableMap<string>());
141 > /** Per-`(session, turnId)` debounce timers for mid-turn per-turn changeset recomputation. */
142 > private readonly _perTurnDebouncedDiffTimers = this._register(new DisposableMap<string>());
143 > private readonly _activeStaticComputes = new Set<ProtocolURI>();
144 > private static readonly _DIFF_DEBOUNCE_MS = 5000;
145 >
146 > /**
147 > * Sessions whose static changeset refresh was requested before the
148 > * working directory was known (provisional / not-yet-materialized
149 > * sessions). Drained from {@link onWorkingDirectoryAvailable} once the
150 > * working directory is set, which recomputes every changeset still
151 > * subscribed for the session.
152 > *
153 > * Firing a refresh before the working directory is known would compute
154 > * against a missing directory and the git path would bail, so we defer
155 > * instead and re-run once materialization / restore populates it.
156 > */
157 > private readonly _pendingMaterialization = new Set<ProtocolURI>();
158 >
159 > constructor(
160 > @IAgentHostStateManager private readonly _stateManager: AgentHostStateManager, agentHostChangesetService.ts
161 > @ILogService private readonly _logService: ILogService,
162 > @ISessionDataService private readonly _sessionDataService: ISessionDataService,
163 > @IAgentHostGitService private readonly _gitService: IAgentHostGitService,
164 > @IAgentHostCheckpointService private readonly _checkpointService: IAgentHostCheckpointService,
165 > @IAgentConfigurationService private readonly _configurationService: IAgentConfigurationService,
166 > @IAgentHostChangesetOperationService private readonly _changesetOperationService: IAgentHostChangesetOperationService,
167 > @IAgentHostChangesetSubscriptionService private readonly _changesetSubscriptions: IAgentHostChangesetSubscriptionService,
168 > @IAgentHostReviewService private readonly _reviewService: IAgentHostReviewService,
169 > ) {
170 > super();
171 > this._diffComputeService = this._register(new NodeWorkerDiffComputeService(this._logService));
172 > }
174 > /**
175 > * Returns true when at least one client is subscribed to `changeset`
176 > * under `session`.
177 > */
178 > private _hasSubscription(session: ProtocolURI, changeset: ProtocolURI): boolean {
179 return this._changesetSubscriptions.getSessionSubscriptions(session).has(changeset);
180 }
182 > private _hasWorkingDirectory(session: ProtocolURI): boolean {
183 return !!this._configurationService.getEffectiveWorkingDirectory(session);
184 }
186 > registerStaticChangesets(session: ProtocolURI): void {
187 this._stateManager.registerChangeset(buildBranchChangesetUri(session));
188 this._stateManager.registerChangeset(buildUncommittedChangesetUri(session));
189 this._stateManager.registerChangeset(buildSessionChangesetUri(session));
190 }
192 > restoreStaticChangeset(session: ProtocolURI, kind: StaticChangesetKind, diffs: readonly ISessionFileDiff[]): void {
193 const changesetUri = this._stateManager.registerChangeset(staticChangesetUri(session, kind));
194 this._publishChangesetDiffs(session, changesetUri, diffs);
195 }
197 > parsePersistedStaticChangesets(sessionUri: ProtocolURI, metadata: IPersistedChangesetMetadata): IRestoredChangesetDiffs {
198 const persistedBranch = tryParsePersistedDiffs(metadata.branchRaw, sessionUri, 'branch', this._logService);
199
205 return { branch: persistedBranch, session: persistedSession };
206 }
208 > applyPersistedStaticChangesets(sessionUri: ProtocolURI, diffs: IRestoredChangesetDiffs): void {
209 // `seedIfEmpty`: only reseed persisted diffs when the matching live
210 // changeset state is absent or empty. Live state (e.g. from a prior
215 this._seedIfEmpty(sessionUri, 'session', diffs.session);
216 }
218 > restorePersistedStaticChangesets(sessionUri: ProtocolURI, metadata: IPersistedChangesetMetadata): IRestoredChangesetDiffs {
219 const parsed = this.parsePersistedStaticChangesets(sessionUri, metadata);
220 this.applyPersistedStaticChangesets(sessionUri, parsed);
221 return parsed;
222 }
224 > persistChangesSummary(sessionUri: ProtocolURI, summary: ChangesSummary): void {
225 this._persistSessionFlag(sessionUri, META_CHANGES_SUMMARY, JSON.stringify(summary));
226 }
228 > getListMetadataKeys(sessionUri: ProtocolURI): Record<string, true> | undefined {
229 // Fast path: a live `summary.changes` (loaded session) or a ready live
230 // `changeKind: 'session'` changeset state (registered but not-yet-
241 return CHANGESET_DB_METADATA_KEYS;
242 }
244 > computeListEntryChanges(sessionUri: ProtocolURI, metadata: Record<string, string | undefined>): ChangesSummary | undefined {
245 // Loaded session: the caller has already projected
246 // `state.summary.changes` onto the entry. Nothing to overlay.
294 return undefined;
295 }
297 > isStaticChangesetComputeActive(changesetUri: ProtocolURI): boolean {
298 return this._activeStaticComputes.has(changesetUri);
299 }
301 > private _seedIfEmpty(session: ProtocolURI, kind: StaticChangesetKind, diffs: readonly ISessionFileDiff[] | undefined): void {
302 if (!diffs) {
303 return;
309 this.restoreStaticChangeset(session, kind, diffs);
310 }
312 > refreshChangesetCatalog(session: ProtocolURI): void {
313 const state = this._stateManager.getSessionState(session);
314 if (!state || state?.lifecycle === SessionLifecycle.CreationFailed) {
319 this._stateManager.setSessionChangesets(session, changesets);
320 }
322 > refreshBranchChangeset(session: ProtocolURI): void {
323 if (!this._hasWorkingDirectory(session)) {
324 this._pendingMaterialization.add(session);
327 this._scheduleStaticRecompute(session, 'branch', undefined, this._markStaticChangesetComputing(session, 'branch'));
328 }
330 > refreshSessionChangeset(session: ProtocolURI): void {
331 if (!this._hasWorkingDirectory(session)) {
332 this._pendingMaterialization.add(session);
335 this._scheduleStaticRecompute(session, 'session', undefined, this._markStaticChangesetComputing(session, 'session'));
336 }
338 > /**
339 > * Drains static changeset refreshes that were deferred because the
340 > * session's working directory was not yet known. Called by the
341 > * coordinator once a session is materialized or restored. Recomputes
342 > * every changeset still subscribed for the session; subscriptions that
343 > * dropped while the working directory was unknown are naturally skipped.
344 > */
345 > onWorkingDirectoryAvailable(session: ProtocolURI): void {
346 if (this._pendingMaterialization.delete(session)) {
347 this.recomputeSubscribedChangesets(session);
348 }
349 }
351 > /**
352 > * Recomputes every changeset currently subscribed for `session`. Each
353 > * subscribed changeset is dispatched to its kind-specific recompute; the
354 > * recomputes self-defer when the working directory is still unknown.
355 > */
356 > recomputeSubscribedChangesets(session: ProtocolURI): void {
357 const subscriptions = this._changesetSubscriptions.getSessionSubscriptions(session);
358 if (subscriptions.size === 0) {
388 }
389 }
391 > /**
392 > * Forgets any deferred static changeset refreshes queued for a session
393 > * that is being disposed.
394 > */
395 > onSessionDisposed(session: ProtocolURI): void {
396 this._pendingMaterialization.delete(session);
397 }
399 > async computeTurnChangeset(session: ProtocolURI, turnId: string): Promise<ProtocolURI> {
400 const turnUri = this._stateManager.registerChangeset(buildTurnChangesetUri(session, turnId));
401 let ref: ReturnType<ISessionDataService['openDatabase']>;
431 return turnUri;
432 }
434 > async computeCompareTurnsChangeset(session: ProtocolURI, originalTurnId: string, modifiedTurnId: string): Promise<ProtocolURI> {
435 const compareUri = this._stateManager.registerChangeset(buildCompareTurnsChangesetUri(session, originalTurnId, modifiedTurnId));
436 let ref: ReturnType<ISessionDataService['openDatabase']>;
516 return compareUri;
517 }
519 > async computeUncommittedChangeset(session: ProtocolURI): Promise<ProtocolURI> {
520 const uncommittedUri = this._stateManager.registerChangeset(buildUncommittedChangesetUri(session));
521 if (!this._hasSubscription(session, uncommittedUri)) {
567 return uncommittedUri;
568 }
570 > private async _computeUncommittedDiffs(session: ProtocolURI): Promise<readonly ISessionFileDiff[] | undefined> {
571 const workingDirectory = this._stateManager.getSessionState(session)?.workingDirectories?.[0];
572 if (!workingDirectory) {
585 });
586 }
588 > private async _computeTurnDiffsPreferCheckpoint(session: ProtocolURI, db: ISessionDatabase, turnId: string): Promise<readonly ISessionFileDiff[]> {
589 const pair = await this._checkpointService.getTurnCheckpointPair(URI.parse(session), turnId);
590 if (pair && pair.parent !== pair.current) {
609 return computeTurnDiffs(session, db, this._diffComputeService, turnId);
610 }
612 > private async _resolveWorkingDirectory(db: ISessionDatabase): Promise<URI | undefined> {
613 // Checkpoint baseline writes `checkpoint.workingDir` alongside
614 // `checkpoint.baseRef`. We use that as the canonical working
618 return raw ? URI.parse(raw) : undefined;
619 }
621 > // ---- Lifecycle hooks invoked by AgentSideEffects -----------------------
622 >
623 > onToolCallEditsApplied(session: ProtocolURI, turnId: string): void {
624 this._scheduleDebouncedDiffComputation(session, turnId);
625 // Per-turn URIs have no catalogue chip aggregates, so skip the
631 }
632 }
634 > onTurnComplete(session: ProtocolURI, turnId: string | undefined): void {
635 // Ordering matters for cancellation: cancel any pending mid-turn
636 // debounces first so the final turn-complete computes supersede
653 this._scheduleStaticRecompute(session, 'session', turnId);
654 }
656 > onSessionTruncated(session: ProtocolURI): void {
657 // Turns were removed — recompute from scratch (no changedTurnId).
658 this._scheduleStaticRecompute(session, 'branch');
659 this._scheduleStaticRecompute(session, 'session');
660 }
662 > // ---- Internal compute pipeline -----------------------------------------
663 >
664 > /**
665 > * Schedules a debounced session-changeset recomputation. Uncommitted
666 > * recomputes ride the same turn-complete path; mid-turn debounce only
667 > * makes sense for the SDK-tracked session-wide diff (which sees fresh
668 > * `tool_complete` events between turn boundaries).
669 > */
670 > private _scheduleDebouncedDiffComputation(session: ProtocolURI, turnId: string): void {
671 this._debouncedDiffTimers.set(session, disposableTimeout(() => {
672 this._debouncedDiffTimers.deleteAndDispose(session);
675 }, AgentHostChangesetService._DIFF_DEBOUNCE_MS));
676 }
678 > /**
679 > * Cancels any pending debounced diff computation for a session.
680 > * Called at turn end before the final (non-debounced) computation.
681 > */
682 > private _cancelDebouncedDiffComputation(session: ProtocolURI): void {
683 this._debouncedDiffTimers.deleteAndDispose(session);
684 }
686 > /**
687 > * Schedules a debounced per-turn changeset recomputation. Mirrors
688 > * {@link _scheduleDebouncedDiffComputation} but uses a per-
689 > * `(session, turnId)` map key so a long-running per-turn compute
690 > * doesn't block the static session recompute path (and vice versa).
691 > */
692 > private _scheduleDebouncedTurnDiffComputation(session: ProtocolURI, turnId: string): void {
693 const key = `${session}\u0000${turnId}`;
694 this._perTurnDebouncedDiffTimers.set(key, disposableTimeout(() => {
697 }, AgentHostChangesetService._DIFF_DEBOUNCE_MS));
698 }
700 > /**
701 > * Cancels any pending debounced per-turn diff computation for a
702 > * `(session, turnId)`. Called at turn end before the final
703 > * (non-debounced) per-turn computation.
704 > */
705 > private _cancelDebouncedTurnDiffComputation(session: ProtocolURI, turnId: string): void {
706 this._perTurnDebouncedDiffTimers.deleteAndDispose(`${session}\u0000${turnId}`);
707 }
709 > /**
710 > * Queues a per-turn recompute on a per-`(session, turnId)` sequencer
711 > * key so back-to-back recomputes for the same turn serialise, but
712 > * recomputes for different turns (or for the static `session` /
713 > * `uncommitted` slots) run independently. Fire-and-forget — failures
714 > * are logged inside `computeTurnChangeset` and do not fail the turn.
715 > */
716 > private _scheduleTurnRecompute(session: ProtocolURI, turnId: string): void {
717 this._diffComputationSequencer.queue(`${session}\u0000turn\u0000${turnId}`, () => this.computeTurnChangeset(session, turnId).then(() => undefined));
718 }
720 > private _scheduleUncommittedRecompute(session: ProtocolURI): void {
721 this._diffComputationSequencer.queue(`${session}\u0000uncommitted`, () => this.computeUncommittedChangeset(session).then(() => undefined));
722 }
724 > /**
725 > * Schedules a static changeset (`uncommitted` or `session`) recompute,
726 > * serialised per-session so back-to-back triggers don't race against
727 > * stale `previousDiffs` reads. Fire-and-forget — failures are logged
728 > * but do not fail the turn.
729 > */
730 > private _scheduleStaticRecompute(session: ProtocolURI, kind: StaticChangesetKind, changedTurnId?: string, statusBeforeRefresh?: ChangesetStatus): void {
731 this._diffComputationSequencer.queue(`${session}\u0000${kind}`, () => this._doComputeStaticChangeset(session, kind, changedTurnId, statusBeforeRefresh));
732 }
734 > private _markStaticChangesetComputing(session: ProtocolURI, kind: StaticChangesetKind): ChangesetStatus | undefined {
735 const changesetUri = staticChangesetUri(session, kind);
736 this._stateManager.registerChangeset(changesetUri);
744 return status;
745 }
747 > private async _doComputeStaticChangeset(session: ProtocolURI, kind: StaticChangesetKind, changedTurnId?: string, statusBeforeRefresh?: ChangesetStatus): Promise<void> {
748 const changesetUri = staticChangesetUri(session, kind);
749 this._activeStaticComputes.add(changesetUri);
851 }
852 }
854 > /**
855 > * Refresh requests optimistically mark static changesets as Computing
856 > * while preserving their current files. Some refresh paths intentionally
857 > * do not publish a replacement file list (for example, uncommitted git
858 > * diff is temporarily unavailable), so restore the previous non-computing
859 > * status instead of leaving a stale cached snapshot stuck as Computing.
860 > */
861 > private _restoreStaticChangesetStatus(changesetUri: ProtocolURI, status: ChangesetStatus | undefined): void {
862 if (!status || status === ChangesetStatus.Computing) {
863 return;
868 });
869 }
871 > /**
872 > * Reads the previous diff list back out of the changeset state so the
873 > * incremental aggregator can avoid recomputing files that haven't
874 > * changed.
875 > */
876 > private _readPreviousChangesetDiffs(changesetUri: ProtocolURI): readonly ISessionFileDiff[] | undefined {
877 const state = this._stateManager.getChangesetState(changesetUri);
878 if (!state || state.files.length === 0) {
881 return state.files.map(f => f.edit);
882 }
884 > /**
885 > * Translates the new file list into a sequence of changeset/* actions
886 > * (fileSet, fileRemoved) and moves the changeset to `ready` once the
887 > * fresh file list has been applied.
888 > */
889 > private _publishChangesetDiffs(session: ProtocolURI, changesetUri: ProtocolURI, diffs: readonly ISessionFileDiff[], reviewed?: { readonly repoRoot: URI; readonly paths: ReadonlySet<string> }): void {
890 // Get the available operations for this changeset. This call assumes that at this point
891 // the git state of the session is up-to-date as it is being used to determine the available
930 }
931 }
933 > /**
934 > * Opens the databases for every non-default (peer) chat in a multi-chat
935 > * session. Each peer chat records its file edits into its own database
936 > * keyed by the chat URI, so the session changeset must union those
937 > * databases with the session DB. Returns an empty array for single-chat
938 > * sessions. Callers MUST dispose every returned `ref`.
939 > */
940 > private _openPeerChatSources(session: ProtocolURI): { sessionUri: ProtocolURI; ref: ReturnType<ISessionDataService['openDatabase']> }[] {
941 const chats = this._stateManager.getSessionState(session)?.chats ?? [];
942 const sources: { sessionUri: ProtocolURI; ref: ReturnType<ISessionDataService['openDatabase']> }[] = [];
954 return sources;
955 }
957 > /**
958 > * Returns the turn id whose checkpoint best represents the latest state of
959 > * the session's shared working tree. For single-chat sessions this is the
960 > * default chat's last turn. For multi-chat sessions it is the last turn of
961 > * the most-recently-modified chat (peer-chat turn checkpoints are stored
962 > * under the session URI keyed by their turn id). Returns `undefined` when
963 > * no chat has any turns.
964 > */
965 > private _latestTurnIdAcrossChats(session: ProtocolURI): string | undefined {
966 const sessionState = this._stateManager.getSessionState(session);
967 if (!sessionState) {
988 return bestTurnId;
989 }
991 > /**
992 > * Computes diffs for a static changeset by shelling out to git.
993 > * Returns the diff list when the session has a working directory and
994 > * that directory is a git work tree; returns `undefined` otherwise so
995 > * the caller can fall back to the edit-tracker aggregator (for
996 > * `kind: 'session'`) or preserve cached state (for `kind: 'branch'`).
997 > *
998 > * For `kind: 'session'` the diff is computed between the baseline
999 > * checkpoint ref and the latest turn checkpoint ref.
1000 > * For `kind: 'branch'` the diff is computed against the merge-base
1001 > * with {@link META_DIFF_BASE_BRANCH} when one is set; without a base
1002 > * branch git falls back to `HEAD`.
1003 > */
1004 > private async _tryComputeGitDiffs(session: ProtocolURI, db: ISessionDatabase, kind: StaticChangesetKind): Promise<readonly ISessionFileDiff[] | undefined> {
1005 const workingDirectory = this._stateManager.getSessionState(session)?.workingDirectories?.[0];
1006 if (!workingDirectory) {
1060 }
1061 }
1063 > /**
1064 > * Resolves the Branch Changes base branch, reused by the diff computation
1065 > * and the review-status lookup so both are keyed on the same baseline.
1066 > */
1067 > private async _resolveBranchBaseBranch(session: ProtocolURI, db: ISessionDatabase): Promise<string | undefined> {
1068 const persistedBaseBranch = await db.getMetadata(META_DIFF_BASE_BRANCH);
1069 const gitStateBaseBranch = readSessionGitState(this._stateManager.getSessionState(session)?._meta)?.baseBranchName;
1073 return resolveDiffBaseBranchName(persistedBaseBranch, gitStateBaseBranch);
1074 }
1076 > /**
1077 > * Computes the reviewed-paths overlay for the Branch changeset: the
1078 > * repository root (used to key file ids to repo-relative paths) and the set
1079 > * of reviewed repo-relative paths. Returns `undefined` when the session has
1080 > * no git working directory (review status is then simply omitted).
1081 > */
1082 > private async _computeReviewedInfo(session: ProtocolURI, db: ISessionDatabase): Promise<{ readonly repoRoot: URI; readonly paths: ReadonlySet<string> } | undefined> {
1083 const workingDirectory = this._stateManager.getSessionState(session)?.workingDirectories?.[0];
1084 if (!workingDirectory) {
1103 return { repoRoot, paths };
1104 }
1106 > /**
1107 > * Persists a session metadata key/value pair to the session database.
1108 > * Counterpart in `agentSideEffects.ts` (`AgentSideEffects._persistSessionFlag`):
1109 > * keep both copies in sync if the signature changes. Duplicated rather
1110 > * than lifted because the two consumers persist disjoint metadata
1111 > * (changeset diffs here vs. customTitle / isRead / isArchived /
1112 > * configValues there) and a shared util would only have two callers.
1113 > */
1114 > private _persistSessionFlag(session: ProtocolURI, key: string, value: string): void {
1115 const ref = this._sessionDataService.openDatabase(URI.parse(session));
1116 ref.object.setMetadata(key, value).catch(err => {
src/vs/base/common/map.ts 338 covered LOC · 106 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- map.ts
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 > import { URI } from './uri.js';
7 >
8 > export function getOrSet<K, V>(map: Map<K, V>, key: K, value: V): V {
9 let result = map.get(key);
10 if (result === undefined) {
15 return result;
16 }
17 > map.ts
18 > export function mapToString<K, V>(map: Map<K, V>): string {
19 const entries: string[] = [];
20 map.forEach((value, key) => {
24 return `Map(${map.size}) {${entries.join(', ')}}`;
25 }
26 > map.ts
27 > export function setToString<K>(set: Set<K>): string {
28 const entries: K[] = [];
29 set.forEach(value => {
33 return `Set(${set.size}) {${entries.join(', ')}}`;
34 }
35 > map.ts
36 > interface ResourceMapKeyFn {
37 > (resource: URI): string;
38 > }
39 >
40 > class ResourceMapEntry<T> {
41 > constructor(readonly uri: URI, readonly value: T) { }
42 > }
43 >
44 > function isEntries<T>(arg: ResourceMap<T> | ResourceMapKeyFn | readonly (readonly [URI, T])[] | undefined): arg is readonly (readonly [URI, T])[] { map.ts
45 > return Array.isArray(arg);
46 > }
47 > map.ts
48 > export class ResourceMap<T> implements Map<URI, T> {
49 >
50 > private static readonly defaultToKey = (resource: URI) => resource.toString();
51 >
52 > readonly [Symbol.toStringTag] = 'ResourceMap';
53 >
54 > private readonly map: Map<string, ResourceMapEntry<T>>;
55 > private readonly toKey: ResourceMapKeyFn;
56 >
57 > /**
58 > *
59 > * @param toKey Custom uri identity function, e.g use an existing `IExtUri#getComparison`-util
60 > */
61 > constructor(toKey?: ResourceMapKeyFn);
62 >
63 > /**
64 > *
65 > * @param other Another resource which this maps is created from
66 > * @param toKey Custom uri identity function, e.g use an existing `IExtUri#getComparison`-util
67 > */
68 > constructor(other?: ResourceMap<T>, toKey?: ResourceMapKeyFn);
69 >
70 > /**
71 > *
72 > * @param other Another resource which this maps is created from
73 > * @param toKey Custom uri identity function, e.g use an existing `IExtUri#getComparison`-util
74 > */
75 > constructor(entries?: readonly (readonly [URI, T])[], toKey?: ResourceMapKeyFn);
76 >
77 > constructor(arg?: ResourceMap<T> | ResourceMapKeyFn | readonly (readonly [URI, T])[], toKey?: ResourceMapKeyFn) {
78 > if (arg instanceof ResourceMap) { map.ts
79 this.map = new Map(arg.map);
80 this.toKey = toKey ?? ResourceMap.defaultToKey;
81 > } else if (isEntries(arg)) { map.ts
82 this.map = new Map();
83 this.toKey = toKey ?? ResourceMap.defaultToKey;
86 this.set(resource, value);
87 }
88 > } else { map.ts
89 > this.map = new Map();
90 > this.toKey = arg ?? ResourceMap.defaultToKey;
91 > }
92 > }
93 > map.ts
94 > set(resource: URI, value: T): this {
95 this.map.set(this.toKey(resource), new ResourceMapEntry(resource, value));
96 return this;
97 }
98 > map.ts
99 > get(resource: URI): T | undefined {
100 return this.map.get(this.toKey(resource))?.value;
101 }
102 > map.ts
103 > has(resource: URI): boolean {
104 return this.map.has(this.toKey(resource));
105 }
106 > map.ts
107 > get size(): number {
108 > return this.map.size; map.ts
109 > }
110 > map.ts
111 > clear(): void {
112 this.map.clear();
113 }
114 > map.ts
115 > delete(resource: URI): boolean {
116 return this.map.delete(this.toKey(resource));
117 }
118 > map.ts
119 > forEach(clb: (value: T, key: URI, map: Map<URI, T>) => void, thisArg?: object): void {
120 if (typeof thisArg !== 'undefined') {
121 clb = clb.bind(thisArg);
125 }
126 }
127 > map.ts
128 > *values(): MapIterator<T> {
129 for (const entry of this.map.values()) {
130 yield entry.value;
131 }
132 }
133 > map.ts
134 > *keys(): MapIterator<URI> {
135 for (const entry of this.map.values()) {
136 yield entry.uri;
137 }
138 }
139 > map.ts
140 > *entries(): MapIterator<[URI, T]> {
141 for (const entry of this.map.values()) {
142 yield [entry.uri, entry.value];
143 }
144 }
145 > map.ts
146 > *[Symbol.iterator](): MapIterator<[URI, T]> {
147 for (const [, entry] of this.map) {
148 yield [entry.uri, entry.value];
149 }
150 }
151 > } map.ts
152 >
153 > export class ResourceSet implements Set<URI> {
154 >
155 > readonly [Symbol.toStringTag]: string = 'ResourceSet';
156 >
157 > private readonly _map: ResourceMap<URI>;
158 >
159 > constructor(toKey?: ResourceMapKeyFn);
160 > constructor(entries: readonly URI[], toKey?: ResourceMapKeyFn);
161 > constructor(entriesOrKey?: readonly URI[] | ResourceMapKeyFn, toKey?: ResourceMapKeyFn) {
162 if (!entriesOrKey || typeof entriesOrKey === 'function') {
163 this._map = new ResourceMap(entriesOrKey);
167 }
168 }
169 > map.ts
170 >
171 > get size(): number {
172 return this._map.size;
173 }
174 > map.ts
175 > add(value: URI): this {
176 this._map.set(value, value);
177 return this;
178 }
179 > map.ts
180 > clear(): void {
181 this._map.clear();
182 }
183 > map.ts
184 > delete(value: URI): boolean {
185 return this._map.delete(value);
186 }
187 > map.ts
188 > forEach(callbackfn: (value: URI, value2: URI, set: Set<URI>) => void, thisArg?: unknown): void {
189 this._map.forEach((_value, key) => callbackfn.call(thisArg, key, key, this));
190 }
191 > map.ts
192 > has(value: URI): boolean {
193 return this._map.has(value);
194 }
195 > map.ts
196 > entries(): SetIterator<[URI, URI]> {
197 return this._map.entries() as unknown as SetIterator<[URI, URI]>;
198 }
199 > map.ts
200 > keys(): SetIterator<URI> {
201 return this._map.keys() as unknown as SetIterator<URI>;
202 }
203 > map.ts
204 > values(): SetIterator<URI> {
205 return this._map.keys() as unknown as SetIterator<URI>;
206 }
207 > map.ts
208 > [Symbol.iterator](): SetIterator<URI> {
209 return this.keys();
210 }
211 > } map.ts
212 >
213 >
214 > interface Item<K, V> {
215 > previous: Item<K, V> | undefined;
216 > next: Item<K, V> | undefined;
217 > key: K;
218 > value: V;
219 > }
220 >
221 > export const enum Touch {
222 > None = 0,
223 > AsOld = 1,
224 > AsNew = 2
225 > }
226 >
227 > export class LinkedMap<K, V> implements Map<K, V> {
228 >
229 > readonly [Symbol.toStringTag] = 'LinkedMap';
230 >
231 > private _map: Map<K, Item<K, V>>;
232 > private _head: Item<K, V> | undefined;
233 > private _tail: Item<K, V> | undefined;
234 > private _size: number;
235 >
236 > private _state: number;
237 >
238 > constructor() {
239 > this._map = new Map<K, Item<K, V>>(); map.ts
240 > this._head = undefined;
241 > this._tail = undefined;
242 > this._size = 0;
243 > this._state = 0;
244 > }
245 > map.ts
246 > clear(): void {
247 this._map.clear();
248 this._head = undefined;
251 this._state++;
252 }
253 > map.ts
254 > isEmpty(): boolean {
255 return !this._head && !this._tail;
256 }
257 > map.ts
258 > get size(): number {
259 return this._size;
260 }
261 > map.ts
262 > get first(): V | undefined {
263 return this._head?.value;
264 }
265 > map.ts
266 > get last(): V | undefined {
267 return this._tail?.value;
268 }
269 > map.ts
270 > has(key: K): boolean {
271 return this._map.has(key);
272 }
273 > map.ts
274 > get(key: K, touch: Touch = Touch.None): V | undefined {
275 const item = this._map.get(key);
276 if (!item) {
282 return item.value;
283 }
284 > map.ts
285 > set(key: K, value: V, touch: Touch = Touch.None): this {
286 let item = this._map.get(key);
287 if (item) {
311 return this;
312 }
313 > map.ts
314 > delete(key: K): boolean {
315 return !!this.remove(key);
316 }
317 > map.ts
318 > remove(key: K): V | undefined {
319 const item = this._map.get(key);
320 if (!item) {
326 return item.value;
327 }
328 > map.ts
329 > shift(): V | undefined {
330 if (!this._head && !this._tail) {
331 return undefined;
340 return item.value;
341 }
342 > map.ts
343 > forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, thisArg?: unknown): void {
344 const state = this._state;
345 let current = this._head;
356 }
357 }
358 > map.ts
359 > keys(): MapIterator<K> {
360 const map = this;
361 const state = this._state;
381 return iterator;
382 }
383 > map.ts
384 > values(): MapIterator<V> {
385 const map = this;
386 const state = this._state;
406 return iterator;
407 }
408 > map.ts
409 > entries(): MapIterator<[K, V]> {
410 const map = this;
411 const state = this._state;
431 return iterator;
432 }
433 > map.ts
434 > [Symbol.iterator](): MapIterator<[K, V]> {
435 return this.entries();
436 }
437 > map.ts
438 > protected trimOld(newSize: number) {
439 if (newSize >= this.size) {
440 return;
458 this._state++;
459 }
460 > map.ts
461 > protected trimNew(newSize: number) {
462 if (newSize >= this.size) {
463 return;
481 this._state++;
482 }
483 > map.ts
484 > private addItemFirst(item: Item<K, V>): void {
485 // First time Insert
486 if (!this._head && !this._tail) {
495 this._state++;
496 }
497 > map.ts
498 > private addItemLast(item: Item<K, V>): void {
499 // First time Insert
500 if (!this._head && !this._tail) {
509 this._state++;
510 }
511 > map.ts
512 > private removeItem(item: Item<K, V>): void {
513 if (item === this._head && item === this._tail) {
514 this._head = undefined;
546 this._state++;
547 }
548 > map.ts
549 > private touch(item: Item<K, V>, touch: Touch): void {
550 if (!this._head || !this._tail) {
551 throw new Error('Invalid list');
608 }
609 }
610 > map.ts
611 > toJSON(): [K, V][] {
612 const data: [K, V][] = [];
613
618 return data;
619 }
620 > map.ts
621 > fromJSON(data: [K, V][]): void {
622 this.clear();
623
626 }
627 }
628 > } map.ts
629 >
630 > abstract class Cache<K, V> extends LinkedMap<K, V> {
631 >
632 > protected _limit: number;
633 > protected _ratio: number;
634 >
635 > constructor(limit: number, ratio: number = 1) {
636 > super(); map.ts
637 > this._limit = limit;
638 > this._ratio = Math.min(Math.max(0, ratio), 1);
639 > }
640 > map.ts
641 > get limit(): number {
642 return this._limit;
643 }
644 > map.ts
645 > set limit(limit: number) {
646 this._limit = limit;
647 this.checkTrim();
648 }
649 > map.ts
650 > get ratio(): number {
651 return this._ratio;
652 }
653 > map.ts
654 > set ratio(ratio: number) {
655 this._ratio = Math.min(Math.max(0, ratio), 1);
656 this.checkTrim();
657 }
658 > map.ts
659 > override get(key: K, touch: Touch = Touch.AsNew): V | undefined {
660 return super.get(key, touch);
661 }
662 > map.ts
663 > peek(key: K): V | undefined {
664 return super.get(key, Touch.None);
665 }
666 > map.ts
667 > override set(key: K, value: V): this {
668 super.set(key, value, Touch.AsNew);
669 return this;
670 }
671 > map.ts
672 > protected checkTrim() {
673 if (this.size > this._limit) {
674 this.trim(Math.round(this._limit * this._ratio));
675 }
676 }
677 > map.ts
678 > protected abstract trim(newSize: number): void;
679 > }
680 >
681 > export class LRUCache<K, V> extends Cache<K, V> {
682 >
683 > constructor(limit: number, ratio: number = 1) {
684 > super(limit, ratio); map.ts
685 > }
686 > map.ts
687 > protected override trim(newSize: number) {
688 this.trimOld(newSize);
689 }
690 > map.ts
691 > override set(key: K, value: V): this {
692 super.set(key, value);
693 this.checkTrim();
694 return this;
695 }
696 > } map.ts
697 >
698 > export class MRUCache<K, V> extends Cache<K, V> {
699 >
700 > constructor(limit: number, ratio: number = 1) {
701 super(limit, ratio);
702 }
703 > map.ts
704 > protected override trim(newSize: number) {
705 this.trimNew(newSize);
706 }
707 > map.ts
708 > override set(key: K, value: V): this {
709 if (this._limit <= this.size && !this.has(key)) {
710 this.trim(Math.round(this._limit * this._ratio) - 1);
714 return this;
715 }
716 > } map.ts
717 >
718 > export class CounterSet<T> {
719
720 private map = new Map<T, number>();
721 > map.ts
722 > add(value: T): CounterSet<T> {
723 this.map.set(value, (this.map.get(value) || 0) + 1);
724 return this;
725 }
726 > map.ts
727 > delete(value: T): boolean {
728 let counter = this.map.get(value) || 0;
729
742 return true;
743 }
744 > map.ts
745 > has(value: T): boolean {
746 return this.map.has(value);
747 }
748 > } map.ts
749 >
750 > /**
751 > * A map that allows access both by keys and values.
752 > * **NOTE**: values need to be unique.
753 > */
754 > export class BidirectionalMap<K, V> {
755 >
756 > private readonly _m1 = new Map<K, V>();
757 > private readonly _m2 = new Map<V, K>();
758 >
759 > constructor(entries?: readonly (readonly [K, V])[]) {
760 if (entries) {
761 for (const [key, value] of entries) {
764 }
765 }
766 > map.ts
767 > clear(): void {
768 this._m1.clear();
769 this._m2.clear();
770 }
771 > map.ts
772 > set(key: K, value: V): void {
773 this._m1.set(key, value);
774 this._m2.set(value, key);
775 }
776 > map.ts
777 > get(key: K): V | undefined {
778 return this._m1.get(key);
779 }
780 > map.ts
781 > getKey(value: V): K | undefined {
782 return this._m2.get(value);
783 }
784 > map.ts
785 > delete(key: K): boolean {
786 const value = this._m1.get(key);
787 if (value === undefined) {
792 return true;
793 }
794 > map.ts
795 > forEach(callbackfn: (value: V, key: K, map: BidirectionalMap<K, V>) => void, thisArg?: unknown): void {
796 this._m1.forEach((value, key) => {
797 callbackfn.call(thisArg, value, key, this);
798 });
799 }
800 > map.ts
801 > keys(): IterableIterator<K> {
802 return this._m1.keys();
803 }
804 > map.ts
805 > values(): IterableIterator<V> {
806 return this._m1.values();
807 }
808 > } map.ts
809 >
810 > export class SetMap<K, V> {
811
812 private map = new Map<K, Set<V>>();
813 > map.ts
814 > add(key: K, value: V): void {
815 let values = this.map.get(key);
816
822 values.add(value);
823 }
824 > map.ts
825 > delete(key: K, value: V): void {
826 const values = this.map.get(key);
827
836 }
837 }
838 > map.ts
839 > forEach(key: K, fn: (value: V) => void): void {
840 const values = this.map.get(key);
841
846 values.forEach(fn);
847 }
848 > map.ts
849 > get(key: K): ReadonlySet<V> {
850 const values = this.map.get(key);
851 if (!values) {
854 return values;
855 }
856 > } map.ts
857 >
858 > export function mapsStrictEqualIgnoreOrder(a: Map<unknown, unknown>, b: Map<unknown, unknown>): boolean {
859 if (a === b) {
860 return true;
879 return true;
880 }
881 > map.ts
882 > /**
883 > * A map that is addressable with an arbitrary number of keys. This is useful in high performance
884 > * scenarios where creating a composite key whenever the data is accessed is too expensive. For
885 > * example for a very hot function, constructing a string like `first-second-third` for every call
886 > * will cause a significant hit to performance.
887 > */
888 > export class NKeyMap<TValue, TKeys extends (string | boolean | number)[]> {
889 > private _data: Map<any, any> = new Map(); map.ts
890 > map.ts
891 > /**
892 > * Sets a value on the map. Note that unlike a standard `Map`, the first argument is the value.
893 > * This is because the spread operator is used for the keys and must be last..
894 > * @param value The value to set.
895 > * @param keys The keys for the value.
896 > */
897 > public set(value: TValue, ...keys: [...TKeys]): void {
898 let currentMap = this._data;
899 for (let i = 0; i < keys.length - 1; i++) {
907 currentMap.set(keys[keys.length - 1], value);
908 }
909 > map.ts
910 > public get(...keys: [...TKeys]): TValue | undefined {
911 let currentMap = this._data;
912 for (let i = 0; i < keys.length - 1; i++) {
919 return currentMap.get(keys[keys.length - 1]);
920 }
921 > map.ts
922 > public delete(...keys: [...TKeys]): boolean {
923 const maps: Map<any, any>[] = [this._data];
924 let currentMap = this._data;
939 return deleted;
940 }
941 > map.ts
942 > public deleteAll(...keys: Partial<TKeys>): boolean {
943 if (keys.length === 0) {
944 const hadData = this._data.size > 0;
964 return deleted;
965 }
966 > map.ts
967 > public clear(): void {
968 this._data.clear();
969 }
970 > map.ts
971 > public *getAll(...keys: Partial<TKeys>): IterableIterator<TValue> {
972 let currentMap = this._data;
973 for (const key of keys) {
980 yield* this._values(currentMap);
981 }
982 > map.ts
983 > public *values(): IterableIterator<TValue> {
984 yield* this._values(this._data);
985 }
986 > map.ts
987 > private *_values(map: Map<any, any>): IterableIterator<TValue> {
988 for (const value of map.values()) {
989 if (value instanceof Map) {
994 }
995 }
996 > map.ts
997 > /**
998 > * Get a textual representation of the map for debugging purposes.
999 > */
1000 > public toString(): string {
1001 const printMap = (map: Map<any, any>, depth: number): string => {
1002 let result = '';
1014 return printMap(this._data, 0);
1015 }
1016 > } map.ts
src/vs/base/common/naturalLanguage/korean.ts 329 covered LOC · 5 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- korean.ts
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 > // allow-any-unicode-comment-file
7 >
8 > /**
9 > * Gets alternative Korean characters for the character code. This will return the ascii
10 > * character code(s) that a Hangul character may have been input with using a qwerty layout.
11 > *
12 > * This only aims to cover modern (not archaic) Hangul syllables.
13 > *
14 > * @param code The character code to get alternate characters for
15 > */
16 > export function getKoreanAltChars(code: number): ArrayLike<number> | undefined {
17 const result = disassembleKorean(code);
18 if (result && result.length > 0) {
21 return undefined;
22 }
23 > korean.ts
24 > let codeBufferLength = 0;
25 > const codeBuffer = new Uint32Array(10);
26 function disassembleKorean(code: number): Uint32Array | undefined {
27 codeBufferLength = 0;
91 return undefined;
92 }
93 > korean.ts
94 function getCodesFromArray(code: number, array: ArrayLike<number>, arrayStartIndex: number): void {
95 // Verify the code is within the array's range
98 }
99 }
100 > korean.ts
101 function addCodesToBuffer(codes: number): void {
102 // NUL is ignored, this is used for archaic characters to avoid using a Map
114 }
115 }
116 > korean.ts
117 > const enum HangulRangeStartCode {
118 > InitialConsonant = 0x1100,
119 > Vowel = 0x1161,
120 > FinalConsonant = 0x11A8,
121 > CompatibilityJamo = 0x3131,
122 > }
123 >
124 > const enum AsciiCode {
125 > NUL = 0,
126 > A = 65,
127 > B = 66,
128 > C = 67,
129 > D = 68,
130 > E = 69,
131 > F = 70,
132 > G = 71,
133 > H = 72,
134 > I = 73,
135 > J = 74,
136 > K = 75,
137 > L = 76,
138 > M = 77,
139 > N = 78,
140 > O = 79,
141 > P = 80,
142 > Q = 81,
143 > R = 82,
144 > S = 83,
145 > T = 84,
146 > U = 85,
147 > V = 86,
148 > W = 87,
149 > X = 88,
150 > Y = 89,
151 > Z = 90,
152 > a = 97,
153 > b = 98,
154 > c = 99,
155 > d = 100,
156 > e = 101,
157 > f = 102,
158 > g = 103,
159 > h = 104,
160 > i = 105,
161 > j = 106,
162 > k = 107,
163 > l = 108,
164 > m = 109,
165 > n = 110,
166 > o = 111,
167 > p = 112,
168 > q = 113,
169 > r = 114,
170 > s = 115,
171 > t = 116,
172 > u = 117,
173 > v = 118,
174 > w = 119,
175 > x = 120,
176 > y = 121,
177 > z = 122,
178 > }
179 >
180 > /**
181 > * Numbers that represent multiple ascii codes. These are precomputed at compile time to reduce
182 > * bundle and runtime overhead.
183 > */
184 > const enum AsciiCodeCombo {
185 > fa = AsciiCode.a << 8 | AsciiCode.f,
186 > fg = AsciiCode.g << 8 | AsciiCode.f,
187 > fq = AsciiCode.q << 8 | AsciiCode.f,
188 > fr = AsciiCode.r << 8 | AsciiCode.f,
189 > ft = AsciiCode.t << 8 | AsciiCode.f,
190 > fv = AsciiCode.v << 8 | AsciiCode.f,
191 > fx = AsciiCode.x << 8 | AsciiCode.f,
192 > hk = AsciiCode.k << 8 | AsciiCode.h,
193 > hl = AsciiCode.l << 8 | AsciiCode.h,
194 > ho = AsciiCode.o << 8 | AsciiCode.h,
195 > ml = AsciiCode.l << 8 | AsciiCode.m,
196 > nj = AsciiCode.j << 8 | AsciiCode.n,
197 > nl = AsciiCode.l << 8 | AsciiCode.n,
198 > np = AsciiCode.p << 8 | AsciiCode.n,
199 > qt = AsciiCode.t << 8 | AsciiCode.q,
200 > rt = AsciiCode.t << 8 | AsciiCode.r,
201 > sg = AsciiCode.g << 8 | AsciiCode.s,
202 > sw = AsciiCode.w << 8 | AsciiCode.s,
203 > }
204 >
205 > /**
206 > * Hangul Jamo - Modern consonants #1
207 > *
208 > * Range U+1100..U+1112
209 > *
210 > * | | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
211 > * |--------|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
212 > * | U+110x | ᄀ | ᄁ | ᄂ | ᄃ | ᄄ | ᄅ | ᄆ | ᄇ | ᄈ | ᄉ | ᄊ | ᄋ | ᄌ | ᄍ | ᄎ | ᄏ |
213 > * | U+111x | ᄐ | ᄑ | ᄒ |
214 > */
215 > const modernConsonants = new Uint8Array([
216 > AsciiCode.r, // ㄱ
217 > AsciiCode.R, // ㄲ
218 > AsciiCode.s, // ㄴ
219 > AsciiCode.e, // ㄷ
220 > AsciiCode.E, // ㄸ
221 > AsciiCode.f, // ㄹ
222 > AsciiCode.a, // ㅁ
223 > AsciiCode.q, // ㅂ
224 > AsciiCode.Q, // ㅃ
225 > AsciiCode.t, // ㅅ
226 > AsciiCode.T, // ㅆ
227 > AsciiCode.d, // ㅇ
228 > AsciiCode.w, // ㅈ
229 > AsciiCode.W, // ㅉ
230 > AsciiCode.c, // ㅊ
231 > AsciiCode.z, // ㅋ
232 > AsciiCode.x, // ㅌ
233 > AsciiCode.v, // ㅍ
234 > AsciiCode.g, // ㅎ
235 > ]);
236 >
237 > /**
238 > * Hangul Jamo - Modern Vowels
239 > *
240 > * Range U+1161..U+1175
241 > *
242 > * | | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
243 > * |--------|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
244 > * | U+116x | | ᅡ | ᅢ | ᅣ | ᅤ | ᅥ | ᅦ | ᅧ | ᅨ | ᅩ | ᅪ | ᅫ | ᅬ | ᅭ | ᅮ | ᅯ |
245 > * | U+117x | ᅰ | ᅱ | ᅲ | ᅳ | ᅴ | ᅵ |
246 > */
247 > const modernVowels = new Uint16Array([
248 > AsciiCode.k, // -> ㅏ
249 > AsciiCode.o, // -> ㅐ
250 > AsciiCode.i, // -> ㅑ
251 > AsciiCode.O, // -> ㅒ
252 > AsciiCode.j, // -> ㅓ
253 > AsciiCode.p, // -> ㅔ
254 > AsciiCode.u, // -> ㅕ
255 > AsciiCode.P, // -> ㅖ
256 > AsciiCode.h, // -> ㅗ
257 > AsciiCodeCombo.hk, // -> ㅘ
258 > AsciiCodeCombo.ho, // -> ㅙ
259 > AsciiCodeCombo.hl, // -> ㅚ
260 > AsciiCode.y, // -> ㅛ
261 > AsciiCode.n, // -> ㅜ
262 > AsciiCodeCombo.nj, // -> ㅝ
263 > AsciiCodeCombo.np, // -> ㅞ
264 > AsciiCodeCombo.nl, // -> ㅟ
265 > AsciiCode.b, // -> ㅠ
266 > AsciiCode.m, // -> ㅡ
267 > AsciiCodeCombo.ml, // -> ㅢ
268 > AsciiCode.l, // -> ㅣ
269 > ]);
270 >
271 > /**
272 > * Hangul Jamo - Modern Consonants #2
273 > *
274 > * Range U+11A8..U+11C2
275 > *
276 > * | | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
277 > * |--------|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
278 > * | U+11Ax | | | | | | | | | ᆨ | ᆩ | ᆪ | ᆫ | ᆬ | ᆭ | ᆮ | ᆯ |
279 > * | U+11Bx | ᆰ | ᆱ | ᆲ | ᆳ | ᆴ | ᆵ | ᆶ | ᆷ | ᆸ | ᆹ | ᆺ | ᆻ | ᆼ | ᆽ | ᆾ | ᆿ |
280 > * | U+11Cx | ᇀ | ᇁ | ᇂ |
281 > */
282 > const modernFinalConsonants = new Uint16Array([
283 > AsciiCode.r, // ㄱ
284 > AsciiCode.R, // ㄲ
285 > AsciiCodeCombo.rt, // ㄳ
286 > AsciiCode.s, // ㄴ
287 > AsciiCodeCombo.sw, // ㄵ
288 > AsciiCodeCombo.sg, // ㄶ
289 > AsciiCode.e, // ㄷ
290 > AsciiCode.f, // ㄹ
291 > AsciiCodeCombo.fr, // ㄺ
292 > AsciiCodeCombo.fa, // ㄻ
293 > AsciiCodeCombo.fq, // ㄼ
294 > AsciiCodeCombo.ft, // ㄽ
295 > AsciiCodeCombo.fx, // ㄾ
296 > AsciiCodeCombo.fv, // ㄿ
297 > AsciiCodeCombo.fg, // ㅀ
298 > AsciiCode.a, // ㅁ
299 > AsciiCode.q, // ㅂ
300 > AsciiCodeCombo.qt, // ㅄ
301 > AsciiCode.t, // ㅅ
302 > AsciiCode.T, // ㅆ
303 > AsciiCode.d, // ㅇ
304 > AsciiCode.w, // ㅈ
305 > AsciiCode.c, // ㅊ
306 > AsciiCode.z, // ㅋ
307 > AsciiCode.x, // ㅌ
308 > AsciiCode.v, // ㅍ
309 > AsciiCode.g, // ㅎ
310 > ]);
311 >
312 > /**
313 > * Hangul Compatibility Jamo
314 > *
315 > * Range U+3131..U+318F
316 > *
317 > * This includes range includes archaic jamo which we don't consider, these are
318 > * given the NUL character code in order to be ignored.
319 > *
320 > * | | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
321 > * |--------|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
322 > * | U+313x | | ㄱ | ㄲ | ㄳ | ㄴ | ㄵ | ㄶ | ㄷ | ㄸ | ㄹ | ㄺ | ㄻ | ㄼ | ㄽ | ㄾ | ㄿ |
323 > * | U+314x | ㅀ | ㅁ | ㅂ | ㅃ | ㅄ | ㅅ | ㅆ | ㅇ | ㅈ | ㅉ | ㅊ | ㅋ | ㅌ | ㅍ | ㅎ | ㅏ |
324 > * | U+315x | ㅐ | ㅑ | ㅒ | ㅓ | ㅔ | ㅕ | ㅖ | ㅗ | ㅘ | ㅙ | ㅚ | ㅛ | ㅜ | ㅝ | ㅞ | ㅟ |
325 > * | U+316x | ㅠ | ㅡ | ㅢ | ㅣ | HF | ㅥ | ㅦ | ㅧ | ㅨ | ㅩ | ㅪ | ㅫ | ㅬ | ㅭ | ㅮ | ㅯ |
326 > * | U+317x | ㅰ | ㅱ | ㅲ | ㅳ | ㅴ | ㅵ | ㅶ | ㅷ | ㅸ | ㅹ | ㅺ | ㅻ | ㅼ | ㅽ | ㅾ | ㅿ |
327 > * | U+318x | ㆀ | ㆁ | ㆂ | ㆃ | ㆄ | ㆅ | ㆆ | ㆇ | ㆈ | ㆉ | ㆊ | ㆋ | ㆌ | ㆍ | ㆎ |
328 > */
329 > const compatibilityJamo = new Uint16Array([
330 > AsciiCode.r, // ㄱ
331 > AsciiCode.R, // ㄲ
332 > AsciiCodeCombo.rt, // ㄳ
333 > AsciiCode.s, // ㄴ
334 > AsciiCodeCombo.sw, // ㄵ
335 > AsciiCodeCombo.sg, // ㄶ
336 > AsciiCode.e, // ㄷ
337 > AsciiCode.E, // ㄸ
338 > AsciiCode.f, // ㄹ
339 > AsciiCodeCombo.fr, // ㄺ
340 > AsciiCodeCombo.fa, // ㄻ
341 > AsciiCodeCombo.fq, // ㄼ
342 > AsciiCodeCombo.ft, // ㄽ
343 > AsciiCodeCombo.fx, // ㄾ
344 > AsciiCodeCombo.fv, // ㄿ
345 > AsciiCodeCombo.fg, // ㅀ
346 > AsciiCode.a, // ㅁ
347 > AsciiCode.q, // ㅂ
348 > AsciiCode.Q, // ㅃ
349 > AsciiCodeCombo.qt, // ㅄ
350 > AsciiCode.t, // ㅅ
351 > AsciiCode.T, // ㅆ
352 > AsciiCode.d, // ㅇ
353 > AsciiCode.w, // ㅈ
354 > AsciiCode.W, // ㅉ
355 > AsciiCode.c, // ㅊ
356 > AsciiCode.z, // ㅋ
357 > AsciiCode.x, // ㅌ
358 > AsciiCode.v, // ㅍ
359 > AsciiCode.g, // ㅎ
360 > AsciiCode.k, // ㅏ
361 > AsciiCode.o, // ㅐ
362 > AsciiCode.i, // ㅑ
363 > AsciiCode.O, // ㅒ
364 > AsciiCode.j, // ㅓ
365 > AsciiCode.p, // ㅔ
366 > AsciiCode.u, // ㅕ
367 > AsciiCode.P, // ㅖ
368 > AsciiCode.h, // ㅗ
369 > AsciiCodeCombo.hk, // ㅘ
370 > AsciiCodeCombo.ho, // ㅙ
371 > AsciiCodeCombo.hl, // ㅚ
372 > AsciiCode.y, // ㅛ
373 > AsciiCode.n, // ㅜ
374 > AsciiCodeCombo.nj, // ㅝ
375 > AsciiCodeCombo.np, // ㅞ
376 > AsciiCodeCombo.nl, // ㅟ
377 > AsciiCode.b, // ㅠ
378 > AsciiCode.m, // ㅡ
379 > AsciiCodeCombo.ml, // ㅢ
380 > AsciiCode.l, // ㅣ
381 > // HF: Hangul Filler (everything after this is archaic)
382 > // ㅥ
383 > // ㅦ
384 > // ㅧ
385 > // ㅨ
386 > // ㅩ
387 > // ㅪ
388 > // ㅫ
389 > // ㅬ
390 > // ㅮ
391 > // ㅯ
392 > // ㅰ
393 > // ㅱ
394 > // ㅲ
395 > // ㅳ
396 > // ㅴ
397 > // ㅵ
398 > // ㅶ
399 > // ㅷ
400 > // ㅸ
401 > // ㅹ
402 > // ㅺ
403 > // ㅻ
404 > // ㅼ
405 > // ㅽ
406 > // ㅾ
407 > // ㅿ
408 > // ㆀ
409 > // ㆁ
410 > // ㆂ
411 > // ㆃ
412 > // ㆄ
413 > // ㆅ
414 > // ㆆ
415 > // ㆇ
416 > // ㆈ
417 > // ㆉ
418 > // ㆊ
419 > // ㆋ
420 > // ㆌ
421 > // ㆍ
422 > // ㆎ
423 > ]);
src/vs/platform/agentHost/node/commandAutoApprover.ts 326 covered LOC · 18 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- commandAutoApprover.ts
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 > import type { Language, Parser, Query, QueryCapture } from '@vscode/tree-sitter-wasm';
7 > import * as fs from 'fs';
8 > import { Disposable, toDisposable } from '../../../base/common/lifecycle.js';
9 > import { FileAccess } from '../../../base/common/network.js';
10 > import { escapeRegExpCharacters, regExpLeadsToEndlessLoop } from '../../../base/common/strings.js';
11 > import { URI } from '../../../base/common/uri.js';
12 > import { getAppNodeModulesPath } from './appNodeModules.js';
13 > import { ILogService } from '../../log/common/log.js';
14 > import type { AgentHostTerminalAutoApproveRuleValue, AgentHostTerminalAutoApproveRules } from '../common/agentHostSchema.js';
15 >
16 > /**
17 > * Redirect destinations that do not result in a write to an arbitrary file
18 > * on disk: the /dev sinks that discard output (`/dev/null`) or write back to
19 > * the same terminal (`/dev/stdout`, `/dev/stderr`, `/dev/tty`).
20 > */
21 > const SAFE_REDIRECT_TARGETS: ReadonlySet<string> = new Set([
22 > '/dev/null',
23 > '/dev/stdout',
24 > '/dev/stderr',
25 > '/dev/tty',
26 > ]);
27 >
28 > /**
29 > * Returns true when the given redirection destination is known to be safe:
30 > * either a known-safe /dev sink or a file-descriptor duplication target
31 > * like `&1` (used in `2>&1`).
32 > */
33 function isSafeRedirectDestination(dest: string): boolean {
34 let cleaned = dest.trim();
46 return SAFE_REDIRECT_TARGETS.has(cleaned);
47 }
49 > /**
50 > * Classification of a tree-sitter `file_redirect` node.
51 > * - `read`: input-only redirect (`<`, `<&N`) — never writes.
52 > * - `safeWrite`: write to a known-safe sink (`/dev/null`, fd duplication, ...).
53 > * - `unsafeWrite`: write to an arbitrary destination. The destination string
54 > * (with surrounding quotes stripped) is included when it could be parsed,
55 > * so the caller may decide whether the target is acceptable.
56 > */
57 > type FileRedirectClassification =
58 > | { kind: 'read' }
59 > | { kind: 'safeWrite' }
60 > | { kind: 'unsafeWrite'; dest: string | undefined };
61 >
62 function classifyFileRedirect(redirectText: string): FileRedirectClassification {
63 if (!redirectText.includes('>')) {
79 return { kind: 'unsafeWrite', dest };
80 }
82 > /**
83 > * Result of a command auto-approval check.
84 > * - `approved`: all sub-commands match allow rules and none are denied
85 > * - `denied`: at least one sub-command matches a deny rule
86 > * - `noMatch`: no rule matched — requires user confirmation
87 > */
88 > export type CommandApprovalResult = 'approved' | 'denied' | 'noMatch';
89 >
90 > /** Options for {@link CommandAutoApprover.shouldAutoApprove}. */
91 > export interface IShouldAutoApproveOptions {
92 > /**
93 > * Predicate that decides whether a write redirection to the given
94 > * destination is acceptable. Called once per write-redirect destination
95 > * found in the command line; the destination is the raw string the user
96 > * typed (with surrounding quotes stripped). The predicate is responsible
97 > * for resolving relative paths and applying its own policy.
98 > *
99 > * When omitted, any write redirect to a destination outside the known-safe
100 > * sinks (e.g. `/dev/null`) downgrades the result to `noMatch`.
101 > */
102 > readonly isWriteDestApproved?: (dest: string) => boolean;
103 > /**
104 > * Effective VS Code `chat.tools.terminal.autoApprove` rules forwarded from
105 > * the renderer. When omitted, the agent host falls back to its bundled
106 > * default rules for compatibility with older clients.
107 > */
108 > readonly autoApproveRules?: AgentHostTerminalAutoApproveRules;
109 > }
110 >
111 > interface IAutoApproveRule {
112 > readonly regex: RegExp;
113 > }
114 >
115 > interface IAutoApproveRules {
116 > readonly allowRules: IAutoApproveRule[];
117 > readonly denyRules: IAutoApproveRule[];
118 > readonly allowCommandLineRules: IAutoApproveRule[];
119 > readonly denyCommandLineRules: IAutoApproveRule[];
120 > }
121 >
122 > const neverMatchRegex = /(?!.*)/;
123 > const transientEnvVarRegex = /^[A-Z_][A-Z0-9_]*=/i;
124 >
125 > /**
126 > * Auto-approves or denies shell commands based on terminal auto-approve rules.
127 > *
128 > * Uses tree-sitter to parse compound commands (`foo && bar`) into
129 > * sub-commands that are individually checked against allow/deny lists.
130 > * The rules are normally forwarded from VS Code's
131 > * `chat.tools.terminal.autoApprove` setting. A bundled default table is kept
132 > * as a compatibility fallback for clients that have not forwarded rules yet.
133 > *
134 > * Tree-sitter is initialized eagerly; call {@link initialize} and await the
135 > * result before using {@link shouldAutoApprove} to guarantee synchronous
136 > * parsing. If tree-sitter fails to load or parse the command,
137 > * {@link shouldAutoApprove} returns `noMatch` so the user is prompted for
138 > * confirmation rather than auto-approving based on the command name alone.
139 > */
140 > export class CommandAutoApprover extends Disposable {
141 >
142 > private _fallbackRules: IAutoApproveRules | undefined;
143 > private _cachedRuleConfig: AgentHostTerminalAutoApproveRules | undefined;
144 > private _cachedRules: IAutoApproveRules | undefined;
145 > private _parser: Parser | undefined;
146 > private _bashLanguage: Language | undefined;
147 > private _queryClass: typeof Query | undefined;
148 > private readonly _initPromise: Promise<void>;
149 >
150 > constructor(
151 > private readonly _logService: ILogService, commandAutoApprover.ts
152 > ) {
153 > super();
154 > this._initPromise = this._initTreeSitter();
155 > }
157 > /**
158 > * Returns a promise that resolves once tree-sitter WASM has been loaded.
159 > * Await this before processing any events to guarantee that
160 > * {@link shouldAutoApprove} can parse commands synchronously.
161 > */
162 > initialize(): Promise<void> {
163 return this._initPromise;
164 }
166 > /**
167 > * Synchronously check whether the given command line should be auto-approved.
168 > * Uses tree-sitter (if loaded) to parse compound commands into sub-commands.
169 > *
170 > * When the command contains write redirections, `options.isWriteDestApproved`
171 > * is consulted for each destination. If every destination is approved by the
172 > * predicate, write redirections do not block auto-approval.
173 > */
174 > shouldAutoApprove(commandLine: string, options?: IShouldAutoApproveOptions): CommandApprovalResult {
175 const trimmed = commandLine.trimStart();
176 if (trimmed.length === 0) {
204 return result;
205 }
207 > private _matchSubCommands(subCommands: string[], rules: IAutoApproveRules): CommandApprovalResult {
208 let allApproved = true;
209 for (const subCommand of subCommands) {
223 return allApproved ? 'approved' : 'noMatch';
224 }
226 > private _matchSingleCommand(command: string, rules: IAutoApproveRules): CommandApprovalResult {
227 // Check deny rules first
228 if (this._matchesRule(command, rules.denyRules)) {
237 return 'noMatch';
238 }
240 > private _matchesRule(command: string, rules: readonly IAutoApproveRule[]): boolean {
241 for (const rule of rules) {
242 if (rule.regex.test(command)) {
246 return false;
247 }
249 > // ---- Tree-sitter --------------------------------------------------------
250 >
251 > private _extractSubCommands(commandLine: string): { subCommands: string[]; unsafeWriteDests: (string | undefined)[] } | undefined {
252 if (!this._parser || !this._bashLanguage || !this._queryClass) {
253 return undefined;
291 }
292 }
294 > private async _initTreeSitter(): Promise<void> {
296 > const { default: TreeSitter } = (await import('@vscode/tree-sitter-wasm'));
297 >
298 > if (this._store.isDisposed) {
299 return;
300 }
302 > // Resolve WASM files from node_modules. In the desktop app the `.wasm`
303 > // files are unpacked next to the ASAR archive (`node_modules.asar.unpacked`),
304 > // while in dev and on the server (which has no ASAR) they live in a plain
305 > // `node_modules`.
306 > const moduleRoot = URI.joinPath(FileAccess.asFileUri(getAppNodeModulesPath()), '@vscode', 'tree-sitter-wasm', 'wasm');
307 > const wasmPath = URI.joinPath(moduleRoot, 'tree-sitter.wasm').fsPath;
308 >
309 > await TreeSitter.Parser.init({
310 > locateFile() {
311 > return wasmPath;
312 > }
313 > });
314
315 if (this._store.isDisposed) {
347 this._logService.warn('[CommandAutoApprover] Failed to initialize tree-sitter', err);
348 }
351 > // ---- Rules --------------------------------------------------------------
352 >
353 > private _compileRules(ruleConfig: AgentHostTerminalAutoApproveRules | undefined): IAutoApproveRules {
354 if (!ruleConfig) {
355 if (!this._fallbackRules) {
367 return this._cachedRules;
368 }
370 > private _compileRuleEntries(ruleConfig: Readonly<Record<string, AgentHostTerminalAutoApproveRuleValue>>): IAutoApproveRules {
371 const allowRules: IAutoApproveRule[] = [];
372 const denyRules: IAutoApproveRule[] = [];
399 return { allowRules, denyRules, allowCommandLineRules, denyCommandLineRules };
400 }
402 >
403 > // ---- Regex conversion -------------------------------------------------------
404 >
405 function convertAutoApproveEntryToRegex(value: string): RegExp {
406 // If wrapped in `/`, treat as regex
446 return new RegExp(`^${sanitizedValue}\\b`);
447 }
449 > // ---- Default rules ----------------------------------------------------------
450 > //
451 > // Compatibility fallback for clients that do not forward the VS Code
452 > // `chat.tools.terminal.autoApprove` setting.
453 > // TODO: Remove this fallback once all agent-host clients are guaranteed to
454 > // forward `chat.tools.terminal.autoApprove` before shell approvals run.
455 >
456 > const DEFAULT_TERMINAL_AUTO_APPROVE_RULES: Readonly<Record<string, AgentHostTerminalAutoApproveRuleValue>> = {
457 > // Safe readonly commands
458 > cd: true,
459 > echo: true,
460 > ls: true,
461 > dir: true,
462 > pwd: true,
463 > cat: true,
464 > head: true,
465 > tail: true,
466 > findstr: true,
467 > wc: true,
468 > tr: true,
469 > cut: true,
470 > cmp: true,
471 > which: true,
472 > basename: true,
473 > dirname: true,
474 > realpath: true,
475 > readlink: true,
476 > stat: true,
477 > file: true,
478 > od: true,
479 > du: true,
480 > df: true,
481 > sleep: true,
482 > nl: true,
483 >
484 > grep: true,
485 >
486 > // Safe git sub-commands
487 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+status\\b/': true,
488 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+log\\b/': true,
489 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+log\\b.*\\s--output(=|\\s|$)/': false,
490 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+show\\b/': true,
491 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+diff\\b/': true,
492 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+ls-files\\b/': true,
493 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+grep\\b/': true,
494 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+branch\\b/': true,
495 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+branch\\b.*\\s-(d|D|m|M|-delete|-force)\\b/': false,
496 >
497 > // Docker readonly sub-commands
498 > '/^docker\\s+(ps|images|info|version|inspect|logs|top|stats|port|diff|search|events)\\b/': true,
499 > '/^docker\\s+(container|image|network|volume|context|system)\\s+(ls|ps|inspect|history|show|df|info)\\b/': true,
500 > '/^docker\\s+compose\\s+(ps|ls|top|logs|images|config|version|port|events)\\b/': true,
501 >
502 > // PowerShell
503 > 'Get-ChildItem': true,
504 > 'Get-Content': true,
505 > 'Get-Date': true,
506 > 'Get-Random': true,
507 > 'Get-Location': true,
508 > 'Set-Location': true,
509 > 'Write-Host': true,
510 > 'Write-Output': true,
511 > 'Out-String': true,
512 > 'Split-Path': true,
513 > 'Join-Path': true,
514 > 'Start-Sleep': true,
515 > 'Where-Object': true,
516 > '/^Select-[a-z0-9]/i': true,
517 > '/^Measure-[a-z0-9]/i': true,
518 > '/^Compare-[a-z0-9]/i': true,
519 > '/^Format-[a-z0-9]/i': true,
520 > '/^Sort-[a-z0-9]/i': true,
521 >
522 > // Package manager read-only commands
523 > '/^npm\\s+(ls|list|outdated|view|info|show|explain|why|root|prefix|bin|search|doctor|fund|repo|bugs|docs|home|help(-search)?)\\b/': true,
524 > '/^npm\\s+config\\s+(list|get)\\b/': true,
525 > '/^npm\\s+pkg\\s+get\\b/': true,
526 > '/^npm\\s+audit$/': true,
527 > '/^npm\\s+cache\\s+verify\\b/': true,
528 > '/^yarn\\s+(list|outdated|info|why|bin|help|versions)\\b/': true,
529 > '/^yarn\\s+licenses\\b/': true,
530 > '/^yarn\\s+audit\\b(?!.*\\bfix\\b)/': true,
531 > '/^yarn\\s+config\\s+(list|get)\\b/': true,
532 > '/^yarn\\s+cache\\s+dir\\b/': true,
533 > '/^pnpm\\s+(ls|list|outdated|why|root|bin|doctor)\\b/': true,
534 > '/^pnpm\\s+licenses\\b/': true,
535 > '/^pnpm\\s+audit\\b(?!.*\\bfix\\b)/': true,
536 > '/^pnpm\\s+config\\s+(list|get)\\b/': true,
537 >
538 > // Safe lockfile-only installs
539 > 'npm ci': true,
540 > '/^yarn\\s+install\\s+--frozen-lockfile\\b/': true,
541 > '/^pnpm\\s+install\\s+--frozen-lockfile\\b/': true,
542 >
543 > // Safe commands with dangerous arg blocking
544 > column: true,
545 > '/^column\\b.*\\s-c\\s+[0-9]{4,}/': false,
546 > date: true,
547 > '/^date\\b.*\\s(-s|--set)\\b/': false,
548 > find: true,
549 > '/^find\\b.*\\s-(delete|exec|execdir|fprint|fprintf|fls|ok|okdir)\\b/': false,
550 > rg: true,
551 > '/^rg\\b.*\\s(--pre|--hostname-bin)\\b/': false,
552 > sed: true,
553 > '/^sed\\b.*\\s(-[a-zA-Z]*(e|f)[a-zA-Z]*|--expression|--file)\\b/': false,
554 > '/^sed\\b.*s\\/.*\\/.*\\/[ew]/': false,
555 > '/^sed\\b.*;W/': false,
556 > sort: true,
557 > '/^sort\\b.*\\s-(o|S)\\b/': false,
558 > tree: true,
559 > '/^tree\\b.*\\s-o\\b/': false,
560 > '/^xxd$/': true,
561 > '/^xxd\\b(\\s+-\\S+)*\\s+[^-\\s]\\S*$/': true,
562 >
563 > // Dangerous commands
564 > rm: false,
565 > rmdir: false,
566 > del: false,
567 > 'Remove-Item': false,
568 > ri: false,
569 > rd: false,
570 > erase: false,
571 > dd: false,
572 > kill: false,
573 > ps: false,
574 > top: false,
575 > 'Stop-Process': false,
576 > spps: false,
577 > taskkill: false,
578 > 'taskkill.exe': false,
579 > curl: false,
580 > wget: false,
581 > 'Invoke-RestMethod': false,
582 > 'Invoke-WebRequest': false,
583 > irm: false,
584 > iwr: false,
585 > chmod: false,
586 > chown: false,
587 > 'Set-ItemProperty': false,
588 > sp: false,
589 > 'Set-Acl': false,
590 > jq: false,
591 > xargs: false,
592 > eval: false,
593 > 'Invoke-Expression': false,
594 > iex: false,
595 > };
src/vs/base/common/stream.ts 325 covered LOC · 29 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- stream.ts
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 > import { CancellationToken } from './cancellation.js';
7 > import { onUnexpectedError } from './errors.js';
8 > import { DisposableStore, toDisposable } from './lifecycle.js';
9 >
10 > /**
11 > * The payload that flows in readable stream events.
12 > */
13 > export type ReadableStreamEventPayload<T> = T | Error | 'end';
14 >
15 > export interface ReadableStreamEvents<T> {
16 >
17 > /**
18 > * The 'data' event is emitted whenever the stream is
19 > * relinquishing ownership of a chunk of data to a consumer.
20 > *
21 > * NOTE: PLEASE UNDERSTAND THAT ADDING A DATA LISTENER CAN
22 > * TURN THE STREAM INTO FLOWING MODE. IT IS THEREFOR THE
23 > * LAST LISTENER THAT SHOULD BE ADDED AND NOT THE FIRST
24 > *
25 > * Use `listenStream` as a helper method to listen to
26 > * stream events in the right order.
27 > */
28 > on(event: 'data', callback: (data: T) => void): void;
29 >
30 > /**
31 > * Emitted when any error occurs.
32 > */
33 > on(event: 'error', callback: (err: Error) => void): void;
34 >
35 > /**
36 > * The 'end' event is emitted when there is no more data
37 > * to be consumed from the stream. The 'end' event will
38 > * not be emitted unless the data is completely consumed.
39 > */
40 > on(event: 'end', callback: () => void): void;
41 > }
42 >
43 > /**
44 > * A interface that emulates the API shape of a node.js readable
45 > * stream for use in native and web environments.
46 > */
47 > export interface ReadableStream<T> extends ReadableStreamEvents<T> {
48 >
49 > /**
50 > * Stops emitting any events until resume() is called.
51 > */
52 > pause(): void;
53 >
54 > /**
55 > * Starts emitting events again after pause() was called.
56 > */
57 > resume(): void;
58 >
59 > /**
60 > * Destroys the stream and stops emitting any event.
61 > */
62 > destroy(): void;
63 >
64 > /**
65 > * Allows to remove a listener that was previously added.
66 > */
67 > removeListener(event: string, callback: Function): void;
68 > }
69 >
70 > /**
71 > * A interface that emulates the API shape of a node.js readable
72 > * for use in native and web environments.
73 > */
74 > export interface Readable<T> {
75 >
76 > /**
77 > * Read data from the underlying source. Will return
78 > * null to indicate that no more data can be read.
79 > */
80 > read(): T | null;
81 > }
82 >
83 > export function isReadable<T>(obj: unknown): obj is Readable<T> {
84 const candidate = obj as Readable<T> | undefined;
85 if (!candidate) {
89 return typeof candidate.read === 'function';
90 }
91 > stream.ts
92 > /**
93 > * A interface that emulates the API shape of a node.js writeable
94 > * stream for use in native and web environments.
95 > */
96 > export interface WriteableStream<T> extends ReadableStream<T> {
97 >
98 > /**
99 > * Writing data to the stream will trigger the on('data')
100 > * event listener if the stream is flowing and buffer the
101 > * data otherwise until the stream is flowing.
102 > *
103 > * If a `highWaterMark` is configured and writing to the
104 > * stream reaches this mark, a promise will be returned
105 > * that should be awaited on before writing more data.
106 > * Otherwise there is a risk of buffering a large number
107 > * of data chunks without consumer.
108 > */
109 > write(data: T): void | Promise<void>;
110 >
111 > /**
112 > * Signals an error to the consumer of the stream via the
113 > * on('error') handler if the stream is flowing.
114 > *
115 > * NOTE: call `end` to signal that the stream has ended,
116 > * this DOES NOT happen automatically from `error`.
117 > */
118 > error(error: Error): void;
119 >
120 > /**
121 > * Signals the end of the stream to the consumer. If the
122 > * result is provided, will trigger the on('data') event
123 > * listener if the stream is flowing and buffer the data
124 > * otherwise until the stream is flowing.
125 > */
126 > end(result?: T): void;
127 > }
128 >
129 > /**
130 > * A stream that has a buffer already read. Returns the original stream
131 > * that was read as well as the chunks that got read.
132 > *
133 > * The `ended` flag indicates if the stream has been fully consumed.
134 > */
135 > export interface ReadableBufferedStream<T> {
136 >
137 > /**
138 > * The original stream that is being read.
139 > */
140 > stream: ReadableStream<T>;
141 >
142 > /**
143 > * An array of chunks already read from this stream.
144 > */
145 > buffer: T[];
146 >
147 > /**
148 > * Signals if the stream has ended or not. If not, consumers
149 > * should continue to read from the stream until consumed.
150 > */
151 > ended: boolean;
152 > }
153 >
154 > export function isReadableStream<T>(obj: unknown): obj is ReadableStream<T> {
155 const candidate = obj as ReadableStream<T> | undefined;
156 if (!candidate) {
160 return [candidate.on, candidate.pause, candidate.resume, candidate.destroy].every(fn => typeof fn === 'function');
161 }
162 > stream.ts
163 > export function isReadableBufferedStream<T>(obj: unknown): obj is ReadableBufferedStream<T> {
164 const candidate = obj as ReadableBufferedStream<T> | undefined;
165 if (!candidate) {
169 return isReadableStream(candidate.stream) && Array.isArray(candidate.buffer) && typeof candidate.ended === 'boolean';
170 }
171 > stream.ts
172 > export interface IReducer<T, R = T> {
173 > (data: T[]): R;
174 > }
175 >
176 > export interface IDataTransformer<Original, Transformed> {
177 > (data: Original): Transformed;
178 > }
179 >
180 > export interface IErrorTransformer {
181 > (error: Error): Error;
182 > }
183 >
184 > export interface ITransformer<Original, Transformed> {
185 > data: IDataTransformer<Original, Transformed>;
186 > error?: IErrorTransformer;
187 > }
188 >
189 > export function newWriteableStream<T>(reducer: IReducer<T> | null, options?: WriteableStreamOptions): WriteableStream<T> {
190 return new WriteableStreamImpl<T>(reducer, options);
191 }
192 > stream.ts
193 > export interface WriteableStreamOptions {
194 >
195 > /**
196 > * The number of objects to buffer before WriteableStream#write()
197 > * signals back that the buffer is full. Can be used to reduce
198 > * the memory pressure when the stream is not flowing.
199 > */
200 > highWaterMark?: number;
201 > }
202 >
203 > class WriteableStreamImpl<T> implements WriteableStream<T> {
204 >
205 > private readonly state = {
206 > flowing: false,
207 > ended: false,
208 > destroyed: false
209 > };
210 >
211 > private readonly buffer = {
212 > data: [] as T[],
213 > error: [] as Error[]
214 > };
215 >
216 > private readonly listeners = {
217 > data: [] as { (data: T): void }[],
218 > error: [] as { (error: Error): void }[],
219 > end: [] as { (): void }[]
220 > };
221 >
222 > private readonly pendingWritePromises: Function[] = [];
223 >
224 > /**
225 > * @param reducer a function that reduces the buffered data into a single object;
226 > * because some objects can be complex and non-reducible, we also
227 > * allow passing the explicit `null` value to skip the reduce step
228 > * @param options stream options
229 > */
230 > constructor(private reducer: IReducer<T> | null, private options?: WriteableStreamOptions) { }
231 >
232 > pause(): void {
233 if (this.state.destroyed) {
234 return;
237 this.state.flowing = false;
238 }
239 > stream.ts
240 > resume(): void {
241 if (this.state.destroyed) {
242 return;
252 }
253 }
254 > stream.ts
255 > write(data: T): void | Promise<void> {
256 if (this.state.destroyed) {
257 return;
273 }
274 }
275 > stream.ts
276 > error(error: Error): void {
277 if (this.state.destroyed) {
278 return;
289 }
290 }
291 > stream.ts
292 > end(result?: T): void {
293 if (this.state.destroyed) {
294 return;
312 }
313 }
314 > stream.ts
315 > private emitData(data: T): void {
316 this.listeners.data.slice(0).forEach(listener => listener(data)); // slice to avoid listener mutation from delivering event
317 }
318 > stream.ts
319 > private emitError(error: Error): void {
320 if (this.listeners.error.length === 0) {
321 onUnexpectedError(error); // nobody listened to this error so we log it as unexpected
324 }
325 }
326 > stream.ts
327 > private emitEnd(): void {
328 this.listeners.end.slice(0).forEach(listener => listener()); // slice to avoid listener mutation from delivering event
329 }
330 > stream.ts
331 > on(event: 'data', callback: (data: T) => void): void;
332 > on(event: 'error', callback: (err: Error) => void): void;
333 > on(event: 'end', callback: () => void): void;
334 > on(event: 'data' | 'error' | 'end', callback: ((data: T) => void) | ((err: Error) => void) | (() => void)): void {
335 if (this.state.destroyed) {
336 return;
372 }
373 }
374 > stream.ts
375 > removeListener(event: string, callback: Function): void {
376 if (this.state.destroyed) {
377 return;
401 }
402 }
403 > stream.ts
404 > private flowData(): void {
405 // if buffer is empty, nothing to do
406 if (this.buffer.data.length === 0) {
428 pendingWritePromises.forEach(pendingWritePromise => pendingWritePromise());
429 }
430 > stream.ts
431 > private flowErrors(): void {
432 if (this.listeners.error.length > 0) {
433 for (const error of this.buffer.error) {
438 }
439 }
440 > stream.ts
441 > private flowEnd(): boolean {
442 if (this.state.ended) {
443 this.emitEnd();
448 return false;
449 }
450 > stream.ts
451 > destroy(): void {
452 if (!this.state.destroyed) {
453 this.state.destroyed = true;
464 }
465 }
466 > } stream.ts
467 >
468 > /**
469 > * Helper to fully read a T readable into a T.
470 > */
471 > export function consumeReadable<T>(readable: Readable<T>, reducer: IReducer<T>): T {
472 const chunks: T[] = [];
473
479 return reducer(chunks);
480 }
481 > stream.ts
482 > /**
483 > * Helper to read a T readable up to a maximum of chunks. If the limit is
484 > * reached, will return a readable instead to ensure all data can still
485 > * be read.
486 > */
487 > export function peekReadable<T>(readable: Readable<T>, reducer: IReducer<T>, maxChunks: number): T | Readable<T> {
488 const chunks: T[] = [];
489
527 };
528 }
529 > stream.ts
530 > /**
531 > * Helper to fully read a T stream into a T or consuming
532 > * a stream fully, awaiting all the events without caring
533 > * about the data.
534 > */
535 > export function consumeStream<T, R = T>(stream: ReadableStreamEvents<T>, reducer: IReducer<T, R>): Promise<R>;
536 > export function consumeStream(stream: ReadableStreamEvents<unknown>): Promise<undefined>;
537 > export function consumeStream<T, R = T>(stream: ReadableStreamEvents<T>, reducer?: IReducer<T, R>): Promise<R | undefined> {
538 return new Promise((resolve, reject) => {
539 const chunks: T[] = [];
562 });
563 }
564 > stream.ts
565 > export interface IStreamListener<T> {
566 >
567 > /**
568 > * The 'data' event is emitted whenever the stream is
569 > * relinquishing ownership of a chunk of data to a consumer.
570 > */
571 > onData(data: T): void;
572 >
573 > /**
574 > * Emitted when any error occurs.
575 > */
576 > onError(err: Error): void;
577 >
578 > /**
579 > * The 'end' event is emitted when there is no more data
580 > * to be consumed from the stream. The 'end' event will
581 > * not be emitted unless the data is completely consumed.
582 > */
583 > onEnd(): void;
584 > }
585 >
586 > /**
587 > * Helper to listen to all events of a T stream in proper order.
588 > */
589 > export function listenStream<T>(stream: ReadableStreamEvents<T>, listener: IStreamListener<T>, token?: CancellationToken): void {
590
591 stream.on('error', error => {
610 });
611 }
612 > stream.ts
613 > /**
614 > * Helper to peek up to `maxChunks` into a stream. The return type signals if
615 > * the stream has ended or not. If not, caller needs to add a `data` listener
616 > * to continue reading.
617 > */
618 > export function peekStream<T>(stream: ReadableStream<T>, maxChunks: number): Promise<ReadableBufferedStream<T>> {
619 return new Promise((resolve, reject) => {
620 const streamListeners = new DisposableStore();
666 });
667 }
668 > stream.ts
669 > /**
670 > * Helper to create a readable stream from an existing T.
671 > */
672 > export function toStream<T>(t: T, reducer: IReducer<T>): ReadableStream<T> {
673 const stream = newWriteableStream<T>(reducer);
674
677 return stream;
678 }
679 > stream.ts
680 > /**
681 > * Helper to create an empty stream
682 > */
683 > export function emptyStream(): ReadableStream<never> {
684 const stream = newWriteableStream<never>(() => { throw new Error('not supported'); });
685 stream.end();
687 return stream;
688 }
689 > stream.ts
690 > /**
691 > * Helper to convert a T into a Readable<T>.
692 > */
693 > export function toReadable<T>(t: T): Readable<T> {
694 let consumed = false;
695
706 };
707 }
708 > stream.ts
709 > /**
710 > * Helper to transform a readable stream into another stream.
711 > */
712 > export function transform<Original, Transformed>(stream: ReadableStreamEvents<Original>, transformer: ITransformer<Original, Transformed>, reducer: IReducer<Transformed>): ReadableStream<Transformed> {
713 const target = newWriteableStream<Transformed>(reducer);
714
721 return target;
722 }
723 > stream.ts
724 > /**
725 > * Helper to take an existing readable that will
726 > * have a prefix injected to the beginning.
727 > */
728 > export function prefixedReadable<T>(prefix: T, readable: Readable<T>, reducer: IReducer<T>): Readable<T> {
729 let prefixHandled = false;
730
751 };
752 }
753 > stream.ts
754 > /**
755 > * Helper to take an existing stream that will
756 > * have a prefix injected to the beginning.
757 > */
758 > export function prefixedStream<T>(prefix: T, stream: ReadableStream<T>, reducer: IReducer<T>): ReadableStream<T> {
759 let prefixHandled = false;
760
src/vs/platform/agentHost/common/state/protocol/channels-session/commands.ts 323 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- commands.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > import type { URI } from '../common/state.js';
10 > import type { BaseParams } from '../common/commands.js';
11 > import type { SessionActiveClient } from './state.js';
12 > import type { MessageAttachment } from '../channels-chat/state.js';
13 >
14 > // ─── createSession ───────────────────────────────────────────────────────────
15 >
16 > /**
17 > * Creates a new session with the specified agent provider.
18 > *
19 > * If the session URI already exists, the server MUST return an error with code
20 > * `-32003` (`SessionAlreadyExists`).
21 > *
22 > * After creation, the client should subscribe to the session URI to receive state
23 > * updates. The server also broadcasts a `root/sessionAdded` notification to all
24 > * clients.
25 > *
26 > * @category Commands
27 > * @method createSession
28 > * @direction Client → Server
29 > * @messageType Request
30 > * @version 1
31 > * @example
32 > * ```jsonc
33 > * // Client → Server
34 > * { "jsonrpc": "2.0", "id": 2, "method": "createSession",
35 > * "params": { "channel": "ahp-session:/<uuid>", "provider": "copilot" } }
36 > *
37 > * // Server → Client (success)
38 > * { "jsonrpc": "2.0", "id": 2, "result": null }
39 > *
40 > * // Server → Client (failure — provider not found)
41 > * { "jsonrpc": "2.0", "id": 2, "error": { "code": -32002, "message": "No agent for provider" } }
42 > *
43 > * // Server → Client (failure — session already exists)
44 > * { "jsonrpc": "2.0", "id": 2, "error": { "code": -32003, "message": "Session already exists" } }
45 > * ```
46 > */
47 > /**
48 > * Identifies a source session and turn to fork from.
49 > *
50 > * When provided in `createSession`, the server populates the new session with
51 > * content from the source session up to and including the response of the
52 > * specified turn.
53 > */
54 > export interface SessionForkSource {
55 > /** URI of the existing session to fork from */
56 > session: URI;
57 > /** Turn ID in the source session; content up to and including this turn's response is copied */
58 > turnId: string;
59 > }
60 >
61 > export interface CreateSessionParams extends BaseParams {
62 > /** Session URI (client-chosen, e.g. `ahp-session:/<uuid>`) */
63 > channel: URI;
64 > /** Agent provider ID */
65 > provider?: string;
66 > /**
67 > * The working directories the session's agent is granted tool access to.
68 > * A session may span multiple directories; they are equal peers except when
69 > * the agent advertises
70 > * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case
71 > * one of them should be designated the primary via
72 > * {@link primaryWorkingDirectory}.
73 > *
74 > * A client MUST NOT supply more than one entry unless the agent advertises
75 > * {@link AgentCapabilities.multipleWorkingDirectories}; a server without that
76 > * capability treats only the first entry as the session's working directory
77 > * and ignores the rest. Dispatch `session/workingDirectorySet` /
78 > * `session/workingDirectoryRemoved` to change the set after the session has
79 > * started.
80 > *
81 > * Ignored for forked sessions — a fork inherits its working directories
82 > * from the source session identified by `fork`.
83 > */
84 > workingDirectories?: URI[];
85 > /**
86 > * The primary working directory for the session's **default chat**.
87 > *
88 > * A session has no primary of its own — primary is a per-chat notion (see
89 > * {@link ChatState.primaryWorkingDirectory}). But `createSession` implicitly
90 > * creates the session's default chat, and there is no separate `createChat`
91 > * call to carry that chat's create-time fields. This field is therefore the
92 > * only place a client can designate the **default chat's** primary at birth;
93 > * it is copied into that chat's read-only `primaryWorkingDirectory`. For any
94 > * non-default chat, pass {@link CreateChatParams.primaryWorkingDirectory}
95 > * instead.
96 > *
97 > * When set, it MUST be one of {@link workingDirectories}. A client SHOULD
98 > * supply this when the agent advertises
99 > * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY
100 > * reject creation that omits it, or fall back to the first entry of
101 > * `workingDirectories`. Ignored for forked sessions (a fork inherits the
102 > * source session's chats and their primaries).
103 > */
104 > primaryWorkingDirectory?: URI;
105 > /**
106 > * Fork from an existing session. The new session is populated with content
107 > * from the source session up to and including the specified turn's response.
108 > */
109 > fork?: SessionForkSource;
110 > /**
111 > * Agent-specific configuration values collected via `resolveSessionConfig`.
112 > * Keys and values correspond to the schema returned by the server.
113 > */
114 > config?: Record<string, unknown>;
115 > /**
116 > * Eagerly claim an active client role for the new session.
117 > *
118 > * When provided, the server initializes the session with this client as an
119 > * active client, equivalent to dispatching a `session/activeClientSet`
120 > * action immediately after creation. The `clientId` MUST match the
121 > * `clientId` the creating client supplied in `initialize`.
122 > */
123 > activeClient?: SessionActiveClient;
124 > /**
125 > * Opt-in progress token. When set, the client is offering to receive
126 > * `progress` notifications (see `ProgressParams`) for any long-running work
127 > * the server does to bring this session up — most notably the lazy,
128 > * first-use download of the provider's native SDK. The server echoes this
129 > * exact token on every `progress` frame so the client can correlate it to
130 > * this `createSession` call (and the UI awaiting it).
131 > *
132 > * The token MUST be unique across the client's active requests. The server
133 > * MAY ignore it (e.g. when nothing long-running is needed), in which case no
134 > * `progress` notifications are emitted.
135 > */
136 > progressToken?: string;
137 > }
138 >
139 > // ─── disposeSession ──────────────────────────────────────────────────────────
140 >
141 > /**
142 > * Disposes a session and cleans up server-side resources.
143 > *
144 > * The server broadcasts a `root/sessionRemoved` notification to all clients.
145 > *
146 > * @category Commands
147 > * @method disposeSession
148 > * @direction Client → Server
149 > * @messageType Request
150 > * @version 1
151 > */
152 > export interface DisposeSessionParams extends BaseParams { }
153 >
154 > // ─── fetchTurns ──────────────────────────────────────────────────────────────
155 >
156 > /**
157 > * Requests that the host load older historical turns into a chat state.
158 > *
159 > * The command result does not carry turns. Instead, before responding, the host
160 > * MUST dispatch `chat/turnsLoaded` to insert any loaded turns into the chat
161 > * channel's `turns` state, ahead of the already-loaded window, and update or
162 > * clear `turnsNextCursor`.
163 > *
164 > * Before applying any operation that references a turn outside the currently
165 > * loaded window, the host MUST eagerly load enough older turns into state for
166 > * that operation to reduce against valid state.
167 > *
168 > * @category Commands
169 > * @method fetchTurns
170 > * @direction Client → Server
171 > * @messageType Request
172 > * @version 1
173 > * @example
174 > * ```jsonc
175 > * // Client → Server (load the next page indicated by ChatState.turnsNextCursor)
176 > * { "jsonrpc": "2.0", "id": 8, "method": "fetchTurns",
177 > * "params": { "channel": "ahp-chat:/<uuid>", "cursor": "opaque-cursor" } }
178 > *
179 > * // Server updates chat state, then responds
180 > * { "jsonrpc": "2.0", "id": 8, "result": {} }
181 > * ```
182 > */
183 > export interface FetchTurnsParams extends BaseParams {
184 > /** Chat URI */
185 > channel: URI;
186 > /**
187 > * Opaque cursor from `ChatState.turnsNextCursor`.
188 > *
189 > * The host MUST reject unrecognised cursors with `InvalidParams`. Omit only
190 > * when asking the host to opportunistically load its next older page for the
191 > * chat, if any.
192 > */
193 > cursor?: string;
194 > }
195 >
196 > /**
197 > * Result of the `fetchTurns` command.
198 > */
199 > export interface FetchTurnsResult { }
200 >
201 > // ─── completions ─────────────────────────────────────────────────────────────
202 >
203 > /**
204 > * The kind of completion items being requested.
205 > *
206 > * @category Commands
207 > */
208 > export const enum CompletionItemKind {
209 > /**
210 > * Completions for the text of a {@link Message} the user is composing.
211 > * Each returned item carries an attachment that gets associated with the
212 > * message when accepted.
213 > */
214 > UserMessage = 'userMessage',
215 > }
216 >
217 > /**
218 > * Requests completion items for a partially-typed input (e.g. a user message
219 > * the user is currently composing). Used to power `@`-mention pickers,
220 > * file/symbol references, and similar inline-completion experiences.
221 > *
222 > * Servers SHOULD treat this command as best-effort and return promptly. The
223 > * client SHOULD debounce calls to avoid flooding the server with requests on
224 > * every keystroke.
225 > *
226 > * @category Commands
227 > * @method completions
228 > * @direction Client → Server
229 > * @messageType Request
230 > * @version 1
231 > * @example
232 > * ```jsonc
233 > * // User has typed "look at @foo" and the cursor is just after "@foo".
234 > * // Client → Server
235 > * { "jsonrpc": "2.0", "id": 12, "method": "completions",
236 > * "params": { "kind": "userMessage", "channel": "ahp-chat:/<uuid>",
237 > * "text": "look at @foo", "offset": 12 } }
238 > *
239 > * // Server → Client
240 > * { "jsonrpc": "2.0", "id": 12, "result": {
241 > * "items": [
242 > * {
243 > * "insertText": "@foo.ts",
244 > * "rangeStart": 8,
245 > * "rangeEnd": 12,
246 > * "attachment": {
247 > * "type": "resource",
248 > * "label": "foo.ts",
249 > * "displayKind": "document",
250 > * "uri": "file:///workspace/foo.ts"
251 > * }
252 > * }
253 > * ]
254 > * }}
255 > * ```
256 > */
257 > export interface CompletionsParams extends BaseParams {
258 > /** What kind of completion is being requested. */
259 > kind: CompletionItemKind;
260 > /** The chat URI the completion is being requested for. */
261 > channel: URI;
262 > /**
263 > * The complete text of the input being completed (e.g. the full user
264 > * message text typed so far).
265 > */
266 > text: string;
267 > /**
268 > * The character offset within `text` at which the completion is requested,
269 > * measured in UTF-16 code units. MUST satisfy `0 <= offset <= text.length`.
270 > */
271 > offset: number;
272 > }
273 >
274 > /**
275 > * A single completion item returned by the `completions` command.
276 > *
277 > * When the user accepts an item, the client SHOULD:
278 > * 1. Replace the range `[rangeStart, rangeEnd)` in the input with `insertText`
279 > * (or insert `insertText` at the cursor when the range is omitted).
280 > * 2. Associate the item's `attachment` with the resulting {@link Message}.
281 > *
282 > * @category Commands
283 > */
284 > export interface CompletionItem {
285 > /**
286 > * The text inserted into the input when this item is accepted.
287 > */
288 > insertText: string;
289 >
290 > /**
291 > * If defined, the start of the range in the input's `text` that is replaced
292 > * by `insertText`. The range is the half-open interval
293 > * `[rangeStart, rangeEnd)` of character offsets, measured in UTF-16 code
294 > * units.
295 > *
296 > * When omitted, the client SHOULD insert `insertText` at the cursor.
297 > *
298 > * Note: this range refers to positions in the *current* input. The
299 > * attachment's own `rangeStart`/`rangeEnd` (when present) refer to
300 > * positions in the final {@link Message.text} after the item is
301 > * accepted.
302 > */
303 > rangeStart?: number;
304 >
305 > /**
306 > * The end of the range in the input's `text` that is replaced by
307 > * `insertText`. See {@link rangeStart}.
308 > */
309 > rangeEnd?: number;
310 >
311 > /**
312 > * The attachment associated with this completion item.
313 > */
314 > attachment: MessageAttachment;
315 > }
316 >
317 > /**
318 > * Result of the `completions` command.
319 > */
320 > export interface CompletionsResult {
321 > /** The completion items, in the order the server suggests displaying them. */
322 > items: CompletionItem[];
323 > }
src/vs/base/common/path.ts 318 covered LOC · 78 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- path.ts
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 > // NOTE: VSCode's copy of nodejs path library to be usable in common (non-node) namespace
7 > // Copied from: https://github.com/nodejs/node/commits/v22.15.0/lib/path.js
8 > // Excluding: the change that adds primordials
9 > // (https://github.com/nodejs/node/commit/187a862d221dec42fa9a5c4214e7034d9092792f and others)
10 > // Excluding: the change that adds glob matching
11 > // (https://github.com/nodejs/node/commit/57b8b8e18e5e2007114c63b71bf0baedc01936a6)
12 >
13 > /**
14 > * Copyright Joyent, Inc. and other Node contributors.
15 > *
16 > * Permission is hereby granted, free of charge, to any person obtaining a
17 > * copy of this software and associated documentation files (the
18 > * "Software"), to deal in the Software without restriction, including
19 > * without limitation the rights to use, copy, modify, merge, publish,
20 > * distribute, sublicense, and/or sell copies of the Software, and to permit
21 > * persons to whom the Software is furnished to do so, subject to the
22 > * following conditions:
23 > *
24 > * The above copyright notice and this permission notice shall be included
25 > * in all copies or substantial portions of the Software.
26 > *
27 > * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
28 > * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
29 > * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
30 > * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
31 > * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
32 > * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
33 > * USE OR OTHER DEALINGS IN THE SOFTWARE.
34 > */
35 >
36 > import * as process from './process.js';
37 >
38 > const CHAR_UPPERCASE_A = 65;/* A */
39 > const CHAR_LOWERCASE_A = 97; /* a */
40 > const CHAR_UPPERCASE_Z = 90; /* Z */
41 > const CHAR_LOWERCASE_Z = 122; /* z */
42 > const CHAR_DOT = 46; /* . */
43 > const CHAR_FORWARD_SLASH = 47; /* / */
44 > const CHAR_BACKWARD_SLASH = 92; /* \ */
45 > const CHAR_COLON = 58; /* : */
46 > const CHAR_QUESTION_MARK = 63; /* ? */
47 >
48 > class ErrorInvalidArgType extends Error {
49 > code: 'ERR_INVALID_ARG_TYPE';
50 > constructor(name: string, expected: string, actual: unknown) {
51 // determiner: 'must be' or 'must not be'
52 let determiner;
66 this.code = 'ERR_INVALID_ARG_TYPE';
67 }
68 > } path.ts
69 >
70 function validateObject(pathObject: object, name: string) {
71 if (pathObject === null || typeof pathObject !== 'object') {
73 }
74 }
75 > path.ts
76 > function validateString(value: string, name: string) { path.ts
77 > if (typeof value !== 'string') {
78 throw new ErrorInvalidArgType(name, 'string', value);
79 }
80 > } path.ts
81 > path.ts
82 > const platformIsWin32 = (process.platform === 'win32');
83 >
84 function isPathSeparator(code: number | undefined) {
85 return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH;
86 }
87 > path.ts
88 > function isPosixPathSeparator(code: number | undefined) { path.ts
89 > return code === CHAR_FORWARD_SLASH;
90 > }
91 > path.ts
92 function isWindowsDeviceRoot(code: number) {
93 return (code >= CHAR_UPPERCASE_A && code <= CHAR_UPPERCASE_Z) ||
94 (code >= CHAR_LOWERCASE_A && code <= CHAR_LOWERCASE_Z);
95 }
96 > path.ts
97 > // Resolves . and .. elements in a path with directory names
98 > function normalizeString(path: string, allowAboveRoot: boolean, separator: string, isPathSeparator: (code?: number) => boolean) { path.ts
99 > let res = '';
100 > let lastSegmentLength = 0;
101 > let lastSlash = -1;
102 > let dots = 0;
103 > let code = 0;
104 > for (let i = 0; i <= path.length; ++i) {
105 > if (i < path.length) {
106 > code = path.charCodeAt(i);
107 > }
108 > else if (isPathSeparator(code)) {
109 break;
110 }
111 > else { path.ts
112 > code = CHAR_FORWARD_SLASH;
113 > }
114 > path.ts
115 > if (isPathSeparator(code)) {
116 > if (lastSlash === i - 1 || dots === 1) {
117 > // NOOP path.ts
118 > } else if (dots === 2) { path.ts
119 > if (res.length < 2 || lastSegmentLength !== 2 || path.ts
120 > res.charCodeAt(res.length - 1) !== CHAR_DOT || path.ts
121 > res.charCodeAt(res.length - 2) !== CHAR_DOT) { path.ts
122 > if (res.length > 2) {
123 > const lastSlashIndex = res.lastIndexOf(separator);
124 > if (lastSlashIndex === -1) {
125 res = '';
126 lastSegmentLength = 0;
127 > } else { path.ts
128 > res = res.slice(0, lastSlashIndex); path.ts
129 > lastSegmentLength = res.length - 1 - res.lastIndexOf(separator);
130 > }
131 > lastSlash = i; path.ts
132 > dots = 0;
133 > continue;
134 > } else if (res.length !== 0) {
135 res = '';
136 lastSegmentLength = 0;
139 continue;
140 }
141 > } path.ts
142 if (allowAboveRoot) {
143 res += res.length > 0 ? `${separator}..` : '..';
144 lastSegmentLength = 2;
145 }
146 > } else { path.ts
147 > if (res.length > 0) {
148 > res += `${separator}${path.slice(lastSlash + 1, i)}`; path.ts
149 > }
150 > else { path.ts
151 > res = path.slice(lastSlash + 1, i);
152 > }
153 > lastSegmentLength = i - lastSlash - 1;
154 > }
155 > lastSlash = i; path.ts
156 > dots = 0;
157 > } else if (code === CHAR_DOT && dots !== -1) {
158 > ++dots; path.ts
159 > } else { path.ts
160 > dots = -1;
161 > }
162 > } path.ts
163 > return res;
164 > }
165 > path.ts
166 function formatExt(ext: string): string {
167 return ext ? `${ext[0] === '.' ? '' : '.'}${ext}` : '';
168 }
169 > path.ts
170 function _format(sep: string, pathObject: ParsedPath) {
171 validateObject(pathObject, 'pathObject');
178 return dir === pathObject.root ? `${dir}${base}` : `${dir}${sep}${base}`;
179 }
180 > path.ts
181 > export interface ParsedPath {
182 > root: string;
183 > dir: string;
184 > base: string;
185 > ext: string;
186 > name: string;
187 > }
188 >
189 > export interface IPath {
190 > normalize(path: string): string;
191 > isAbsolute(path: string): boolean;
192 > join(...paths: string[]): string;
193 > resolve(...pathSegments: string[]): string;
194 > relative(from: string, to: string): string;
195 > dirname(path: string): string;
196 > basename(path: string, suffix?: string): string;
197 > extname(path: string): string;
198 > format(pathObject: ParsedPath): string;
199 > parse(path: string): ParsedPath;
200 > toNamespacedPath(path: string): string;
201 > sep: '\\' | '/';
202 > delimiter: string;
203 > win32: IPath | null;
204 > posix: IPath | null;
205 > }
206 >
207 > export const win32: IPath = {
208 > // path.resolve([from ...], to)
209 > resolve(...pathSegments: string[]): string {
210 let resolvedDevice = '';
211 let resolvedTail = '';
343 `${resolvedDevice}${resolvedTail}` || '.';
344 },
345 > path.ts
346 > normalize(path: string): string {
347 validateString(path, 'path');
348 const len = path.length;
450 return isAbsolute ? `${device}\\${tail}` : `${device}${tail}`;
451 },
452 > path.ts
453 > isAbsolute(path: string): boolean {
454 validateString(path, 'path');
455 const len = path.length;
466 isPathSeparator(path.charCodeAt(2)));
467 },
468 > path.ts
469 > join(...paths: string[]): string {
470 if (paths.length === 0) {
471 return '.';
536 return win32.normalize(joined);
537 },
538 > path.ts
539 >
540 > // It will solve the relative path from `from` to `to`, for instance:
541 > // from = 'C:\\orandea\\test\\aaa'
542 > // to = 'C:\\orandea\\impl\\bbb'
543 > // The output of the function should be: '..\\..\\impl\\bbb'
544 > relative(from: string, to: string): string {
545 validateString(from, 'from');
546 validateString(to, 'to');
699 return toOrig.slice(toStart, toEnd);
700 },
701 > path.ts
702 > toNamespacedPath(path: string): string {
703 // Note: this will *probably* throw somewhere.
704 if (typeof path !== 'string' || path.length === 0) {
730 return resolvedPath;
731 },
732 > path.ts
733 > dirname(path: string): string {
734 validateString(path, 'path');
735 const len = path.length;
818 return path.slice(0, end);
819 },
820 > path.ts
821 > basename(path: string, suffix?: string): string {
822 if (suffix !== undefined) {
823 validateString(suffix, 'suffix');
906 return path.slice(start, end);
907 },
908 > path.ts
909 > extname(path: string): string {
910 validateString(path, 'path');
911 let start = 0;
972 return path.slice(startDot, end);
973 },
974 > path.ts
975 > format: _format.bind(null, '\\'),
976 >
977 > parse(path) {
978 validateString(path, 'path');
979
1126 return ret;
1127 },
1128 > path.ts
1129 > sep: '\\',
1130 > delimiter: ';',
1131 > win32: null,
1132 > posix: null
1133 > };
1134 >
1135 > const posixCwd = (() => {
1136 > if (platformIsWin32) {
1137 // Converts Windows' backslash path separators to POSIX forward slashes
1138 // and truncates any drive indicator
1143 };
1144 }
1145 > path.ts
1146 > // We're already on POSIX, no need for any transformations
1147 > return () => process.cwd();
1148 > })();
1149 >
1150 > export const posix: IPath = {
1151 > // path.resolve([from ...], to)
1152 > resolve(...pathSegments: string[]): string {
1153 let resolvedPath = '';
1154 let resolvedAbsolute = false;
1186 return resolvedPath.length > 0 ? resolvedPath : '.';
1187 },
1188 > path.ts
1189 > normalize(path: string): string {
1190 > validateString(path, 'path'); path.ts
1191 >
1192 > if (path.length === 0) {
1193 return '.';
1194 }
1195 > path.ts
1196 > const isAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH;
1197 > const trailingSeparator =
1198 > path.charCodeAt(path.length - 1) === CHAR_FORWARD_SLASH;
1199 >
1200 > // Normalize the path
1201 > path = normalizeString(path, !isAbsolute, '/', isPosixPathSeparator);
1202 >
1203 > if (path.length === 0) {
1204 if (isAbsolute) {
1205 return '/';
1207 return trailingSeparator ? './' : '.';
1208 }
1209 > if (trailingSeparator) { path.ts
1210 path += '/';
1211 }
1212 > path.ts
1213 > return isAbsolute ? `/${path}` : path; path.ts
1214 > },
1215 > path.ts
1216 > isAbsolute(path: string): boolean {
1217 validateString(path, 'path');
1218 return path.length > 0 && path.charCodeAt(0) === CHAR_FORWARD_SLASH;
1219 },
1220 > path.ts
1221 > join(...paths: string[]): string {
1222 > if (paths.length === 0) { path.ts
1223 return '.';
1224 }
1225 > path.ts
1226 > const path = [];
1227 > for (let i = 0; i < paths.length; ++i) {
1228 > const arg = paths[i];
1229 > validateString(arg, 'path');
1230 > if (arg.length > 0) {
1231 > path.push(arg);
1232 > }
1233 > }
1234 >
1235 > if (path.length === 0) {
1236 return '.';
1237 }
1238 > path.ts
1239 > return posix.normalize(path.join('/'));
1240 > },
1241 > path.ts
1242 > relative(from: string, to: string): string {
1243 validateString(from, 'from');
1244 validateString(to, 'to');
1312 return `${out}${to.slice(toStart + lastCommonSep)}`;
1313 },
1314 > path.ts
1315 > toNamespacedPath(path: string): string {
1316 // Non-op on posix systems
1317 return path;
1318 },
1319 > path.ts
1320 > dirname(path: string): string {
1321 > validateString(path, 'path'); path.ts
1322 > if (path.length === 0) {
1323 return '.';
1324 }
1325 > const hasRoot = path.charCodeAt(0) === CHAR_FORWARD_SLASH; path.ts
1326 > let end = -1;
1327 > let matchedSlash = true;
1328 > for (let i = path.length - 1; i >= 1; --i) {
1329 > if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) {
1330 > if (!matchedSlash) { path.ts
1331 > end = i;
1332 > break;
1333 > }
1334 > } else { path.ts
1335 > // We saw the first non-path separator
1336 > matchedSlash = false;
1337 > }
1338 > }
1339 >
1340 > if (end === -1) {
1341 > return hasRoot ? '/' : '.'; path.ts
1342 > }
1343 > if (hasRoot && end === 1) { path.ts
1344 return '//';
1345 }
1346 > return path.slice(0, end); path.ts
1347 > }, path.ts
1348 > path.ts
1349 > basename(path: string, suffix?: string): string {
1350 > if (suffix !== undefined) { path.ts
1351 validateString(suffix, 'suffix');
1352 }
1353 > validateString(path, 'path'); path.ts
1354 >
1355 > let start = 0;
1356 > let end = -1;
1357 > let matchedSlash = true;
1358 > let i;
1359 >
1360 > if (suffix !== undefined && suffix.length > 0 && suffix.length <= path.length) {
1361 if (suffix === path) {
1362 return '';
1405 return path.slice(start, end);
1406 }
1407 > for (i = path.length - 1; i >= 0; --i) { path.ts
1408 > if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) { path.ts
1409 > // If we reached a path separator that was not part of a set of path path.ts
1410 > // separators at the end of the string, stop now
1411 > if (!matchedSlash) {
1412 > start = i + 1;
1413 > break;
1414 > }
1415 > } else if (end === -1) { path.ts
1416 > // We saw the first non-path separator, mark this as the end of our
1417 > // path component
1418 > matchedSlash = false;
1419 > end = i + 1;
1420 > }
1421 > }
1422 > path.ts
1423 > if (end === -1) {
1424 return '';
1425 }
1426 > return path.slice(start, end); path.ts
1427 > }, path.ts
1428 > path.ts
1429 > extname(path: string): string {
1430 validateString(path, 'path');
1431 let startDot = -1;
1480 return path.slice(startDot, end);
1481 },
1482 > path.ts
1483 > format: _format.bind(null, '/'),
1484 >
1485 > parse(path: string): ParsedPath {
1486 validateString(path, 'path');
1487
1565 return ret;
1566 },
1567 > path.ts
1568 > sep: '/',
1569 > delimiter: ':',
1570 > win32: null,
1571 > posix: null
1572 > };
1573 >
1574 > posix.win32 = win32.win32 = win32;
1575 > posix.posix = win32.posix = posix;
1576 >
1577 > export const normalize = (platformIsWin32 ? win32.normalize : posix.normalize);
1578 > export const isAbsolute = (platformIsWin32 ? win32.isAbsolute : posix.isAbsolute);
1579 > export const join = (platformIsWin32 ? win32.join : posix.join);
1580 > export const resolve = (platformIsWin32 ? win32.resolve : posix.resolve);
1581 > export const relative = (platformIsWin32 ? win32.relative : posix.relative);
1582 > export const dirname = (platformIsWin32 ? win32.dirname : posix.dirname);
1583 > export const basename = (platformIsWin32 ? win32.basename : posix.basename);
1584 > export const extname = (platformIsWin32 ? win32.extname : posix.extname);
1585 > export const format = (platformIsWin32 ? win32.format : posix.format);
1586 > export const parse = (platformIsWin32 ? win32.parse : posix.parse);
1587 > export const toNamespacedPath = (platformIsWin32 ? win32.toNamespacedPath : posix.toNamespacedPath);
1588 > export const sep = (platformIsWin32 ? win32.sep : posix.sep);
1589 > export const delimiter = (platformIsWin32 ? win32.delimiter : posix.delimiter);
src/vs/base/common/network.ts 310 covered LOC · 27 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- network.ts
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 > import * as errors from './errors.js';
7 > import * as platform from './platform.js';
8 > import { equalsIgnoreCase, startsWithIgnoreCase } from './strings.js';
9 > import { URI } from './uri.js';
10 > import * as paths from './path.js';
11 >
12 > export namespace Schemas {
13 >
14 > /**
15 > * A schema that is used for models that exist in memory
16 > * only and that have no correspondence on a server or such.
17 > */
18 > export const inMemory = 'inmemory';
19 >
20 > /**
21 > * A schema that is used for setting files
22 > */
23 > export const vscode = 'vscode';
24 >
25 > /**
26 > * A schema that is used for internal private files
27 > */
28 > export const internal = 'private';
29 >
30 > /**
31 > * A walk-through document.
32 > */
33 > export const walkThrough = 'walkThrough';
34 >
35 > /**
36 > * An embedded code snippet.
37 > */
38 > export const walkThroughSnippet = 'walkThroughSnippet';
39 >
40 > export const http = 'http';
41 >
42 > export const https = 'https';
43 >
44 > export const file = 'file';
45 >
46 > export const mailto = 'mailto';
47 >
48 > export const untitled = 'untitled';
49 >
50 > export const data = 'data';
51 >
52 > export const command = 'command';
53 >
54 > export const vscodeRemote = 'vscode-remote';
55 >
56 > export const vscodeRemoteResource = 'vscode-remote-resource';
57 >
58 > export const vscodeManagedRemoteResource = 'vscode-managed-remote-resource';
59 >
60 > export const vscodeUserData = 'vscode-userdata';
61 >
62 > export const vscodeCustomEditor = 'vscode-custom-editor';
63 >
64 > export const vscodeNotebookCell = 'vscode-notebook-cell';
65 > export const vscodeNotebookCellMetadata = 'vscode-notebook-cell-metadata';
66 > export const vscodeNotebookCellMetadataDiff = 'vscode-notebook-cell-metadata-diff';
67 > export const vscodeNotebookCellOutput = 'vscode-notebook-cell-output';
68 > export const vscodeNotebookCellOutputDiff = 'vscode-notebook-cell-output-diff';
69 > export const vscodeNotebookMetadata = 'vscode-notebook-metadata';
70 > export const vscodeInteractiveInput = 'vscode-interactive-input';
71 >
72 > export const vscodeSettings = 'vscode-settings';
73 >
74 > export const vscodeWorkspaceTrust = 'vscode-workspace-trust';
75 >
76 > export const vscodeTerminal = 'vscode-terminal';
77 >
78 > /** Scheme used for the image carousel editor. */
79 > export const vscodeImageCarousel = 'vscode-image-carousel';
80 >
81 > /** Scheme used for code blocks in chat. */
82 > export const vscodeChatCodeBlock = 'vscode-chat-code-block';
83 >
84 > /** Scheme used for LHS of code compare (aka diff) blocks in chat. */
85 > export const vscodeChatCodeCompareBlock = 'vscode-chat-code-compare-block';
86 >
87 > /** Scheme used for the chat input editor. */
88 > export const vscodeChatEditor = 'vscode-chat-editor';
89 >
90 > /** Scheme used for the chat input part */
91 > export const vscodeChatInput = 'chatSessionInput';
92 >
93 > /** Scheme used for local chat session content */
94 > export const vscodeLocalChatSession = 'vscode-chat-session';
95 >
96 > /**
97 > * Scheme used internally for webviews that aren't linked to a resource (i.e. not custom editors)
98 > */
99 > export const webviewPanel = 'webview-panel';
100 >
101 > /**
102 > * Scheme used for loading the wrapper html and script in webviews.
103 > */
104 > export const vscodeWebview = 'vscode-webview';
105 >
106 > /**
107 > * Scheme used for integrated browser tabs using WebContentsView.
108 > */
109 > export const vscodeBrowser = 'vscode-browser';
110 >
111 > /**
112 > * Scheme used for extension pages
113 > */
114 > export const extension = 'extension';
115 >
116 > /**
117 > * Scheme used as a replacement of `file` scheme to load
118 > * files with our custom protocol handler (desktop only).
119 > */
120 > export const vscodeFileResource = 'vscode-file';
121 >
122 > /**
123 > * Scheme used for temporary resources
124 > */
125 > export const tmp = 'tmp';
126 >
127 > /**
128 > * Scheme used vs live share
129 > */
130 > export const vsls = 'vsls';
131 >
132 > /**
133 > * Scheme used for the Source Control commit input's text document
134 > */
135 > export const vscodeSourceControl = 'vscode-scm';
136 >
137 > /**
138 > * Scheme used for input box for creating comments.
139 > */
140 > export const commentsInput = 'comment';
141 >
142 > /**
143 > * Scheme used for special rendering of settings in the release notes
144 > */
145 > export const codeSetting = 'code-setting';
146 >
147 > /**
148 > * Scheme used for output panel resources
149 > */
150 > export const outputChannel = 'output';
151 >
152 > /**
153 > * Scheme used for the accessible view
154 > */
155 > export const accessibleView = 'accessible-view';
156 >
157 > /**
158 > * Used for snapshots of chat edits
159 > */
160 > export const chatEditingSnapshotScheme = 'chat-editing-snapshot-text-model';
161 > export const chatEditingModel = 'chat-editing-text-model';
162 >
163 > /**
164 > * Used for rendering multidiffs in copilot agent sessions
165 > */
166 > export const copilotPr = 'copilot-pr';
167 > }
168 >
169 > export function matchesScheme(target: URI | string, scheme: string): boolean {
170 if (URI.isUri(target)) {
171 return equalsIgnoreCase(target.scheme, scheme);
174 }
175 }
176 > network.ts
177 > export function matchesSomeScheme(target: URI | string, ...schemes: string[]): boolean {
178 return schemes.some(scheme => matchesScheme(target, scheme));
179 }
180 > network.ts
181 > export const connectionTokenCookieName = 'vscode-tkn';
182 > export const connectionTokenQueryName = 'tkn';
183 >
184 > class RemoteAuthoritiesImpl {
185 > private readonly _hosts: { [authority: string]: string | undefined } = Object.create(null);
186 > private readonly _ports: { [authority: string]: number | undefined } = Object.create(null);
187 > private readonly _connectionTokens: { [authority: string]: string | undefined } = Object.create(null);
188 > private _preferredWebSchema: 'http' | 'https' = 'http';
189 > private _delegate: ((uri: URI) => URI) | null = null;
190 > private _serverRootPath: string = '/';
191 >
192 > setPreferredWebSchema(schema: 'http' | 'https') {
193 this._preferredWebSchema = schema;
194 }
195 > network.ts
196 > setDelegate(delegate: (uri: URI) => URI): void {
197 this._delegate = delegate;
198 }
199 > network.ts
200 > setServerRootPath(product: { quality?: string; commit?: string }, serverBasePath: string | undefined): void {
201 this._serverRootPath = paths.posix.join(serverBasePath ?? '/', getServerProductSegment(product));
202 }
203 > network.ts
204 > getServerRootPath(): string {
205 return this._serverRootPath;
206 }
207 > network.ts
208 > private get _remoteResourcesPath(): string {
209 return paths.posix.join(this._serverRootPath, Schemas.vscodeRemoteResource);
210 }
211 > network.ts
212 > set(authority: string, host: string, port: number): void {
213 this._hosts[authority] = host;
214 this._ports[authority] = port;
215 }
216 > network.ts
217 > setConnectionToken(authority: string, connectionToken: string): void {
218 this._connectionTokens[authority] = connectionToken;
219 }
220 > network.ts
221 > getPreferredWebSchema(): 'http' | 'https' {
222 return this._preferredWebSchema;
223 }
224 > network.ts
225 > rewrite(uri: URI): URI {
226 if (this._delegate) {
227 try {
250 });
251 }
252 > } network.ts
253 >
254 > export const RemoteAuthorities = new RemoteAuthoritiesImpl();
255 >
256 > export function getServerProductSegment(product: { quality?: string; commit?: string }) {
257 return `${product.quality ?? 'oss'}-${product.commit ?? 'dev'}`;
258 }
259 > network.ts
260 > /**
261 > * A string pointing to a path inside the app. It should not begin with ./ or ../
262 > */
263 > export type AppResourcePath = (
264 > `a${string}` | `b${string}` | `c${string}` | `d${string}` | `e${string}` | `f${string}`
265 > | `g${string}` | `h${string}` | `i${string}` | `j${string}` | `k${string}` | `l${string}`
266 > | `m${string}` | `n${string}` | `o${string}` | `p${string}` | `q${string}` | `r${string}`
267 > | `s${string}` | `t${string}` | `u${string}` | `v${string}` | `w${string}` | `x${string}`
268 > | `y${string}` | `z${string}`
269 > );
270 >
271 > export const builtinExtensionsPath: AppResourcePath = 'vs/../../extensions';
272 > export const nodeModulesPath: AppResourcePath = 'vs/../../node_modules';
273 > export const nodeModulesAsarPath: AppResourcePath = 'vs/../../node_modules.asar';
274 > export const nodeModulesAsarUnpackedPath: AppResourcePath = 'vs/../../node_modules.asar.unpacked';
275 >
276 > export const VSCODE_AUTHORITY = 'vscode-app';
277 >
278 > class FileAccessImpl {
279 >
280 > private static readonly FALLBACK_AUTHORITY = VSCODE_AUTHORITY;
281 >
282 > /**
283 > * Returns a URI to use in contexts where the browser is responsible
284 > * for loading (e.g. fetch()) or when used within the DOM.
285 > *
286 > * **Note:** use `dom.ts#asCSSUrl` whenever the URL is to be used in CSS context.
287 > */
288 > asBrowserUri(resourcePath: AppResourcePath | ''): URI {
289 const uri = this.toUri(resourcePath);
290 return this.uriToBrowserUri(uri);
291 }
292 > network.ts
293 > /**
294 > * Returns a URI to use in contexts where the browser is responsible
295 > * for loading (e.g. fetch()) or when used within the DOM.
296 > *
297 > * **Note:** use `dom.ts#asCSSUrl` whenever the URL is to be used in CSS context.
298 > */
299 > uriToBrowserUri(uri: URI): URI {
300 // Handle remote URIs via `RemoteAuthorities`
301 if (uri.scheme === Schemas.vscodeRemote) {
328 return uri;
329 }
330 > network.ts
331 > /**
332 > * Returns the `file` URI to use in contexts where node.js
333 > * is responsible for loading.
334 > */
335 > asFileUri(resourcePath: AppResourcePath | ''): URI {
336 > const uri = this.toUri(resourcePath); network.ts
337 > return this.uriToFileUri(uri);
338 > }
339 > network.ts
340 > /**
341 > * Returns the `file` URI to use in contexts where node.js
342 > * is responsible for loading.
343 > */
344 > uriToFileUri(uri: URI): URI {
345 > // Only convert the URI if it is `vscode-file:` scheme network.ts
346 > if (uri.scheme === Schemas.vscodeFileResource) {
347 return uri.with({
348 scheme: Schemas.file,
355 });
356 }
357 > network.ts
358 > return uri;
359 > } network.ts
360 > network.ts
361 > private toUri(uriOrModule: URI | string): URI {
362 > if (URI.isUri(uriOrModule)) { network.ts
363 return uriOrModule;
364 }
365 > network.ts
366 > if (globalThis._VSCODE_FILE_ROOT) {
367 > const rootUriOrPath = globalThis._VSCODE_FILE_ROOT;
368 >
369 > // File URL (with scheme)
370 > if (/^\w[\w\d+.-]*:\/\//.test(rootUriOrPath)) {
371 > return URI.joinPath(URI.parse(rootUriOrPath, true), uriOrModule);
372 > }
373
374 // File Path (no scheme)
378
379 throw new Error('Cannot determine URI for module id!');
380 > } network.ts
381 > } network.ts
382 >
383 > export const FileAccess = new FileAccessImpl();
384 >
385 > export const CacheControlheaders: Record<string, string> = Object.freeze({
386 > 'Cache-Control': 'no-cache, no-store'
387 > });
388 >
389 > export const DocumentPolicyheaders: Record<string, string> = Object.freeze({
390 > 'Document-Policy': 'include-js-call-stacks-in-crash-reports'
391 > });
392 >
393 > export namespace COI {
394 >
395 > const coiHeaders = new Map<'3' | '2' | '1' | string, Record<string, string>>([
396 > ['1', { 'Cross-Origin-Opener-Policy': 'same-origin' }],
397 > ['2', { 'Cross-Origin-Embedder-Policy': 'require-corp' }],
398 > ['3', { 'Cross-Origin-Opener-Policy': 'same-origin', 'Cross-Origin-Embedder-Policy': 'require-corp' }],
399 > ]);
400 >
401 > export const CoopAndCoep = Object.freeze(coiHeaders.get('3'));
402 >
403 > const coiSearchParamName = 'vscode-coi';
404 >
405 > /**
406 > * Extract desired headers from `vscode-coi` invocation
407 > */
408 > export function getHeadersFromQuery(url: string | URI | URL): Record<string, string> | undefined {
409 let params: URLSearchParams | undefined;
410 if (typeof url === 'string') {
421 return coiHeaders.get(value);
422 }
423 > network.ts
424 > /**
425 > * Add the `vscode-coi` query attribute based on wanting `COOP` and `COEP`. Will be a noop when `crossOriginIsolated`
426 > * isn't enabled the current context
427 > */
428 > export function addSearchParam(urlOrSearch: URLSearchParams | Record<string, string>, coop: boolean, coep: boolean): void {
429 if (!(globalThis as typeof globalThis & { crossOriginIsolated?: boolean }).crossOriginIsolated) {
430 // depends on the current context being COI
src/vs/base/node/pfs.ts 306 covered LOC · 32 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- pfs.ts
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 > import * as fs from 'fs';
7 > import { tmpdir } from 'os';
8 > import { promisify } from 'util';
9 > import { ResourceQueue, timeout } from '../common/async.js';
10 > import { isEqualOrParent, isRootOrDriveLetter, randomPath } from '../common/extpath.js';
11 > import { normalizeNFC } from '../common/normalization.js';
12 > import { basename, dirname, join, normalize, sep } from '../common/path.js';
13 > import { isLinux, isMacintosh, isWindows } from '../common/platform.js';
14 > import { extUriBiasedIgnorePathCase } from '../common/resources.js';
15 > import { URI } from '../common/uri.js';
16 > import { CancellationToken } from '../common/cancellation.js';
17 > import { rtrim } from '../common/strings.js';
18 >
19 > //#region rimraf
20 >
21 > export enum RimRafMode {
22 >
23 > /**
24 > * Slow version that unlinks each file and folder.
25 > */
26 > UNLINK,
27 >
28 > /**
29 > * Fast version that first moves the file/folder
30 > * into a temp directory and then deletes that
31 > * without waiting for it.
32 > */
33 > MOVE
34 > }
35 >
36 > /**
37 > * Allows to delete the provided path (either file or folder) recursively
38 > * with the options:
39 > * - `UNLINK`: direct removal from disk
40 > * - `MOVE`: faster variant that first moves the target to temp dir and then
41 > * deletes it in the background without waiting for that to finish.
42 > * the optional `moveToPath` allows to override where to rename the
43 > * path to before deleting it.
44 > */
45 > async function rimraf(path: string, mode: RimRafMode.UNLINK): Promise<void>;
46 > async function rimraf(path: string, mode: RimRafMode.MOVE, moveToPath?: string): Promise<void>;
47 > async function rimraf(path: string, mode?: RimRafMode, moveToPath?: string): Promise<void>;
48 async function rimraf(path: string, mode = RimRafMode.UNLINK, moveToPath?: string): Promise<void> {
49 if (isRootOrDriveLetter(path)) {
59 return rimrafMove(path, moveToPath);
60 }
61 > pfs.ts
62 async function rimrafMove(path: string, moveToPath = randomPath(tmpdir())): Promise<void> {
63 try {
80 }
81 }
82 > pfs.ts
83 async function rimrafUnlink(path: string): Promise<void> {
84 return fs.promises.rm(path, { recursive: true, force: true, maxRetries: 3 });
85 }
86 > pfs.ts
87 > //#endregion
88 >
89 > //#region readdir with NFC support (macos)
90 >
91 > export interface IDirent {
92 > name: string;
93 >
94 > isFile(): boolean;
95 > isDirectory(): boolean;
96 > isSymbolicLink(): boolean;
97 > }
98 >
99 > /**
100 > * Drop-in replacement of `fs.readdir` with support
101 > * for converting from macOS NFD unicon form to NFC
102 > * (https://github.com/nodejs/node/issues/2165)
103 > */
104 > async function readdir(path: string): Promise<string[]>;
105 > async function readdir(path: string, options: { withFileTypes: true }): Promise<IDirent[]>;
106 async function readdir(path: string, options?: { withFileTypes: true }): Promise<(string | IDirent)[]> {
107 try {
121 }
122 }
123 > pfs.ts
124 async function doReaddir(path: string, options?: { withFileTypes: true }): Promise<(string | IDirent)[]> {
125 return handleDirectoryChildren(await (options ? safeReaddirWithFileTypes(path) : fs.promises.readdir(path)));
126 }
127 > pfs.ts
128 async function safeReaddirWithFileTypes(path: string): Promise<IDirent[]> {
129 try {
170 return result;
171 }
172 > pfs.ts
173 > function handleDirectoryChildren(children: string[]): string[];
174 > function handleDirectoryChildren(children: IDirent[]): IDirent[];
175 > function handleDirectoryChildren(children: (string | IDirent)[]): (string | IDirent)[];
176 function handleDirectoryChildren(children: (string | IDirent)[]): (string | IDirent)[] {
177 return children.map(child => {
189 });
190 }
191 > pfs.ts
192 > /**
193 > * A convenience method to read all children of a path that
194 > * are directories.
195 > */
196 async function readDirsInDir(dirPath: string): Promise<string[]> {
197 const children = await readdir(dirPath);
206 return directories;
207 }
208 > pfs.ts
209 > //#endregion
210 >
211 > //#region whenDeleted()
212 >
213 > /**
214 > * A `Promise` that resolves when the provided `path`
215 > * is deleted from disk.
216 > */
217 > export function whenDeleted(path: string, intervalMs = 1000): Promise<void> {
218 return new Promise<void>(resolve => {
219 let running = false;
233 });
234 }
235 > pfs.ts
236 > //#endregion
237 >
238 > //#region Methods with symbolic links support
239 >
240 > export namespace SymlinkSupport {
241 >
242 > export interface IStats {
243 >
244 > // The stats of the file. If the file is a symbolic
245 > // link, the stats will be of that target file and
246 > // not the link itself.
247 > // If the file is a symbolic link pointing to a non
248 > // existing file, the stat will be of the link and
249 > // the `dangling` flag will indicate this.
250 > stat: fs.Stats;
251 >
252 > // Will be provided if the resource is a symbolic link
253 > // on disk. Use the `dangling` flag to find out if it
254 > // points to a resource that does not exist on disk.
255 > symbolicLink?: { dangling: boolean };
256 > }
257 >
258 > /**
259 > * Resolves the `fs.Stats` of the provided path. If the path is a
260 > * symbolic link, the `fs.Stats` will be from the target it points
261 > * to. If the target does not exist, `dangling: true` will be returned
262 > * as `symbolicLink` value.
263 > */
264 > export async function stat(path: string): Promise<IStats> {
265
266 // First stat the link
313 }
314 }
315 > pfs.ts
316 > /**
317 > * Figures out if the `path` exists and is a file with support
318 > * for symlinks.
319 > *
320 > * Note: this will return `false` for a symlink that exists on
321 > * disk but is dangling (pointing to a nonexistent path).
322 > *
323 > * Use `exists` if you only care about the path existing on disk
324 > * or not without support for symbolic links.
325 > */
326 > export async function existsFile(path: string): Promise<boolean> {
327 try {
328 const { stat, symbolicLink } = await SymlinkSupport.stat(path);
335 return false;
336 }
337 > pfs.ts
338 > /**
339 > * Figures out if the `path` exists and is a directory with support for
340 > * symlinks.
341 > *
342 > * Note: this will return `false` for a symlink that exists on
343 > * disk but is dangling (pointing to a nonexistent path).
344 > *
345 > * Use `exists` if you only care about the path existing on disk
346 > * or not without support for symbolic links.
347 > */
348 > export async function existsDirectory(path: string): Promise<boolean> {
349 try {
350 const { stat, symbolicLink } = await SymlinkSupport.stat(path);
357 return false;
358 }
359 > } pfs.ts
360 >
361 > //#endregion
362 >
363 > //#region Write File
364 >
365 > // According to node.js docs (https://nodejs.org/docs/v14.16.0/api/fs.html#fs_fs_writefile_file_data_options_callback)
366 > // it is not safe to call writeFile() on the same path multiple times without waiting for the callback to return.
367 > // Therefor we use a Queue on the path that is given to us to sequentialize calls to the same path properly.
368 > const writeQueues = new ResourceQueue();
369 >
370 > /**
371 > * Same as `fs.writeFile` but with an additional call to
372 > * `fs.fdatasync` after writing to ensure changes are
373 > * flushed to disk.
374 > *
375 > * In addition, multiple writes to the same path are queued.
376 > */
377 > function writeFile(path: string, data: string, options?: IWriteFileOptions): Promise<void>;
378 > function writeFile(path: string, data: Buffer, options?: IWriteFileOptions): Promise<void>;
379 > function writeFile(path: string, data: Uint8Array, options?: IWriteFileOptions): Promise<void>;
380 > function writeFile(path: string, data: string | Buffer | Uint8Array, options?: IWriteFileOptions): Promise<void>;
381 function writeFile(path: string, data: string | Buffer | Uint8Array, options?: IWriteFileOptions): Promise<void> {
382 return writeQueues.queueFor(URI.file(path), () => {
386 }, extUriBiasedIgnorePathCase);
387 }
388 > pfs.ts
389 > interface IWriteFileOptions {
390 > mode?: number;
391 > flag?: string;
392 > }
393 >
394 > interface IEnsuredWriteFileOptions extends IWriteFileOptions {
395 > mode: number;
396 > flag: string;
397 > }
398 >
399 > let canFlush = true;
400 > export function configureFlushOnWrite(enabled: boolean): void {
401 canFlush = enabled;
402 }
403 > pfs.ts
404 > // Calls fs.writeFile() followed by a fs.sync() call to flush the changes to disk
405 > // We do this in cases where we want to make sure the data is really on disk and
406 > // not in some cache.
407 > //
408 > // See https://github.com/nodejs/node/blob/v5.10.0/lib/fs.js#L1194
409 function doWriteFileAndFlush(path: string, data: string | Buffer | Uint8Array, options: IEnsuredWriteFileOptions, callback: (error: Error | null) => void): void {
410 if (!canFlush) {
440 });
441 }
442 > pfs.ts
443 > /**
444 > * Same as `fs.writeFileSync` but with an additional call to
445 > * `fs.fdatasyncSync` after writing to ensure changes are
446 > * flushed to disk.
447 > *
448 > * @deprecated always prefer async variants over sync!
449 > */
450 > export function writeFileSync(path: string, data: string | Buffer, options?: IWriteFileOptions): void {
451 const ensuredOptions = ensureWriteOptions(options);
452
474 }
475 }
476 > pfs.ts
477 function ensureWriteOptions(options?: IWriteFileOptions): IEnsuredWriteFileOptions {
478 if (!options) {
485 };
486 }
487 > pfs.ts
488 > //#endregion
489 >
490 > //#region Move / Copy
491 >
492 > /**
493 > * A drop-in replacement for `fs.rename` that:
494 > * - allows to move across multiple disks
495 > * - attempts to retry the operation for certain error codes on Windows
496 > */
497 async function rename(source: string, target: string, windowsRetryTimeout: number | false = 60000): Promise<void> {
498 if (source === target) {
525 }
526 }
527 > pfs.ts
528 async function renameWithRetry(source: string, target: string, startTime: number, retryTimeout: number, attempt = 0): Promise<void> {
529 try {
563 }
564 }
565 > pfs.ts
566 > interface ICopyPayload {
567 > readonly root: { source: string; target: string };
568 > readonly options: { preserveSymlinks: boolean };
569 > readonly handledSourcePaths: Set<string>;
570 > }
571 >
572 > /**
573 > * Recursively copies all of `source` to `target`.
574 > *
575 > * The options `preserveSymlinks` configures how symbolic
576 > * links should be handled when encountered. Set to
577 > * `false` to not preserve them and `true` otherwise.
578 > */
579 async function copy(source: string, target: string, options: { preserveSymlinks: boolean }): Promise<void> {
580 return doCopy(source, target, { root: { source, target }, options, handledSourcePaths: new Set<string>() });
581 }
582 > pfs.ts
583 > // When copying a file or folder, we want to preserve the mode
584 > // it had and as such provide it when creating. However, modes
585 > // can go beyond what we expect (see link below), so we mask it.
586 > // (https://github.com/nodejs/node-v0.x-archive/issues/3045#issuecomment-4862588)
587 > const COPY_MODE_MASK = 0o777;
588 >
589 async function doCopy(source: string, target: string, payload: ICopyPayload): Promise<void> {
590
626 }
627 }
628 > pfs.ts
629 async function doCopyDirectory(source: string, target: string, mode: number, payload: ICopyPayload): Promise<void> {
630
638 }
639 }
640 > pfs.ts
641 async function doCopyFile(source: string, target: string, mode: number): Promise<void> {
642
647 await fs.promises.chmod(target, mode);
648 }
649 > pfs.ts
650 async function doCopySymlink(source: string, target: string, payload: ICopyPayload): Promise<void> {
651
664 await fs.promises.symlink(linkTarget, target);
665 }
666 > pfs.ts
667 > //#endregion
668 >
669 > //#region Path resolvers
670 >
671 > /**
672 > * Given an absolute, normalized, and existing file path 'realcase' returns the
673 > * exact path that the file has on disk.
674 > * On a case insensitive file system, the returned path might differ from the original
675 > * path by character casing.
676 > * On a case sensitive file system, the returned path will always be identical to the
677 > * original path.
678 > * In case of errors, null is returned. But you cannot use this function to verify that
679 > * a path exists.
680 > *
681 > * realcase does not handle '..' or '.' path segments and it does not take the locale into account.
682 > */
683 export async function realcase(path: string, token?: CancellationToken): Promise<string | null> {
684 if (isLinux) {
724 return null;
725 }
726 > pfs.ts
727 async function realpath(path: string): Promise<string> {
728 try {
746 }
747 }
748 > pfs.ts
749 > /**
750 > * @deprecated always prefer async variants over sync!
751 > */
752 > export function realpathSync(path: string): string {
753 try {
754 return fs.realpathSync(path);
767 }
768 }
769 > pfs.ts
770 function normalizePath(path: string): string {
771 return rtrim(normalize(path), sep);
772 }
773 > pfs.ts
774 > //#endregion
775 >
776 > //#region Promise based fs methods
777 >
778 > /**
779 > * Some low level `fs` methods provided as `Promises` similar to
780 > * `fs.promises` but with notable differences, either implemented
781 > * by us or by restoring the original callback based behavior.
782 > *
783 > * At least `realpath` is implemented differently in the promise
784 > * based implementation compared to the callback based one. The
785 > * promise based implementation actually calls `fs.realpath.native`.
786 > * (https://github.com/microsoft/vscode/issues/118562)
787 > */
788 > export const Promises = new class {
789 >
790 > //#region Implemented by node.js
791 >
792 > get read() {
793
794 // Not using `promisify` here for a reason: the return
808 };
809 }
810 > pfs.ts
811 > get write() {
812
813 // Not using `promisify` here for a reason: the return
827 };
828 }
829 > pfs.ts
830 > get fdatasync() { return promisify(fs.fdatasync); } // not exposed as API in 22.x yet
831 >
832 > get open() { return promisify(fs.open); } // changed to return `FileHandle` in promise API
833 > get close() { return promisify(fs.close); } // not exposed as API due to the `FileHandle` return type of `open`
834 >
835 > get ftruncate() { return promisify(fs.ftruncate); } // not exposed as API in 22.x yet
836 >
837 > //#endregion
838 >
839 > //#region Implemented by us
840 >
841 > async exists(path: string): Promise<boolean> {
842 try {
843 await fs.promises.access(path);
848 }
849 }
850 > pfs.ts
851 > get readdir() { return readdir; }
852 > get readDirsInDir() { return readDirsInDir; }
853 >
854 > get writeFile() { return writeFile; }
855 >
856 > get rm() { return rimraf; }
857 >
858 > get rename() { return rename; }
859 > get copy() { return copy; }
860 >
861 > get realpath() { return realpath; } // `fs.promises.realpath` will use `fs.realpath.native` which we do not want
862 >
863 > //#endregion
864 > };
865 >
866 > //#endregion
src/vs/platform/agentHost/node/agentHostTerminalManager.ts 300 covered LOC · 52 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostTerminalManager.ts
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 > import * as fs from 'fs';
7 > import { DeferredPromise, raceCancellablePromises, timeout } from '../../../base/common/async.js';
8 > import { Emitter } from '../../../base/common/event.js';
9 > import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
10 > import { dirname, parse as pathParse } from '../../../base/common/path.js';
11 > import * as platform from '../../../base/common/platform.js';
12 > import { getSystemShell } from '../../../base/node/shell.js';
13 > import { URI } from '../../../base/common/uri.js';
14 > import { generateUuid } from '../../../base/common/uuid.js';
15 > import { AiAgentEnvValue, AiAgentEnvVar } from '../../chat/common/aiAgentEnv.js';
16 > import { createDecorator } from '../../instantiation/common/instantiation.js';
17 > import { ILogService } from '../../log/common/log.js';
18 > import { IProductService } from '../../product/common/productService.js';
19 > import { getShellIntegrationInjection } from '../../terminal/node/terminalEnvironment.js';
20 > import { AgentHostConfigKey, agentHostCustomizationConfigSchema } from '../common/agentHostCustomizationConfig.js';
21 > import { ActionType } from '../common/state/protocol/actions.js';
22 > import type { CreateTerminalParams } from '../common/state/protocol/commands.js';
23 > import { TerminalClaim, TerminalContentPart, TerminalInfo, TerminalState, TerminalClaimKind } from '../common/state/protocol/state.js';
24 > import { isTerminalAction } from '../common/state/sessionActions.js';
25 > import { ROOT_STATE_URI } from '../common/state/sessionState.js';
26 > import { IAgentConfigurationService } from './agentConfigurationService.js';
27 > import { AgentHostHeadlessTerminal } from './agentHostHeadlessTerminal.js';
28 > import { isZsh } from './agentHostShellUtils.js';
29 > import { AgentHostStateManager, IAgentHostStateManager } from './agentHostStateManager.js';
30 > import { Osc633Event, Osc633EventType, Osc633ParseSegment, Osc633Parser } from './osc633Parser.js';
31 >
32 > const WAIT_FOR_PROMPT_TIMEOUT = 10_000;
33 > const HEADLESS_TERMINAL_SCROLLBACK = 0;
34 > const DSR_CURSOR_POSITION_QUERY = '\x1b[6n';
35 > const DEC_DSR_CURSOR_POSITION_QUERY = '\x1b[?6n';
36 > const SERVER_HANDLED_QUERY_PREFIXES = ['\x1b[?6', '\x1b[?', '\x1b[6', '\x1b[', '\x1b'];
37 >
38 > export const IAgentHostTerminalManager = createDecorator<IAgentHostTerminalManager>('agentHostTerminalManager');
39 >
40 > export interface ICommandFinishedEvent {
41 > commandId: string;
42 > exitCode: number | undefined;
43 > command: string;
44 > output: string;
45 > }
46 >
47 > export interface ITerminalQueryFilterState {
48 > pendingData: string;
49 > }
50 >
51 > export interface ISendTextOptions {
52 > shouldExecute: boolean;
53 > /**
54 > * Match workbench terminal sendText: wrap in bracketed paste markers only
55 > * when requested by the caller and enabled by the terminal.
56 > */
57 > bracketedPasteMode?: boolean;
58 > }
59 >
60 > export interface IFormatTerminalTextOptions {
61 > shouldExecute: boolean;
62 > forceBracketedPasteMode?: boolean;
63 > }
64 >
65 > export function removeServerHandledTerminalQueries(data: string, state: ITerminalQueryFilterState): string {
66 if (
67 !state.pendingData
84 .replaceAll(DSR_CURSOR_POSITION_QUERY, '');
85 }
87 function getServerHandledTerminalQueryPrefix(data: string): string {
88 for (const prefix of SERVER_HANDLED_QUERY_PREFIXES) {
93 return '';
94 }
96 > export function formatTerminalText(data: string, options: IFormatTerminalTextOptions): string {
97 if (options.forceBracketedPasteMode) {
98 data = `\x1b[200~${data}\x1b[201~`;
104 return data;
105 }
107 > /**
108 > * Service interface for terminal management in the agent host.
109 > */
110 > export interface IAgentHostTerminalManager {
111 > readonly _serviceBrand: undefined;
112 > createTerminal(params: CreateTerminalParams, options?: { shell?: string; preventShellHistory?: boolean; nonInteractive?: boolean }): Promise<void>;
113 > writeInput(uri: string, data: string): void;
114 > sendText(uri: string, data: string, options: ISendTextOptions): Promise<void>;
115 > onData(uri: string, cb: (data: string) => void): IDisposable;
116 > onExit(uri: string, cb: (exitCode: number) => void): IDisposable;
117 > onClaimChanged(uri: string, cb: (claim: TerminalClaim) => void): IDisposable;
118 > onCommandFinished(uri: string, cb: (event: ICommandFinishedEvent) => void): IDisposable;
119 > createAltBufferPromise(uri: string, store: DisposableStore): Promise<void>;
120 > getContent(uri: string): string | undefined;
121 > getClaim(uri: string): TerminalClaim | undefined;
122 > hasTerminal(uri: string): boolean;
123 > getExitCode(uri: string): number | undefined;
124 > supportsCommandDetection(uri: string): boolean;
125 > disposeTerminal(uri: string): void;
126 > getTerminalInfos(): TerminalInfo[];
127 > getTerminalState(uri: string): TerminalState | undefined;
128 > getDefaultShell(): Promise<string>;
129 > createOutputTerminal(uri: string, options: { title: string; claim: TerminalClaim }): void;
130 > appendOutputTerminalData(uri: string, data: string): void;
131 > resetOutputTerminal(uri: string): void;
132 > finalizeOutputTerminal(uri: string, exitCode: number | undefined): void;
133 > }
134 >
135 > // node-pty is loaded dynamically to avoid bundling issues in non-node environments
136 > let nodePtyModule: typeof import('node-pty') | undefined;
137 async function getNodePty(): Promise<typeof import('node-pty')> {
138 if (!nodePtyModule) {
141 return nodePtyModule;
142 }
144 > /** Per-terminal command detection tracking state. */
145 > interface ICommandTracker {
146 > readonly parser: Osc633Parser;
147 > readonly nonce: string;
148 > commandCounter: number;
149 > detectionAvailableEmitted: boolean;
150 > pendingCommandLine?: string;
151 > activeCommandId?: string;
152 > activeCommandTimestamp?: number;
153 > }
154 >
155 > /** Represents a single managed terminal with its PTY process. */
156 > interface IManagedTerminal {
157 > readonly uri: string;
158 > readonly store: DisposableStore;
159 > readonly pty: import('node-pty').IPty;
160 > readonly onDataEmitter: Emitter<string>;
161 > readonly onExitEmitter: Emitter<number>;
162 > readonly onClaimChangedEmitter: Emitter<TerminalClaim>;
163 > readonly onCommandFinishedEmitter: Emitter<ICommandFinishedEvent>;
164 > title: string;
165 > cwd: string;
166 > cols: number;
167 > rows: number;
168 > content: TerminalContentPart[];
169 > contentSize: number;
170 > claim: TerminalClaim;
171 > exitCode?: number;
172 > commandTracker?: ICommandTracker;
173 > headlessTerminal?: AgentHostHeadlessTerminal;
174 > terminalQueryFilterState: ITerminalQueryFilterState;
175 > }
176 >
177 > /**
178 > * A lightweight output-only terminal channel: no PTY behind it, plain-text
179 > * content appended by its owner (e.g. runtime-executed shell tools). Served
180 > * to subscribers with `isPty: false` so clients skip VT parsing.
181 > */
182 > interface IOutputTerminal {
183 > title: string;
184 > content: TerminalContentPart[];
185 > contentSize: number;
186 > claim: TerminalClaim;
187 > exitCode?: number;
188 > }
189 >
190 > /**
191 > * Manages terminal processes for the agent host. Each terminal is backed by
192 > * a node-pty instance and identified by a protocol URI.
193 > *
194 > * Listens to the {@link AgentHostStateManager} for client-dispatched terminal
195 > * actions (input, resize, claim changes) and dispatches server-originated
196 > * PTY output back through the state manager.
197 > */
198 > export class AgentHostTerminalManager extends Disposable implements IAgentHostTerminalManager {
199 > declare readonly _serviceBrand: undefined;
200 >
201 > private readonly _terminals = new Map<string, IManagedTerminal>();
202 > private readonly _outputTerminals = new Map<string, IOutputTerminal>();
203 >
204 > constructor(
205 > @IAgentHostStateManager private readonly _stateManager: AgentHostStateManager, agentHostTerminalManager.ts
206 > @ILogService private readonly _logService: ILogService,
207 > @IProductService private readonly _productService: IProductService,
208 > @IAgentConfigurationService private readonly _configurationService: IAgentConfigurationService,
209 > ) {
210 > super();
211 >
212 > // React to client-dispatched terminal actions flowing through the state manager
213 > this._register(this._stateManager.onDidEmitEnvelope(envelope => {
214 > const action = envelope.action; agentHostTerminalManager.ts
215 > if (!isTerminalAction(action)) {
217 > }
218 const channel = envelope.channel;
219 switch (action.type) {
221 this._writeInput(channel, action.data);
222 break;
223 > case ActionType.TerminalResized: agentHostTerminalManager.ts
224 this._resize(channel, action.cols, action.rows);
225 break;
226 > case ActionType.TerminalClaimed: agentHostTerminalManager.ts
227 this._setClaim(channel, action.claim);
228 break;
229 > case ActionType.TerminalTitleChanged: agentHostTerminalManager.ts
230 this._setTitle(channel, action.title);
231 break;
232 > case ActionType.TerminalCleared: agentHostTerminalManager.ts
233 this._clearContent(channel);
234 break;
237 > }
239 > /** Get metadata for all active terminals (for root state). */
240 > getTerminalInfos(): TerminalInfo[] {
241 return [...this._terminals.values()].map(t => ({
242 resource: t.uri,
246 }));
247 }
249 > /** Get the full state for a terminal (for subscribe snapshots). */
250 > getTerminalState(uri: string): TerminalState | undefined {
251 const outputTerminal = this._outputTerminals.get(uri);
252 if (outputTerminal) {
275 };
276 }
278 > /**
279 > * Create a new terminal backed by node-pty.
280 > * Spawns the user's default shell.
281 > */
282 > async createTerminal(params: CreateTerminalParams, options?: { shell?: string; preventShellHistory?: boolean; nonInteractive?: boolean }): Promise<void> {
283 const uri = params.channel;
284 if (this._terminals.has(uri)) {
478 this._broadcastTerminalList();
479 }
481 > protected async _spawnPty(file: string, args: string[], options: import('node-pty').IPtyForkOptions | import('node-pty').IWindowsPtyForkOptions): Promise<import('node-pty').IPty> {
482 const nodePty = await getNodePty();
483 return nodePty.spawn(file, args, options);
484 }
486 > /** Send input data to a terminal's PTY process (from client-dispatched actions). */
487 > private _writeInput(uri: string, data: string): void {
488 this.writeInput(uri, data);
489 }
491 > /** Send input data to a terminal's PTY process. */
492 > writeInput(uri: string, data: string): void {
493 const terminal = this._terminals.get(uri);
494 if (terminal && terminal.exitCode === undefined) {
496 }
497 }
499 > /** Send formatted text to a terminal's PTY process. */
500 > async sendText(uri: string, data: string, options: ISendTextOptions): Promise<void> {
501 const terminal = this._terminals.get(uri);
502 let forceBracketedPasteMode = false;
507 this.writeInput(uri, formatTerminalText(data, { shouldExecute: options.shouldExecute, forceBracketedPasteMode }));
508 }
510 > /** Register a callback for PTY data events on a terminal. */
511 > onData(uri: string, cb: (data: string) => void): IDisposable {
512 const terminal = this._terminals.get(uri);
513 if (!terminal) {
516 return terminal.onDataEmitter.event(cb);
517 }
519 > /** Register a callback for PTY exit events on a terminal. */
520 > onExit(uri: string, cb: (exitCode: number) => void): IDisposable {
521 const terminal = this._terminals.get(uri);
522 if (!terminal) {
525 return terminal.onExitEmitter.event(cb);
526 }
528 > /** Register a callback for terminal claim changes. */
529 > onClaimChanged(uri: string, cb: (claim: TerminalClaim) => void): IDisposable {
530 const terminal = this._terminals.get(uri);
531 if (!terminal) {
534 return terminal.onClaimChangedEmitter.event(cb);
535 }
537 > /** Register a callback for command completion events (requires shell integration). */
538 > onCommandFinished(uri: string, cb: (event: ICommandFinishedEvent) => void): IDisposable {
539 const terminal = this._terminals.get(uri);
540 if (!terminal) {
543 return terminal.onCommandFinishedEmitter.event(cb);
544 }
546 > createAltBufferPromise(uri: string, store: DisposableStore): Promise<void> {
547 const terminal = this._terminals.get(uri);
548 if (!terminal?.headlessTerminal) {
551 return terminal.headlessTerminal.createAltBufferPromise(store);
552 }
554 > /** Get accumulated scrollback content for a terminal as raw text. */
555 > getContent(uri: string): string | undefined {
556 const terminal = this._terminals.get(uri);
557 if (!terminal) {
560 return terminal.content.map(p => p.type === 'command' ? p.output : p.value).join('');
561 }
563 > /** Get the current claim for a terminal. */
564 > getClaim(uri: string): TerminalClaim | undefined {
565 return this._terminals.get(uri)?.claim;
566 }
568 > /** Check whether a terminal exists. */
569 > hasTerminal(uri: string): boolean {
570 return this._terminals.has(uri);
571 }
573 > /** Whether the terminal has shell integration active for command detection. */
574 > supportsCommandDetection(uri: string): boolean {
575 const terminal = this._terminals.get(uri);
576 return terminal?.commandTracker?.detectionAvailableEmitted ?? false;
577 }
579 > /** Get the exit code for a terminal, or undefined if still running. */
580 > getExitCode(uri: string): number | undefined {
581 return this._terminals.get(uri)?.exitCode;
582 }
584 > /** Resize a terminal. */
585 > private _resize(uri: string, cols: number, rows: number): void {
586 const terminal = this._terminals.get(uri);
587 if (terminal && terminal.exitCode === undefined) {
592 }
593 }
595 > /** Update a terminal's claim. */
596 > private _setClaim(uri: string, claim: TerminalClaim): void {
597 const terminal = this._terminals.get(uri);
598 if (terminal) {
602 }
603 }
605 > /** Update a terminal's title. */
606 > private _setTitle(uri: string, title: string): void {
607 const terminal = this._terminals.get(uri);
608 if (terminal) {
611 }
612 }
614 > /** Clear a terminal's scrollback buffer. */
615 > private _clearContent(uri: string): void {
616 const terminal = this._terminals.get(uri);
617 if (terminal) {
621 }
622 }
624 > /** Process raw PTY output: parse OSC 633 sequences, dispatch actions, track content. */
625 > private _handlePtyData(managed: IManagedTerminal, rawData: string): void {
626 const tracker = managed.commandTracker;
627
674 this._trimContent(managed);
675 }
677 > /** Handle a parsed OSC 633 event by dispatching the appropriate protocol actions. */
678 > private _handleOsc633Event(managed: IManagedTerminal, tracker: ICommandTracker, event: Osc633Event): void {
679 // Emit TerminalCommandDetectionAvailable on first sequence
680 if (!tracker.detectionAvailableEmitted) {
775 }
776 }
778 > /** Append cleaned data to the terminal's structured content array. */
779 > private _appendToContent(managed: { content: TerminalContentPart[]; contentSize: number }, data: string): void {
780 const tail = managed.content.length > 0 ? managed.content[managed.content.length - 1] : undefined;
781
794 }
795 }
797 > private _getContentPartSize(part: TerminalContentPart): number {
798 return part.type === 'command' ? part.output.length : part.value.length;
799 }
801 > /** Trim content parts to stay within the rolling buffer limit. */
802 > private _trimContent(managed: { content: TerminalContentPart[]; contentSize: number }): void {
803 const maxSize = 100_000;
804 const targetSize = 80_000;
823 }
824 }
826 > /**
827 > * Create an output-only terminal channel. Unlike {@link createTerminal}
828 > * there is no PTY behind it: the owner appends plain-text output via
829 > * {@link appendOutputTerminalData}. The channel is not announced on the
830 > * root terminal list — clients discover it through the tool result's
831 > * terminal content block and subscribe to its URI.
832 > */
833 > createOutputTerminal(uri: string, options: { title: string; claim: TerminalClaim }): void {
834 if (this._terminals.has(uri) || this._outputTerminals.has(uri)) {
835 throw new Error(`Terminal already exists: ${uri}`);
842 });
843 }
845 > /** Append plain-text data to an output-only terminal and stream it to subscribers. */
846 > appendOutputTerminalData(uri: string, data: string): void {
847 const terminal = this._outputTerminals.get(uri);
848 if (!terminal || data.length === 0) {
856 });
857 }
859 > /** Clear an output-only terminal's content (e.g. when cumulative source output was rewritten). */
860 > resetOutputTerminal(uri: string): void {
861 const terminal = this._outputTerminals.get(uri);
862 if (!terminal) {
869 });
870 }
872 > /** Record the command's exit on an output-only terminal and notify subscribers. */
873 > finalizeOutputTerminal(uri: string, exitCode: number | undefined): void {
874 const terminal = this._outputTerminals.get(uri);
875 if (!terminal || terminal.exitCode !== undefined) {
884 }
885 }
887 > /** Dispose a terminal: kill the process and remove it. */
888 > disposeTerminal(uri: string): void {
889 if (this._outputTerminals.delete(uri)) {
890 return;
897 }
898 }
900 > async getDefaultShell(): Promise<string> {
901 const configured = this._configurationService.getRootValue(agentHostCustomizationConfigSchema, AgentHostConfigKey.DefaultShell);
902 if (configured) {
910 return getSystemShell(platform.OS, process.env);
911 }
913 > /**
914 > * Resolves the cwd string from {@link CreateTerminalParams} to an
915 > * accessible filesystem path, falling back to $HOME if the requested
916 > * directory is missing (otherwise node-pty exits silently with code 1).
917 > * Accepts either a `file://` URI string or a raw absolute filesystem path.
918 > */
919 > private async _resolveCwd(cwd: string | undefined, terminalURI: string): Promise<string> {
920 let resolved = cwd;
921 if (cwd) {
943 return fallback;
944 }
946 > /** Dispatch root/terminalsChanged with the current terminal list. */
947 > private _broadcastTerminalList(): void {
948 this._stateManager.dispatchServerAction(ROOT_STATE_URI, {
949 type: ActionType.RootTerminalsChanged,
src/vs/base/common/types.ts 299 covered LOC · 31 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- types.ts
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 > import { assert } from './assert.js';
7 >
8 > /**
9 > * @returns whether the provided parameter is a JavaScript String or not.
10 > */
11 > export function isString(str: unknown): str is string {
12 > return (typeof str === 'string'); types.ts
13 > }
14 > types.ts
15 > /**
16 > * @returns whether the provided parameter is a JavaScript Array and each element in the array is a string.
17 > */
18 > export function isStringArray(value: unknown): value is string[] {
19 return isArrayOf(value, isString);
20 }
21 > types.ts
22 > /**
23 > * @returns whether the provided parameter is a JavaScript Array and each element in the array satisfies the provided type guard.
24 > */
25 > export function isArrayOf<T>(value: unknown, check: (item: unknown) => item is T): value is T[] {
26 return Array.isArray(value) && value.every(check);
27 }
28 > types.ts
29 > /**
30 > * @returns whether the provided parameter is of type `object` but **not**
31 > * `null`, an `array`, a `regexp`, nor a `date`.
32 > */
33 > export function isObject(obj: unknown): obj is Object {
34 > // The method can't do a type cast since there are type (like strings) which types.ts
35 > // are subclasses of any put not positvely matched by the function. Hence type
36 > // narrowing results in wrong results.
37 > return typeof obj === 'object'
38 > && obj !== null types.ts
39 > && !Array.isArray(obj)
40 > && !(obj instanceof RegExp) types.ts
41 > && !(obj instanceof Date);
42 > } types.ts
43 > types.ts
44 > /**
45 > * @returns whether the provided parameter is of type `Buffer` or Uint8Array dervived type
46 > */
47 > export function isTypedArray(obj: unknown): obj is Object {
48 const TypedArray = Object.getPrototypeOf(Uint8Array);
49 return typeof obj === 'object'
50 && obj instanceof TypedArray;
51 }
52 > types.ts
53 > /**
54 > * In **contrast** to just checking `typeof` this will return `false` for `NaN`.
55 > * @returns whether the provided parameter is a JavaScript Number or not.
56 > */
57 > export function isNumber(obj: unknown): obj is number {
58 return (typeof obj === 'number' && !isNaN(obj));
59 }
60 > types.ts
61 > /**
62 > * @returns whether the provided parameter is an Iterable, casting to the given generic
63 > */
64 > export function isIterable<T>(obj: unknown): obj is Iterable<T> {
65 // eslint-disable-next-line local/code-no-any-casts
66 return !!obj && typeof (obj as any)[Symbol.iterator] === 'function';
67 }
68 > types.ts
69 > /**
70 > * @returns whether the provided parameter is an Iterable, casting to the given generic
71 > */
72 > export function isAsyncIterable<T>(obj: unknown): obj is AsyncIterable<T> {
73 // eslint-disable-next-line local/code-no-any-casts
74 return !!obj && typeof (obj as any)[Symbol.asyncIterator] === 'function';
75 }
76 > types.ts
77 > /**
78 > * @returns whether the provided parameter is a JavaScript Boolean or not.
79 > */
80 > export function isBoolean(obj: unknown): obj is boolean {
81 return (obj === true || obj === false);
82 }
83 > types.ts
84 > /**
85 > * @returns whether the provided parameter is undefined.
86 > */
87 > export function isUndefined(obj: unknown): obj is undefined {
88 > return (typeof obj === 'undefined'); types.ts
89 > }
90 > types.ts
91 > /**
92 > * @returns whether the provided parameter is defined.
93 > */
94 > export function isDefined<T>(arg: T | null | undefined): arg is T {
95 return !isUndefinedOrNull(arg);
96 }
97 > types.ts
98 > /**
99 > * @returns whether the provided parameter is undefined or null.
100 > */
101 > export function isUndefinedOrNull(obj: unknown): obj is undefined | null {
102 > return (isUndefined(obj) || obj === null); types.ts
103 > }
104 > types.ts
105 >
106 > export function assertType(condition: unknown, type?: string): asserts condition {
107 if (!condition) {
108 throw new Error(type ? `Unexpected type, expected '${type}'` : 'Unexpected type');
109 }
110 }
111 > types.ts
112 > /**
113 > * Asserts that the argument passed in is neither undefined nor null.
114 > *
115 > * @see {@link assertDefined} for a similar utility that leverages TS assertion functions to narrow down the type of `arg` to be non-nullable.
116 > */
117 > export function assertReturnsDefined<T>(arg: T | null | undefined): NonNullable<T> {
118 assert(
119 arg !== null && arg !== undefined,
123 return arg;
124 }
125 > types.ts
126 > /**
127 > * Asserts that a provided `value` is `defined` - not `null` or `undefined`,
128 > * throwing an error with the provided error or error message, while also
129 > * narrowing down the type of the `value` to be `NonNullable` using TS
130 > * assertion functions.
131 > *
132 > * @throws if the provided `value` is `null` or `undefined`.
133 > *
134 > * ## Examples
135 > *
136 > * ```typescript
137 > * // an assert with an error message
138 > * assertDefined('some value', 'String constant is not defined o_O.');
139 > *
140 > * // `throws!` the provided error
141 > * assertDefined(null, new Error('Should throw this error.'));
142 > *
143 > * // narrows down the type of `someValue` to be non-nullable
144 > * const someValue: string | undefined | null = blackbox();
145 > * assertDefined(someValue, 'Some value must be defined.');
146 > * console.log(someValue.length); // now type of `someValue` is `string`
147 > * ```
148 > *
149 > * @see {@link assertReturnsDefined} for a similar utility but without assertion.
150 > * @see {@link https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions typescript-3-7.html#assertion-functions}
151 > */
152 > export function assertDefined<T>(value: T, error: string | NonNullable<Error>): asserts value is NonNullable<T> {
153 if (value === null || value === undefined) {
154 const errorToThrow = typeof error === 'string' ? new Error(error) : error;
157 }
158 }
159 > types.ts
160 > /**
161 > * Asserts that each argument passed in is neither undefined nor null.
162 > */
163 > export function assertReturnsAllDefined<T1, T2>(t1: T1 | null | undefined, t2: T2 | null | undefined): [T1, T2];
164 > export function assertReturnsAllDefined<T1, T2, T3>(t1: T1 | null | undefined, t2: T2 | null | undefined, t3: T3 | null | undefined): [T1, T2, T3];
165 > export function assertReturnsAllDefined<T1, T2, T3, T4>(t1: T1 | null | undefined, t2: T2 | null | undefined, t3: T3 | null | undefined, t4: T4 | null | undefined): [T1, T2, T3, T4];
166 > export function assertReturnsAllDefined(...args: (unknown | null | undefined)[]): unknown[] {
167 const result = [];
168
179 return result;
180 }
181 > types.ts
182 > /**
183 > * Checks if the provided value is one of the vales in the provided list.
184 > *
185 > * ## Examples
186 > *
187 > * ```typescript
188 > * // note! item type is a `subset of string`
189 > * type TItem = ':' | '.' | '/';
190 > *
191 > * // note! item is type of `string` here
192 > * const item: string = ':';
193 > * // list of the items to check against
194 > * const list: TItem[] = [':', '.'];
195 > *
196 > * // ok
197 > * assert(
198 > * isOneOf(item, list),
199 > * 'Must succeed.',
200 > * );
201 > *
202 > * // `item` is of `TItem` type now
203 > * ```
204 > */
205 > export const isOneOf = <TType, TSubtype extends TType>(
206 value: TType,
207 validValues: readonly TSubtype[],
211 return validValues.includes(<TSubtype>value);
212 };
213 > types.ts
214 > /**
215 > * Compile-time type check of a variable.
216 > */
217 > export function typeCheck<T = never>(_thing: NoInfer<T>): void { }
218 >
219 > const hasOwnProperty = Object.prototype.hasOwnProperty;
220 >
221 > /**
222 > * @returns whether the provided parameter is an empty JavaScript Object or not.
223 > */
224 > export function isEmptyObject(obj: unknown): obj is object {
225 if (!isObject(obj)) {
226 return false;
235 return true;
236 }
237 > types.ts
238 > /**
239 > * @returns whether the provided parameter is a JavaScript Function or not.
240 > */
241 > export function isFunction(obj: unknown): obj is Function {
242 return (typeof obj === 'function');
243 }
244 > types.ts
245 > /**
246 > * @returns whether the provided parameters is are JavaScript Function or not.
247 > */
248 > export function areFunctions(...objects: unknown[]): boolean {
249 return objects.length > 0 && objects.every(isFunction);
250 }
251 > types.ts
252 > export type TypeConstraint = string | Function;
253 >
254 > export function validateConstraints(args: unknown[], constraints: Array<TypeConstraint | undefined>): void {
255 const len = Math.min(args.length, constraints.length);
256 for (let i = 0; i < len; i++) {
258 }
259 }
260 > types.ts
261 > export function validateConstraint(arg: unknown, constraint: TypeConstraint | undefined): void {
262
263 if (isString(constraint)) {
283 }
284 }
285 > types.ts
286 > /**
287 > * Helper type assertion that safely upcasts a type to a supertype.
288 > *
289 > * This can be used to make sure the argument correctly conforms to the subtype while still being able to pass it
290 > * to contexts that expects the supertype.
291 > */
292 > export function upcast<Base, Sub extends Base = Base>(x: Sub): Base {
293 return x;
294 }
295 > types.ts
296 > type AddFirstParameterToFunction<T, TargetFunctionsReturnType, FirstParameter> = T extends (...args: any[]) => TargetFunctionsReturnType ?
297 > // Function: add param to function
298 > (firstArg: FirstParameter, ...args: Parameters<T>) => ReturnType<T> :
299 >
300 > // Else: just leave as is
301 > T;
302 >
303 > /**
304 > * Allows to add a first parameter to functions of a type.
305 > */
306 > export type AddFirstParameterToFunctions<Target, TargetFunctionsReturnType, FirstParameter> = {
307 > // For every property
308 > [K in keyof Target]: AddFirstParameterToFunction<Target[K], TargetFunctionsReturnType, FirstParameter>;
309 > };
310 >
311 > /**
312 > * Given an object with all optional properties, requires at least one to be defined.
313 > * i.e. AtLeastOne<MyObject>;
314 > */
315 > export type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U];
316 >
317 > /**
318 > * Only picks the non-optional properties of a type.
319 > */
320 > export type OmitOptional<T> = { [K in keyof T as T[K] extends Required<T>[K] ? K : never]: T[K] };
321 >
322 > /**
323 > * A type that removed readonly-less from all properties of `T`
324 > */
325 > export type Mutable<T> = {
326 > -readonly [P in keyof T]: T[P]
327 > };
328 >
329 > /**
330 > * A type that adds readonly to all properties of T, recursively.
331 > */
332 > export type DeepImmutable<T> = T extends (infer U)[]
333 > ? ReadonlyArray<DeepImmutable<U>>
334 > : T extends ReadonlyArray<infer U>
335 > ? ReadonlyArray<DeepImmutable<U>>
336 > : T extends Map<infer K, infer V>
337 > ? ReadonlyMap<K, DeepImmutable<V>>
338 > : T extends Set<infer U>
339 > ? ReadonlySet<DeepImmutable<U>>
340 > : T extends object
341 > ? {
342 > readonly [K in keyof T]: DeepImmutable<T[K]>;
343 > }
344 > : T;
345 >
346 > /**
347 > * A single object or an array of the objects.
348 > */
349 > export type SingleOrMany<T> = T | T[];
350 >
351 > /**
352 > * Given a `type X = { foo?: string }` checking that an object `satisfies X`
353 > * will ensure each property was explicitly defined, ensuring no properties
354 > * are omitted or forgotten.
355 > */
356 > export type WithDefinedProps<T> = { [K in keyof Required<T>]: T[K] };
357 >
358 >
359 > /**
360 > * A type that recursively makes all properties of `T` required
361 > */
362 > export type DeepRequiredNonNullable<T> = {
363 > [P in keyof T]-?: T[P] extends object ? DeepRequiredNonNullable<T[P]> : Required<NonNullable<T[P]>>;
364 > };
365 >
366 >
367 > /**
368 > * Represents a type that is a partial version of a given type `T`, where all properties are optional and can be deeply nested.
369 > */
370 > export type DeepPartial<T> = {
371 > [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : Partial<T[P]>;
372 > };
373 >
374 > /**
375 > * Represents a type that is a partial version of a given type `T`, except a subset.
376 > */
377 > export type PartialExcept<T, K extends keyof T> = Partial<Omit<T, K>> & Pick<T, K>;
378 >
379 >
380 > type KeysOfUnionType<T> = T extends T ? keyof T : never;
381 > type FilterType<T, TTest> = T extends TTest ? T : never;
382 > type MakeOptionalAndTrue<T extends object> = { [K in keyof T]?: true };
383 >
384 > /**
385 > * Type guard that checks if an object has specific keys and narrows the type accordingly.
386 > *
387 > * @param x - The object to check
388 > * @param key - An object with boolean values indicating which keys to check for
389 > * @returns true if all specified keys exist in the object, false otherwise
390 > *
391 > * @example
392 > * ```typescript
393 > * type A = { a: string };
394 > * type B = { b: number };
395 > * const obj: A | B = getObject();
396 > *
397 > * if (hasKey(obj, { a: true })) {
398 > * // obj is now narrowed to type A
399 > * console.log(obj.a);
400 > * }
401 > * ```
402 > */
403 > export function hasKey<T extends object, TKeys extends MakeOptionalAndTrue<T>>(x: T, key: TKeys): x is FilterType<T, { [K in KeysOfUnionType<T> & keyof TKeys]: unknown }> {
404 for (const k in key) {
405 if (!(k in x)) {
src/vs/platform/agentHost/common/agentHostChangesetService.ts 293 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostChangesetService.ts
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 > import { createDecorator } from '../../instantiation/common/instantiation.js';
7 > import type { ChangesSummary } from './state/protocol/state.js';
8 > import type { ISessionFileDiff, URI as ProtocolURI } from './state/sessionState.js';
9 >
10 > /** Metadata key under which the branch changeset's diff list is persisted. */
11 > export const META_CHANGESET_BRANCH = 'agentHost.changeset.branch';
12 >
13 > /** Metadata key under which the session-wide changeset's diff list is persisted. */
14 > export const META_CHANGESET_SESSION = 'agentHost.changeset.session';
15 >
16 > /**
17 > * Legacy metadata key used by older builds to persist the session-wide
18 > * changeset's diff list. Read-only fallback for {@link META_CHANGESET_SESSION}.
19 > */
20 > export const META_LEGACY_DIFFS = 'diffs';
21 >
22 > /**
23 > * Metadata key under which the session's changes is persisted.
24 > */
25 > export const META_CHANGES_SUMMARY = 'agentHost.changes';
26 >
27 > /**
28 > * The set of session-DB metadata keys the changeset service needs in a
29 > * batched read to synthesise the `summary.changes` aggregate for the
30 > * session-list overlay. {@link IAgentHostChangesetService.getListMetadataKeys}
31 > * returns this (or `undefined` when live state already answers the question);
32 > * `AgentService` merges the returned keys into its own metadata key set so the
33 > * DB is hit exactly once per session.
34 > */
35 > export const CHANGESET_DB_METADATA_KEYS: Record<string, true> = {
36 > [META_CHANGESET_BRANCH]: true,
37 > [META_CHANGESET_SESSION]: true,
38 > [META_CHANGES_SUMMARY]: true,
39 > [META_LEGACY_DIFFS]: true,
40 > };
41 >
42 > /** The two static changeset kinds we publish by default. */
43 > export type StaticChangesetKind = 'branch' | 'session';
44 >
45 > /**
46 > * Raw metadata values for the persisted changeset blobs, batch-read
47 > * by the caller (`AgentService.listSessions` / `AgentService.restoreSession`).
48 > * The caller owns the database read so multiple metadata keys can be
49 > * fetched in a single round-trip; the service owns parsing, applying,
50 > * and `seedIfEmpty` gating.
51 > */
52 > export interface IPersistedChangesetMetadata {
53 > readonly branchRaw?: string;
54 > readonly sessionRaw?: string;
55 > readonly legacyRaw?: string;
56 > }
57 >
58 > /**
59 > * The parsed diffs returned from {@link IAgentHostChangesetService.restorePersistedStaticChangesets},
60 > * suitable for synthesising a `summary.changes` aggregate for the
61 > * session-list overlay (see {@link IAgentHostChangesetService.computeListEntryChanges}).
62 > */
63 > export interface IRestoredChangesetDiffs {
64 > readonly branch?: readonly ISessionFileDiff[];
65 > readonly session?: readonly ISessionFileDiff[];
66 > }
67 >
68 > export const IAgentHostChangesetService = createDecorator<IAgentHostChangesetService>('agentHostChangesetService');
69 >
70 > /**
71 > * Owns the lifecycle of static and per-turn changesets for the agent host:
72 > * registers the `<session>/changeset/{uncommitted,session,turn/<id>}` URIs
73 > * on the state manager, runs git-driven and edit-tracker-driven diff
74 > * computations, debounces mid-turn recomputes, publishes file lists
75 > * (`changeset/fileSet` / `changeset/fileRemoved`) and aggregate counts
76 > * (`session/summaryChanged`), and persists results to the session DB so
77 > * restarts can rehydrate without recomputing.
78 > *
79 > * Created locally by `AgentService` (not via `registerSingleton`) and
80 > * added to the local `ServiceCollection` so `AgentSideEffects` can
81 > * resolve it via `@IAgentHostChangesetService`. `AgentHostStateManager`
82 > * is passed as a plain ctor argument (it has no decorator today); the
83 > * git / log / session-data services are DI-injected.
84 > */
85 > export interface IAgentHostChangesetService {
86 > readonly _serviceBrand: undefined;
87 >
88 > /**
89 > * Registers the two static changeset URIs (`uncommitted`, `session`)
90 > * on the state manager so client subscriptions resolve to a
91 > * `status: computing` snapshot before the first compute pass
92 > * completes. The catalogue itself (`state.changesets`) is seeded
93 > * upstream by `_buildInitialSummary` / `restoreSession` — this only
94 > * deals with the state-manager-side per-changeset entries.
95 > *
96 > * Idempotent; safe to call on every create and restore path.
97 > */
98 > registerStaticChangesets(session: ProtocolURI): void;
99 >
100 > /**
101 > * Re-seed a static changeset (`uncommitted` or `session`) from a
102 > * previously persisted file list (e.g. read out of the session DB on
103 > * restore / listSessions). Idempotently registers the changeset URI
104 > * on the state manager, fans the persisted files out as
105 > * `changeset/fileSet` actions, and transitions the status to `Ready`.
106 > */
107 > restoreStaticChangeset(session: ProtocolURI, kind: StaticChangesetKind, diffs: readonly ISessionFileDiff[]): void;
108 >
109 > /**
110 > * Parses the persisted changeset metadata blobs (`uncommitted`,
111 > * `session`, and the legacy `diffs` fallback for `session`) without
112 > * mutating live state. Intended for list overlays that only need
113 > * aggregate catalogue counts and should not pin full changeset state in
114 > * memory.
115 > */
116 > parsePersistedStaticChangesets(sessionUri: ProtocolURI, metadata: IPersistedChangesetMetadata): IRestoredChangesetDiffs;
117 >
118 > /**
119 > * Applies parsed persisted changeset diffs to live state via
120 > * {@link restoreStaticChangeset}. This is the side-effectful half of
121 > * persisted restore and should only be used on real restore/subscribe
122 > * paths that need a subscribable changeset snapshot.
123 > *
124 > * Honours `seedIfEmpty`: when a live changeset state already has files
125 > * for the same kind, persisted diffs are NOT applied (they would
126 > * otherwise overwrite the live state).
127 > */
128 > applyPersistedStaticChangesets(sessionUri: ProtocolURI, diffs: IRestoredChangesetDiffs): void;
129 >
130 > /**
131 > * Compatibility wrapper that parses persisted changeset metadata and then
132 > * applies it to live state. New list-overlay callers should prefer
133 > * {@link parsePersistedStaticChangesets}; restore/subscribe callers can
134 > * use this method when they intentionally want both parse and seed.
135 > *
136 > * The `AgentService` orchestration boundary batches the metadata read
137 > * (custom title + read / archive flags + config values + these three
138 > * blobs) in a single database round-trip, then hands the raw values
139 > * here; the service does not open the database itself for this method.
140 > */
141 > restorePersistedStaticChangesets(sessionUri: ProtocolURI, metadata: IPersistedChangesetMetadata): IRestoredChangesetDiffs;
142 >
143 > /**
144 > * Fire-and-forget persistence of the `summary.changes` aggregate to the
145 > * session DB under {@link META_CHANGES_SUMMARY}. Used both by the
146 > * happy-path turn-complete write and by the {@link ChangesetSessionCoordinator}
147 > * one-shot migration that reads the old `META_CHANGESET_SESSION` /
148 > * `META_LEGACY_DIFFS` blobs and projects them into the new key on
149 > * sessions written by older builds. Errors are logged, not thrown.
150 > */
151 > persistChangesSummary(sessionUri: ProtocolURI, summary: ChangesSummary): void;
152 >
153 > /**
154 > * Returns the session-DB metadata keys to merge into a batched read for
155 > * `sessionUri` (so the session-list overlay can synthesise the `changes`
156 > * aggregate), OR `undefined` when live state already answers the
157 > * aggregate-counts question (loaded session or a ready live
158 > * `changeKind: 'session'` changeset state) so the caller can skip loading
159 > * the potentially-large persisted diff blobs.
160 > */
161 > getListMetadataKeys(sessionUri: ProtocolURI): Record<string, true> | undefined;
162 >
163 > /**
164 > * Computes the `summary.changes` aggregate (additions / deletions / files
165 > * for the session-wide changeset) for a single session-list entry, using
166 > * the already-batched DB `metadata` read. Returns `undefined` when no
167 > * aggregate should be advertised (loaded session whose `summary.changes`
168 > * the caller already projected, or no live/persisted source).
169 > *
170 > * Precedence: live session (caller owns projection) > persisted
171 > * `META_CHANGES_SUMMARY` blob > ready live `changeKind: 'session'`
172 > * changeset state > parsed persisted session-wide diff blob. The latter
173 > * two paths also migrate the result forward to {@link META_CHANGES_SUMMARY}.
174 > */
175 > computeListEntryChanges(sessionUri: ProtocolURI, metadata: Record<string, string | undefined>): ChangesSummary | undefined;
176 >
177 > /**
178 > * Returns true when the static changeset identified by `changesetUri` is
179 > * currently being recomputed. Used by cache eviction to avoid dropping a
180 > * slot while its producer is mid-flight.
181 > */
182 > isStaticChangesetComputeActive(changesetUri: ProtocolURI): boolean;
183 >
184 > /**
185 > * Refreshes the list of changesets for the given session.
186 > */
187 > refreshChangesetCatalog(session: ProtocolURI): void;
188 >
189 > /**
190 > * Lazy refresh of the branch changeset, kicked off when a client
191 > * first subscribes to `<session>/changeset/branch`. Self-defers when the
192 > * session's working directory is not yet known; the deferred refresh is
193 > * drained by {@link onWorkingDirectoryAvailable}.
194 > */
195 > refreshBranchChangeset(session: ProtocolURI): void;
196 >
197 > /**
198 > * Lazy refresh of the session changeset, kicked off when a
199 > * client first subscribes to `<session>/changeset/session` or the
200 > * session URI itself (e.g. Agents Window observing the session). The
201 > * recompute keeps the catalogue chip fresh across session opens even
202 > * when no turn has run since process start. Self-defers when the
203 > * session's working directory is not yet known.
204 > */
205 > refreshSessionChangeset(session: ProtocolURI): void;
206 >
207 > /**
208 > * Drains static changeset refreshes (`branch` / `session` /
209 > * `uncommitted`) that were deferred because the session's working
210 > * directory was not yet known. Called when a session is materialized or
211 > * restored. Recomputes every changeset currently subscribed for the
212 > * session via {@link recomputeSubscribedChangesets}; subscriptions that
213 > * dropped while the working directory was unknown are naturally skipped.
214 > * Idempotent.
215 > */
216 > onWorkingDirectoryAvailable(session: ProtocolURI): void;
217 >
218 > /**
219 > * Recomputes every changeset currently subscribed for `session`, read
220 > * from the shared changeset subscription service. Each subscribed changeset
221 > * is dispatched to its kind-specific recompute (branch / session / uncommitted
222 > * / turn); the individual recomputes self-defer when the working directory is
223 > * not yet known. Used as the session-level refresh entry point (drain on
224 > * materialization, git-state change).
225 > */
226 > recomputeSubscribedChangesets(session: ProtocolURI): void;
227 >
228 > /**
229 > * Forgets any deferred static changeset refreshes queued for a session
230 > * that is being disposed.
231 > */
232 > onSessionDisposed(session: ProtocolURI): void;
233 >
234 > /**
235 > * Computes and publishes the per-turn changeset for `turnId` on `session`.
236 > * Per-turn changesets are not persisted.
237 > */
238 > computeTurnChangeset(session: ProtocolURI, turnId: string): Promise<ProtocolURI>;
239 >
240 > /**
241 > * Computes and publishes the compare-turns changeset between
242 > * `originalTurnId` (the "from" endpoint) and `modifiedTurnId` (the
243 > * "to" endpoint) on `session`. Diff direction is
244 > * `originalTurnId.current → modifiedTurnId.current` — endpoint-to-
245 > * endpoint, so it captures what differs between the two turn states.
246 > *
247 > * Implemented via git: both refs come from the per-turn checkpoint
248 > * captured at the end of each turn. When either checkpoint is missing
249 > * (non-git session, baseline never captured, capture failure), the
250 > * changeset transitions to `status: Error` instead of rejecting; no
251 > * SDK edit-tracker fallback exists.
252 > *
253 > * Compare-turns changesets are not persisted and are computed once
254 > * on subscribe (no live recompute).
255 > */
256 > computeCompareTurnsChangeset(session: ProtocolURI, originalTurnId: string, modifiedTurnId: string): Promise<ProtocolURI>;
257 >
258 > /**
259 > * Computes and publishes the uncommitted changeset for `session`
260 > * directly via git (`git status` against HEAD). The uncommitted slot
261 > * has no SDK edit-tracker fallback — the aggregator answers a different
262 > * question than `git status` and would silently rebrand SDK-tracked
263 > * edits as uncommitted git changes. When the session has no working
264 > * directory, the working directory isn't a git work tree, or the git
265 > * command fails, the changeset transitions to `status: Error`.
266 > *
267 > * Uncommitted changesets are not persisted; callers schedule recomputes
268 > * (e.g. on turn complete, post-commit, working-tree watcher event)
269 > * directly via this method.
270 > */
271 > computeUncommittedChangeset(session: ProtocolURI): Promise<ProtocolURI>;
272 >
273 > /**
274 > * Hook called by `AgentSideEffects` after a tool call that produced
275 > * file edits completes. Schedules a debounced session-changeset recompute.
276 > */
277 > onToolCallEditsApplied(session: ProtocolURI, turnId: string): void;
278 >
279 > /**
280 > * Hook called by `AgentSideEffects` when a turn completes. Cancels any
281 > * pending mid-turn debounce, then schedules a final session + uncommitted
282 > * recompute. Ordering matters — see implementation.
283 > */
284 > onTurnComplete(session: ProtocolURI, turnId: string | undefined): void;
285 >
286 > /**
287 > * Hook called by `AgentSideEffects` when a session is truncated (turns
288 > * removed). Recomputes the session changeset from scratch (no
289 > * `changedTurnId`, no incremental reuse).
290 > */
291 > onSessionTruncated(session: ProtocolURI): void;
292 >
293 > }
src/vs/platform/agentHost/node/sessionDatabase.ts 288 covered LOC · 50 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- sessionDatabase.ts
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 > import * as fs from 'fs';
7 > import { SequencerByKey } from '../../../base/common/async.js';
8 > import type { Database, RunResult } from '@vscode/sqlite3';
9 > import type { IFileEditContent, IFileEditRecord, ILocalTurnRecord, IReviewedFileRecord, ISessionDatabase } from '../common/sessionDataService.js';
10 > import { dirname } from '../../../base/common/path.js';
11 > import { URI } from '../../../base/common/uri.js';
12 > import type { Message } from '../common/state/sessionState.js';
13 >
14 > /**
15 > * A single numbered migration. Migrations are applied in order of
16 > * {@link version} and tracked via `PRAGMA user_version`.
17 > */
18 > export interface ISessionDatabaseMigration {
19 > /** Monotonically-increasing version number (1-based). */
20 > readonly version: number;
21 > /** SQL to execute for this migration. */
22 > readonly sql: string;
23 > }
24 >
25 > /**
26 > * The set of migrations that define the current session database schema.
27 > * New migrations should be **appended** to this array with the next version
28 > * number. Never reorder or mutate existing entries.
29 > */
30 > export const sessionDatabaseMigrations: readonly ISessionDatabaseMigration[] = [
31 > {
32 > version: 1,
33 > sql: [
34 > `CREATE TABLE IF NOT EXISTS turns (
35 > id TEXT PRIMARY KEY NOT NULL
36 > )`,
37 > `CREATE TABLE IF NOT EXISTS file_edits (
38 > turn_id TEXT NOT NULL REFERENCES turns(id) ON DELETE CASCADE,
39 > tool_call_id TEXT NOT NULL,
40 > file_path TEXT NOT NULL,
41 > before_content BLOB NOT NULL,
42 > after_content BLOB NOT NULL,
43 > added_lines INTEGER,
44 > removed_lines INTEGER,
45 > PRIMARY KEY (tool_call_id, file_path)
46 > )`,
47 > ].join(';\n'),
48 > },
49 > {
50 > version: 2,
51 > sql: `CREATE TABLE IF NOT EXISTS session_metadata (
52 > key TEXT PRIMARY KEY NOT NULL,
53 > value TEXT NOT NULL
54 > )`,
55 > },
56 > {
57 > version: 3,
58 > sql: [
59 > // Recreate file_edits with new columns: edit_type, original_path,
60 > // and nullable before_content/after_content.
61 > `CREATE TABLE file_edits_v3 (
62 > turn_id TEXT NOT NULL REFERENCES turns(id) ON DELETE CASCADE,
63 > tool_call_id TEXT NOT NULL,
64 > file_path TEXT NOT NULL,
65 > edit_type TEXT NOT NULL DEFAULT 'edit',
66 > original_path TEXT,
67 > before_content BLOB,
68 > after_content BLOB,
69 > added_lines INTEGER,
70 > removed_lines INTEGER,
71 > PRIMARY KEY (tool_call_id, file_path)
72 > )`,
73 > `INSERT INTO file_edits_v3 (turn_id, tool_call_id, file_path, edit_type, before_content, after_content, added_lines, removed_lines)
74 > SELECT turn_id, tool_call_id, file_path, 'edit', before_content, after_content, added_lines, removed_lines FROM file_edits`,
75 > `DROP TABLE file_edits`,
76 > `ALTER TABLE file_edits_v3 RENAME TO file_edits`,
77 > ].join(';\n'),
78 > },
79 > {
80 > version: 4,
81 > sql: [
82 > `ALTER TABLE turns ADD COLUMN event_id TEXT`,
83 > `CREATE INDEX IF NOT EXISTS idx_turns_event_id ON turns(event_id)`,
84 > ].join(';\n'),
85 > },
86 > {
87 > version: 5,
88 > sql: `ALTER TABLE turns ADD COLUMN checkpoint_ref TEXT`,
89 > },
90 > {
91 > version: 6,
92 > sql: `CREATE TABLE IF NOT EXISTS chat_drafts (
93 > chat_uri TEXT PRIMARY KEY NOT NULL,
94 > draft TEXT NOT NULL
95 > )`,
96 > },
97 > {
98 > version: 7,
99 > sql: `CREATE TABLE IF NOT EXISTS reviewed_files (
100 > uri TEXT NOT NULL,
101 > nonce TEXT NOT NULL,
102 > PRIMARY KEY (uri, nonce)
103 > )`,
104 > },
105 > {
106 > version: 8,
107 > sql: `CREATE TABLE IF NOT EXISTS local_turns (
108 > turn_id TEXT PRIMARY KEY NOT NULL,
109 > chat_uri TEXT NOT NULL,
110 > anchor_turn_id TEXT,
111 > seq INTEGER NOT NULL,
112 > payload TEXT NOT NULL
113 > )`,
114 > },
115 > ];
116 >
117 > // ---- Promise wrappers around callback-based @vscode/sqlite3 API -----------
118 >
119 function dbExec(db: Database, sql: string): Promise<void> {
120 return new Promise((resolve, reject) => {
122 });
123 }
125 function dbRun(db: Database, sql: string, params: unknown[]): Promise<{ changes: number; lastID: number }> {
126 return new Promise((resolve, reject) => {
133 });
134 }
136 function dbGet(db: Database, sql: string, params: unknown[]): Promise<Record<string, unknown> | undefined> {
137 return new Promise((resolve, reject) => {
144 });
145 }
147 function dbAll(db: Database, sql: string, params: unknown[]): Promise<Record<string, unknown>[]> {
148 return new Promise((resolve, reject) => {
155 });
156 }
158 function dbClose(db: Database): Promise<void> {
159 return new Promise((resolve, reject) => {
161 });
162 }
164 function dbOpen(path: string): Promise<Database> {
165 return new Promise((resolve, reject) => {
174 });
175 }
177 > /**
178 > * Applies any pending {@link ISessionDatabaseMigration migrations} to a
179 > * database. Migrations whose version is greater than the current
180 > * `PRAGMA user_version` are run inside a serialized transaction. After all
181 > * migrations complete the pragma is updated to the highest applied version.
182 > */
183 export async function runMigrations(db: Database, migrations: readonly ISessionDatabaseMigration[]): Promise<void> {
184 // Enable foreign key enforcement — must be set outside a transaction
210 }
211 }
213 > /**
214 > * A wrapper around a `@vscode/sqlite3` {@link Database} instance with
215 > * lazy initialisation.
216 > *
217 > * The underlying connection is opened on the first async method call
218 > * (not at construction time), allowing the object to be created
219 > * synchronously and shared via a {@link ReferenceCollection}.
220 > *
221 > * Calling {@link dispose} closes the connection.
222 > */
223 > export class SessionDatabase implements ISessionDatabase {
224 >
225 > protected _dbPromise: Promise<Database> | undefined;
226 > protected _closed: Promise<void> | true | undefined;
227 > private readonly _fileEditSequencer = new SequencerByKey<string>();
228 >
229 > /**
230 > * Serializes `setMetadata` writes per key. `@vscode/sqlite3` runs in
231 > * parallelized mode, so two `db.run()` calls on the same connection
232 > * can be dispatched to the libuv thread pool and complete out of
233 > * submission order. For "last writer wins" keys (notably `configValues`
234 > * via {@link setMetadata}), that meant a fast-following second write
235 > * could be overtaken by the first and silently lose its value — see
236 > * the "Session Config persistence across restarts" integration test.
237 > * Sequencing by key preserves intra-key order while still allowing
238 > * writes for different keys to run concurrently.
239 > */
240 > private readonly _metadataSequencer = new SequencerByKey<string>();
241 >
242 > /**
243 > * In-flight write operations. Tracked so {@link whenIdle} can await them
244 > * before the process exits — without this, a `SIGTERM` arriving between
245 > * a fire-and-forget mutating call (e.g. `setMetadata`) being invoked and
246 > * its underlying SQLite query completing would silently drop the write.
247 > * Every public mutating method routes its returned promise through
248 > * {@link _track}; reads (`getMetadata`, `getFileEdits`, ...) skip
249 > * tracking since shutdown does not need to wait for them.
250 > */
251 > private readonly _pendingWrites = new Set<Promise<unknown>>();
252 >
253 > constructor(
254 private readonly _path: string,
255 private readonly _migrations: readonly ISessionDatabaseMigration[] = sessionDatabaseMigrations,
256 ) { }
258 > /**
259 > * Opens (or creates) a SQLite database at {@link path} and applies
260 > * any pending migrations. Only used in tests where synchronous
261 > * construction + immediate readiness is desired.
262 > */
263 > static async open(path: string, migrations: readonly ISessionDatabaseMigration[] = sessionDatabaseMigrations): Promise<SessionDatabase> {
264 const inst = new SessionDatabase(path, migrations);
265 await inst._ensureDb();
266 return inst;
267 }
269 > protected _ensureDb(): Promise<Database> {
270 if (this._closed) {
271 return Promise.reject(new Error('SessionDatabase has been disposed'));
297 return this._dbPromise;
298 }
300 > /**
301 > * Returns the names of all user-created tables in the database.
302 > * Useful for testing migration behavior.
303 > */
304 > async getAllTables(): Promise<string[]> {
305 const db = await this._ensureDb();
306 const rows = await dbAll(db, `SELECT name FROM sqlite_master WHERE type='table' ORDER BY name`, []);
307 return rows.map(r => r.name as string);
308 }
310 > // ---- Turns ----------------------------------------------------------
311 >
312 > createTurn(turnId: string): Promise<void> {
313 return this._track(async () => {
314 const db = await this._ensureDb();
316 });
317 }
319 > deleteTurn(turnId: string): Promise<void> {
320 return this._track(async () => {
321 const db = await this._ensureDb();
323 });
324 }
326 > setTurnEventId(turnId: string, eventId: string): Promise<void> {
327 return this._track(async () => {
328 const db = await this._ensureDb();
334 });
335 }
337 > async getTurnEventId(turnId: string): Promise<string | undefined> {
338 const db = await this._ensureDb();
339 const row = await dbGet(db, 'SELECT event_id FROM turns WHERE id = ?', [turnId]);
340 return row?.event_id as string | undefined ?? undefined;
341 }
343 > async getNextTurnEventId(turnId: string): Promise<string | undefined> {
344 const db = await this._ensureDb();
345 // `turns.id` is the canonical turn key — either a live `request_xxx`
360 return row?.event_id as string | undefined ?? undefined;
361 }
363 > async getFirstTurnEventId(): Promise<string | undefined> {
364 const db = await this._ensureDb();
365 const row = await dbGet(db, 'SELECT event_id FROM turns ORDER BY rowid LIMIT 1', []);
366 return row?.event_id as string | undefined ?? undefined;
367 }
369 > setTurnCheckpointRef(turnId: string, ref: string): Promise<void> {
370 return this._track(async () => {
371 const db = await this._ensureDb();
374 });
375 }
377 > async getTurnCheckpointRef(turnId: string): Promise<string | undefined> {
378 const db = await this._ensureDb();
379 const row = await dbGet(db, 'SELECT checkpoint_ref FROM turns WHERE id = ?1 OR event_id = ?1 LIMIT 1', [turnId]);
380 return row?.checkpoint_ref as string | undefined ?? undefined;
381 }
383 > async getPreviousCheckpointRef(turnId: string): Promise<string | undefined> {
384 const db = await this._ensureDb();
385 const row = await dbGet(
393 return row?.checkpoint_ref as string | undefined ?? undefined;
394 }
396 > async getAllCheckpointRefs(): Promise<string[]> {
397 const db = await this._ensureDb();
398 const rows = await dbAll(db, 'SELECT checkpoint_ref FROM turns WHERE checkpoint_ref IS NOT NULL ORDER BY rowid', []);
399 return rows.map(r => r.checkpoint_ref as string);
400 }
402 > truncateFromTurn(turnId: string): Promise<void> {
403 return this._track(async () => {
404 const db = await this._ensureDb();
411 });
412 }
414 > deleteTurnsAfter(turnId: string): Promise<void> {
415 return this._track(async () => {
416 const db = await this._ensureDb();
424 });
425 }
427 > deleteAllTurns(): Promise<void> {
428 return this._track(async () => {
429 const db = await this._ensureDb();
431 });
432 }
434 > // ---- Local (host-injected) turns ------------------------------------
435 >
436 > insertLocalTurn(record: ILocalTurnRecord): Promise<void> {
437 return this._track(async () => {
438 const db = await this._ensureDb();
443 });
444 }
446 > async getLocalTurns(): Promise<ILocalTurnRecord[]> {
447 const db = await this._ensureDb();
448 const rows = await dbAll(db, 'SELECT turn_id, chat_uri, anchor_turn_id, seq, payload FROM local_turns ORDER BY seq', []);
455 }));
456 }
458 > deleteLocalTurns(turnIds: readonly string[]): Promise<void> {
459 return this._track(async () => {
460 if (turnIds.length === 0) {
466 });
467 }
469 > // ---- File edits -----------------------------------------------------
470 >
471 > storeFileEdit(edit: IFileEditRecord & IFileEditContent): Promise<void> {
472 return this._track(() => this._fileEditSequencer.queue(edit.filePath, async () => {
473 const db = await this._ensureDb();
494 }));
495 }
497 > async getFileEdits(toolCallIds: string[]): Promise<IFileEditRecord[]> {
498 if (toolCallIds.length === 0) {
499 return [];
519 }));
520 }
522 > async getAllFileEdits(): Promise<IFileEditRecord[]> {
523 const db = await this._ensureDb();
524 const rows = await dbAll(
539 }));
540 }
542 > async getFileEditsByTurn(turnId: string): Promise<IFileEditRecord[]> {
543 const db = await this._ensureDb();
544 const rows = await dbAll(
560 }));
561 }
563 > async readFileEditContent(toolCallId: string, filePath: string): Promise<IFileEditContent | undefined> {
564 return this._fileEditSequencer.queue(filePath, async () => {
565 const db = await this._ensureDb();
580 });
581 }
583 > // ---- Session metadata -----------------------------------------------
584 >
585 > async getMetadata(key: string): Promise<string | undefined> {
586 const db = await this._ensureDb();
587 const row = await dbGet(db, 'SELECT value FROM session_metadata WHERE key = ?', [key]);
588 return row?.value as string | undefined;
589 }
591 > async getMetadataObject<T extends Record<string, unknown>>(obj: T): Promise<{ [K in keyof T]: string | undefined }> {
592 const keys = Object.keys(obj) as (keyof T & string)[];
593 // eslint-disable-next-line local/code-no-dangerous-type-assertions
607 return result;
608 }
610 > setMetadata(key: string, value: string): Promise<void> {
611 return this._track(() => this._metadataSequencer.queue(key, async () => {
612 const db = await this._ensureDb();
614 }));
615 }
617 > setChatDraft(chat: URI, draft: Message | undefined): Promise<void> {
618 const chatUri = chat.toString();
619 return this._track(async () => {
626 });
627 }
629 > async getChatDraft(chat: URI): Promise<Message | undefined> {
630 const db = await this._ensureDb();
631 const row = await dbGet(db, 'SELECT draft FROM chat_drafts WHERE chat_uri = ?', [chat.toString()]);
639 }
640 }
642 > // ---- Reviewed files -------------------------------------------------
643 >
644 > markFileReviewed(uri: URI, nonce: string): Promise<void> {
645 return this._track(async () => {
646 const db = await this._ensureDb();
648 });
649 }
651 > unmarkFileReviewed(uri: URI, nonce: string): Promise<void> {
652 return this._track(async () => {
653 const db = await this._ensureDb();
655 });
656 }
658 > async getReviewedFiles(): Promise<IReviewedFileRecord[]> {
659 const db = await this._ensureDb();
660 const rows = await dbAll(db, 'SELECT uri, nonce FROM reviewed_files ORDER BY rowid', []);
661 return rows.map(toReviewedFileRecord);
662 }
664 > async getReviewedFilesForUri(uri: URI): Promise<IReviewedFileRecord[]> {
665 const db = await this._ensureDb();
666 const rows = await dbAll(db, 'SELECT uri, nonce FROM reviewed_files WHERE uri = ? ORDER BY rowid', [uri.toString()]);
667 return rows.map(toReviewedFileRecord);
668 }
670 > async isFileReviewed(uri: URI, nonce: string): Promise<boolean> {
671 const db = await this._ensureDb();
672 const row = await dbGet(db, 'SELECT 1 FROM reviewed_files WHERE uri = ? AND nonce = ? LIMIT 1', [uri.toString(), nonce]);
673 return !!row;
674 }
676 > remapTurnIds(mapping: ReadonlyMap<string, string>): Promise<void> {
677 return this._track(async () => {
678 const db = await this._ensureDb();
718 });
719 }
721 > /**
722 > * Resolves once all currently in-flight write operations have settled.
723 > * Used by graceful shutdown to flush pending fire-and-forget writes
724 > * before the process exits. Should be called from a path where no
725 > * further writes are expected; loops until idle to also drain any
726 > * writes that get queued while we're awaiting.
727 > */
728 > async whenIdle(): Promise<void> {
729 while (this._pendingWrites.size > 0) {
730 await Promise.allSettled([...this._pendingWrites]);
731 }
732 }
734 > async vacuumInto(targetPath: string) {
735 const db = await this._ensureDb();
736 await dbRun(db, 'VACUUM INTO ?', [targetPath]);
737 }
739 > /**
740 > * Wrap a mutating operation's promise so {@link whenIdle} can await it.
741 > * Invoke at the **outermost** layer of every public mutating method so
742 > * that any internal awaits (notably `_ensureDb()`) are covered too —
743 > * tracking only the leaf `dbRun`/`dbExec` would miss the window
744 > * between the method being called and the query actually being queued.
745 > */
746 > private _track<T>(fn: () => Promise<T>): Promise<T> {
747 const p = fn();
748 this._pendingWrites.add(p);
751 return p;
752 }
754 > async close() {
755 await (this._closed ??= this._dbPromise?.then(db => dbClose(db)).catch(() => { }) || true);
756 }
758 > dispose(): void {
759 this.close();
760 }
762 >
763 function toReviewedFileRecord(row: Record<string, unknown>): IReviewedFileRecord {
764 return {
767 };
768 }
770 function toUint8Array(value: unknown): Uint8Array {
771 if (value instanceof Buffer) {
src/vs/base/common/resources.ts 281 covered LOC · 38 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- resources.ts
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 > import { CharCode } from './charCode.js';
7 > import * as extpath from './extpath.js';
8 > import { Schemas } from './network.js';
9 > import * as paths from './path.js';
10 > import { isLinux, isWindows } from './platform.js';
11 > import { compare as strCompare, equalsIgnoreCase } from './strings.js';
12 > import { URI, uriToFsPath } from './uri.js';
13 >
14 > export function originalFSPath(uri: URI): string {
15 return uriToFsPath(uri, true);
16 }
18 > //#region IExtUri
19 >
20 > export interface IExtUri {
21 >
22 > // --- identity
23 >
24 > /**
25 > * Compares two uris.
26 > *
27 > * @param uri1 Uri
28 > * @param uri2 Uri
29 > * @param ignoreFragment Ignore the fragment (defaults to `false`)
30 > */
31 > compare(uri1: URI, uri2: URI, ignoreFragment?: boolean): number;
32 >
33 > /**
34 > * Tests whether two uris are equal
35 > *
36 > * @param uri1 Uri
37 > * @param uri2 Uri
38 > * @param ignoreFragment Ignore the fragment (defaults to `false`)
39 > */
40 > isEqual(uri1: URI | undefined, uri2: URI | undefined, ignoreFragment?: boolean): boolean;
41 >
42 > /**
43 > * Tests whether a `candidate` URI is a parent or equal of a given `base` URI.
44 > *
45 > * @param base A uri which is "longer" or at least same length as `parentCandidate`
46 > * @param parentCandidate A uri which is "shorter" or up to same length as `base`
47 > * @param ignoreFragment Ignore the fragment (defaults to `false`)
48 > */
49 > isEqualOrParent(base: URI, parentCandidate: URI, ignoreFragment?: boolean): boolean;
50 >
51 > /**
52 > * Creates a key from a resource URI to be used to resource comparison and for resource maps.
53 > * @see {@link ResourceMap}
54 > * @param uri Uri
55 > * @param ignoreFragment Ignore the fragment (defaults to `false`)
56 > */
57 > getComparisonKey(uri: URI, ignoreFragment?: boolean): string;
58 >
59 > /**
60 > * Whether the casing of the path-component of the uri should be ignored.
61 > */
62 > ignorePathCasing(uri: URI): boolean;
63 >
64 > // --- path math
65 >
66 > basenameOrAuthority(resource: URI): string;
67 >
68 > /**
69 > * Returns the basename of the path component of an uri.
70 > * @param resource
71 > */
72 > basename(resource: URI): string;
73 >
74 > /**
75 > * Returns the extension of the path component of an uri.
76 > * @param resource
77 > */
78 > extname(resource: URI): string;
79 > /**
80 > * Return a URI representing the directory of a URI path.
81 > *
82 > * @param resource The input URI.
83 > * @returns The URI representing the directory of the input URI.
84 > */
85 > dirname(resource: URI): URI;
86 > /**
87 > * Join a URI path with path fragments and normalizes the resulting path.
88 > *
89 > * @param resource The input URI.
90 > * @param pathFragment The path fragment to add to the URI path.
91 > * @returns The resulting URI.
92 > */
93 > joinPath(resource: URI, ...pathFragment: string[]): URI;
94 > /**
95 > * Normalizes the path part of a URI: Resolves `.` and `..` elements with directory names.
96 > *
97 > * @param resource The URI to normalize the path.
98 > * @returns The URI with the normalized path.
99 > */
100 > normalizePath(resource: URI): URI;
101 > /**
102 > *
103 > * @param from
104 > * @param to
105 > */
106 > relativePath(from: URI, to: URI): string | undefined;
107 > /**
108 > * Resolves an absolute or relative path against a base URI.
109 > * The path can be relative or absolute posix or a Windows path
110 > */
111 > resolvePath(base: URI, path: string): URI;
112 >
113 > // --- misc
114 >
115 > /**
116 > * Returns true if the URI path is absolute.
117 > */
118 > isAbsolutePath(resource: URI): boolean;
119 > /**
120 > * Tests whether the two authorities are the same
121 > */
122 > isEqualAuthority(a1: string, a2: string): boolean;
123 > /**
124 > * Returns true if the URI path has a trailing path separator
125 > */
126 > hasTrailingPathSeparator(resource: URI, sep?: string): boolean;
127 > /**
128 > * Removes a trailing path separator, if there's one.
129 > * Important: Doesn't remove the first slash, it would make the URI invalid
130 > */
131 > removeTrailingPathSeparator(resource: URI, sep?: string): URI;
132 > /**
133 > * Adds a trailing path separator to the URI if there isn't one already.
134 > * For example, c:\ would be unchanged, but c:\users would become c:\users\
135 > */
136 > addTrailingPathSeparator(resource: URI, sep?: string): URI;
137 > }
138 >
139 > export class ExtUri implements IExtUri {
140 >
141 > constructor(private _ignorePathCasing: (uri: URI) => boolean) { }
142 >
143 > compare(uri1: URI, uri2: URI, ignoreFragment: boolean = false): number {
144 if (uri1 === uri2) {
145 return 0;
147 return strCompare(this.getComparisonKey(uri1, ignoreFragment), this.getComparisonKey(uri2, ignoreFragment));
148 }
149 > resources.ts
150 > isEqual(uri1: URI | undefined, uri2: URI | undefined, ignoreFragment: boolean = false): boolean {
151 > if (uri1 === uri2) { resources.ts
152 > return true; resources.ts
153 > }
154 > if (!uri1 || !uri2) { resources.ts
155 return false;
156 }
157 > return this.getComparisonKey(uri1, ignoreFragment) === this.getComparisonKey(uri2, ignoreFragment); resources.ts
158 > } resources.ts
159 > resources.ts
160 > getComparisonKey(uri: URI, ignoreFragment: boolean = false): string {
161 > return uri.with({ resources.ts
162 > path: this._ignorePathCasing(uri) ? uri.path.toLowerCase() : undefined,
163 > fragment: ignoreFragment ? null : undefined
164 > }).toString();
165 > }
166 > resources.ts
167 > ignorePathCasing(uri: URI): boolean {
168 return this._ignorePathCasing(uri);
169 }
170 > resources.ts
171 > isEqualOrParent(base: URI, parentCandidate: URI, ignoreFragment: boolean = false): boolean {
172 if (base.scheme === parentCandidate.scheme) {
173 if (base.scheme === Schemas.file) {
180 return false;
181 }
182 > resources.ts
183 > // --- path math
184 >
185 > joinPath(resource: URI, ...pathFragment: string[]): URI {
186 > return URI.joinPath(resource, ...pathFragment); resources.ts
187 > }
188 > resources.ts
189 > basenameOrAuthority(resource: URI): string {
190 return basename(resource) || resource.authority;
191 }
192 > resources.ts
193 > basename(resource: URI, suffix?: string): string {
194 > return paths.posix.basename(resource.path, suffix); resources.ts
195 > }
196 > resources.ts
197 > extname(resource: URI): string {
198 return paths.posix.extname(resource.path);
199 }
200 > resources.ts
201 > dirname(resource: URI): URI {
202 > if (resource.path.length === 0) { resources.ts
203 return resource;
204 }
205 > let dirname; resources.ts
206 > if (resource.scheme === Schemas.file) {
207 dirname = URI.file(paths.dirname(originalFSPath(resource))).path;
208 > } else { resources.ts
209 > dirname = paths.posix.dirname(resource.path); resources.ts
210 > if (resource.authority && dirname.length && dirname.charCodeAt(0) !== CharCode.Slash) {
211 console.error(`dirname("${resource.toString})) resulted in a relative path`);
212 dirname = '/'; // If a URI contains an authority component, then the path component must either be empty or begin with a CharCode.Slash ("/") character
213 }
214 > } resources.ts
215 > return resource.with({ resources.ts
216 > path: dirname
217 > });
218 > }
219 > resources.ts
220 > normalizePath(resource: URI): URI {
221 if (!resource.path.length) {
222 return resource;
232 });
233 }
234 > resources.ts
235 > relativePath(from: URI, to: URI): string | undefined {
236 if (from.scheme !== to.scheme || !isEqualAuthority(from.authority, to.authority)) {
237 return undefined;
257 return paths.posix.relative(fromPath, toPath);
258 }
259 > resources.ts
260 > resolvePath(base: URI, path: string): URI {
261 if (base.scheme === Schemas.file) {
262 const newURI = URI.file(paths.resolve(originalFSPath(base), path));
271 });
272 }
273 > resources.ts
274 > // --- misc
275 >
276 > isAbsolutePath(resource: URI): boolean {
277 > return !!resource.path && resource.path[0] === '/'; resources.ts
278 > }
279 > resources.ts
280 > isEqualAuthority(a1: string | undefined, a2: string | undefined) {
281 return a1 === a2 || (a1 !== undefined && a2 !== undefined && equalsIgnoreCase(a1, a2));
282 }
283 > resources.ts
284 > hasTrailingPathSeparator(resource: URI, sep: string = paths.sep): boolean {
285 if (resource.scheme === Schemas.file) {
286 const fsp = originalFSPath(resource);
291 }
292 }
293 > resources.ts
294 > removeTrailingPathSeparator(resource: URI, sep: string = paths.sep): URI {
295 // Make sure that the path isn't a drive letter. A trailing separator there is not removable.
296 if (hasTrailingPathSeparator(resource, sep)) {
299 return resource;
300 }
301 > resources.ts
302 > addTrailingPathSeparator(resource: URI, sep: string = paths.sep): URI {
303 let isRootSep: boolean = false;
304 if (resource.scheme === Schemas.file) {
315 return resource;
316 }
317 > } resources.ts
318 >
319 >
320 > /**
321 > * Unbiased utility that takes uris "as they are". This means it can be interchanged with
322 > * uri#toString() usages. The following is true
323 > * ```
324 > * assertEqual(aUri.toString() === bUri.toString(), exturi.isEqual(aUri, bUri))
325 > * ```
326 > */
327 > export const extUri = new ExtUri(() => false);
328 >
329 > /**
330 > * BIASED utility that _mostly_ ignored the case of urs paths. ONLY use this util if you
331 > * understand what you are doing.
332 > *
333 > * This utility is INCOMPATIBLE with `uri.toString()`-usages and both CANNOT be used interchanged.
334 > *
335 > * When dealing with uris from files or documents, `extUri` (the unbiased friend)is sufficient
336 > * because those uris come from a "trustworthy source". When creating unknown uris it's always
337 > * better to use `IUriIdentityService` which exposes an `IExtUri`-instance which knows when path
338 > * casing matters.
339 > */
340 > export const extUriBiasedIgnorePathCase = new ExtUri(uri => {
341 // A file scheme resource is in the same platform as code, so ignore case for non linux platforms
342 // Resource can be from another platform. Lowering the case as an hack. Should come from File system provider
343 return uri.scheme === Schemas.file ? !isLinux : true;
344 });
345 > resources.ts
346 >
347 > /**
348 > * BIASED utility that always ignores the casing of uris paths. ONLY use this util if you
349 > * understand what you are doing.
350 > *
351 > * This utility is INCOMPATIBLE with `uri.toString()`-usages and both CANNOT be used interchanged.
352 > *
353 > * When dealing with uris from files or documents, `extUri` (the unbiased friend)is sufficient
354 > * because those uris come from a "trustworthy source". When creating unknown uris it's always
355 > * better to use `IUriIdentityService` which exposes an `IExtUri`-instance which knows when path
356 > * casing matters.
357 > */
358 > export const extUriIgnorePathCase = new ExtUri(_ => true);
359 >
360 > export const isEqual = extUri.isEqual.bind(extUri);
361 > export const isEqualOrParent = extUri.isEqualOrParent.bind(extUri);
362 > export const getComparisonKey = extUri.getComparisonKey.bind(extUri);
363 > export const basenameOrAuthority = extUri.basenameOrAuthority.bind(extUri);
364 > export const basename = extUri.basename.bind(extUri);
365 > export const extname = extUri.extname.bind(extUri);
366 > export const dirname = extUri.dirname.bind(extUri);
367 > export const joinPath = extUri.joinPath.bind(extUri);
368 > export const normalizePath = extUri.normalizePath.bind(extUri);
369 > export const relativePath = extUri.relativePath.bind(extUri);
370 > export const resolvePath = extUri.resolvePath.bind(extUri);
371 > export const isAbsolutePath = extUri.isAbsolutePath.bind(extUri);
372 > export const isEqualAuthority = extUri.isEqualAuthority.bind(extUri);
373 > export const hasTrailingPathSeparator = extUri.hasTrailingPathSeparator.bind(extUri);
374 > export const removeTrailingPathSeparator = extUri.removeTrailingPathSeparator.bind(extUri);
375 > export const addTrailingPathSeparator = extUri.addTrailingPathSeparator.bind(extUri);
376 >
377 > //#endregion
378 >
379 > export function distinctParents<T>(items: T[], resourceAccessor: (item: T) => URI): T[] {
380 const distinctParents: T[] = [];
381 for (let i = 0; i < items.length; i++) {
396 return distinctParents;
397 }
398 > resources.ts
399 > /**
400 > * Data URI related helpers.
401 > */
402 > export namespace DataUri {
403 >
404 > export const META_DATA_LABEL = 'label';
405 > export const META_DATA_DESCRIPTION = 'description';
406 > export const META_DATA_SIZE = 'size';
407 > export const META_DATA_MIME = 'mime';
408 >
409 > export function parseMetaData(dataUri: URI): Map<string, string> {
410 const metadata = new Map<string, string>();
411
429 return metadata;
430 }
431 > } resources.ts
432 >
433 > export function toLocalResource(resource: URI, authority: string | undefined, localScheme: string): URI {
434 if (authority) {
435 let path = resource.path;
src/vs/platform/agentHost/node/shared/mcpCustomizationController.ts 280 covered LOC · 29 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- mcpCustomizationController.ts
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 > import { Disposable } from '../../../../base/common/lifecycle.js';
7 > import { derived, observableValue, transaction, type IObservable, type ITransaction } from '../../../../base/common/observable.js';
8 > import { URI } from '../../../../base/common/uri.js';
9 > import { ActionType } from '../../common/state/protocol/common/actions.js';
10 > import { CustomizationType, McpServerStatus, type AhpMcpUiHostCapabilities, type ChildCustomization, type Customization, type McpServerCustomization, type McpServerState } from '../../common/state/protocol/channels-session/state.js';
11 > import { DEFAULT_MCP_APP, DEFAULT_MCP_APP_CAPABILITIES } from '../../common/state/protocol/mcpAppDefaults.js';
12 > import type { SessionAction } from '../../common/state/sessionActions.js';
13 > import { AgentHostStateManager, IAgentHostStateManager } from '../agentHostStateManager.js';
14 >
15 > /**
16 > * SDK-neutral description of a single MCP server, as the controller's
17 > * caller sees it. Each provider adapts its own SDK events into this
18 > * shape (Copilot, Claude, Codex, …) and feeds them to
19 > * {@link McpCustomizationController}.
20 > */
21 > export interface ISdkMcpServer {
22 > /** Server name (used both as the customization name and the channel suffix). */
23 > readonly name: string;
24 > /** Current lifecycle state. */
25 > readonly state: McpServerState;
26 > /** Explicit runtime enablement when the SDK distinguishes disabled from stopped. */
27 > readonly enabled?: boolean;
28 > }
29 >
30 > /**
31 > * Runtime fields of an MCP server customization that this controller
32 > * owns — the high-frequency `state`/`channel` pair. Consumers overlay
33 > * these onto their published customizations (keyed by customization id)
34 > * so a wholesale customization republish preserves live MCP status
35 > * rather than resetting it to the `Stopped` default baked into
36 > * `makeMcpServerCustomization`.
37 > */
38 > export type IMcpServerRuntimeState = Pick<McpServerCustomization, 'state' | 'channel'>;
39 >
40 > /**
41 > * Re-export so existing imports of `DEFAULT_MCP_APP_CAPABILITIES` from
42 > * the controller keep working — the canonical home is now
43 > * `agentHost/common/state/protocol/mcpAppDefaults.ts`.
44 > */
45 > export { DEFAULT_MCP_APP_CAPABILITIES, DEFAULT_MCP_APP };
46 >
47 > /**
48 > * Lookup callback the controller uses to find an existing child MCP
49 > * customization id by server name. The agent's plugin layer publishes
50 > * MCP customizations with provider-defined ids
51 > * (e.g. `pluginParsers.makeMcpServerCustomization` uses
52 > * `buildChildId(definitionUri, 'mcp=' + encodeURIComponent(name))`), so
53 > * we resolve them by name at action-dispatch time rather than trying to
54 > * reconstruct the id.
55 > *
56 > * Returns `undefined` when no existing entry matches — in that case the
57 > * controller surfaces a bare top-level customization for the server.
58 > */
59 > export type IMcpChildIdResolver = (serverName: string) => string | undefined;
60 >
61 > /**
62 > * Options for {@link McpCustomizationController}.
63 > */
64 > export interface IMcpCustomizationControllerOptions {
65 > /** Provider id (e.g. `'copilotcli'`). Used as the channel URI authority. */
66 > readonly providerId: string;
67 > /** Session id (the raw id, not the full URI). Used as the channel path segment. */
68 > readonly sessionId: string;
69 > /** Canonical session URI used to resolve persisted customization state. */
70 > readonly sessionUri: URI;
71 > /**
72 > * Resolves an existing child customization id for a given server
73 > * name. See {@link IMcpChildIdResolver}.
74 > */
75 > readonly resolveChildId: IMcpChildIdResolver;
76 > /** Emits a {@link SessionAction} into the session's action stream. */
77 > readonly emit: (action: SessionAction) => void;
78 > /**
79 > * MCP App capabilities to advertise on every ready server. Defaults
80 > * to {@link DEFAULT_MCP_APP_CAPABILITIES}.
81 > */
82 > readonly capabilities?: AhpMcpUiHostCapabilities;
83 > }
84 >
85 > interface ILiveEntry {
86 > readonly serverName: string;
87 > readonly state: McpServerState;
88 > readonly enabled: boolean;
89 > /** Top-level customization id (when no child match was found). */
90 > readonly topLevelId?: string;
91 > }
92 >
93 > export function buildMcpTopLevelCustomizationId(providerId: string, sessionId: string, serverName: string): string {
94 return `mcp-top-level:${providerId}:${sessionId}:${serverName}`;
95 }
97 > export function buildMcpChannel(providerId: string, sessionId: string, serverName: string): string {
98 return `mcp://${providerId}/${encodeURIComponent(sessionId)}/${encodeURIComponent(serverName)}`;
99 }
101 > /**
102 > * Translates a stream of SDK-reported MCP server states into AHP
103 > * customization actions:
104 > *
105 > * - For servers backed by an existing child customization (plugin- or
106 > * directory-derived), the controller emits
107 > * {@link ActionType.SessionMcpServerStateChanged} keyed on the
108 > * resolved child id. The reducer narrowly updates `state` and
109 > * `channel` on the matching child.
110 > * - For servers with no matching child (typically globally-configured
111 > * MCP servers the SDK reports), the controller emits a full
112 > * {@link ActionType.SessionCustomizationUpdated} carrying a bare
113 > * top-level {@link McpServerCustomization}. The same id is reused
114 > * across updates, so the reducer's upsert keeps in-place.
115 > *
116 > * The controller is SDK-agnostic: providers translate their own events
117 > * into {@link ISdkMcpServer} and call {@link applyAll} / {@link applyOne}.
118 > * If a provider reports a coarse {@link McpServerStatus.Starting} update
119 > * after a richer {@link McpServerStatus.AuthRequired} state, the controller
120 > * preserves the auth-required state until a definitive
121 > * {@link McpServerStatus.Ready}, {@link McpServerStatus.Error}, or
122 > * {@link McpServerStatus.Stopped} update arrives.
123 > */
124 > export class McpCustomizationController extends Disposable {
125 >
126 > /** Per-server live entries, keyed by server name. */
127 > private readonly _live = observableValue<ReadonlyMap<string, ILiveEntry>>(this, new Map());
128 >
129 > /**
130 > * Snapshot of every live server's runtime {@link IMcpServerRuntimeState},
131 > * keyed by the customization id under which it is published (the
132 > * minted top-level id, or the plugin-derived child id resolved via
133 > * {@link IMcpChildIdResolver}). Derived from {@link _live}. Callers mirror
134 > * this into their own published customizations so a wholesale republish
135 > * preserves live MCP status. Servers whose child id cannot currently be
136 > * resolved are omitted.
137 > */
138 > readonly runtimeStates: IObservable<ReadonlyMap<string, IMcpServerRuntimeState>>;
139 >
140 > constructor(
141 private readonly _options: IMcpCustomizationControllerOptions,
142 @IAgentHostStateManager private readonly _stateManager: AgentHostStateManager,
155 });
156 }
158 > /** Snapshot for inclusion in `getSessionCustomizations()` results. */
159 > topLevelCustomizations(): readonly McpServerCustomization[] {
160 const out: McpServerCustomization[] = [];
161 for (const entry of this._live.get().values()) {
167 return out;
168 }
170 > /**
171 > * Names of MCP servers currently in {@link McpServerStatus.Ready},
172 > * paired with their channel URI. Used by providers to drive
173 > * polling-based notification streams (e.g. re-fetch `tools/list`
174 > * after a refresh hint and fire
175 > * `notifications/tools/list_changed` if the result changed).
176 > */
177 > readyChannels(): readonly { readonly serverName: string; readonly channel: string }[] {
178 const out: { serverName: string; channel: string }[] = [];
179 for (const entry of this._live.get().values()) {
188 return out;
189 }
191 > /**
192 > * Returns the customization id currently associated with the MCP
193 > * server named `serverName`, or `undefined` when no customization
194 > * exists. Top-level entries return the minted top-level id; child
195 > * entries return whatever {@link IMcpChildIdResolver} resolves to
196 > * for that server. Used by providers to tag
197 > * {@link ToolCallMcpContributor.customizationId | tool-call contributors}
198 > * so clients can correlate MCP tool calls with the originating
199 > * server customization.
200 > */
201 > customizationIdForServer(serverName: string): string | undefined {
202 const live = this._live.get().get(serverName);
203 if (live?.topLevelId !== undefined) {
206 return this._options.resolveChildId(serverName);
207 }
209 > /** Returns the live server name associated with a customization id. */
210 > serverNameForCustomizationId(id: string): string | undefined {
211 for (const entry of this._live.get().values()) {
212 const entryId = entry.topLevelId ?? this._options.resolveChildId(entry.serverName);
217 return undefined;
218 }
220 > /** Returns the last live state recorded for the MCP server named `serverName`. */
221 > stateForServer(serverName: string): McpServerState | undefined {
222 return this._live.get().get(serverName)?.state;
223 }
225 > /** Snapshot used by providers to reconcile desired and observed enablement. */
226 > serverEnablement(): readonly { readonly serverName: string; readonly customizationId: string; readonly enabled: boolean }[] {
227 const result: { serverName: string; customizationId: string; enabled: boolean }[] = [];
228 for (const entry of this._live.get().values()) {
234 return result;
235 }
237 > /**
238 > * Returns the `mcp://` AHP channel URI currently advertised for the
239 > * MCP server named `serverName`, or `undefined` when the server is
240 > * not in {@link McpServerStatus.Ready}. Used by providers to attach
241 > * the channel to MCP App `_meta.ui` so clients can route App
242 > * sub-RPCs (tools/call, resources/read, sampling/createMessage)
243 > * back through {@link IAgentHostService.handleMcpRequest}.
244 > */
245 > channelForServer(serverName: string): string | undefined {
246 const live = this._live.get().get(serverName);
247 if (!live || live.state.kind !== McpServerStatus.Ready) {
250 return this._buildChannel(serverName, live.state);
251 }
253 > /**
254 > * Replaces the live inventory with `servers`. Servers no longer
255 > * present are removed; new servers and changed servers are upserted.
256 > * Batched in a single transaction so {@link runtimeStates} observers
257 > * see one coalesced update.
258 > */
259 > applyAll(servers: readonly ISdkMcpServer[]): void {
260 transaction(tx => {
261 const seen = new Set<string>();
271 });
272 }
274 > /** Upserts a single server. */
275 > applyOne(server: ISdkMcpServer): void {
276 transaction(tx => this._applyOne(server, tx));
277 }
279 > /**
280 > * Optimistically transitions the named servers to
281 > * {@link McpServerStatus.Starting}, skipping any that are already
282 > * {@link McpServerStatus.Ready} (nothing to (re)start), blocked on
283 > * {@link McpServerStatus.AuthRequired} (needs the user, not a background
284 > * start), or already {@link McpServerStatus.Starting}.
285 > *
286 > * The SDK connects enabled servers in the background — on an explicit
287 > * start or when a turn begins — but emits no live "starting" event, so
288 > * without this a connecting server would read as its last settled state
289 > * (e.g. `Stopped`) until it resolves. Callers invoke this immediately
290 > * before the (blocking) connect so clients see the transient `Starting`
291 > * state; the subsequent SDK status settles each server. Batched in a
292 > * single transaction so {@link runtimeStates} observers see one update.
293 > */
294 > markStarting(serverNames: Iterable<string>): void {
295 transaction(tx => {
296 for (const name of serverNames) {
303 });
304 }
306 > private _applyOne(server: ISdkMcpServer, tx: ITransaction): void {
307 const previous = this._live.get().get(server.name);
308 const state = this._stateForUpdate(previous?.state, server.state);
332 });
333 }
335 > /**
336 > * Removes a server from the live inventory. For top-level entries
337 > * (bare servers with no plugin-derived child) emits
338 > * {@link ActionType.SessionCustomizationRemoved} so the entry is
339 > * dropped from session state, not just from the in-memory live
340 > * inventory.
341 > *
342 > * For child entries we emit a final {@link ActionType.SessionMcpServerStateChanged}
343 > * carrying {@link McpServerStatus.Stopped} so the UI sees the
344 > * server settle into a terminal state; the plugin layer owns the
345 > * actual removal of the child container.
346 > */
347 > remove(serverName: string): void {
348 transaction(tx => this._remove(serverName, tx));
349 }
351 > private _remove(serverName: string, tx: ITransaction): void {
352 const entry = this._live.get().get(serverName);
353 if (!entry) {
372 });
373 }
375 > // ---- internals ---------------------------------------------------------
376 >
377 > /** Immutable upsert into the {@link _live} observable. */
378 > private _setLiveEntry(serverName: string, entry: ILiveEntry, tx: ITransaction): void {
379 const next = new Map(this._live.get());
380 next.set(serverName, entry);
381 this._live.set(next, tx);
382 }
384 > /** Immutable delete from the {@link _live} observable. */
385 > private _deleteLiveEntry(serverName: string, tx: ITransaction): void {
386 const current = this._live.get();
387 if (!current.has(serverName)) {
392 this._live.set(next, tx);
393 }
395 > private _stateForUpdate(previous: McpServerState | undefined, next: McpServerState): McpServerState {
396 if (previous?.kind === McpServerStatus.AuthRequired && next.kind === McpServerStatus.Starting) {
397 return previous;
399 return next;
400 }
402 > private _mintTopLevelId(serverName: string): string {
403 return buildMcpTopLevelCustomizationId(this._options.providerId, this._options.sessionId, serverName);
404 }
406 > private _buildChannel(serverName: string, state: McpServerState): string | undefined {
407 if (state.kind !== McpServerStatus.Ready) {
408 return undefined;
410 return buildMcpChannel(this._options.providerId, this._options.sessionId, serverName);
411 }
413 > private _buildTopLevel(id: string, serverName: string, state: McpServerState, enabled: boolean): McpServerCustomization {
414 const channel = this._buildChannel(serverName, state);
415 // Per AHP spec, `mcpApp` is a static capability declaration —
433 };
434 }
436 >
437 > /**
438 > * Convenience helper: given a flat list of {@link Customization}
439 > * entries, returns the id of the first MCP child customization whose
440 > * name matches `serverName`. Used by providers to wire up
441 > * {@link IMcpCustomizationControllerOptions.resolveChildId} without
442 > * each provider having to walk the customization tree itself.
443 > */
444 > export function findMcpChildId(customizations: readonly Customization[], serverName: string): string | undefined {
445 return getMcpServerCustomizations(customizations).find(server => server.name === serverName)?.id;
446 }
448 > export function getMcpServerCustomizations(customizations: readonly Customization[]): readonly McpServerCustomization[] {
449 const result: McpServerCustomization[] = [];
450 for (const top of customizations) {
461 return result;
462 }
464 > export function getEffectiveMcpServerCustomizations(customizations: readonly Customization[]): readonly McpServerCustomization[] {
465 const result: McpServerCustomization[] = [];
466 for (const top of customizations) {
477 return result;
478 }
480 > export function applyMcpServerEnablement(customizations: readonly Customization[], desired: readonly Customization[]): readonly Customization[] {
481 const desiredById = new Map(getEffectiveMcpServerCustomizations(desired).map(server => [server.id, server.enabled]));
482 return customizations.map(customization => {
493 });
494 }
496 function applyMcpEnablement<T extends McpServerCustomization | Extract<ChildCustomization, { type: CustomizationType.McpServer }>>(customization: T, desiredById: ReadonlyMap<string, boolean>): T {
497 const enabled = desiredById.get(customization.id);
498 return enabled === undefined || enabled === customization.enabled ? customization : { ...customization, enabled };
499 }
501 > export function findMcpServerName(customizations: readonly Customization[], id: string): string | undefined {
502 return getMcpServerCustomizations(customizations).find(server => server.id === id)?.name;
503 }
505 > /**
506 > * Parsed `mcp://<providerId>/<sessionId>/<serverName>` URI as minted by
507 > * {@link McpCustomizationController}. The path segments are
508 > * URL-decoded.
509 > */
510 > export interface IMcpChannelRoute {
511 > readonly providerId: string;
512 > readonly sessionId: string;
513 > readonly serverName: string;
514 > }
515 >
516 > /**
517 > * Decodes a channel URI string into a {@link IMcpChannelRoute}, or
518 > * returns `undefined` when the URI is not an `mcp://` channel or the
519 > * path is malformed. Intentionally uses string parsing rather than
520 > * `URI.parse` so the helper stays usable from layers (e.g. agentService
521 > * test fixtures) without a full URI dependency.
522 > */
523 > export function parseMcpChannelUri(uri: string): IMcpChannelRoute | undefined {
524 const prefix = 'mcp://';
525 if (!uri.startsWith(prefix)) {
src/vs/platform/agentHost/common/state/protocol/channels-changeset/state.ts 277 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- state.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > import type { StringOrMarkdown, FileEdit, ErrorInfo } from '../common/state.js';
10 >
11 > // ─── Changesets ──────────────────────────────────────────────────────────────
12 >
13 > /**
14 > * Catalogue entry describing one changeset the server can produce for a
15 > * session.
16 > *
17 > * Catalogue entries are intentionally lightweight — just enough to render a
18 > * chip or list row without subscribing. Full per-changeset detail
19 > * ({@link ChangesetState}) lives on the subscribable URI obtained by
20 > * expanding {@link uriTemplate}.
21 > *
22 > * @category Changesets
23 > */
24 > export interface Changeset {
25 > /** Human-readable label, e.g. `"Uncommitted Changes"`. */
26 > label: string;
27 > /**
28 > * RFC 6570 URI template. Clients parse the variables directly out of the
29 > * template using the standard `{name}` syntax — they are not redeclared
30 > * here.
31 > *
32 > * Only the following template shapes are defined by this protocol; any
33 > * other variable name MUST be ignored by clients (there is no
34 > * protocol-defined way to obtain values for unknown variables):
35 > *
36 > * | Variables in template | Meaning |
37 > * | ------------------------------------------- | ------------------------------------------------------------------------------------ |
38 > * | _(none)_ | A static, session-wide changeset. The template is itself a subscribable URI. |
39 > * | `{turnId}` | Per-turn slice. Expand with a `Turn.id` from the session. |
40 > * | `{originalTurnId}` and `{modifiedTurnId}` | Diff between two turns. Both variables MUST be present. |
41 > *
42 > * Future protocol versions MAY add new well-known variables.
43 > */
44 > uriTemplate: string;
45 > /** Optional longer description. */
46 > description?: string;
47 > /**
48 > * Advisory hint describing what kind of changeset this is, so clients can
49 > * group, sort, or render an appropriate icon without parsing
50 > * {@link uriTemplate}. Recognized values include:
51 > *
52 > * - `'session'`: a static, session-wide changeset covering all changes the
53 > * agent has produced in this session.
54 > * - `'branch'`: changes relative to a base branch (e.g. a feature branch
55 > * diffed against `main`).
56 > * - `'uncommitted'`: the workspace's current uncommitted changes.
57 > * - `'turn'`: changes produced by a single turn. Typically paired with a
58 > * `{turnId}` variable in {@link uriTemplate}.
59 > * - `'compare-turns'`: a diff between two turns. Typically paired with
60 > * `{originalTurnId}` and `{modifiedTurnId}` variables in
61 > * {@link uriTemplate}.
62 > *
63 > * Implementations MAY provide additional values; clients SHOULD fall back
64 > * to a reasonable default when an unknown value is encountered.
65 > */
66 > changeKind: string;
67 > /**
68 > * Optional capability declarations for this changeset. Absent (or an empty
69 > * object) means the changeset advertises no optional capabilities.
70 > *
71 > * Because the catalogue entry is delivered up-front on
72 > * {@link ChangesetState | the session's changeset list}, clients can decide
73 > * whether to surface capability-gated UI (such as review checkboxes) without
74 > * first subscribing to the changeset URI. Mirrors the presence-flag
75 > * convention of `ClientCapabilities`.
76 > */
77 > capabilities?: ChangesetCapabilities;
78 > }
79 >
80 > /**
81 > * Optional capabilities a changeset advertises on its catalogue
82 > * {@link Changeset} entry.
83 > *
84 > * Each field is a presence flag: an empty object `{}` means "supported",
85 > * absence means "not supported". Sub-fields on individual capabilities are
86 > * reserved for future per-capability options.
87 > *
88 > * @category Changesets
89 > */
90 > export interface ChangesetCapabilities {
91 > /**
92 > * The changeset supports the per-file **review** workflow. When declared,
93 > * clients MAY surface a GitHub-style "Viewed" toggle per file and dispatch
94 > * {@link ChangesetFilesReviewChangedAction | `changeset/filesReviewChanged`} to
95 > * set each file's {@link ChangesetFile.reviewed} flag. Clients that omit
96 > * handling MUST treat the changeset as non-reviewable.
97 > */
98 > review?: Record<string, never>;
99 > }
100 >
101 > /**
102 > * Computation lifecycle of a {@link ChangesetState}.
103 > *
104 > * @category Changesets
105 > */
106 > export const enum ChangesetStatus {
107 > /** The server is still computing the contents of this changeset. */
108 > Computing = 'computing',
109 > /** The changeset has been fully computed and is up-to-date. */
110 > Ready = 'ready',
111 > /**
112 > * Computation failed. The cause is described by
113 > * {@link ChangesetState.error}.
114 > */
115 > Error = 'error',
116 > }
117 >
118 > /**
119 > * Full state for a single changeset, returned when a client subscribes to
120 > * an expanded changeset URI.
121 > *
122 > * The client already knows the URI it subscribed to, so this state does
123 > * not redundantly carry it (or the catalogue's `id`, `label`, etc.).
124 > * Aggregate counts (`additions`, `deletions`, `files`) are likewise
125 > * omitted: clients trivially compute them from `files[].edit.diff`.
126 > *
127 > * @category Changesets
128 > */
129 > export interface ChangesetState {
130 > /** Computation lifecycle. */
131 > status: ChangesetStatus;
132 > /** Present iff `status === ChangesetStatus.Error`. */
133 > error?: ErrorInfo;
134 > /** Files in this changeset, keyed by {@link ChangesetFile.id}. */
135 > files: ChangesetFile[];
136 > /**
137 > * Operations the client may invoke against this changeset. Omit when no
138 > * operations are available.
139 > */
140 > operations?: ChangesetOperation[];
141 > }
142 >
143 > /**
144 > * One file entry within a {@link ChangesetState}.
145 > *
146 > * @category Changesets
147 > */
148 > export interface ChangesetFile {
149 > /**
150 > * Stable identifier within the changeset. Typically `after.uri`
151 > * (or `before.uri` for deletions).
152 > */
153 > id: string;
154 > /**
155 > * Reuses the existing {@link FileEdit} shape. Clients derive line
156 > * additions, deletions, and rename/create/delete semantics from this.
157 > */
158 > edit: FileEdit;
159 > /**
160 > * Whether a reviewer has marked this file as reviewed (the GitHub-style
161 > * "Viewed" checkbox). Absent is equivalent to `false` — clients MUST treat
162 > * a missing value as not-yet-reviewed.
163 > *
164 > * Requires the changeset to advertise {@link ChangesetCapabilities.review}.
165 > * Clients toggle it by dispatching
166 > * {@link ChangesetFilesReviewChangedAction | `changeset/filesReviewChanged`};
167 > * the server MAY also originate it (e.g. an agent self-reviewing its own
168 > * output).
169 > *
170 > * There is no content version in the protocol, so review is **not** reset
171 > * automatically when a file's contents change under a stable id. The server,
172 > * which is the authority on what changed, resets review explicitly — either
173 > * by re-emitting the file (via {@link ChangesetFileSetAction} or
174 > * {@link ChangesetContentChangedAction}) without `reviewed: true`, or by
175 > * dispatching `changeset/filesReviewChanged` with `reviewed: false`.
176 > */
177 > reviewed?: boolean;
178 > /**
179 > * Server-defined opaque metadata, surfaced to operations and tooling
180 > * but not interpreted by the protocol.
181 > */
182 > _meta?: Record<string, unknown>;
183 > }
184 >
185 > /**
186 > * Execution lifecycle of a {@link ChangesetOperation}.
187 > *
188 > * An operation is invoked imperatively via `invokeChangesetOperation`, but
189 > * its progress and outcome are reflected back into changeset state so that
190 > * every subscriber observes a consistent view (e.g. a spinner on a "Create
191 > * Pull Request" button, or an inline error after a failed "revert").
192 > *
193 > * @category Changesets
194 > */
195 > export const enum ChangesetOperationStatus {
196 > /**
197 > * The operation is ready to be invoked. This is the default when
198 > * {@link ChangesetOperation.status} is omitted.
199 > */
200 > Idle = 'idle',
201 > /** An invocation of this operation is currently in flight. */
202 > Running = 'running',
203 > /**
204 > * The most recent invocation failed. The cause is described by
205 > * {@link ChangesetOperation.error}.
206 > */
207 > Error = 'error',
208 > /**
209 > * The operation is currently disabled and cannot be invoked.
210 > */
211 > Disabled = 'disabled',
212 > }
213 >
214 > /**
215 > * Where a {@link ChangesetOperation} can be invoked.
216 > *
217 > * @category Changesets
218 > */
219 > export const enum ChangesetOperationScope {
220 > /** Applies to the whole changeset. */
221 > Changeset = 'changeset',
222 > /** Applies to a single file within the changeset. */
223 > Resource = 'resource',
224 > /** Applies to a line range within a single file. */
225 > Range = 'range',
226 > }
227 >
228 > /**
229 > * A server-declared invokable verb the client can run against a
230 > * changeset, a file, or a range — `"stage"`, `"revert"`, `"create-pr"`,
231 > * and so on.
232 > *
233 > * The term "operation" is used deliberately to avoid colliding with the
234 > * protocol-level [Actions](/guide/actions) that mutate state.
235 > *
236 > * @category Changesets
237 > */
238 > export interface ChangesetOperation {
239 > /** Stable identifier, unique within this changeset. */
240 > id: string;
241 > /** Human-readable button/menu label. */
242 > label: string;
243 > /** Optional longer description shown on hover or in tooltips. */
244 > description?: string;
245 > /** Where this operation can be invoked. */
246 > scopes: ChangesetOperationScope[];
247 > /**
248 > * Optional confirmation prompt to show before invoking. When present,
249 > * the client MUST display this message to the user (typically in a
250 > * confirmation dialog) and only invoke the operation after the user
251 > * accepts. The presence of this field also signals that the operation
252 > * is destructive — clients SHOULD style the affirmative button
253 > * accordingly (e.g. with a warning colour).
254 > */
255 > confirmation?: StringOrMarkdown;
256 > /** Optional generic icon hint, e.g. `"check"`, `"trash"`. */
257 > icon?: string;
258 > /** Optional group identifier, used to group related operations together. */
259 > group?: string;
260 > /**
261 > * Current execution status. The server sets
262 > * {@link ChangesetOperationStatus.Running | Running} while an invocation
263 > * is in flight, {@link ChangesetOperationStatus.Error | Error} when the
264 > * most recent invocation failed, and
265 > * {@link ChangesetOperationStatus.Idle | Idle} otherwise.
266 > *
267 > * Clients SHOULD reflect this state in the UI — e.g. disabling the
268 > * control or showing a spinner while `Running`, and surfacing
269 > * {@link error} while `Error`.
270 > */
271 > status: ChangesetOperationStatus;
272 > /**
273 > * Cause of failure. Present iff
274 > * `status === ChangesetOperationStatus.Error`; otherwise omitted.
275 > */
276 > error?: ErrorInfo;
277 > }
src/vs/platform/request/common/request.ts 277 covered LOC · 15 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- request.ts
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 > import { streamToBuffer } from '../../../base/common/buffer.js';
7 > import { CancellationToken } from '../../../base/common/cancellation.js';
8 > import { getErrorMessage } from '../../../base/common/errors.js';
9 > import { Emitter, Event } from '../../../base/common/event.js';
10 > import { Disposable } from '../../../base/common/lifecycle.js';
11 > import { IHeaders, IRequestContext, IRequestOptions } from '../../../base/parts/request/common/request.js';
12 > import { localize } from '../../../nls.js';
13 > import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationRegistry } from '../../configuration/common/configurationRegistry.js';
14 > import { createDecorator } from '../../instantiation/common/instantiation.js';
15 > import { ILogService } from '../../log/common/log.js';
16 > import { Registry } from '../../registry/common/platform.js';
17 >
18 > export const IRequestService = createDecorator<IRequestService>('requestService');
19 >
20 > /**
21 > * Use as the {@link IRequestOptions.callSite} value to prevent
22 > * request telemetry from being emitted. This is needed for
23 > * callers such as the telemetry sender to avoid cyclical calls.
24 > */
25 > export const NO_FETCH_TELEMETRY = 'NO_FETCH_TELEMETRY';
26 >
27 > export interface IRequestCompleteEvent {
28 > readonly callSite: string;
29 > readonly latency: number;
30 > readonly statusCode: number | undefined;
31 > }
32 >
33 > export interface AuthInfo {
34 > isProxy: boolean;
35 > scheme: string;
36 > host: string;
37 > port: number;
38 > realm: string;
39 > attempt: number;
40 > }
41 >
42 > export interface Credentials {
43 > username: string;
44 > password: string;
45 > }
46 >
47 > export interface IRequestService {
48 > readonly _serviceBrand: undefined;
49 >
50 > /**
51 > * Fires when a request completes (successfully or with an error response).
52 > */
53 > readonly onDidCompleteRequest: Event<IRequestCompleteEvent>;
54 >
55 > request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext>;
56 >
57 > resolveProxy(url: string): Promise<string | undefined>;
58 > lookupAuthorization(authInfo: AuthInfo): Promise<Credentials | undefined>;
59 > lookupKerberosAuthorization(url: string): Promise<string | undefined>;
60 > loadCertificates(): Promise<string[]>;
61 > }
62 >
63 > class LoggableHeaders {
64 >
65 > private headers: IHeaders | undefined;
66 >
67 > constructor(private readonly original: IHeaders) { }
68 >
69 > toJSON(): any {
70 if (!this.headers) {
71 const headers = Object.create(null);
81 return this.headers;
82 }
83 > request.ts
84 > }
85 >
86 > export abstract class AbstractRequestService extends Disposable implements IRequestService {
87 >
88 > declare readonly _serviceBrand: undefined;
89 >
90 > private counter = 0;
91 >
92 > private readonly _onDidCompleteRequest = this._register(new Emitter<IRequestCompleteEvent>());
93 > readonly onDidCompleteRequest = this._onDidCompleteRequest.event;
94 >
95 > constructor(protected readonly logService: ILogService) {
96 super();
97 }
98 > request.ts
99 > protected async logAndRequest(options: IRequestOptions, request: () => Promise<IRequestContext>): Promise<IRequestContext> {
100 const prefix = `#${++this.counter}: ${options.url}`;
101 this.logService.trace(`${prefix} - begin`, options.type, new LoggableHeaders(options.headers ?? {}));
115 }
116 }
117 > request.ts
118 > abstract request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext>;
119 > abstract resolveProxy(url: string): Promise<string | undefined>;
120 > abstract lookupAuthorization(authInfo: AuthInfo): Promise<Credentials | undefined>;
121 > abstract lookupKerberosAuthorization(url: string): Promise<string | undefined>;
122 > abstract loadCertificates(): Promise<string[]>;
123 > }
124 >
125 > export function isSuccess(context: IRequestContext): boolean {
126 return (context.res.statusCode && context.res.statusCode >= 200 && context.res.statusCode < 300) || context.res.statusCode === 1223;
127 }
128 > request.ts
129 > export function isClientError(context: IRequestContext): boolean {
130 return !!context.res.statusCode && context.res.statusCode >= 400 && context.res.statusCode < 500;
131 }
132 > request.ts
133 > export function isServerError(context: IRequestContext): boolean {
134 return !!context.res.statusCode && context.res.statusCode >= 500 && context.res.statusCode < 600;
135 }
136 > request.ts
137 > /**
138 > * Reads a header value from an {@link IHeaders} map, tolerating array-shaped
139 > * values and case-insensitive lookups.
140 > */
141 > export function readHeader(headers: IHeaders | undefined, name: string): string | undefined {
142 if (!headers) {
143 return undefined;
149 return value;
150 }
151 > request.ts
152 > /**
153 > * Parses the `Retry-After` header as a number of seconds. Returns `undefined`
154 > * if absent or not a finite positive number. The HTTP-date form is not parsed.
155 > */
156 > export function retryAfterFromHeaders(headers: IHeaders | undefined): number | undefined {
157 const value = readHeader(headers, 'retry-after');
158 if (!value) {
162 return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
163 }
164 > request.ts
165 > export function hasNoContent(context: IRequestContext): boolean {
166 return context.res.statusCode === 204;
167 }
168 > request.ts
169 export async function asText(context: IRequestContext): Promise<string | null> {
170 if (hasNoContent(context)) {
174 return buffer.toString();
175 }
176 > request.ts
177 export async function asTextOrError(context: IRequestContext): Promise<string | null> {
178 if (!isSuccess(context)) {
181 return asText(context);
182 }
183 > request.ts
184 export async function asJson<T = {}>(context: IRequestContext): Promise<T | null> {
185 if (!isSuccess(context)) {
198 }
199 }
200 > request.ts
201 > export function updateProxyConfigurationsScope(useHostProxy: boolean, useHostProxyDefault: boolean): void {
202 registerProxyConfigurations(useHostProxy, useHostProxyDefault);
203 }
204 > request.ts
205 > export const USER_LOCAL_AND_REMOTE_SETTINGS = [
206 > 'http.proxy',
207 > 'http.proxyStrictSSL',
208 > 'http.proxyKerberosServicePrincipal',
209 > 'http.noProxy',
210 > 'http.proxyAuthorization',
211 > 'http.proxySupport',
212 > 'http.systemCertificates',
213 > 'http.systemCertificatesNode',
214 > 'http.experimental.systemCertificatesV2',
215 > 'http.fetchAdditionalSupport',
216 > 'http.experimental.networkInterfaceCheckInterval',
217 > ];
218 >
219 > export const systemCertificatesNodeDefault = false;
220 >
221 > let proxyConfiguration: IConfigurationNode[] = [];
222 > let previousUseHostProxy: boolean | undefined = undefined;
223 > let previousUseHostProxyDefault: boolean | undefined = undefined;
224 > function registerProxyConfigurations(useHostProxy = true, useHostProxyDefault = true): void {
225 > if (previousUseHostProxy === useHostProxy && previousUseHostProxyDefault === useHostProxyDefault) {
226 return;
227 }
228 > request.ts
229 > previousUseHostProxy = useHostProxy;
230 > previousUseHostProxyDefault = useHostProxyDefault;
231 >
232 > const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
233 > const oldProxyConfiguration = proxyConfiguration;
234 > proxyConfiguration = [
235 > {
236 > id: 'http',
237 > order: 15,
238 > title: localize('httpConfigurationTitle', "HTTP"),
239 > type: 'object',
240 > scope: ConfigurationScope.MACHINE,
241 > properties: {
242 > 'http.useLocalProxyConfiguration': {
243 > type: 'boolean',
244 > default: useHostProxyDefault,
245 > markdownDescription: localize('useLocalProxy', "Controls whether in the remote extension host the local proxy configuration should be used. This setting only applies as a remote setting during [remote development](https://aka.ms/vscode-remote)."),
246 > restricted: true
247 > },
248 > }
249 > },
250 > {
251 > id: 'http',
252 > order: 15,
253 > title: localize('httpConfigurationTitle', "HTTP"),
254 > type: 'object',
255 > scope: ConfigurationScope.APPLICATION,
256 > properties: {
257 > 'http.electronFetch': {
258 > type: 'boolean',
259 > default: false,
260 > description: localize('electronFetch', "Controls whether use of Electron's fetch implementation instead of Node.js' should be enabled. All local extensions will get Electron's fetch implementation for the global fetch API."),
261 > restricted: true
262 > },
263 > }
264 > },
265 > {
266 > id: 'http',
267 > order: 15,
268 > title: localize('httpConfigurationTitle', "HTTP"),
269 > type: 'object',
270 > scope: useHostProxy ? ConfigurationScope.APPLICATION : ConfigurationScope.MACHINE,
271 > properties: {
272 > 'http.proxy': {
273 > type: 'string',
274 > pattern: '^(https?|socks|socks4a?|socks5h?)://([^:]*(:[^@]*)?@)?([^:]+|\\[[:0-9a-fA-F]+\\])(:\\d+)?/?$|^$',
275 > markdownDescription: localize('proxy', "The proxy setting to use. If not set, will be inherited from the `http_proxy` and `https_proxy` environment variables. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'),
276 > restricted: true
277 > },
278 > 'http.proxyStrictSSL': {
279 > type: 'boolean',
280 > default: true,
281 > markdownDescription: localize('strictSSL', "Controls whether the proxy server certificate should be verified against the list of supplied CAs. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'),
282 > restricted: true
283 > },
284 > 'http.proxyKerberosServicePrincipal': {
285 > type: 'string',
286 > markdownDescription: localize('proxyKerberosServicePrincipal', "Overrides the principal service name for Kerberos authentication with the HTTP proxy. A default based on the proxy hostname is used when this is not set. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'),
287 > restricted: true
288 > },
289 > 'http.noProxy': {
290 > type: 'array',
291 > items: { type: 'string' },
292 > markdownDescription: localize('noProxy', "Specifies domain names for which proxy settings should be ignored for HTTP/HTTPS requests. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'),
293 > restricted: true
294 > },
295 > 'http.proxyAuthorization': {
296 > type: ['null', 'string'],
297 > default: null,
298 > markdownDescription: localize('proxyAuthorization', "The value to send as the `Proxy-Authorization` header for every network request. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'),
299 > restricted: true
300 > },
301 > 'http.proxySupport': {
302 > type: 'string',
303 > enum: ['off', 'on', 'fallback', 'override'],
304 > enumDescriptions: [
305 > localize('proxySupportOff', "Disable proxy support for extensions."),
306 > localize('proxySupportOn', "Enable proxy support for extensions."),
307 > localize('proxySupportFallback', "Enable proxy support for extensions, fall back to request options, when no proxy found."),
308 > localize('proxySupportOverride', "Enable proxy support for extensions, override request options."),
309 > ],
310 > default: 'override',
311 > markdownDescription: localize('proxySupport', "Use the proxy support for extensions. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'),
312 > restricted: true
313 > },
314 > 'http.systemCertificates': {
315 > type: 'boolean',
316 > default: true,
317 > markdownDescription: localize('systemCertificates', "Controls whether CA certificates should be loaded from the OS. On Windows and macOS, a reload of the window is required after turning this off. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'),
318 > restricted: true
319 > },
320 > 'http.systemCertificatesNode': {
321 > type: 'boolean',
322 > tags: ['experimental'],
323 > default: systemCertificatesNodeDefault,
324 > markdownDescription: localize('systemCertificatesNode', "Controls whether system certificates should be loaded using Node.js built-in support. Reload the window after changing this setting. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'),
325 > restricted: true,
326 > experiment: {
327 > mode: 'auto'
328 > }
329 > },
330 > 'http.experimental.systemCertificatesV2': {
331 > type: 'boolean',
332 > tags: ['experimental'],
333 > default: false,
334 > markdownDescription: localize('systemCertificatesV2', "Controls whether experimental loading of CA certificates from the OS should be enabled. This uses a more general approach than the default implementation. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'),
335 > restricted: true
336 > },
337 > 'http.fetchAdditionalSupport': {
338 > type: 'boolean',
339 > default: true,
340 > markdownDescription: localize('fetchAdditionalSupport', "Controls whether Node.js' fetch implementation should be extended with additional support. Currently proxy support ({1}) and system certificates ({2}) are added when the corresponding settings are enabled. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`', '`#http.proxySupport#`', '`#http.systemCertificates#`'),
341 > restricted: true
342 > },
343 > 'http.webSocketAdditionalSupport': {
344 > type: 'boolean',
345 > default: true,
346 > markdownDescription: localize('webSocketAdditionalSupport', "Controls whether the built-in WebSocket implementation should be extended with additional support. Currently proxy support ({1}) and system certificates ({2}) are added when the corresponding settings are enabled. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`', '`#http.proxySupport#`', '`#http.systemCertificates#`'),
347 > restricted: true
348 > },
349 > 'http.experimental.networkInterfaceCheckInterval': {
350 > type: 'number',
351 > default: 300,
352 > minimum: -1,
353 > tags: ['experimental'],
354 > markdownDescription: localize('networkInterfaceCheckInterval', "Controls the interval in seconds for checking network interface changes to invalidate the proxy cache. Set to -1 to disable. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'),
355 > restricted: true,
356 > experiment: {
357 > mode: 'auto'
358 > }
359 > }
360 > }
361 > }
362 > ];
363 > configurationRegistry.updateConfigurations({ add: proxyConfiguration, remove: oldProxyConfiguration });
364 > }
365 >
366 > registerProxyConfigurations();
src/vs/platform/agentHost/node/shared/agentFeedbackServerTools.ts 273 covered LOC · 23 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentFeedbackServerTools.ts
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 > import { generateUuid } from '../../../../base/common/uuid.js';
7 > import { localize } from '../../../../nls.js';
8 > import { FEEDBACK_ANNOTATION_META_KEY, readFeedbackAnnotationMeta, VIEW_UNREVIEWED_COMMENTS_TOOL_NAME, ADD_COMMENT_TOOL_NAME, type IFeedbackAnnotationMeta } from '../../common/meta/agentFeedbackAnnotations.js';
9 > import { buildAnnotationsUri } from '../../common/annotationsUri.js';
10 > import type { AnnotationsAction } from '../../common/state/sessionActions.js';
11 > import { ActionType } from '../../common/state/protocol/common/actions.js';
12 > import { parseChatUri, type Annotation, type AnnotationsState, type StringOrMarkdown, type TextRange, type ToolDefinition } from '../../common/state/sessionState.js';
13 > import type { IServerToolDisplay, IServerToolDisplayResult, IServerToolGroup } from './agentServerToolHost.js';
14 >
15 > /**
16 > * Server-side implementation of the agent feedback ("comments") tools.
17 > *
18 > * These tools used to be registered on the client (agents window) and keyed
19 > * off an in-memory store. For agent-host sessions they now execute on the
20 > * server against the session's annotations channel: each comment is an
21 > * {@link Annotation} on `<session>/annotations`, with feedback semantics
22 > * carried in {@link Annotation._meta} under {@link FEEDBACK_ANNOTATION_META_KEY}
23 > * (see `agentFeedbackAnnotations.ts`). The functions here are pure — they read
24 > * the current {@link AnnotationsState} and return the annotation actions to
25 > * dispatch plus a textual tool result — so they can be unit tested without a
26 > * running state manager. The host wiring (reading the snapshot, dispatching
27 > * the actions) lives in the caller.
28 > */
29 >
30 > export const addCommentToolName = ADD_COMMENT_TOOL_NAME;
31 > export const listCommentsToolName = 'listComments';
32 > export const deleteCommentsToolName = 'deleteComments';
33 > export const resolveCommentsToolName = 'resolveComments';
34 > export const viewUnreviewedCommentsToolName = VIEW_UNREVIEWED_COMMENTS_TOOL_NAME;
35 >
36 > /**
37 > * Feedback kinds that originate from a review the user is expected to triage
38 > * (a pull request review or an in-product code review) rather than being
39 > * authored by the user directly. Comments of these kinds that are still in the
40 > * `created` state are surfaced to the agent via the {@link listCommentsToolName}
41 > * note and revealed through {@link viewUnreviewedCommentsToolName}.
42 > */
43 > const REVIEWABLE_FEEDBACK_KINDS: ReadonlySet<string> = new Set(['prReview', 'codeReview']);
44 >
45 > /**
46 > * Server tools that must not be auto-approved: invoking them surfaces a
47 > * confirmation to the user (rendered by a custom client content part) before
48 > * the tool body runs. Providers consult {@link feedbackToolRequiresConfirmation}
49 > * (via the host) to exclude these from their server-tool auto-approve lists.
50 > */
51 > const feedbackConfirmationToolNames: ReadonlySet<string> = new Set([viewUnreviewedCommentsToolName]);
52 >
53 > /** Whether the given feedback server tool requires user confirmation before it runs. */
54 > export function feedbackToolRequiresConfirmation(toolName: string): boolean {
55 return feedbackConfirmationToolNames.has(toolName);
56 }
58 > const addCommentInputSchema: ToolDefinition['inputSchema'] = {
59 > type: 'object',
60 > properties: {
61 > resourceUri: { type: 'string', description: 'URI of the file to add a comment to.' },
62 > range: {
63 > type: 'object',
64 > description: 'One-based text range to comment on.',
65 > properties: {
66 > startLineNumber: { type: 'number', description: 'One-based start line number.' },
67 > startColumn: { type: 'number', description: 'One-based start column.' },
68 > endLineNumber: { type: 'number', description: 'One-based end line number.' },
69 > endColumn: { type: 'number', description: 'One-based end column.' },
70 > },
71 > required: ['startLineNumber', 'startColumn', 'endLineNumber', 'endColumn'],
72 > },
73 > text: { type: 'string', description: 'Comment text to add.' },
74 > },
75 > required: ['resourceUri', 'range', 'text'],
76 > };
77 >
78 > const listCommentsInputSchema: ToolDefinition['inputSchema'] = {
79 > type: 'object',
80 > properties: {},
81 > };
82 >
83 > const viewUnreviewedCommentsInputSchema: ToolDefinition['inputSchema'] = {
84 > type: 'object',
85 > properties: {},
86 > };
87 >
88 > const deleteCommentsInputSchema: ToolDefinition['inputSchema'] = {
89 > type: 'object',
90 > properties: {
91 > commentIds: { type: 'array', items: { type: 'string' }, description: 'Comment IDs to delete.' },
92 > },
93 > required: ['commentIds'],
94 > };
95 >
96 > const resolveCommentsInputSchema: ToolDefinition['inputSchema'] = {
97 > type: 'object',
98 > properties: {
99 > commentIds: { type: 'array', items: { type: 'string' }, description: 'Comment IDs to update.' },
100 > resolved: { type: 'boolean', description: 'Whether the comments should be marked as resolved. Defaults to true.' },
101 > },
102 > required: ['commentIds'],
103 > };
104 >
105 > /**
106 > * Protocol {@link ToolDefinition}s for the feedback server tools, advertised on
107 > * {@link SessionState.serverTools} so clients know these tools are owned and
108 > * executed by the agent host.
109 > */
110 > export const feedbackServerToolDefinitions: ToolDefinition[] = [
111 > {
112 > name: addCommentToolName,
113 > title: 'Add Comment (Agent Feedback)',
114 > description: 'Add a comment to a file range.',
115 > inputSchema: addCommentInputSchema,
116 > annotations: { readOnlyHint: false },
117 > },
118 > {
119 > name: listCommentsToolName,
120 > title: 'List Comments (Agent Feedback)',
121 > description: 'List comments for this session.',
122 > inputSchema: listCommentsInputSchema,
123 > annotations: { readOnlyHint: true },
124 > },
125 > {
126 > name: deleteCommentsToolName,
127 > title: 'Delete Comments (Agent Feedback)',
128 > description: 'Delete comments for this session.',
129 > inputSchema: deleteCommentsInputSchema,
130 > annotations: { readOnlyHint: false, destructiveHint: true },
131 > },
132 > {
133 > name: resolveCommentsToolName,
134 > title: 'Resolve Comments (Agent Feedback)',
135 > description: 'Mark comments for this session as resolved or unresolved.',
136 > inputSchema: resolveCommentsInputSchema,
137 > annotations: { readOnlyHint: false },
138 > },
139 > {
140 > name: viewUnreviewedCommentsToolName,
141 > title: 'View Unreviewed Comments (Agent Feedback)',
142 > description: 'View pull request or code review comments that the user has not reviewed yet. Calling this asks the user to choose which of those comments to reveal; only the comments the user reveals are returned.',
143 > inputSchema: viewUnreviewedCommentsInputSchema,
144 > annotations: { readOnlyHint: true },
145 > },
146 > ];
147 >
148 > // --- Argument validation ------------------------------------------------------
149 >
150 > interface IOneBasedRange {
151 > readonly startLineNumber: number;
152 > readonly startColumn: number;
153 > readonly endLineNumber: number;
154 > readonly endColumn: number;
155 > }
156 >
157 > interface IAddCommentArgs {
158 > readonly resourceUri?: unknown;
159 > readonly range?: unknown;
160 > readonly text?: unknown;
161 > }
162 >
163 > interface IDeleteCommentsArgs {
164 > readonly commentIds?: unknown;
165 > }
166 >
167 > interface IResolveCommentsArgs {
168 > readonly commentIds?: unknown;
169 > readonly resolved?: unknown;
170 > }
171 >
172 function getRequiredString(value: unknown, field: string, toolName: string): string {
173 if (typeof value !== 'string' || value.length === 0) {
176 return value;
177 }
179 function getRequiredPositiveInteger(value: unknown, field: string, toolName: string): number {
180 if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {
183 return value;
184 }
186 function getAddCommentArgs(rawArgs: unknown): { resourceUri: string; range: IOneBasedRange; text: string } {
187 const args = (rawArgs ?? {}) as IAddCommentArgs;
203 };
204 }
206 function getUniqueCommentIds(value: unknown, toolName: string): readonly string[] {
207 if (!Array.isArray(value) || value.length === 0) {
214 return [...new Set(ids)];
215 }
217 function getResolvedFlag(value: unknown): boolean {
218 if (value === undefined) {
224 return value;
225 }
227 > // --- Annotation <-> feedback conversion ---------------------------------------
228 >
229 function toTextRange(range: IOneBasedRange): TextRange {
230 return {
233 };
234 }
236 function fromTextRange(range: TextRange | undefined): IOneBasedRange {
237 if (!range) {
245 };
246 }
248 function entryText(text: StringOrMarkdown): string {
249 return typeof text === 'string' ? text : text.markdown;
250 }
252 function readMeta(annotation: Annotation): IFeedbackAnnotationMeta | undefined {
253 return readFeedbackAnnotationMeta(annotation);
254 }
256 > interface ISerializedComment {
257 > readonly id: string;
258 > readonly resourceUri: string;
259 > readonly range: IOneBasedRange;
260 > readonly text: string;
261 > readonly kind: string;
262 > readonly resolved: boolean;
263 > readonly replies?: readonly string[];
264 > }
265 >
266 function serializeComment(annotation: Annotation): ISerializedComment {
267 const entries = annotation.entries ?? [];
278 };
279 }
281 > /**
282 > * Comments visible to the agent: everything except items still in the
283 > * `created` state (the agent added them but the user has not accepted them
284 > * yet). Mirrors the client `getListableFeedback` behavior.
285 > */
286 function listableAnnotations(state: AnnotationsState): Annotation[] {
287 return state.annotations.filter(annotation => {
298 });
299 }
301 > /**
302 > * Feedback annotations of a {@link REVIEWABLE_FEEDBACK_KINDS reviewable kind}
303 > * the user has flagged for reveal to the agent (via the confirmation of the
304 > * {@link viewUnreviewedCommentsToolName} tool). These are exactly the comments
305 > * the user chose to reveal for the current invocation; everything else
306 > * (including review comments that happen to be accepted from a previous reveal
307 > * or a manual accept) is excluded.
308 > */
309 function pendingRevealAnnotations(state: AnnotationsState): Annotation[] {
310 return state.annotations.filter(annotation => {
316 });
317 }
319 > /** Returns a copy of {@link annotation} with the {@link IFeedbackAnnotationMeta.pendingAgentReveal} flag cleared. */
320 function clearPendingReveal(annotation: Annotation): Annotation {
321 const meta = readMeta(annotation);
326 return { ...annotation, _meta: { ...annotation._meta, [FEEDBACK_ANNOTATION_META_KEY]: nextMeta } };
327 }
329 > /**
330 > * Reviewable (PR / code review) feedback annotations the user has not reviewed
331 > * yet, i.e. still in the `created` state. Used to build the
332 > * {@link listCommentsToolName} note.
333 > */
334 function createdReviewableAnnotations(state: AnnotationsState): Annotation[] {
335 return state.annotations.filter(annotation => {
341 });
342 }
344 > /**
345 > * A short note appended to the {@link listCommentsToolName} result when there
346 > * are reviewable comments the user has not accepted yet, pointing the agent at
347 > * {@link viewUnreviewedCommentsToolName}. Returns `undefined` (no note) when
348 > * there are no such comments.
349 > */
350 function buildUnreviewedCommentsNote(state: AnnotationsState): string | undefined {
351 const created = createdReviewableAnnotations(state);
374 return `There ${verb} ${subject} which the user has not reviewed yet. If the user wants you to tackle them, call the \`${viewUnreviewedCommentsToolName}\` tool to view them.`;
375 }
377 > // --- Tool execution -----------------------------------------------------------
378 >
379 > export interface IFeedbackToolOutcome {
380 > /** Annotation actions to dispatch on the session's annotations channel. */
381 > readonly actions: readonly AnnotationsAction[];
382 > /** Textual tool result returned to the agent. */
383 > readonly result: string;
384 > }
385 >
386 > /**
387 > * Executes a feedback server tool against the current annotation state.
388 > *
389 > * Pure: it does not mutate {@link state}, instead returning the annotation
390 > * actions the caller should dispatch (so the authoritative state manager
391 > * remains the single writer) along with the textual tool result.
392 > *
393 > * @throws if {@link toolName} is unknown or the arguments are invalid.
394 > */
395 > export function applyFeedbackTool(state: AnnotationsState, sessionResource: string, toolName: string, rawArgs: unknown): IFeedbackToolOutcome {
396 switch (toolName) {
397 case addCommentToolName: {
502 }
503 }
505 > /**
506 > * Parses the number of comments returned by the {@link listCommentsToolName}
507 > * tool from its JSON result (`{ comments: [...] }`). Returns `undefined` when
508 > * the result is missing or not in the expected shape, so the caller can fall
509 > * back to a count-less message.
510 > */
511 function parseListedCommentCount(resultText: string | undefined): number | undefined {
512 if (!resultText) {
520 }
521 }
523 > /**
524 > * Display strings for the feedback ("comments") tools, authored here so every
525 > * provider (Copilot, Claude, Codex, …) renders them identically instead of
526 > * each provider's display layer re-deriving the strings from the tool name.
527 > * Returns `undefined` for tools this group does not own, so the caller falls
528 > * back to its generic display.
529 > *
530 > * {@link toolName} is the bare tool name (any transport prefix such as Claude's
531 > * `mcp__<server>__` has already been stripped by the dispatcher).
532 > */
533 function getFeedbackToolDisplay(toolName: string, _args: unknown, result?: IServerToolDisplayResult): IServerToolDisplay | undefined {
534 switch (toolName) {
577 }
578 }
580 > /**
581 > * The feedback ("comments") server-tool group, contributed to the
582 > * {@link AgentServerToolHost} at startup (see `node/agentService.ts`). Wraps
583 > * the pure {@link applyFeedbackTool} executor with the annotations-channel I/O:
584 > * it reads the session's current {@link AnnotationsState}, applies the tool,
585 > * and dispatches the resulting annotation actions through the state manager
586 > * (the single writer).
587 > */
588 > export const feedbackServerToolGroup: IServerToolGroup = {
589 > definitions: feedbackServerToolDefinitions,
590 > requiresConfirmation(toolName): boolean {
591 return feedbackToolRequiresConfirmation(toolName);
592 },
593 > getDisplay(toolName, args, result): IServerToolDisplay | undefined { agentFeedbackServerTools.ts
594 return getFeedbackToolDisplay(toolName, args, result);
595 },
596 > execute(stateManager, chatUri, toolName, rawArgs): string { agentFeedbackServerTools.ts
597 // A session can contain multiple chats, each addressed by its own
598 // `ahp-chat` URI but sharing the same context/workspace. Comments belong
src/vs/platform/configuration/common/configurationModels.ts 271 covered LOC · 92 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- configurationModels.ts
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 > import * as arrays from '../../../base/common/arrays.js';
7 > import { IStringDictionary } from '../../../base/common/collections.js';
8 > import { Emitter, Event } from '../../../base/common/event.js';
9 > import * as json from '../../../base/common/json.js';
10 > import { Disposable } from '../../../base/common/lifecycle.js';
11 > import { getOrSet, ResourceMap } from '../../../base/common/map.js';
12 > import * as objects from '../../../base/common/objects.js';
13 > import { IExtUri } from '../../../base/common/resources.js';
14 > import * as types from '../../../base/common/types.js';
15 > import { URI, UriComponents } from '../../../base/common/uri.js';
16 > import { addToValueTree, ConfigurationTarget, getConfigurationValue, IConfigurationChange, IConfigurationChangeEvent, IConfigurationCompareResult, IConfigurationData, IConfigurationModel, IConfigurationOverrides, IConfigurationUpdateOverrides, IConfigurationValue, IInspectValue, IOverrides, removeFromValueTree, toValuesTree } from './configuration.js';
17 > import { ConfigurationScope, Extensions, IConfigurationPropertySchema, IConfigurationRegistry, overrideIdentifiersFromKey, OVERRIDE_PROPERTY_REGEX, IRegisteredConfigurationPropertySchema } from './configurationRegistry.js';
18 > import { FileOperation, IFileService } from '../../files/common/files.js';
19 > import { ILogService } from '../../log/common/log.js';
20 > import { Registry } from '../../registry/common/platform.js';
21 > import { Workspace } from '../../workspace/common/workspace.js';
22 >
23 function freeze<T>(data: T): T {
24 return Object.isFrozen(data) ? data : objects.deepFreeze(data);
25 }
27 > type InspectValue<V> = IInspectValue<V> & { merged?: V };
28 >
29 > export class ConfigurationModel implements IConfigurationModel {
30 >
31 > static createEmptyModel(logService: ILogService): ConfigurationModel {
32 > return new ConfigurationModel({}, [], [], undefined, logService);
33 > }
34 >
35 > private readonly overrideConfigurations = new Map<string, ConfigurationModel>();
36 >
37 > constructor(
38 private readonly _contents: IStringDictionary<unknown>,
39 private readonly _keys: string[],
43 ) {
44 }
46 > private _rawConfiguration: ConfigurationModel | undefined;
47 > get rawConfiguration(): ConfigurationModel {
48 if (!this._rawConfiguration) {
49 if (this._raw) {
64 return this._rawConfiguration;
65 }
67 > get contents(): IStringDictionary<unknown> {
68 return this._contents;
69 }
71 > get overrides(): IOverrides[] {
72 return this._overrides;
73 }
75 > get keys(): string[] {
76 return this._keys;
77 }
79 > get raw(): IStringDictionary<unknown> | IStringDictionary<unknown>[] | undefined {
80 if (!this._raw) {
81 return undefined;
86 return this._raw as IStringDictionary<unknown> | IStringDictionary<unknown>[];
87 }
89 > isEmpty(): boolean {
90 return this._keys.length === 0 && Object.keys(this._contents).length === 0 && this._overrides.length === 0;
91 }
93 > getValue<V>(section: string | undefined): V | undefined {
94 return section ? getConfigurationValue<V>(this.contents, section) : this.contents as V;
95 }
97 > inspect<V>(section: string | undefined, overrideIdentifier?: string | null): InspectValue<V> {
98 const that = this;
99 return {
119 };
120 }
122 > getOverrideValue<V>(section: string | undefined, overrideIdentifier: string): V | undefined {
123 const overrideContents = this.getContentsForOverrideIdentifer(overrideIdentifier);
124 return overrideContents
126 : undefined;
127 }
129 > getKeysForOverrideIdentifier(identifier: string): string[] {
130 const keys: string[] = [];
131 for (const override of this.overrides) {
136 return arrays.distinct(keys);
137 }
139 > getAllOverrideIdentifiers(): string[] {
140 const result: string[] = [];
141 for (const override of this.overrides) {
144 return arrays.distinct(result);
145 }
147 > override(identifier: string): ConfigurationModel {
148 let overrideConfigurationModel = this.overrideConfigurations.get(identifier);
149 if (!overrideConfigurationModel) {
153 return overrideConfigurationModel;
154 }
156 > merge(...others: ConfigurationModel[]): ConfigurationModel {
157 const contents = objects.deepClone(this.contents);
158 const overrides = objects.deepClone(this.overrides);
185 return new ConfigurationModel(contents, keys, overrides, !raws.length || raws.every(raw => raw instanceof ConfigurationModel) ? undefined : raws, this.logService);
186 }
188 > private createOverrideConfigurationModel(identifier: string): ConfigurationModel {
189 const overrideContents = this.getContentsForOverrideIdentifer(identifier);
190
216 return new ConfigurationModel(contents, this.keys, this.overrides, undefined, this.logService);
217 }
219 > private mergeContents(source: IStringDictionary<unknown>, target: IStringDictionary<unknown>): void {
220 for (const key of Object.keys(target)) {
221 if (key in source) {
228 }
229 }
231 > private getContentsForOverrideIdentifer(identifier: string): IStringDictionary<unknown> | null {
232 let contentsForIdentifierOnly: IStringDictionary<unknown> | null = null;
233 let contents: IStringDictionary<unknown> | null = null;
252 return contents;
253 }
255 > toJSON(): IConfigurationModel {
256 return {
257 contents: this.contents,
260 };
261 }
263 > // Update methods
264 >
265 > public addValue(key: string, value: unknown): void {
266 this.updateValue(key, value, true);
267 }
269 > public setValue(key: string, value: unknown): void {
270 this.updateValue(key, value, false);
271 }
273 > public removeValue(key: string): void {
274 const index = this.keys.indexOf(key);
275 if (index === -1) {
282 }
283 }
285 > private updateValue(key: string, value: unknown, add: boolean): void {
286 addToValueTree(this.contents, key, value, e => this.logService.error(e));
287 add = add || this.keys.indexOf(key) === -1;
305 }
306 }
308 >
309 > export interface ConfigurationParseOptions {
310 > skipUnregistered?: boolean;
311 > scopes?: ConfigurationScope[];
312 > skipRestricted?: boolean;
313 > include?: string[];
314 > exclude?: string[];
315 > }
316 >
317 > export class ConfigurationModelParser {
318 >
319 > private _raw: IStringDictionary<unknown> | null = null;
320 > private _configurationModel: ConfigurationModel | null = null;
321 > private _restrictedConfigurations: string[] = [];
322 > private _parseErrors: json.ParseError[] = [];
323 >
324 > constructor(
325 protected readonly _name: string,
326 protected readonly logService: ILogService
327 ) { }
329 > get configurationModel(): ConfigurationModel {
330 return this._configurationModel || ConfigurationModel.createEmptyModel(this.logService);
331 }
333 > get restrictedConfigurations(): string[] {
334 return this._restrictedConfigurations;
335 }
337 > get errors(): json.ParseError[] {
338 return this._parseErrors;
339 }
341 > public parse(content: string | null | undefined, options?: ConfigurationParseOptions): void {
342 if (!types.isUndefinedOrNull(content)) {
343 const raw = this.doParseContent(content);
345 }
346 }
348 > public reparse(options: ConfigurationParseOptions): void {
349 if (this._raw) {
350 this.parseRaw(this._raw, options);
351 }
352 }
354 > public parseRaw(raw: IStringDictionary<unknown>, options?: ConfigurationParseOptions): void {
355 this._raw = raw;
356 const { contents, keys, overrides, restricted, hasExcludedProperties } = this.doParseRaw(raw, options);
358 this._restrictedConfigurations = restricted || [];
359 }
361 > private doParseContent(content: string): IStringDictionary<unknown> {
362 let raw: IStringDictionary<unknown> = {};
363 let currentProperty: string | null = null;
415 return raw;
416 }
418 > protected doParseRaw(raw: IStringDictionary<unknown>, options?: ConfigurationParseOptions): IConfigurationModel & { restricted?: string[]; hasExcludedProperties?: boolean } {
419 const registry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
420 const configurationProperties = registry.getConfigurationProperties();
427 return { contents, keys, overrides, restricted: filtered.restricted, hasExcludedProperties: filtered.hasExcludedProperties };
428 }
430 > private filter(properties: IStringDictionary<unknown>, configurationProperties: IStringDictionary<IRegisteredConfigurationPropertySchema>, excludedConfigurationProperties: IStringDictionary<IRegisteredConfigurationPropertySchema>, filterOverriddenProperties: boolean, options?: ConfigurationParseOptions): { raw: IStringDictionary<unknown>; restricted: string[]; hasExcludedProperties: boolean } {
431 let hasExcludedProperties = false;
432 if (!options?.scopes && !options?.skipRestricted && !options?.skipUnregistered && !options?.exclude?.length) {
455 return { raw, restricted, hasExcludedProperties };
456 }
458 > private shouldInclude(key: string, propertySchema: IConfigurationPropertySchema | undefined, excludedConfigurationProperties: IStringDictionary<IRegisteredConfigurationPropertySchema>, options: ConfigurationParseOptions): boolean {
459 if (options.exclude?.includes(key)) {
460 return false;
481 return options.scopes.includes(scope);
482 }
484 > private toOverrides(raw: IStringDictionary<unknown>, conflictReporter: (message: string) => void): IOverrides[] {
485 const overrides: IOverrides[] = [];
486 for (const key of Object.keys(raw)) {
500 return overrides;
501 }
503 > }
504 >
505 > export class UserSettings extends Disposable {
506 >
507 > private readonly parser: ConfigurationModelParser;
508 > protected readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>());
509 > readonly onDidChange: Event<void> = this._onDidChange.event;
510 >
511 > constructor(
512 private readonly userSettingsResource: URI,
513 protected parseOptions: ConfigurationParseOptions,
526 )(() => this._onDidChange.fire()));
527 }
529 > async loadConfiguration(): Promise<ConfigurationModel> {
530 try {
531 const content = await this.fileService.readFile(this.userSettingsResource);
536 }
537 }
539 > reparse(parseOptions?: ConfigurationParseOptions): ConfigurationModel {
540 if (parseOptions) {
541 this.parseOptions = parseOptions;
544 return this.parser.configurationModel;
545 }
547 > getRestrictedSettings(): string[] {
548 return this.parser.restrictedConfigurations;
549 }
551 >
552 > class ConfigurationInspectValue<V> implements IConfigurationValue<V> {
553 >
554 > constructor(
555 private readonly key: string,
556 private readonly overrides: IConfigurationOverrides,
568 ) {
569 }
571 > get value(): V | undefined {
572 return freeze(this._value);
573 }
575 > private toInspectValue(inspectValue: IInspectValue<V> | undefined | null): IInspectValue<V> | undefined {
576 return inspectValue?.value !== undefined || inspectValue?.override !== undefined || inspectValue?.overrides !== undefined ? inspectValue : undefined;
577 }
579 > private _defaultInspectValue: InspectValue<V> | undefined;
580 > private get defaultInspectValue(): InspectValue<V> {
581 if (!this._defaultInspectValue) {
582 this._defaultInspectValue = this.defaultConfiguration.inspect<V>(this.key, this.overrides.overrideIdentifier);
584 return this._defaultInspectValue;
585 }
587 > get defaultValue(): V | undefined {
588 return this.defaultInspectValue.merged;
589 }
591 > get default(): IInspectValue<V> | undefined {
592 return this.toInspectValue(this.defaultInspectValue);
593 }
595 > private _policyInspectValue: InspectValue<V> | undefined | null;
596 > private get policyInspectValue(): InspectValue<V> | null {
597 if (this._policyInspectValue === undefined) {
598 this._policyInspectValue = this.policyConfiguration ? this.policyConfiguration.inspect<V>(this.key) : null;
600 return this._policyInspectValue;
601 }
603 > get policyValue(): V | undefined {
604 return this.policyInspectValue?.merged;
605 }
607 > get policy(): IInspectValue<V> | undefined {
608 return this.policyInspectValue?.value !== undefined ? { value: this.policyInspectValue.value } : undefined;
609 }
611 > private _applicationInspectValue: InspectValue<V> | undefined | null;
612 > private get applicationInspectValue(): InspectValue<V> | null {
613 if (this._applicationInspectValue === undefined) {
614 this._applicationInspectValue = this.applicationConfiguration ? this.applicationConfiguration.inspect<V>(this.key) : null;
616 return this._applicationInspectValue;
617 }
619 > get applicationValue(): V | undefined {
620 return this.applicationInspectValue?.merged;
621 }
623 > get application(): IInspectValue<V> | undefined {
624 return this.toInspectValue(this.applicationInspectValue);
625 }
627 > private _userInspectValue: InspectValue<V> | undefined;
628 > private get userInspectValue(): InspectValue<V> {
629 if (!this._userInspectValue) {
630 this._userInspectValue = this.userConfiguration.inspect<V>(this.key, this.overrides.overrideIdentifier);
632 return this._userInspectValue;
633 }
635 > get userValue(): V | undefined {
636 return this.userInspectValue.merged;
637 }
639 > get user(): IInspectValue<V> | undefined {
640 return this.toInspectValue(this.userInspectValue);
641 }
643 > private _userLocalInspectValue: InspectValue<V> | undefined;
644 > private get userLocalInspectValue(): InspectValue<V> {
645 if (!this._userLocalInspectValue) {
646 this._userLocalInspectValue = this.localUserConfiguration.inspect<V>(this.key, this.overrides.overrideIdentifier);
648 return this._userLocalInspectValue;
649 }
651 > get userLocalValue(): V | undefined {
652 return this.userLocalInspectValue.merged;
653 }
655 > get userLocal(): IInspectValue<V> | undefined {
656 return this.toInspectValue(this.userLocalInspectValue);
657 }
659 > private _userRemoteInspectValue: InspectValue<V> | undefined;
660 > private get userRemoteInspectValue(): InspectValue<V> {
661 if (!this._userRemoteInspectValue) {
662 this._userRemoteInspectValue = this.remoteUserConfiguration.inspect<V>(this.key, this.overrides.overrideIdentifier);
664 return this._userRemoteInspectValue;
665 }
667 > get userRemoteValue(): V | undefined {
668 return this.userRemoteInspectValue.merged;
669 }
671 > get userRemote(): IInspectValue<V> | undefined {
672 return this.toInspectValue(this.userRemoteInspectValue);
673 }
675 > private _workspaceInspectValue: InspectValue<V> | undefined | null;
676 > private get workspaceInspectValue(): InspectValue<V> | null {
677 if (this._workspaceInspectValue === undefined) {
678 this._workspaceInspectValue = this.workspaceConfiguration ? this.workspaceConfiguration.inspect<V>(this.key, this.overrides.overrideIdentifier) : null;
680 return this._workspaceInspectValue;
681 }
683 > get workspaceValue(): V | undefined {
684 return this.workspaceInspectValue?.merged;
685 }
687 > get workspace(): IInspectValue<V> | undefined {
688 return this.toInspectValue(this.workspaceInspectValue);
689 }
691 > private _workspaceFolderInspectValue: InspectValue<V> | undefined | null;
692 > private get workspaceFolderInspectValue(): InspectValue<V> | null {
693 if (this._workspaceFolderInspectValue === undefined) {
694 this._workspaceFolderInspectValue = this.folderConfigurationModel ? this.folderConfigurationModel.inspect<V>(this.key, this.overrides.overrideIdentifier) : null;
696 return this._workspaceFolderInspectValue;
697 }
699 > get workspaceFolderValue(): V | undefined {
700 return this.workspaceFolderInspectValue?.merged;
701 }
703 > get workspaceFolder(): IInspectValue<V> | undefined {
704 return this.toInspectValue(this.workspaceFolderInspectValue);
705 }
707 > private _memoryInspectValue: InspectValue<V> | undefined;
708 > private get memoryInspectValue(): InspectValue<V> {
709 if (this._memoryInspectValue === undefined) {
710 this._memoryInspectValue = this.memoryConfigurationModel.inspect<V>(this.key, this.overrides.overrideIdentifier);
712 return this._memoryInspectValue;
713 }
715 > get memoryValue(): V | undefined {
716 return this.memoryInspectValue.merged;
717 }
719 > get memory(): IInspectValue<V> | undefined {
720 return this.toInspectValue(this.memoryInspectValue);
721 }
723 > }
724 >
725 > export class Configuration {
726 >
727 > private _workspaceConsolidatedConfiguration: ConfigurationModel | null = null;
728 > private _foldersConsolidatedConfigurations = new ResourceMap<ConfigurationModel>();
729 >
730 > constructor(
731 private _defaultConfiguration: ConfigurationModel,
732 private _policyConfiguration: ConfigurationModel,
960
961 private _userConfiguration: ConfigurationModel | null = null;
962 > get userConfiguration(): ConfigurationModel { configurationModels.ts
963 if (!this._userConfiguration) {
964 if (this._remoteUserConfiguration.isEmpty()) {
971 return this._userConfiguration;
972 }
974 > get localUserConfiguration(): ConfigurationModel {
975 return this._localUserConfiguration;
976 }
978 > get remoteUserConfiguration(): ConfigurationModel {
979 return this._remoteUserConfiguration;
980 }
982 > get workspaceConfiguration(): ConfigurationModel {
983 return this._workspaceConfiguration;
984 }
986 > get folderConfigurations(): ResourceMap<ConfigurationModel> {
987 return this._folderConfigurations;
988 }
990 > private getConsolidatedConfigurationModel(section: string | undefined, overrides: IConfigurationOverrides, workspace: Workspace | undefined): ConfigurationModel {
991 let configurationModel = this.getConsolidatedConfigurationModelForResource(overrides, workspace);
992 if (overrides.overrideIdentifier) {
1002 return configurationModel;
1003 }
1005 > private getConsolidatedConfigurationModelForResource({ resource }: IConfigurationOverrides, workspace: Workspace | undefined): ConfigurationModel {
1006 let consolidateConfiguration = this.getWorkspaceConsolidatedConfiguration();
1007
1019 return consolidateConfiguration;
1020 }
1022 > private getWorkspaceConsolidatedConfiguration(): ConfigurationModel {
1023 if (!this._workspaceConsolidatedConfiguration) {
1024 this._workspaceConsolidatedConfiguration = this._defaultConfiguration.merge(this.applicationConfiguration, this.userConfiguration, this._workspaceConfiguration, this._memoryConfiguration);
1026 return this._workspaceConsolidatedConfiguration;
1027 }
1029 > private getFolderConsolidatedConfiguration(folder: URI): ConfigurationModel {
1030 let folderConsolidatedConfiguration = this._foldersConsolidatedConfigurations.get(folder);
1031 if (!folderConsolidatedConfiguration) {
1041 return folderConsolidatedConfiguration;
1042 }
1044 > private getFolderConfigurationModelForResource(resource: URI | null | undefined, workspace: Workspace | undefined): ConfigurationModel | undefined {
1045 if (workspace && resource) {
1046 const root = workspace.getFolder(resource);
1051 return undefined;
1052 }
1054 > toData(): IConfigurationData {
1055 return {
1056 defaults: {
1094 };
1095 }
1097 > allKeys(): string[] {
1098 const keys: Set<string> = new Set<string>();
1099 this._defaultConfiguration.keys.forEach(key => keys.add(key));
1103 return [...keys.values()];
1104 }
1106 > protected allOverrideIdentifiers(): string[] {
1107 const keys: Set<string> = new Set<string>();
1108 this._defaultConfiguration.getAllOverrideIdentifiers().forEach(key => keys.add(key));
1112 return [...keys.values()];
1113 }
1115 > protected getAllKeysForOverrideIdentifier(overrideIdentifier: string): string[] {
1116 const keys: Set<string> = new Set<string>();
1117 this._defaultConfiguration.getKeysForOverrideIdentifier(overrideIdentifier).forEach(key => keys.add(key));
1121 return [...keys.values()];
1122 }
1124 > static parse(data: IConfigurationData, logService: ILogService): Configuration {
1125 const defaultConfiguration = this.parseConfigurationModel(data.defaults, logService);
1126 const policyConfiguration = this.parseConfigurationModel(data.policy, logService);
1146 );
1147 }
1149 > private static parseConfigurationModel(model: IConfigurationModel, logService: ILogService): ConfigurationModel {
1150 return new ConfigurationModel(model.contents, model.keys, model.overrides, model.raw, logService);
1151 }
1153 > }
1154 >
1155 > export function mergeChanges(...changes: IConfigurationChange[]): IConfigurationChange {
1156 if (changes.length === 0) {
1157 return { keys: [], overrides: [] };
1173 return { keys: [...keysSet.values()], overrides };
1174 }
1176 > export class ConfigurationChangeEvent implements IConfigurationChangeEvent {
1177 >
1178 > private readonly _marker = '\n';
1179 > private readonly _markerCode1 = this._marker.charCodeAt(0);
1180 > private readonly _markerCode2 = '.'.charCodeAt(0);
1181 > private readonly _affectsConfigStr: string;
1182 >
1183 > readonly affectedKeys = new Set<string>();
1184 > source!: ConfigurationTarget;
1185 >
1186 > constructor(
1187 readonly change: IConfigurationChange,
1188 private readonly previous: { workspace?: Workspace; data: IConfigurationData } | undefined,
1206 }
1207 }
1209 > private _previousConfiguration: Configuration | undefined = undefined;
1210 > get previousConfiguration(): Configuration | undefined {
1211 if (!this._previousConfiguration && this.previous) {
1212 this._previousConfiguration = Configuration.parse(this.previous.data, this.logService);
1214 return this._previousConfiguration;
1215 }
1217 > affectsConfiguration(section: string, overrides?: IConfigurationOverrides): boolean {
1218 // we have one large string with all keys that have changed. we pad (marker) the section
1219 // and check that either find it padded or before a segment character
1240 return true;
1241 }
1243 >
1244 function compare(from: ConfigurationModel | undefined, to: ConfigurationModel | undefined): IConfigurationCompareResult {
1245 const { added, removed, updated } = compareConfigurationContents(to?.rawConfiguration, from?.rawConfiguration);
1274 return { added, removed, updated, overrides };
1275 }
1277 function compareConfigurationContents(to: { keys: string[]; contents: IStringDictionary<unknown> } | undefined, from: { keys: string[]; contents: IStringDictionary<unknown> } | undefined) {
1278 const added = to
src/vs/platform/agentHost/node/sessionPermissions.ts 261 covered LOC · 27 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- sessionPermissions.ts
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 > import { realpath as fsRealpath } from 'fs';
7 > import { homedir } from 'os';
8 > import { promisify } from 'util';
9 > import { match as globMatch } from '../../../base/common/glob.js';
10 > import { untildify } from '../../../base/common/labels.js';
11 > import { Disposable } from '../../../base/common/lifecycle.js';
12 > import { Schemas } from '../../../base/common/network.js';
13 > import * as path from '../../../base/common/path.js';
14 > import { isMacintosh, isWindows } from '../../../base/common/platform.js';
15 > import { extUriBiasedIgnorePathCase, normalizePath } from '../../../base/common/resources.js';
16 > import { isDefined } from '../../../base/common/types.js';
17 > import { URI } from '../../../base/common/uri.js';
18 > import { localize } from '../../../nls.js';
19 > import { ILogService } from '../../log/common/log.js';
20 > import { AgentHostGlobalAutoApproveEnabledConfigKey, AgentHostTerminalAutoApproveEnabledConfigKey, AgentHostTerminalAutoApproveRulesConfigKey, platformRootSchema, platformSessionSchema } from '../common/agentHostSchema.js';
21 > import type { IAgentToolPendingConfirmationSignal } from '../common/agentService.js';
22 > import { SessionConfigKey } from '../common/sessionConfigKeys.js';
23 > import { ConfirmationOptionKind, type ConfirmationOption } from '../common/state/protocol/state.js';
24 > import { ActionType, type IToolCallReadyAction } from '../common/state/sessionActions.js';
25 > import {
26 > isAhpChatChannel,
27 > parseRequiredSessionUriFromChatUri,
28 > ResponsePartKind,
29 > ToolCallConfirmationReason,
30 > type URI as ProtocolURI,
31 > } from '../common/state/sessionState.js';
32 > import { IAgentConfigurationService } from './agentConfigurationService.js';
33 > import { AgentHostStateManager } from './agentHostStateManager.js';
34 > import { CommandAutoApprover } from './commandAutoApprover.js';
35 >
36 > /**
37 > * Event fields needed for auto-approval decisions.
38 > * Matches the subset of {@link IAgentToolPendingConfirmationSignal} used by the
39 > * approval pipeline.
40 > */
41 > export interface IToolApprovalEvent {
42 > readonly toolCallId: string;
43 > readonly session: URI;
44 > readonly permissionKind?: IAgentToolPendingConfirmationSignal['permissionKind'];
45 > readonly permissionPath?: string;
46 > readonly toolInput?: string;
47 > readonly requestSandboxBypass?: boolean;
48 > }
49 >
50 > /** Standard per-tool confirmation options presented to the user. */
51 > const ALLOW_SESSION_OPTION_ID = 'allow-session';
52 > const CONFIRMATION_OPTIONS: readonly ConfirmationOption[] = [
53 > { id: ALLOW_SESSION_OPTION_ID, label: localize('sessionPermissions.allowSession', "Allow in this Session"), kind: ConfirmationOptionKind.Approve, group: 1 },
54 > { id: 'allow-once', label: localize('sessionPermissions.allowOnce', "Allow Once"), kind: ConfirmationOptionKind.Approve },
55 > { id: 'skip', label: localize('sessionPermissions.skip', "Skip"), kind: ConfirmationOptionKind.Deny, group: 2 },
56 > ];
57 >
58 > /** Default write-path glob rules applied to auto-approved edits. */
59 > const DEFAULT_EDIT_AUTO_APPROVE_PATTERNS: Readonly<Record<string, boolean>> = {
60 > '**/*': true,
61 > '**/.vscode/*.json': false,
62 > '**/.git/**': false,
63 > '**/{package.json,server.xml,build.rs,web.config,.gitattributes,.env}': false,
64 > '**/*.{code-workspace,csproj,fsproj,vbproj,vcxproj,proj,targets,props}': false,
65 > '**/*.lock': false,
66 > '**/*-lock.{yaml,json}': false,
67 > // Files that can register lifecycle hooks running arbitrary shell commands.
68 > // Writing them must never be auto-approved. Keep in sync with the hook and
69 > // agent source locations in `promptFileLocations.ts`.
70 > '**/.github/agents/**': false,
71 > '**/.github/hooks/**': false,
72 > '**/.claude/agents/**': false,
73 > '**/.claude/settings.json': false,
74 > '**/.claude/settings.local.json': false,
75 > };
76 >
77 > const HOME_DIR = URI.file(homedir());
78 >
79 > /**
80 > * Absolute directory prefixes whose contents are platform configuration data
81 > * (e.g. `~/Library`, `%APPDATA%`). Writes under these require confirmation
82 > * unless the working directory itself lives inside the restricted directory.
83 > */
84 > const PLATFORM_RESTRICTED_DIRS: readonly string[] = (
85 > isWindows
86 ? [process.env.APPDATA, process.env.LOCALAPPDATA]
87 > : isMacintosh sessionPermissions.ts
88 ? [homedir() + '/Library']
90 > ).filter(isDefined);
91 >
92 > const realpath = promisify(fsRealpath);
93 >
94 > /**
95 > * Validates that a path doesn't contain suspicious characters that could be
96 > * used to bypass security checks on Windows (e.g. NTFS Alternate Data Streams,
97 > * invalid characters, reserved device names). Throws if the path is suspicious.
98 > */
99 function assertPathIsSafe(fsPath: string, _isWindows = isWindows): void {
100 if (fsPath.includes('\0')) {
150 }
151 }
153 > /**
154 > * Resolves the real path of `resource`, walking up the parent chain when the path
155 > * (or its ancestors) does not yet exist on disk. This ensures a symlink at any
156 > * ancestor is followed even for files that are about to be created.
157 > */
158 async function resolveRealPathForNonexistent(resource: URI, realpath: (fsPath: string) => Promise<string>): Promise<URI> {
159 const fsPath = resource.fsPath;
187 }
188 }
190 > /**
191 > * Single entry point for all tool-call approval logic in the agent host.
192 > *
193 > * Modeled after {@link ILanguageModelToolsConfirmationService} in the
194 > * workbench layer, this manager owns:
195 > *
196 > * - **Auto-approval** (`getAutoApproval`) — checks session-level config,
197 > * per-tool session permissions, read/write path rules, and shell
198 > * command rules. Returns a {@link ToolCallConfirmationReason} when
199 > * the tool should be auto-approved, or `undefined` when user
200 > * confirmation is needed.
201 > *
202 > * - **Confirmation options** (`createToolReadyAction`) — constructs the
203 > * protocol action with the standard "Allow Once / Allow in this
204 > * Session / Skip" options baked in.
205 > *
206 > * - **Post-confirmation side effects** (`handleToolCallConfirmed`) —
207 > * persists the user's choice (e.g. adding a tool to the session
208 > * permissions list).
209 > */
210 > export class SessionPermissionManager extends Disposable {
211 >
212 > // ---- Edit auto-approve patterns -----------------------------------------
213 >
214 > private readonly _commandAutoApprover: CommandAutoApprover;
215 > private readonly _realpath: (fsPath: string) => Promise<string>;
216 >
217 > constructor(
218 > private readonly _stateManager: AgentHostStateManager, sessionPermissions.ts
219 > options: { realpath?: (fsPath: string) => Promise<string> },
220 > @IAgentConfigurationService private readonly _configService: IAgentConfigurationService,
221 > @ILogService private readonly _logService: ILogService,
222 > ) {
223 > super();
224 > this._realpath = options?.realpath ?? realpath;
225 > this._commandAutoApprover = this._register(new CommandAutoApprover(this._logService));
226 > }
228 > /**
229 > * Initializes async resources (tree-sitter WASM) used for shell command
230 > * auto-approval. Await this before any session events can arrive so that
231 > * shell command parsing within {@link getAutoApproval} is synchronous.
232 > */
233 > initialize(): Promise<void> {
234 return this._commandAutoApprover.initialize();
235 }
237 > // ---- Auto-approval (analogous to getPreConfirmAction) -------------------
238 >
239 > /**
240 > * Checks whether a `tool_ready` event should be auto-approved. Returns a
241 > * {@link ToolCallConfirmationReason} when the tool call should proceed
242 > * without user interaction, or `undefined` when user confirmation is
243 > * required.
244 > *
245 > * Checks are evaluated in order:
246 > * 1. Global auto-approve setting (`chat.tools.global.autoApprove`)
247 > * 2. Session-level bypass (`autoApprove` config)
248 > * 3. Per-tool session permissions (`permissions.allow`)
249 > * 4. Read path rules (within working directory)
250 > * 5. Write path rules (within working directory + glob patterns)
251 > * 6. Shell command rules (tree-sitter parsed, default allow/deny)
252 > */
253 > async getAutoApproval(e: IToolApprovalEvent, sessionKey: ProtocolURI): Promise<ToolCallConfirmationReason | undefined> {
254 const workDir = this._configService.getEffectiveWorkingDirectory(sessionKey);
255 const workingDirectory = workDir ? URI.parse(workDir) : undefined;
316 return undefined;
317 }
319 > /**
320 > * Returns whether VS Code's global auto-approve setting (`chat.tools.global.autoApprove`) is enabled.
321 > * When enabled, every tool call is auto-approved without changing the session's approval level in the permissions picker.
322 > */
323 > isGlobalAutoApproveEnabled(): boolean {
324 return this._configService.getRootValue(platformRootSchema, AgentHostGlobalAutoApproveEnabledConfigKey) === true;
325 }
327 > getEffectiveApprovalLevel(sessionKey: ProtocolURI): string {
328 return this._configService.getEffectiveValue(sessionKey, platformSessionSchema, SessionConfigKey.AutoApprove) ?? 'default';
329 }
331 > isSessionAutoApproveEnabled(sessionKey: ProtocolURI): boolean {
332 // `autoApprove` (Allow All) auto-approves every tool call.
333 return this.getEffectiveApprovalLevel(sessionKey) === 'autoApprove';
334 }
336 > // ---- Action construction (analogous to getPreConfirmActions) -------------
337 >
338 > /**
339 > * Constructs a `ChatToolCallReady` action from an agent
340 > * `pending_confirmation` signal. When the tool needs user confirmation
341 > * (the protocol state carries `confirmationTitle`), the standard
342 > * confirmation options are baked in so clients can render them directly.
343 > */
344 > createToolReadyAction(e: IAgentToolPendingConfirmationSignal, _sessionKey: ProtocolURI, turnId: string): IToolCallReadyAction {
345 const state = e.state;
346 if (state.confirmationTitle) {
372 };
373 }
375 > // ---- Post-confirmation side effects -------------------------------------
376 >
377 > /**
378 > * Handles the side effect of a `ChatToolCallConfirmed` action when the
379 > * user selected "Allow in this Session". Adds the tool to the session's
380 > * permission allow list so future calls are auto-approved.
381 > */
382 > handleToolCallConfirmed(chatChannel: ProtocolURI, toolCallId: string, selectedOptionId: string | undefined): void {
383 if (!isAhpChatChannel(chatChannel)) {
384 throw new Error(`Tool call confirmations must be handled on an AHP chat channel: ${chatChannel}`);
392 }
393 }
395 > // ---- Internal helpers ---------------------------------------------------
396 >
397 > private async _isReadAutoApproved(resource: URI, workingDirectory: URI | undefined): Promise<boolean> {
398 if (!workingDirectory) {
399 return false;
408 && resourcesToCheck.every(candidate => workingDirectories.some(directory => this._isResourceInDirectory(candidate, directory)));
409 }
411 > private _isResourceInWorkingDirectory(resource: URI, workingDirectory: URI | undefined): boolean {
412 return workingDirectory !== undefined && this._isResourceInDirectory(resource, workingDirectory);
413 }
415 > private _isResourceInDirectory(resource: URI, directory: URI): boolean {
416 return extUriBiasedIgnorePathCase.isEqualOrParent(normalizePath(resource), normalizePath(directory));
417 }
419 > /**
420 > * Checks whether a shell write-redirection destination (e.g. the `out.txt`
421 > * in `echo hi > out.txt`) should be auto-approved by reusing the same
422 > * rules that govern write tool calls: the destination must resolve to a
423 > * path inside the working directory and must not match a denied glob.
424 > */
425 > private _isShellWriteDestApproved(dest: string, workingDirectory: URI | undefined): boolean {
426 const resource = this._resolveShellRedirectResource(dest, workingDirectory);
427 if (!resource) {
430 return this._checkWriteResource(resource, workingDirectory);
431 }
433 > /**
434 > * Resolves the raw text of a shell redirect destination to an absolute
435 > * filesystem path. `~` is expanded to the user's home directory; the
436 > * downstream working-directory check rejects paths that end up outside
437 > * the workspace. Returns `undefined` when resolution would require a
438 > * working directory that isn't configured.
439 > */
440 > private _resolveShellRedirectResource(dest: string, workingDirectory: URI | undefined): URI | undefined {
441 const trimmed = untildify(dest.trim(), homedir());
442 if (!trimmed) {
451 return URI.file(path.resolve(workingDirectory.fsPath, trimmed));
452 }
454 > /**
455 > * Determines whether a write to `resource` can be auto-approved. Mirrors the
456 > * checks performed by the workbench edit-confirmation pipeline:
457 > *
458 > * 1. The path is resolved through any symlinks (following ancestors that do
459 > * not yet exist) so a link can't redirect an edit outside the working
460 > * directory. Both the literal and resolved paths must pass every check.
461 > * 2. The path must be free of suspicious characters (see {@link assertPathIsSafe}).
462 > * 3. The path must live inside the working directory.
463 > * 4. The path must not target a platform-restricted location (home dotfiles,
464 > * `~/Library`, `%APPDATA%`, ...).
465 > * 5. The path must match the edit auto-approve glob rules.
466 > */
467 > private async _isEditAutoApproved(resource: URI, workingDirectory: URI | undefined): Promise<boolean> {
468 const resourcesToCheck = await this._resolveResourcesForApproval(resource);
469 return resourcesToCheck !== undefined && resourcesToCheck.every(candidate => this._checkWriteResource(candidate, workingDirectory));
470 }
472 > /**
473 > * Returns the literal path plus, for absolute paths, the symlink-resolved
474 > * real path. Returns `undefined` when the path cannot be resolved due to
475 > * missing permissions, signalling that confirmation is required.
476 > */
477 > private async _resolveResourcesForApproval(resource: URI): Promise<URI[] | undefined> {
478 const resourcesToCheck = [resource];
479 if (resource.scheme !== Schemas.file) {
495 return resourcesToCheck;
496 }
498 > /** Runs the write checks for a single (already symlink-resolved) resource. */
499 > private _checkWriteResource(resource: URI, workingDirectory: URI | undefined): boolean {
500 try {
501 assertPathIsSafe(resource.fsPath);
511 return this._matchesEditAutoApprovePatterns(resource.fsPath);
512 }
514 > /**
515 > * Returns whether `resource` targets a platform-restricted location that
516 > * should always require confirmation. Edits within home-directory dotfiles
517 > * are never auto-approved. Edits within platform config directories are
518 > * allowed only when the working directory itself lives inside them.
519 > */
520 > private _isPlatformRestrictedResource(resource: URI, workingDirectory: URI | undefined): boolean {
521 const relativeToHome = extUriBiasedIgnorePathCase.relativePath(HOME_DIR, resource);
522 const topLevelName = relativeToHome?.split('/')[0];
534 return false;
535 }
537 > private _matchesEditAutoApprovePatterns(filePath: string): boolean {
538 let approved = true;
539 for (const [pattern, isApproved] of Object.entries(DEFAULT_EDIT_AUTO_APPROVE_PATTERNS)) {
544 return approved;
545 }
547 > private _isToolAllowedByPermissions(sessionKey: ProtocolURI, toolCallId: string): boolean {
548 const toolName = this._getToolNameForToolCall(sessionKey, toolCallId);
549 if (!toolName) {
560 return allowed;
561 }
563 > private _getToolNameForToolCall(sessionKey: ProtocolURI, toolCallId: string): string | undefined {
564 const sessionState = this._stateManager.getSessionState(sessionKey);
565 const parts = sessionState?.activeTurn?.responseParts;
574 return undefined;
575 }
577 > private _addToolToSessionPermissions(sessionKey: ProtocolURI, toolName: string): void {
578 const permissions = this._configService.getEffectiveValue(sessionKey, platformSessionSchema, SessionConfigKey.Permissions)
579 ?? { allow: [], deny: [] };
589 this._logService.info(`[SessionPermissionManager] Added "${toolName}" to session permissions for ${sessionKey}`);
590 }
src/vs/platform/agentHost/common/agentHostFileSystemProvider.ts 253 covered LOC · 25 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostFileSystemProvider.ts
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 > import { decodeBase64, VSBuffer } from '../../../base/common/buffer.js';
7 > import { disposableTimeout } from '../../../base/common/async.js';
8 > import { Emitter, Event } from '../../../base/common/event.js';
9 > import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../base/common/lifecycle.js';
10 > import { URI } from '../../../base/common/uri.js';
11 > import { createFileSystemProviderError, FileChangeType, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileChange, IFileDeleteOptions, IFileOverwriteOptions, IFileSystemProvider, IFileSystemProviderWithFileRealpathCapability, IFileWriteOptions, IStat, IWatchOptions } from '../../files/common/files.js';
12 > import { fromAgentHostUri, toAgentHostUri } from './agentHostUri.js';
13 > import { ContentEncoding, type CreateResourceWatchParams, type DirectoryEntry, type ResourceCopyParams, type ResourceCopyResult, type ResourceDeleteParams, type ResourceDeleteResult, type ResourceListResult, type ResourceMkdirParams, type ResourceMkdirResult, type ResourceMoveParams, type ResourceMoveResult, type ResourceReadResult, type ResourceRequestParams, type ResourceRequestResult, type ResourceResolveParams, type ResourceResolveResult, type ResourceWriteParams, type ResourceWriteResult } from './state/protocol/commands.js';
14 > import { AhpErrorCodes } from './state/protocol/errors.js';
15 > import { ProtocolError } from './state/sessionProtocol.js';
16 > import { ActionType, type ActionEnvelope } from './state/sessionActions.js';
17 > import { ROOT_STATE_URI } from './state/sessionState.js';
18 >
19 > /**
20 > * Interface for performing resource operations on a remote endpoint.
21 > *
22 > * Both {@link IAgentConnection} (client→server) and client-exposed
23 > * filesystems (server→client) satisfy this contract.
24 > */
25 > export interface IRemoteFilesystemConnection {
26 > resourceList(uri: URI): Promise<ResourceListResult>;
27 > resourceRead(uri: URI): Promise<ResourceReadResult>;
28 > resourceWrite(params: ResourceWriteParams): Promise<ResourceWriteResult>;
29 > resourceDelete(params: ResourceDeleteParams): Promise<ResourceDeleteResult>;
30 > resourceMove(params: ResourceMoveParams): Promise<ResourceMoveResult>;
31 > /** Copy a resource on the remote endpoint. */
32 > resourceCopy(params: ResourceCopyParams): Promise<ResourceCopyResult>;
33 > /**
34 > * Negotiate access to a resource the receiver mediates. Optional because
35 > * not every connection in the codebase carries one — only the agent-host
36 > * server-to-client direction needs to send `resourceRequest` today.
37 > */
38 > resourceRequest?(params: ResourceRequestParams): Promise<ResourceRequestResult>;
39 > /** Resolve (stat + realpath) a resource on the remote endpoint. */
40 > resourceResolve(params: ResourceResolveParams): Promise<ResourceResolveResult>;
41 > /** Create a directory on the remote endpoint (mkdir -p semantics). */
42 > resourceMkdir(params: ResourceMkdirParams): Promise<ResourceMkdirResult>;
43 > /**
44 > * Start a file-system watcher on the remote endpoint and return a
45 > * handle whose `onDidChange` event fires for every change the remote
46 > * reports under the watched root. Disposing the handle unsubscribes
47 > * the watch (subject to the receiver's grace window).
48 > *
49 > * Optional: implementations without subscription machinery omit it; the
50 > * filesystem provider degrades to a no-op `watch()` in that case.
51 > */
52 > watchResource?(params: CreateResourceWatchParams): Promise<IRemoteWatchHandle>;
53 > }
54 >
55 > /**
56 > * Handle for a remote file-system watcher returned by
57 > * {@link IRemoteFilesystemConnection.watchResource}. Mirrors the shape
58 > * of `IFileSystemWatcher` from `../../files/common/files.js` so the FS
59 > * provider can plug events straight into its own `onDidChangeFile`
60 > * emitter.
61 > */
62 > export interface IRemoteWatchHandle extends IDisposable {
63 > readonly onDidChange: Event<readonly IFileChange[]>;
64 > }
65 >
66 > /**
67 > * Shared implementation of {@link IAgentConnection.watchResource} —
68 > * bundles `createResourceWatch` + `subscribe` + a per-channel listener
69 > * on the action stream into an {@link IRemoteWatchHandle}. Used by
70 > * every transport that exposes those four primitives so we don't need
71 > * to duplicate the wire bookkeeping in each `IAgentConnection`
72 > * implementation.
73 > */
74 export async function createRemoteWatchHandle(
75 primitives: {
119 };
120 }
122 > /**
123 > * Build a {@link AGENT_HOST_SCHEME} URI for a given connection authority
124 > * and remote path. Assumes the remote path is a `file://` resource.
125 > */
126 > export function agentHostUri(authority: string, path: string): URI {
127 return toAgentHostUri(URI.file(path), authority);
128 }
130 > /**
131 > * Extract the remote filesystem path from a {@link AGENT_HOST_SCHEME} URI.
132 > */
133 > export function agentHostRemotePath(uri: URI): string {
134 return fromAgentHostUri(uri).path;
135 }
137 > // ---- Abstract base ----------------------------------------------------------
138 >
139 > interface IAuthorityEntry {
140 > /**
141 > * All currently-registered connections for this authority, oldest
142 > * first. The active connection is the last entry (most recent
143 > * registration wins). Older registrations are kept so that if a
144 > * caller registers `A`, then `B`, then disposes `B`, we transparently
145 > * fall back to `A` instead of entering a grace window.
146 > *
147 > * Empty while the entry is inside the grace window.
148 > */
149 > connections: IRemoteFilesystemConnection[];
150 > /**
151 > * Pending eviction timer; armed while {@link connections} is empty,
152 > * cleared on re-registration or eviction.
153 > */
154 > readonly expiry: MutableDisposable<IDisposable>;
155 > }
156 >
157 > /**
158 > * {@link IFileSystemProvider} that proxies filesystem operations
159 > * through a {@link IRemoteFilesystemConnection}.
160 > *
161 > * URIs encode the original scheme and authority in the path so any remote
162 > * resource can be represented. Subclasses provide the URI decode function
163 > * and scheme-specific helpers.
164 > *
165 > * Individual connections are identified by the URI's authority component.
166 > */
167 > export abstract class AHPFileSystemProvider extends Disposable implements IFileSystemProvider, IFileSystemProviderWithFileRealpathCapability {
168 >
169 > readonly capabilities =
170 > FileSystemProviderCapabilities.PathCaseSensitive |
171 > FileSystemProviderCapabilities.FileReadWrite |
172 > FileSystemProviderCapabilities.FileFolderCopy |
173 > FileSystemProviderCapabilities.FileRealpath;
174 >
175 > private readonly _onDidChangeCapabilities = this._register(new Emitter<void>());
176 > readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event;
177 >
178 > private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
179 > readonly onDidChangeFile = this._onDidChangeFile.event;
180 > private readonly _onDidWatchError = this._register(new Emitter<string>());
181 > readonly onDidWatchError = this._onDidWatchError.event;
182 >
183 > /**
184 > * Per-authority registration slot. We keep the slot alive for a brief
185 > * grace period after the last registration is disposed, so an
186 > * operation issued during a reconnection window can wait for the
187 > * replacement registration instead of failing immediately.
188 > */
189 > private readonly _authorities = new Map<string, IAuthorityEntry>();
190 >
191 > /**
192 > * Fires the authority whose active connection has changed: added,
193 > * replaced, fallen back to an older registration, entered the grace
194 > * window (no active connection), or evicted. Long-lived consumers
195 > * (e.g. {@link watch}) subscribe here so they continue to receive
196 > * notifications across full entry eviction + later re-creation —
197 > * something a per-entry emitter cannot offer.
198 > */
199 > private readonly _onDidChangeConnection = this._register(new Emitter<string>());
200 >
201 > /**
202 > * Grace period during which {@link _getConnection} will await a new
203 > * registration after the previous one is disposed. Covers the window
204 > * where a transport is briefly torn down and re-registered (e.g. an
205 > * agent-host client reconnect that races a plugin sync). 5s matches
206 > * the typical reconnect timeout. Consumers should still implement
207 > * logical retries for longer reconnection latencies, but this is a
208 > * low level, best-effort mechanism.
209 > *
210 > * Tests can override this via the constructor parameter.
211 > */
212 > private static readonly _DEFAULT_CONNECTION_GRACE_MS = 5000;
213 >
214 > constructor(
215 private readonly _connectionGraceMs: number = AHPFileSystemProvider._DEFAULT_CONNECTION_GRACE_MS,
216 ) {
217 super();
218 }
220 > /**
221 > * Register a mapping from a URI authority to a connection.
222 > * Returns a disposable that unregisters the mapping. Multiple
223 > * concurrent registrations for the same authority are supported;
224 > * the most recent registration wins, and disposing it falls back to
225 > * the previous one (if any). After the *last* registration is
226 > * disposed the entry is held open for {@link _connectionGraceMs} so
227 > * that a reconnect can replace it without orphaning in-flight
228 > * operations.
229 > */
230 > registerAuthority(authority: string, connection: IRemoteFilesystemConnection): IDisposable {
231 let entry = this._authorities.get(authority);
232 if (!entry) {
263 });
264 }
266 > private _expireAuthority(authority: string, entry: IAuthorityEntry): void {
267 // A re-registration may have landed between scheduling and
268 // firing — bail in that case.
274 this._onDidChangeConnection.fire(authority);
275 }
277 > override dispose(): void {
278 for (const entry of this._authorities.values()) {
279 entry.expiry.dispose();
283 super.dispose();
284 }
286 > /** Decode a provider URI back to the original URI for the remote endpoint. */
287 > protected abstract _decodeUri(resource: URI): URI;
288 >
289 > /** Encode a remote URI back into a provider URI with the given authority. */
290 > protected abstract _encodeUri(resource: URI, authority: string): URI;
291 >
292 > watch(resource: URI, opts: IWatchOptions): IDisposable {
293 // `IFileSystemProvider.watch` is synchronous, but acquiring a
294 // connection may have to wait for a (re)registration and the
416 }
417 }
419 > async realpath(resource: URI): Promise<string> {
420 const path = resource.path;
421 // Synthetic roots and virtual content schemes have no distinct
439 }
440 }
442 > async readdir(resource: URI): Promise<[string, FileType][]> {
443 const entries = await this._listDirectory(resource.authority, resource);
444 return entries.map(e => [e.name, e.type === 'directory' ? FileType.Directory : FileType.File]);
445 }
447 > async readFile(resource: URI): Promise<Uint8Array> {
448 const connection = await this._getConnection(resource.authority);
449 try {
458 }
459 }
461 > async writeFile(resource: URI, content: Uint8Array, _opts: IFileWriteOptions): Promise<void> {
462 const connection = await this._getConnection(resource.authority);
463 try {
473 }
474 }
476 > async mkdir(resource: URI): Promise<void> {
477 const connection = await this._getConnection(resource.authority);
478 try {
483 }
484 }
486 > async delete(resource: URI, opts: IFileDeleteOptions): Promise<void> {
487 const connection = await this._getConnection(resource.authority);
488 try {
493 }
494 }
496 > async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> {
497 const connection = await this._getConnection(from.authority);
498 try {
504 }
505 }
507 > async copy(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> {
508 const connection = await this._getConnection(from.authority);
509 try {
515 }
516 }
518 > /**
519 > * Negotiate access to {@link resource} with the receiver, asking for the
520 > * granted modes in {@link opts}. Used after a `NoPermissions` failure to
521 > * prompt the receiver to grant access; the caller can then retry.
522 > *
523 > * Resolves on success. Rejects if the receiver denies, the connection
524 > * is missing, or the connection doesn't implement `resourceRequest`.
525 > */
526 > async requestResourceAccess(resource: URI, opts: { readonly read?: boolean; readonly write?: boolean }): Promise<void> {
527 const connection = await this._getConnection(resource.authority);
528 if (!connection.resourceRequest) {
544 }
545 }
547 > // ---- Internals ----------------------------------------------------------
548 >
549 > private _getConnection(authority: string): Promise<IRemoteFilesystemConnection> {
550 const entry = this._authorities.get(authority);
551 if (!entry) {
591 });
592 }
594 > /**
595 > * Translate a thrown error from a {@link IRemoteFilesystemConnection}
596 > * into a {@link FileSystemProviderError}. Preserves `PermissionDenied`
597 > * (-32009) as `NoPermissions` so callers can distinguish a
598 > * permission failure from `NotFound` and decide whether to negotiate
599 > * via {@link requestResourceAccess}.
600 > */
601 > private _mapError(err: unknown, defaultCode: FileSystemProviderErrorCode): Error {
602 if (err instanceof ProtocolError && err.code === AhpErrorCodes.PermissionDenied) {
603 return createFileSystemProviderError(err.message, FileSystemProviderErrorCode.NoPermissions);
608 );
609 }
611 > /**
612 > * Resolve a decoded resource over {@link connection}. Shared by
613 > * {@link stat} and {@link realpath}.
614 > */
615 > private _resolve(connection: IRemoteFilesystemConnection, decoded: URI): Promise<ResourceResolveResult> {
616 return connection.resourceResolve({ channel: ROOT_STATE_URI, uri: decoded.toString() });
617 }
619 > private async _listDirectory(authority: string, resource: URI): Promise<readonly DirectoryEntry[]> {
620 const connection = await this._getConnection(authority);
621 try {
627 }
628 }
630 >
631 > // ---- Agent Host filesystem (client reads agent host files) ------------------
632 >
633 > /**
634 > * Filesystem provider for accessing agent host files from the
635 > * client side. Registered under the `vscode-agent-host` scheme.
636 > *
637 > * ```
638 > * vscode-agent-host://[connectionAuthority]/[originalScheme]/[originalAuthority]/[originalPath]
639 > * ```
640 > */
641 > export class AgentHostFileSystemProvider extends AHPFileSystemProvider {
642 > protected _decodeUri(resource: URI): URI {
643 return fromAgentHostUri(resource);
644 }
646 > protected _encodeUri(resource: URI, authority: string): URI {
647 return toAgentHostUri(resource, authority);
648 }
src/vs/platform/agentHost/common/state/protocol/channels-root/state.ts 244 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- state.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > import type { ConfigSchema, JsonPrimitive, ProtectedResourceMetadata } from '../common/state.js';
10 > import type { TerminalInfo } from '../channels-terminal/state.js';
11 > import type { Customization } from '../channels-session/state.js';
12 >
13 > // ─── Root State ──────────────────────────────────────────────────────────────
14 >
15 > /**
16 > * Policy configuration state for a model.
17 > *
18 > * @category Root State
19 > */
20 > export const enum PolicyState {
21 > Enabled = 'enabled',
22 > Disabled = 'disabled',
23 > Unconfigured = 'unconfigured',
24 > }
25 >
26 > /**
27 > * Global state shared with every client subscribed to `ahp-root://`.
28 > *
29 > * @category Root State
30 > */
31 > export interface RootState {
32 > /** Available agent backends and their models */
33 > agents: AgentInfo[];
34 > /** Number of active (non-disposed) sessions on the server */
35 > activeSessions?: number;
36 > /** Known terminals on the server. Subscribe to individual terminal URIs for full state. */
37 > terminals?: TerminalInfo[];
38 > /** Agent host configuration schema and current values */
39 > config?: RootConfigState;
40 > /**
41 > * Additional implementation-defined metadata about the agent host itself.
42 > *
43 > * Clients MAY look for well-known keys here to provide enhanced UI.
44 > */
45 > _meta?: Record<string, unknown>;
46 > }
47 >
48 > /**
49 > * @category Root State
50 > */
51 > export interface AgentInfo {
52 > /** Agent provider ID (e.g. `'copilot'`) */
53 > provider: string;
54 > /** Human-readable name */
55 > displayName: string;
56 > /** Description string */
57 > description: string;
58 > /** Available models for this agent */
59 > models: SessionModelInfo[];
60 > /**
61 > * Protected resources this agent requires authentication for.
62 > *
63 > * Each entry describes an OAuth 2.0 protected resource using
64 > * [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) semantics.
65 > * Clients should obtain tokens from the declared `authorization_servers`
66 > * and push them via the `authenticate` command before creating sessions
67 > * with this agent.
68 > *
69 > * @see {@link /specification/authentication | Authentication}
70 > */
71 > protectedResources?: ProtectedResourceMetadata[];
72 > /**
73 > * Customizations associated with this agent.
74 > *
75 > * Either container customizations —
76 > * {@link PluginCustomization | `PluginCustomization`} entries the agent
77 > * bundles, plus {@link DirectoryCustomization | `DirectoryCustomization`}
78 > * entries it watches in any workspace it's used with — or top-level
79 > * {@link McpServerCustomization | `McpServerCustomization`} entries
80 > * the agent host declares directly. When a session is created with
81 > * this agent, these entries are augmented (e.g. directory URIs are
82 > * resolved against the workspace, children are parsed) and propagated
83 > * into the session's `customizations` list.
84 > */
85 > customizations?: Customization[];
86 > /**
87 > * Static capabilities the agent advertises about itself. Clients use these
88 > * to gate features (multi-chat, fork) instead of switching on the provider
89 > * id.
90 > */
91 > capabilities?: AgentCapabilities;
92 > }
93 >
94 > /**
95 > * Static capabilities an {@link AgentInfo} advertises. Modelled after MCP
96 > * capabilities: each field is opt-in and its presence (an empty object `{}`)
97 > * signals support, while absence means the feature is unsupported and the
98 > * corresponding client commands MUST NOT be used. Sub-fields carry
99 > * per-capability options.
100 > *
101 > * @category Root State
102 > */
103 > export interface AgentCapabilities {
104 > /**
105 > * The agent can host more than one concurrent chat per session. When absent,
106 > * clients MUST NOT call `createChat` to open chats beyond the default one the
107 > * session starts with. An empty object `{}` advertises multi-chat without
108 > * source-based creation; set {@link MultipleChatsCapability.fork} or
109 > * {@link MultipleChatsCapability.sideChat} to allow the corresponding mode.
110 > */
111 > multipleChats?: MultipleChatsCapability;
112 > /**
113 > * The session's agent can be granted tool access to more than one working
114 > * directory. The directories are treated as equal peers except where the
115 > * agent advertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary}
116 > * (some backends need one directory designated as a primary root).
117 > *
118 > * When absent, clients MUST NOT mutate a session's or chat's working-directory
119 > * set and MUST NOT set more than one entry in
120 > * {@link CreateSessionParams.workingDirectories}.
121 > */
122 > multipleWorkingDirectories?: MultipleWorkingDirectoriesCapability;
123 > }
124 >
125 > /**
126 > * Options for the {@link AgentCapabilities.multipleChats} capability.
127 > *
128 > * @category Root State
129 > */
130 > export interface MultipleChatsCapability {
131 > /**
132 > * The agent can fork a chat from a specific turn. When absent or `false`,
133 > * clients MUST NOT pass a {@link ChatSource} with `kind: "fork"` to
134 > * `createChat`.
135 > * Forking always implies multi-chat support.
136 > */
137 > fork?: boolean;
138 > /**
139 > * The agent can create a side chat from a specific turn. When absent or
140 > * `false`, clients MUST NOT pass a {@link ChatSource} with
141 > * `kind: "sideChat"` to `createChat`.
142 > *
143 > * A side chat receives the source turn as context without copying the source
144 > * transcript into its own visible history. The source is identified by a
145 > * stable `turnId`, which the host resolves against the source chat's current
146 > * `activeTurn` or retained history. When it names the current active turn,
147 > * the host snapshots the available partial assistant response at creation
148 > * time. Side-chat support always implies multi-chat support.
149 > */
150 > sideChat?: boolean;
151 > }
152 >
153 > /**
154 > * Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability.
155 > *
156 > * @category Root State
157 > */
158 > export interface MultipleWorkingDirectoriesCapability {
159 > /**
160 > * The agent requires each chat to designate one of its working directories as
161 > * the **primary** — a distinguished root the chat is centered on (e.g. the
162 > * agent's process root for that chat, the default location for relative
163 > * paths). Primary is a **per-chat** notion, fixed at chat creation. When
164 > * `true`, a client SHOULD supply {@link CreateChatParams.primaryWorkingDirectory}
165 > * (and {@link CreateSessionParams.primaryWorkingDirectory}, which seeds the
166 > * session's default chat); a host MAY reject creation that omits it, or fall
167 > * back to the first entry of the chat's working directories. The chosen
168 > * primary is reported (read-only) on {@link ChatState.primaryWorkingDirectory}.
169 > *
170 > * When absent or `false`, the agent has no primary — all directories are
171 > * equal peers and clients need not designate one.
172 > */
173 > requiresPrimary?: boolean;
174 > }
175 >
176 > /**
177 > * @category Root State
178 > */
179 > export interface SessionModelInfo {
180 > /** Model identifier */
181 > id: string;
182 > /** Provider this model belongs to */
183 > provider: string;
184 > /** Human-readable model name */
185 > name: string;
186 > /** Maximum context window size */
187 > maxContextWindow?: number;
188 > /** Maximum number of output tokens the model can generate */
189 > maxOutputTokens?: number;
190 > /** Maximum number of prompt (input) tokens the model accepts */
191 > maxPromptTokens?: number;
192 > /** Whether the model supports vision */
193 > supportsVision?: boolean;
194 > /** Policy configuration state */
195 > policyState?: PolicyState;
196 > /**
197 > * Configuration schema describing model-specific options (e.g. thinking
198 > * level). Clients present this as a form and pass the resolved values in
199 > * {@link ModelSelection.config} when creating or changing sessions.
200 > */
201 > configSchema?: ConfigSchema;
202 > /**
203 > * Additional provider-specific metadata for this model.
204 > *
205 > * Clients MAY look for well-known keys here to provide enhanced UI.
206 > * For example, a `pricing` key may carry model pricing metadata.
207 > */
208 > _meta?: Record<string, unknown>;
209 > }
210 >
211 > /**
212 > * A model selection: the chosen model ID together with any model-specific
213 > * configuration values whose keys correspond to the model's
214 > * {@link SessionModelInfo.configSchema}.
215 > *
216 > * @category Root State
217 > */
218 > export interface ModelSelection {
219 > /** Model identifier */
220 > id: string;
221 > /**
222 > * Model-specific configuration values. Values are JSON primitives: most
223 > * pickers produce strings, but some (e.g. a numeric context-size picker)
224 > * produce numbers or booleans, which are carried through as-is.
225 > */
226 > config?: Record<string, JsonPrimitive>;
227 > }
228 >
229 > // ─── Root Config Types ───────────────────────────────────────────────────────
230 >
231 > /**
232 > * Live agent-host configuration metadata.
233 > *
234 > * The schema describes the available configuration properties and the values
235 > * contain the current value for each resolved property.
236 > *
237 > * @category Root State
238 > */
239 > export interface RootConfigState {
240 > /** JSON Schema describing available configuration properties */
241 > schema: ConfigSchema;
242 > /** Current configuration values */
243 > values: Record<string, unknown>;
244 > }
src/vs/platform/agentHost/common/state/protocol/common/actions.ts 238 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- actions.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > import type { URI } from './state.js';
10 >
11 > import type { RootAgentsChangedAction, RootActiveSessionsChangedAction, RootTerminalsChangedAction, RootConfigChangedAction } from '../channels-root/actions.js';
12 >
13 > import type { SessionReadyAction, SessionCreationFailedAction, SessionChatAddedAction, SessionChatRemovedAction, SessionChatUpdatedAction, SessionDefaultChatChangedAction, SessionTitleChangedAction, SessionServerToolsChangedAction, SessionActiveClientSetAction, SessionActiveClientRemovedAction, SessionWorkingDirectorySetAction, SessionWorkingDirectoryRemovedAction, SessionInputNeededSetAction, SessionInputNeededRemovedAction, SessionCustomizationsChangedAction, SessionCustomizationToggledAction, SessionCustomizationUpdatedAction, SessionCustomizationRemovedAction, SessionMcpServerStateChangedAction, SessionMcpServerStartRequestedAction, SessionMcpServerStopRequestedAction, SessionIsReadChangedAction, SessionIsArchivedChangedAction, SessionActivityChangedAction, SessionChangesetsChangedAction, SessionConfigChangedAction, SessionMetaChangedAction } from '../channels-session/actions.js';
14 >
15 > import type { ChatTurnStartedAction, ChatDeltaAction, ChatResponsePartAction, ChatToolCallStartAction, ChatToolCallDeltaAction, ChatToolCallReadyAction, ChatToolCallConfirmedAction, ChatToolCallCompleteAction, ChatToolCallResultConfirmedAction, ChatToolCallContentChangedAction, ChatToolCallAuthRequiredAction, ChatToolCallAuthResolvedAction, ChatTurnCompleteAction, ChatTurnCancelledAction, ChatErrorAction, ChatActivityChangedAction, ChatWorkingDirectorySetAction, ChatWorkingDirectoryRemovedAction, ChatUsageAction, ChatReasoningAction, ChatPendingMessageSetAction, ChatPendingMessageRemovedAction, ChatQueuedMessagesReorderedAction, ChatDraftChangedAction, ChatInputRequestedAction, ChatInputAnswerChangedAction, ChatInputCompletedAction, ChatTruncatedAction, ChatTurnsLoadedAction } from '../channels-chat/actions.js';
16 >
17 > import type { ChangesetStatusChangedAction, ChangesetFileSetAction, ChangesetFileRemovedAction, ChangesetFilesReviewChangedAction, ChangesetContentChangedAction, ChangesetOperationsChangedAction, ChangesetOperationStatusChangedAction, ChangesetClearedAction } from '../channels-changeset/actions.js';
18 >
19 > import type { AnnotationsSetAction, AnnotationsUpdatedAction, AnnotationsRemovedAction, AnnotationsEntrySetAction, AnnotationsEntryRemovedAction } from '../channels-annotations/actions.js';
20 >
21 > import type { TerminalDataAction, TerminalInputAction, TerminalResizedAction, TerminalClaimedAction, TerminalTitleChangedAction, TerminalCwdChangedAction, TerminalExitedAction, TerminalClearedAction, TerminalCommandDetectionAvailableAction, TerminalCommandExecutedAction, TerminalCommandFinishedAction } from '../channels-terminal/actions.js';
22 >
23 > import type { ResourceWatchChangedAction } from '../channels-resource-watch/actions.js';
24 >
25 > // ─── Action Type Enum ────────────────────────────────────────────────────────
26 >
27 > /**
28 > * Discriminant values for all state actions.
29 > *
30 > * @category Actions
31 > */
32 > export const enum ActionType {
33 > RootAgentsChanged = 'root/agentsChanged',
34 > RootActiveSessionsChanged = 'root/activeSessionsChanged',
35 > SessionReady = 'session/ready',
36 > SessionCreationFailed = 'session/creationFailed',
37 > SessionChatAdded = 'session/chatAdded',
38 > SessionChatRemoved = 'session/chatRemoved',
39 > SessionChatUpdated = 'session/chatUpdated',
40 > SessionDefaultChatChanged = 'session/defaultChatChanged',
41 > ChatTurnStarted = 'chat/turnStarted',
42 > ChatDelta = 'chat/delta',
43 > ChatResponsePart = 'chat/responsePart',
44 > ChatToolCallStart = 'chat/toolCallStart',
45 > ChatToolCallDelta = 'chat/toolCallDelta',
46 > ChatToolCallReady = 'chat/toolCallReady',
47 > ChatToolCallConfirmed = 'chat/toolCallConfirmed',
48 > ChatToolCallComplete = 'chat/toolCallComplete',
49 > ChatToolCallResultConfirmed = 'chat/toolCallResultConfirmed',
50 > ChatToolCallContentChanged = 'chat/toolCallContentChanged',
51 > ChatToolCallAuthRequired = 'chat/toolCallAuthRequired',
52 > ChatToolCallAuthResolved = 'chat/toolCallAuthResolved',
53 > ChatTurnComplete = 'chat/turnComplete',
54 > ChatTurnCancelled = 'chat/turnCancelled',
55 > ChatError = 'chat/error',
56 > ChatActivityChanged = 'chat/activityChanged',
57 > ChatWorkingDirectorySet = 'chat/workingDirectorySet',
58 > ChatWorkingDirectoryRemoved = 'chat/workingDirectoryRemoved',
59 > SessionTitleChanged = 'session/titleChanged',
60 > ChatUsage = 'chat/usage',
61 > ChatReasoning = 'chat/reasoning',
62 > SessionServerToolsChanged = 'session/serverToolsChanged',
63 > SessionActiveClientSet = 'session/activeClientSet',
64 > SessionActiveClientRemoved = 'session/activeClientRemoved',
65 > SessionWorkingDirectorySet = 'session/workingDirectorySet',
66 > SessionWorkingDirectoryRemoved = 'session/workingDirectoryRemoved',
67 > SessionInputNeededSet = 'session/inputNeededSet',
68 > SessionInputNeededRemoved = 'session/inputNeededRemoved',
69 > ChatPendingMessageSet = 'chat/pendingMessageSet',
70 > ChatPendingMessageRemoved = 'chat/pendingMessageRemoved',
71 > ChatQueuedMessagesReordered = 'chat/queuedMessagesReordered',
72 > ChatDraftChanged = 'chat/draftChanged',
73 > ChatInputRequested = 'chat/inputRequested',
74 > ChatInputAnswerChanged = 'chat/inputAnswerChanged',
75 > ChatInputCompleted = 'chat/inputCompleted',
76 > SessionCustomizationsChanged = 'session/customizationsChanged',
77 > SessionCustomizationToggled = 'session/customizationToggled',
78 > SessionCustomizationUpdated = 'session/customizationUpdated',
79 > SessionCustomizationRemoved = 'session/customizationRemoved',
80 > SessionMcpServerStateChanged = 'session/mcpServerStateChanged',
81 > SessionMcpServerStartRequested = 'session/mcpServerStartRequested',
82 > SessionMcpServerStopRequested = 'session/mcpServerStopRequested',
83 > ChatTruncated = 'chat/truncated',
84 > ChatTurnsLoaded = 'chat/turnsLoaded',
85 > SessionIsReadChanged = 'session/isReadChanged',
86 > SessionIsArchivedChanged = 'session/isArchivedChanged',
87 > SessionActivityChanged = 'session/activityChanged',
88 > SessionChangesetsChanged = 'session/changesetsChanged',
89 > SessionConfigChanged = 'session/configChanged',
90 > SessionMetaChanged = 'session/metaChanged',
91 > ChangesetStatusChanged = 'changeset/statusChanged',
92 > ChangesetFileSet = 'changeset/fileSet',
93 > ChangesetFileRemoved = 'changeset/fileRemoved',
94 > ChangesetFilesReviewChanged = 'changeset/filesReviewChanged',
95 > ChangesetContentChanged = 'changeset/contentChanged',
96 > ChangesetOperationsChanged = 'changeset/operationsChanged',
97 > ChangesetOperationStatusChanged = 'changeset/operationStatusChanged',
98 > ChangesetCleared = 'changeset/cleared',
99 > AnnotationsSet = 'annotations/set',
100 > AnnotationsUpdated = 'annotations/updated',
101 > AnnotationsRemoved = 'annotations/removed',
102 > AnnotationsEntrySet = 'annotations/entrySet',
103 > AnnotationsEntryRemoved = 'annotations/entryRemoved',
104 > RootTerminalsChanged = 'root/terminalsChanged',
105 > RootConfigChanged = 'root/configChanged',
106 > TerminalData = 'terminal/data',
107 > TerminalInput = 'terminal/input',
108 > TerminalResized = 'terminal/resized',
109 > TerminalClaimed = 'terminal/claimed',
110 > TerminalTitleChanged = 'terminal/titleChanged',
111 > TerminalCwdChanged = 'terminal/cwdChanged',
112 > TerminalExited = 'terminal/exited',
113 > TerminalCleared = 'terminal/cleared',
114 > TerminalCommandDetectionAvailable = 'terminal/commandDetectionAvailable',
115 > TerminalCommandExecuted = 'terminal/commandExecuted',
116 > TerminalCommandFinished = 'terminal/commandFinished',
117 > ResourceWatchChanged = 'resourceWatch/changed',
118 > }
119 >
120 > // ─── Action Envelope ─────────────────────────────────────────────────────────
121 >
122 > /**
123 > * Identifies the client that originally dispatched an action.
124 > */
125 > export interface ActionOrigin {
126 > clientId: string;
127 > clientSeq: number;
128 > }
129 >
130 > /**
131 > * Every action is wrapped in an `ActionEnvelope`.
132 > *
133 > * The envelope identifies the channel the action belongs to (e.g.
134 > * `ahp-root://` for root actions, the session URI for session actions, the
135 > * terminal URI for terminal actions). Individual action payloads carry only
136 > * fields that are intrinsic to the action; the channel comes from the
137 > * envelope so that any subscribable resource can route its actions uniformly.
138 > */
139 > export interface ActionEnvelope {
140 > /** Channel URI this action belongs to. */
141 > readonly channel: URI;
142 > readonly action: StateAction;
143 > readonly serverSeq: number;
144 > readonly origin: ActionOrigin | undefined;
145 > readonly rejectionReason?: string;
146 > }
147 >
148 > // ─── Discriminated Union ─────────────────────────────────────────────────────
149 >
150 > /**
151 > * Discriminated union of all state actions.
152 > */
153 > export type StateAction =
154 > | RootAgentsChangedAction
155 > | RootActiveSessionsChangedAction
156 > | RootTerminalsChangedAction
157 > | RootConfigChangedAction
158 > | SessionReadyAction
159 > | SessionCreationFailedAction
160 > | SessionChatAddedAction
161 > | SessionChatRemovedAction
162 > | SessionChatUpdatedAction
163 > | SessionDefaultChatChangedAction
164 > | SessionTitleChangedAction
165 > | SessionServerToolsChangedAction
166 > | SessionActiveClientSetAction
167 > | SessionActiveClientRemovedAction
168 > | SessionWorkingDirectorySetAction
169 > | SessionWorkingDirectoryRemovedAction
170 > | SessionInputNeededSetAction
171 > | SessionInputNeededRemovedAction
172 > | SessionCustomizationsChangedAction
173 > | SessionCustomizationToggledAction
174 > | SessionCustomizationUpdatedAction
175 > | SessionCustomizationRemovedAction
176 > | SessionMcpServerStateChangedAction
177 > | SessionMcpServerStartRequestedAction
178 > | SessionMcpServerStopRequestedAction
179 > | SessionIsReadChangedAction
180 > | SessionIsArchivedChangedAction
181 > | SessionActivityChangedAction
182 > | SessionChangesetsChangedAction
183 > | SessionConfigChangedAction
184 > | SessionMetaChangedAction
185 > | ChatTurnStartedAction
186 > | ChatDeltaAction
187 > | ChatResponsePartAction
188 > | ChatToolCallStartAction
189 > | ChatToolCallDeltaAction
190 > | ChatToolCallReadyAction
191 > | ChatToolCallConfirmedAction
192 > | ChatToolCallCompleteAction
193 > | ChatToolCallResultConfirmedAction
194 > | ChatToolCallContentChangedAction
195 > | ChatToolCallAuthRequiredAction
196 > | ChatToolCallAuthResolvedAction
197 > | ChatTurnCompleteAction
198 > | ChatTurnCancelledAction
199 > | ChatErrorAction
200 > | ChatActivityChangedAction
201 > | ChatWorkingDirectorySetAction
202 > | ChatWorkingDirectoryRemovedAction
203 > | ChatUsageAction
204 > | ChatReasoningAction
205 > | ChatPendingMessageSetAction
206 > | ChatPendingMessageRemovedAction
207 > | ChatQueuedMessagesReorderedAction
208 > | ChatDraftChangedAction
209 > | ChatInputRequestedAction
210 > | ChatInputAnswerChangedAction
211 > | ChatInputCompletedAction
212 > | ChatTruncatedAction
213 > | ChatTurnsLoadedAction
214 > | ChangesetStatusChangedAction
215 > | ChangesetFileSetAction
216 > | ChangesetFileRemovedAction
217 > | ChangesetFilesReviewChangedAction
218 > | ChangesetContentChangedAction
219 > | ChangesetOperationsChangedAction
220 > | ChangesetOperationStatusChangedAction
221 > | ChangesetClearedAction
222 > | AnnotationsSetAction
223 > | AnnotationsUpdatedAction
224 > | AnnotationsRemovedAction
225 > | AnnotationsEntrySetAction
226 > | AnnotationsEntryRemovedAction
227 > | TerminalDataAction
228 > | TerminalInputAction
229 > | TerminalResizedAction
230 > | TerminalClaimedAction
231 > | TerminalTitleChangedAction
232 > | TerminalCwdChangedAction
233 > | TerminalExitedAction
234 > | TerminalClearedAction
235 > | TerminalCommandDetectionAvailableAction
236 > | TerminalCommandExecutedAction
237 > | TerminalCommandFinishedAction
238 > | ResourceWatchChangedAction;
src/vs/platform/agentHost/node/agentConfigurationService.ts 238 covered LOC · 26 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentConfigurationService.ts
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 > import * as fs from 'fs';
7 > import { Emitter, Event } from '../../../base/common/event.js';
8 > import { Disposable } from '../../../base/common/lifecycle.js';
9 > import { dirname } from '../../../base/common/path.js';
10 > import { hasKey } from '../../../base/common/types.js';
11 > import { URI } from '../../../base/common/uri.js';
12 > import { createDecorator } from '../../instantiation/common/instantiation.js';
13 > import { ILogService } from '../../log/common/log.js';
14 > import { AgentHostConfigKey, agentHostCustomizationConfigSchema, defaultAgentHostCustomizationConfigValues } from '../common/agentHostCustomizationConfig.js';
15 > import { getAgentCustomizationSettingsEntries, getProviderBackedRootConfigKeys, withAgentCustomizationSettings, type IAgentCustomizationSettingsRegistration } from '../common/agentCustomizationSettings.js';
16 > import { copilotCliConfigSchema } from '../common/copilotCliConfig.js';
17 > import { sandboxConfigSchema } from '../common/sandboxConfigSchema.js';
18 > import type { ISchema, SchemaDefinition, SchemaValue } from '../common/agentHostSchema.js';
19 > import { ProtocolError } from '../common/state/sessionProtocol.js';
20 > import { ActionType } from '../common/state/sessionActions.js';
21 > import { parseSubagentSessionUri, ROOT_STATE_URI, type URI as ProtocolURI } from '../common/state/sessionState.js';
22 > import { AgentSession } from '../common/agentService.js';
23 > import { AgentHostStateManager } from './agentHostStateManager.js';
24 > import type { WorktreeIsolation } from './shared/worktreeIsolation.js';
25 >
26 > export const IAgentConfigurationService = createDecorator<IAgentConfigurationService>('agentConfigurationService');
27 >
28 > export interface IAgentSessionConfigurationChangeEvent {
29 > readonly session: ProtocolURI;
30 > readonly config: Record<string, unknown>;
31 > }
32 >
33 > /**
34 > * Cohesive read/write surface for agent-host configuration.
35 > *
36 > * All platform-layer consumers (tool auto-approval, side effects, future
37 > * host-config editors) should read and mutate config values through this
38 > * service rather than reaching into raw session state. The service owns
39 > * the `session → parent session → host` inheritance chain so that
40 > * host-level defaults, subagent inheritance, and per-session overrides
41 > * compose the same way everywhere.
42 > *
43 > * Reads go through a caller-supplied {@link ISchema}: each raw value is
44 > * validated against the property's schema before being returned, so a
45 > * malformed value in one layer transparently falls back to the next.
46 > */
47 > export interface IAgentConfigurationService {
48 > readonly _serviceBrand: undefined;
49 >
50 > /**
51 > * Fires whenever a {@link ActionType.RootConfigChanged} action is
52 > * processed by the state manager, signalling that callers should
53 > * re-read any root config values they depend on.
54 > */
55 > readonly onDidRootConfigChange: Event<void>;
56 >
57 > /** Fires whenever a session configuration change is processed. */
58 > readonly onDidSessionConfigChange: Event<IAgentSessionConfigurationChangeEvent>;
59 >
60 > /**
61 > * Returns the effective value of `key` for `session`, walking the
62 > * `session → parent session → host` chain and returning the first
63 > * layer that provides a value which validates against
64 > * `schema.definition[key]`. Layers that provide a malformed value
65 > * are logged and skipped. Returns `undefined` when no layer provides
66 > * a valid value.
67 > */
68 > getEffectiveValue<D extends SchemaDefinition, K extends keyof D & string>(
69 > session: ProtocolURI,
70 > schema: ISchema<D>,
71 > key: K,
72 > ): SchemaValue<D[K]> | undefined;
73 >
74 > /**
75 > * Returns the effective working directory for a session, falling back
76 > * to the parent (subagent) session's working directory when the
77 > * session itself does not have one set. The host layer does not carry
78 > * a working directory.
79 > */
80 > getEffectiveWorkingDirectory(session: ProtocolURI): string | undefined;
81 >
82 > /**
83 > * Whether a fresh worktree-isolation session's worktree has not yet been
84 > * created. Agents consult this to defer prewarming (and any other eager
85 > * materialization) until the host resolves the worktree on the first send.
86 > */
87 > isWorkingDirectoryPending(session: ProtocolURI): boolean;
88 >
89 > /** Resolves a persisted working directory, repairing a removed worktree when possible. */
90 > resolveWorkingDirectoryForResume(session: ProtocolURI, workingDirectory: URI): Promise<URI>;
91 >
92 > /**
93 > * Merges a partial config patch into a session's values via a
94 > * {@link ActionType.SessionConfigChanged} action. Keys not present in
95 > * `patch` are left untouched. The patch is applied atomically through
96 > * the state manager's reducer.
97 > */
98 > updateSessionConfig(session: ProtocolURI, patch: Record<string, unknown>): void;
99 >
100 > /**
101 > * Returns the merged config values currently stored on `session`.
102 > *
103 > * Reflects the live state managed by the reducer: every
104 > * {@link ActionType.SessionConfigChanged} action mutates these values
105 > * before this method returns. Callers materializing a provisional session
106 > * use this to read the user's latest selections without subscribing to
107 > * the action stream themselves.
108 > */
109 > getSessionConfigValues(session: ProtocolURI): Record<string, unknown> | undefined;
110 >
111 > /**
112 > * Returns the host-level value for `key`, validating it against
113 > * `schema.definition[key]`. Invalid persisted values are logged and treated
114 > * as missing.
115 > */
116 > getRootValue<D extends SchemaDefinition, K extends keyof D & string>(
117 > schema: ISchema<D>,
118 > key: K,
119 > ): SchemaValue<D[K]> | undefined;
120 >
121 > /**
122 > * Merges a partial config patch into the host-level value bag and persists
123 > * the updated values for future agent-host lifetimes.
124 > */
125 > updateRootConfig(patch: Record<string, unknown>, replace?: boolean): void;
126 >
127 > /**
128 > * Persists the current host-level value bag without mutating it.
129 > */
130 > persistRootConfig(): void;
131 >
132 > /**
133 > * Resolves once any in-flight root-config write has settled.
134 > */
135 > whenIdle(): Promise<void>;
136 >
137 > registerProviderConfiguration?(registration: IAgentCustomizationSettingsRegistration): void;
138 > getRootConfigValues?(): Readonly<Record<string, unknown>>;
139 > }
140 >
141 > export class AgentConfigurationService extends Disposable implements IAgentConfigurationService {
142 > declare readonly _serviceBrand: undefined;
143 > private _rootConfigWrite = Promise.resolve();
144 >
145 > private readonly _onDidRootConfigChange = this._register(new Emitter<void>());
146 > readonly onDidRootConfigChange: Event<void> = this._onDidRootConfigChange.event;
147 > private readonly _onDidSessionConfigChange = this._register(new Emitter<IAgentSessionConfigurationChangeEvent>());
148 > readonly onDidSessionConfigChange: Event<IAgentSessionConfigurationChangeEvent> = this._onDidSessionConfigChange.event;
149 >
150 > /**
151 > * Host-owned worktree isolation controller. Injected after construction (via
152 > * {@link setWorktreeIsolation}) because it only becomes available once the
153 > * branch-name generator has been wired, which happens after this service is
154 > * built. Consulted by {@link isWorkingDirectoryPending}, which degrades to
155 > * folder behavior while it is unset (tests, early startup).
156 > */
157 > private _worktree: WorktreeIsolation | undefined;
158 >
159 > setWorktreeIsolation(worktree: WorktreeIsolation): void {
160 > this._worktree = worktree;
161 > }
162 >
163 > constructor(
164 > private readonly _stateManager: AgentHostStateManager, agentConfigurationService.ts
165 > @ILogService private readonly _logService: ILogService,
166 > private readonly _rootConfigResource?: URI,
167 > providerConfigurations: readonly IAgentCustomizationSettingsRegistration[] = [],
168 > ) {
169 > super();
170 > // Merge our customization schema/values into the existing root config
171 > // (which already carries platform properties like permissions) rather
172 > // than replacing it.
173 > const existing = this._stateManager.rootState.config;
174 > const ownSchema = agentHostCustomizationConfigSchema.toProtocol();
175 > const sandboxSchema = sandboxConfigSchema.toProtocol();
176 > const copilotCliSchema = copilotCliConfigSchema.toProtocol();
177 > this._stateManager.rootState.config = {
178 > schema: {
179 > type: 'object',
180 > properties: { ...existing?.schema.properties, ...ownSchema.properties, ...sandboxSchema.properties, ...copilotCliSchema.properties },
181 > },
182 > values: { ...existing?.values, ...this._loadPersistedRootConfig() },
183 > };
184 > for (const registration of providerConfigurations) {
185 this.registerProviderConfiguration(registration);
186 }
188 > this._register(this._stateManager.onDidEmitEnvelope(envelope => {
189 > if (envelope.action.type === ActionType.RootConfigChanged) { agentConfigurationService.ts
190 this._onDidRootConfigChange.fire();
191 > } else if (envelope.action.type === ActionType.SessionConfigChanged) { agentConfigurationService.ts
192 this._onDidSessionConfigChange.fire({
193 session: envelope.channel,
218 return undefined;
219 }
221 > getEffectiveWorkingDirectory(session: ProtocolURI): string | undefined {
222 const own = this._stateManager.getSessionState(session)?.workingDirectories?.[0];
223 if (own !== undefined) {
230 return undefined;
231 }
233 > isWorkingDirectoryPending(session: ProtocolURI): boolean {
234 return this._worktree?.isWorkingDirectoryPending(AgentSession.id(session)) ?? false;
235 }
237 > async resolveWorkingDirectoryForResume(session: ProtocolURI, workingDirectory: URI): Promise<URI> {
238 return this._worktree?.resolveWorkingDirectoryForResume(URI.parse(session), AgentSession.id(session), workingDirectory) ?? workingDirectory;
239 }
241 > updateSessionConfig(session: ProtocolURI, patch: Record<string, unknown>): void {
242 this._stateManager.dispatchServerAction(session, {
243 type: ActionType.SessionConfigChanged,
245 });
246 }
248 > getSessionConfigValues(session: ProtocolURI): Record<string, unknown> | undefined {
249 return this._stateManager.getSessionState(session)?.config?.values;
250 }
252 > getRootValue<D extends SchemaDefinition, K extends keyof D & string>(
253 > schema: ISchema<D>, agentConfigurationService.ts
254 > key: K,
255 > ): SchemaValue<D[K]> | undefined {
256 > const root = this._stateManager.rootState.config?.values;
257 > const raw = root?.[key];
258 > if (raw === undefined) {
259 > return undefined; agentConfigurationService.ts
260 > }
261 try {
262 schema.assertValid(key, raw);
267 return undefined;
268 }
271 > updateRootConfig(patch: Record<string, unknown>, replace = false): void {
272 this._stateManager.dispatchServerAction(ROOT_STATE_URI, {
273 type: ActionType.RootConfigChanged,
277 this.persistRootConfig();
278 }
280 > persistRootConfig(): void {
281 if (!this._rootConfigResource) {
282 return;
302 });
303 }
305 > async whenIdle(): Promise<void> {
306 await this._rootConfigWrite;
307 }
309 > registerProviderConfiguration(registration: IAgentCustomizationSettingsRegistration): void {
310 const config = this._stateManager.rootState.config;
311 if (!config) {
327 }]);
328 }
330 > getRootConfigValues(): Readonly<Record<string, unknown>> {
331 return this._stateManager.rootState.config?.values ?? {};
332 }
334 > /**
335 > * Yields the raw value bags that contribute to the effective config
336 > * for `session`, in precedence order: session, parent subagent
337 > * session (if any), host.
338 > */
339 > private *_effectiveChain(session: ProtocolURI): Iterable<Record<string, unknown>> {
340 const own = this._stateManager.getSessionState(session)?.config?.values;
341 if (own) {
354 }
355 }
357 > private _loadPersistedRootConfig(): Record<string, unknown> {
358 > const defaults = defaultAgentHostCustomizationConfigValues; agentConfigurationService.ts
359 > if (!this._rootConfigResource) {
360 > return { ...defaults };
361 > }
362
363 try {
src/vs/platform/agentHost/common/state/sessionActions.ts 222 covered LOC · 12 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- sessionActions.ts
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 > // Action and notification types for the sessions process protocol.
7 > // Re-exports from the auto-generated protocol layer with local aliases.
8 > //
9 > // VS Code-specific additions:
10 > // - IToolCallStartAction extends protocol with `toolKind` and `language`
11 > // - isRootAction / isSessionAction type guards
12 > // - INotification alias for ProtocolNotification
13 >
14 > // ---- Re-exports from protocol -----------------------------------------------
15 >
16 > export {
17 > ActionType,
18 > type ActionEnvelope,
19 > type ActionOrigin,
20 > type RootAgentsChangedAction,
21 > type RootActiveSessionsChangedAction,
22 > type SessionCreationFailedAction,
23 > type SessionChatAddedAction,
24 > type SessionChatRemovedAction,
25 > type SessionChatUpdatedAction,
26 > type SessionDefaultChatChangedAction,
27 > type ChatDeltaAction,
28 > type ChatErrorAction,
29 > type SessionReadyAction,
30 > type ChatReasoningAction,
31 > type ChatResponsePartAction,
32 > type ChatToolCallCompleteAction,
33 > type ChatToolCallConfirmedAction,
34 > type ChatToolCallApprovedAction,
35 > type ChatToolCallDeniedAction,
36 > type ChatToolCallDeltaAction,
37 > type ChatToolCallReadyAction,
38 > type ChatToolCallResultConfirmedAction,
39 > type ChatToolCallStartAction,
40 > type SessionTitleChangedAction,
41 > type ChatTurnCancelledAction,
42 > type ChatTurnCompleteAction,
43 > type ChatTurnStartedAction,
44 > type ChatUsageAction,
45 > type SessionServerToolsChangedAction,
46 > type SessionActiveClientSetAction,
47 > type SessionActiveClientRemovedAction,
48 > type SessionCustomizationsChangedAction,
49 > type SessionCustomizationToggledAction,
50 > type ChatPendingMessageSetAction,
51 > type ChatPendingMessageRemovedAction,
52 > type ChatQueuedMessagesReorderedAction,
53 > type ChatInputRequestedAction,
54 > type ChatInputCompletedAction,
55 > type ChatInputAnswerChangedAction,
56 > type SessionIsReadChangedAction,
57 > type SessionIsArchivedChangedAction,
58 > type ChatToolCallContentChangedAction,
59 > type ChatTruncatedAction,
60 > type ChangesetStatusChangedAction,
61 > type ChangesetFileSetAction,
62 > type ChangesetFileRemovedAction,
63 > type ChangesetContentChangedAction,
64 > type ChangesetOperationsChangedAction,
65 > type ChangesetClearedAction,
66 > type AnnotationsSetAction,
67 > type AnnotationsUpdatedAction,
68 > type AnnotationsRemovedAction,
69 > type AnnotationsEntrySetAction,
70 > type AnnotationsEntryRemovedAction,
71 > type ResourceWatchChangedAction,
72 > type StateAction,
73 > } from './protocol/actions.js';
74 >
75 > export {
76 > AuthRequiredReason,
77 > type SessionAddedParams,
78 > type SessionRemovedParams,
79 > type SessionSummaryChangedParams,
80 > type ProgressParams,
81 > type AuthRequiredParams,
82 > } from './protocol/notifications.js';
83 >
84 > /**
85 > * String discriminants for the protocol notification methods that previously
86 > * lived inside a `notification` wrapper. These values are the JSON-RPC method
87 > * names sent over the wire by a channels-era server; they are also the `type`
88 > * discriminant on {@link ProtocolNotification} variants.
89 > */
90 > export const NotificationType = {
91 > SessionAdded: 'root/sessionAdded',
92 > SessionRemoved: 'root/sessionRemoved',
93 > SessionSummaryChanged: 'root/sessionSummaryChanged',
94 > Progress: 'root/progress',
95 > AuthRequired: 'auth/required',
96 > } as const;
97 > export type NotificationType = typeof NotificationType[keyof typeof NotificationType];
98 >
99 > // ---- Local aliases for short names ------------------------------------------
100 > // Consumers use these shorter names; they're type-only aliases.
101 >
102 > import type {
103 > RootAgentsChangedAction,
104 > RootActiveSessionsChangedAction,
105 > ChatDeltaAction,
106 > ChatReasoningAction,
107 > ChatResponsePartAction,
108 > ChatToolCallApprovedAction,
109 > ChatToolCallCompleteAction,
110 > ChatToolCallConfirmedAction,
111 > ChatToolCallDeniedAction,
112 > ChatToolCallDeltaAction,
113 > ChatToolCallReadyAction,
114 > ChatToolCallResultConfirmedAction,
115 > ChatToolCallStartAction,
116 > SessionTitleChangedAction,
117 > ChatTurnCancelledAction,
118 > ChatTurnCompleteAction,
119 > ChatTurnStartedAction,
120 > ChatErrorAction,
121 > ChatUsageAction,
122 > ChatToolCallContentChangedAction,
123 > StateAction,
124 > ChatPendingMessageSetAction,
125 > ChatPendingMessageRemovedAction,
126 > ChatQueuedMessagesReorderedAction,
127 > SessionIsReadChangedAction,
128 > SessionIsArchivedChangedAction,
129 > RootConfigChangedAction,
130 > } from './protocol/actions.js';
131 >
132 > import type { SessionAddedParams, SessionRemovedParams, SessionSummaryChangedParams, ProgressParams, AuthRequiredParams } from './protocol/notifications.js';
133 > import type { RootAction as IRootAction_, SessionAction as ISessionAction_, ChatAction as IChatAction_, ClientSessionAction as IClientSessionAction_, ServerSessionAction as IServerSessionAction_, ClientChatAction as IClientChatAction_, ServerChatAction as IServerChatAction_, TerminalAction as ITerminalAction_, ClientTerminalAction as IClientTerminalAction_, ChangesetAction as IChangesetAction_, ClientChangesetAction as IClientChangesetAction_, AnnotationsAction as IAnnotationsAction_, ClientAnnotationsAction as IClientAnnotationsAction_ } from './protocol/action-origin.generated.js';
134 >
135 > /**
136 > * Discriminated union of all server→client protocol notifications other than
137 > * the action envelope. Each variant carries its protocol `method` so callers
138 > * can switch on `type` the same way they did against the old `NotificationType`
139 > * enum.
140 > */
141 > export type ProtocolNotification =
142 > | ({ type: 'root/sessionAdded' } & SessionAddedParams)
143 > | ({ type: 'root/sessionRemoved' } & SessionRemovedParams)
144 > | ({ type: 'root/sessionSummaryChanged' } & SessionSummaryChangedParams)
145 > | ({ type: 'root/progress' } & ProgressParams)
146 > | ({ type: 'auth/required' } & AuthRequiredParams);
147 >
148 > export type RootAction = IRootAction_;
149 > export type SessionAction = ISessionAction_;
150 > export type ChatAction = IChatAction_;
151 > export type ClientSessionAction = IClientSessionAction_;
152 > export type ServerSessionAction = IServerSessionAction_;
153 > export type ClientChatAction = IClientChatAction_;
154 > export type ServerChatAction = IServerChatAction_;
155 > export type TerminalAction = ITerminalAction_;
156 > export type ClientTerminalAction = IClientTerminalAction_;
157 > export type ChangesetAction = IChangesetAction_;
158 > export type ClientChangesetAction = IClientChangesetAction_;
159 > export type AnnotationsAction = IAnnotationsAction_;
160 > export type ClientAnnotationsAction = IClientAnnotationsAction_;
161 >
162 > // Root actions
163 > export type IAgentsChangedAction = RootAgentsChangedAction;
164 > export type IActiveSessionsChangedAction = RootActiveSessionsChangedAction;
165 > export type IRootConfigChangedAction = RootConfigChangedAction;
166 >
167 > // Chat/turn actions — short aliases (turns now live on the chat channel)
168 > export type ITurnStartedAction = ChatTurnStartedAction;
169 > export type IDeltaAction = ChatDeltaAction;
170 > export type IResponsePartAction = ChatResponsePartAction;
171 > export type IToolCallStartAction = ChatToolCallStartAction;
172 > export type IToolCallDeltaAction = ChatToolCallDeltaAction;
173 > export type IToolCallReadyAction = ChatToolCallReadyAction;
174 > export type IToolCallApprovedAction = ChatToolCallApprovedAction;
175 > export type IToolCallDeniedAction = ChatToolCallDeniedAction;
176 > export type IToolCallConfirmedAction = ChatToolCallConfirmedAction;
177 > export type IToolCallCompleteAction = ChatToolCallCompleteAction;
178 > export type IToolCallResultConfirmedAction = ChatToolCallResultConfirmedAction;
179 > export type ITurnCompleteAction = ChatTurnCompleteAction;
180 > export type ITurnCancelledAction = ChatTurnCancelledAction;
181 > export type ITitleChangedAction = SessionTitleChangedAction;
182 > export type IUsageAction = ChatUsageAction;
183 > export type IReasoningAction = ChatReasoningAction;
184 > export type IErrorAction = ChatErrorAction;
185 > export type IToolCallContentChangedAction = ChatToolCallContentChangedAction;
186 > export type ICustomizationsChangedAction = import('./protocol/actions.js').SessionCustomizationsChangedAction;
187 > export type ICustomizationToggledAction = import('./protocol/actions.js').SessionCustomizationToggledAction;
188 >
189 > export type IPendingMessageSetAction = ChatPendingMessageSetAction;
190 > export type IPendingMessageRemovedAction = ChatPendingMessageRemovedAction;
191 > export type IQueuedMessagesReorderedAction = ChatQueuedMessagesReorderedAction;
192 > export type IIsReadChangedAction = SessionIsReadChangedAction;
193 > export type IIsArchivedChangedAction = SessionIsArchivedChangedAction;
194 >
195 > // Notifications
196 > export type INotification = ProtocolNotification;
197 >
198 > // ---- Type guards ------------------------------------------------------------
199 >
200 > export function isRootAction(action: StateAction): action is RootAction {
201 > return action.type.startsWith('root/'); sessionActions.ts
202 > }
204 > export function isSessionAction(action: StateAction): action is SessionAction {
205 > return action.type.startsWith('session/'); sessionActions.ts
206 > }
208 > export function isChatAction(action: StateAction): action is ChatAction {
209 > return action.type.startsWith('chat/'); sessionActions.ts
210 > }
212 > export function isTerminalAction(action: StateAction): action is TerminalAction {
213 > return action.type.startsWith('terminal/'); sessionActions.ts
214 > }
216 > export function isChangesetAction(action: StateAction): action is ChangesetAction {
217 > return action.type.startsWith('changeset/'); sessionActions.ts
218 > }
220 > export function isAnnotationsAction(action: StateAction): action is AnnotationsAction {
221 > return action.type.startsWith('annotations/'); sessionActions.ts
222 > }
src/vs/platform/telemetry/common/telemetryService.ts 218 covered LOC · 17 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- telemetryService.ts
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 > import { DisposableStore } from '../../../base/common/lifecycle.js';
7 > import { mixin } from '../../../base/common/objects.js';
8 > import { isWeb } from '../../../base/common/platform.js';
9 > import { PolicyCategory } from '../../../base/common/policy.js';
10 > import { escapeRegExpCharacters } from '../../../base/common/strings.js';
11 > import { localize } from '../../../nls.js';
12 > import { IConfigurationService } from '../../configuration/common/configuration.js';
13 > import { ConfigurationScope, Extensions, IConfigurationRegistry } from '../../configuration/common/configurationRegistry.js';
14 > import { IMeteredConnectionService } from '../../meteredConnection/common/meteredConnection.js';
15 > import product from '../../product/common/product.js';
16 > import { IProductService } from '../../product/common/productService.js';
17 > import { Registry } from '../../registry/common/platform.js';
18 > import { ClassifiedEvent, IGDPRProperty, OmitMetadata, StrictPropertyCheck } from './gdprTypings.js';
19 > import { ITelemetryData, ITelemetryService, TelemetryConfiguration, TelemetryLevel, TELEMETRY_CRASH_REPORTER_SETTING_ID, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SECTION_ID, TELEMETRY_SETTING_ID, ICommonProperties } from './telemetry.js';
20 > import { cleanData, getTelemetryLevel, ITelemetryAppender, TelemetryTrustedValue } from './telemetryUtils.js';
21 >
22 > export interface ITelemetryServiceConfig {
23 > appenders: ITelemetryAppender[];
24 > sendErrorTelemetry?: boolean;
25 > commonProperties?: ICommonProperties;
26 > piiPaths?: string[];
27 > /**
28 > * If true, telemetry events will be buffered until setExperimentProperty is called
29 > * (up to 10 seconds) to ensure experiment context is attached to all events.
30 > */
31 > waitForExperimentProperties?: boolean;
32 > /**
33 > * If provided, telemetry events will be dropped when the connection is metered.
34 > */
35 > meteredConnectionService?: IMeteredConnectionService;
36 > }
37 >
38 > interface IPendingEvent {
39 > eventName: string;
40 > eventLevel: TelemetryLevel;
41 > data: ITelemetryData | undefined;
42 > }
43 >
44 > export class TelemetryService implements ITelemetryService {
45 >
46 > static readonly IDLE_START_EVENT_NAME = 'UserIdleStart';
47 > static readonly IDLE_STOP_EVENT_NAME = 'UserIdleStop';
48 >
49 > private static readonly BUFFER_FLUSH_TIMEOUT = 10000; // 10 seconds
50 > private static readonly MAX_BUFFER_SIZE = 1000;
51 >
52 > declare readonly _serviceBrand: undefined;
53 >
54 > readonly sessionId: string;
55 > readonly machineId: string;
56 > readonly sqmId: string;
57 > readonly devDeviceId: string;
58 > readonly firstSessionDate: string;
59 > readonly msftInternal: boolean | undefined;
60 >
61 > private _appenders: ITelemetryAppender[];
62 > private _commonProperties: ICommonProperties;
63 > private _experimentProperties: { [name: string]: string | TelemetryTrustedValue<string> } = {};
64 > private _piiPaths: string[];
65 > private _telemetryLevel: TelemetryLevel;
66 > private _sendErrorTelemetry: boolean;
67 >
68 > private readonly _meteredConnectionService: IMeteredConnectionService | undefined;
69 >
70 > private _pendingEvents: IPendingEvent[] = [];
71 > private _isExperimentPropertySet = false;
72 > private _flushTimeout: ReturnType<typeof setTimeout> | undefined;
73 >
74 > private readonly _disposables = new DisposableStore();
75 > private _cleanupPatterns: RegExp[] = [];
76 >
77 > constructor(
78 config: ITelemetryServiceConfig,
79 @IConfigurationService private _configurationService: IConfigurationService,
126 }
127 }
129 > setExperimentProperty(name: string, value: string): void {
130 this._experimentProperties[name] = new TelemetryTrustedValue(value);
131
135 }
136 }
138 > setCommonProperty(name: string, value: string | boolean): void {
139 this._commonProperties[name] = value;
140 }
142 > private _flushPendingEvents(): void {
143 if (this._isExperimentPropertySet) {
144 return;
158 this._pendingEvents = [];
159 }
161 > private _updateTelemetryLevel(): void {
162 let level = getTelemetryLevel(this._configurationService);
163 const collectableTelemetry = this._productService.enabledTelemetryLevels;
172 this._telemetryLevel = level;
173 }
175 > get sendErrorTelemetry(): boolean {
176 return this._sendErrorTelemetry;
177 }
179 > get telemetryLevel(): TelemetryLevel {
180 return this._telemetryLevel;
181 }
183 > dispose(): void {
184 // Flush any remaining pending events before disposing
185 this._flushPendingEvents();
186 this._disposables.dispose();
187 }
189 > private _log(eventName: string, eventLevel: TelemetryLevel, data?: ITelemetryData) {
190 // don't send events when the user is optout
191 if (this._telemetryLevel < eventLevel) {
208 this._doLog(eventName, eventLevel, data);
209 }
211 > private _doLog(eventName: string, eventLevel: TelemetryLevel, data?: ITelemetryData) {
212 // add experiment properties
213 data = mixin(data, this._experimentProperties);
227 this._appenders.forEach(a => a.log(eventName, data ?? {}));
228 }
230 > publicLog(eventName: string, data?: ITelemetryData) {
231 this._log(eventName, TelemetryLevel.USAGE, data);
232 }
234 > publicLog2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
235 this.publicLog(eventName, data as ITelemetryData);
236 }
238 > publicLogError(errorEventName: string, data?: ITelemetryData) {
239 if (!this._sendErrorTelemetry) {
240 return;
244 this._log(errorEventName, TelemetryLevel.ERROR, data);
245 }
247 > publicLogError2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
248 this.publicLogError(eventName, data as ITelemetryData);
249 }
251 >
252 > function getTelemetryLevelSettingDescription(): string {
253 > const telemetryText = localize('telemetry.telemetryLevelMd', "Controls {0} telemetry, first-party extension telemetry, and participating third-party extension telemetry. Some third party extensions might not respect this setting. Consult the specific extension's documentation to be sure. Telemetry helps us better understand how {0} is performing, where improvements need to be made, and how features are being used.", product.nameLong);
254 > const externalLinksStatement = !product.privacyStatementUrl ?
255 > localize("telemetry.docsStatement", "Read more about the [data we collect]({0}).", 'https://aka.ms/vscode-telemetry') :
256 localize("telemetry.docsAndPrivacyStatement", "Read more about the [data we collect]({0}) and our [privacy statement]({1}).", 'https://aka.ms/vscode-telemetry', product.privacyStatementUrl);
257 > const restartString = !isWeb ? localize('telemetry.restart', 'A full restart of the application is necessary for crash reporting changes to take effect.') : ''; telemetryService.ts
258 >
259 > const crashReportsHeader = localize('telemetry.crashReports', "Crash Reports");
260 > const errorsHeader = localize('telemetry.errors', "Error Telemetry");
261 > const usageHeader = localize('telemetry.usage', "Usage Data");
262 >
263 > const telemetryTableDescription = localize('telemetry.telemetryLevel.tableDescription', "The following table outlines the data sent with each setting:");
264 > const telemetryTable = `
265 > | | ${crashReportsHeader} | ${errorsHeader} | ${usageHeader} |
266 > |:------|:-------------:|:---------------:|:----------:|
267 > | all | ✓ | ✓ | ✓ |
268 > | error | ✓ | ✓ | - |
269 > | crash | ✓ | - | - |
270 > | off | - | - | - |
271 > `;
272 >
273 > const deprecatedSettingNote = localize('telemetry.telemetryLevel.deprecated', "****Note:*** If this setting is 'off', no telemetry will be sent regardless of other telemetry settings. If this setting is set to anything except 'off' and telemetry is disabled with deprecated settings, no telemetry will be sent.*");
274 > const telemetryDescription = `
275 > ${telemetryText} ${externalLinksStatement} ${restartString}
276 >
277 > &nbsp;
278 >
279 > ${telemetryTableDescription}
280 > ${telemetryTable}
281 >
282 > &nbsp;
283 >
284 > ${deprecatedSettingNote}
285 > `;
286 >
287 > return telemetryDescription;
288 > }
289 >
290 > const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
291 > configurationRegistry.registerConfiguration({
292 > 'id': TELEMETRY_SECTION_ID,
293 > 'order': 1,
294 > 'type': 'object',
295 > 'title': localize('telemetryConfigurationTitle', "Telemetry"),
296 > 'properties': {
297 > [TELEMETRY_SETTING_ID]: {
298 > 'type': 'string',
299 > 'enum': [TelemetryConfiguration.ON, TelemetryConfiguration.ERROR, TelemetryConfiguration.CRASH, TelemetryConfiguration.OFF],
300 > 'enumDescriptions': [
301 > localize('telemetry.telemetryLevel.default', "Sends usage data, errors, and crash reports."),
302 > localize('telemetry.telemetryLevel.error', "Sends general error telemetry and crash reports."),
303 > localize('telemetry.telemetryLevel.crash', "Sends OS level crash reports."),
304 > localize('telemetry.telemetryLevel.off', "Disables all product telemetry.")
305 > ],
306 > 'markdownDescription': getTelemetryLevelSettingDescription(),
307 > 'default': TelemetryConfiguration.ON,
308 > 'restricted': true,
309 > 'scope': ConfigurationScope.APPLICATION,
310 > 'tags': ['usesOnlineServices', 'telemetry'],
311 > 'policy': {
312 > name: 'TelemetryLevel',
313 > category: PolicyCategory.Telemetry,
314 > minimumVersion: '1.99',
315 > localization: {
316 > description: {
317 > key: 'telemetry.telemetryLevel.policyDescription',
318 > value: localize('telemetry.telemetryLevel.policyDescription', "Controls the level of telemetry."),
319 > },
320 > enumDescriptions: [
321 > {
322 > key: 'telemetry.telemetryLevel.default',
323 > value: localize('telemetry.telemetryLevel.default', "Sends usage data, errors, and crash reports."),
324 > },
325 > {
326 > key: 'telemetry.telemetryLevel.error',
327 > value: localize('telemetry.telemetryLevel.error', "Sends general error telemetry and crash reports."),
328 > },
329 > {
330 > key: 'telemetry.telemetryLevel.crash',
331 > value: localize('telemetry.telemetryLevel.crash', "Sends OS level crash reports."),
332 > },
333 > {
334 > key: 'telemetry.telemetryLevel.off',
335 > value: localize('telemetry.telemetryLevel.off', "Disables all product telemetry."),
336 > }
337 > ]
338 > }
339 > }
340 > },
341 > 'telemetry.feedback.enabled': {
342 > type: 'boolean',
343 > default: true,
344 > description: localize('telemetry.feedback.enabled', "Enable feedback mechanisms such as the issue reporter, surveys, and other feedback options."),
345 > policy: {
346 > name: 'EnableFeedback',
347 > category: PolicyCategory.Telemetry,
348 > minimumVersion: '1.99',
349 > localization: { description: { key: 'telemetry.feedback.enabled', value: localize('telemetry.feedback.enabled', "Enable feedback mechanisms such as the issue reporter, surveys, and other feedback options.") } },
350 > }
351 > },
352 > // Deprecated telemetry setting
353 > [TELEMETRY_OLD_SETTING_ID]: {
354 > 'type': 'boolean',
355 > 'markdownDescription':
356 > !product.privacyStatementUrl ?
357 > localize('telemetry.enableTelemetry', "Enable diagnostic data to be collected. This helps us to better understand how {0} is performing and where improvements need to be made.", product.nameLong) :
358 localize('telemetry.enableTelemetryMd', "Enable diagnostic data to be collected. This helps us to better understand how {0} is performing and where improvements need to be made. [Read more]({1}) about what we collect and our privacy statement.", product.nameLong, product.privacyStatementUrl),
359 > 'default': true, telemetryService.ts
360 > 'restricted': true,
361 > 'markdownDeprecationMessage': localize('enableTelemetryDeprecated', "If this setting is false, no telemetry will be sent regardless of the new setting's value. Deprecated in favor of the {0} setting.", `\`#${TELEMETRY_SETTING_ID}#\``),
362 > 'scope': ConfigurationScope.APPLICATION,
363 > 'tags': ['usesOnlineServices', 'telemetry']
364 > }
365 > },
366 > });
src/vs/base/test/common/virtualScheduling/processor.ts 215 covered LOC · 19 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- processor.ts
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 > import { CancellationToken } from '../../../common/cancellation.js';
7 > import { Disposable, DisposableStore, IDisposable } from '../../../common/lifecycle.js';
8 > import { Embedding, nextMacrotask } from './embedding.js';
9 > import { TimeApi } from './timeApi.js';
10 > import { ROOT_TRACE, TraceContext } from './trace.js';
11 > import { EventSource, VirtualClock, VirtualEvent, VirtualTime } from './virtualClock.js';
12 >
13 > // ============================================================================
14 > // Termination policy
15 > // ============================================================================
16 >
17 > /**
18 > * When a {@link Run} should terminate.
19 > *
20 > * Greenfield design choice: termination is *always* explicit. There is no
21 > * "bare run()" that terminates on first empty queue, because that creates a
22 > * race with the caller's microtask chain (the run can resolve before the
23 > * caller's `.then` has had a chance to schedule).
24 > */
25 > export type TerminationPolicy =
26 > /** Resolve as soon as the virtual queue is empty. */
27 > | { readonly kind: 'idle' }
28 > /** Resolve when the token is cancelled AND the queue is empty. */
29 > | { readonly kind: 'token'; readonly token: CancellationToken }
30 > /** Resolve when virtual time has reached `time` and all events scheduled
31 > * at or before `time` have been processed. A sentinel event at `time`
32 > * is scheduled by the processor so virtual time always reaches it. */
33 > | { readonly kind: 'time'; readonly time: VirtualTime };
34 >
35 > export const untilIdle: TerminationPolicy = { kind: 'idle' };
36 > export function untilToken(token: CancellationToken): TerminationPolicy { return { kind: 'token', token }; }
37 > export function untilTime(time: VirtualTime): TerminationPolicy { return { kind: 'time', time }; }
38 >
39 > export interface RunOptions {
40 > readonly until: TerminationPolicy;
41 > /** Maximum number of virtual events this run will execute. Default: 100. */
42 > readonly maxEvents?: number;
43 > /** Maximum causal-trace depth this run will tolerate. Useful for catching
44 > * runaway self-rescheduling timers. */
45 > readonly maxTraceDepth?: number;
46 > }
47 >
48 > // ============================================================================
49 > // Run — internal state for a single processor.run() invocation
50 > // ============================================================================
51 >
52 > type RunStatus = 'continue' | 'done' | { readonly error: Error };
53 >
54 > class Run {
55 > private static _idCounter = 0;
56 > public readonly id = ++Run._idCounter;
57 >
58 > public readonly promise: Promise<void>;
59 > private _resolve!: () => void;
60 > private _reject!: (e: Error) => void;
61 > private _settled = false;
62 > public get settled(): boolean { return this._settled; }
63 >
64 > constructor(
65 public readonly options: RunOptions,
66 public readonly executedAtStart: number,
69 this.promise = new Promise<void>((res, rej) => { this._resolve = res; this._reject = rej; });
70 }
72 > settle(error?: Error): void {
73 if (this._settled) { return; }
74 this._settled = true;
75 if (error) { this._reject(error); } else { this._resolve(); }
76 }
78 > evaluate(clock: VirtualClock, executedTotal: number, makeOverflow: () => Error): RunStatus {
79 const local = executedTotal - this.executedAtStart;
80 if (local >= this.maxEvents && clock.hasEvents) {
98 }
99 }
100 > } processor.ts
101 >
102 > // ============================================================================
103 > // Step outcome — what the pure state machine tells the trampoline
104 > // ============================================================================
105 >
106 > type StepOutcome =
107 > /** Either a virtual event was executed, or a run was rejected for a
108 > * bookkeeping reason (depth/event overflow). The trampoline should let
109 > * the embedding decide how to reach the next step. */
110 > | 'progress'
111 > /** No actionable event under any active deadline. The trampoline should
112 > * park until something wakes the processor. */
113 > | 'park'
114 > /** No active runs. The trampoline should stop driving. */
115 > | 'quiesce';
116 >
117 > // ============================================================================
118 > // VirtualTimeProcessor
119 > // ============================================================================
120 >
121 > export interface VirtualTimeProcessorOptions {
122 > readonly defaultMaxEvents?: number;
123 > }
124 >
125 > /**
126 > * # VirtualTimeProcessor
127 > *
128 > * Drives a {@link VirtualClock} from the host event loop. This is the
129 > * **embedding** of a small virtual event loop into the host event loop.
130 > *
131 > * ## Responsibilities, separated
132 > *
133 > * - {@link _step} is a *pure* state-machine advance. It reads the clock,
134 > * decides what to do, optionally executes one virtual event, and returns
135 > * a {@link StepOutcome}. It never touches host time.
136 > *
137 > * - {@link _drive} is the *trampoline*. It calls `_step` and lets the
138 > * {@link Embedding} decide whether to loop in place (`'continueSync'`)
139 > * or schedule the next iteration on the host (`'cbScheduled'`). It is
140 > * the only code that touches host time.
141 > *
142 > * - {@link Run} carries the user's termination predicate. Runs are pure
143 > * over `_step`'s observations; they never schedule.
144 > *
145 > * ## Invariants
146 > *
147 > * 1. **Single driver.** At any moment at most one `_drive` invocation is
148 > * active per processor (the `_inDrive` guard).
149 > *
150 > * 2. **Step is pure w.r.t. host time.** `_step` only reads the clock,
151 > * mutates the run set via settling, and synchronously runs at most one
152 > * virtual event. It never calls into a host time API.
153 > *
154 > * 3. **Embedding chooses the host primitive.** Whether the next step runs
155 > * inline, after a microtask drain, or on a paint frame is entirely the
156 > * embedding's decision — *per event*.
157 > *
158 > * 4. **Park is breakable.** While parked, the processor wakes on
159 > * {@link VirtualClock.onEventScheduled}, on a token cancellation, and
160 > * on a new run being added.
161 > *
162 > * 5. **Disposal is terminal.** After dispose, all runs are rejected and
163 > * `_step`/`_drive` short-circuit to `'quiesce'`.
164 > *
165 > * ## On the trace-reset sink
166 > *
167 > * The trace context's deferred reset (see {@link TraceContext.runAsHandler})
168 > * needs a "fire after the microtask closure" primitive. The processor passes
169 > * its *own* {@link nextMacrotask} as that sink, so the reset goes through
170 > * the same primitive the embedding uses for its own host hops. This removes
171 > * any race between the processor's hops and the trace-reset timer.
172 > */
173 > export class VirtualTimeProcessor extends Disposable {
174 >
175 > private readonly _runs = new Map<Run, IDisposable>();
176 > private readonly _history: VirtualEvent[] = [];
177 > private _executedTotal = 0;
178 > private _disposed = false;
179 >
180 > private _inDrive = false;
181 > private _parkCleanup: IDisposable | undefined;
182 >
183 > private readonly _defaultMaxEvents: number;
184 >
185 > public get history(): readonly VirtualEvent[] { return this._history; }
186 > public get executedTotal(): number { return this._executedTotal; }
187 >
188 > constructor(
189 private readonly _clock: VirtualClock,
190 private readonly _embedding: Embedding,
196 this._register({ dispose: () => this._onDispose() });
197 }
198 > processor.ts
199 > // ---- Public API -----------------------------------------------------
200 >
201 > /** Start a run with the given termination policy. */
202 > run(options: RunOptions): Promise<void> {
203 const run = new Run(options, this._executedTotal, options.maxEvents ?? this._defaultMaxEvents);
204 const cleanup = new DisposableStore();
226 return run.promise;
227 }
228 > processor.ts
229 > // ---- The pure step --------------------------------------------------
230 >
231 > private _step(): StepOutcome {
232 if (this._disposed) { return 'quiesce'; }
233
254 return 'progress';
255 }
256 > processor.ts
257 > private _executeOne(event: VirtualEvent): void {
258 try {
259 TraceContext.instance.runAsHandler(
280 }
281 }
282 > processor.ts
283 > // ---- The trampoline -------------------------------------------------
284 >
285 > private readonly _drive = (): void => {
286 > if (this._inDrive) { return; }
287 > this._inDrive = true;
288 > try {
289 > while (true) {
290 > const outcome = this._step();
291 > if (outcome === 'quiesce') { return; }
292 > if (outcome === 'park') { this._park(); return; } processor.ts
293 >
294 > // 'progress': read the next event so the embedding can pick a
295 > // per-event primitive. If there is none, loop and let the next
296 > // `_step` decide between 'park' and 'quiesce'.
297 > const next = this._clock.peekNext();
298 > if (next === undefined) { continue; }
299 > processor.ts
300 > const choice = this._embedding(next, this._drive);
301 > if (choice === 'cbScheduled') { return; }
302 > // 'continueSync': loop in place. processor.ts
303 > }
304 > } finally {
305 > this._inDrive = false;
306 > }
307 > };
308 >
309 > // ---- Park & wake ----------------------------------------------------
310 >
311 > private _park(): void {
312 this._unpark();
313 const store = new DisposableStore();
315 this._parkCleanup = store;
316 }
317 > processor.ts
318 > private _unpark(): void {
319 this._parkCleanup?.dispose();
320 this._parkCleanup = undefined;
321 }
322 > processor.ts
323 > private _wake(): void {
324 if (this._disposed) { return; }
325 this._unpark();
337 nextMacrotask(this._realApi, this._drive);
338 }
339 > processor.ts
340 > // ---- Run lifecycle --------------------------------------------------
341 >
342 > private _settleFinishedRuns(): void {
343 for (const run of [...this._runs.keys()]) {
344 if (run.settled) { continue; }
351 }
352 }
353 > processor.ts
354 > private _settleRun(run: Run, error?: Error): void {
355 const cleanup = this._runs.get(run);
356 if (!cleanup) { return; }
359 run.settle(error);
360 }
361 > processor.ts
362 > private _buildOverflow(run: Run): Error {
363 const local = this._executedTotal - run.executedAtStart;
364 return new Error(
367 );
368 }
369 > processor.ts
370 > private _buildDepthOverflow(run: Run, depth: number): Error {
371 return new Error(
372 `[VirtualTimeProcessor] Run #${run.id} exceeded maxTraceDepth (${run.options.maxTraceDepth}) — ` +
375 );
376 }
377 > processor.ts
378 > private _onDispose(): void {
379 this._disposed = true;
380 this._unpark();
382 for (const run of [...this._runs.keys()]) { this._settleRun(run, err); }
383 }
384 > } processor.ts
src/vs/platform/agentHost/common/state/protocol/common/errors.ts 215 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- errors.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > import type { ProtectedResourceMetadata } from './state.js';
10 > import type { ResourceRequestParams } from './commands.js';
11 >
12 > // ─── Standard JSON-RPC Codes ─────────────────────────────────────────────────
13 >
14 > /**
15 > * Standard JSON-RPC 2.0 error codes.
16 > *
17 > * @category Standard JSON-RPC Codes
18 > */
19 > export const JsonRpcErrorCodes = {
20 > /** Invalid JSON */
21 > ParseError: -32700,
22 > /** Not a valid JSON-RPC request */
23 > InvalidRequest: -32600,
24 > /** Unknown method name */
25 > MethodNotFound: -32601,
26 > /** Invalid method parameters */
27 > InvalidParams: -32602,
28 > /** Unspecified server error */
29 > InternalError: -32603,
30 > } as const;
31 >
32 > // ─── AHP Application Codes ──────────────────────────────────────────────────
33 >
34 > /**
35 > * AHP application-specific error codes.
36 > *
37 > * @category AHP Application Codes
38 > * @version 1
39 > */
40 > export const AhpErrorCodes = {
41 > /** The referenced session URI does not exist */
42 > SessionNotFound: -32001,
43 > /** The requested agent provider is not registered */
44 > ProviderNotFound: -32002,
45 > /** A session with the given URI already exists */
46 > SessionAlreadyExists: -32003,
47 > /** The operation requires no active turn, but one is in progress */
48 > TurnInProgress: -32004,
49 > /**
50 > * The server cannot speak any of the protocol versions offered by the
51 > * client in `InitializeParams.protocolVersions`. The `data` field of the
52 > * JSON-RPC error MAY be an `UnsupportedProtocolVersionErrorData` advertising
53 > * the protocol versions the server is willing to speak.
54 > */
55 > UnsupportedProtocolVersion: -32005,
56 > /** The requested content URI does not exist */
57 > ContentNotFound: -32006,
58 > /**
59 > * A command failed because the client has not authenticated for a required
60 > * protected resource. The `data` field of the JSON-RPC error MUST be an
61 > * `AuthRequiredErrorData` describing the resources that require
62 > * authentication.
63 > *
64 > * @see {@link /specification/authentication | Authentication}
65 > */
66 > AuthRequired: -32007,
67 > /** The requested file, folder, or URI does not exist */
68 > NotFound: -32008,
69 > /**
70 > * The client is not permitted to access the requested resource.
71 > *
72 > * Servers SHOULD return this when a client attempts to read or browse
73 > * a path outside the allowed set (e.g. outside the session's working
74 > * directory or workspace roots).
75 > *
76 > * The `data` field of the JSON-RPC error MAY be a
77 > * `PermissionDeniedErrorData` advertising a `resourceRequest` that, if
78 > * granted, would unlock the operation.
79 > */
80 > PermissionDenied: -32009,
81 > /**
82 > * The target resource already exists and the operation does not allow
83 > * overwriting (e.g. `resourceWrite` with `createOnly: true`).
84 > */
85 > AlreadyExists: -32010,
86 > /**
87 > * An optimistic-concurrency precondition failed.
88 > *
89 > * Returned when a request carries a precondition token that no longer
90 > * matches the receiver's current state — for example, `resourceWrite`
91 > * with an `ifMatch` etag that has been superseded by a concurrent
92 > * write. Callers SHOULD re-read the resource (e.g. via
93 > * `resourceResolve`) and decide whether to retry the operation with the
94 > * fresh token or surface the conflict to the user.
95 > */
96 > Conflict: -32011,
97 > } as const;
98 >
99 > /** Union type of all AHP application error codes. */
100 > export type AhpErrorCode = (typeof AhpErrorCodes)[keyof typeof AhpErrorCodes];
101 >
102 > /** Union type of all JSON-RPC error codes. */
103 > export type JsonRpcErrorCode = (typeof JsonRpcErrorCodes)[keyof typeof JsonRpcErrorCodes];
104 >
105 > // ─── Error Detail Types ──────────────────────────────────────────────────────
106 >
107 > /**
108 > * Details carried in the `data` field of an `AuthRequired` (-32007) error.
109 > *
110 > * Wraps the protected resource list in `{ resources: [...] }` rather than
111 > * returning a bare array, so additional fields can be added in future
112 > * versions without breaking the wire shape.
113 > *
114 > * @category Error Details
115 > * @version 1
116 > */
117 > export interface AuthRequiredErrorData {
118 > /** Protected resources that require authentication. */
119 > resources: ProtectedResourceMetadata[];
120 > }
121 >
122 > /**
123 > * Details carried in the `data` field of a `PermissionDenied` (-32009) error.
124 > *
125 > * The receiver MAY advertise a `resourceRequest` payload describing the
126 > * access that, if granted, would unlock the operation. The caller MAY then
127 > * issue `resourceRequest` with that payload to negotiate access.
128 > *
129 > * @category Error Details
130 > * @version 1
131 > */
132 > export interface PermissionDeniedErrorData {
133 > /**
134 > * The resource access that, if granted via `resourceRequest`, would unlock
135 > * the operation. Omitted when no specific access grant would resolve the
136 > * denial (for example, when the resource is fundamentally inaccessible).
137 > */
138 > request?: ResourceRequestParams;
139 > }
140 >
141 > /**
142 > * Details carried in the `data` field of an `UnsupportedProtocolVersion`
143 > * (-32005) error.
144 > *
145 > * @category Error Details
146 > * @version 1
147 > */
148 > export interface UnsupportedProtocolVersionErrorData {
149 > /**
150 > * Protocol versions the server is willing to speak.
151 > *
152 > * Each entry is either a [SemVer](https://semver.org) `MAJOR.MINOR.PATCH`
153 > * string (e.g. `"0.1.0"`) or a [SemVer range](https://semver.org/#spec-item-11)
154 > * constraint (e.g. `">=0.1.0 <0.3.0"` or `"^0.2.0"`).
155 > */
156 > supportedVersions: string[];
157 > }
158 >
159 > /**
160 > * Maps each AHP error code that carries structured `data` to the type of
161 > * that data.
162 > *
163 > * Error codes not present in this map either have no `data` payload or
164 > * carry an unspecified payload that callers SHOULD treat as `unknown`.
165 > *
166 > * @category Error Details
167 > * @version 1
168 > */
169 > export interface AhpErrorDetailsMap {
170 > [AhpErrorCodes.AuthRequired]: AuthRequiredErrorData;
171 > [AhpErrorCodes.PermissionDenied]: PermissionDeniedErrorData;
172 > [AhpErrorCodes.UnsupportedProtocolVersion]: UnsupportedProtocolVersionErrorData;
173 > }
174 >
175 > /** AHP error codes that carry a structured `data` payload. */
176 > export type AhpErrorCodeWithData = keyof AhpErrorDetailsMap;
177 >
178 > /**
179 > * A typed JSON-RPC error object whose `data` is narrowed by `code`.
180 > *
181 > * Distributes over the `AhpErrorCode` union so narrowing on `code` reveals
182 > * the precise `data` type. For codes listed in {@link AhpErrorDetailsMap}
183 > * `data` is required; for all other codes `data` is an optional `unknown`.
184 > *
185 > * ```ts
186 > * function handle(err: AhpError) {
187 > * if (err.code === AhpErrorCodes.PermissionDenied) {
188 > * err.data.request; // typed as ResourceRequestParams | undefined
189 > * }
190 > * }
191 > * ```
192 > *
193 > * @category Error Details
194 > * @version 1
195 > */
196 > export type AhpError<C extends AhpErrorCode = AhpErrorCode> =
197 > C extends AhpErrorCode
198 > ? C extends keyof AhpErrorDetailsMap
199 > ? {
200 > /** The error code. */
201 > readonly code: C;
202 > /** Human-readable error message. */
203 > readonly message: string;
204 > /** Structured detail payload mandated by `AhpErrorDetailsMap`. */
205 > readonly data: AhpErrorDetailsMap[C];
206 > }
207 > : {
208 > /** The error code. */
209 > readonly code: C;
210 > /** Human-readable error message. */
211 > readonly message: string;
212 > /** Optional, unspecified detail payload. */
213 > readonly data?: unknown;
214 > }
215 > : never;
src/vs/platform/files/common/inMemoryFilesystemProvider.ts 215 covered LOC · 52 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- inMemoryFilesystemProvider.ts
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 > import { VSBuffer } from '../../../base/common/buffer.js';
7 > import { Emitter, Event } from '../../../base/common/event.js';
8 > import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
9 > import * as resources from '../../../base/common/resources.js';
10 > import { ReadableStreamEvents, newWriteableStream } from '../../../base/common/stream.js';
11 > import { URI } from '../../../base/common/uri.js';
12 > import { FileChangeType, IFileDeleteOptions, IFileOverwriteOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions, createFileSystemProviderError, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileOpenOptions, IFileSystemProviderWithFileAtomicDeleteCapability, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileAtomicWriteCapability, IFileSystemProviderWithFileReadStreamCapability, isFileOpenForWriteOptions } from './files.js';
13 >
14 > class File implements IStat {
15 >
16 > readonly type: FileType.File;
17 > readonly ctime: number;
18 > mtime: number;
19 > size: number;
20 >
21 > name: string;
22 > data?: Uint8Array;
23 >
24 > constructor(name: string) {
25 > this.type = FileType.File; inMemoryFilesystemProvider.ts
26 > this.ctime = Date.now();
27 > this.mtime = Date.now();
28 > this.size = 0;
29 > this.name = name;
30 > }
32 >
33 > class Directory implements IStat {
34 >
35 > readonly type: FileType.Directory;
36 > readonly ctime: number;
37 > mtime: number;
38 > size: number;
39 >
40 > name: string;
41 > readonly entries: Map<string, File | Directory>;
42 >
43 > constructor(name: string) {
44 > this.type = FileType.Directory; inMemoryFilesystemProvider.ts
45 > this.ctime = Date.now();
46 > this.mtime = Date.now();
47 > this.size = 0;
48 > this.name = name;
49 > this.entries = new Map();
50 > }
52 >
53 > type Entry = File | Directory;
54 >
55 > export class InMemoryFileSystemProvider extends Disposable implements
56 > IFileSystemProviderWithFileReadWriteCapability,
57 > IFileSystemProviderWithOpenReadWriteCloseCapability,
58 > IFileSystemProviderWithFileReadStreamCapability,
59 > IFileSystemProviderWithFileAtomicReadCapability,
60 > IFileSystemProviderWithFileAtomicWriteCapability,
61 > IFileSystemProviderWithFileAtomicDeleteCapability {
63 > private memoryFdCounter = 0;
64 > private readonly fdMemory = new Map<number, { file: File; resource: URI; append: boolean; write: boolean }>();
65 > private _onDidChangeCapabilities = this._register(new Emitter<void>());
66 > readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event;
67 >
68 > private _capabilities = FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.FileAppend | FileSystemProviderCapabilities.PathCaseSensitive;
69 > get capabilities(): FileSystemProviderCapabilities { return this._capabilities; }
70 >
71 > setReadOnly(readonly: boolean) {
72 const isReadonly = !!(this._capabilities & FileSystemProviderCapabilities.Readonly);
73 if (readonly !== isReadonly) {
76 }
77 }
79 > root = new Directory('');
80 >
81 > // --- manage file metadata
82 >
83 > async stat(resource: URI): Promise<IStat> {
84 > return this._lookup(resource, false); inMemoryFilesystemProvider.ts
85 > }
87 > async readdir(resource: URI): Promise<[string, FileType][]> {
88 > const entry = this._lookupAsDirectory(resource, false); inMemoryFilesystemProvider.ts
89 > const result: [string, FileType][] = [];
90 > entry.entries.forEach((child, name) => result.push([name, child.type]));
91 > return result;
92 > }
94 > // --- manage file contents
95 >
96 > async readFile(resource: URI): Promise<Uint8Array> {
97 const data = this._lookupAsFile(resource, false).data;
98 if (data) {
101 throw createFileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);
102 }
104 > readFileStream(resource: URI): ReadableStreamEvents<Uint8Array> {
105 const data = this._lookupAsFile(resource, false).data;
106
110 return stream;
111 }
113 > async writeFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> {
114 > const basename = resources.basename(resource); inMemoryFilesystemProvider.ts
115 > const parent = this._lookupParentDirectory(resource);
116 > let entry = parent.entries.get(basename);
117 > if (entry instanceof Directory) {
118 throw createFileSystemProviderError('file is directory', FileSystemProviderErrorCode.FileIsADirectory);
119 }
120 > if (!entry && !opts.create) { inMemoryFilesystemProvider.ts
121 throw createFileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);
122 }
123 > if (entry && opts.create && !opts.overwrite) { inMemoryFilesystemProvider.ts
124 throw createFileSystemProviderError('file exists already', FileSystemProviderErrorCode.FileExists);
125 }
126 > if (!entry) { inMemoryFilesystemProvider.ts
127 > entry = new File(basename);
128 > parent.entries.set(basename, entry);
129 > this._fireSoon({ type: FileChangeType.ADDED, resource });
130 > }
131 > entry.mtime = Date.now();
132 >
133 > if (opts.append) {
134 entry.size += content.byteLength;
135 const oldData = entry.data ?? new Uint8Array(0);
138 newData.set(content, oldData.byteLength);
139 entry.data = newData;
141 > entry.size = content.byteLength; inMemoryFilesystemProvider.ts
142 > entry.data = content;
143 > }
145 > this._fireSoon({ type: FileChangeType.UPDATED, resource });
146 > }
148 > // file open/read/write/close
149 > open(resource: URI, opts: IFileOpenOptions): Promise<number> {
150 let file = this._lookup(resource, true);
151 const write = isFileOpenForWriteOptions(opts);
175 return Promise.resolve(fd);
176 }
178 > close(fd: number): Promise<void> {
179 const fdData = this.fdMemory.get(fd);
180 if (fdData?.write) {
187 return Promise.resolve();
188 }
190 > read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
191 const fdData = this.fdMemory.get(fd);
192 if (!fdData) {
202 return Promise.resolve(toWrite.byteLength);
203 }
205 > write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
206 const fdData = this.fdMemory.get(fd);
207 if (!fdData) {
226 return Promise.resolve(toWrite.byteLength);
227 }
229 > // --- manage files/folders
230 >
231 > async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> {
232 if (!opts.overwrite && this._lookup(to, true)) {
233 throw createFileSystemProviderError('file exists already', FileSystemProviderErrorCode.FileExists);
249 );
250 }
252 > async delete(resource: URI, opts: IFileDeleteOptions): Promise<void> {
253 const dirname = resources.dirname(resource);
254 const basename = resources.basename(resource);
260 }
261 }
263 > async mkdir(resource: URI): Promise<void> {
264 > if (this._lookup(resource, true)) { inMemoryFilesystemProvider.ts
265 throw createFileSystemProviderError('file exists already', FileSystemProviderErrorCode.FileExists);
266 }
268 > const basename = resources.basename(resource);
269 > const dirname = resources.dirname(resource);
270 > const parent = this._lookupAsDirectory(dirname, false);
271 >
272 > const entry = new Directory(basename);
273 > parent.entries.set(entry.name, entry);
274 > parent.mtime = Date.now();
275 > parent.size += 1;
276 > this._fireSoon({ type: FileChangeType.UPDATED, resource: dirname }, { type: FileChangeType.ADDED, resource });
277 > }
279 > // --- lookup
280 >
281 > private _lookup(uri: URI, silent: false): Entry;
282 > private _lookup(uri: URI, silent: boolean): Entry | undefined;
283 > private _lookup(uri: URI, silent: boolean): Entry | undefined {
284 > const parts = uri.path.split('/'); inMemoryFilesystemProvider.ts
285 > let entry: Entry = this.root;
286 > for (const part of parts) {
287 > if (!part) {
288 > continue;
289 > }
290 > let child: Entry | undefined;
291 > if (entry instanceof Directory) {
292 > child = entry.entries.get(part);
293 > }
294 > if (!child) {
295 > if (!silent) {
296 > throw createFileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);
297 > } else {
298 > return undefined; inMemoryFilesystemProvider.ts
299 > }
301 > entry = child; inMemoryFilesystemProvider.ts
302 > }
303 > return entry;
306 > private _lookupAsDirectory(uri: URI, silent: boolean): Directory {
307 > const entry = this._lookup(uri, silent); inMemoryFilesystemProvider.ts
308 > if (entry instanceof Directory) {
309 > return entry;
310 > }
311 throw createFileSystemProviderError('file not a directory', FileSystemProviderErrorCode.FileNotADirectory);
314 > private _lookupAsFile(uri: URI, silent: boolean): File {
315 const entry = this._lookup(uri, silent);
316 if (entry instanceof File) {
319 throw createFileSystemProviderError('file is a directory', FileSystemProviderErrorCode.FileIsADirectory);
320 }
322 > private _lookupParentDirectory(uri: URI): Directory {
323 > const dirname = resources.dirname(uri); inMemoryFilesystemProvider.ts
324 > return this._lookupAsDirectory(dirname, false);
325 > }
327 > // --- manage file events
328 >
329 > private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
330 > readonly onDidChangeFile: Event<readonly IFileChange[]> = this._onDidChangeFile.event;
331 >
332 > private _bufferedChanges: IFileChange[] = [];
333 > private _fireSoonHandle?: Timeout; inMemoryFilesystemProvider.ts
334 >
335 > watch(resource: URI, opts: IWatchOptions): IDisposable {
336 // ignore, fires for all changes...
337 return Disposable.None;
338 }
340 > private _fireSoon(...changes: IFileChange[]): void {
341 > this._bufferedChanges.push(...changes); inMemoryFilesystemProvider.ts
342 >
343 > if (this._fireSoonHandle) {
344 > clearTimeout(this._fireSoonHandle); inMemoryFilesystemProvider.ts
345 > }
347 > this._fireSoonHandle = setTimeout(() => {
348 > this._onDidChangeFile.fire(this._bufferedChanges); inMemoryFilesystemProvider.ts
349 > this._bufferedChanges.length = 0;
351 > }
353 > override dispose(): void {
354 > super.dispose(); inMemoryFilesystemProvider.ts
355 >
356 > this.fdMemory.clear();
357 > }
src/vs/base/common/filters.ts 212 covered LOC · 47 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- filters.ts
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 > import { CharCode } from './charCode.js';
7 > import { LRUCache } from './map.js';
8 > import { getKoreanAltChars } from './naturalLanguage/korean.js';
9 > import { tryNormalizeToBase } from './normalization.js';
10 > import * as strings from './strings.js';
11 >
12 > export interface IFilter {
13 > // Returns null if word doesn't match.
14 > (word: string, wordToMatchAgainst: string): IMatch[] | null;
15 > }
16 >
17 > export interface IMatch {
18 > start: number;
19 > end: number;
20 > }
21 >
22 > // Combined filters
23 >
24 > /**
25 > * @returns A filter which combines the provided set
26 > * of filters with an or. The *first* filters that
27 > * matches defined the return value of the returned
28 > * filter.
29 > */
30 > export function or(...filter: IFilter[]): IFilter {
31 > return function (word: string, wordToMatchAgainst: string): IMatch[] | null {
32 for (let i = 0, len = filter.length; i < len; i++) {
33 const match = filter[i](word, wordToMatchAgainst);
38 return null;
39 };
40 > } filters.ts
41 >
42 > // Prefix
43 >
44 > export const matchesStrictPrefix: IFilter = _matchesPrefix.bind(undefined, false);
45 > export const matchesPrefix: IFilter = _matchesPrefix.bind(undefined, true);
46 >
47 function _matchesPrefix(ignoreCase: boolean, word: string, wordToMatchAgainst: string): IMatch[] | null {
48 if (!wordToMatchAgainst || wordToMatchAgainst.length < word.length) {
63 return word.length > 0 ? [{ start: 0, end: word.length }] : [];
64 }
65 > filters.ts
66 > // Contiguous Substring
67 >
68 > export function matchesContiguousSubString(word: string, wordToMatchAgainst: string): IMatch[] | null {
69 if (word.length > wordToMatchAgainst.length) {
70 return null;
78 return [{ start: index, end: index + word.length }];
79 }
80 > filters.ts
81 > export function matchesBaseContiguousSubString(word: string, wordToMatchAgainst: string): IMatch[] | null {
82 if (word.length > wordToMatchAgainst.length) {
83 return null;
93 return [{ start: index, end: index + word.length }];
94 }
95 > filters.ts
96 > // Substring
97 >
98 > export function matchesSubString(word: string, wordToMatchAgainst: string): IMatch[] | null {
99 if (word.length > wordToMatchAgainst.length) {
100 return null;
103 return _matchesSubString(word.toLowerCase(), wordToMatchAgainst.toLowerCase(), 0, 0);
104 }
105 > filters.ts
106 function _matchesSubString(word: string, wordToMatchAgainst: string, i: number, j: number): IMatch[] | null {
107 if (i === word.length) {
121 }
122 }
123 > filters.ts
124 > // CamelCase
125 >
126 function isLower(code: number): boolean {
127 return CharCode.a <= code && code <= CharCode.z;
128 }
129 > filters.ts
130 > export function isUpper(code: number): boolean {
131 return CharCode.A <= code && code <= CharCode.Z;
132 }
133 > filters.ts
134 function isNumber(code: number): boolean {
135 return CharCode.Digit0 <= code && code <= CharCode.Digit9;
136 }
137 > filters.ts
138 function isWhitespace(code: number): boolean {
139 return (
144 );
145 }
146 > filters.ts
147 > const wordSeparators = new Set<number>();
148 > // These are chosen as natural word separators based on written text.
149 > // It is a subset of the word separators used by the monaco editor.
150 > '()[]{}<>`\'"-/;:,.?!'
151 > .split('')
152 > .forEach(s => wordSeparators.add(s.charCodeAt(0)));
153 >
154 function isWordSeparator(code: number): boolean {
155 return isWhitespace(code) || wordSeparators.has(code);
156 }
157 > filters.ts
158 function charactersMatch(codeA: number, codeB: number): boolean {
159 return (codeA === codeB) || (isWordSeparator(codeA) && isWordSeparator(codeB));
160 }
161 > filters.ts
162 > const alternateCharsCache: Map<number, ArrayLike<number> | undefined> = new Map();
163 > /**
164 > * Gets alternative codes to the character code passed in. This comes in the
165 > * form of an array of character codes, all of which must match _in order_ to
166 > * successfully match.
167 > *
168 > * @param code The character code to check.
169 > */
170 function getAlternateCodes(code: number): ArrayLike<number> | undefined {
171 if (alternateCharsCache.has(code)) {
186 return result;
187 }
188 > filters.ts
189 function isAlphanumeric(code: number): boolean {
190 return isLower(code) || isUpper(code) || isNumber(code);
191 }
192 > filters.ts
193 function join(head: IMatch, tail: IMatch[]): IMatch[] {
194 if (tail.length === 0) {
201 return tail;
202 }
203 > filters.ts
204 function nextAnchor(camelCaseWord: string, start: number): number {
205 for (let i = start; i < camelCaseWord.length; i++) {
211 return camelCaseWord.length;
212 }
213 > filters.ts
214 function _matchesCamelCase(word: string, camelCaseWord: string, i: number, j: number): IMatch[] | null {
215 if (i === word.length) {
230 }
231 }
232 > filters.ts
233 > interface ICamelCaseAnalysis {
234 > upperPercent: number;
235 > lowerPercent: number;
236 > alphaPercent: number;
237 > numericPercent: number;
238 > }
239 >
240 > // Heuristic to avoid computing camel case matcher for words that don't
241 > // look like camelCaseWords.
242 function analyzeCamelCaseWord(word: string): ICamelCaseAnalysis {
243 let upper = 0, lower = 0, alpha = 0, numeric = 0, code = 0;
259 return { upperPercent, lowerPercent, alphaPercent, numericPercent };
260 }
261 > filters.ts
262 function isUpperCaseWord(analysis: ICamelCaseAnalysis): boolean {
263 const { upperPercent, lowerPercent } = analysis;
264 return lowerPercent === 0 && upperPercent > 0.6;
265 }
266 > filters.ts
267 function isCamelCaseWord(analysis: ICamelCaseAnalysis): boolean {
268 const { upperPercent, lowerPercent, alphaPercent, numericPercent } = analysis;
269 return lowerPercent > 0.2 && upperPercent < 0.8 && alphaPercent > 0.6 && numericPercent < 0.2;
270 }
271 > filters.ts
272 > // Heuristic to avoid computing camel case matcher for words that don't
273 > // look like camel case patterns.
274 function isCamelCasePattern(word: string): boolean {
275 let upper = 0, lower = 0, code = 0, whitespace = 0;
289 }
290 }
291 > filters.ts
292 > export function matchesCamelCase(word: string, camelCaseWord: string): IMatch[] | null {
293 if (!camelCaseWord) {
294 return null;
330 return result;
331 }
332 > filters.ts
333 > // Matches beginning of words supporting non-ASCII languages
334 > // If `contiguous` is true then matches word with beginnings of the words in the target. E.g. "pul" will match "Git: Pull"
335 > // Otherwise also matches sub string of the word with beginnings of the words in the target. E.g. "gp" or "g p" will match "Git: Pull"
336 > // Useful in cases where the target is words (e.g. command labels)
337 >
338 > export function matchesWords(word: string, target: string, contiguous: boolean = false): IMatch[] | null {
339 if (!target || target.length === 0) {
340 return null;
361 return result;
362 }
363 > filters.ts
364 function cloneMatches(matches: IMatch[] | null): IMatch[] | null {
365 if (matches === null) {
372 return result;
373 }
374 > filters.ts
375 function _matchesWords(word: string, target: string, wordIndex: number, targetIndex: number, contiguous: boolean, memo: Map<number, IMatch[] | null>): IMatch[] | null {
376 if (wordIndex === word.length) {
391 return computed;
392 }
393 > filters.ts
394 function _matchesWordsCompute(word: string, target: string, wordIndex: number, targetIndex: number, contiguous: boolean, memo: Map<number, IMatch[] | null>): IMatch[] | null {
395 let targetIndexOffset = 0;
440 return join({ start: targetIndex, end: targetIndex + targetIndexOffset + 1 }, result);
441 }
442 > filters.ts
443 function nextWord(word: string, start: number): number {
444 for (let i = start; i < word.length; i++) {
450 return word.length;
451 }
452 > filters.ts
453 > // Fuzzy
454 >
455 > const fuzzyContiguousFilter = or(matchesPrefix, matchesCamelCase, matchesContiguousSubString);
456 > const fuzzySeparateFilter = or(matchesPrefix, matchesCamelCase, matchesSubString);
457 > const fuzzyRegExpCache = new LRUCache<string, RegExp>(10000); // bounded to 10000 elements
458 >
459 > export function matchesFuzzy(word: string, wordToMatchAgainst: string, enableSeparateSubstringMatching = false): IMatch[] | null {
460 if (typeof word !== 'string' || typeof wordToMatchAgainst !== 'string') {
461 return null; // return early for invalid input
478 return enableSeparateSubstringMatching ? fuzzySeparateFilter(word, wordToMatchAgainst) : fuzzyContiguousFilter(word, wordToMatchAgainst);
479 }
480 > filters.ts
481 > /**
482 > * Match pattern against word in a fuzzy way. As in IntelliSense and faster and more
483 > * powerful than `matchesFuzzy`
484 > */
485 > export function matchesFuzzy2(pattern: string, word: string): IMatch[] | null {
486 const score = fuzzyScore(pattern, pattern.toLowerCase(), 0, word, word.toLowerCase(), 0, { firstMatchCanBeWeak: true, boostFullMatch: true });
487 return score ? createMatches(score) : null;
488 }
489 > filters.ts
490 > export function anyScore(pattern: string, lowPattern: string, patternPos: number, word: string, lowWord: string, wordPos: number): FuzzyScore {
491 const max = Math.min(13, pattern.length);
492 for (; patternPos < max; patternPos++) {
498 return [0, wordPos];
499 }
500 > filters.ts
501 > //#region --- fuzzyScore ---
502 >
503 > export function createMatches(score: undefined | FuzzyScore): IMatch[] {
504 if (typeof score === 'undefined') {
505 return [];
518 return res;
519 }
520 > filters.ts
521 > const _maxLen = 128;
522 >
523 > function initTable() {
524 > const table: number[][] = [];
525 > const row: number[] = [];
526 > for (let i = 0; i <= _maxLen; i++) {
527 > row[i] = 0;
528 > }
529 > for (let i = 0; i <= _maxLen; i++) {
530 > table.push(row.slice(0));
531 > }
532 > return table;
533 > }
534 >
535 > function initArr(maxLen: number) {
536 > const row: number[] = [];
537 > for (let i = 0; i <= maxLen; i++) {
538 > row[i] = 0;
539 > }
540 > return row;
541 > }
542 >
543 > const _minWordMatchPos = initArr(2 * _maxLen); // min word position for a certain pattern position
544 > const _maxWordMatchPos = initArr(2 * _maxLen); // max word position for a certain pattern position
545 > const _diag = initTable(); // the length of a contiguous diagonal match
546 > const _table = initTable();
547 > const _arrows = <Arrow[][]>initTable();
548 > const _debug = false;
549 >
550 function printTable(table: number[][], pattern: string, patternLen: number, word: string, wordLen: number): string {
551 function pad(s: string, n: number, pad = ' ') {
567 return ret;
568 }
569 > filters.ts
570 function printTables(pattern: string, patternStart: number, word: string, wordStart: number): void {
571 pattern = pattern.substr(patternStart);
575 console.log(printTable(_diag, pattern, pattern.length, word, word.length));
576 }
577 > filters.ts
578 function isSeparatorAtPos(value: string, index: number): boolean {
579 if (index < 0 || index >= value.length) {
610 }
611 }
612 > filters.ts
613 function isWhitespaceAtPos(value: string, index: number): boolean {
614 if (index < 0 || index >= value.length) {
624 }
625 }
626 > filters.ts
627 function isUpperCaseAtPos(pos: number, word: string, wordLow: string): boolean {
628 return word[pos] !== wordLow[pos];
629 }
630 > filters.ts
631 > export function isPatternInWord(patternLow: string, patternPos: number, patternLen: number, wordLow: string, wordPos: number, wordLen: number, fillMinWordPosArr = false): boolean {
632 while (patternPos < patternLen && wordPos < wordLen) {
633 if (patternLow[patternPos] === wordLow[wordPos]) {
642 return patternPos === patternLen; // pattern must be exhausted
643 }
644 > filters.ts
645 > const enum Arrow { Diag = 1, Left = 2, LeftLeft = 3 }
646 >
647 > /**
648 > * An array representing a fuzzy match.
649 > *
650 > * 0. the score
651 > * 1. the offset at which matching started
652 > * 2. `<match_pos_N>`
653 > * 3. `<match_pos_1>`
654 > * 4. `<match_pos_0>` etc
655 > */
656 > export type FuzzyScore = [score: number, wordStart: number, ...matches: number[]];
657 >
658 > export namespace FuzzyScore {
659 > /**
660 > * No matches and value `-100`
661 > */
662 > export const Default: FuzzyScore = ([-100, 0]);
663 >
664 > export function isDefault(score?: FuzzyScore): score is [-100, 0] {
665 return !score || (score.length === 2 && score[0] === -100 && score[1] === 0);
666 }
667 > } filters.ts
668 >
669 > export abstract class FuzzyScoreOptions {
670 >
671 > static default = { boostFullMatch: true, firstMatchCanBeWeak: false };
672 >
673 > constructor(
674 readonly firstMatchCanBeWeak: boolean,
675 readonly boostFullMatch: boolean,
676 ) { }
677 > } filters.ts
678 >
679 > export interface FuzzyScorer {
680 > (pattern: string, lowPattern: string, patternPos: number, word: string, lowWord: string, wordPos: number, options?: FuzzyScoreOptions): FuzzyScore | undefined;
681 > }
682 >
683 > export function fuzzyScore(pattern: string, patternLow: string, patternStart: number, word: string, wordLow: string, wordStart: number, options: FuzzyScoreOptions = FuzzyScoreOptions.default): FuzzyScore | undefined {
684
685 const patternLen = pattern.length > _maxLen ? _maxLen : pattern.length;
832 return result;
833 }
834 > filters.ts
835 function _fillInMaxWordMatchPos(patternLen: number, wordLen: number, patternStart: number, wordStart: number, patternLow: string, wordLow: string) {
836 let patternPos = patternLen - 1;
844 }
845 }
846 > filters.ts
847 function _doScore(
848 pattern: string, patternLow: string, patternPos: number, patternStart: number,
913 return score;
914 }
915 > filters.ts
916 > //#endregion
917 >
918 >
919 > //#region --- graceful ---
920 >
921 > export function fuzzyScoreGracefulAggressive(pattern: string, lowPattern: string, patternPos: number, word: string, lowWord: string, wordPos: number, options?: FuzzyScoreOptions): FuzzyScore | undefined {
922 return fuzzyScoreWithPermutations(pattern, lowPattern, patternPos, word, lowWord, wordPos, true, options);
923 }
924 > filters.ts
925 > export function fuzzyScoreGraceful(pattern: string, lowPattern: string, patternPos: number, word: string, lowWord: string, wordPos: number, options?: FuzzyScoreOptions): FuzzyScore | undefined {
926 return fuzzyScoreWithPermutations(pattern, lowPattern, patternPos, word, lowWord, wordPos, false, options);
927 }
928 > filters.ts
929 function fuzzyScoreWithPermutations(pattern: string, lowPattern: string, patternPos: number, word: string, lowWord: string, wordPos: number, aggressive: boolean, options?: FuzzyScoreOptions): FuzzyScore | undefined {
930 let top = fuzzyScore(pattern, lowPattern, patternPos, word, lowWord, wordPos, options);
959 return top;
960 }
961 > filters.ts
962 function nextTypoPermutation(pattern: string, patternPos: number): string | undefined {
963
978 + pattern.slice(patternPos + 2);
979 }
980 > filters.ts
981 > //#endregion
src/vs/nls.ts 211 covered LOC · 17 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- nls.ts
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 > export function getNLSMessages(): string[] {
7 return globalThis._VSCODE_NLS_MESSAGES;
8 }
9 > nls.ts
10 > export function getNLSLanguage(): string | undefined {
11 > return globalThis._VSCODE_NLS_LANGUAGE;
12 > }
13 >
14 > declare const document: { location?: { hash?: string } } | undefined;
15 > const isPseudo = getNLSLanguage() === 'pseudo' || (typeof document !== 'undefined' && document.location && typeof document.location.hash === 'string' && document.location.hash.indexOf('pseudo=true') >= 0);
16 >
17 > export interface ILocalizeInfo {
18 > key: string;
19 > comment: string[];
20 > }
21 >
22 > export interface ILocalizedString {
23 > original: string;
24 > value: string;
25 > }
26 >
27 > function _format(message: string, args: (string | number | boolean | undefined | null)[]): string { nls.ts
28 > let result: string;
29 >
30 > if (args.length === 0) {
31 > result = message; nls.ts
32 > } else { nls.ts
33 > result = message.replace(/\{(\d+)\}/g, (match, rest) => { nls.ts
34 > const index = rest[0];
35 > const arg = args[index];
36 > let result = match;
37 > if (typeof arg === 'string') {
38 > result = arg; nls.ts
39 > } else if (typeof arg === 'number' || typeof arg === 'boolean' || arg === void 0 || arg === null) { nls.ts
40 result = String(arg);
41 }
42 > return result; nls.ts
43 > });
44 > }
45 > nls.ts
46 > if (isPseudo) {
47 // FF3B and FF3D is the Unicode zenkaku representation for [ and ]
48 result = '\uFF3B' + result.replace(/[aouei]/g, '$&$&') + '\uFF3D';
49 }
50 > nls.ts
51 > return result;
52 > }
53 > nls.ts
54 > /**
55 > * Marks a string to be localized. Returns the localized string.
56 > *
57 > * @param info The {@linkcode ILocalizeInfo} which describes the id and comments associated with the localized string.
58 > * @param message The string to localize
59 > * @param args The arguments to the string
60 > *
61 > * @note `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
62 > * @example `localize({ key: 'sayHello', comment: ['Welcomes user'] }, 'hello {0}', name)`
63 > *
64 > * @returns string The localized string.
65 > */
66 > export function localize(info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): string;
67 >
68 > /**
69 > * Marks a string to be localized. Returns the localized string.
70 > *
71 > * @param key The key to use for localizing the string
72 > * @param message The string to localize
73 > * @param args The arguments to the string
74 > *
75 > * @note `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
76 > * @example For example, `localize('sayHello', 'hello {0}', name)`
77 > *
78 > * @returns string The localized string.
79 > */
80 > export function localize(key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): string;
81 >
82 > /**
83 > * @skipMangle
84 > */
85 > export function localize(data: ILocalizeInfo | string /* | number when built */, message: string /* | null when built */, ...args: (string | number | boolean | undefined | null)[]): string {
86 > if (typeof data === 'number') { nls.ts
87 return _format(lookupMessage(data, message), args);
88 }
89 > return _format(message, args); nls.ts
90 > }
91 > nls.ts
92 > /**
93 > * Only used when built: Looks up the message in the global NLS table.
94 > * This table is being made available as a global through bootstrapping
95 > * depending on the target context.
96 > */
97 function lookupMessage(index: number, fallback: string | null): string {
98 const message = getNLSMessages()?.[index];
105 return message;
106 }
107 > nls.ts
108 > /**
109 > * Marks a string to be localized. Returns an {@linkcode ILocalizedString}
110 > * which contains the localized string and the original string.
111 > *
112 > * @param info The {@linkcode ILocalizeInfo} which describes the id and comments associated with the localized string.
113 > * @param message The string to localize
114 > * @param args The arguments to the string
115 > *
116 > * @note `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
117 > * @example `localize2({ key: 'sayHello', comment: ['Welcomes user'] }, 'hello {0}', name)`
118 > *
119 > * @returns ILocalizedString which contains the localized string and the original string.
120 > */
121 > export function localize2(info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString;
122 >
123 > /**
124 > * Marks a string to be localized. Returns an {@linkcode ILocalizedString}
125 > * which contains the localized string and the original string.
126 > *
127 > * @param key The key to use for localizing the string
128 > * @param message The string to localize
129 > * @param args The arguments to the string
130 > *
131 > * @note `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
132 > * @example `localize('sayHello', 'hello {0}', name)`
133 > *
134 > * @returns ILocalizedString which contains the localized string and the original string.
135 > */
136 > export function localize2(key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString;
137 >
138 > /**
139 > * @skipMangle
140 > */
141 > export function localize2(data: ILocalizeInfo | string /* | number when built */, originalMessage: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString {
142 let message: string;
143 if (typeof data === 'number') {
154 };
155 }
156 > nls.ts
157 > export interface INLSLanguagePackConfiguration {
158 >
159 > /**
160 > * The path to the translations config file that contains pointers to
161 > * all message bundles for `main` and extensions.
162 > */
163 > readonly translationsConfigFile: string;
164 >
165 > /**
166 > * The path to the file containing the translations for this language
167 > * pack as flat string array.
168 > */
169 > readonly messagesFile: string;
170 >
171 > /**
172 > * The path to the file that can be used to signal a corrupt language
173 > * pack, for example when reading the `messagesFile` fails. This will
174 > * instruct the application to re-create the cache on next startup.
175 > */
176 > readonly corruptMarkerFile: string;
177 > }
178 >
179 > export interface INLSConfiguration {
180 >
181 > /**
182 > * Locale as defined in `argv.json` or `app.getLocale()`.
183 > */
184 > readonly userLocale: string;
185 >
186 > /**
187 > * Locale as defined by the OS (e.g. `app.getPreferredSystemLanguages()`).
188 > */
189 > readonly osLocale: string;
190 >
191 > /**
192 > * The actual language of the UI that ends up being used considering `userLocale`
193 > * and `osLocale`.
194 > */
195 > readonly resolvedLanguage: string;
196 >
197 > /**
198 > * Defined if a language pack is used that is not the
199 > * default english language pack. This requires a language
200 > * pack to be installed as extension.
201 > */
202 > readonly languagePack?: INLSLanguagePackConfiguration;
203 >
204 > /**
205 > * The path to the file containing the default english messages
206 > * as flat string array. The file is only present in built
207 > * versions of the application.
208 > */
209 > readonly defaultMessagesFile: string;
210 >
211 > /**
212 > * Below properties are deprecated and only there to continue support
213 > * for `vscode-nls` module that depends on them.
214 > * Refs https://github.com/microsoft/vscode-nls/blob/main/src/node/main.ts#L36-L46
215 > */
216 > /** @deprecated */
217 > readonly locale: string;
218 > /** @deprecated */
219 > readonly availableLanguages: Record<string, string>;
220 > /** @deprecated */
221 > readonly _languagePackSupport?: boolean;
222 > /** @deprecated */
223 > readonly _languagePackId?: string;
224 > /** @deprecated */
225 > readonly _translationsConfigFile?: string;
226 > /** @deprecated */
227 > readonly _cacheRoot?: string;
228 > /** @deprecated */
229 > readonly _resolvedLanguagePackCoreLocation?: string;
230 > /** @deprecated */
231 > readonly _corruptedFile?: string;
232 > }
233 >
234 > export interface ILanguagePack {
235 > readonly hash: string;
236 > readonly label: string | undefined;
237 > readonly extensions: {
238 > readonly extensionIdentifier: { readonly id: string; readonly uuid?: string };
239 > readonly version: string;
240 > }[];
241 > readonly translations: Record<string, string | undefined>;
242 > }
243 >
244 > export type ILanguagePacks = Record<string, ILanguagePack | undefined>;
src/vs/base/common/observableInternal/base.ts 206 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- base.ts
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 > import { DisposableStore, onUnexpectedError } from './commonFacade/deps.js';
7 >
8 > /**
9 > * Represents an observable value.
10 > *
11 > * @template T The type of the values the observable can hold.
12 > */
13 > // This interface exists so that, for example for string observables,
14 > // typescript renders the type as `IObservable<string>` instead of `IObservable<string, unknown>`.
15 > export interface IObservable<T> extends IObservableWithChange<T, unknown> { }
16 >
17 > /**
18 > * Represents an observable value.
19 > *
20 > * @template T The type of the values the observable can hold.
21 > * @template TChange The type used to describe value changes
22 > * (usually `void` and only used in advanced scenarios).
23 > * While observers can miss temporary values of an observable,
24 > * they will receive all change values (as long as they are subscribed)!
25 > */
26 > export interface IObservableWithChange<T, TChange = unknown> {
27 > /**
28 > * Returns the current value.
29 > *
30 > * Calls {@link IObserver.handleChange} if the observable notices that the value changed.
31 > * Must not be called from {@link IObserver.handleChange}!
32 > */
33 > get(): T;
34 >
35 > /**
36 > * Forces the observable to check for changes and report them.
37 > *
38 > * Has the same effect as calling {@link IObservable.get}, but does not force the observable
39 > * to actually construct the value, e.g. if change deltas are used.
40 > * Calls {@link IObserver.handleChange} if the observable notices that the value changed.
41 > * Must not be called from {@link IObserver.handleChange}!
42 > */
43 > reportChanges(): void;
44 >
45 > /**
46 > * Adds the observer to the set of subscribed observers.
47 > * This method is idempotent.
48 > */
49 > addObserver(observer: IObserver): void;
50 >
51 > /**
52 > * Removes the observer from the set of subscribed observers.
53 > * This method is idempotent.
54 > */
55 > removeObserver(observer: IObserver): void;
56 >
57 > // #region These members have a standard implementation and are only part of the interface for convenience.
58 >
59 > /**
60 > * Reads the current value and subscribes the reader to this observable.
61 > *
62 > * Calls {@link IReader.readObservable} if a reader is given, otherwise {@link IObservable.get}
63 > * (see {@link ConvenientObservable.read} for the implementation).
64 > */
65 > read(reader: IReader | undefined): T;
66 >
67 > /**
68 > * Makes sure this value is computed eagerly.
69 > */
70 > recomputeInitiallyAndOnChange(store: DisposableStore, handleValue?: (value: T) => void): IObservable<T>;
71 >
72 > /**
73 > * Makes sure this value is cached.
74 > */
75 > keepObserved(store: DisposableStore): IObservable<T>;
76 >
77 > /**
78 > * Creates a derived observable that depends on this observable.
79 > * Use the reader to read other observables
80 > * (see {@link ConvenientObservable.map} for the implementation).
81 > */
82 > map<TNew>(fn: (value: T, reader: IReader) => TNew): IObservable<TNew>;
83 > map<TNew>(owner: object, fn: (value: T, reader: IReader) => TNew): IObservable<TNew>;
84 >
85 > flatten<TNew>(this: IObservable<IObservable<TNew>>): IObservable<TNew>;
86 >
87 > /**
88 > * ONLY FOR DEBUGGING!
89 > * Logs computations of this derived.
90 > */
91 > log(): IObservableWithChange<T, TChange>;
92 >
93 > /**
94 > * A human-readable name for debugging purposes.
95 > */
96 > readonly debugName: string;
97 >
98 > /**
99 > * This property captures the type of the change object. Do not use it at runtime!
100 > */
101 > readonly TChange: TChange;
102 >
103 > // #endregion
104 > }
105 >
106 > /**
107 > * Represents an observer that can be subscribed to an observable.
108 > *
109 > * If an observer is subscribed to an observable and that observable didn't signal
110 > * a change through one of the observer methods, the observer can assume that the
111 > * observable didn't change.
112 > * If an observable reported a possible change, {@link IObservable.reportChanges} forces
113 > * the observable to report an actual change if there was one.
114 > */
115 > export interface IObserver {
116 > /**
117 > * Signals that the given observable might have changed and a transaction potentially modifying that observable started.
118 > * Before the given observable can call this method again, is must call {@link IObserver.endUpdate}.
119 > *
120 > * Implementations must not get/read the value of other observables, as they might not have received this event yet!
121 > * The method {@link IObservable.reportChanges} can be used to force the observable to report the changes.
122 > */
123 > beginUpdate<T>(observable: IObservable<T>): void;
124 >
125 > /**
126 > * Signals that the transaction that potentially modified the given observable ended.
127 > * This is a good place to react to (potential) changes.
128 > */
129 > endUpdate<T>(observable: IObservable<T>): void;
130 >
131 > /**
132 > * Signals that the given observable might have changed.
133 > * The method {@link IObservable.reportChanges} can be used to force the observable to report the changes.
134 > *
135 > * Implementations must not get/read the value of other observables, as they might not have received this event yet!
136 > * The change should be processed lazily or in {@link IObserver.endUpdate}.
137 > */
138 > handlePossibleChange<T>(observable: IObservable<T>): void;
139 >
140 > /**
141 > * Signals that the given {@link observable} changed.
142 > *
143 > * Implementations must not get/read the value of other observables, as they might not have received this event yet!
144 > * The change should be processed lazily or in {@link IObserver.endUpdate}.
145 > *
146 > * @param change Indicates how or why the value changed.
147 > */
148 > handleChange<T, TChange>(observable: IObservableWithChange<T, TChange>, change: TChange): void;
149 > }
150 >
151 > /**
152 > * A reader allows code to track what it depends on, so the caller knows when the computed value or produced side-effect is no longer valid.
153 > * Use `derived(reader => ...)` to turn code that needs a reader into an observable value.
154 > */
155 > export interface IReader {
156 > /**
157 > * Reads the value of an observable and subscribes to it.
158 > */
159 > readObservable<T>(observable: IObservableWithChange<T, any>): T;
160 > }
161 >
162 > export interface ISettable<T, TChange = void> {
163 > /**
164 > * Sets the value of the observable.
165 > * Use a transaction to batch multiple changes (with a transaction, observers only react at the end of the transaction).
166 > *
167 > * @param transaction When given, value changes are handled on demand or when the transaction ends.
168 > * @param change Describes how or why the value changed.
169 > */
170 > set(value: T, transaction: ITransaction | undefined, change: TChange): void;
171 > }
172 >
173 > export interface ITransaction {
174 > /**
175 > * Calls {@link Observer.beginUpdate} immediately
176 > * and {@link Observer.endUpdate} when the transaction ends.
177 > */
178 > updateObserver(observer: IObserver, observable: IObservableWithChange<any, any>): void;
179 > }
180 >
181 > /**
182 > * This function is used to indicate that the caller recovered from an error that indicates a bug.
183 > */
184 > export function handleBugIndicatingErrorRecovery(message: string) {
185 const err = new Error('BugIndicatingErrorRecovery: ' + message);
186 onUnexpectedError(err);
187 console.error('recovered from an error that indicates a bug', err);
188 }
189 > base.ts
190 > /**
191 > * A settable observable.
192 > */
193 > export interface ISettableObservable<T, TChange = void> extends IObservableWithChange<T, TChange>, ISettable<T, TChange> {
194 > }
195 >
196 > export interface IReaderWithStore extends IReader {
197 > /**
198 > * Items in this store get disposed just before the observable recomputes/reruns or when it becomes unobserved.
199 > */
200 > get store(): DisposableStore;
201 >
202 > /**
203 > * Items in this store get disposed just after the observable recomputes/reruns or when it becomes unobserved.
204 > * This is important if the current run needs the undisposed result from the last run.
205 > *
206 > * Warning: Items in this store might still get disposed before dependents (that read the now disposed value in the past) are recomputed with the new (undisposed) value!
207 > * A clean solution for this is ref counting.
208 > */
209 > get delayedStore(): DisposableStore;
210 > }
src/vs/base/common/observableInternal/logging/debugger/devToolsLogger.ts 204 covered LOC · 24 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- devToolsLogger.ts
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 > import { AutorunObserver, AutorunState } from '../../reactions/autorunImpl.js';
7 > import { TransactionImpl } from '../../transaction.js';
8 > import { IChangeInformation, IObservableLogger } from '../logging.js';
9 > import { formatValue } from '../consoleObservableLogger.js';
10 > import { ObsDebuggerApi, IObsDeclaration, ObsInstanceId, ObsStateUpdate, ITransactionState, ObserverInstanceState } from './debuggerApi.js';
11 > import { registerDebugChannel } from './debuggerRpc.js';
12 > import { deepAssign, deepAssignDeleteNulls, Throttler } from './utils.js';
13 > import { isDefined } from '../../../types.js';
14 > import { FromEventObservable } from '../../observables/observableFromEvent.js';
15 > import { BugIndicatingError, onUnexpectedError } from '../../../errors.js';
16 > import { IObservable, IObserver } from '../../base.js';
17 > import { BaseObservable } from '../../observables/baseObservable.js';
18 > import { Derived, DerivedState } from '../../observables/derivedImpl.js';
19 > import { ObservableValue } from '../../observables/observableValue.js';
20 > import { DebugLocation } from '../../debugLocation.js';
21 >
22 > interface IInstanceInfo {
23 > declarationId: number;
24 > instanceId: number;
25 > }
26 >
27 > interface IObservableInfo extends IInstanceInfo {
28 > listenerCount: number;
29 > lastValue: string | undefined;
30 > updateCount: number;
31 > changedObservables: Set<IObservable<any>>;
32 > }
33 >
34 > interface IAutorunInfo extends IInstanceInfo {
35 > updateCount: number;
36 > changedObservables: Set<IObservable<any>>;
37 > }
38 >
39 > export class DevToolsLogger implements IObservableLogger {
40 > private static _instance: DevToolsLogger | undefined = undefined;
41 > public static getInstance(): DevToolsLogger {
42 if (DevToolsLogger._instance === undefined) {
43 DevToolsLogger._instance = new DevToolsLogger();
45 return DevToolsLogger._instance;
46 }
48 > private _declarationId = 0;
49 > private _instanceId = 0;
50 >
51 > private readonly _declarations = new Map</* declarationId + type */string, IObsDeclaration>();
52 > private readonly _instanceInfos = new WeakMap<object, IObservableInfo | IAutorunInfo>();
53 > private readonly _aliveInstances = new Map<ObsInstanceId, IObservable<any> | AutorunObserver>();
54 > private readonly _activeTransactions = new Set<TransactionImpl>();
55 >
56 > private readonly _channel = registerDebugChannel<ObsDebuggerApi>('observableDevTools', () => {
57 > return {
58 > notifications: {
59 > setDeclarationIdFilter: declarationIds => {
60 >
61 > },
62 > logObservableValue: (observableId) => {
63 > console.log('logObservableValue', observableId);
64 > },
65 > flushUpdates: () => {
66 > this._flushUpdates();
67 > },
68 > resetUpdates: () => {
69 > this._pendingChanges = null;
70 > this._channel.api.notifications.handleChange(this._fullState, true);
71 > },
72 > },
73 > requests: {
74 > getDeclarations: () => {
75 > const result: Record<string, IObsDeclaration> = {};
76 > for (const decl of this._declarations.values()) {
77 > result[decl.id] = decl;
78 > }
79 > return { decls: result };
80 > },
81 > getSummarizedInstances: () => {
82 > return null!;
83 > },
84 > getObservableValueInfo: instanceId => {
85 > const obs = this._aliveInstances.get(instanceId) as BaseObservable<any>;
86 > return {
87 > observers: [...obs.debugGetObservers()].map(d => this._formatObserver(d)).filter(isDefined),
88 > };
89 > },
90 > getDerivedInfo: instanceId => {
91 > const d = this._aliveInstances.get(instanceId) as Derived<any>;
92 > return {
93 > dependencies: [...d.debugGetState().dependencies].map(d => this._formatObservable(d)).filter(isDefined),
94 > observers: [...d.debugGetObservers()].map(d => this._formatObserver(d)).filter(isDefined),
95 > };
96 > },
97 > getAutorunInfo: instanceId => {
98 > const obs = this._aliveInstances.get(instanceId) as AutorunObserver;
99 > return {
100 > dependencies: [...obs.debugGetState().dependencies].map(d => this._formatObservable(d)).filter(isDefined),
101 > };
102 > },
103 > getTransactionState: () => {
104 > return this.getTransactionState();
105 > },
106 > setValue: (instanceId, jsonValue) => {
107 > const obs = this._aliveInstances.get(instanceId) as BaseObservable<any>;
108 >
109 > if (obs instanceof Derived) {
110 > obs.debugSetValue(jsonValue);
111 > } else if (obs instanceof ObservableValue) {
112 > obs.debugSetValue(jsonValue);
113 > } else if (obs instanceof FromEventObservable) {
114 > obs.debugSetValue(jsonValue);
115 > } else {
116 > throw new BugIndicatingError('Observable is not supported');
117 > }
118 >
119 > const observers = [...obs.debugGetObservers()];
120 > for (const d of observers) {
121 > d.beginUpdate(obs);
122 > }
123 > for (const d of observers) {
124 > d.handleChange(obs, undefined);
125 > }
126 > for (const d of observers) {
127 > d.endUpdate(obs);
128 > }
129 > },
130 > getValue: instanceId => {
131 > const obs = this._aliveInstances.get(instanceId) as BaseObservable<any>;
132 > if (obs instanceof Derived) {
133 > return formatValue(obs.debugGetState().value, 200);
134 > } else if (obs instanceof ObservableValue) {
135 > return formatValue(obs.debugGetState().value, 200);
136 > }
137 >
138 > return undefined;
139 > },
140 > logValue: (instanceId) => {
141 > const obs = this._aliveInstances.get(instanceId);
142 > if (obs && 'get' in obs) {
143 > console.log('Logged Value:', obs.get());
144 > } else {
145 > throw new BugIndicatingError('Observable is not supported');
146 > }
147 > },
148 > rerun: (instanceId) => {
149 > const obs = this._aliveInstances.get(instanceId);
150 > if (obs instanceof Derived) {
151 > obs.debugRecompute();
152 > } else if (obs instanceof AutorunObserver) {
153 > obs.debugRerun();
154 > } else {
155 > throw new BugIndicatingError('Observable is not supported');
156 > }
157 > },
158 > }
159 > };
160 > });
161 >
162 > private getTransactionState(): ITransactionState | undefined {
163 const affected: ObserverInstanceState[] = [];
164 const txs = [...this._activeTransactions];
188 return { names: txs.map(t => t.getDebugName() ?? 'tx'), affected };
189 }
191 > private _getObservableInfo(observable: IObservable<any>): IObservableInfo | undefined {
192 const info = this._instanceInfos.get(observable);
193 if (!info) {
197 return info as IObservableInfo;
198 }
200 > private _getAutorunInfo(autorun: AutorunObserver): IAutorunInfo | undefined {
201 const info = this._instanceInfos.get(autorun);
202 if (!info) {
206 return info as IAutorunInfo;
207 }
209 > private _getInfo(observer: IObserver, queue: (observer: IObserver) => void): ObserverInstanceState | undefined {
210 if (observer instanceof Derived) {
211 const observersToUpdate = [...observer.debugGetObservers()];
255 return undefined;
256 }
258 > private _formatObservable(obs: IObservable<any>): { name: string; instanceId: ObsInstanceId } | undefined {
259 const info = this._getObservableInfo(obs);
260 if (!info) { return undefined; }
261 return { name: obs.debugName, instanceId: info.instanceId };
262 }
264 > private _formatObserver(obs: IObserver): { name: string; instanceId: ObsInstanceId } | undefined {
265 if (obs instanceof Derived) {
266 return { name: obs.toString(), instanceId: this._getObservableInfo(obs)?.instanceId! };
273 return undefined;
274 }
276 > private constructor() {
277 DebugLocation.enable();
278 }
280 > private _pendingChanges: ObsStateUpdate | null = null;
281 > private readonly _changeThrottler = new Throttler();
282 >
283 > private readonly _fullState = {};
284 >
285 > private _handleChange(update: ObsStateUpdate): void {
286 deepAssignDeleteNulls(this._fullState, update);
287
294 this._changeThrottler.throttle(this._flushUpdates, 10);
295 }
297 > private readonly _flushUpdates = () => {
298 > if (this._pendingChanges !== null) {
299 > this._channel.api.notifications.handleChange(this._pendingChanges, false);
300 > this._pendingChanges = null;
301 > }
302 > };
303 >
304 > private _getDeclarationId(type: IObsDeclaration['type'], location: DebugLocation): number {
305 if (!location) {
306 return -1;
322 return decInfo.id;
323 }
325 > handleObservableCreated(observable: IObservable<any>, location: DebugLocation): void {
326 const declarationId = this._getDeclarationId('observable/value', location);
327
336 this._instanceInfos.set(observable, info);
337 }
339 > handleOnListenerCountChanged(observable: IObservable<any>, newCount: number): void {
340 const info = this._getObservableInfo(observable);
341 if (!info) { return; }
364 info.listenerCount = newCount;
365 }
367 > handleObservableUpdated(observable: IObservable<any>, changeInfo: IChangeInformation): void {
368 if (observable instanceof Derived) {
369 this._handleDerivedRecomputed(observable, changeInfo);
383 }
384 }
386 > handleAutorunCreated(autorun: AutorunObserver, location: DebugLocation): void {
387 const declarationId = this._getDeclarationId('autorun', location);
388 const info: IAutorunInfo = {
408 }
409 }
410 > handleAutorunDisposed(autorun: AutorunObserver): void { devToolsLogger.ts
411 const info = this._getAutorunInfo(autorun);
412 if (!info) { return; }
418 this._aliveInstances.delete(info.instanceId);
419 }
420 > handleAutorunDependencyChanged(autorun: AutorunObserver, observable: IObservable<any>, change: unknown): void { devToolsLogger.ts
421 const info = this._getAutorunInfo(autorun);
422 if (!info) { return; }
424 info.changedObservables.add(observable);
425 }
426 > handleAutorunStarted(autorun: AutorunObserver): void { devToolsLogger.ts
427
428 }
429 > handleAutorunFinished(autorun: AutorunObserver): void { devToolsLogger.ts
430 const info = this._getAutorunInfo(autorun);
431 if (!info) { return; }
437 });
438 }
440 > handleDerivedDependencyChanged(derived: Derived<any>, observable: IObservable<any>, change: unknown): void {
441 const info = this._getObservableInfo(derived);
442 if (info) {
444 }
445 }
446 > _handleDerivedRecomputed(observable: Derived<any>, changeInfo: IChangeInformation): void { devToolsLogger.ts
447 const info = this._getObservableInfo(observable);
448 if (!info) { return; }
459 }
460 }
461 > handleDerivedCleared(observable: Derived<any>): void { devToolsLogger.ts
462 const info = this._getObservableInfo(observable);
463 if (!info) { return; }
475 }
476 }
477 > handleBeginTransaction(transaction: TransactionImpl): void { devToolsLogger.ts
478 this._activeTransactions.add(transaction);
479 }
480 > handleEndTransaction(transaction: TransactionImpl): void { devToolsLogger.ts
481 this._activeTransactions.delete(transaction);
482 }
src/vs/platform/configuration/common/configuration.ts 203 covered LOC · 12 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- configuration.ts
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 > import { assertNever } from '../../../base/common/assert.js';
7 > import { IStringDictionary } from '../../../base/common/collections.js';
8 > import { Event } from '../../../base/common/event.js';
9 > import * as types from '../../../base/common/types.js';
10 > import { URI, UriComponents } from '../../../base/common/uri.js';
11 > import { createDecorator } from '../../instantiation/common/instantiation.js';
12 > import { IWorkspaceFolder } from '../../workspace/common/workspace.js';
13 >
14 > export const IConfigurationService = createDecorator<IConfigurationService>('configurationService');
15 >
16 > export function isConfigurationOverrides(obj: unknown): obj is IConfigurationOverrides {
17 const thing = obj as IConfigurationOverrides;
18 return thing
21 && (!thing.resource || thing.resource instanceof URI);
22 }
24 > export interface IConfigurationOverrides {
25 > overrideIdentifier?: string | null;
26 > resource?: URI | null;
27 > }
28 >
29 > export function isConfigurationUpdateOverrides(obj: unknown): obj is IConfigurationUpdateOverrides {
30 const thing = obj as IConfigurationUpdateOverrides | IConfigurationOverrides;
31 return thing
35 && (!thing.resource || thing.resource instanceof URI);
36 }
38 > export type IConfigurationUpdateOverrides = Omit<IConfigurationOverrides, 'overrideIdentifier'> & { overrideIdentifiers?: string[] | null };
39 >
40 > export const enum ConfigurationTarget {
41 > APPLICATION = 1,
42 > USER,
43 > USER_LOCAL,
44 > USER_REMOTE,
45 > WORKSPACE,
46 > WORKSPACE_FOLDER,
47 > DEFAULT,
48 > MEMORY
49 > }
50 > export function ConfigurationTargetToString(configurationTarget: ConfigurationTarget) {
51 switch (configurationTarget) {
52 case ConfigurationTarget.APPLICATION: return 'APPLICATION';
60 }
61 }
63 > export interface IConfigurationChange {
64 > keys: string[];
65 > overrides: [string, string[]][];
66 > }
67 >
68 > export interface IConfigurationChangeEvent {
69 >
70 > readonly source: ConfigurationTarget;
71 > readonly affectedKeys: ReadonlySet<string>;
72 > readonly change: IConfigurationChange;
73 >
74 > affectsConfiguration(configuration: string, overrides?: IConfigurationOverrides): boolean;
75 > }
76 >
77 > export interface IInspectValue<T> {
78 > readonly value?: T;
79 > readonly override?: T;
80 > readonly overrides?: { readonly identifiers: string[]; readonly value: T }[];
81 > }
82 >
83 > export interface IConfigurationValue<T> {
84 >
85 > readonly defaultValue?: T;
86 > readonly applicationValue?: T;
87 > readonly userValue?: T;
88 > readonly userLocalValue?: T;
89 > readonly userRemoteValue?: T;
90 > readonly workspaceValue?: T;
91 > readonly workspaceFolderValue?: T;
92 > readonly memoryValue?: T;
93 > readonly policyValue?: T;
94 > readonly value?: T;
95 >
96 > readonly default?: IInspectValue<T>;
97 > readonly application?: IInspectValue<T>;
98 > readonly user?: IInspectValue<T>;
99 > readonly userLocal?: IInspectValue<T>;
100 > readonly userRemote?: IInspectValue<T>;
101 > readonly workspace?: IInspectValue<T>;
102 > readonly workspaceFolder?: IInspectValue<T>;
103 > readonly memory?: IInspectValue<T>;
104 > readonly policy?: { value?: T };
105 >
106 > readonly overrideIdentifiers?: string[];
107 > }
108 >
109 > export function getConfigValueInTarget<T>(configValue: IConfigurationValue<T>, scope: ConfigurationTarget): T | undefined {
110 switch (scope) {
111 case ConfigurationTarget.APPLICATION:
129 }
130 }
132 > export function isConfigured<T>(configValue: IConfigurationValue<T>): configValue is IConfigurationValue<T> & { value: T } {
133 return configValue.applicationValue !== undefined ||
134 configValue.userValue !== undefined ||
138 configValue.workspaceFolderValue !== undefined;
139 }
141 > export interface IConfigurationUpdateOptions {
142 > /**
143 > * If `true`, do not notifies the error to user by showing the message box. Default is `false`.
144 > */
145 > donotNotifyError?: boolean;
146 > /**
147 > * How to handle dirty file when updating the configuration.
148 > */
149 > handleDirtyFile?: 'save' | 'revert';
150 > }
151 >
152 > export interface IConfigurationService {
153 > readonly _serviceBrand: undefined;
154 >
155 > readonly onDidChangeConfiguration: Event<IConfigurationChangeEvent>;
156 >
157 > getConfigurationData(): IConfigurationData | null;
158 >
159 > /**
160 > * Fetches the value of the section for the given overrides.
161 > * Value can be of native type or an object keyed off the section name.
162 > *
163 > * @param section - Section of the configuration. Can be `null` or `undefined`.
164 > * @param overrides - Overrides that has to be applied while fetching
165 > *
166 > */
167 > getValue<T>(): T;
168 > getValue<T>(section: string): T;
169 > getValue<T>(overrides: IConfigurationOverrides): T;
170 > getValue<T>(section: string, overrides: IConfigurationOverrides): T;
171 >
172 > /**
173 > * Update a configuration value.
174 > *
175 > * Use `target` to update the configuration in a specific `ConfigurationTarget`.
176 > *
177 > * Use `overrides` to update the configuration for a resource or for override identifiers or both.
178 > *
179 > * Passing a resource through overrides will update the configuration in the workspace folder containing that resource.
180 > *
181 > * *Note 1:* Updating configuration to a default value will remove the configuration from the requested target. If not target is passed, it will be removed from all writeable targets.
182 > *
183 > * *Note 2:* Use `undefined` value to remove the configuration from the given target. If not target is passed, it will be removed from all writeable targets.
184 > *
185 > * Use `donotNotifyError` and set it to `true` to surpresss errors.
186 > *
187 > * @param key setting to be updated
188 > * @param value The new value
189 > */
190 > updateValue(key: string, value: unknown): Promise<void>;
191 > updateValue(key: string, value: unknown, target: ConfigurationTarget): Promise<void>;
192 > updateValue(key: string, value: unknown, overrides: IConfigurationOverrides | IConfigurationUpdateOverrides): Promise<void>;
193 > updateValue(key: string, value: unknown, overrides: IConfigurationOverrides | IConfigurationUpdateOverrides, target: ConfigurationTarget, options?: IConfigurationUpdateOptions): Promise<void>;
194 >
195 > inspect<T>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<Readonly<T>>;
196 >
197 > reloadConfiguration(target?: ConfigurationTarget | IWorkspaceFolder): Promise<void>;
198 >
199 > keys(): {
200 > default: string[];
201 > policy: string[];
202 > user: string[];
203 > workspace: string[];
204 > workspaceFolder: string[];
205 > memory?: string[];
206 > };
207 > }
208 >
209 > export interface IConfigurationModel {
210 > contents: IStringDictionary<unknown>;
211 > keys: string[];
212 > overrides: IOverrides[];
213 > raw?: ReadonlyArray<IStringDictionary<unknown>> | IStringDictionary<unknown>;
214 > }
215 >
216 > export interface IOverrides {
217 > keys: string[];
218 > contents: IStringDictionary<unknown>;
219 > identifiers: string[];
220 > }
221 >
222 > export interface IConfigurationData {
223 > defaults: IConfigurationModel;
224 > policy: IConfigurationModel;
225 > application: IConfigurationModel;
226 > userLocal: IConfigurationModel;
227 > userRemote: IConfigurationModel;
228 > workspace: IConfigurationModel;
229 > folders: [UriComponents, IConfigurationModel][];
230 > }
231 >
232 > export interface IConfigurationCompareResult {
233 > added: string[];
234 > removed: string[];
235 > updated: string[];
236 > overrides: [string, string[]][];
237 > }
238 >
239 > export function toValuesTree(properties: IStringDictionary<unknown>, conflictReporter: (message: string) => void): IStringDictionary<unknown> {
240 const root = Object.create(null);
241
246 return root;
247 }
249 > export function addToValueTree(settingsTreeRoot: IStringDictionary<unknown>, key: string, value: unknown, conflictReporter: (message: string) => void): void {
250 const segments = key.split('.');
251 const last = segments.pop()!;
282 }
283 }
285 > export function removeFromValueTree(valueTree: IStringDictionary<unknown>, key: string): void {
286 const segments = key.split('.');
287 doRemoveFromValueTree(valueTree, segments);
288 }
290 function doRemoveFromValueTree(valueTree: IStringDictionary<unknown> | unknown, segments: string[]): void {
291 if (!valueTree) {
311 }
312 }
314 > /**
315 > * A helper function to get the configuration value with a specific settings path (e.g. config.some.setting)
316 > */
317 > export function getConfigurationValue<T>(config: IStringDictionary<unknown>, settingPath: string): T | undefined;
318 > export function getConfigurationValue<T>(config: IStringDictionary<unknown>, settingPath: string, defaultValue: T): T;
319 > export function getConfigurationValue<T>(config: IStringDictionary<unknown>, settingPath: string, defaultValue?: T): T | undefined {
320 function accessSetting(config: IStringDictionary<unknown>, path: string[]): unknown {
321 let current: unknown = config;
334 return typeof result === 'undefined' ? defaultValue : result as T;
335 }
337 > export function merge(base: IStringDictionary<unknown>, add: IStringDictionary<unknown>, overwrite: boolean): void {
338 Object.keys(add).forEach(key => {
339 if (key !== '__proto__') {
350 });
351 }
353 > export function getLanguageTagSettingPlainKey(settingKey: string) {
354 return settingKey
355 .replace(/^\[/, '')
src/vs/platform/contextkey/common/scanner.ts 203 covered LOC · 58 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- scanner.ts
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 > import { CharCode } from '../../../base/common/charCode.js';
7 > import { illegalState } from '../../../base/common/errors.js';
8 > import { localize } from '../../../nls.js';
9 >
10 > export const enum TokenType {
11 > LParen,
12 > RParen,
13 > Neg,
14 > Eq,
15 > NotEq,
16 > Lt,
17 > LtEq,
18 > Gt,
19 > GtEq,
20 > RegexOp,
21 > RegexStr,
22 > True,
23 > False,
24 > In,
25 > Not,
26 > And,
27 > Or,
28 > Str,
29 > QuotedStr,
30 > Error,
31 > EOF,
32 > }
33 >
34 > export type Token =
35 > | { type: TokenType.LParen; offset: number }
36 > | { type: TokenType.RParen; offset: number }
37 > | { type: TokenType.Neg; offset: number }
38 > | { type: TokenType.Eq; offset: number; isTripleEq: boolean }
39 > | { type: TokenType.NotEq; offset: number; isTripleEq: boolean }
40 > | { type: TokenType.Lt; offset: number }
41 > | { type: TokenType.LtEq; offset: number }
42 > | { type: TokenType.Gt; offset: number }
43 > | { type: TokenType.GtEq; offset: number }
44 > | { type: TokenType.RegexOp; offset: number }
45 > | { type: TokenType.RegexStr; offset: number; lexeme: string }
46 > | { type: TokenType.True; offset: number }
47 > | { type: TokenType.False; offset: number }
48 > | { type: TokenType.In; offset: number }
49 > | { type: TokenType.Not; offset: number }
50 > | { type: TokenType.And; offset: number }
51 > | { type: TokenType.Or; offset: number }
52 > | { type: TokenType.Str; offset: number; lexeme: string }
53 > | { type: TokenType.QuotedStr; offset: number; lexeme: string }
54 > | { type: TokenType.Error; offset: number; lexeme: string }
55 > | { type: TokenType.EOF; offset: number };
56 >
57 > type KeywordTokenType = TokenType.Not | TokenType.In | TokenType.False | TokenType.True;
58 > type TokenTypeWithoutLexeme =
59 > TokenType.LParen |
60 > TokenType.RParen |
61 > TokenType.Neg |
62 > TokenType.Lt |
63 > TokenType.LtEq |
64 > TokenType.Gt |
65 > TokenType.GtEq |
66 > TokenType.RegexOp |
67 > TokenType.True |
68 > TokenType.False |
69 > TokenType.In |
70 > TokenType.Not |
71 > TokenType.And |
72 > TokenType.Or |
73 > TokenType.EOF;
74 >
75 > /**
76 > * Example:
77 > * `foo == bar'` - note how single quote doesn't have a corresponding closing quote,
78 > * so it's reported as unexpected
79 > */
80 > export type LexingError = {
81 > offset: number; /** note that this doesn't take into account escape characters from the original encoding of the string, e.g., within an extension manifest file's JSON encoding */
82 > lexeme: string;
83 > additionalInfo?: string;
84 > };
85 >
86 function hintDidYouMean(...meant: string[]) {
87 switch (meant.length) {
96 }
97 }
98 > scanner.ts
99 > const hintDidYouForgetToOpenOrCloseQuote = localize('contextkey.scanner.hint.didYouForgetToOpenOrCloseQuote', "Did you forget to open or close the quote?");
100 > const hintDidYouForgetToEscapeSlash = localize('contextkey.scanner.hint.didYouForgetToEscapeSlash', "Did you forget to escape the '/' (slash) character? Put two backslashes before it to escape, e.g., '\\\\/\'.");
101 >
102 > /**
103 > * A simple scanner for context keys.
104 > *
105 > * Example:
106 > *
107 > * ```ts
108 > * const scanner = new Scanner().reset('resourceFileName =~ /docker/ && !config.docker.enabled');
109 > * const tokens = [...scanner];
110 > * if (scanner.errorTokens.length > 0) {
111 > * scanner.errorTokens.forEach(err => console.error(`Unexpected token at ${err.offset}: ${err.lexeme}\nHint: ${err.additional}`));
112 > * } else {
113 > * // process tokens
114 > * }
115 > * ```
116 > */
117 > export class Scanner {
118 >
119 > static getLexeme(token: Token): string {
120 > switch (token.type) {
121 > case TokenType.LParen:
122 > return '('; scanner.ts
123 > case TokenType.RParen: scanner.ts
124 > return ')'; scanner.ts
125 > case TokenType.Neg: scanner.ts
126 > return '!'; scanner.ts
127 > case TokenType.Eq: scanner.ts
128 > return token.isTripleEq ? '===' : '=='; scanner.ts
129 > case TokenType.NotEq: scanner.ts
130 > return token.isTripleEq ? '!==' : '!='; scanner.ts
131 > case TokenType.Lt: scanner.ts
132 > return '<'; scanner.ts
133 > case TokenType.LtEq: scanner.ts
134 > return '<='; scanner.ts
135 > case TokenType.Gt: scanner.ts
136 > return '>'; scanner.ts
137 > case TokenType.GtEq: scanner.ts
138 > return '>='; scanner.ts
139 > case TokenType.RegexOp: scanner.ts
140 > return '=~'; scanner.ts
141 > case TokenType.RegexStr: scanner.ts
142 > return token.lexeme; scanner.ts
143 > case TokenType.True: scanner.ts
144 > return 'true'; scanner.ts
145 > case TokenType.False: scanner.ts
146 > return 'false'; scanner.ts
147 > case TokenType.In: scanner.ts
148 > return 'in'; scanner.ts
149 > case TokenType.Not: scanner.ts
150 > return 'not'; scanner.ts
151 > case TokenType.And: scanner.ts
152 > return '&&'; scanner.ts
153 > case TokenType.Or: scanner.ts
154 > return '||'; scanner.ts
155 > case TokenType.Str: scanner.ts
156 > return token.lexeme; scanner.ts
157 > case TokenType.QuotedStr: scanner.ts
158 > return token.lexeme; scanner.ts
159 > case TokenType.Error: scanner.ts
160 > return token.lexeme; scanner.ts
161 > case TokenType.EOF: scanner.ts
162 > return 'EOF'; scanner.ts
163 > default: scanner.ts
164 > throw illegalState(`unhandled token type: ${JSON.stringify(token)}; have you forgotten to add a case?`); scanner.ts
165 > } scanner.ts
166 > }
167 >
168 > private static _regexFlags = new Set(['i', 'g', 's', 'm', 'y', 'u'].map(ch => ch.charCodeAt(0)));
169 >
170 > private static _keywords = new Map<string, KeywordTokenType>([
171 > ['not', TokenType.Not],
172 > ['in', TokenType.In],
173 > ['false', TokenType.False],
174 > ['true', TokenType.True],
175 > ]);
176 >
177 > private _input: string = '';
178 > private _start: number = 0;
179 > private _current: number = 0;
180 > private _tokens: Token[] = [];
181 > private _errors: LexingError[] = [];
182 >
183 > get errors(): Readonly<LexingError[]> {
184 return this._errors;
185 }
186 > scanner.ts
187 > reset(value: string) {
188 this._input = value;
189
195 return this;
196 }
197 > scanner.ts
198 > scan() {
199 while (!this._isAtEnd()) {
200
267 return Array.from(this._tokens);
268 }
269 > scanner.ts
270 > private _match(expected: number): boolean {
271 if (this._isAtEnd()) {
272 return false;
278 return true;
279 }
280 > scanner.ts
281 > private _advance(): number {
282 return this._input.charCodeAt(this._current++);
283 }
284 > scanner.ts
285 > private _peek(): number {
286 return this._isAtEnd() ? CharCode.Null : this._input.charCodeAt(this._current);
287 }
288 > scanner.ts
289 > private _addToken(type: TokenTypeWithoutLexeme) {
290 this._tokens.push({ type, offset: this._start });
291 }
292 > scanner.ts
293 > private _error(additional?: string) {
294 const offset = this._start;
295 const lexeme = this._input.substring(this._start, this._current);
298 this._tokens.push(errToken);
299 }
300 > scanner.ts
301 > // u - unicode, y - sticky // TODO@ulugbekna: we accept double quotes as part of the string rather than as a delimiter (to preserve old parser's behavior)
302 > private stringRe = /[a-zA-Z0-9_<>\-\./\\:\*\?\+\[\]\^,#@;"%\$\p{L}-]+/uy;
303 > private _string() {
304 this.stringRe.lastIndex = this._start;
305 const match = this.stringRe.exec(this._input);
315 }
316 }
317 > scanner.ts
318 > // captures the lexeme without the leading and trailing '
319 > private _quotedString() {
320 while (this._peek() !== CharCode.SingleQuote && !this._isAtEnd()) { // TODO@ulugbekna: add support for escaping ' ?
321 this._advance();
332 this._tokens.push({ type: TokenType.QuotedStr, lexeme: this._input.substring(this._start + 1, this._current - 1), offset: this._start + 1 });
333 }
334 > scanner.ts
335 > /*
336 > * Lexing a regex expression: /.../[igsmyu]*
337 > * Based on https://github.com/microsoft/TypeScript/blob/9247ef115e617805983740ba795d7a8164babf89/src/compiler/scanner.ts#L2129-L2181
338 > *
339 > * Note that we want slashes within a regex to be escaped, e.g., /file:\\/\\/\\// should match `file:///`
340 > */
341 > private _regex() {
342 let p = this._current;
343
378 this._tokens.push({ type: TokenType.RegexStr, lexeme, offset: this._start });
379 }
380 > scanner.ts
381 > private _isAtEnd() {
382 return this._current >= this._input.length;
383 }
384 > } scanner.ts
src/vs/platform/telemetry/common/telemetryUtils.ts 199 covered LOC · 15 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- telemetryUtils.ts
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 > import { cloneAndChange, safeStringify } from '../../../base/common/objects.js';
7 > import { isObject } from '../../../base/common/types.js';
8 > import { URI } from '../../../base/common/uri.js';
9 > import { localize } from '../../../nls.js';
10 > import { IConfigurationService } from '../../configuration/common/configuration.js';
11 > import { IEnvironmentService } from '../../environment/common/environment.js';
12 > import { LoggerGroup } from '../../log/common/log.js';
13 > import { IProductService } from '../../product/common/productService.js';
14 > import { getRemoteName } from '../../remote/common/remoteHosts.js';
15 > import { verifyMicrosoftInternalDomain } from './commonProperties.js';
16 > import { ICustomEndpointTelemetryService, ITelemetryData, ITelemetryEndpoint, ITelemetryService, TelemetryConfiguration, TelemetryLevel, TELEMETRY_CRASH_REPORTER_SETTING_ID, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SETTING_ID } from './telemetry.js';
17 >
18 > /**
19 > * A special class used to denoting a telemetry value which should not be clean.
20 > * This is because that value is "Trusted" not to contain identifiable information such as paths.
21 > * NOTE: This is used as an API type as well, and should not be changed.
22 > */
23 > export class TelemetryTrustedValue<T> {
24 > // This is merely used as an identifier as the instance will be lost during serialization over the exthost
25 > public readonly isTrustedTelemetryValue = true;
26 > constructor(public readonly value: T) { }
27 > }
28 >
29 > export class NullTelemetryServiceShape implements ITelemetryService {
30 > declare readonly _serviceBrand: undefined;
31 > readonly telemetryLevel = TelemetryLevel.NONE;
32 > readonly sessionId = 'someValue.sessionId';
33 > readonly machineId = 'someValue.machineId';
34 > readonly sqmId = 'someValue.sqmId';
35 > readonly devDeviceId = 'someValue.devDeviceId';
36 > readonly firstSessionDate = 'someValue.firstSessionDate';
37 > readonly sendErrorTelemetry = false;
38 > publicLog() { }
39 > publicLog2() { }
40 > publicLogError() { }
41 > publicLogError2() { }
42 > setExperimentProperty() { }
43 > setCommonProperty() { }
44 > }
45 >
46 > export const NullTelemetryService = new NullTelemetryServiceShape();
47 >
48 > export class NullEndpointTelemetryService implements ICustomEndpointTelemetryService {
49 > _serviceBrand: undefined;
50 >
51 > async publicLog(_endpoint: ITelemetryEndpoint, _eventName: string, _data?: ITelemetryData): Promise<void> {
52 // noop
53 }
55 > async publicLogError(_endpoint: ITelemetryEndpoint, _errorEventName: string, _data?: ITelemetryData): Promise<void> {
56 // noop
57 }
59 >
60 > export const telemetryLogId = 'telemetry';
61 > export const TelemetryLogGroup: LoggerGroup = { id: telemetryLogId, name: localize('telemetryLogName', "Telemetry") };
62 >
63 > export interface ITelemetryAppender {
64 > log(eventName: string, data: ITelemetryData): void;
65 > flush(): Promise<void>;
66 > }
67 >
68 > export const NullAppender: ITelemetryAppender = { log: () => null, flush: () => Promise.resolve(undefined) };
69 >
70 >
71 > /* __GDPR__FRAGMENT__
72 > "URIDescriptor" : {
73 > "mimeType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
74 > "scheme": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
75 > "ext": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
76 > "path": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
77 > }
78 > */
79 > export interface URIDescriptor {
80 > mimeType?: string;
81 > scheme?: string;
82 > ext?: string;
83 > path?: string;
84 > }
85 >
86 > /**
87 > * Determines whether or not we support logging telemetry.
88 > * This checks if the product is capable of collecting telemetry but not whether or not it can send it
89 > * For checking the user setting and what telemetry you can send please check `getTelemetryLevel`.
90 > * This returns true if `--disable-telemetry` wasn't used, the product.json allows for telemetry, and we're not testing an extension
91 > * If false telemetry is disabled throughout the product
92 > * @param productService
93 > * @param environmentService
94 > * @returns false - telemetry is completely disabled, true - telemetry is logged locally, but may not be sent
95 > */
96 > export function supportsTelemetry(productService: IProductService, environmentService: IEnvironmentService): boolean {
97 // If it's OSS and telemetry isn't disabled via the CLI we will allow it for logging only purposes
98 if (!environmentService.isBuilt && !environmentService.disableTelemetry) {
101 return !(environmentService.disableTelemetry || !productService.enableTelemetry);
102 }
104 > /**
105 > * Checks to see if we're in logging only mode to debug telemetry.
106 > * This is if telemetry is enabled and we're in OSS, but no telemetry key is provided so it's not being sent just logged.
107 > * @param productService
108 > * @param environmentService
109 > * @returns True if telemetry is actually disabled and we're only logging for debug purposes
110 > */
111 > export function isLoggingOnly(productService: IProductService, environmentService: IEnvironmentService): boolean {
112 // If we're testing an extension, log telemetry for debug purposes
113 if (environmentService.extensionTestsLocationURI) {
129 return true;
130 }
132 > /**
133 > * Determines how telemetry is handled based on the user's configuration.
134 > *
135 > * @param configurationService
136 > * @returns OFF, ERROR, ON
137 > */
138 > export function getTelemetryLevel(configurationService: IConfigurationService): TelemetryLevel {
139 const newConfig = configurationService.getValue<TelemetryConfiguration>(TELEMETRY_SETTING_ID);
140 const crashReporterConfig = configurationService.getValue<boolean | undefined>(TELEMETRY_CRASH_REPORTER_SETTING_ID);
158 }
159 }
161 > export interface Properties {
162 > [key: string]: string;
163 > }
164 >
165 > export interface Measurements {
166 > [key: string]: number;
167 > }
168 >
169 > export function validateTelemetryData(data?: unknown): { properties: Properties; measurements: Measurements } {
170
171 const properties: Properties = {};
204 };
205 }
207 > interface IRemoteAuthoringConfig {
208 > remoteExtensionTips?: { readonly [remoteName: string]: unknown };
209 > virtualWorkspaceExtensionTips?: { readonly [remoteName: string]: unknown };
210 > }
211 >
212 > export function cleanRemoteAuthority(remoteAuthority: string | undefined, config: IRemoteAuthoringConfig): string {
213 if (!remoteAuthority) {
214 return 'none';
229 return 'other';
230 }
232 function flatten(obj: unknown, result: Record<string, unknown>, order: number = 0, prefix?: string): void {
233 if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) {
258 }
259 }
261 > /**
262 > * Whether or not this is an internal user
263 > * @param productService The product service
264 > * @param configService The config servivce
265 > * @returns true if internal, false otherwise
266 > */
267 > export function isInternalTelemetry(productService: IProductService, configService: IConfigurationService) {
268 const msftInternalDomains = productService.msftInternalDomains || [];
269 const internalTesting = configService.getValue<boolean>('telemetry.internalTesting');
270 return verifyMicrosoftInternalDomain(msftInternalDomains) || internalTesting;
271 }
273 > interface IPathEnvironment {
274 > appRoot: string;
275 > extensionsPath: string;
276 > userDataPath: string;
277 > userHome: URI;
278 > tmpDir: URI;
279 > }
280 >
281 > export function getPiiPathsFromEnvironment(paths: IPathEnvironment): string[] {
282 return [paths.appRoot, paths.extensionsPath, paths.userHome.fsPath, paths.tmpDir.fsPath, paths.userDataPath];
283 }
285 > //#region Telemetry Cleaning
286 >
287 > /**
288 > * Cleans a given stack of possible paths
289 > * @param stack The stack to sanitize
290 > * @param cleanupPatterns Cleanup patterns to remove from the stack
291 > * @returns The cleaned stack
292 > */
293 function anonymizeFilePaths(stack: string, cleanupPatterns: RegExp[]): string {
294
356 return updatedStack;
357 }
359 > const userDataRegexes = [
360 > { label: 'URL', regex: /[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s]*/ },
361 > { label: 'Google API Key', regex: /AIza[A-Za-z0-9_\\\-]{35}/ },
362 > { label: 'JWT', regex: /eyJ[0eXAiOiJKV1Qi|hbGci|a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+/ },
363 > { label: 'Slack Token', regex: /xox[pbar]\-[A-Za-z0-9]/ },
364 > { label: 'GitHub Token', regex: /(gh[psuro]_[a-zA-Z0-9]{36}|github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59})/ },
365 > { label: 'Generic Secret', regex: /(key|token|sig|secret|signature|password|passwd|pwd|android:value)[^a-zA-Z0-9]/i },
366 > { label: 'CLI Credentials', regex: /((login|psexec|(certutil|psexec)\.exe).{1,50}(\s-u(ser(name)?)?\s+.{3,100})?\s-(admin|user|vm|root)?p(ass(word)?)?\s+["']?[^$\-\/\s]|(^|[\s\r\n\\])net(\.exe)?.{1,5}(user\s+|share\s+\/user:| user -? secrets ? set) \s + [^ $\s \/])/ },
367 > { label: 'Microsoft Entra ID', regex: /eyJ(?:0eXAiOiJKV1Qi|hbGci|[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.)/ },
368 > { label: 'Email', regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/ }
369 > ];
370 >
371 > /**
372 > * Redacts a value if it contains commonly leaked PII.
373 > * @param value The value returned (as-is) when no PII is detected
374 > * @param probe The string actually matched against the PII heuristics. Defaults
375 > * to `value`; callers may pass a value that includes a trailing delimiter (e.g. a
376 > * newline) so that heuristics relying on a non-alphanumeric boundary match the
377 > * same way they would against the original whole string.
378 > * @returns A `<REDACTED: ...>` marker if the probe matched, otherwise `value`
379 > */
380 function redactIfPossibleUserInfo(value: string, probe: string = value): string {
381 for (const secretRegex of userDataRegexes) {
386 return value;
387 }
389 > /**
390 > * Attempts to remove commonly leaked PII.
391 > *
392 > * When a match is found the check is applied per line so that a single suspicious
393 > * frame (e.g. a stack frame containing a function name such as `getStorageKey`
394 > * which matches the broad `Generic Secret` heuristic) only redacts that line —
395 > * replacing it with a `<REDACTED: ...>` marker — instead of wiping the entire
396 > * multi-line value such as a whole callstack.
397 > * @param property The property whose offending lines will be replaced with a redaction marker if they contain user data
398 > * @returns The new value for the property
399 > */
400 function removePropertiesWithPossibleUserInfo(property: string): string {
401 // If for some reason it is undefined we skip it (this shouldn't be possible);
436 return lines.join('\n');
437 }
439 >
440 > /**
441 > * Does a best possible effort to clean a data object from any possible PII.
442 > * @param data The data object to clean
443 > * @param paths Any additional patterns that should be removed from the data set
444 > * @returns A new object with the PII removed
445 > */
446 > export function cleanData(data: ITelemetryData | undefined, cleanUpPatterns: RegExp[]): Record<string, unknown> {
447 if (!data) {
448 return {};
src/vs/base/common/jsonSchema.ts 194 covered LOC · 3 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- jsonSchema.ts
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 > export type JSONSchemaType = 'string' | 'number' | 'integer' | 'boolean' | 'null' | 'array' | 'object';
7 >
8 > export interface IJSONSchema {
9 > id?: string;
10 > $id?: string;
11 > $schema?: string;
12 > type?: JSONSchemaType | JSONSchemaType[];
13 > title?: string;
14 > default?: any;
15 > definitions?: IJSONSchemaMap;
16 > description?: string;
17 > properties?: IJSONSchemaMap;
18 > patternProperties?: IJSONSchemaMap;
19 > additionalProperties?: boolean | IJSONSchema;
20 > minProperties?: number;
21 > maxProperties?: number;
22 > dependencies?: IJSONSchemaMap | { [prop: string]: string[] };
23 > items?: IJSONSchema | IJSONSchema[];
24 > minItems?: number;
25 > maxItems?: number;
26 > uniqueItems?: boolean;
27 > additionalItems?: boolean | IJSONSchema;
28 > pattern?: string;
29 > minLength?: number;
30 > maxLength?: number;
31 > minimum?: number;
32 > maximum?: number;
33 > exclusiveMinimum?: boolean | number;
34 > exclusiveMaximum?: boolean | number;
35 > multipleOf?: number;
36 > required?: string[];
37 > $ref?: string;
38 > anyOf?: IJSONSchema[];
39 > allOf?: IJSONSchema[];
40 > oneOf?: IJSONSchema[];
41 > not?: IJSONSchema;
42 > enum?: any[];
43 > format?: string;
44 >
45 > // schema draft 06
46 > const?: any;
47 > contains?: IJSONSchema;
48 > propertyNames?: IJSONSchema;
49 > examples?: any[];
50 >
51 > // schema draft 07
52 > $comment?: string;
53 > if?: IJSONSchema;
54 > then?: IJSONSchema;
55 > else?: IJSONSchema;
56 >
57 > // schema 2019-09
58 > unevaluatedProperties?: boolean | IJSONSchema;
59 > unevaluatedItems?: boolean | IJSONSchema;
60 > minContains?: number;
61 > maxContains?: number;
62 > deprecated?: boolean;
63 > dependentRequired?: { [prop: string]: string[] };
64 > dependentSchemas?: IJSONSchemaMap;
65 > $defs?: { [name: string]: IJSONSchema };
66 > $anchor?: string;
67 > $recursiveRef?: string;
68 > $recursiveAnchor?: string;
69 > $vocabulary?: any;
70 >
71 > // schema 2020-12
72 > prefixItems?: IJSONSchema[];
73 > $dynamicRef?: string;
74 > $dynamicAnchor?: string;
75 >
76 > // VSCode extensions
77 >
78 > defaultSnippets?: IJSONSchemaSnippet[];
79 > errorMessage?: string;
80 > patternErrorMessage?: string;
81 > deprecationMessage?: string;
82 > markdownDeprecationMessage?: string;
83 > enumDescriptions?: string[];
84 > markdownEnumDescriptions?: string[];
85 > markdownDescription?: string;
86 > doNotSuggest?: boolean;
87 > suggestSortText?: string;
88 > allowComments?: boolean;
89 > allowTrailingCommas?: boolean;
90 > secret?: boolean;
91 > }
92 >
93 > export interface IJSONSchemaMap {
94 > [name: string]: IJSONSchema;
95 > }
96 >
97 > export interface IJSONSchemaSnippet {
98 > label?: string;
99 > description?: string;
100 > body?: any; // a object that will be JSON stringified
101 > bodyText?: string; // an already stringified JSON object that can contain new lines (\n) and tabs (\t)
102 > }
103 >
104 > /**
105 > * Converts a basic JSON schema to a TypeScript type.
106 > */
107 > export type TypeFromJsonSchema<T> =
108 > // enum
109 > T extends { enum: infer EnumValues }
110 > ? UnionOf<EnumValues>
111 >
112 > // Object with list of required properties.
113 > // Values are required or optional based on `required` list.
114 > : T extends { type: 'object'; properties: infer P; required: infer RequiredList }
115 > ? {
116 > [K in keyof P]: IsRequired<K, RequiredList> extends true ? TypeFromJsonSchema<P[K]> : TypeFromJsonSchema<P[K]> | undefined;
117 > } & AdditionalPropertiesType<T>
118 >
119 > // Object with no required properties.
120 > // All values are optional
121 > : T extends { type: 'object'; properties: infer P }
122 > ? { [K in keyof P]: TypeFromJsonSchema<P[K]> | undefined } & AdditionalPropertiesType<T>
123 >
124 > // Array
125 > : T extends { type: 'array'; items: infer Items }
126 > ? Items extends [...infer R]
127 > // If items is an array, we treat it like a tuple
128 > ? { [K in keyof R]: TypeFromJsonSchema<Items[K]> }
129 > : Array<TypeFromJsonSchema<Items>>
130 >
131 > // oneOf / anyof
132 > // These are handled the same way as they both represent a union type.
133 > // However at the validation level, they have different semantics.
134 > : T extends { oneOf: infer I }
135 > ? MapSchemaToType<I>
136 > : T extends { anyOf: infer I }
137 > ? MapSchemaToType<I>
138 >
139 > // Primitive types
140 > : T extends { type: infer Type }
141 > // Basic type
142 > ? Type extends 'string' | 'number' | 'integer' | 'boolean' | 'null'
143 > ? SchemaPrimitiveTypeNameToType<Type>
144 > // Union of primitive types
145 > : Type extends [...infer R]
146 > ? UnionOf<{ [K in keyof R]: SchemaPrimitiveTypeNameToType<R[K]> }>
147 > : never
148 >
149 > // Fallthrough
150 > : never;
151 >
152 > type SchemaPrimitiveTypeNameToType<T> =
153 > T extends 'string' ? string :
154 > T extends 'number' | 'integer' ? number :
155 > T extends 'boolean' ? boolean :
156 > T extends 'null' ? null :
157 > never;
158 >
159 > type UnionOf<T> =
160 > T extends [infer First, ...infer Rest]
161 > ? First | UnionOf<Rest>
162 > : never;
163 >
164 > type IsRequired<K, RequiredList> =
165 > RequiredList extends []
166 > ? false
167 >
168 > : RequiredList extends [K, ...infer _]
169 > ? true
170 >
171 > : RequiredList extends [infer _, ...infer R]
172 > ? IsRequired<K, R>
173 >
174 > : false;
175 >
176 > type AdditionalPropertiesType<Schema> =
177 > Schema extends { additionalProperties: infer AP }
178 > ? AP extends false ? {} : { [key: string]: TypeFromJsonSchema<Schema['additionalProperties']> }
179 > : {};
180 >
181 > type MapSchemaToType<T> = T extends [infer First, ...infer Rest]
182 > ? TypeFromJsonSchema<First> | MapSchemaToType<Rest>
183 > : never;
184 >
185 > interface Equals { schemas: IJSONSchema[]; id?: string }
186 >
187 > export function getCompressedContent(schema: IJSONSchema): string {
188 let hasDups = false;
189
258 return str;
259 }
261 > type IJSONSchemaRef = IJSONSchema | boolean;
262 >
263 function isObject(thing: unknown): thing is object {
264 return typeof thing === 'object' && thing !== null;
265 }
267 > /*
268 > * Traverse a JSON schema and visit each schema node
269 > */
270 function traverseNodes(root: IJSONSchema, visit: (schema: IJSONSchema) => boolean) {
271 if (!root || typeof root !== 'object') {
src/vs/base/common/observableInternal/reactions/autorunImpl.ts 192 covered LOC · 45 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- autorunImpl.ts
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 > import { IObservable, IObservableWithChange, IObserver, IReaderWithStore } from '../base.js';
7 > import { DebugNameData } from '../debugName.js';
8 > import { assertFn, BugIndicatingError, DisposableStore, IDisposable, markAsDisposed, onBugIndicatingError, trackDisposable } from '../commonFacade/deps.js';
9 > import { getLogger } from '../logging/logging.js';
10 > import { IChangeTracker } from '../changeTracker.js';
11 > import { DebugLocation } from '../debugLocation.js';
12 >
13 > export const enum AutorunState {
14 > /**
15 > * A dependency could have changed.
16 > * We need to explicitly ask them if at least one dependency changed.
17 > */
18 > dependenciesMightHaveChanged = 1,
19 >
20 > /**
21 > * A dependency changed and we need to recompute.
22 > */
23 > stale = 2,
24 > upToDate = 3,
25 > }
26 >
27 function autorunStateToString(state: AutorunState): string {
28 switch (state) {
33 }
34 }
36 > export class AutorunObserver<TChangeSummary = any> implements IObserver, IReaderWithStore, IDisposable {
37 > private _state = AutorunState.stale;
38 > private _updateCount = 0;
39 > private _disposed = false;
40 > private _dependencies = new Set<IObservable<any>>();
41 > private _dependenciesToBeRemoved = new Set<IObservable<any>>();
42 > private _changeSummary: TChangeSummary | undefined;
43 > private _isRunning = false;
44 > private _iteration = 0;
45 >
46 > public get debugName(): string {
47 > return this._debugNameData.getDebugName(this) ?? '(anonymous)';
48 > }
49 >
50 > constructor(
51 > public readonly _debugNameData: DebugNameData, autorunImpl.ts
52 > public readonly _runFn: (reader: IReaderWithStore, changeSummary: TChangeSummary) => void,
53 > private readonly _changeTracker: IChangeTracker<TChangeSummary> | undefined,
54 > debugLocation: DebugLocation
55 > ) {
56 > this._changeSummary = this._changeTracker?.createChangeSummary(undefined);
57 > getLogger()?.handleAutorunCreated(this, debugLocation);
58 > this._run();
59 >
60 > trackDisposable(this);
61 > }
63 > public dispose(): void {
64 > if (this._disposed) { autorunImpl.ts
65 return;
66 }
67 > this._disposed = true; autorunImpl.ts
68 > for (const o of this._dependencies) {
69 > o.removeObserver(this); // Warning: external call!
70 > }
71 > this._dependencies.clear();
72 >
73 > if (this._store !== undefined) {
74 this._store.dispose();
75 }
76 > if (this._delayedStore !== undefined) { autorunImpl.ts
77 this._delayedStore.dispose();
78 }
80 > getLogger()?.handleAutorunDisposed(this);
81 > markAsDisposed(this);
82 > }
84 > private _run() {
85 > const emptySet = this._dependenciesToBeRemoved; autorunImpl.ts
86 > this._dependenciesToBeRemoved = this._dependencies;
87 > this._dependencies = emptySet;
88 >
89 > this._state = AutorunState.upToDate;
90 >
91 > try {
92 > if (!this._disposed) {
93 > getLogger()?.handleAutorunStarted(this);
94 > const changeSummary = this._changeSummary!;
95 > const delayedStore = this._delayedStore;
96 > if (delayedStore !== undefined) {
97 this._delayedStore = undefined;
98 }
99 > try { autorunImpl.ts
100 > this._isRunning = true;
101 > if (this._changeTracker) {
102 this._changeTracker.beforeUpdate?.(this, changeSummary);
103 this._changeSummary = this._changeTracker.createChangeSummary(changeSummary); // Warning: external call!
104 }
105 > if (this._store !== undefined) { autorunImpl.ts
106 this._store.dispose();
107 this._store = undefined;
108 }
110 > this._runFn(this, changeSummary); // Warning: external call!
111 > } catch (e) {
112 onBugIndicatingError(e);
113 > } finally { autorunImpl.ts
114 > this._isRunning = false;
115 > if (delayedStore !== undefined) {
116 delayedStore.dispose();
117 }
118 > } autorunImpl.ts
119 > }
120 > } finally {
121 > if (!this._disposed) {
122 > getLogger()?.handleAutorunFinished(this);
123 > }
124 > // We don't want our observed observables to think that they are (not even temporarily) not being observed.
125 > // Thus, we only unsubscribe from observables that are definitely not read anymore.
126 > for (const o of this._dependenciesToBeRemoved) {
127 o.removeObserver(this); // Warning: external call!
128 }
129 > this._dependenciesToBeRemoved.clear(); autorunImpl.ts
130 > }
131 > }
133 > public toString(): string {
134 return `Autorun<${this.debugName}>`;
135 }
137 > // IObserver implementation
138 > public beginUpdate(_observable: IObservable<any>): void {
139 > if (this._state === AutorunState.upToDate) { autorunImpl.ts
140 > this._checkIterations();
141 > this._state = AutorunState.dependenciesMightHaveChanged;
142 > }
143 > this._updateCount++;
144 > }
146 > public endUpdate(_observable: IObservable<any>): void {
147 > try { autorunImpl.ts
148 > if (this._updateCount === 1) {
149 > this._iteration = 1;
150 > do {
151 > if (this._checkIterations()) {
152 return;
153 }
154 > if (this._state === AutorunState.dependenciesMightHaveChanged) { autorunImpl.ts
155 this._state = AutorunState.upToDate;
156 for (const d of this._dependencies) {
162 }
163 }
165 > this._iteration++;
166 > if (this._state !== AutorunState.upToDate) {
167 > this._run(); // Warning: indirect external call! autorunImpl.ts
168 > }
169 > } while (this._state !== AutorunState.upToDate); autorunImpl.ts
170 > }
171 > } finally {
172 > this._updateCount--;
173 > }
174 >
175 > assertFn(() => this._updateCount >= 0);
176 > }
178 > public handlePossibleChange(observable: IObservable<any>): void {
179 if (this._state === AutorunState.upToDate && this._isDependency(observable)) {
180 this._checkIterations();
182 }
183 }
185 > public handleChange<T, TChange>(observable: IObservableWithChange<T, TChange>, change: TChange): void {
186 > if (this._isDependency(observable)) { autorunImpl.ts
187 > getLogger()?.handleAutorunDependencyChanged(this, observable, change);
188 > try {
189 > // Warning: external call!
190 > const shouldReact = this._changeTracker ? this._changeTracker.handleChange({
191 changedObservable: observable,
192 change,
193 // eslint-disable-next-line local/code-no-any-casts
194 didChange: (o): this is any => o === observable as any,
195 > }, this._changeSummary!) : true; autorunImpl.ts
196 > if (shouldReact) {
197 > this._checkIterations();
198 > this._state = AutorunState.stale;
199 > }
200 > } catch (e) {
201 onBugIndicatingError(e);
202 }
203 > } autorunImpl.ts
204 > }
206 > private _isDependency(observable: IObservableWithChange<any, any>): boolean {
207 > return this._dependencies.has(observable) && !this._dependenciesToBeRemoved.has(observable); autorunImpl.ts
208 > }
210 > // IReader implementation
211 >
212 > private _ensureNoRunning(): void {
213 > if (!this._isRunning) { throw new BugIndicatingError('The reader object cannot be used outside its compute function!'); } autorunImpl.ts
214 > }
216 > public readObservable<T>(observable: IObservable<T>): T {
217 > this._ensureNoRunning(); autorunImpl.ts
218 >
219 > // In case the run action disposes the autorun
220 > if (this._disposed) {
221 return observable.get(); // warning: external call!
222 }
224 > observable.addObserver(this); // warning: external call!
225 > const value = observable.get(); // warning: external call!
226 > this._dependencies.add(observable);
227 > this._dependenciesToBeRemoved.delete(observable);
228 > return value;
229 > }
231 > private _store: DisposableStore | undefined = undefined;
232 > get store(): DisposableStore {
233 this._ensureNoRunning();
234 if (this._disposed) {
241 return this._store;
242 }
244 > private _delayedStore: DisposableStore | undefined = undefined;
245 > get delayedStore(): DisposableStore {
246 this._ensureNoRunning();
247 if (this._disposed) {
254 return this._delayedStore;
255 }
257 > public debugGetState() {
258 return {
259 isRunning: this._isRunning,
264 };
265 }
267 > public debugRerun(): void {
268 if (!this._isRunning) {
269 this._run();
272 }
273 }
275 > private _checkIterations(): boolean {
276 > if (this._iteration > 100) { autorunImpl.ts
277 onBugIndicatingError(new BugIndicatingError(`Autorun '${this.debugName}' is stuck in an infinite update loop.`));
278 return true;
279 }
280 > return false; autorunImpl.ts
281 > }
282 > } autorunImpl.ts
src/vs/base/common/fuzzyScorer.ts 191 covered LOC · 25 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- fuzzyScorer.ts
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 > import { CharCode } from './charCode.js';
7 > import { compareAnything } from './comparers.js';
8 > import { createMatches as createFuzzyMatches, fuzzyScore, IMatch, isUpper, matchesPrefix } from './filters.js';
9 > import { hash } from './hash.js';
10 > import { sep } from './path.js';
11 > import { isLinux, isWindows } from './platform.js';
12 > import { equalsIgnoreCase } from './strings.js';
13 >
14 > //#region Fuzzy scorer
15 >
16 > export type FuzzyScore = [number /* score */, number[] /* match positions */];
17 > export type FuzzyScorerCache = { [key: string]: IItemScore };
18 >
19 > const NO_MATCH = 0;
20 > const NO_SCORE: FuzzyScore = [NO_MATCH, []];
21 >
22 > // const DEBUG = true;
23 > // const DEBUG_MATRIX = false;
24 >
25 > export function scoreFuzzy(target: string, query: string, queryLower: string, allowNonContiguousMatches: boolean): FuzzyScore {
26 if (!target || !query) {
27 return NO_SCORE; // return early if target or query are undefined
49 return res;
50 }
52 function doScoreFuzzy(query: string, queryLower: string, queryLength: number, target: string, targetLower: string, targetLength: number, allowNonContiguousMatches: boolean): FuzzyScore {
53 const scores: number[] = [];
155 return [scores[queryLength * targetLength - 1], positions.reverse()];
156 }
158 function computeCharScore(queryCharAtIndex: string, queryLowerCharAtIndex: string, target: string, targetLower: string, targetIndex: number, matchesSequenceLength: number): number {
159 let score = 0;
235 return score;
236 }
238 function considerAsEqual(a: string, b: string): boolean {
239 if (a === b) {
248 return false;
249 }
251 function scoreSeparatorAtPos(charCode: number): number {
252 switch (charCode) {
266 }
267 }
269 > // function printMatrix(query: string, target: string, matches: number[], scores: number[]): void {
270 > // console.log('\t' + target.split('').join('\t'));
271 > // for (let queryIndex = 0; queryIndex < query.length; queryIndex++) {
272 > // let line = query[queryIndex] + '\t';
273 > // for (let targetIndex = 0; targetIndex < target.length; targetIndex++) {
274 > // const currentIndex = queryIndex * target.length + targetIndex;
275 > // line = line + 'M' + matches[currentIndex] + '/' + 'S' + scores[currentIndex] + '\t';
276 > // }
277 >
278 > // console.log(line);
279 > // }
280 > // }
281 >
282 > //#endregion
283 >
284 >
285 > //#region Alternate fuzzy scorer implementation that is e.g. used for symbols
286 >
287 > export type FuzzyScore2 = [number | undefined /* score */, IMatch[]];
288 >
289 > const NO_SCORE2: FuzzyScore2 = [undefined, []];
290 >
291 > export function scoreFuzzy2(target: string, query: IPreparedQuery | IPreparedQueryPiece, patternStart = 0, wordStart = 0): FuzzyScore2 {
292
293 // Score: multiple inputs
300 return doScoreFuzzy2Single(target, query, patternStart, wordStart);
301 }
303 function doScoreFuzzy2Multiple(target: string, query: IPreparedQueryPiece[], patternStart: number, wordStart: number): FuzzyScore2 {
304 let totalScore = 0;
321 return [totalScore, normalizeMatches(totalMatches)];
322 }
324 function doScoreFuzzy2Single(target: string, query: IPreparedQueryPiece, patternStart: number, wordStart: number): FuzzyScore2 {
325 const score = fuzzyScore(query.normalized, query.normalizedLowercase, patternStart, target, target.toLowerCase(), wordStart, { firstMatchCanBeWeak: true, boostFullMatch: true });
330 return [score[0], createFuzzyMatches(score)];
331 }
333 > //#endregion
334 >
335 >
336 > //#region Item (label, description, path) scorer
337 >
338 > /**
339 > * Scoring on structural items that have a label and optional description.
340 > */
341 > export interface IItemScore {
342 >
343 > /**
344 > * Overall score.
345 > */
346 > score: number;
347 >
348 > /**
349 > * Matches within the label.
350 > */
351 > labelMatch?: IMatch[];
352 >
353 > /**
354 > * Matches within the description.
355 > */
356 > descriptionMatch?: IMatch[];
357 > }
358 >
359 > const NO_ITEM_SCORE = Object.freeze<IItemScore>({ score: 0 });
360 >
361 > export interface IItemAccessor<T> {
362 >
363 > /**
364 > * Just the label of the item to score on.
365 > */
366 > getItemLabel(item: T): string | undefined;
367 >
368 > /**
369 > * The optional description of the item to score on.
370 > */
371 > getItemDescription(item: T): string | undefined;
372 >
373 > /**
374 > * If the item is a file, the path of the file to score on.
375 > */
376 > getItemPath(file: T): string | undefined;
377 > }
378 >
379 > const PATH_IDENTITY_SCORE = 1 << 18;
380 > const LABEL_PREFIX_SCORE_THRESHOLD = 1 << 17;
381 > const LABEL_SCORE_THRESHOLD = 1 << 16;
382 >
383 function getCacheHash(label: string, description: string | undefined, allowNonContiguousMatches: boolean, query: IPreparedQuery) {
384 const values = query.values ? query.values : [query];
393 return cacheHash;
394 }
396 > export function scoreItemFuzzy<T>(item: T, query: IPreparedQuery, allowNonContiguousMatches: boolean, accessor: IItemAccessor<T>, cache: FuzzyScorerCache): IItemScore {
397 if (!item || !query.normalized) {
398 return NO_ITEM_SCORE; // we need an item and query to score on at least
422 return itemScore;
423 }
425 function doScoreItemFuzzy(label: string, description: string | undefined, path: string | undefined, query: IPreparedQuery, allowNonContiguousMatches: boolean): IItemScore {
426 const preferLabelMatches = !path || !query.containsPathSeparator;
439 return doScoreItemFuzzySingle(label, description, path, query, preferLabelMatches, allowNonContiguousMatches);
440 }
442 function doScoreItemFuzzyMultiple(label: string, description: string | undefined, path: string | undefined, query: IPreparedQueryPiece[], preferLabelMatches: boolean, allowNonContiguousMatches: boolean): IItemScore {
443 let totalScore = 0;
471 };
472 }
474 function doScoreItemFuzzySingle(label: string, description: string | undefined, path: string | undefined, query: IPreparedQueryPiece, preferLabelMatches: boolean, allowNonContiguousMatches: boolean): IItemScore {
475
554 return NO_ITEM_SCORE;
555 }
557 function createMatches(offsets: number[] | undefined): IMatch[] {
558 const ret: IMatch[] = [];
573 return ret;
574 }
576 function normalizeMatches(matches: IMatch[]): IMatch[] {
577
603 return normalizedMatches;
604 }
606 function matchOverlaps(matchA: IMatch, matchB: IMatch): boolean {
607 if (matchA.end < matchB.start) {
615 return true;
616 }
618 > //#endregion
619 >
620 >
621 > //#region Comparers
622 >
623 > export function compareItemsByFuzzyScore<T>(itemA: T, itemB: T, query: IPreparedQuery, allowNonContiguousMatches: boolean, accessor: IItemAccessor<T>, cache: FuzzyScorerCache): number {
624 const itemScoreA = scoreItemFuzzy(itemA, query, allowNonContiguousMatches, accessor, cache);
625 const itemScoreB = scoreItemFuzzy(itemB, query, allowNonContiguousMatches, accessor, cache);
682 return fallbackCompare(itemA, itemB, query, accessor);
683 }
685 function computeLabelAndDescriptionMatchDistance<T>(item: T, score: IItemScore, accessor: IItemAccessor<T>): number {
686 let matchStart = -1;
717 return matchEnd - matchStart;
718 }
720 function compareByMatchLength(matchesA?: IMatch[], matchesB?: IMatch[]): number {
721 if ((!matchesA && !matchesB) || ((!matchesA?.length) && (!matchesB?.length))) {
744 return matchLengthA === matchLengthB ? 0 : matchLengthB < matchLengthA ? 1 : -1;
745 }
747 function fallbackCompare<T>(itemA: T, itemB: T, query: IPreparedQuery, accessor: IItemAccessor<T>): number {
748
789 return 0;
790 }
792 > //#endregion
793 >
794 >
795 > //#region Query Normalizer
796 >
797 > export interface IPreparedQueryPiece {
798 >
799 > /**
800 > * The original query as provided as input.
801 > */
802 > original: string;
803 > originalLowercase: string;
804 >
805 > /**
806 > * Original normalized to platform separators:
807 > * - Windows: \
808 > * - Posix: /
809 > */
810 > pathNormalized: string;
811 >
812 > /**
813 > * In addition to the normalized path, will have
814 > * whitespace, wildcards, quotes, ellipsis, and trailing hash characters removed.
815 > */
816 > normalized: string;
817 > normalizedLowercase: string;
818 >
819 > /**
820 > * The query is wrapped in quotes which means
821 > * this query must be a substring of the input.
822 > * In other words, no fuzzy matching is used.
823 > */
824 > expectContiguousMatch: boolean;
825 > }
826 >
827 > export interface IPreparedQuery extends IPreparedQueryPiece {
828 >
829 > /**
830 > * Query split by spaces into pieces.
831 > */
832 > values: IPreparedQueryPiece[] | undefined;
833 >
834 > /**
835 > * Whether the query contains path separator(s) or not.
836 > */
837 > containsPathSeparator: boolean;
838 > }
839 >
840 > /*
841 > * If a query is wrapped in quotes, the user does not want to
842 > * use fuzzy search for this query.
843 > */
844 function queryExpectsExactMatch(query: string) {
845 return query.startsWith('"') && query.endsWith('"');
846 }
848 > /**
849 > * Helper function to prepare a search value for scoring by removing unwanted characters
850 > * and allowing to score on multiple pieces separated by whitespace character.
851 > */
852 > const MULTIPLE_QUERY_VALUES_SEPARATOR = ' ';
853 > export function prepareQuery(original: string): IPreparedQuery {
854 if (typeof original !== 'string') {
855 original = '';
892 return { original, originalLowercase, pathNormalized, normalized, normalizedLowercase, values, containsPathSeparator, expectContiguousMatch: expectExactMatch };
893 }
895 function normalizeQuery(original: string): { pathNormalized: string; normalized: string; normalizedLowercase: string } {
896 let pathNormalized: string;
915 };
916 }
918 > export function pieceToQuery(piece: IPreparedQueryPiece): IPreparedQuery;
919 > export function pieceToQuery(pieces: IPreparedQueryPiece[]): IPreparedQuery;
920 > export function pieceToQuery(arg1: IPreparedQueryPiece | IPreparedQueryPiece[]): IPreparedQuery {
921 if (Array.isArray(arg1)) {
922 return prepareQuery(arg1.map(piece => piece.original).join(MULTIPLE_QUERY_VALUES_SEPARATOR));
925 return prepareQuery(arg1.original);
926 }
928 > //#endregion
src/vs/platform/agentHost/node/agentHostSessionTitleController.ts 191 covered LOC · 28 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostSessionTitleController.ts
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 > import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
7 > import { Disposable } from '../../../base/common/lifecycle.js';
8 > import { URI } from '../../../base/common/uri.js';
9 > import { ILogService } from '../../log/common/log.js';
10 > import { ISessionDataService } from '../common/sessionDataService.js';
11 > import { ActionType } from '../common/state/sessionActions.js';
12 > import { isAhpChatChannel, isDefaultChatUri, type Turn, type URI as ProtocolURI } from '../common/state/sessionState.js';
13 > import { buildConversationContext, renderResponseMarkdown, truncateMiddle } from '../common/agentHostConversationContext.js';
14 > import { AgentHostStateManager } from './agentHostStateManager.js';
15 > import { ICopilotApiService, type ICopilotUtilityChatMessage } from './shared/copilotApiService.js';
16 >
17 > const MAX_TITLE_LENGTH = 200;
18 >
19 > /**
20 > * Soft upper bound, in characters, for the first-turn context fed to the
21 > * utility model when refining a session title. Sized to stay well within the
22 > * small model's context window while leaving room for the prompt scaffolding.
23 > */
24 > const MAX_TITLE_CONTEXT_CHARS = 20000;
25 >
26 > export interface IAgentHostSessionTitleControllerOptions {
27 > readonly sessionDataService: ISessionDataService;
28 > readonly getGitHubCopilotToken?: () => string | undefined;
29 > readonly copilotApiService?: ICopilotApiService;
30 > }
31 >
32 > export class AgentHostSessionTitleController extends Disposable {
33 >
34 > private readonly _titleGenerationCancellationSources = new Map<ProtocolURI, CancellationTokenSource>();
35 >
36 > /**
37 > * The most recent title this controller applied for a given session/chat
38 > * key. Used to detect whether the title was changed (e.g. a manual
39 > * `/rename` or user edit) since we last set it, so we never clobber a
40 > * deliberate title with an auto-generated one.
41 > */
42 > private readonly _lastAppliedTitle = new Map<ProtocolURI, string>();
43 >
44 > /**
45 > * Session/chat keys whose current title is a provisional placeholder set by
46 > * {@link seedProvisionalTitle} (e.g. from a `!command`). Such a title does
47 > * not describe the session's topic, so the first subsequent request that
48 > * carries real intent replaces it with a generated title via
49 > * {@link seedTitleFromFirstMessage}.
50 > */
51 > private readonly _provisionalTitles = new Set<ProtocolURI>();
52 >
53 > constructor(
54 > private readonly _stateManager: AgentHostStateManager, agentHostSessionTitleController.ts
55 > private readonly _options: IAgentHostSessionTitleControllerOptions,
56 > @ILogService private readonly _logService: ILogService,
57 > ) {
58 > super();
59 > }
61 > seedTitleFromFirstMessage(channel: ProtocolURI, userPrompt: string, chatChannel?: ProtocolURI): void {
62 const fallbackTitle = this._normalizeTitle(userPrompt);
63 if (!fallbackTitle) {
87 );
88 }
90 > /** Seeds and persists a provisional title suggested by a locally handled command. */
91 > seedProvisionalTitle(channel: ProtocolURI, suggestedTitle: string, chatChannel?: ProtocolURI): void {
92 const title = this._normalizeTitle(suggestedTitle);
93 if (!title) {
105 this._persistSeedTitle(channel, additionalChat, title);
106 }
108 > /** Trims, collapses whitespace, and length-caps a candidate title. */
109 > private _normalizeTitle(text: string): string {
110 return text.trim().replace(/\s+/g, ' ').slice(0, MAX_TITLE_LENGTH);
111 }
113 > /**
114 > * The peer (additional) chat a seed should title, or `undefined` to title
115 > * the session itself. The default chat maps to the session.
116 > */
117 > private _additionalChatChannel(chatChannel?: ProtocolURI): ProtocolURI | undefined {
118 return !!chatChannel && isAhpChatChannel(chatChannel) && !isDefaultChatUri(chatChannel) ? chatChannel : undefined;
119 }
121 > /**
122 > * Applies `title` to the addressed peer chat (`additionalChat`) or, when
123 > * that is `undefined`, to the session itself, recording it as last-applied.
124 > */
125 > private _applySeedTitle(channel: ProtocolURI, additionalChat: ProtocolURI | undefined, title: string): void {
126 if (additionalChat) {
127 this._applyTitle(additionalChat, title, t => this._stateManager.updateChatTitle(channel, additionalChat, t));
133 }
134 }
136 > /** Persists `title` as the custom title of the addressed peer chat or session. */
137 > private _persistSeedTitle(channel: ProtocolURI, additionalChat: ProtocolURI | undefined, title: string): void {
138 this._persistSessionFlag(channel, additionalChat ? `customChatTitle:${additionalChat}` : 'customTitle', title);
139 }
141 > /** The live title of the addressed peer chat or session. */
142 > private _currentSeedTitle(channel: ProtocolURI, additionalChat: ProtocolURI | undefined): string | undefined {
143 return additionalChat ? this._stateManager.getChatState(additionalChat)?.title : this._stateManager.getSessionState(channel)?.title;
144 }
146 > /**
147 > * Whether {@link seedTitleFromFirstMessage} may (re)title `key`: true for a
148 > * fresh, untitled target (its first message) or when its title is a
149 > * provisional placeholder we applied and no one has changed it since — the
150 > * first real request supersedes the placeholder.
151 > */
152 > private _canSeedFirstMessageTitle(key: ProtocolURI, turnsLength: number, currentTitle: string | undefined): boolean {
153 if (turnsLength === 0 && !currentTitle) {
154 return true;
156 return this._provisionalTitles.has(key) && !!currentTitle && currentTitle === this._lastAppliedTitle.get(key);
157 }
159 > /**
160 > * Whether {@link seedProvisionalTitle} may (re)title `key`: true when it is
161 > * untitled (the first message carried a suggestion) or when its title is a
162 > * provisional placeholder we applied and no one has changed it since —
163 > * successive suggestions keep the newest one visible without clobbering a
164 > * manual rename.
165 > */
166 > private _canSeedProvisionalTitle(key: ProtocolURI, currentTitle: string | undefined): boolean {
167 if (!currentTitle) {
168 return true;
170 return this._provisionalTitles.has(key) && currentTitle === this._lastAppliedTitle.get(key);
171 }
173 > /**
174 > * Re-generates the title once the first turn has completed, this time
175 > * using the full first-turn context (the user request plus the agent's
176 > * textual response) rather than just the opening message. This only runs
177 > * for the very first turn and only when the current title is still the one
178 > * this controller last applied — a manual `/rename`, a user edit, or a
179 > * forked session's inherited title all suppress it.
180 > *
181 > * Only normal text response parts are considered (tool calls, reasoning,
182 > * and other parts are ignored). If the context still exceeds the budget
183 > * the middle is removed (marked with `...`). The user's first request is
184 > * always preserved.
185 > */
186 > refineTitleFromFirstTurn(channel: ProtocolURI, chatChannel?: ProtocolURI): void {
187 const isAdditionalChat = !!chatChannel && isAhpChatChannel(chatChannel) && !isDefaultChatUri(chatChannel);
188 if (isAdditionalChat) {
238 );
239 }
241 > /**
242 > * Generates a title for a freshly forked session or chat from its
243 > * inherited conversation context. Forks copy the source history up to the
244 > * fork point, so neither {@link seedTitleFromFirstMessage} nor
245 > * {@link refineTitleFromFirstTurn} (which require an empty / single-turn
246 > * state) ever fire for them. This is the fork equivalent, run once at fork
247 > * time over the kept turns, so the new chat gets a content-derived title
248 > * instead of permanently inheriting the source's `Forked: …` title.
249 > *
250 > * `fallbackTitle` is the title the caller already applied to the new
251 > * session/chat (e.g. `Forked: <source>`); it is recorded as the
252 > * last-applied title so a concurrent manual rename suppresses the
253 > * generated title, and stays visible until generation completes. The
254 > * context is bounded to {@link MAX_TITLE_CONTEXT_CHARS} (middle-truncated),
255 > * so generation costs at most a single small-model call.
256 > */
257 > generateForkedTitle(channel: ProtocolURI, chatChannel: ProtocolURI | undefined, turns: readonly Turn[], fallbackTitle: string, sourceTitle?: string): void {
258 const context = this._buildConversationContext(turns, sourceTitle);
259 if (!context) {
351 persist(generatedTitle);
352 }
354 > private async _generateTitleFromPrompt(promptContent: string, isConversation: boolean, token: CancellationToken): Promise<string | undefined> {
355 if (token.isCancellationRequested) {
356 return undefined;
382 }
383 }
385 > private _buildTitlePrompt(promptContent: string, isConversation: boolean): ICopilotUtilityChatMessage[] {
386 const userInstruction = isConversation
387 ? `Please write a brief title for the following conversation:\n\n${promptContent}`
409 ];
410 }
412 > private _cleanTitle(rawTitle: string): string | undefined {
413 let title = rawTitle.trim();
414 const firstLine = title.split(/\r?\n/).map(line => line.trim()).find(line => line.length > 0);
424 return title.slice(0, MAX_TITLE_LENGTH);
425 }
427 > /**
428 > * Builds the first-turn context string for title refinement. The user's
429 > * request is always kept (truncated in the middle only if it alone exceeds
430 > * half the budget). Only normal text (markdown) response parts are
431 > * considered — tool calls, reasoning, and other parts are ignored. If the
432 > * combined text is over budget, the middle of the response is removed.
433 > *
434 > * @returns the context string, or `undefined` when the turn has no text
435 > * response worth refining from (the opening message already produced a
436 > * title in that case).
437 > */
438 > private _buildFirstTurnContext(turn: Turn): string | undefined {
439 const response = renderResponseMarkdown(turn.responseParts);
440 if (!response) {
455 return trimmedResponse ? `${userBlock}${responseLabel}${trimmedResponse}` : userBlock;
456 }
458 > /**
459 > * Builds a conversation context string for forked-title generation by
460 > * concatenating each kept turn's user request and textual response. Only
461 > * normal text (markdown) response parts are considered — tool calls,
462 > * reasoning, and other parts are ignored, mirroring
463 > * {@link _buildFirstTurnContext}. When the fork's `sourceTitle` is known, a
464 > * short framing note is prepended so the model understands the conversation
465 > * is a branch continued from an earlier chat. The conversation is
466 > * middle-truncated to {@link MAX_TITLE_CONTEXT_CHARS} to bound model cost;
467 > * the framing note is always preserved in full.
468 > *
469 > * @returns the context string, or `undefined` when no turn carries any
470 > * text worth titling from.
471 > */
472 > private _buildConversationContext(turns: readonly Turn[], sourceTitle?: string): string | undefined {
473 const framedTitle = sourceTitle?.trim();
474 const framing = framedTitle
477 return buildConversationContext(turns, { maxChars: MAX_TITLE_CONTEXT_CHARS, framing });
478 }
480 > private _persistSessionFlag(session: ProtocolURI, key: string, value: string): void {
481 const ref = this._options.sessionDataService.openDatabase(URI.parse(session));
482 ref.object.setMetadata(key, value).catch(err => {
486 });
487 }
489 > private _cancelTitleGeneration(session: ProtocolURI): void {
490 const source = this._titleGenerationCancellationSources.get(session);
491 if (!source) {
495 this._titleGenerationCancellationSources.delete(session);
496 }
498 > override dispose(): void {
499 > for (const source of this._titleGenerationCancellationSources.values()) { agentHostSessionTitleController.ts
500 source.dispose(true);
501 }
502 > this._titleGenerationCancellationSources.clear(); agentHostSessionTitleController.ts
503 > this._lastAppliedTitle.clear();
504 > this._provisionalTitles.clear();
505 > super.dispose();
506 > }
src/vs/base/common/platform.ts 189 covered LOC · 12 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- platform.ts
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 > import * as nls from '../../nls.js';
7 >
8 > export const LANGUAGE_DEFAULT = 'en';
9 >
10 > let _isWindows = false;
11 > let _isMacintosh = false;
12 > let _isLinux = false;
13 > let _isLinuxSnap = false;
14 > let _isNative = false;
15 > let _isWeb = false;
16 > let _isElectron = false;
17 > let _isIOS = false;
18 > let _isCI = false;
19 > let _isMobile = false;
20 > let _locale: string | undefined = undefined;
21 > let _language: string = LANGUAGE_DEFAULT;
22 > let _platformLocale: string = LANGUAGE_DEFAULT;
23 > let _translationsConfigFile: string | undefined = undefined;
24 > let _userAgent: string | undefined = undefined;
25 >
26 > export interface IProcessEnvironment {
27 > [key: string]: string | undefined;
28 > }
29 >
30 > /**
31 > * This interface is intentionally not identical to node.js
32 > * process because it also works in sandboxed environments
33 > * where the process object is implemented differently. We
34 > * define the properties here that we need for `platform`
35 > * to work and nothing else.
36 > */
37 > export interface INodeProcess {
38 > platform: string;
39 > arch: string;
40 > env: IProcessEnvironment;
41 > versions?: {
42 > node?: string;
43 > electron?: string;
44 > chrome?: string;
45 > };
46 > type?: string;
47 > cwd: () => string;
48 > }
49 >
50 > declare const process: INodeProcess;
51 >
52 > const $globalThis: any = globalThis;
53 >
54 > let nodeProcess: INodeProcess | undefined = undefined;
55 > if (typeof $globalThis.vscode !== 'undefined' && typeof $globalThis.vscode.process !== 'undefined') {
56 // Native environment (sandboxed)
57 nodeProcess = $globalThis.vscode.process;
58 > } else if (typeof process !== 'undefined' && typeof process?.versions?.node === 'string') { platform.ts
59 > // Native environment (non-sandboxed)
60 > nodeProcess = process;
61 > }
62 >
63 > const isElectronProcess = typeof nodeProcess?.versions?.electron === 'string';
64 > const isElectronRenderer = isElectronProcess && nodeProcess?.type === 'renderer';
65 >
66 > interface INavigator {
67 > userAgent: string;
68 > maxTouchPoints?: number;
69 > language: string;
70 > }
71 > declare const navigator: INavigator;
72 >
73 > // Native environment
74 > if (typeof nodeProcess === 'object') {
75 > _isWindows = (nodeProcess.platform === 'win32');
76 > _isMacintosh = (nodeProcess.platform === 'darwin');
77 > _isLinux = (nodeProcess.platform === 'linux');
78 > _isLinuxSnap = _isLinux && !!nodeProcess.env['SNAP'] && !!nodeProcess.env['SNAP_REVISION'];
79 > _isElectron = isElectronProcess;
80 > _isCI = !!nodeProcess.env['CI'] || !!nodeProcess.env['BUILD_ARTIFACTSTAGINGDIRECTORY'] || !!nodeProcess.env['GITHUB_WORKSPACE'];
81 > _locale = LANGUAGE_DEFAULT;
82 > _language = LANGUAGE_DEFAULT;
83 > const rawNlsConfig = nodeProcess.env['VSCODE_NLS_CONFIG'];
84 > if (rawNlsConfig) {
85 try {
86 const nlsConfig: nls.INLSConfiguration = JSON.parse(rawNlsConfig);
113 console.error('Unable to resolve platform.');
114 }
115 > platform.ts
116 > export const enum Platform {
117 > Web,
118 > Mac,
119 > Linux,
120 > Windows
121 > }
122 > export type PlatformName = 'Web' | 'Windows' | 'Mac' | 'Linux';
123 >
124 > export function PlatformToString(platform: Platform): PlatformName {
125 switch (platform) {
126 case Platform.Web: return 'Web';
130 }
131 }
132 > platform.ts
133 > let _platform: Platform = Platform.Web;
134 > if (_isMacintosh) {
135 _platform = Platform.Mac;
136 > } else if (_isWindows) { platform.ts
137 _platform = Platform.Windows;
138 > } else if (_isLinux) { platform.ts
139 > _platform = Platform.Linux;
140 > }
141 >
142 > export const isWindows = _isWindows;
143 > export const isMacintosh = _isMacintosh;
144 > export const isLinux = _isLinux;
145 > export const isLinuxSnap = _isLinuxSnap;
146 > export const isNative = _isNative;
147 > export const isElectron = _isElectron;
148 > export const isWeb = _isWeb;
149 > export const isWebWorker = (_isWeb && typeof $globalThis.importScripts === 'function');
150 > export const webWorkerOrigin = isWebWorker ? $globalThis.origin : undefined;
151 > export const isIOS = _isIOS;
152 > export const isMobile = _isMobile;
153 > /**
154 > * Whether we run inside a CI environment, such as
155 > * GH actions or Azure Pipelines.
156 > */
157 > export const isCI = _isCI;
158 > export const platform = _platform;
159 > export const userAgent = _userAgent;
160 >
161 > /**
162 > * The language used for the user interface. The format of
163 > * the string is all lower case (e.g. zh-tw for Traditional
164 > * Chinese or de for German)
165 > */
166 > export const language = _language;
167 >
168 > export namespace Language {
169 >
170 > export function value(): string {
171 return language;
172 }
173 > platform.ts
174 > export function isDefaultVariant(): boolean {
175 if (language.length === 2) {
176 return language === 'en';
181 }
182 }
183 > platform.ts
184 > export function isDefault(): boolean {
185 return language === 'en';
186 }
187 > } platform.ts
188 >
189 > /**
190 > * Desktop: The OS locale or the locale specified by --locale or `argv.json`.
191 > * Web: matches `platformLocale`.
192 > *
193 > * The UI is not necessarily shown in the provided locale.
194 > */
195 > export const locale = _locale;
196 >
197 > /**
198 > * This will always be set to the OS/browser's locale regardless of
199 > * what was specified otherwise. The format of the string is all
200 > * lower case (e.g. zh-tw for Traditional Chinese). The UI is not
201 > * necessarily shown in the provided locale.
202 > */
203 > export const platformLocale = _platformLocale;
204 >
205 > /**
206 > * The translations that are available through language packs.
207 > */
208 > export const translationsConfigFile = _translationsConfigFile;
209 >
210 > export const setTimeout0IsFaster = (typeof $globalThis.postMessage === 'function' && !$globalThis.importScripts);
211 >
212 > /**
213 > * See https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#:~:text=than%204%2C%20then-,set%20timeout%20to%204,-.
214 > *
215 > * Works similarly to `setTimeout(0)` but doesn't suffer from the 4ms artificial delay
216 > * that browsers set when the nesting level is > 5.
217 > */
218 > export const setTimeout0 = (() => {
219 > if (setTimeout0IsFaster) {
220 interface IQueueElement {
221 id: number;
246 };
247 }
248 > return (callback: () => void) => setTimeout(callback); platform.ts
249 > })();
250 >
251 > export const enum OperatingSystem {
252 > Windows = 1,
253 > Macintosh = 2,
254 > Linux = 3
255 > }
256 > export const OS = (_isMacintosh || _isIOS ? OperatingSystem.Macintosh : (_isWindows ? OperatingSystem.Windows : OperatingSystem.Linux));
257 >
258 > let _isLittleEndian = true;
259 > let _isLittleEndianComputed = false;
260 > export function isLittleEndian(): boolean {
261 if (!_isLittleEndianComputed) {
262 _isLittleEndianComputed = true;
269 return _isLittleEndian;
270 }
271 > platform.ts
272 > export const isChrome = !!(userAgent && userAgent.indexOf('Chrome') >= 0);
273 > export const isFirefox = !!(userAgent && userAgent.indexOf('Firefox') >= 0);
274 > export const isSafari = !!(!isChrome && (userAgent && userAgent.indexOf('Safari') >= 0));
275 > export const isEdge = !!(userAgent && userAgent.indexOf('Edg/') >= 0);
276 > export const isAndroid = !!(userAgent && userAgent.indexOf('Android') >= 0);
277 > export const hasElectronUserAgent = !!(userAgent && userAgent.indexOf('Electron') >= 0);
278 >
279 > export function isTahoeOrNewer(osVersion: string): boolean {
280 return parseFloat(osVersion) >= 25;
281 }
src/vs/platform/agentHost/common/changesetUri.ts 185 covered LOC · 16 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- changesetUri.ts
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 > import { localize } from '../../../nls.js';
7 > import { readSessionGitState, readSessionWorkspaceless, SessionLifecycle, type Changeset, type ISessionGitState, type ISessionWithDefaultChat, type URI } from './state/sessionState.js';
8 >
9 > /**
10 > * Helpers for building / parsing the URI clients subscribe to in order to
11 > * receive a {@link import('./state/protocol/state.js').ChangesetState}.
12 > *
13 > * Shapes recognised by this module:
14 > *
15 > * <sessionUri>/changeset/uncommitted
16 > * <sessionUri>/changeset/session
17 > * <sessionUri>/changeset/turn/<turnId>
18 > * <sessionUri>/changeset/compare/<originalTurnId>/<modifiedTurnId>
19 > *
20 > * Catalogue entries on `summary.changesets` may also advertise the
21 > * URI-template forms `<sessionUri>/changeset/turn/{turnId}` and
22 > * `<sessionUri>/changeset/compare/{originalTurnId}/{modifiedTurnId}`;
23 > * clients expand the template before subscribing.
24 > *
25 > * Keeping changeset URIs nested under the session URI namespace lets the
26 > * server cleanly tear down every changeset for a session when that session
27 > * is disposed (the reverse-lookup is just a string-prefix scan).
28 > */
29 >
30 > /** /** Stable id of the catalogue entry for the branch changeset. */
31 > const BRANCH_CHANGESET_ID = 'branch';
32 >
33 > /** Stable id of the catalogue entry for the uncommitted-changes changeset. */
34 > const UNCOMMITTED_CHANGESET_ID = 'uncommitted';
35 >
36 > /** Stable id of the catalogue entry for the session-wide changeset. */
37 > const SESSION_CHANGESET_ID = 'session';
38 >
39 > /** Path prefix used by per-turn changeset URIs (`turn/<turnId>`). */
40 > const TURN_CHANGESET_PREFIX = 'turn/';
41 >
42 > /** Template variable name used inside the per-turn URI template. */
43 > const TURN_TEMPLATE_VARIABLE = '{turnId}';
44 >
45 > /** Path prefix used by compare-turns changeset URIs (`compare/<originalTurnId>/<modifiedTurnId>`). */
46 > const COMPARE_CHANGESET_PREFIX = 'compare/';
47 >
48 > /** Template variable name for the original turn in the compare-turns URI template. */
49 > const COMPARE_ORIGINAL_TEMPLATE_VARIABLE = '{originalTurnId}';
50 >
51 > /** Template variable name for the modified turn in the compare-turns URI template. */
52 > const COMPARE_MODIFIED_TEMPLATE_VARIABLE = '{modifiedTurnId}';
53 >
54 > /** Localized human-readable label for the branch changeset entry. */
55 > export const branchChangesetLabel = (): string => localize('branchChangeset.label', "Branch Changes");
56 >
57 > /** Localized human-readable label for the session-wide changeset entry. */
58 > export const sessionChangesetLabel = (): string => localize('sessionChangeset.label', "All Changes");
59 >
60 > /** Localized human-readable description for the session-wide changeset entry. */
61 > export const sessionChangesetDescription = (): string => localize('sessionChangeset.description', "Show all changes made in this session");
62 >
63 > /** Localized human-readable label for the uncommitted-changes changeset entry. */
64 > export const uncommittedChangesetLabel = (): string => localize('uncommittedChangeset.label', "Uncommitted Changes");
65 >
66 > /** Localized human-readable description for the uncommitted-changes changeset entry. */
67 > export const uncommittedChangesetDescription = (): string => localize('uncommittedChangeset.description', "Show uncommitted changes in this session");
68 >
69 > /** Localized human-readable label for the per-turn changeset template entry. */
70 > export const thisTurnChangesetLabel = (): string => localize('thisTurnChangeset.label', "This Turn");
71 >
72 > /** Localized human-readable description for the per-turn changeset template entry. */
73 > export const thisTurnChangesetDescription = (): string => localize('thisTurnChangeset.description', "Show changes made in this turn");
74 >
75 > /** Localized human-readable label for the compare-turns changeset template entry. */
76 > export const compareTurnsChangesetLabel = (): string => localize('compareTurnsChangeset.label', "Compare Turns");
77 >
78 > /** Localized human-readable description for the compare-turns changeset template entry. */
79 > export const compareTurnsChangesetDescription = (): string => localize('compareTurnsChangeset.description', "Show changes made between different turns");
80 >
81 > /**
82 > * Returns the description shown next to the `Branch Changes` catalogue
83 > * entry. Prefers `${branchName} → ${baseBranchName}` when both values
84 > * are known (typical worktree-isolation case). If `baseBranchName` is
85 > * unknown, falls back to `${branchName} → ${upstreamBranchName}` when an
86 > * upstream is available. Finally falls back to `branchName` alone.
87 > * Returns `undefined` only when no branch name is known at all, so
88 > * callers can omit the description entirely.
89 > */
90 > export function formatBranchChangesetDescription(gitState: ISessionGitState): string | undefined {
91 const { baseBranchName, branchName, upstreamBranchName } = gitState;
92
103 return branchName;
104 }
106 > /** Marker injected into a changeset URI's path. */
107 > const CHANGESET_PATH_SEGMENT = '/changeset/';
108 >
109 > /** Discriminates the well-known changeset URI shapes. */
110 > export const enum ChangesetKind {
111 > Branch = 'branch',
112 > Uncommitted = 'uncommitted',
113 > Session = 'session',
114 > Turn = 'turn',
115 > Compare = 'compare-turns',
116 > /** Producer-defined id we don't recognise (single-segment only). */
117 > Unknown = 'unknown',
118 > }
119 >
120 > export function buildBranchChangesetUri(sessionUri: URI): URI {
121 return `${sessionUri}${CHANGESET_PATH_SEGMENT}${BRANCH_CHANGESET_ID}`;
122 }
124 > /** Returns the subscribable URI for the session-wide changeset. */
125 > export function buildSessionChangesetUri(sessionUri: URI): URI {
126 return `${sessionUri}${CHANGESET_PATH_SEGMENT}${SESSION_CHANGESET_ID}`;
127 }
129 > /** Returns the subscribable URI for the uncommitted-changes changeset. */
130 > export function buildUncommittedChangesetUri(sessionUri: URI): URI {
131 return `${sessionUri}${CHANGESET_PATH_SEGMENT}${UNCOMMITTED_CHANGESET_ID}`;
132 }
134 > /**
135 > * Returns the URI _template_ that catalogue entries advertise for the
136 > * per-turn changeset; clients expand `{turnId}` to build the
137 > * subscribable URI via {@link buildTurnChangesetUri}.
138 > */
139 > export function buildTurnChangesetUriTemplate(sessionUri: URI): URI {
140 return `${sessionUri}${CHANGESET_PATH_SEGMENT}${TURN_CHANGESET_PREFIX}${TURN_TEMPLATE_VARIABLE}`;
141 }
143 > /** Returns the subscribable URI for the per-turn changeset of `turnId`. */
144 > export function buildTurnChangesetUri(sessionUri: URI, turnId: string): URI {
145 if (!turnId || turnId.includes('/')) {
146 throw new Error(`buildTurnChangesetUri: turnId must be non-empty and not contain '/' (got ${JSON.stringify(turnId)})`);
148 return `${sessionUri}${CHANGESET_PATH_SEGMENT}${TURN_CHANGESET_PREFIX}${turnId}`;
149 }
151 > /**
152 > * Returns the URI _template_ that catalogue entries advertise for the
153 > * compare-turns changeset; clients expand both `{originalTurnId}` and
154 > * `{modifiedTurnId}` to build the subscribable URI via
155 > * {@link buildCompareTurnsChangesetUri}.
156 > */
157 > export function buildCompareTurnsChangesetUriTemplate(sessionUri: URI): URI {
158 return `${sessionUri}${CHANGESET_PATH_SEGMENT}${COMPARE_CHANGESET_PREFIX}${COMPARE_ORIGINAL_TEMPLATE_VARIABLE}/${COMPARE_MODIFIED_TEMPLATE_VARIABLE}`;
159 }
161 > /**
162 > * Returns the subscribable URI for the compare-turns changeset between
163 > * `originalTurnId` (the "from" endpoint) and `modifiedTurnId` (the "to"
164 > * endpoint). Diff direction is `originalTurnId → modifiedTurnId`.
165 > */
166 > export function buildCompareTurnsChangesetUri(sessionUri: URI, originalTurnId: string, modifiedTurnId: string): URI {
167 if (!originalTurnId || originalTurnId.includes('/')) {
168 throw new Error(`buildCompareTurnsChangesetUri: originalTurnId must be non-empty and not contain '/' (got ${JSON.stringify(originalTurnId)})`);
173 return `${sessionUri}${CHANGESET_PATH_SEGMENT}${COMPARE_CHANGESET_PREFIX}${originalTurnId}/${modifiedTurnId}`;
174 }
176 > /**
177 > * Returns the subscribable URI for an opaque, producer-defined
178 > * `changesetId`. The id must not contain `/` — well-known multi-segment
179 > * shapes have dedicated builders (e.g. {@link buildTurnChangesetUri}).
180 > */
181 > export function buildChangesetUri(sessionUri: URI, changesetId: string): URI {
182 if (!changesetId) {
183 throw new Error('buildChangesetUri: changesetId must be non-empty');
188 return `${sessionUri}${CHANGESET_PATH_SEGMENT}${changesetId}`;
189 }
191 > /**
192 > * Parses a changeset URI back into `(sessionUri, changesetId, kind)`,
193 > * or returns `undefined` if `uri` is not a changeset URI we recognise.
194 > */
195 > export function parseChangesetUri(uri: URI): { sessionUri: URI; changesetId: string; kind: ChangesetKind; turnId?: string; originalTurnId?: string; modifiedTurnId?: string } | undefined {
196 const idx = uri.lastIndexOf(CHANGESET_PATH_SEGMENT);
197 if (idx < 0) {
241 return { sessionUri, changesetId, kind: ChangesetKind.Unknown };
242 }
244 > /** Returns `true` iff `uri` looks like a changeset URI we recognise. */
245 > export function isChangesetUri(uri: URI): boolean {
246 return parseChangesetUri(uri) !== undefined;
247 }
249 > /** Returns `true` iff `uri` is the session-wide changeset URI. */
250 > export function isSessionChangesetUri(uri: URI): boolean {
251 return parseChangesetUri(uri)?.kind === ChangesetKind.Session;
252 }
254 > /** Returns `true` iff `uri` is the uncommitted-changes changeset URI. */
255 > export function isUncommittedChangesetUri(uri: URI): boolean {
256 return parseChangesetUri(uri)?.kind === ChangesetKind.Uncommitted;
257 }
259 > /** Returns the parsed turn id when `uri` is a per-turn changeset URI. */
260 > export function parseTurnChangesetUri(uri: URI): { sessionUri: URI; turnId: string } | undefined {
261 const parsed = parseChangesetUri(uri);
262 if (parsed?.kind !== ChangesetKind.Turn || parsed.turnId === undefined) {
265 return { sessionUri: parsed.sessionUri, turnId: parsed.turnId };
266 }
268 > /** Returns the parsed turn ids when `uri` is a compare-turns changeset URI. */
269 > export function parseCompareTurnsChangesetUri(uri: URI): { sessionUri: URI; originalTurnId: string; modifiedTurnId: string } | undefined {
270 const parsed = parseChangesetUri(uri);
271 if (parsed?.kind !== ChangesetKind.Compare || parsed.originalTurnId === undefined || parsed.modifiedTurnId === undefined) {
274 return { sessionUri: parsed.sessionUri, originalTurnId: parsed.originalTurnId, modifiedTurnId: parsed.modifiedTurnId };
275 }
277 > /**
278 > * Builds the default ordered `summary.changesets` catalogue for a
279 > * session (`Branch Changes`, `Uncommitted Changes`, `This Turn`) with
280 > * label + uriTemplate only. Aggregate counts are filled in later by the
281 > * diff producer as compute passes complete.
282 > *
283 > * The first two entries (`Branch Changes`, `Uncommitted Changes`) are
284 > * git-only; `AgentService._attachGitState` strips them asynchronously
285 > * for sessions whose working directory is not a git repo. The backing
286 > * per-changeset states are still registered for every session — only
287 > * the catalogue advertisements are stripped.
288 > *
289 > * The compare-turns changeset (built by
290 > * {@link buildCompareTurnsChangesetUri}) is intentionally NOT included
291 > * in the default catalogue: it is subscribe-only. Clients that want
292 > * compare-turns diffs construct the URI themselves from two known
293 > * turn ids and subscribe directly.
294 > */
295 > export function buildDefaultChangesetCatalog(sessionUri: URI, state?: ISessionWithDefaultChat): Changeset[] {
296 // Session that failed to create
297 if (!state || state.lifecycle === SessionLifecycle.CreationFailed) {
src/vs/base/common/errors.ts 183 covered LOC · 32 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- errors.ts
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 > export interface ErrorListenerCallback {
7 > (error: any): void;
8 > }
9 >
10 > export interface ErrorListenerUnbind {
11 > (): void;
12 > }
13 >
14 > // Avoid circular dependency on EventEmitter by implementing a subset of the interface.
15 > export class ErrorHandler {
16 > private unexpectedErrorHandler: (e: any) => void;
17 > private listeners: ErrorListenerCallback[];
18 >
19 > constructor() {
20 >
21 > this.listeners = [];
22 >
23 > this.unexpectedErrorHandler = function (e: any) {
24 setTimeout(() => {
25 if (e.stack) {
34 }, 0);
35 };
36 > } errors.ts
37 >
38 > addListener(listener: ErrorListenerCallback): ErrorListenerUnbind {
39 this.listeners.push(listener);
40
43 };
44 }
45 > errors.ts
46 > private emit(e: any): void {
47 this.listeners.forEach((listener) => {
48 listener(e);
49 });
50 }
51 > errors.ts
52 > private _removeListener(listener: ErrorListenerCallback): void {
53 this.listeners.splice(this.listeners.indexOf(listener), 1);
54 }
55 > errors.ts
56 > setUnexpectedErrorHandler(newUnexpectedErrorHandler: (e: any) => void): void {
57 > this.unexpectedErrorHandler = newUnexpectedErrorHandler;
58 > }
59 >
60 > getUnexpectedErrorHandler(): (e: any) => void {
61 return this.unexpectedErrorHandler;
62 }
63 > errors.ts
64 > onUnexpectedError(e: any): void {
65 this.unexpectedErrorHandler(e);
66 this.emit(e);
67 }
68 > errors.ts
69 > // For external errors, we don't want the listeners to be called
70 > onUnexpectedExternalError(e: any): void {
71 this.unexpectedErrorHandler(e);
72 }
73 > } errors.ts
74 >
75 > export const errorHandler = new ErrorHandler();
76 >
77 > /** @skipMangle */
78 > export function setUnexpectedErrorHandler(newUnexpectedErrorHandler: (e: any) => void): void {
79 > errorHandler.setUnexpectedErrorHandler(newUnexpectedErrorHandler);
80 > }
81 >
82 > /**
83 > * Returns if the error is a SIGPIPE error. SIGPIPE errors should generally be
84 > * logged at most once, to avoid a loop.
85 > *
86 > * @see https://github.com/microsoft/vscode-remote-release/issues/6481
87 > */
88 > export function isSigPipeError(e: unknown): e is Error {
89 if (!e || typeof e !== 'object') {
90 return false;
94 return cast.code === 'EPIPE' && cast.syscall?.toUpperCase() === 'WRITE';
95 }
96 > errors.ts
97 > /**
98 > * This function should only be called with errors that indicate a bug in the product.
99 > * E.g. buggy extensions/invalid user-input/network issues should not be able to trigger this code path.
100 > * If they are, this indicates there is also a bug in the product.
101 > */
102 > export function onBugIndicatingError(e: any): undefined {
103 errorHandler.onUnexpectedError(e);
104 return undefined;
105 }
106 > errors.ts
107 > export function onUnexpectedError(e: any): undefined {
108 // ignore errors from cancelled promises
109 if (!isCancellationError(e)) {
112 return undefined;
113 }
114 > errors.ts
115 > export function onUnexpectedExternalError(e: any): undefined {
116 // ignore errors from cancelled promises
117 if (!isCancellationError(e)) {
120 return undefined;
121 }
122 > errors.ts
123 > type ObjectWithCode = {
124 > readonly code: unknown;
125 > };
126 >
127 function hasErrorCode(error: object): error is ObjectWithCode {
128 return Object.hasOwn(error, 'code');
129 }
130 > errors.ts
131 > export function getErrorCode(error: unknown): string | undefined {
132 if (!error || typeof error !== 'object' || !hasErrorCode(error)) {
133 return undefined;
136 return typeof code === 'string' || typeof code === 'number' ? String(code) : undefined;
137 }
138 > errors.ts
139 > export interface SerializedError {
140 > readonly $isError: true;
141 > readonly name: string;
142 > readonly message: string;
143 > readonly stack: string;
144 > readonly noTelemetry: boolean;
145 > readonly code?: string;
146 > readonly cause?: SerializedError;
147 > }
148 >
149 > type ErrorWithCode = Error & {
150 > code: string | undefined;
151 > };
152 >
153 > export function transformErrorForSerialization(error: Error): SerializedError;
154 > export function transformErrorForSerialization(error: any): any;
155 > export function transformErrorForSerialization(error: any): any {
156 if (error instanceof Error) {
157 const { name, message, cause } = error;
172 return error;
173 }
174 > errors.ts
175 > export function transformErrorFromSerialization(data: SerializedError): Error {
176 let error: Error;
177 if (data.noTelemetry) {
191 return error;
192 }
193 > errors.ts
194 > // see https://github.com/v8/v8/wiki/Stack%20Trace%20API#basic-stack-traces
195 > export interface V8CallSite {
196 > getThis(): unknown;
197 > getTypeName(): string | null;
198 > getFunction(): Function | undefined;
199 > getFunctionName(): string | null;
200 > getMethodName(): string | null;
201 > getFileName(): string | null;
202 > getLineNumber(): number | null;
203 > getColumnNumber(): number | null;
204 > getEvalOrigin(): string | undefined;
205 > isToplevel(): boolean;
206 > isEval(): boolean;
207 > isNative(): boolean;
208 > isConstructor(): boolean;
209 > toString(): string;
210 > }
211 >
212 > export const canceledName = 'Canceled';
213 >
214 > /**
215 > * Checks if the given error is a promise in canceled state
216 > */
217 > export function isCancellationError(error: any): boolean {
218 if (error instanceof CancellationError) {
219 return true;
221 return error instanceof Error && error.name === canceledName && error.message === canceledName;
222 }
223 > errors.ts
224 > // !!!IMPORTANT!!!
225 > // Do NOT change this class because it is also used as an API-type.
226 > export class CancellationError extends Error {
227 > constructor() {
228 super(canceledName);
229 this.name = this.message;
230 }
231 > } errors.ts
232 >
233 > export class PendingMigrationError extends Error {
234 >
235 > private static readonly _name = 'PendingMigrationError';
236 >
237 > static is(error: unknown): error is PendingMigrationError {
238 return error instanceof PendingMigrationError || (error instanceof Error && error.name === PendingMigrationError._name);
239 }
240 > errors.ts
241 > constructor(message: string) {
242 super(message);
243 this.name = PendingMigrationError._name;
244 }
245 > } errors.ts
246 >
247 > /**
248 > * @deprecated use {@link CancellationError `new CancellationError()`} instead
249 > */
250 > export function canceled(): Error {
251 const error = new Error(canceledName);
252 error.name = error.message;
253 return error;
254 }
255 > errors.ts
256 > export function illegalArgument(name?: string): Error {
257 if (name) {
258 return new Error(`Illegal argument: ${name}`);
261 }
262 }
263 > errors.ts
264 > export function illegalState(name?: string): Error {
265 if (name) {
266 return new Error(`Illegal state: ${name}`);
269 }
270 }
271 > errors.ts
272 > export class ReadonlyError extends TypeError {
273 > constructor(name?: string) {
274 super(name ? `${name} is read-only and cannot be changed` : 'Cannot change read-only property');
275 }
276 > } errors.ts
277 >
278 > export function getErrorMessage(err: any): string {
279 if (!err) {
280 return 'Error';
291 return String(err);
292 }
293 > errors.ts
294 > export class NotImplementedError extends Error {
295 > constructor(message?: string) {
296 super('NotImplemented');
297 if (message) {
299 }
300 }
301 > } errors.ts
302 >
303 > export class NotSupportedError extends Error {
304 > constructor(message?: string) {
305 super('NotSupported');
306 if (message) {
308 }
309 }
310 > } errors.ts
311 >
312 > export class ExpectedError extends Error {
313 readonly isExpected = true;
314 > } errors.ts
315 >
316 > /**
317 > * Error that when thrown won't be logged in telemetry as an unhandled error.
318 > */
319 > export class ErrorNoTelemetry extends Error {
320 > override readonly name: string;
321 >
322 > constructor(msg?: string) {
323 super(msg);
324 this.name = 'CodeExpectedError';
325 }
326 > errors.ts
327 > public static fromError(err: Error): ErrorNoTelemetry {
328 if (err instanceof ErrorNoTelemetry) {
329 return err;
335 return result;
336 }
337 > errors.ts
338 > public static isErrorNoTelemetry(err: Error): err is ErrorNoTelemetry {
339 return err.name === 'CodeExpectedError';
340 }
341 > } errors.ts
342 >
343 > /**
344 > * This error indicates a bug.
345 > * Do not throw this for invalid user input.
346 > * Only catch this error to recover gracefully from bugs.
347 > */
348 > export class BugIndicatingError extends Error {
349 > constructor(message?: string) {
350 super(message || 'An unexpected bug occurred.');
351 Object.setPrototypeOf(this, BugIndicatingError.prototype);
src/vs/platform/agentHost/node/localCommands/localChatCommand.ts 183 covered LOC · 8 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- localChatCommand.ts
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 > import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
7 > import { StopWatch } from '../../../../base/common/stopwatch.js';
8 > import { ILogService } from '../../../log/common/log.js';
9 > import { ISessionDataService } from '../../common/sessionDataService.js';
10 > import { ActionType, StateAction } from '../../common/state/sessionActions.js';
11 > import { isAhpChatChannel, parseRequiredSessionUriFromChatUri, ResponsePartKind, ToolCallStatus, ToolResultContentType, type ISessionWithDefaultChat, type Turn, type URI as ProtocolURI } from '../../common/state/sessionState.js';
12 > import { AgentHostLocalTurns } from '../agentHostLocalTurns.js';
13 > import { IAgentHostTerminalManager } from '../agentHostTerminalManager.js';
14 > import { AgentHostStateManager } from '../agentHostStateManager.js';
15 > import { persistSessionMetadata } from '../shared/persistSessionMetadata.js';
16 >
17 > /**
18 > * A just-started chat turn offered to the local-command dispatcher before it is
19 > * forwarded to the agent SDK.
20 > */
21 > export interface ILocalChatCommandRequest {
22 > /** The chat channel the turn was started on (default or peer chat). */
23 > readonly turnChannel: ProtocolURI;
24 > /** The turn identifier opened by the reducer for this message. */
25 > readonly turnId: string;
26 > /** The raw user message text. */
27 > readonly text: string;
28 > }
29 >
30 > /**
31 > * The narrow set of agent-host capabilities a {@link ILocalChatCommand} may use
32 > * to fulfil a request. Keeps commands decoupled from `AgentSideEffects`
33 > * internals — they emit response content by dispatching server actions and read
34 > * conversation state, plus the few extra capabilities specific commands need
35 > * (terminal execution, chat rename/persist).
36 > */
37 > export interface ILocalChatCommandContext {
38 > readonly logService: ILogService;
39 > readonly terminalManager: IAgentHostTerminalManager;
40 > /** Dispatch a server-originated action on a channel. */
41 > dispatch(channel: ProtocolURI, action: StateAction): void;
42 > /** Read the merged session/chat state for a session or chat channel. */
43 > getState(channel: ProtocolURI): ISessionWithDefaultChat | undefined;
44 > /** Rename a single chat (independently of the session title). */
45 > updateChatTitle(session: ProtocolURI, chat: ProtocolURI, title: string): void;
46 > /** Persist a session-metadata key/value pair (e.g. a custom title). */
47 > persistSessionFlag(session: ProtocolURI, key: string, value: string): void;
48 > }
49 >
50 > /**
51 > * The outcome of a {@link ILocalChatCommand.tryHandle} that accepted a request:
52 > * the work to perform plus any metadata the dispatcher and its caller need.
53 > */
54 > export interface ILocalChatCommandHandling {
55 > /** Performs the (possibly async) work of the command. */
56 > run(): Promise<void>;
57 > /**
58 > * A provisional title the command suggests for a brand-new session — for
59 > * example a `!command`'s command text. It is surfaced up through the
60 > * {@link AgentHostLocalCommands} dispatcher so the caller can title an
61 > * otherwise-untitled session; a subsequent real request replaces it with a
62 > * generated title. Commands that do not title the session omit this.
63 > */
64 > readonly suggestedTitle?: string;
65 > }
66 >
67 > /**
68 > * A generic, agent-agnostic chat command handled entirely by the agent host
69 > * (never forwarded to the agent SDK) — for example `/rename` or `!command`.
70 > *
71 > * A command decides synchronously whether it applies (so the caller knows
72 > * immediately not to forward the message), then performs its work — emitting
73 > * response parts/tool calls via {@link ILocalChatCommandContext}. The
74 > * {@link AgentHostLocalCommands} dispatcher owns the common tail: completing the
75 > * turn, optionally persisting it as a local turn (so it survives reload and
76 > * anchors fork/truncate), and draining the message queue.
77 > */
78 > export interface ILocalChatCommand extends IDisposable {
79 > /** Stable identifier for logging/telemetry. */
80 > readonly name: string;
81 > /**
82 > * Whether the completed turn should be persisted as a host-injected local
83 > * turn (survives reload; anchors fork/truncate to the preceding concrete
84 > * turn). Most user-visible commands want `true`.
85 > */
86 > readonly recordsLocalTurn: boolean;
87 > /**
88 > * Synchronously decide whether this command handles `request`. Returns an
89 > * {@link ILocalChatCommandHandling} describing the (possibly async) work when
90 > * it does, or `undefined` to decline so the dispatcher tries the next command
91 > * (and ultimately forwards the message to the agent).
92 > */
93 > tryHandle(request: ILocalChatCommandRequest): ILocalChatCommandHandling | undefined;
94 > }
95 >
96 > /** Constructs a {@link ILocalChatCommand} bound to a context. */
97 > export interface ILocalChatCommandCtor {
98 > new(context: ILocalChatCommandContext): ILocalChatCommand;
99 > }
100 >
101 > /**
102 > * Global registry of {@link ILocalChatCommand} constructors. Command modules
103 > * register themselves at load time; {@link AgentHostLocalCommands} instantiates
104 > * all registered commands per session-effects instance with its context.
105 > */
106 > class LocalChatCommandRegistryImpl {
107 > private readonly _ctors: ILocalChatCommandCtor[] = [];
108 >
109 > register(ctor: ILocalChatCommandCtor): void {
110 > this._ctors.push(ctor);
111 > }
112 >
113 > createAll(context: ILocalChatCommandContext): ILocalChatCommand[] {
114 > return this._ctors.map(ctor => new ctor(context)); localChatCommand.ts
115 > }
117 >
118 > export const LocalChatCommandRegistry = new LocalChatCommandRegistryImpl();
119 >
120 > /**
121 > * Dispatches just-started turns to the registered {@link ILocalChatCommand}s
122 > * and owns everything a host-handled command needs end-to-end: it builds the
123 > * {@link ILocalChatCommandContext} from the state manager and injected services,
124 > * runs the first accepting command, then performs the common tail — completing
125 > * the turn, persisting it as a local turn (so it survives reload and anchors
126 > * fork/truncate), and asking the owner to drain the message queue.
127 > */
128 > export class AgentHostLocalCommands extends Disposable {
129 >
130 > private readonly _commands: readonly ILocalChatCommand[];
131 >
132 > constructor(
133 > private readonly _stateManager: AgentHostStateManager, localChatCommand.ts
134 > private readonly _localTurns: AgentHostLocalTurns,
135 > /**
136 > * Invoked after a handled turn is completed so the owner can start the
137 > * next queued message. Draining re-enters the agent-send pipeline, which
138 > * is the owner's concern — not the dispatcher's.
139 > */
140 > private readonly _notifyTurnConsumable: (turnChannel: ProtocolURI) => void,
141 > @ILogService private readonly _logService: ILogService,
142 > @IAgentHostTerminalManager private readonly _terminalManager: IAgentHostTerminalManager,
143 > @ISessionDataService private readonly _sessionDataService: ISessionDataService,
144 > ) {
145 > super();
146 > const context: ILocalChatCommandContext = {
147 > logService: this._logService,
148 > terminalManager: this._terminalManager,
149 > dispatch: (channel, action) => this._stateManager.dispatchServerAction(channel, action),
150 > getState: channel => this._stateManager.getSessionState(channel),
151 > updateChatTitle: (session, chat, title) => this._stateManager.updateChatTitle(session, chat, title),
152 > persistSessionFlag: (session, key, value) => persistSessionMetadata(this._sessionDataService, this._logService, session, key, value),
153 > };
154 > this._commands = LocalChatCommandRegistry.createAll(context).map(command => this._register(command));
155 > }
157 > /**
158 > * Offers `request` to each command. When one handles it, the dispatcher has
159 > * already scheduled its `run`; it returns the {@link ILocalChatCommandHandling}
160 > * so the caller can act on carried metadata such as
161 > * {@link ILocalChatCommandHandling.suggestedTitle}. Its presence means the
162 > * caller MUST NOT forward the message to the agent (and MUST NOT invoke `run`
163 > * again). Returns `undefined` when no command applies.
164 > */
165 > tryHandle(request: ILocalChatCommandRequest): ILocalChatCommandHandling | undefined {
166 for (const command of this._commands) {
167 const handling = command.tryHandle(request);
173 return undefined;
174 }
176 > private async _run(command: ILocalChatCommand, handling: ILocalChatCommandHandling, request: ILocalChatCommandRequest): Promise<void> {
177 const stopWatch = StopWatch.create(false);
178 try {
192 }
193 }
195 > /**
196 > * Records the just-completed turn `turnId` as a host-injected local turn so
197 > * it survives reload and fork/truncate can resolve it to the preceding
198 > * concrete turn. Works uniformly for the default chat and any peer chat —
199 > * the turn is keyed by its chat channel. Live terminal references are
200 > * stripped from the payload (the PTY does not survive a reload).
201 > */
202 > private _recordLocalTurn(turnChannel: ProtocolURI, turnId: string): void {
203 const chat = turnChannel;
204 const session = isAhpChatChannel(turnChannel) ? parseRequiredSessionUriFromChatUri(turnChannel) : turnChannel;
222 this._localTurns.record(session, chat, sanitizeLocalTurnForPersistence(turns[index]), anchorTurnId);
223 }
225 >
226 > /**
227 > * Prepares a host-injected local turn for persistence by dropping live
228 > * {@link ToolResultContentType.Terminal} references from its tool calls — the
229 > * PTY does not survive a reload, so only the captured output (text) is kept.
230 > */
231 function sanitizeLocalTurnForPersistence(turn: Turn): Turn {
232 const responseParts = turn.responseParts.map(part => {
src/vs/platform/instantiation/common/instantiationService.ts 177 covered LOC · 41 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- instantiationService.ts
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 > import { GlobalIdleValue } from '../../../base/common/async.js';
7 > import { Event } from '../../../base/common/event.js';
8 > import { illegalState } from '../../../base/common/errors.js';
9 > import { DisposableStore, dispose, IDisposable, isDisposable, toDisposable } from '../../../base/common/lifecycle.js';
10 > import { SyncDescriptor, SyncDescriptor0 } from './descriptors.js';
11 > import { Graph } from './graph.js';
12 > import { GetLeadingNonServiceArgs, IInstantiationService, ServiceIdentifier, ServicesAccessor, _util } from './instantiation.js';
13 > import { ServiceCollection } from './serviceCollection.js';
14 > import { LinkedList } from '../../../base/common/linkedList.js';
15 >
16 > // TRACING
17 > const _enableAllTracing = false
18 > // || "TRUE" // DO NOT CHECK IN!
19 > ;
20 >
21 > class CyclicDependencyError extends Error {
22 > constructor(graph: Graph<any>) {
23 super('cyclic dependency between services');
24 this.message = graph.findCycleSlow() ?? `UNABLE to detect cycle, dumping graph: \n${graph.toString()}`;
25 }
27 >
28 > export class InstantiationService implements IInstantiationService {
29 >
30 > declare readonly _serviceBrand: undefined;
31 >
32 > readonly _globalGraph?: Graph<string>;
33 > private _globalGraphImplicitDependency?: string;
34 >
35 > private _isDisposed = false;
36 > private readonly _servicesToMaybeDispose = new Set<any>();
37 > private readonly _children = new Set<InstantiationService>();
38 >
39 > constructor(
40 > private readonly _services: ServiceCollection = new ServiceCollection(), instantiationService.ts
41 > private readonly _strict: boolean = false,
42 > private readonly _parent?: InstantiationService,
43 > private readonly _enableTracing: boolean = _enableAllTracing
44 > ) {
45 >
46 > this._services.set(IInstantiationService, this);
47 > this._globalGraph = _enableTracing ? _parent?._globalGraph ?? new Graph(e => e) : undefined;
48 > }
50 > dispose(): void {
51 > if (!this._isDisposed) { instantiationService.ts
52 > this._isDisposed = true;
53 > // dispose all child services
54 > dispose(this._children);
55 > this._children.clear();
56 >
57 > // dispose all services created by this service
58 > for (const candidate of this._servicesToMaybeDispose) {
59 if (isDisposable(candidate)) {
60 candidate.dispose();
61 }
62 }
63 > this._servicesToMaybeDispose.clear(); instantiationService.ts
64 > }
65 > }
67 > private _throwIfDisposed(): void {
68 > if (this._isDisposed) { instantiationService.ts
69 throw new Error('InstantiationService has been disposed');
70 }
73 > createChild(services: ServiceCollection, store?: DisposableStore): IInstantiationService {
74 this._throwIfDisposed();
75
86 return result;
87 }
89 > invokeFunction<R, TS extends any[] = []>(fn: (accessor: ServicesAccessor, ...args: TS) => R, ...args: TS): R {
90 this._throwIfDisposed();
91
113 }
114 }
116 > createInstance<T>(descriptor: SyncDescriptor0<T>): T;
117 > createInstance<Ctor extends new (...args: any[]) => unknown, R extends InstanceType<Ctor>>(ctor: Ctor, ...args: GetLeadingNonServiceArgs<ConstructorParameters<Ctor>>): R;
118 > createInstance(ctorOrDescriptor: any | SyncDescriptor<any>, ...rest: unknown[]): unknown {
119 > this._throwIfDisposed(); instantiationService.ts
120 >
121 > let _trace: Trace;
122 > let result: unknown;
123 > if (ctorOrDescriptor instanceof SyncDescriptor) {
124 _trace = Trace.traceCreation(this._enableTracing, ctorOrDescriptor.ctor);
125 result = this._createInstance(ctorOrDescriptor.ctor, ctorOrDescriptor.staticArguments.concat(rest), _trace);
126 > } else { instantiationService.ts
127 > _trace = Trace.traceCreation(this._enableTracing, ctorOrDescriptor);
128 > result = this._createInstance(ctorOrDescriptor, rest, _trace);
129 > }
130 > _trace.stop();
131 > return result;
132 > }
134 > private _createInstance<T>(ctor: any, args: unknown[] = [], _trace: Trace): T {
136 > // arguments defined by service decorators
137 > const serviceDependencies = _util.getServiceDependencies(ctor).sort((a, b) => a.index - b.index);
138 > const serviceArgs: unknown[] = [];
139 > for (const dependency of serviceDependencies) {
140 > const service = this._getOrCreateServiceInstance(dependency.id, _trace); instantiationService.ts
141 > if (!service) {
142 this._throwIfStrict(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id}.`, false);
143 }
144 > serviceArgs.push(service); instantiationService.ts
145 > }
147 > const firstServiceArgPos = serviceDependencies.length > 0 ? serviceDependencies[0].index : args.length;
148 >
149 > // check for argument mismatches, adjust static args if needed
150 > if (args.length !== firstServiceArgPos) {
151 console.trace(`[createInstance] First service dependency of ${ctor.name} at position ${firstServiceArgPos + 1} conflicts with ${args.length} static arguments`);
152
158 }
159 }
161 > // now create the instance
162 > return Reflect.construct<any, T>(ctor, args.concat(serviceArgs));
163 > }
165 > private _setCreatedServiceInstance<T>(id: ServiceIdentifier<T>, instance: T): void {
166 if (this._services.get(id) instanceof SyncDescriptor) {
167 this._services.set(id, instance);
172 }
173 }
175 > private _getServiceInstanceOrDescriptor<T>(id: ServiceIdentifier<T>): T | SyncDescriptor<T> {
176 > const instanceOrDesc = this._services.get(id); instantiationService.ts
177 > if (!instanceOrDesc && this._parent) {
178 return this._parent._getServiceInstanceOrDescriptor(id);
179 > } else { instantiationService.ts
180 > return instanceOrDesc;
181 > }
182 > }
184 > protected _getOrCreateServiceInstance<T>(id: ServiceIdentifier<T>, _trace: Trace): T {
185 > if (this._globalGraph && this._globalGraphImplicitDependency) { instantiationService.ts
186 this._globalGraph.insertEdge(this._globalGraphImplicitDependency, String(id));
187 }
188 > const thing = this._getServiceInstanceOrDescriptor(id); instantiationService.ts
189 > if (thing instanceof SyncDescriptor) {
190 return this._safeCreateAndCacheServiceInstance(id, thing, _trace.branch(id, true));
191 > } else { instantiationService.ts
192 > _trace.branch(id, false); instantiationService.ts
193 > return thing;
194 > }
197 > private readonly _activeInstantiations = new Set<ServiceIdentifier<any>>();
198 >
199 >
200 > private _safeCreateAndCacheServiceInstance<T>(id: ServiceIdentifier<T>, desc: SyncDescriptor<T>, _trace: Trace): T {
201 if (this._activeInstantiations.has(id)) {
202 throw new Error(`illegal state - RECURSIVELY instantiating service '${id}'`);
209 }
210 }
212 > private _createAndCacheServiceInstance<T>(id: ServiceIdentifier<T>, desc: SyncDescriptor<T>, _trace: Trace): T {
213
214 type Triple = { id: ServiceIdentifier<any>; desc: SyncDescriptor<any>; _trace: Trace };
279 return <T>this._getServiceInstanceOrDescriptor(id);
280 }
282 > private _createServiceInstanceWithOwner<T>(id: ServiceIdentifier<T>, ctor: any, args: unknown[] = [], supportsDelayedInstantiation: boolean, _trace: Trace): T {
283 if (this._services.get(id) instanceof SyncDescriptor) {
284 return this._createServiceInstance(id, ctor, args, supportsDelayedInstantiation, _trace, this._servicesToMaybeDispose);
289 }
290 }
292 > private _createServiceInstance<T>(id: ServiceIdentifier<T>, ctor: any, args: unknown[] = [], supportsDelayedInstantiation: boolean, _trace: Trace, disposeBucket: Set<any>): T {
293 if (!supportsDelayedInstantiation) {
294 // eager instantiation
384 }
385 }
387 > private _throwIfStrict(msg: string, printWarning: boolean): void {
388 if (printWarning) {
389 console.warn(msg);
393 }
394 }
396 >
397 > //#region -- tracing ---
398 >
399 > const enum TraceType {
400 > None = 0,
401 > Creation = 1,
402 > Invocation = 2,
403 > Branch = 3,
404 > }
405 >
406 > export class Trace {
407 >
408 > static all = new Set<string>();
409 >
410 > private static readonly _None = new class extends Trace {
411 > constructor() { super(TraceType.None, null); }
412 > override stop() { }
413 > override branch() { return this; }
414 > };
415 >
416 > static traceInvocation(_enableTracing: boolean, ctor: any): Trace {
417 return !_enableTracing ? Trace._None : new Trace(TraceType.Invocation, ctor.name || new Error().stack!.split('\n').slice(3, 4).join('\n'));
418 }
420 > static traceCreation(_enableTracing: boolean, ctor: any): Trace {
421 > return !_enableTracing ? Trace._None : new Trace(TraceType.Creation, ctor.name); instantiationService.ts
422 > }
424 > private static _totals: number = 0;
425 > private readonly _start: number = Date.now();
426 > private readonly _dep: [ServiceIdentifier<any>, boolean, Trace?][] = [];
427 >
428 > private constructor(
429 > readonly type: TraceType,
430 > readonly name: string | null
431 > ) { }
432 >
433 > branch(id: ServiceIdentifier<any>, first: boolean): Trace {
434 const child = new Trace(TraceType.Branch, id.toString());
435 this._dep.push([id, first, child]);
436 return child;
437 }
439 > stop() {
440 const dur = Date.now() - this._start;
441 Trace._totals += dur;
src/vs/platform/agentHost/common/state/protocol/channels-terminal/state.ts 173 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- state.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > import type { URI } from '../common/state.js';
10 >
11 > // ─── Terminal Types ──────────────────────────────────────────────────────────
12 >
13 > /**
14 > * Lightweight terminal metadata exposed on the root state.
15 > *
16 > * @category Terminal Types
17 > */
18 > export interface TerminalInfo {
19 > /** Terminal URI (subscribable for full terminal state) */
20 > resource: URI;
21 > /** Human-readable terminal title */
22 > title: string;
23 > /** Who currently holds this terminal */
24 > claim: TerminalClaim;
25 > /** Process exit code, if the terminal process has exited */
26 > exitCode?: number;
27 > }
28 >
29 > /**
30 > * Discriminant for terminal claim kinds.
31 > *
32 > * @category Terminal Types
33 > */
34 > export const enum TerminalClaimKind {
35 > Client = 'client',
36 > Session = 'session',
37 > }
38 >
39 > /**
40 > * A terminal claimed by a connected client.
41 > *
42 > * @category Terminal Types
43 > */
44 > export interface TerminalClientClaim {
45 > /** Discriminant */
46 > kind: TerminalClaimKind.Client;
47 > /** The `clientId` of the claiming client */
48 > clientId: string;
49 > }
50 >
51 > /**
52 > * A terminal claimed by a session, optionally scoped to a specific turn or tool call.
53 > *
54 > * @category Terminal Types
55 > */
56 > export interface TerminalSessionClaim {
57 > /** Discriminant */
58 > kind: TerminalClaimKind.Session;
59 > /** Session URI that claimed the terminal */
60 > session: URI;
61 > /** Optional turn identifier within the session */
62 > turnId?: string;
63 > /** Optional tool call identifier within the turn */
64 > toolCallId?: string;
65 > }
66 >
67 > /**
68 > * Describes who currently holds a terminal. A terminal may be claimed by
69 > * either a connected client or a session (e.g. during a tool call).
70 > *
71 > * @category Terminal Types
72 > */
73 > export type TerminalClaim = TerminalClientClaim | TerminalSessionClaim;
74 >
75 > /**
76 > * Full state for a single terminal, loaded when a client subscribes to the terminal's URI.
77 > *
78 > * @category Terminal Types
79 > */
80 > export interface TerminalState {
81 > /** Human-readable terminal title */
82 > title: string;
83 > /** Current working directory of the terminal process */
84 > cwd?: URI;
85 > /** Terminal width in columns */
86 > cols?: number;
87 > /** Terminal height in rows */
88 > rows?: number;
89 > /**
90 > * Typed content parts, replacing the flat `content: string`.
91 > *
92 > * Naive consumers that only need the raw VT stream can reconstruct it with:
93 > * `content.map(p => p.type === 'command' ? p.output : p.value).join('')`
94 > *
95 > * Consumers that need command boundaries can filter by part type.
96 > */
97 > content: TerminalContentPart[];
98 > /** Process exit code, set when the terminal process exits */
99 > exitCode?: number;
100 > /** Who currently holds this terminal */
101 > claim: TerminalClaim;
102 > /**
103 > * Whether this terminal emits `terminal/commandExecuted` and
104 > * `terminal/commandFinished` actions and populates `command`-typed parts.
105 > *
106 > * Clients MUST check this flag before relying on command detection.
107 > * Do NOT use the presence of a `command` part as a feature flag — parts
108 > * are absent in the normal idle state.
109 > */
110 > supportsCommandDetection?: boolean;
111 > /**
112 > * Whether this terminal-style resource is backed by a pseudoterminal.
113 > * When `false`, output is plain text and clients do not need to parse
114 > * VT sequences.
115 > */
116 > isPty?: boolean;
117 > }
118 >
119 > // ─── Terminal Content Parts ──────────────────────────────────────────────────
120 >
121 > /**
122 > * A content part within terminal output.
123 > *
124 > * @category Terminal Types
125 > */
126 > export type TerminalContentPart =
127 > | TerminalUnclassifiedPart
128 > | TerminalCommandPart;
129 >
130 > /**
131 > * Unstructured terminal output — content before, between, or after commands,
132 > * or from terminals that do not support command detection.
133 > *
134 > * @category Terminal Types
135 > */
136 > export interface TerminalUnclassifiedPart {
137 > type: 'unclassified';
138 > /** Accumulated VT output. Appended to by `terminal/data` when no command is executing. */
139 > value: string;
140 > }
141 >
142 > /**
143 > * A single command: its command line and the output it produced.
144 > *
145 > * While `isComplete` is false the command is still executing; `output` grows
146 > * as `terminal/data` actions arrive. At `terminal/commandFinished` the part
147 > * is mutated in-place with `isComplete: true` and the completion metadata.
148 > *
149 > * @category Terminal Types
150 > */
151 > export interface TerminalCommandPart {
152 > type: 'command';
153 > /**
154 > * Stable id matching the `commandId` on the corresponding
155 > * `terminal/commandExecuted` and `terminal/commandFinished` actions.
156 > */
157 > commandId: string;
158 > /** The command line submitted to the shell. */
159 > commandLine: string;
160 > /**
161 > * Accumulated VT output. Appended to by `terminal/data` while `isComplete`
162 > * is false. Shell integration escape sequences are stripped by the server.
163 > */
164 > output: string;
165 > /** Unix timestamp (ms) when execution started, as reported by the server. */
166 > timestamp: number;
167 > /** Whether the command has finished. */
168 > isComplete: boolean;
169 > /** Shell exit code. Set at completion. `undefined` if unknown. */
170 > exitCode?: number;
171 > /** Wall-clock duration in milliseconds. Set at completion. */
172 > durationMs?: number;
173 > }
src/vs/platform/agentHost/node/agentHostChangesetCoordinator.ts 172 covered LOC · 17 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostChangesetCoordinator.ts
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 > import { Disposable } from '../../../base/common/lifecycle.js';
7 > import { URI } from '../../../base/common/uri.js';
8 > import { IAgentSessionMetadata } from '../common/agentService.js';
9 > import { buildBranchChangesetUri, ChangesetKind, parseChangesetUri } from '../common/changesetUri.js';
10 > import { ChangesetFileMonitorCoordinator } from './agentHostChangesetFileMonitorCoordinator.js';
11 > import { AgentHostStateManager, IAgentHostStateManager } from './agentHostStateManager.js';
12 > import { IAgentHostChangesetService, META_CHANGESET_BRANCH, META_CHANGESET_SESSION, META_LEGACY_DIFFS } from '../common/agentHostChangesetService.js';
13 > import { IAgentHostChangesetSubscriptionService } from '../common/agentHostChangesetSubscriptionService.js';
14 > import { IAgentHostChangesetOperationService } from '../common/agentHostChangesetOperationService.js';
15 > import { IAgentHostGitStateService } from '../common/agentHostGitStateService.js';
16 > import { IInstantiationService } from '../../instantiation/common/instantiation.js';
17 > import { isAhpChatChannel } from '../common/state/sessionState.js';
18 >
19 > /**
20 > * Raw metadata blob values for the session DB, batch-read by the caller.
21 > * Keys are the changeset-specific metadata keys ({@link META_CHANGESET_BRANCH}
22 > * etc.); values are the raw `string | undefined` payloads as returned by
23 > * `ISessionDatabase.getMetadataObject`.
24 > */
25 > export type IChangesetSessionMetadata = Record<string, string | undefined>;
26 >
27 > /**
28 > * Coordinator that encapsulates all `AgentService`-side orchestration of
29 > * the changeset feature. Sits between `AgentService` (which owns session
30 > * lifecycle / subscription refcounting / batched DB reads) and
31 > * {@link IAgentHostChangesetService} (which owns compute / publish /
32 > * persist primitives).
33 > *
34 > * Owns only URI routing and forwards lifecycle signals. Subscription state is
35 > * recorded in the shared changeset subscription service. All computation,
36 > * working-directory gating, and the deferred-refresh state machine live in
37 > * {@link IAgentHostChangesetService}.
38 > *
39 > * No per-session controllers — the cross-cutting concerns (listSessions
40 > * overlay, subscribe URI routing) inherently span sessions, so a single
41 > * coordinator with internal maps is simpler than per-session RAII.
42 > */
43 > export class AgentHostChangesetCoordinator extends Disposable {
44 > private readonly _changesetFileMonitor: ChangesetFileMonitorCoordinator;
45 >
46 > constructor(
47 > @IAgentHostStateManager private readonly _stateManager: AgentHostStateManager, agentHostChangesetCoordinator.ts
48 > @IAgentHostChangesetOperationService private readonly _changesetOperationService: IAgentHostChangesetOperationService,
49 > @IAgentHostChangesetService private readonly _changesets: IAgentHostChangesetService,
50 > @IAgentHostChangesetSubscriptionService private readonly _changesetSubscriptions: IAgentHostChangesetSubscriptionService,
51 > @IAgentHostGitStateService gitStateService: IAgentHostGitStateService,
52 > @IInstantiationService instantiationService: IInstantiationService,
53 > ) {
54 > super();
55 >
56 > this._changesetFileMonitor = this._register(instantiationService.createInstance(ChangesetFileMonitorCoordinator));
57 > this._register(gitStateService.onDidRefreshSessionGitState(sessionStr => this.onDidRunSessionGitStateRefresh(sessionStr)));
58 > }
60 > // ---- Lifecycle hooks ----------------------------------------------------
61 >
62 > /**
63 > * Seeds the create-time catalogue and registers its backing changeset state
64 > * before `SessionReady` is dispatched.
65 > */
66 > onSessionCreated(sessionStr: string): void {
67 this._changesets.refreshChangesetCatalog(sessionStr);
68 this._changesets.registerStaticChangesets(sessionStr);
69 }
71 > /**
72 > * Called at session restore time. Registers the static changeset URIs
73 > * and reseeds them from any persisted blobs already read from the DB.
74 > * `metadata` must come from the same batched `getMetadataObject` call
75 > * `AgentService` already issues for title / read / archive / config
76 > * keys.
77 > */
78 > onSessionRestored(sessionStr: string, metadata: IChangesetSessionMetadata): void {
79 this._changesets.refreshChangesetCatalog(sessionStr);
80 this._changesets.registerStaticChangesets(sessionStr);
90 this._changesetFileMonitor.onSessionRestored(sessionStr);
91 }
93 > /**
94 > * Called when a provisional session is materialized (working directory
95 > * becomes known). Drains any static changeset refresh that was deferred
96 > * because the working directory was not yet known.
97 > */
98 > onSessionMaterialized(sessionStr: string): void {
99 this._changesets.refreshChangesetCatalog(sessionStr);
100 this._changesets.onWorkingDirectoryAvailable(sessionStr);
102 this._changesetFileMonitor.onSessionMaterialized(sessionStr);
103 }
105 > /**
106 > * Called when a session is disposed. Forgets any pending refresh
107 > * queued for that session.
108 > */
109 > onSessionDisposed(sessionStr: string): void {
110 this._changesets.onSessionDisposed(sessionStr);
111 this._changesetFileMonitor.onSessionDisposed(sessionStr);
113 this._changesetSubscriptions.clearSessionSubscriptions(sessionStr);
114 }
116 > onSessionTurnActiveChanged(sessionStr: string, active: boolean): void {
117 this._changesetFileMonitor.onSessionTurnActiveChanged(sessionStr, active);
118
122 this._changesetOperationService.updateOperations(sessionStr);
123 }
125 > // ---- Subscription hooks -------------------------------------------------
126 >
127 > /**
128 > * Called on every `addSubscriber` 0→1 transition. When `resource` is a
129 > * static changeset URI, triggers the first git-diff refresh (the
130 > * changeset service self-defers it when the working directory is not yet
131 > * known).
132 > *
133 > * Both {@link AgentService.subscribe} and the handshake fast-path
134 > * (`ProtocolServerHandler.initialSubscriptions`) call into
135 > * `addSubscriber`, so this single hook covers both paths.
136 > */
137 > onFirstSubscriber(resource: URI): void {
138 const resourceStr = resource.toString();
139 const parsed = parseChangesetUri(resourceStr);
182 }
183 }
185 > /**
186 > * Called when a resource's last subscriber drops. Removes the
187 > * changeset from the session's subscription set so a later
188 > * materialization / git-state recompute (driven by
189 > * {@link IAgentHostChangesetService.recomputeSubscribedChangesets})
190 > * naturally skips it — no explicit cancellation needed.
191 > */
192 > onLastSubscriber(resource: URI): void {
193 const resourceStr = resource.toString();
194 const parsed = parseChangesetUri(resourceStr);
217 }
218 }
220 > /**
221 > * Restores the parent session when `resource` is a changeset URI and the
222 > * parent session is not already live. Non-changeset URIs are ignored.
223 > *
224 > * This is intentionally narrower than {@link tryHandleSubscribe}: it does
225 > * not compute per-turn / compare changesets and does not register static
226 > * changesets. It exists for the AgentService subscribe path where
227 > * `addSubscriber` may have already created a placeholder changeset snapshot
228 > * before the parent session restore had a chance to apply persisted diffs.
229 > */
230 > async restoreSessionIfChangesetSubscription(resource: URI, restoreSession: (session: URI) => Promise<void>): Promise<void> {
231 const resourceStr = resource.toString();
232 const parsed = parseChangesetUri(resourceStr);
241 }
242 }
244 > /**
245 > * If `resource` is a known changeset URI (uncommitted / session /
246 > * turn), seeds its state on the state manager and returns `true`.
247 > * Returns `false` for non-changeset URIs so callers fall through to
248 > * their default routing (session / subagent / terminal).
249 > *
250 > * The parent session is restored via the provided `restoreSession`
251 > * callback when no live state exists yet — this matches the previous
252 > * inline behaviour in `AgentService.subscribe`.
253 > *
254 > * Throws when the URI matches the changeset shape but the id is not
255 > * a well-known kind ({@link ChangesetKind.Unknown}). The unknown-id
256 > * rejection MUST fire before any parent-session restore so subscribing
257 > * to a bogus child URI cannot materialize the parent as a side effect.
258 > */
259 > async tryHandleSubscribe(resource: URI, restoreSession: (session: URI) => Promise<void>): Promise<boolean> {
260 const resourceStr = resource.toString();
261 const parsed = parseChangesetUri(resourceStr);
285 return true;
286 }
288 > private _addSubscription(sessionStr: string, changesetStr: string) {
289 this._changesetSubscriptions.addSubscription(sessionStr, changesetStr);
290 }
292 > private _removeSubscription(sessionStr: string, changesetStr: string) {
293 this._changesetSubscriptions.removeSubscription(sessionStr, changesetStr);
294 }
296 > // ---- listSessions overlay ----------------------------------------------
297 >
298 > /**
299 > * Returns the session-DB metadata keys to merge into a batched read
300 > * for `sessionStr`, OR `undefined` when live state already answers
301 > * the aggregate-counts question. Delegates to the changeset service,
302 > * which owns the live-vs-persisted decision.
303 > */
304 > getListMetadataKeys(sessionStr: string): Record<string, true> | undefined {
305 return this._changesets.getListMetadataKeys(sessionStr);
306 }
308 > /**
309 > * Decorates a single listSessions entry with the `changes` aggregate
310 > * (additions / deletions / files for the session-wide changeset). The
311 > * aggregate computation lives in the changeset service; the coordinator
312 > * only projects the result onto the entry.
313 > */
314 > decorateListEntry(entry: IAgentSessionMetadata, metadata: IChangesetSessionMetadata): IAgentSessionMetadata {
315 const changes = this._changesets.computeListEntryChanges(entry.session.toString(), metadata);
316 return changes ? { ...entry, changes } : entry;
317 }
319 > // ---- Git state events -------------------------------------------------
320 >
321 > /**
322 > * Called when a session's Git state is refreshed.
323 > */
324 > private onDidRunSessionGitStateRefresh(sessionStr: string): void {
325 // Refresh the list of changesets for the session.
326 this._changesets.refreshChangesetCatalog(sessionStr);
src/vs/platform/agentHost/node/shared/forwardedChatError.ts 170 covered LOC · 12 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- forwardedChatError.ts
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 > import { CopilotApiError, COPILOT_API_ERROR_STATUS_STREAMING } from './copilotApiService.js';
7 >
8 > /**
9 > * Marker prefix used to smuggle a structured, serialized chat fetch error
10 > * through the agent SDK subprocess boundary. The model proxies run in this
11 > * (the agent host) process and hold the rich {@link CopilotApiError}, but the
12 > * agent SDKs (Claude, Codex, Copilot CLI) run as child processes that only
13 > * see an HTTP/SSE error. The proxy appends `VSCODE_PROXY_ERROR:<base64>` to
14 > * the error message; the SDK forwards that text back verbatim, and the agent
15 > * decodes it on the way out.
16 > *
17 > * Mirrors the Copilot Chat extension's `PROXY_ERROR_PREFIX`
18 > * (`extensions/copilot/src/extension/chatSessions/claude/common/claudeMessageDispatch.ts`).
19 > */
20 > export const PROXY_ERROR_PREFIX = 'VSCODE_PROXY_ERROR:';
21 >
22 > /**
23 > * Upper bound on the base64 marker payload we will decode. A forwarded chat
24 > * error serializes to well under 1 KB; this cap prevents an oversized or
25 > * adversarial marker riding along in model-influenced error text from driving
26 > * an unbounded base64/JSON allocation.
27 > */
28 > const MAX_FORWARDED_MARKER_B64_LENGTH = 8 * 1024;
29 >
30 > /** Standard base64 alphabet with optional padding. */
31 > const FORWARDED_MARKER_B64_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;
32 >
33 > /**
34 > * Serialized chat fetch error payload. This is the JSON shape forwarded over
35 > * the protocol's `ErrorInfo._meta.chatError`. The core consumer
36 > * (`src/vs/workbench/contrib/chat/common/chatErrorMessages.ts`) reads the same
37 > * JSON shape to render localized, user-facing messages. The two definitions
38 > * are intentionally decoupled (the platform/node layer cannot import workbench
39 > * code), so any field change must be mirrored on both sides.
40 > */
41 > export interface IForwardedChatFetchError {
42 > /** Mirrors the extension's `ChatFetchResponseType` string value. */
43 > readonly type: string;
44 > readonly reason?: string;
45 > readonly requestId?: string;
46 > readonly serverRequestId?: string;
47 > readonly category?: string;
48 > readonly retryAfter?: number;
49 > readonly isAuto?: boolean;
50 > readonly capiError?: { readonly code?: string; readonly message?: string };
51 > }
52 >
53 > /**
54 > * The full forwarded chat error placed at `ErrorInfo._meta.chatError`.
55 > */
56 > export interface IForwardedChatError {
57 > readonly fetchError: IForwardedChatFetchError;
58 > readonly copilotPlan?: string;
59 > readonly isUsageBasedBilling?: boolean;
60 > readonly quotaResetDate?: string;
61 > }
62 >
63 > /**
64 > * Maps a {@link CopilotApiError} HTTP status (or the mid-stream streaming
65 > * sentinel) to the extension's `ChatFetchResponseType` string value. Kept in
66 > * sync with the Copilot Chat extension's error classification so the core
67 > * formatter produces identical messages.
68 > */
69 function statusToFetchType(status: number): string {
70 switch (status) {
86 }
87 }
89 > /**
90 > * Builds a {@link IForwardedChatError} from a {@link CopilotApiError}. The
91 > * error's Anthropic envelope carries the upstream message and type, which are
92 > * surfaced as `reason`/`capiError` so the core formatter can render the right
93 > * message (rate limit, quota, filtered, etc.).
94 > */
95 > export function buildForwardedChatError(err: CopilotApiError): IForwardedChatError {
96 const status = err.status === COPILOT_API_ERROR_STATUS_STREAMING ? 502 : err.status;
97 const requestId = typeof err.envelope.request_id === 'string' ? err.envelope.request_id : '';
111 };
112 }
114 > /**
115 > * Attempts to parse a CAPI-style error body (`{ "error": { "code", "message" } }`)
116 > * out of an envelope message string. Returns `undefined` when the message is
117 > * not such a JSON payload.
118 > */
119 function extractCapiError(message: string): { code?: string; message?: string } | undefined {
120 let parsed: unknown;
141 };
142 }
144 > /**
145 > * Encodes a {@link IForwardedChatError} as a `VSCODE_PROXY_ERROR:<base64>`
146 > * marker string. Base64 survives the SDK's JSON re-encoding without
147 > * double-escaping issues.
148 > */
149 > export function encodeForwardedChatError(forwarded: IForwardedChatError): string {
150 return `${PROXY_ERROR_PREFIX}${Buffer.from(JSON.stringify(forwarded)).toString('base64')}`;
151 }
153 > /**
154 > * Fields from a structured agent-SDK error (notably the Copilot CLI SDK's
155 > * `ErrorData`) used to build a forwarded chat error directly, without a
156 > * {@link PROXY_ERROR_PREFIX} marker. The Copilot CLI authenticates with CAPI
157 > * itself (no VS Code proxy to embed a marker), but its `session.error` event
158 > * already carries the structured classification we need.
159 > */
160 > export interface ISdkChatErrorFields {
161 > readonly errorType: string;
162 > readonly errorCode?: string;
163 > readonly message: string;
164 > readonly statusCode?: number;
165 > readonly providerCallId?: string;
166 > readonly serviceRequestId?: string;
167 > }
168 >
169 > /**
170 > * Maps an agent-SDK error category (and optional HTTP status) to the
171 > * extension's `ChatFetchResponseType` string value, or `undefined` when the
172 > * error is not a model/CAPI error we can render richly. Categories mirror the
173 > * Copilot CLI SDK's `ErrorData.errorType` values.
174 > */
175 function sdkErrorTypeToFetchType(errorType: string, statusCode: number | undefined): string | undefined {
176 switch (errorType) {
187 return statusCode !== undefined ? statusToFetchType(statusCode) : undefined;
188 }
190 > /**
191 > * Builds a {@link IForwardedChatError} from a structured agent-SDK error.
192 > * Returns `undefined` when the error cannot be classified as a model/CAPI
193 > * error, so callers can fall back to the raw message.
194 > */
195 > export function buildForwardedChatErrorFromFields(data: ISdkChatErrorFields): IForwardedChatError | undefined {
196 const type = sdkErrorTypeToFetchType(data.errorType, data.statusCode);
197 if (!type) {
215 };
216 }
218 > /**
219 > * Attempts to decode a {@link IForwardedChatError} from arbitrary error text
220 > * that may contain a {@link PROXY_ERROR_PREFIX} marker. Returns `undefined`
221 > * when no marker is present or the payload cannot be parsed.
222 > *
223 > * Mirrors the extension's `tryParseProxyError`.
224 > */
225 > export function tryParseForwardedChatError(errorText: string | undefined): IForwardedChatError | undefined {
226 if (!errorText) {
227 return undefined;
250 }
251 }
253 > /**
254 > * Removes the `VSCODE_PROXY_ERROR:<base64>` marker (and anything after it) from
255 > * an error message so the human-readable text isn't polluted by the forwarding
256 > * payload. The structured payload is consumed separately via `_meta`. A no-op
257 > * when no marker is present.
258 > */
259 > export function stripProxyErrorMarker(text: string): string {
260 const idx = text.indexOf(PROXY_ERROR_PREFIX);
261 if (idx === -1) {
264 return text.slice(0, idx).trim() || text.slice(0, idx);
265 }
267 > /**
268 > * Wraps a {@link IForwardedChatError} into the `_meta` record carried on the
269 > * protocol `ErrorInfo`. The core consumer reads `_meta.chatError`.
270 > */
271 > export function toChatErrorMeta(forwarded: IForwardedChatError): Record<string, unknown> {
272 return { chatError: forwarded };
273 }
275 > /**
276 > * Convenience: decode a {@link IForwardedChatError} from arbitrary error text
277 > * and wrap it into the protocol `ErrorInfo._meta` record. Returns `undefined`
278 > * when the text carries no {@link PROXY_ERROR_PREFIX} marker, so callers can
279 > * spread it onto an `ErrorInfo` without changing behavior for plain errors.
280 > */
281 > export function tryBuildChatErrorMeta(errorText: string | undefined): Record<string, unknown> | undefined {
282 const forwarded = tryParseForwardedChatError(errorText);
283 return forwarded ? toChatErrorMeta(forwarded) : undefined;
284 }
286 > /**
287 > * Convenience: build the protocol `ErrorInfo._meta` record from a structured
288 > * agent-SDK error. Returns `undefined` when the error cannot be classified as
289 > * a model/CAPI error, so callers can fall back to the raw message.
290 > */
291 > export function tryBuildChatErrorMetaFromFields(data: ISdkChatErrorFields): Record<string, unknown> | undefined {
292 const forwarded = buildForwardedChatErrorFromFields(data);
293 return forwarded ? toChatErrorMeta(forwarded) : undefined;
294 }
296 > /**
297 > * Decodes a forwarded {@link PROXY_ERROR_PREFIX} marker out of an error message
298 > * and returns the cleaned human-readable message together with the protocol
299 > * `ErrorInfo._meta` record. When no marker is present the message is returned
300 > * unchanged and `_meta` is omitted, so the result can be spread directly onto
301 > * an `ErrorInfo` without changing behavior for plain errors:
302 > *
303 > * ```ts
304 > * error: { errorType: 'CodexError', ...extractForwardedErrorInfo(message) }
305 > * ```
306 > */
307 > export function extractForwardedErrorInfo(message: string): { message: string; _meta?: Record<string, unknown> } {
308 const forwarded = tryParseForwardedChatError(message);
309 if (!forwarded) {
src/vs/base/common/glob.ts 167 covered LOC · 24 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- glob.ts
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 > import { equals } from './arrays.js';
7 > import { isThenable } from './async.js';
8 > import { CharCode } from './charCode.js';
9 > import { isEqualOrParent } from './extpath.js';
10 > import { LRUCache } from './map.js';
11 > import { basename, extname, posix, sep } from './path.js';
12 > import { isLinux } from './platform.js';
13 > import { endsWithIgnoreCase, equalsIgnoreCase, escapeRegExpCharacters, ltrim } from './strings.js';
14 >
15 > export interface IRelativePattern {
16 >
17 > /**
18 > * A base file path to which this pattern will be matched against relatively.
19 > */
20 > readonly base: string;
21 >
22 > /**
23 > * A file glob pattern like `*.{ts,js}` that will be matched on file paths
24 > * relative to the base path.
25 > *
26 > * Example: Given a base of `/home/work/folder` and a file path of `/home/work/folder/index.js`,
27 > * the file glob pattern will match on `index.js`.
28 > */
29 > readonly pattern: string;
30 > }
31 >
32 > export interface IExpression {
33 > [pattern: string]: boolean | SiblingClause;
34 > }
35 >
36 > export function getEmptyExpression(): IExpression {
37 return Object.create(null);
38 }
39 > glob.ts
40 > interface SiblingClause {
41 > when: string;
42 > }
43 >
44 > export const GLOBSTAR = '**';
45 > export const GLOB_SPLIT = '/';
46 >
47 > const PATH_REGEX = '[/\\\\]'; // any slash or backslash
48 > const NO_PATH_REGEX = '[^/\\\\]'; // any non-slash and non-backslash
49 > const ALL_FORWARD_SLASHES = /\//g;
50 >
51 function starsToRegExp(starCount: number, isLastPattern?: boolean): string {
52 switch (starCount) {
63 }
64 }
65 > glob.ts
66 > export function splitGlobAware(pattern: string, splitChar: string): string[] {
67 if (!pattern) {
68 return [];
109 return segments;
110 }
111 > glob.ts
112 function parseRegExp(pattern: string): string {
113 if (!pattern) {
256 return regEx;
257 }
258 > glob.ts
259 > // regexes to check for trivial glob patterns that just check for String#endsWith
260 > const T1 = /^\*\*\/\*\.[\w\.-]+$/; // **/*.something
261 > const T2 = /^\*\*\/([\w\.-]+)\/?$/; // **/something
262 > const T3 = /^{\*\*\/\*?[\w\.-]+\/?(,\*\*\/\*?[\w\.-]+\/?)*}$/; // {**/*.something,**/*.else} or {**/package.json,**/project.json}
263 > const T3_2 = /^{\*\*\/\*?[\w\.-]+(\/(\*\*)?)?(,\*\*\/\*?[\w\.-]+(\/(\*\*)?)?)*}$/; // Like T3, with optional trailing /**
264 > const T4 = /^\*\*((\/[\w\.-]+)+)\/?$/; // **/something/else
265 > const T5 = /^([\w\.-]+(\/[\w\.-]+)*)\/?$/; // something/else
266 >
267 > export type ParsedPattern = (path: string, basename?: string) => boolean;
268 >
269 > // The `ParsedExpression` returns a `Promise`
270 > // iff `hasSibling` returns a `Promise`.
271 > export type ParsedExpression = (path: string, basename?: string, hasSibling?: (name: string) => boolean | Promise<boolean>) => string | null | Promise<string | null> /* the matching pattern */;
272 >
273 > export interface IGlobOptions {
274 >
275 > /**
276 > * Simplify patterns for use as exclusion filters during
277 > * tree traversal to skip entire subtrees. Cannot be used
278 > * outside of a tree traversal.
279 > */
280 > trimForExclusions?: boolean;
281 >
282 > /**
283 > * Whether glob pattern matching should be case insensitive.
284 > */
285 > ignoreCase?: boolean;
286 > }
287 >
288 > interface IGlobOptionsInternal extends IGlobOptions {
289 > equals: (a: string, b: string) => boolean;
290 > endsWith: (str: string, candidate: string) => boolean;
291 > isEqualOrParent: (base: string, candidate: string) => boolean;
292 > }
293 >
294 > interface ParsedStringPattern {
295 > (path: string, basename?: string): string | null | Promise<string | null> /* the matching pattern */;
296 > basenames?: string[];
297 > patterns?: string[];
298 > allBasenames?: string[];
299 > allPaths?: string[];
300 > }
301 >
302 > interface ParsedExpressionPattern {
303 > (path: string, basename?: string, name?: string, hasSibling?: (name: string) => boolean | Promise<boolean>): string | null | Promise<string | null> /* the matching pattern */;
304 > requiresSiblings?: boolean;
305 > allBasenames?: string[];
306 > allPaths?: string[];
307 > }
308 >
309 > const CACHE = new LRUCache<string, ParsedStringPattern>(10000); // bounded to 10000 elements
310 >
311 > const FALSE = function () {
312 return false;
313 };
314 > glob.ts
315 > const NULL = function (): string | null {
316 return null;
317 };
318 > glob.ts
319 > /**
320 > * Check if a provided parsed pattern or expression
321 > * is empty - hence it won't ever match anything.
322 > *
323 > * See {@link FALSE} and {@link NULL}.
324 > */
325 > export function isEmptyPattern(pattern: ParsedPattern | ParsedExpression): pattern is (typeof FALSE | typeof NULL) {
326 if (pattern === FALSE) {
327 return true;
334 return false;
335 }
336 > glob.ts
337 function parsePattern(arg1: string | IRelativePattern, options: IGlobOptions): ParsedStringPattern {
338 if (!arg1) {
390 return wrapRelativePattern(parsedPattern, arg1, internalOptions);
391 }
392 > glob.ts
393 function wrapRelativePattern(parsedPattern: ParsedStringPattern, arg2: string | IRelativePattern, options: IGlobOptionsInternal): ParsedStringPattern {
394 if (typeof arg2 === 'string') {
421 return wrappedPattern;
422 }
423 > glob.ts
424 function trimForExclusions(pattern: string, options: IGlobOptions): string {
425 return options.trimForExclusions && pattern.endsWith('/**') ? pattern.substring(0, pattern.length - 2) : pattern; // dropping **, tailing / is dropped later
426 }
427 > glob.ts
428 > // common pattern: **/*.txt just need endsWith check
429 function trivia1(base: string, pattern: string, options: IGlobOptionsInternal): ParsedStringPattern {
430 return function (path: string, basename?: string) {
432 };
433 }
434 > glob.ts
435 > // common pattern: **/some.txt just need basename check
436 function trivia2(base: string, pattern: string, options: IGlobOptionsInternal): ParsedStringPattern {
437 const slashBase = `/${base}`;
457 return parsedPattern;
458 }
459 > glob.ts
460 > // repetition of common patterns (see above) {**/*.txt,**/*.png}
461 function trivia3(pattern: string, options: IGlobOptionsInternal): ParsedStringPattern {
462 const parsedPatterns = aggregateBasenameMatches(pattern.slice(1, -1)
496 return parsedPattern;
497 }
498 > glob.ts
499 > // common patterns: **/something/else just need endsWith check, something/else just needs and equals check
500 function trivia4and5(targetPath: string, pattern: string, matchPathEnds: boolean, options: IGlobOptionsInternal): ParsedStringPattern {
501 const usingPosixSep = sep === posix.sep;
522 return parsedPattern;
523 }
524 > glob.ts
525 function toRegExp(pattern: string, options: IGlobOptions): ParsedStringPattern {
526 try {
535 }
536 }
537 > glob.ts
538 > /**
539 > * Simplified glob matching. Supports a subset of glob patterns:
540 > * * `*` to match zero or more characters in a path segment
541 > * * `?` to match on one character in a path segment
542 > * * `**` to match any number of path segments, including none
543 > * * `{}` to group conditions (e.g. *.{ts,js} matches all TypeScript and JavaScript files)
544 > * * `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
545 > * * `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
546 > */
547 > export function match(pattern: string | IRelativePattern, path: string, options?: IGlobOptions): boolean;
548 > export function match(expression: IExpression, path: string, options?: IGlobOptions): boolean;
549 > export function match(arg1: string | IExpression | IRelativePattern, path: string, options?: IGlobOptions): boolean {
550 if (!arg1 || typeof path !== 'string') {
551 return false;
554 return parse(arg1, options)(path) as boolean;
555 }
556 > glob.ts
557 > /**
558 > * Simplified glob matching. Supports a subset of glob patterns:
559 > * * `*` to match zero or more characters in a path segment
560 > * * `?` to match on one character in a path segment
561 > * * `**` to match any number of path segments, including none
562 > * * `{}` to group conditions (e.g. *.{ts,js} matches all TypeScript and JavaScript files)
563 > * * `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
564 > * * `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
565 > */
566 > export function parse(pattern: string | IRelativePattern, options?: IGlobOptions): ParsedPattern;
567 > export function parse(expression: IExpression, options?: IGlobOptions): ParsedExpression;
568 > export function parse(arg1: string | IExpression | IRelativePattern, options?: IGlobOptions): ParsedPattern | ParsedExpression;
569 > export function parse(arg1: string | IExpression | IRelativePattern, options: IGlobOptions = {}): ParsedPattern | ParsedExpression {
570 if (!arg1) {
571 return FALSE;
597 return parsedExpression(arg1, options);
598 }
599 > glob.ts
600 > export function isRelativePattern(obj: unknown): obj is IRelativePattern {
601 const rp = obj as IRelativePattern | undefined | null;
602 if (!rp) {
606 return typeof rp.base === 'string' && typeof rp.pattern === 'string';
607 }
608 > glob.ts
609 > export function getBasenameTerms(patternOrExpression: ParsedPattern | ParsedExpression): string[] {
610 return (<ParsedStringPattern>patternOrExpression).allBasenames || [];
611 }
612 > glob.ts
613 > export function getPathTerms(patternOrExpression: ParsedPattern | ParsedExpression): string[] {
614 return (<ParsedStringPattern>patternOrExpression).allPaths || [];
615 }
616 > glob.ts
617 function parsedExpression(expression: IExpression, options: IGlobOptions): ParsedExpression {
618 const parsedPatterns = aggregateBasenameMatches(Object.getOwnPropertyNames(expression)
745 return resultExpression;
746 }
747 > glob.ts
748 function parseExpressionPattern(pattern: string, value: boolean | SiblingClause, options: IGlobOptions): (ParsedStringPattern | ParsedExpressionPattern) {
749 if (value === false) {
786 return parsedPattern;
787 }
788 > glob.ts
789 function aggregateBasenameMatches(parsedPatterns: Array<ParsedStringPattern | ParsedExpressionPattern>, result?: string): Array<ParsedStringPattern | ParsedExpressionPattern> {
790 const basenamePatterns = parsedPatterns.filter(parsedPattern => !!(<ParsedStringPattern>parsedPattern).basenames);
844 return aggregatedPatterns;
845 }
846 > glob.ts
847 > // NOTE: This is not used for actual matching, only for resetting watcher when patterns change.
848 > // That is why it's ok to avoid case-insensitive comparison here.
849 > export function patternsEquals(patternsA: Array<string | IRelativePattern> | undefined, patternsB: Array<string | IRelativePattern> | undefined): boolean {
850 return equals(patternsA, patternsB, (a, b) => {
851 if (typeof a === 'string' && typeof b === 'string') {
src/vs/base/common/buffer.ts 166 covered LOC · 45 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- buffer.ts
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 > import { Lazy } from './lazy.js';
7 > import * as streams from './stream.js';
8 >
9 > interface NodeBuffer {
10 > allocUnsafe(size: number): Uint8Array;
11 > isBuffer(obj: unknown): obj is NodeBuffer;
12 > from(arrayBuffer: ArrayBufferLike, byteOffset?: number, length?: number): Uint8Array;
13 > from(data: string): Uint8Array;
14 > }
15 >
16 > declare const Buffer: NodeBuffer;
17 >
18 > const hasBuffer = (typeof Buffer !== 'undefined');
19 > const indexOfTable = new Lazy(() => new Uint8Array(256));
20 >
21 > let textEncoder: { encode: (input: string) => Uint8Array } | null;
22 > let textDecoder: { decode: (input: Uint8Array) => string } | null;
23 >
24 > export class VSBuffer {
25 >
26 > /**
27 > * When running in a nodejs context, the backing store for the returned `VSBuffer` instance
28 > * might use a nodejs Buffer allocated from node's Buffer pool, which is not transferrable.
29 > */
30 > static alloc(byteLength: number): VSBuffer {
31 if (hasBuffer) {
32 return new VSBuffer(Buffer.allocUnsafe(byteLength));
35 }
36 }
37 > buffer.ts
38 > /**
39 > * When running in a nodejs context, if `actual` is not a nodejs Buffer, the backing store for
40 > * the returned `VSBuffer` instance might use a nodejs Buffer allocated from node's Buffer pool,
41 > * which is not transferrable.
42 > */
43 > static wrap(actual: Uint8Array): VSBuffer {
44 if (hasBuffer && !(Buffer.isBuffer(actual))) {
45 // https://nodejs.org/dist/latest-v10.x/docs/api/buffer.html#buffer_class_method_buffer_from_arraybuffer_byteoffset_length
49 return new VSBuffer(actual);
50 }
51 > buffer.ts
52 > /**
53 > * When running in a nodejs context, the backing store for the returned `VSBuffer` instance
54 > * might use a nodejs Buffer allocated from node's Buffer pool, which is not transferrable.
55 > */
56 > static fromString(source: string, options?: { dontUseNodeBuffer?: boolean }): VSBuffer {
57 > const dontUseNodeBuffer = options?.dontUseNodeBuffer || false; buffer.ts
58 > if (!dontUseNodeBuffer && hasBuffer) {
59 > return new VSBuffer(Buffer.from(source));
60 > } else {
61 if (!textEncoder) {
62 textEncoder = new TextEncoder();
64 return new VSBuffer(textEncoder.encode(source));
65 }
66 > } buffer.ts
67 > buffer.ts
68 > /**
69 > * When running in a nodejs context, the backing store for the returned `VSBuffer` instance
70 > * might use a nodejs Buffer allocated from node's Buffer pool, which is not transferrable.
71 > */
72 > static fromByteArray(source: number[]): VSBuffer {
73 const result = VSBuffer.alloc(source.length);
74 for (let i = 0, len = source.length; i < len; i++) {
77 return result;
78 }
79 > buffer.ts
80 > /**
81 > * When running in a nodejs context, the backing store for the returned `VSBuffer` instance
82 > * might use a nodejs Buffer allocated from node's Buffer pool, which is not transferrable.
83 > */
84 > static concat(buffers: VSBuffer[], totalLength?: number): VSBuffer {
85 if (typeof totalLength === 'undefined') {
86 totalLength = 0;
100 return ret;
101 }
102 > buffer.ts
103 > static isNativeBuffer(buffer: unknown): boolean {
104 return hasBuffer && Buffer.isBuffer(buffer);
105 }
106 > buffer.ts
107 > readonly buffer: Uint8Array;
108 > readonly byteLength: number;
109 >
110 > private constructor(buffer: Uint8Array) {
111 > this.buffer = buffer; buffer.ts
112 > this.byteLength = this.buffer.byteLength;
113 > }
114 > buffer.ts
115 > /**
116 > * When running in a nodejs context, the backing store for the returned `VSBuffer` instance
117 > * might use a nodejs Buffer allocated from node's Buffer pool, which is not transferrable.
118 > */
119 > clone(): VSBuffer {
120 const result = VSBuffer.alloc(this.byteLength);
121 result.set(this);
122 return result;
123 }
124 > buffer.ts
125 > toString(): string {
126 if (hasBuffer) {
127 return this.buffer.toString();
133 }
134 }
135 > buffer.ts
136 > slice(start?: number, end?: number): VSBuffer {
137 // IMPORTANT: use subarray instead of slice because TypedArray#slice
138 // creates shallow copy and NodeBuffer#slice doesn't. The use of subarray
140 return new VSBuffer(this.buffer.subarray(start, end));
141 }
142 > buffer.ts
143 > set(array: VSBuffer, offset?: number): void;
144 > set(array: Uint8Array, offset?: number): void;
145 > set(array: ArrayBuffer, offset?: number): void;
146 > set(array: ArrayBufferView, offset?: number): void;
147 > set(array: VSBuffer | Uint8Array | ArrayBuffer | ArrayBufferView, offset?: number): void;
148 > set(array: VSBuffer | Uint8Array | ArrayBuffer | ArrayBufferView, offset?: number): void {
149 if (array instanceof VSBuffer) {
150 this.buffer.set(array.buffer, offset);
159 }
160 }
161 > buffer.ts
162 > readUInt32BE(offset: number): number {
163 return readUInt32BE(this.buffer, offset);
164 }
165 > buffer.ts
166 > writeUInt32BE(value: number, offset: number): void {
167 writeUInt32BE(this.buffer, value, offset);
168 }
169 > buffer.ts
170 > readUInt32LE(offset: number): number {
171 return readUInt32LE(this.buffer, offset);
172 }
173 > buffer.ts
174 > writeUInt32LE(value: number, offset: number): void {
175 writeUInt32LE(this.buffer, value, offset);
176 }
177 > buffer.ts
178 > readUInt8(offset: number): number {
179 return readUInt8(this.buffer, offset);
180 }
181 > buffer.ts
182 > writeUInt8(value: number, offset: number): void {
183 writeUInt8(this.buffer, value, offset);
184 }
185 > buffer.ts
186 > indexOf(subarray: VSBuffer | Uint8Array, offset = 0) {
187 return binaryIndexOf(this.buffer, subarray instanceof VSBuffer ? subarray.buffer : subarray, offset);
188 }
189 > buffer.ts
190 > equals(other: VSBuffer): boolean {
191 if (this === other) {
192 return true;
199 return this.buffer.every((value, index) => value === other.buffer[index]);
200 }
201 > } buffer.ts
202 >
203 > /**
204 > * Like String.indexOf, but works on Uint8Arrays.
205 > * Uses the boyer-moore-horspool algorithm to be reasonably speedy.
206 > */
207 > export function binaryIndexOf(haystack: Uint8Array, needle: Uint8Array, offset = 0): number {
208 const needleLen = needle.byteLength;
209 const haystackLen = haystack.byteLength;
248 return result;
249 }
250 > buffer.ts
251 > export function readUInt16LE(source: Uint8Array, offset: number): number {
252 return (
253 ((source[offset + 0] << 0) >>> 0) |
255 );
256 }
257 > buffer.ts
258 > export function writeUInt16LE(destination: Uint8Array, value: number, offset: number): void {
259 destination[offset + 0] = (value & 0b11111111);
260 value = value >>> 8;
261 destination[offset + 1] = (value & 0b11111111);
262 }
263 > buffer.ts
264 > export function readUInt32BE(source: Uint8Array, offset: number): number {
265 return (
266 source[offset] * 2 ** 24
270 );
271 }
272 > buffer.ts
273 > export function writeUInt32BE(destination: Uint8Array, value: number, offset: number): void {
274 destination[offset + 3] = value;
275 value = value >>> 8;
280 destination[offset] = value;
281 }
282 > buffer.ts
283 > export function readUInt32LE(source: Uint8Array, offset: number): number {
284 return (
285 ((source[offset + 0] << 0) >>> 0) |
289 );
290 }
291 > buffer.ts
292 > export function writeUInt32LE(destination: Uint8Array, value: number, offset: number): void {
293 destination[offset + 0] = (value & 0b11111111);
294 value = value >>> 8;
299 destination[offset + 3] = (value & 0b11111111);
300 }
301 > buffer.ts
302 > export function readUInt8(source: Uint8Array, offset: number): number {
303 return source[offset];
304 }
305 > buffer.ts
306 > export function writeUInt8(destination: Uint8Array, value: number, offset: number): void {
307 destination[offset] = value;
308 }
309 > buffer.ts
310 > export interface VSBufferReadable extends streams.Readable<VSBuffer> { }
311 >
312 > export interface VSBufferReadableStream extends streams.ReadableStream<VSBuffer> { }
313 >
314 > export interface VSBufferWriteableStream extends streams.WriteableStream<VSBuffer> { }
315 >
316 > export interface VSBufferReadableBufferedStream extends streams.ReadableBufferedStream<VSBuffer> { }
317 >
318 > export function readableToBuffer(readable: VSBufferReadable): VSBuffer {
319 return streams.consumeReadable<VSBuffer>(readable, chunks => VSBuffer.concat(chunks));
320 }
321 > buffer.ts
322 > export function bufferToReadable(buffer: VSBuffer): VSBufferReadable {
323 return streams.toReadable<VSBuffer>(buffer);
324 }
325 > buffer.ts
326 > export function streamToBuffer(stream: streams.ReadableStream<VSBuffer>): Promise<VSBuffer> {
327 return streams.consumeStream<VSBuffer>(stream, chunks => VSBuffer.concat(chunks));
328 }
329 > buffer.ts
330 export async function bufferedStreamToBuffer(bufferedStream: streams.ReadableBufferedStream<VSBuffer>): Promise<VSBuffer> {
331 if (bufferedStream.ended) {
342 ]);
343 }
344 > buffer.ts
345 > export function bufferToStream(buffer: VSBuffer): streams.ReadableStream<VSBuffer> {
346 return streams.toStream<VSBuffer>(buffer, chunks => VSBuffer.concat(chunks));
347 }
348 > buffer.ts
349 > export function streamToBufferReadableStream(stream: streams.ReadableStreamEvents<Uint8Array | string>): streams.ReadableStream<VSBuffer> {
350 return streams.transform<Uint8Array | string, VSBuffer>(stream, { data: data => typeof data === 'string' ? VSBuffer.fromString(data) : VSBuffer.wrap(data) }, chunks => VSBuffer.concat(chunks));
351 }
352 > buffer.ts
353 > export function newWriteableBufferStream(options?: streams.WriteableStreamOptions): streams.WriteableStream<VSBuffer> {
354 return streams.newWriteableStream<VSBuffer>(chunks => VSBuffer.concat(chunks), options);
355 }
356 > buffer.ts
357 > export function prefixedBufferReadable(prefix: VSBuffer, readable: VSBufferReadable): VSBufferReadable {
358 return streams.prefixedReadable(prefix, readable, chunks => VSBuffer.concat(chunks));
359 }
360 > buffer.ts
361 > export function prefixedBufferStream(prefix: VSBuffer, stream: VSBufferReadableStream): VSBufferReadableStream {
362 return streams.prefixedStream(prefix, stream, chunks => VSBuffer.concat(chunks));
363 }
364 > buffer.ts
365 > /** Decodes base64 to a uint8 array. URL-encoded and unpadded base64 is allowed. */
366 > export function decodeBase64(encoded: string) {
367 let building = 0;
368 let remainder = 0;
424 return VSBuffer.wrap(buffer).slice(0, unpadded);
425 }
426 > buffer.ts
427 > const base64Alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
428 > const base64UrlSafeAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
429 >
430 > /** Encodes a buffer to a base64 string. */
431 > export function encodeBase64({ buffer }: VSBuffer, padded = true, urlSafe = false) {
432 const dictionary = urlSafe ? base64UrlSafeAlphabet : base64Alphabet;
433 let output = '';
463 return output;
464 }
465 > buffer.ts
466 > const hexChars = '0123456789abcdef';
467 > export function encodeHex({ buffer }: VSBuffer): string {
468 let result = '';
469 for (let i = 0; i < buffer.length; i++) {
474 return result;
475 }
476 > buffer.ts
477 > export function decodeHex(hex: string): VSBuffer {
478 if (hex.length % 2 !== 0) {
479 throw new SyntaxError('Hex string must have an even length');
485 return VSBuffer.wrap(out);
486 }
487 > buffer.ts
488 function decodeHexChar(str: string, position: number) {
489 const s = str.charCodeAt(position);
src/vs/platform/environment/common/environment.ts 165 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- environment.ts
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 > import { URI } from '../../../base/common/uri.js';
7 > import { NativeParsedArgs } from './argv.js';
8 > import { createDecorator, refineServiceDecorator } from '../../instantiation/common/instantiation.js';
9 >
10 > export const IEnvironmentService = createDecorator<IEnvironmentService>('environmentService');
11 > export const INativeEnvironmentService = refineServiceDecorator<IEnvironmentService, INativeEnvironmentService>(IEnvironmentService);
12 >
13 > export interface IDebugParams {
14 > port: number | null;
15 > break: boolean;
16 > }
17 >
18 > export interface IExtensionHostDebugParams extends IDebugParams {
19 > debugId?: string;
20 > env?: Record<string, string>;
21 > }
22 >
23 > /**
24 > * Type of extension.
25 > *
26 > * **NOTE**: This is defined in `platform/environment` because it can appear as a CLI argument.
27 > */
28 > export type ExtensionKind = 'ui' | 'workspace' | 'web';
29 >
30 > /**
31 > * A basic environment service that can be used in various processes,
32 > * such as main, renderer and shared process. Use subclasses of this
33 > * service for specific environment.
34 > */
35 > export interface IEnvironmentService {
36 >
37 > readonly _serviceBrand: undefined;
38 >
39 > // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
40 > //
41 > // NOTE: KEEP THIS INTERFACE AS SMALL AS POSSIBLE.
42 > //
43 > // AS SUCH:
44 > // - PUT NON-WEB PROPERTIES INTO NATIVE ENVIRONMENT SERVICE
45 > // - PUT WORKBENCH ONLY PROPERTIES INTO WORKBENCH ENVIRONMENT SERVICE
46 > // - PUT ELECTRON-MAIN ONLY PROPERTIES INTO MAIN ENVIRONMENT SERVICE
47 > //
48 > // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
49 >
50 > // --- user roaming data
51 > stateResource: URI;
52 > userRoamingDataHome: URI;
53 > keyboardLayoutResource: URI;
54 > argvResource: URI;
55 >
56 > // --- data paths
57 > untitledWorkspacesHome: URI;
58 > workspaceStorageHome: URI;
59 > localHistoryHome: URI;
60 > cacheHome: URI;
61 > appSharedDataHome: URI;
62 >
63 > // --- settings sync
64 > userDataSyncHome: URI;
65 > sync: 'on' | 'off' | undefined;
66 >
67 > // --- continue edit session
68 > continueOn?: string;
69 > editSessionId?: string;
70 >
71 > // --- extension development
72 > debugExtensionHost: IExtensionHostDebugParams;
73 > isExtensionDevelopment: boolean;
74 > disableExtensions: boolean | string[];
75 > skipBuiltinExtensions?: readonly string[];
76 > enableExtensions?: readonly string[];
77 > extensionDevelopmentLocationURI?: URI[];
78 > extensionDevelopmentKind?: ExtensionKind[];
79 > extensionTestsLocationURI?: URI;
80 >
81 > // --- logging
82 > logsHome: URI;
83 > logLevel?: string;
84 > extensionLogLevel?: [string, string][];
85 > verbose: boolean;
86 > isBuilt: boolean;
87 >
88 > // --- telemetry/exp
89 > disableTelemetry: boolean;
90 > disableExperiments: boolean;
91 > serviceMachineIdResource: URI;
92 >
93 > // --- agent sessions workspace
94 > agentSessionsWorkspace: URI;
95 > // --- Policy
96 > policyFile?: URI;
97 >
98 > // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
99 > //
100 > // NOTE: KEEP THIS INTERFACE AS SMALL AS POSSIBLE.
101 > //
102 > // AS SUCH:
103 > // - PUT NON-WEB PROPERTIES INTO NATIVE ENVIRONMENT SERVICE
104 > // - PUT WORKBENCH ONLY PROPERTIES INTO WORKBENCH ENVIRONMENT SERVICE
105 > // - PUT ELECTRON-MAIN ONLY PROPERTIES INTO MAIN ENVIRONMENT SERVICE
106 > //
107 > // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
108 > }
109 >
110 > /**
111 > * A subclass of the `IEnvironmentService` to be used only in native
112 > * environments (Windows, Linux, macOS) but not e.g. web.
113 > */
114 > export interface INativeEnvironmentService extends IEnvironmentService {
115 >
116 > // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
117 > //
118 > // NOTE: KEEP THIS INTERFACE AS SMALL AS POSSIBLE.
119 > //
120 > // AS SUCH:
121 > // - PUT WORKBENCH ONLY PROPERTIES INTO WORKBENCH ENVIRONMENT SERVICE
122 > // - PUT ELECTRON-MAIN ONLY PROPERTIES INTO MAIN ENVIRONMENT SERVICE
123 > //
124 > // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
125 >
126 > // --- CLI Arguments
127 > args: NativeParsedArgs;
128 >
129 > // --- data paths
130 > /**
131 > * Root path of the JavaScript sources.
132 > *
133 > * Note: This is NOT the installation root
134 > * directory itself but contained in it at
135 > * a level that is platform dependent.
136 > */
137 > appRoot: string;
138 > userHome: URI;
139 > appSettingsHome: URI;
140 > tmpDir: URI;
141 > userDataPath: string;
142 >
143 > // --- extensions
144 > extensionsPath: string;
145 > extensionsDownloadLocation: URI;
146 > builtinExtensionsPath: string;
147 >
148 > // --- use in-memory Secret Storage
149 > useInMemorySecretStorage?: boolean;
150 >
151 > crossOriginIsolated?: boolean;
152 > exportPolicyData?: string;
153 > exportDefaultKeybindings?: string;
154 >
155 > // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
156 > //
157 > // NOTE: KEEP THIS INTERFACE AS SMALL AS POSSIBLE.
158 > //
159 > // AS SUCH:
160 > // - PUT NON-WEB PROPERTIES INTO NATIVE ENVIRONMENT SERVICE
161 > // - PUT WORKBENCH ONLY PROPERTIES INTO WORKBENCH ENVIRONMENT SERVICE
162 > // - PUT ELECTRON-MAIN ONLY PROPERTIES INTO MAIN ENVIRONMENT SERVICE
163 > //
164 > // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
165 > }
src/vs/base/common/policy.ts 161 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- policy.ts
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 > import { localize } from '../../nls.js';
7 > import { IPolicyData } from './defaultAccount.js';
8 >
9 > /**
10 > * System-wide policy file path for Linux systems.
11 > */
12 > export const LINUX_SYSTEM_POLICY_FILE_PATH = '/etc/vscode/policy.json';
13 >
14 > export type PolicyName = string;
15 > export type LocalizedValue = {
16 > key: string;
17 > value: string;
18 > };
19 >
20 > export type PolicyValue = string | number | boolean;
21 > export type ManagedSettingValue = PolicyValue;
22 > export type ManagedSettingsData = Readonly<Record<string, ManagedSettingValue>>;
23 >
24 > export interface IManagedSettingPolicyDefinition {
25 > readonly type: 'string' | 'number' | 'boolean';
26 > }
27 >
28 > export type IManagedSettingsPolicyDefinitions = Readonly<Record<string, IManagedSettingPolicyDefinition>>;
29 >
30 > export enum PolicyCategory {
31 > Extensions = 'Extensions',
32 > IntegratedTerminal = 'IntegratedTerminal',
33 > InteractiveSession = 'InteractiveSession',
34 > Telemetry = 'Telemetry',
35 > Update = 'Update',
36 > }
37 >
38 > export const PolicyCategoryData: {
39 > [key in PolicyCategory]: { name: LocalizedValue }
40 > } = {
41 > [PolicyCategory.Extensions]: {
42 > name: {
43 > key: 'extensionsConfigurationTitle', value: localize('extensionsConfigurationTitle', "Extensions"),
44 > }
45 > },
46 > [PolicyCategory.IntegratedTerminal]: {
47 > name: {
48 > key: 'terminalIntegratedConfigurationTitle', value: localize('terminalIntegratedConfigurationTitle', "Integrated Terminal"),
49 > }
50 > },
51 > [PolicyCategory.InteractiveSession]: {
52 > name: {
53 > key: 'interactiveSessionConfigurationTitle', value: localize('interactiveSessionConfigurationTitle', "Chat"),
54 > }
55 > },
56 > [PolicyCategory.Telemetry]: {
57 > name: {
58 > key: 'telemetryConfigurationTitle', value: localize('telemetryConfigurationTitle', "Telemetry"),
59 > }
60 > },
61 > [PolicyCategory.Update]: {
62 > name: {
63 > key: 'updateConfigurationTitle', value: localize('updateConfigurationTitle', "Update"),
64 > }
65 > }
66 > };
67 >
68 > export interface IPolicy {
69 >
70 > /**
71 > * The policy name.
72 > */
73 > readonly name: PolicyName;
74 >
75 > /**
76 > * The policy category.
77 > */
78 > readonly category: PolicyCategory;
79 >
80 > /**
81 > * The Code version in which this policy was introduced.
82 > */
83 > readonly minimumVersion: `${number}.${number}`;
84 >
85 > /**
86 > * Localization info for the policy.
87 > *
88 > * IMPORTANT: the key values for these must be unique to avoid collisions, as during the export time the module information is not available.
89 > */
90 > readonly localization: {
91 > /** The localization key or key value pair. If only a key is provided, the default value will fallback to the parent configuration's description property. */
92 > description: LocalizedValue;
93 > /** List of localization key or key value pair. If only a key is provided, the default value will fallback to the parent configuration's enumDescriptions property. */
94 > enumDescriptions?: LocalizedValue[];
95 > };
96 >
97 > /**
98 > * The value that an ACCOUNT-based feature will use when its corresponding policy is active.
99 > *
100 > * Only applicable when policy is tagged with ACCOUNT. When an account-based feature's policy is enabled,
101 > * this value determines what value the feature receives.
102 > *
103 > * For example:
104 > * - If evaluated value is `true`, the feature's setting is locked to `true` WHEN the policy is in effect.
105 > * - If evaluated value is `foo`, the feature's setting is locked to 'foo' WHEN the policy is in effect.
106 > *
107 > * If `undefined`, the feature's setting is not locked and can be overridden by other means.
108 > */
109 > readonly value?: (policyData: IPolicyData) => string | number | boolean | undefined;
110 >
111 > /**
112 > * Declares Copilot managed-settings keys this policy's value callback reads.
113 > * Keys are dot-separated managed-settings paths, for example
114 > * `permissions.disableBypassPermissionsMode`.
115 > */
116 > readonly managedSettings?: IManagedSettingsPolicyDefinitions;
117 >
118 > /**
119 > * The most-restrictive value that should be applied when the user is subject to the
120 > * "Require Approved Account" gate but the gate is not yet satisfied (i.e. no approved
121 > * GitHub account is signed in or the account-side policy data has not yet resolved).
122 > *
123 > * If omitted, the gate falls back to a type-driven safe default
124 > * (`false` for boolean, `0` for number, `''` for string).
125 > *
126 > * Only consulted while the gate is active and unsatisfied; ignored otherwise.
127 > */
128 > readonly restrictedValue?: string | number | boolean;
129 > }
130 >
131 > /**
132 > * A subordinate attachment to an existing {@link IPolicy} (the "owner"). A setting may declare a
133 > * `policyReference` instead of a full `policy` to be governed by a policy owned by another setting,
134 > * letting a single enterprise policy lock more than one setting (e.g. gating an agent in both the
135 > * editor window and the Agents window).
136 > *
137 > * A reference is a pure pointer: it carries no policy semantics of its own. The owner is the single
138 > * source of truth for the policy's catalog metadata *and* its runtime behaviour (type, value
139 > * callback, etc.); a reference only contributes the policy name so the setting is gated and the OS
140 > * policy watcher observes the name in processes where the owner is not loaded.
141 > */
142 > export interface IPolicyReference {
143 >
144 > /** The name of the owning {@link IPolicy} this setting attaches to. */
145 > readonly name: PolicyName;
146 > }
147 >
148 > /**
149 > * A `product.json` `extensionConfigurationPolicy` entry that attaches its setting to a policy
150 > * *owned* by an in-code setting, instead of declaring a full owner {@link IPolicy}. This mirrors the
151 > * in-code `policyReference` configuration field, so the same indirection can be expressed from
152 > * `product.json` — where the owner's runtime behaviour (notably its `value` callback) cannot live.
153 > *
154 > * An `extensionConfigurationPolicy` entry is therefore either a full {@link IPolicy} (the setting
155 > * "parents"/owns the policy, the current syntax) or this reference wrapper.
156 > */
157 > export interface IExtensionConfigurationPolicyReference {
158 >
159 > /** Pointer to the owning {@link IPolicy} declared by an in-code setting. */
160 > readonly policyReference: IPolicyReference;
161 > }
src/vs/platform/agentHost/test/common/sessionTestHelpers.ts 161 covered LOC · 34 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- sessionTestHelpers.ts
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 > import type { IReference } from '../../../../base/common/lifecycle.js';
7 > import { Schemas } from '../../../../base/common/network.js';
8 > import { URI } from '../../../../base/common/uri.js';
9 > import { Event } from '../../../../base/common/event.js';
10 > import type { IDiffComputeService, IDiffCountResult } from '../../common/diffComputeService.js';
11 > import type { IFileEditContent, IFileEditRecord, ILocalTurnRecord, IReviewedFileRecord, ISessionDatabase, ISessionDataService } from '../../common/sessionDataService.js';
12 > import type { Message } from '../../common/state/sessionState.js';
13 >
14 > export class TestSessionDatabase implements ISessionDatabase {
15 private readonly _edits: (IFileEditRecord & IFileEditContent)[] = [];
16 private readonly _metadata = new Map<string, string>();
24 deleteAllTurnsCalls = 0;
25 setTurnEventIdCalls: Array<{ turnId: string; eventId: string }> = [];
27 > addEdit(edit: IFileEditRecord & IFileEditContent): void {
28 this._edits.push(edit);
29 }
31 > async createTurn(): Promise<void> { }
32 >
33 > async deleteTurn(turnId: string): Promise<void> {
34 for (let i = this._edits.length - 1; i >= 0; i--) {
35 if (this._edits[i].turnId === turnId) {
38 }
39 }
41 > async storeFileEdit(edit: IFileEditRecord & IFileEditContent): Promise<void> {
42 const existingIndex = this._edits.findIndex(e => e.toolCallId === edit.toolCallId && e.filePath === edit.filePath);
43 if (existingIndex >= 0) {
47 }
48 }
50 > async getFileEdits(toolCallIds: string[]): Promise<IFileEditRecord[]> {
51 const toolCallIdsSet = new Set(toolCallIds);
52 return this._toEditRecords(this._edits.filter(e => toolCallIdsSet.has(e.toolCallId)));
53 }
55 > async getAllFileEdits(): Promise<IFileEditRecord[]> {
56 this.getAllFileEditsCalls++;
57 return this._toEditRecords(this._edits);
58 }
60 > async getFileEditsByTurn(turnId: string): Promise<IFileEditRecord[]> {
61 this.getFileEditsByTurnCalls++;
62 return this._toEditRecords(this._edits.filter(e => e.turnId === turnId));
63 }
65 > async readFileEditContent(toolCallId: string, filePath: string): Promise<IFileEditContent | undefined> {
66 return this._edits.find(e => e.toolCallId === toolCallId && e.filePath === filePath);
67 }
69 > async getMetadata(key: string): Promise<string | undefined> {
70 return this._metadata.get(key);
71 }
73 > async getMetadataObject<T extends Record<string, unknown>>(obj: T): Promise<{ [K in keyof T]: string | undefined }> {
74 return Object.fromEntries(Object.keys(obj).map(key => [key, this._metadata.get(key)])) as { [K in keyof T]: string | undefined };
75 }
77 > async setMetadata(key: string, value: string): Promise<void> {
78 this._metadata.set(key, value);
79 }
81 > async setChatDraft(chat: URI, draft: Message | undefined): Promise<void> {
82 const key = chat.toString();
83 if (draft) {
87 }
88 }
90 > async getChatDraft(chat: URI): Promise<Message | undefined> {
91 return this._drafts.get(chat.toString());
92 }
94 > async close(): Promise<void> { }
95 >
96 > async vacuumInto(_targetPath: string): Promise<void> { }
97 >
98 > dispose(): void { }
99 >
100 > async setTurnEventId(turnId: string, eventId: string): Promise<void> {
101 this.setTurnEventIdCalls.push({ turnId, eventId });
102 }
104 > async getTurnEventId(_turnId: string): Promise<string | undefined> { return undefined; }
105 >
106 > async getNextTurnEventId(_turnId: string): Promise<string | undefined> { return undefined; }
107 >
108 > async getFirstTurnEventId(): Promise<string | undefined> { return undefined; }
109 >
110 > async truncateFromTurn(_turnId: string): Promise<void> { }
111 >
112 > async deleteTurnsAfter(turnId: string): Promise<void> {
113 this.deleteTurnsAfterCalls.push(turnId);
114 }
116 > async deleteAllTurns(): Promise<void> {
117 this.deleteAllTurnsCalls++;
118 this._edits.length = 0;
119 }
121 > async insertLocalTurn(record: ILocalTurnRecord): Promise<void> {
122 this._localTurns.set(record.turnId, record);
123 }
125 > async getLocalTurns(): Promise<ILocalTurnRecord[]> {
126 return [...this._localTurns.values()].sort((a, b) => a.seq - b.seq);
127 }
129 > async deleteLocalTurns(turnIds: readonly string[]): Promise<void> {
130 for (const id of turnIds) {
131 this._localTurns.delete(id);
132 }
133 }
134 > async remapTurnIds(_mapping: ReadonlyMap<string, string>): Promise<void> { } sessionTestHelpers.ts
135 >
136 > async markFileReviewed(uri: URI, nonce: string): Promise<void> {
137 if (!this._reviewedFiles.some(r => r.uri.toString() === uri.toString() && r.nonce === nonce)) {
138 this._reviewedFiles.push({ uri, nonce });
139 }
140 }
142 > async unmarkFileReviewed(uri: URI, nonce: string): Promise<void> {
143 const index = this._reviewedFiles.findIndex(r => r.uri.toString() === uri.toString() && r.nonce === nonce);
144 if (index >= 0) {
146 }
147 }
149 > async getReviewedFiles(): Promise<IReviewedFileRecord[]> {
150 return [...this._reviewedFiles];
151 }
153 > async getReviewedFilesForUri(uri: URI): Promise<IReviewedFileRecord[]> {
154 return this._reviewedFiles.filter(r => r.uri.toString() === uri.toString());
155 }
157 > async isFileReviewed(uri: URI, nonce: string): Promise<boolean> {
158 return this._reviewedFiles.some(r => r.uri.toString() === uri.toString() && r.nonce === nonce);
159 }
161 > async setTurnCheckpointRef(_turnId: string, _ref: string): Promise<void> { }
162 >
163 > async getTurnCheckpointRef(_turnId: string): Promise<string | undefined> { return undefined; }
164 >
165 > async getPreviousCheckpointRef(_turnId: string): Promise<string | undefined> { return undefined; }
166 >
167 > async getAllCheckpointRefs(): Promise<string[]> { return []; }
168 >
169 > async whenIdle(): Promise<void> { }
170 >
171 > private _toEditRecords(edits: (IFileEditRecord & IFileEditContent)[]): IFileEditRecord[] {
172 return edits.map(({ beforeContent: _, afterContent: _2, ...metadata }) => metadata);
173 }
175 >
176 > export class TestDiffComputeService implements IDiffComputeService {
177 > declare readonly _serviceBrand: undefined;
178 >
179 > callCount = 0;
180 >
181 > constructor(private readonly _result?: IDiffCountResult) { }
182 >
183 > async computeDiffCounts(original: string, modified: string): Promise<IDiffCountResult> {
184 this.callCount++;
185 if (this._result) {
194 };
195 }
197 >
198 > export function createZeroDiffComputeService(): IDiffComputeService {
199 return new TestDiffComputeService({ added: 0, removed: 0 });
200 }
202 > export function createSessionDataService(database: ISessionDatabase = new TestSessionDatabase()): ISessionDataService {
203 return {
204 _serviceBrand: undefined,
213 };
214 }
216 > export function createNullSessionDataService(): ISessionDataService {
217 return {
218 _serviceBrand: undefined,
227 };
228 }
230 > export function encodeString(text: string): Uint8Array {
231 return new TextEncoder().encode(text);
232 }
234 > /**
235 > * Returns a no-op {@link IAgentHostGitService} suitable for tests that
236 > * exercise the {@link AgentService} but don't care about git state.
237 > * Tests that DO care about git state should pass their own implementation.
238 > */
239 > export function createNoopGitService(): import('../../common/agentHostGitService.js').IAgentHostGitService {
240 > return { sessionTestHelpers.ts
241 > _serviceBrand: undefined,
242 > getCurrentBranch: async () => undefined,
243 > getDefaultBranch: async () => undefined,
244 > getBranch: async () => undefined,
245 > getRefs: async () => [],
246 > getBranches: async () => [],
247 > getRepositoryRoot: async () => undefined,
248 > getWorktreeRoots: async () => [],
249 > addWorktree: async () => { },
250 > copyWorktreeIncludeFiles: async () => { },
251 > addExistingWorktree: async () => { },
252 > removeWorktree: async () => { },
253 > branchExists: async () => false,
254 > hasUncommittedChanges: async () => false,
255 > commitAll: async () => { },
256 > restore: async () => { },
257 > hasUpstream: async () => false,
258 > pull: async () => { },
259 > push: async () => { },
260 > getSessionGitState: async () => undefined,
261 > computeSessionFileDiffs: async () => undefined,
262 > resolveBranchBaselineCommit: async () => undefined,
263 > showBlob: async () => undefined,
264 > captureWorkingTreeAsTree: async () => undefined,
265 > commitTree: async () => undefined,
266 > updateRef: async () => { },
267 > deleteRefs: async () => { },
268 > revParse: async () => undefined,
269 > overlayPathIntoTree: async () => undefined,
270 > diffTreePaths: async () => undefined,
271 > computeFileDiffsBetweenRefs: async () => undefined,
272 > getFetchRemoteUrls: async () => undefined,
273 > getUntrackedPaths: async () => [],
274 > getBranchDiffSafetyInfo: async () => undefined,
275 > getDiffPatchBetweenRefs: async () => undefined,
276 > };
277 > }
279 > /**
280 > * Returns a no-op {@link IAgentHostChangesetService} for tests that need to
281 > * inject the changeset service but don't exercise changeset computation.
282 > * Individual methods can be reassigned by callers that want to spy on them.
283 > */
284 > export function createNoopChangesetService(): import('../../common/agentHostChangesetService.js').IAgentHostChangesetService {
285 return {
286 _serviceBrand: undefined,
308 };
309 }
311 function createReference<T>(object: T): IReference<T> {
312 return {
src/vs/platform/agentHost/node/shared/editSurvivalReporter.ts 152 covered LOC · 7 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- editSurvivalReporter.ts
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 > import { TimeoutTimer } from '../../../../base/common/async.js';
7 > import { Disposable, type IDisposable } from '../../../../base/common/lifecycle.js';
8 > import { extname } from '../../../../base/common/path.js';
9 > import { URI } from '../../../../base/common/uri.js';
10 > import { FileOperationResult, IFileService, toFileOperationResult } from '../../../files/common/files.js';
11 > import { createDecorator } from '../../../instantiation/common/instantiation.js';
12 > import { ILogService } from '../../../log/common/log.js';
13 > import { ITelemetryService } from '../../../telemetry/common/telemetry.js';
14 > import { AgentSession } from '../../common/agentService.js';
15 > import { isAhpChatChannel, parseRequiredSessionUriFromChatUri } from '../../common/state/sessionState.js';
16 > import { computeChunkedEditSurvival, computeWholeFileEditSurvival } from './editSurvivalTracker.js';
17 >
18 > /**
19 > * Parameters describing a single completed tool-driven file edit that the
20 > * agent host wants to follow over time.
21 > *
22 > * Only first-party edit tools that go through `FileEditTracker` produce
23 > * these — file modifications from shell tools like `Bash` are not
24 > * observable here.
25 > *
26 > * Notebook tools (`NotebookEdit`) are skipped by the launcher for now
27 > * to avoid the complexity of scoring against notebook JSON. We may
28 > * revisit when we have a notebook-aware tracker.
29 > */
30 > export interface IEditSurvivalReporterLaunchParams {
31 > /** Full session URI string (e.g. `claude:/abc123`). */
32 > readonly sessionUri: string;
33 > readonly turnId: string;
34 > readonly toolCallId: string;
35 > /** Absolute file path on the agent host's local file system. */
36 > readonly filePath: string;
37 > /** File content snapshotted before the tool ran (empty for creates). */
38 > readonly beforeText: string;
39 > /** File content after the tool ran (the AI's output). */
40 > readonly afterText: string;
41 > /** Whether the tool created a new file (no prior content existed). */
42 > readonly isCreate: boolean;
43 > /** Name of the edit tool, e.g. `Edit`, `apply_patch`. Empty if unknown. */
44 > readonly toolName?: string;
45 > /**
46 > * Model that produced this edit, e.g. `claude-sonnet-4.5`. Optional
47 > * defensively, but always expected to be set
48 > */
49 > readonly modelId?: string;
50 > /**
51 > * Explicit AI-written text chunks extracted from the tool input
52 > * (see `editChunkExtractor.ts`). When provided, survival is scored
53 > * against just these chunks; when omitted or empty, the reporter
54 > * falls back to whole-file scoring and tags the event with
55 > * `scoringMode='whole-file'`.
56 > */
57 > readonly aiChunks?: readonly string[];
58 > }
59 >
60 > export const IEditSurvivalReporterFactory = createDecorator<IEditSurvivalReporterFactory>('editSurvivalReporterFactory');
61 >
62 > /**
63 > * Launches background reporters that sample the on-disk file after a tool
64 > * edit and emit edit-survival telemetry over the next 15 minutes.
65 > */
66 > export interface IEditSurvivalReporterFactory {
67 > readonly _serviceBrand: undefined;
68 > /**
69 > * Begin tracking a single file edit. The returned disposable can be
70 > * used to cancel sampling early; otherwise the reporter cleans itself
71 > * up after the final 15-minute sample.
72 > */
73 > launch(params: IEditSurvivalReporterLaunchParams): IDisposable;
74 > }
75 >
76 > /** No-op factory, useful for tests and environments without telemetry. */
77 > export class NullEditSurvivalReporterFactory implements IEditSurvivalReporterFactory {
78 > readonly _serviceBrand: undefined;
79 > launch(_params: IEditSurvivalReporterLaunchParams): IDisposable {
80 return { dispose() { } };
81 }
83 >
84 > interface IEditSurvivalTelemetryEvent {
85 > provider: string;
86 > modelId: string;
87 > toolName: string;
88 > agentSessionId: string;
89 > turnId: string;
90 > toolCallId: string;
91 > fileExtension: string;
92 > survivalRateFourGram: number;
93 > survivalRateNoRevert: number;
94 > scoringMode: string;
95 > aiChunkCount: number;
96 > aiCharCount: number;
97 > timeDelayMs: number;
98 > didFileGetDeleted: number;
99 > isCreate: number;
100 > beforeTextLength: number;
101 > afterTextLength: number;
102 > currentTextLength: number;
103 > }
104 >
105 > type IEditSurvivalTelemetryClassification = {
106 > provider: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider handling the agent host session.' };
107 > modelId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The model that produced the edit, e.g. "claude-sonnet-4.5" or "gpt-5-mini". Empty if the host could not determine the per-edit model.' };
108 > toolName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Name of the edit tool that produced the edit, e.g. "Edit", "apply_patch". Empty if unknown.' };
109 > agentSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The agent host session identifier.' };
110 > turnId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The agent host turn identifier this edit belongs to.' };
111 > toolCallId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The tool call identifier that produced the edit.' };
112 > fileExtension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The file extension (including the leading dot) of the edited file, or empty if the file has no extension.' };
113 > survivalRateFourGram: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'A number between 0 and 1 representing the share of 4-grams the AI wrote that are still present in the file.' };
114 > survivalRateNoRevert: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'A number between 0 and 1; 1 means the user kept the AI edit and 0 means the user fully reverted it.' };
115 > scoringMode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How survivalRateFourGram was computed: "chunked" (asymmetric, denominator bounded by the AI-written text) or "whole-file" (symmetric, denominator includes the whole file).' };
116 > aiChunkCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of distinct AI-written text chunks contributing to chunked scoring (0 when scoringMode is "whole-file").' };
117 > aiCharCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Sum of character lengths of the AI-written chunks (suitable for char-weighted dashboard rollups). Always 0 when scoringMode is "whole-file" because we cannot accurately determine the AI char count from a whole-file snapshot.' };
118 > timeDelayMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Milliseconds since the edit completed when this sample was taken.' };
119 > didFileGetDeleted: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: '1 if the file could not be read when the sample was taken (deleted or moved), otherwise 0.' };
120 > isCreate: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: '1 if the tool call created a new file, otherwise 0.' };
121 > beforeTextLength: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Length in characters of the file content before the AI edit.' };
122 > afterTextLength: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Length in characters of the file content the AI wrote.' };
123 > currentTextLength: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Length in characters of the file content at the time of the sample (0 if the file is missing).' };
124 > owner: 'roblourens';
125 > comment: 'Tracks how long AI-produced file edits survive in the user\'s file over the 15 minutes following a tool call in an agent host session. No file contents are reported.';
126 > };
127 >
128 > /**
129 > * Schedule of samples (in milliseconds since the edit completed) at
130 > * which we read the file again and emit a telemetry event. Matches the
131 > * chat extension's schedule so the resulting data is comparable.
132 > */
133 > const SAMPLE_SCHEDULE_MS = [0, 5_000, 30_000, 120_000, 300_000, 600_000, 900_000];
134 >
135 > class SessionEditSurvivalReporter extends Disposable {
136 > private readonly _startTime = Date.now();
137 > private _samplesTaken = 0;
138 >
139 > constructor(
140 private readonly _params: IEditSurvivalReporterLaunchParams,
141 private readonly _fileService: IFileService,
146 this._scheduleNext();
147 }
149 > private _scheduleNext(): void {
150 if (this._samplesTaken >= SAMPLE_SCHEDULE_MS.length) {
151 this.dispose();
158 timer.setIfNotSet(() => this._takeSample(), delay);
159 }
161 > private async _takeSample(): Promise<void> {
162 const sampleIndex = this._samplesTaken++;
163 const timeDelayMs = SAMPLE_SCHEDULE_MS[sampleIndex];
234 this._scheduleNext();
235 }
237 >
238 > const MAX_TRACKED_FILE_SIZE_CHARS = 5 * 1024 * 1024;
239 >
240 > export class EditSurvivalReporterFactory implements IEditSurvivalReporterFactory {
241 > readonly _serviceBrand: undefined;
242 >
243 > constructor(
244 @IFileService private readonly _fileService: IFileService,
245 @ILogService private readonly _logService: ILogService,
246 @ITelemetryService private readonly _telemetryService: ITelemetryService,
247 ) { }
249 > launch(params: IEditSurvivalReporterLaunchParams): IDisposable {
250 // Skip notebooks for now: scoring against the on-disk JSON
251 // (including output cells) doesn't reflect user intent. We may
260 return new SessionEditSurvivalReporter(params, this._fileService, this._logService, this._telemetryService);
261 }
src/vs/platform/agentHost/common/state/protocol/channels-chat/commands.ts 147 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- commands.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > import type { URI } from '../common/state.js';
10 > import type { BaseParams } from '../common/commands.js';
11 > import type { Message, SideChatSelection } from './state.js';
12 >
13 > // ─── createChat ──────────────────────────────────────────────────────────────
14 >
15 > /**
16 > * How a new chat uses its source chat and turn.
17 > */
18 > export const enum ChatSourceKind {
19 > /** Copy source history through the referenced turn into the new chat. */
20 > Fork = 'fork',
21 > /** Supply source context without copying it into the new chat's visible history. */
22 > SideChat = 'sideChat',
23 > }
24 >
25 > /**
26 > * Copies source history through a completed turn into the new chat.
27 > */
28 > export interface ForkChatSource {
29 > /** Discriminant */
30 > kind: ChatSourceKind.Fork;
31 > /** URI of the existing source chat. */
32 > chat: URI;
33 > /**
34 > * Completed turn identifier in the source chat.
35 > *
36 > * Content through this turn is copied into the new chat's visible `turns`.
37 > */
38 > turnId: string;
39 > }
40 >
41 > /**
42 > * Supplies source context to a new side chat without copying it into the side
43 > * chat's visible history.
44 > */
45 > export interface SideChatSource {
46 > /** Discriminant */
47 > kind: ChatSourceKind.SideChat;
48 > /** URI of the existing source chat. */
49 > chat: URI;
50 > /**
51 > * Stable source-turn identifier in the source chat.
52 > *
53 > * Hosts resolve this id against the source chat's current `activeTurn` or its
54 > * retained `turns` when accepting `createChat`. If it names the current
55 > * active turn, the host snapshots the source chat's retained history plus
56 > * that turn's current user message and any partial assistant response already
57 > * available. Once that turn later becomes historical, it is still referenced
58 > * by this same identifier.
59 > */
60 > turnId: string;
61 > /**
62 > * Optional immutable selected-text snapshot to carry into the created side
63 > * chat's origin.
64 > *
65 > * When present, the host MUST snapshot and preserve this exact selection when
66 > * it accepts `createChat`; later source-turn deltas do not alter it.
67 > */
68 > selection?: SideChatSelection;
69 > }
70 >
71 > /**
72 > * Identifies a source chat for a new chat.
73 > */
74 > export type ChatSource =
75 > | ForkChatSource
76 > | SideChatSource;
77 >
78 > /**
79 > * Creates a new chat within a session.
80 > *
81 > * @category Commands
82 > * @method createChat
83 > * @direction Client → Server
84 > * @messageType Request
85 > * @version 1
86 > */
87 > export interface CreateChatParams extends BaseParams {
88 > /** Session URI containing the new chat. */
89 > channel: URI;
90 > /** Chat URI (client-chosen, e.g. `ahp-chat:/<uuid>`). */
91 > chat: URI;
92 > /** Optional initial message for the new chat. */
93 > initialMessage?: Message;
94 > /**
95 > * Optional source chat and source turn.
96 > *
97 > * The source chat MUST belong to this session. Clients MUST only request
98 > * `kind: "fork"` when the selected agent advertises
99 > * `capabilities.multipleChats.fork`, and `kind: "sideChat"` when the
100 > * selected agent advertises `capabilities.multipleChats.sideChat`. Both
101 > * source forms carry a stable top-level `turnId`. Forks target completed
102 > * turns. Side chats also carry a stable `turnId`, which the host resolves
103 > * against the source chat's current active turn or retained history. If it
104 > * resolves to the active turn, the host snapshots the currently available
105 > * partial response when accepting `createChat`. When
106 > * `source.kind === "sideChat"` and `source.selection` is present, the host
107 > * also snapshots and preserves that exact selected text in the created chat's
108 > * origin; any `responsePartId` there is provenance only, not a live range.
109 > */
110 > source?: ChatSource;
111 > /**
112 > * Initial working-directory subset for this chat. Every entry MUST be
113 > * present in the owning session's `workingDirectories`; the server MUST
114 > * reject any entry that is not. When absent, the chat inherits the full
115 > * session set. Forked chats (those whose `source.kind` is `"fork"`) inherit
116 > * the source chat's `workingDirectories`; this field is ignored for forks.
117 > *
118 > * A client MUST NOT supply this field unless the agent advertises
119 > * {@link AgentCapabilities.multipleWorkingDirectories}.
120 > */
121 > workingDirectories?: URI[];
122 > /**
123 > * The chat's primary working directory — the distinguished root this chat is
124 > * centered on. When set, it MUST be one of the chat's effective working
125 > * directories ({@link workingDirectories}, or the session's set when that is
126 > * omitted). A client SHOULD supply this when the agent advertises
127 > * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY
128 > * reject creation that omits it, or fall back to the first of the chat's
129 > * directories. Fixed at creation and reported (read-only) on
130 > * {@link ChatState.primaryWorkingDirectory}. Ignored for forks (a chat whose
131 > * `source.kind` is `"fork"` inherits the source chat's primary).
132 > */
133 > primaryWorkingDirectory?: URI;
134 > }
135 >
136 > // ─── disposeChat ─────────────────────────────────────────────────────────────
137 >
138 > /**
139 > * Disposes a chat and cleans up server-side resources.
140 > *
141 > * @category Commands
142 > * @method disposeChat
143 > * @direction Client → Server
144 > * @messageType Request
145 > * @version 1
146 > */
147 > export interface DisposeChatParams extends BaseParams { }
src/vs/base/common/labels.ts 146 covered LOC · 12 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- labels.ts
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 > import { hasDriveLetter, toSlashes } from './extpath.js';
7 > import { posix, sep, win32 } from './path.js';
8 > import { isMacintosh, isWindows, OperatingSystem, OS } from './platform.js';
9 > import { extUri, extUriIgnorePathCase } from './resources.js';
10 > import { rtrim, startsWithIgnoreCase } from './strings.js';
11 > import { URI } from './uri.js';
12 >
13 > export interface IPathLabelFormatting {
14 >
15 > /**
16 > * The OS the path label is from to produce a label
17 > * that matches OS expectations.
18 > */
19 > readonly os: OperatingSystem;
20 >
21 > /**
22 > * Whether to add a `~` when the path is in the
23 > * user home directory.
24 > *
25 > * Note: this only applies to Linux, macOS but not
26 > * Windows.
27 > */
28 > readonly tildify?: IUserHomeProvider;
29 >
30 > /**
31 > * Whether to convert to a relative path if the path
32 > * is within any of the opened workspace folders.
33 > */
34 > readonly relative?: IRelativePathProvider;
35 > }
36 >
37 > export interface IRelativePathProvider {
38 >
39 > /**
40 > * Whether to not add a prefix when in multi-root workspace.
41 > */
42 > readonly noPrefix?: boolean;
43 >
44 > getWorkspace(): { folders: { uri: URI; name?: string }[] };
45 > getWorkspaceFolder(resource: URI): { uri: URI; name?: string } | null;
46 > }
47 >
48 > export interface IUserHomeProvider {
49 > userHome: URI;
50 > }
51 >
52 > export function getPathLabel(resource: URI, formatting: IPathLabelFormatting): string {
53 const { os, tildify: tildifier, relative: relatifier } = formatting;
54
93 return pathLib.normalize(normalizeDriveLetter(absolutePath, os === OperatingSystem.Windows));
94 }
95 > labels.ts
96 function getRelativePathLabel(resource: URI, relativePathProvider: IRelativePathProvider, os: OperatingSystem): string | undefined {
97 const pathLib = os === OperatingSystem.Windows ? win32 : posix;
137 return relativePathLabel;
138 }
139 > labels.ts
140 > export function normalizeDriveLetter(path: string, isWindowsOS: boolean = isWindows): string {
141 if (hasDriveLetter(path, isWindowsOS)) {
142 return path.charAt(0).toUpperCase() + path.slice(1);
145 return path;
146 }
147 > labels.ts
148 > let normalizedUserHomeCached: { original: string; normalized: string } = Object.create(null);
149 > export function tildify(path: string, userHome: string, os = OS): string {
150 if (os === OperatingSystem.Windows || !path || !userHome) {
151 return path; // unsupported on Windows
174 return path;
175 }
176 > labels.ts
177 > export function untildify(path: string, userHome: string): string {
178 return path.replace(/^~($|\/|\\)/, `${userHome}$1`);
179 }
180 > labels.ts
181 > /**
182 > * Shortens the paths but keeps them easy to distinguish.
183 > * Replaces not important parts with ellipsis.
184 > * Every shorten path matches only one original path and vice versa.
185 > *
186 > * Algorithm for shortening paths is as follows:
187 > * 1. For every path in list, find unique substring of that path.
188 > * 2. Unique substring along with ellipsis is shortened path of that path.
189 > * 3. To find unique substring of path, consider every segment of length from 1 to path.length of path from end of string
190 > * and if present segment is not substring to any other paths then present segment is unique path,
191 > * else check if it is not present as suffix of any other path and present segment is suffix of path itself,
192 > * if it is true take present segment as unique path.
193 > * 4. Apply ellipsis to unique segment according to whether segment is present at start/in-between/end of path.
194 > *
195 > * Example 1
196 > * 1. consider 2 paths i.e. ['a\\b\\c\\d', 'a\\f\\b\\c\\d']
197 > * 2. find unique path of first path,
198 > * a. 'd' is present in path2 and is suffix of path2, hence not unique of present path.
199 > * b. 'c' is present in path2 and 'c' is not suffix of present path, similarly for 'b' and 'a' also.
200 > * c. 'd\\c' is suffix of path2.
201 > * d. 'b\\c' is not suffix of present path.
202 > * e. 'a\\b' is not present in path2, hence unique path is 'a\\b...'.
203 > * 3. for path2, 'f' is not present in path1 hence unique is '...\\f\\...'.
204 > *
205 > * Example 2
206 > * 1. consider 2 paths i.e. ['a\\b', 'a\\b\\c'].
207 > * a. Even if 'b' is present in path2, as 'b' is suffix of path1 and is not suffix of path2, unique path will be '...\\b'.
208 > * 2. for path2, 'c' is not present in path1 hence unique path is '..\\c'.
209 > */
210 > const ellipsis = '\u2026';
211 > const unc = '\\\\';
212 > const urlSchemaRegexp = /^[^:/\\?#]+?:\/\//;
213 > const home = '~';
214 > export function shorten(paths: string[], defaultPathSeparator: string = sep): string[] {
215 const shortenedPaths: string[] = new Array(paths.length);
216
323 return shortenedPaths;
324 }
325 > labels.ts
326 > export interface ISeparator {
327 > label: string;
328 > }
329 >
330 > enum Type {
331 > TEXT,
332 > VARIABLE,
333 > SEPARATOR
334 > }
335 >
336 > interface ISegment {
337 > value: string;
338 > type: Type;
339 > }
340 >
341 > /**
342 > * Helper to insert values for specific template variables into the string. E.g. "this $(is) a $(template)" can be
343 > * passed to this function together with an object that maps "is" and "template" to strings to have them replaced.
344 > * @param value string to which template is applied
345 > * @param values the values of the templates to use
346 > */
347 > export function template(template: string, values: { [key: string]: string | ISeparator | undefined | null } = Object.create(null)): string {
348 const segments: ISegment[] = [];
349
409 }).map(segment => segment.value).join('');
410 }
411 > labels.ts
412 > /**
413 > * Handles mnemonics for menu items. Depending on OS:
414 > * - Windows: Supported via & character (replace && with &)
415 > * - Linux: Supported via & character (replace && with &)
416 > * - macOS: Unsupported (replace && with empty string)
417 > */
418 > export function mnemonicMenuLabel(label: string, forceDisableMnemonics?: boolean): string {
419 if (isMacintosh || forceDisableMnemonics) {
420 return label.replace(/\(&&\w\)|&&/g, '').replace(/&/g, isMacintosh ? '&' : '&&');
423 return label.replace(/&&|&/g, m => m === '&' ? '&&' : '&');
424 }
425 > labels.ts
426 > /**
427 > * Handles mnemonics for buttons. Depending on OS:
428 > * - Windows: Supported via & character (replace && with & and & with && for escaping)
429 > * - Linux: Supported via _ character (replace && with _)
430 > * - macOS: Unsupported (replace && with empty string)
431 > * When forceDisableMnemonics is set, returns just the label without mnemonics.
432 > */
433 > export function mnemonicButtonLabel(label: string, forceDisableMnemonics: true): string;
434 > export function mnemonicButtonLabel(label: string, forceDisableMnemonics?: false): { readonly withMnemonic: string; readonly withoutMnemonic: string };
435 > export function mnemonicButtonLabel(label: string, forceDisableMnemonics?: boolean): { readonly withMnemonic: string; readonly withoutMnemonic: string } | string {
436 const withoutMnemonic = label.replace(/\(&&\w\)|&&/g, '');
437
451 return { withMnemonic, withoutMnemonic };
452 }
453 > labels.ts
454 > export function unmnemonicLabel(label: string): string {
455 return label.replace(/&/g, '&&');
456 }
457 > labels.ts
458 > /**
459 > * Splits a recent label in name and parent path, supporting both '/' and '\' and workspace suffixes.
460 > * If the location is remote, the remote name is included in the name part.
461 > */
462 > export function splitRecentLabel(recentLabel: string): { name: string; parentPath: string } {
463 if (recentLabel.endsWith(']')) {
464 // label with workspace suffix
472 return splitName(recentLabel);
473 }
474 > labels.ts
475 function splitName(fullPath: string): { name: string; parentPath: string } {
476 const p = fullPath.indexOf('/') !== -1 ? posix : win32;
src/vs/platform/agentHost/common/sandboxConfigSchema.ts 143 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- sandboxConfigSchema.ts
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 > import { localize } from '../../../nls.js';
7 > import { AgentNetworkDomainSettingId } from '../../networkFilter/common/settings.js';
8 > import { AgentSandboxEnabledValue, AgentSandboxSettingId } from '../../sandbox/common/settings.js';
9 > import { createSchema, schemaProperty } from './agentHostSchema.js';
10 >
11 > /**
12 > * Top-level keys the agent host's root config bag exposes for sandboxing.
13 > * All sandbox-related values live nested under {@link AgentHostSandboxConfigKey.Sandbox}
14 > * — the persisted JSON has a single `"sandbox": { ... }` object rather than a
15 > * dozen flat keys.
16 > */
17 > export const enum AgentHostSandboxConfigKey {
18 > Sandbox = 'sandbox',
19 > }
20 >
21 > /**
22 > * Well-known sub-keys inside the agent host's `sandbox` object. These are
23 > * intentionally a flat, prefix-free namespace owned by the agent host —
24 > * distinct from the workbench's `chat.agent.sandbox.*` setting IDs. Hosts
25 > * (today: the workbench client) translate from their setting IDs to these
26 > * keys when forwarding values via a `RootConfigChanged` action.
27 > */
28 > export const enum AgentHostSandboxKey {
29 > Enabled = 'enabled',
30 > WindowsEnabled = 'enabled.windows',
31 > AllowNetwork = 'allowNetwork',
32 > AllowUnsandboxedCommands = 'allowUnsandboxedCommands',
33 > LinuxFileSystem = 'fileSystem.linux',
34 > MacFileSystem = 'fileSystem.mac',
35 > WindowsFileSystem = 'fileSystem.windows',
36 > AdvancedRuntime = 'advanced.runtime',
37 > AllowedNetworkDomains = 'allowedNetworkDomains',
38 > DeniedNetworkDomains = 'deniedNetworkDomains',
39 > }
40 >
41 > /** Shape of the persisted/forwarded `sandbox` object. */
42 > export type ISandboxConfigValue = Partial<{
43 > [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue;
44 > [AgentHostSandboxKey.WindowsEnabled]: AgentSandboxEnabledValue;
45 > [AgentHostSandboxKey.AllowNetwork]: boolean;
46 > [AgentHostSandboxKey.AllowUnsandboxedCommands]: boolean;
47 > [AgentHostSandboxKey.LinuxFileSystem]: Record<string, unknown>;
48 > [AgentHostSandboxKey.MacFileSystem]: Record<string, unknown>;
49 > [AgentHostSandboxKey.WindowsFileSystem]: Record<string, unknown>;
50 > [AgentHostSandboxKey.AdvancedRuntime]: Record<string, unknown>;
51 > [AgentHostSandboxKey.AllowedNetworkDomains]: string[];
52 > [AgentHostSandboxKey.DeniedNetworkDomains]: string[];
53 > }>;
54 >
55 > /**
56 > * Schema for the subset of workbench sandbox settings that hosts (today: the
57 > * workbench client) may forward into the agent host's root config bag.
58 > *
59 > * The agent host's terminal sandbox engine reads these values through
60 > * {@link IAgentConfigurationService.getRootValue}. Only the modern,
61 > * normalized form of each setting is declared here — the workbench is
62 > * expected to:
63 > *
64 > * - map legacy boolean sandbox enabled values to the `'on' | 'off' | 'allowNetwork'`
65 > * agent-host enum, and
66 > * - migrate values from any deprecated setting IDs to their modern key
67 > *
68 > * before pushing a `RootConfigChanged` action. That keeps the agent-host
69 > * schema (and validation) free of backward-compat baggage.
70 > */
71 > export const sandboxConfigSchema = createSchema({
72 > [AgentHostSandboxConfigKey.Sandbox]: schemaProperty<ISandboxConfigValue>({
73 > type: 'object',
74 > title: localize('agentHost.config.sandbox.title', "Agent Sandbox"),
75 > properties: {
76 > [AgentHostSandboxKey.Enabled]: {
77 > type: 'string',
78 > title: localize('agentHost.config.sandbox.enabled.title', "Sandbox Enabled"),
79 > enum: [AgentSandboxEnabledValue.Off, AgentSandboxEnabledValue.On, AgentSandboxEnabledValue.AllowNetwork],
80 > },
81 > [AgentHostSandboxKey.WindowsEnabled]: {
82 > type: 'string',
83 > title: localize('agentHost.config.sandbox.windowsEnabled.title', "Sandbox Enabled (Windows)"),
84 > enum: [AgentSandboxEnabledValue.Off, AgentSandboxEnabledValue.On, AgentSandboxEnabledValue.AllowNetwork],
85 > },
86 > [AgentHostSandboxKey.AllowNetwork]: {
87 > type: 'boolean',
88 > title: localize('agentHost.config.sandbox.allowNetwork.title', "Allow Network"),
89 > },
90 > [AgentHostSandboxKey.AllowUnsandboxedCommands]: {
91 > type: 'boolean',
92 > title: localize('agentHost.config.sandbox.allowUnsandboxedCommands.title', "Allow Unsandboxed Commands"),
93 > },
94 > [AgentHostSandboxKey.LinuxFileSystem]: {
95 > type: 'object',
96 > title: localize('agentHost.config.sandbox.linuxFileSystem.title', "Linux Sandbox Filesystem"),
97 > },
98 > [AgentHostSandboxKey.MacFileSystem]: {
99 > type: 'object',
100 > title: localize('agentHost.config.sandbox.macFileSystem.title', "macOS Sandbox Filesystem"),
101 > },
102 > [AgentHostSandboxKey.WindowsFileSystem]: {
103 > type: 'object',
104 > title: localize('agentHost.config.sandbox.windowsFileSystem.title', "Windows Sandbox Filesystem"),
105 > },
106 > [AgentHostSandboxKey.AdvancedRuntime]: {
107 > type: 'object',
108 > title: localize('agentHost.config.sandbox.advancedRuntime.title', "Advanced Sandbox Runtime"),
109 > },
110 > [AgentHostSandboxKey.AllowedNetworkDomains]: {
111 > type: 'array',
112 > title: localize('agentHost.config.sandbox.allowedDomains.title', "Allowed Network Domains"),
113 > items: { type: 'string', title: localize('agentHost.config.sandbox.allowedDomains.item.title', "Domain") },
114 > },
115 > [AgentHostSandboxKey.DeniedNetworkDomains]: {
116 > type: 'array',
117 > title: localize('agentHost.config.sandbox.deniedDomains.title', "Denied Network Domains"),
118 > items: { type: 'string', title: localize('agentHost.config.sandbox.deniedDomains.item.title', "Domain") },
119 > },
120 > },
121 > }),
122 > });
123 >
124 > /**
125 > * Maps modern workbench sandbox setting IDs (the ones the engine asks about)
126 > * to the sub-keys inside the agent host's `sandbox` config object.
127 > *
128 > * Deprecated setting IDs are intentionally absent: hosts forwarding values
129 > * into the agent host are expected to migrate deprecated → modern IDs
130 > * before dispatching `RootConfigChanged`.
131 > */
132 > export const sandboxSettingIdToAgentHostKey: Readonly<Record<string, AgentHostSandboxKey>> = {
133 > [AgentSandboxSettingId.AgentSandboxEnabled]: AgentHostSandboxKey.Enabled,
134 > [AgentSandboxSettingId.AgentSandboxWindowsEnabled]: AgentHostSandboxKey.WindowsEnabled,
135 > [AgentSandboxSettingId.AgentSandboxAllowNetwork]: AgentHostSandboxKey.AllowNetwork,
136 > [AgentSandboxSettingId.AgentSandboxAllowUnsandboxedCommands]: AgentHostSandboxKey.AllowUnsandboxedCommands,
137 > [AgentSandboxSettingId.AgentSandboxLinuxFileSystem]: AgentHostSandboxKey.LinuxFileSystem,
138 > [AgentSandboxSettingId.AgentSandboxMacFileSystem]: AgentHostSandboxKey.MacFileSystem,
139 > [AgentSandboxSettingId.AgentSandboxWindowsFileSystem]: AgentHostSandboxKey.WindowsFileSystem,
140 > [AgentSandboxSettingId.AgentSandboxAdvancedRuntime]: AgentHostSandboxKey.AdvancedRuntime,
141 > [AgentNetworkDomainSettingId.AllowedNetworkDomains]: AgentHostSandboxKey.AllowedNetworkDomains,
142 > [AgentNetworkDomainSettingId.DeniedNetworkDomains]: AgentHostSandboxKey.DeniedNetworkDomains,
143 > };
144
src/vs/platform/agentHost/common/state/sessionProtocol.ts 140 covered LOC · 5 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- sessionProtocol.ts
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 > // Protocol messages using JSON-RPC 2.0 framing for the sessions process.
7 > // See protocol.md for the full design.
8 > //
9 > // Most types are re-exported from the auto-generated protocol layer.
10 > // This file adds VS Code-specific additions (ISetAuthTokenParams, ProtocolError)
11 > // and backward-compatible aliases.
12 >
13 > // ---- Re-exports from protocol -----------------------------------------------
14 >
15 > // JSON-RPC base types
16 > export type {
17 > JsonRpcErrorResponse,
18 > JsonRpcNotification,
19 > JsonRpcParseErrorResponse,
20 > JsonRpcRequest,
21 > JsonRpcResponse,
22 > JsonRpcSuccessResponse,
23 > } from './protocol/messages.js';
24 >
25 > // Typed message unions
26 > export type {
27 > AhpClientNotification,
28 > AhpNotification,
29 > AhpRequest,
30 > AhpResponse,
31 > AhpServerNotification,
32 > AhpSuccessResponse,
33 > CommandMap,
34 > ClientNotificationMap,
35 > ProtocolMessage,
36 > ServerNotificationMap,
37 > } from './protocol/messages.js';
38 >
39 > // Command params and results
40 > export type {
41 > CreateSessionParams,
42 > DirectoryEntry,
43 > DispatchActionParams,
44 > DisposeSessionParams,
45 > FetchTurnsParams,
46 > FetchTurnsResult,
47 > InitializeParams,
48 > InitializeResult,
49 > ListSessionsParams,
50 > ListSessionsResult,
51 > ReconnectParams,
52 > ReconnectReplayResult,
53 > ReconnectResult,
54 > ReconnectSnapshotResult,
55 > ResourceCopyParams,
56 > ResourceCopyResult,
57 > ResourceDeleteParams,
58 > ResourceDeleteResult,
59 > ResourceListParams,
60 > ResourceListResult,
61 > ResourceMkdirParams,
62 > ResourceMkdirResult,
63 > ResourceMoveParams,
64 > ResourceMoveResult,
65 > ResourceReadParams,
66 > ResourceReadResult,
67 > ResourceResolveParams,
68 > ResourceResolveResult,
69 > ResourceWriteParams,
70 > ResourceWriteResult,
71 > SubscribeParams,
72 > SubscribeResult,
73 > UnsubscribeParams,
74 > } from './protocol/commands.js';
75 >
76 > export type {
77 > CreateResourceWatchParams,
78 > CreateResourceWatchResult,
79 > } from './protocol/channels-resource-watch/commands.js';
80 >
81 > export { ContentEncoding, ReconnectResultType, ResourceType, ResourceWriteMode } from './protocol/commands.js';
82 > export { ResourceChangeType } from './protocol/channels-resource-watch/state.js';
83 > export type { ResourceChange, ResourceWatchState } from './protocol/channels-resource-watch/state.js';
84 >
85 > // Error codes
86 > export { AhpErrorCodes, JsonRpcErrorCodes } from './protocol/errors.js';
87 > export type { AhpErrorCode, JsonRpcErrorCode } from './protocol/errors.js';
88 >
89 > // Snapshot type (re-exported from state). The generated `Snapshot.state`
90 > // union now includes `ChatState`, so per-chat snapshots type-check directly.
91 > import type { Snapshot as ProtocolSnapshot } from './protocol/state.js';
92 > export type IStateSnapshot = ProtocolSnapshot;
93 >
94 > // ---- Backward-compatible error code aliases ---------------------------------
95 >
96 > export const JSON_RPC_PARSE_ERROR = -32700 as const;
97 > export const JSON_RPC_INTERNAL_ERROR = -32603 as const;
98 > export const AHP_SESSION_NOT_FOUND = -32001 as const;
99 > export const AHP_PROVIDER_NOT_FOUND = -32002 as const;
100 > export const AHP_SESSION_ALREADY_EXISTS = -32003 as const;
101 > export const AHP_TURN_IN_PROGRESS = -32004 as const;
102 > export const AHP_UNSUPPORTED_PROTOCOL_VERSION = -32005 as const;
103 > export const AHP_CONTENT_NOT_FOUND = -32006 as const;
104 > export const AHP_AUTH_REQUIRED = -32007 as const;
105 >
106 > // ---- Type guards -----------------------------------------------------------
107 >
108 > import type { AhpRequest, AhpNotification, AhpSuccessResponse, ProtocolMessage, JsonRpcErrorResponse } from './protocol/messages.js';
109 >
110 > export function isJsonRpcRequest(msg: ProtocolMessage): msg is AhpRequest {
111 return 'method' in msg && 'id' in msg;
112 }
114 > export function isJsonRpcNotification(msg: ProtocolMessage): msg is AhpNotification {
115 return 'method' in msg && !('id' in msg);
116 }
118 > export function isJsonRpcResponse(msg: ProtocolMessage): msg is AhpSuccessResponse | JsonRpcErrorResponse {
119 return 'id' in msg && !('method' in msg);
120 }
122 > // ---- VS Code-specific types ------------------------------------------------
123 >
124 > /**
125 > * Error with a JSON-RPC error code for protocol-level failures.
126 > * Optionally carries a `data` payload for structured error details.
127 > */
128 > export class ProtocolError extends Error {
129 > constructor(readonly code: number, message: string, readonly data?: unknown) {
130 super(message);
131 }
133 >
134 > /**
135 > * VS Code-specific extension: set the auth token on the server.
136 > * Not yet part of the official protocol.
137 > */
138 > export interface ISetAuthTokenParams {
139 > readonly token: string;
140 > }
141 >
142 > // ---- Server → Client notification param aliases (backward compat) -----------
143 >
144 > import type { INotification } from './sessionActions.js';
145 >
146 > export interface INotificationBroadcastParams {
147 > readonly notification: INotification;
148 > }
src/vs/platform/agentHost/node/osc633Parser.ts 139 covered LOC · 8 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- osc633Parser.ts
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 > /**
7 > * Lightweight parser for OSC 633 (VS Code shell integration) sequences in raw
8 > * PTY output. Designed for the agent host where we don't have a full xterm.js
9 > * instance - it scans data chunks for the sequences, extracts events, and
10 > * removes the sequences from the data stream.
11 > *
12 > * Handles partial sequences that span across data chunk boundaries.
13 > */
14 >
15 > /** OSC 633 event types we care about. */
16 > export const enum Osc633EventType {
17 > /** 633;A - Prompt start. Used to detect shell integration is active. */
18 > PromptStart,
19 > /** 633;B - Command start (where user inputs command). */
20 > CommandStart,
21 > /** 633;C - Command executed (output begins). */
22 > CommandExecuted,
23 > /** 633;D[;exitCode] - Command finished. */
24 > CommandFinished,
25 > /** 633;E;commandLine[;nonce] - Explicit command line. */
26 > CommandLine,
27 > /** 633;P;Key=Value - Property (e.g. Cwd). */
28 > Property,
29 > }
30 >
31 > export interface IOsc633PromptStartEvent {
32 > type: Osc633EventType.PromptStart;
33 > }
34 >
35 > export interface IOsc633CommandStartEvent {
36 > type: Osc633EventType.CommandStart;
37 > }
38 >
39 > export interface IOsc633CommandExecutedEvent {
40 > type: Osc633EventType.CommandExecuted;
41 > }
42 >
43 > export interface IOsc633CommandFinishedEvent {
44 > type: Osc633EventType.CommandFinished;
45 > exitCode: number | undefined;
46 > }
47 >
48 > export interface IOsc633CommandLineEvent {
49 > type: Osc633EventType.CommandLine;
50 > commandLine: string;
51 > nonce: string | undefined;
52 > }
53 >
54 > export interface IOsc633PropertyEvent {
55 > type: Osc633EventType.Property;
56 > key: string;
57 > value: string;
58 > }
59 >
60 > export type Osc633Event =
61 > | IOsc633PromptStartEvent
62 > | IOsc633CommandStartEvent
63 > | IOsc633CommandExecutedEvent
64 > | IOsc633CommandFinishedEvent
65 > | IOsc633CommandLineEvent
66 > | IOsc633PropertyEvent;
67 >
68 > export interface IOsc633ParseResult {
69 > /** Data with all OSC 633 sequences stripped. */
70 > cleanedData: string;
71 > /** Parsed events in order of appearance. */
72 > events: Osc633Event[];
73 > }
74 >
75 > /**
76 > * A single segment of parsed PTY data: either a run of cleaned output data or
77 > * an OSC 633 event. Segments are emitted in stream order so that output which
78 > * arrives before an event (e.g. a `CommandFinished` marker) can be attributed
79 > * to the command before the event is handled — see {@link Osc633Parser.parseSegments}.
80 > */
81 > export type Osc633ParseSegment =
82 > | { readonly kind: 'data'; readonly data: string }
83 > | { readonly kind: 'event'; readonly event: Osc633Event };
84 >
85 > /**
86 > * Decode escaped values in OSC 633 messages.
87 > * Handles `\\` -> `\` and `\xAB` -> character with code 0xAB.
88 > */
89 function deserializeOscMessage(message: string): string {
90 if (message.indexOf('\\') === -1) {
96 );
97 }
99 function parseOsc633Payload(payload: string): Osc633Event | undefined {
100 const semiIdx = payload.indexOf(';');
142 }
143 }
145 > // OSC introducer is ESC ] (0x1b 0x5d)
146 > const ESC = '\x1b';
147 > const OSC_START = ESC + ']';
148 > // Terminators: BEL (0x07) or ST (ESC \)
149 > const BEL = '\x07';
150 > const ST = ESC + '\\';
151 >
152 > /**
153 > * Stateful parser that handles data chunks, correctly dealing with
154 > * partial sequences that span multiple chunks.
155 > */
156 > export class Osc633Parser {
157 /** Buffer for an incomplete OSC sequence (from ESC] up to but not including the terminator). */
158 private _pendingOsc = '';
161 /** Set when the previous chunk ended with ESC inside an OSC body (potential ST start). */
162 private _pendingEscInOsc = false;
164 > /**
165 > * Parse a chunk of PTY data.
166 > * Returns cleaned data (all OSC 633 sequences removed) and extracted events.
167 > *
168 > * This is a convenience view over {@link parseSegments} that concatenates the
169 > * cleaned-data segments and collects the events. Callers that need to know
170 > * whether a run of output arrived before or after an event (for correct
171 > * command-output attribution) should use {@link parseSegments} instead.
172 > */
173 > parse(data: string): IOsc633ParseResult {
174 const events: Osc633Event[] = [];
175 let cleanedData = '';
183 return { cleanedData, events };
184 }
186 > /**
187 > * Parse a chunk of PTY data into an ordered list of segments, preserving the
188 > * relative order of cleaned output data and OSC 633 events as they appear in
189 > * the stream. Handles partial sequences that span multiple chunks.
190 > *
191 > * Preserving order matters because a single PTY read frequently contains a
192 > * command's output immediately followed by its `CommandFinished` marker;
193 > * consumers must append that output to the command before handling the
194 > * finished event, otherwise the output is lost from the command result.
195 > */
196 > parseSegments(data: string): Osc633ParseSegment[] {
197 const segments: Osc633ParseSegment[] = [];
198 let pending = '';
291 return segments;
292 }
294 > /**
295 > * Consume characters from the OSC body, appending to _pendingOsc until a
296 > * terminator (BEL or ST) is found.
297 > */
298 > private _consumeOscBody(data: string, startIdx: number): { nextIndex: number; complete: boolean; pendingEsc?: boolean; terminator?: string } {
299 const belIdx = data.indexOf(BEL, startIdx);
300 const escIdx = data.indexOf(ESC, startIdx);
322 return { nextIndex: data.length, complete: false };
323 }
325 > /**
326 > * Process a complete OSC payload. If it's a 633; sequence, extract the
327 > * event via {@link emitEvent}. Otherwise, reconstruct the original bytes and
328 > * pass them through to the cleaned output via {@link appendData}.
329 > */
330 > private _handleOscPayload(
331 payload: string,
332 emitEvent: (event: Osc633Event) => void,
src/vs/platform/agentHost/node/shared/agentHostOctoKitService.ts 139 covered LOC · 9 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostOctoKitService.ts
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 > import { LRUCache } from '../../../../base/common/map.js';
7 > import { createDecorator } from '../../../instantiation/common/instantiation.js';
8 > import { ILogService } from '../../../log/common/log.js';
9 > import { IAgentHostGitHubEndpointService } from '../agentHostGitHubEndpointService.js';
10 >
11 > export type FetchFunction = typeof globalThis.fetch;
12 >
13 > /**
14 > * Successful result of {@link IAgentHostOctoKitService.createPullRequest}.
15 > *
16 > * Mirrors the `CreatedPullRequest` type returned by `OctoKitService` in
17 > * `extensions/copilot/src/platform/github/common/githubService.ts` so the
18 > * shapes line up if/when the two are ported together.
19 > */
20 > export interface CreatedPullRequest {
21 > readonly url: string;
22 > readonly number: number;
23 > readonly nodeId?: string;
24 > }
25 >
26 > /**
27 > * Merge strategy used when enabling auto-merge on a pull request.
28 > * Mirrors the GitHub GraphQL `PullRequestMergeMethod` enum.
29 > */
30 > export type AutoMergeMethod = 'MERGE' | 'SQUASH' | 'REBASE';
31 >
32 > interface GitHubPullRequestResponseItem {
33 > readonly number?: unknown;
34 > readonly html_url?: unknown;
35 > readonly node_id?: unknown;
36 > }
37 >
38 > export interface IGitHubApiResponse<T> {
39 > readonly data: T | undefined;
40 > readonly statusCode: number;
41 > readonly etag?: string;
42 > }
43 >
44 > /**
45 > * Minimal GitHub REST client living in the agent-host process.
46 > *
47 > * The agent host runs headless and has no access to the workbench
48 > * `IOctoKitService` / Octokit / VS Code auth providers. This service is a
49 > * deliberately small re-implementation of the bits we need, modelled on
50 > * `OctoKitService` from the Copilot extension so the API surface is
51 > * familiar. Only operations the agent host actually needs are exposed —
52 > * extend this interface as new changeset operations are added.
53 > *
54 > * The caller is responsible for supplying a GitHub OAuth token with the
55 > * scopes required by the operation (e.g. `repo` for {@link createPullRequest}).
56 > * Tokens are typically obtained from the agent host's
57 > * `authenticate(resource, token)` token store, which the workbench pushes
58 > * on session create via the same channel used for `ICopilotApiService`.
59 > */
60 > export interface IAgentHostOctoKitService {
61 > readonly _serviceBrand: undefined;
62 >
63 > /**
64 > * Creates a pull request on github.com.
65 > *
66 > * Mirrors `OctoKitService.createPullRequest` from the Copilot extension.
67 > * Throws on non-2xx responses or malformed payloads.
68 > */
69 > createPullRequest(
70 > owner: string,
71 > repo: string,
72 > title: string,
73 > body: string,
74 > head: string,
75 > base: string,
76 > draft: boolean,
77 > token: string,
78 > signal: AbortSignal,
79 > ): Promise<CreatedPullRequest>;
80 >
81 > /** Finds the most recently updated pull request for `owner:branch`, if any. */
82 > findPullRequestByHeadBranch(owner: string, repo: string, branch: string, token: string, signal: AbortSignal): Promise<CreatedPullRequest | undefined>;
83 >
84 > /**
85 > * Enables auto-merge on a pull request so GitHub merges it automatically
86 > * once all required reviews and status checks pass.
87 > *
88 > * Issues the GraphQL `enablePullRequestAutoMerge` mutation. `pullRequestId`
89 > * is the pull request's GraphQL global node id (see
90 > * {@link CreatedPullRequest.nodeId}). Throws on GraphQL or transport errors,
91 > * including when the repository does not allow the requested merge method or
92 > * auto-merge is not enabled for the repository.
93 > */
94 > enablePullRequestAutoMerge(pullRequestId: string, mergeMethod: AutoMergeMethod, token: string, signal: AbortSignal): Promise<void>;
95 > }
96 >
97 > export const IAgentHostOctoKitService = createDecorator<IAgentHostOctoKitService>('agentHostOctoKitService');
98 >
99 > const GITHUB_API_VERSION = '2022-11-28';
100 > const MAX_ERROR_RESPONSE_BODY_LENGTH = 500;
101 >
102 > const ENABLE_AUTO_MERGE_MUTATION = `mutation EnableAutoMerge($pullRequestId: ID!, $mergeMethod: PullRequestMergeMethod!) {
103 > enablePullRequestAutoMerge(input: { pullRequestId: $pullRequestId, mergeMethod: $mergeMethod }) {
104 > pullRequest { id }
105 > }
106 > }`;
107 >
108 > export class AgentHostOctoKitService implements IAgentHostOctoKitService {
109 >
110 > declare readonly _serviceBrand: undefined;
111 >
112 > private readonly _fetch: FetchFunction;
113 >
114 > /**
115 > * A cache of ETags for pull request search results.
116 > */
117 > private readonly pullRequestSearchEtags = new LRUCache<string, string>(100);
118 >
119 > constructor(
120 > fetchFn: FetchFunction | undefined, agentHostOctoKitService.ts
121 > @ILogService private readonly _logService: ILogService,
122 > @IAgentHostGitHubEndpointService private readonly _endpoint: IAgentHostGitHubEndpointService,
123 > ) {
124 > this._fetch = fetchFn ?? globalThis.fetch;
125 > }
127 > async createPullRequest(
128 owner: string,
129 repo: string,
153 return { url: html_url, number, nodeId: typeof node_id === 'string' ? node_id : undefined };
154 }
156 > async findPullRequestByHeadBranch(owner: string, repo: string, branch: string, token: string, signal: AbortSignal): Promise<CreatedPullRequest | undefined> {
157 const routeSlug = `repos/${owner}/${repo}/pulls?head=${encodeURIComponent(`${owner}:${branch}`)}&state=all&sort=updated&direction=desc&per_page=1`;
158
186 : undefined;
187 }
189 > async enablePullRequestAutoMerge(pullRequestId: string, mergeMethod: AutoMergeMethod, token: string, signal: AbortSignal): Promise<void> {
190 await this._makeGraphQLRequest(ENABLE_AUTO_MERGE_MUTATION, { pullRequestId, mergeMethod }, token, signal);
191 }
193 > private async _makeGHAPIRequest<T>(
194 routeSlug: string,
195 method: 'GET' | 'POST',
262 }
263 }
265 > private async _makeGraphQLRequest(
266 query: string,
267 variables: Record<string, unknown>,
321 return json.data;
322 }
324 > private _formatErrorResponseBody(errorText: string | undefined): string | undefined {
325 const normalized = errorText?.replace(/\s+/g, ' ').trim();
326 if (!normalized) {
331 : normalized;
332 }
334 >
335 function parseRateLimitHeader(value: string | string[] | undefined): number | undefined {
336 if (value === undefined) {
src/vs/platform/agentHost/common/agentHostCustomizationConfig.ts 138 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostCustomizationConfig.ts
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 > import { localize } from '../../../nls.js';
7 > import { createSchema, schemaProperty } from './agentHostSchema.js';
8 > import { CustomizationType, type Customization, type PluginCustomization } from './state/protocol/state.js';
9 > import { customizationId } from './state/sessionState.js';
10 >
11 > export const codexUsageSources = ['copilot', 'openai'] as const;
12 > export type CodexUsageSource = typeof codexUsageSources[number];
13 >
14 > /**
15 > * Well-known root-config keys used by the platform to configure agent-host
16 > * customizations.
17 > */
18 > export const enum AgentHostConfigKey {
19 > /** Host-owned Open Plugins available to remote sessions. */
20 > Customizations = 'customizations',
21 > /**
22 > * Absolute path to the shell executable for host-managed terminals.
23 > * TODO: revisit magic key in config; refine into a dedicated typed channel. https://github.com/microsoft/vscode/issues/313812
24 > */
25 > DefaultShell = 'defaultShell',
26 > /**
27 > * When true (the default), the Claude provider routes all Anthropic
28 > * `messages` traffic through the local Copilot-CAPI proxy (Copilot-routed
29 > * Claude). When false, the Claude Agent SDK talks to Anthropic directly on
30 > * the user's own credentials (BYO Anthropic — Phase 19).
31 > */
32 > ClaudeUseCopilotProxy = 'claudeUseCopilotProxy',
33 > CodexUsageSource = 'codexUsageSource',
34 > /** Controls whether session-scoped file customizations come from local scan or SDK discovery. */
35 > SessionCustomizationDiscoveryMode = 'sessionCustomizationDiscoveryMode',
36 > /**
37 > * Optional GitHub Enterprise base URI (e.g. `https://ghe.example.com` for a
38 > * GitHub Enterprise Server, or `https://tenant.ghe.com` for GitHub Enterprise
39 > * Cloud). When set, the agent host computes its GitHub protected resources and
40 > * REST/GraphQL endpoints from this base instead of github.com. Normally pushed
41 > * by the local VS Code client from the workbench `github-enterprise.uri`
42 > * setting; remote operators set it directly in the remote
43 > * `agent-host-config.json`.
44 > */
45 > GithubEnterpriseUri = 'githubEnterpriseUri',
46 > }
47 >
48 > export const SESSION_CUSTOMIZATION_DISCOVERY_MODES = ['scan', 'discover'] as const;
49 > export type SessionCustomizationDiscoveryMode = typeof SESSION_CUSTOMIZATION_DISCOVERY_MODES[number];
50 > export const DEFAULT_SESSION_CUSTOMIZATION_DISCOVERY_MODE: SessionCustomizationDiscoveryMode = 'scan';
51 >
52 > /**
53 > * Persisted on-disk shape for a host-configured plugin. Kept stable across
54 > * the customization protocol refactor so existing `agent-host-config.json`
55 > * files keep working; entries are mapped to the new
56 > * {@link Customization} shape at read time by
57 > * {@link getAgentHostConfiguredCustomizations}.
58 > */
59 > interface IPersistedCustomizationConfigEntry {
60 > uri: string;
61 > displayName: string;
62 > description?: string;
63 > }
64 >
65 > export const agentHostCustomizationConfigSchema = createSchema({
66 > [AgentHostConfigKey.Customizations]: schemaProperty<IPersistedCustomizationConfigEntry[]>({
67 > type: 'array',
68 > title: localize('agentHost.config.customizations.title', "Plugins"),
69 > description: localize('agentHost.config.customizations.description', "Plugins configured on this agent host and available to remote sessions."),
70 > default: [],
71 > items: {
72 > type: 'object',
73 > title: localize('agentHost.config.customizations.itemTitle', "Plugin"),
74 > properties: {
75 > uri: {
76 > type: 'string',
77 > title: localize('agentHost.config.customizations.uri', "Plugin URI"),
78 > },
79 > displayName: {
80 > type: 'string',
81 > title: localize('agentHost.config.customizations.displayName', "Name"),
82 > },
83 > description: {
84 > type: 'string',
85 > title: localize('agentHost.config.customizations.descriptionField', "Description"),
86 > },
87 > },
88 > required: ['uri', 'displayName'],
89 > },
90 > }),
91 > [AgentHostConfigKey.DefaultShell]: schemaProperty<string>({
92 > type: 'string',
93 > title: localize('agentHost.config.defaultShell.title', "Default Shell"),
94 > description: localize('agentHost.config.defaultShell.description', "Absolute path to the shell executable used by host-managed terminals. Normally pushed by the connected VS Code client from `terminal.integrated.agentHostProfile.<os>` (falling back to `terminal.integrated.defaultProfile.<os>`); when unset, the agent host falls back to the system shell. Only the path is supported; `args` and `env` from the workbench profile are not piped through yet. The workbench only pushes this for the local agent host — remote agent host operators should set this directly in the remote machine's `agent-host-config.json`."),
95 > }),
96 > [AgentHostConfigKey.ClaudeUseCopilotProxy]: schemaProperty<boolean>({
97 > type: 'boolean',
98 > title: localize('agentHost.config.claudeUseCopilotProxy.title', "Route Claude Through Copilot"),
99 > description: localize('agentHost.config.claudeUseCopilotProxy.description', "When enabled (the default), the Claude agent routes all requests through GitHub Copilot. When disabled, Claude talks to Anthropic directly using your own credentials (API key or Claude subscription)."),
100 > default: true,
101 > }),
102 > [AgentHostConfigKey.CodexUsageSource]: schemaProperty<CodexUsageSource>({
103 > type: 'string',
104 > title: localize('agentHost.config.codexUsageSource.title', "Codex Usage Source"),
105 > description: localize('agentHost.config.codexUsageSource.description', "Choose whether Codex usage is routed through GitHub Copilot or uses an existing Codex OpenAI login. VS Code does not provide the OpenAI sign-in flow; authenticate Codex separately before selecting OpenAI."),
106 > default: 'copilot',
107 > enum: [...codexUsageSources],
108 > }),
109 > [AgentHostConfigKey.SessionCustomizationDiscoveryMode]: schemaProperty<SessionCustomizationDiscoveryMode>({
110 > type: 'string',
111 > enum: [...SESSION_CUSTOMIZATION_DISCOVERY_MODES],
112 > title: localize('agentHost.config.sessionCustomizationDiscoveryMode.title', "Session Customization Discovery Mode"),
113 > description: localize('agentHost.config.sessionCustomizationDiscoveryMode.description', "Controls whether session-scoped customizations are populated from local file scanning or from Copilot SDK discovery."),
114 > default: DEFAULT_SESSION_CUSTOMIZATION_DISCOVERY_MODE,
115 > }),
116 > [AgentHostConfigKey.GithubEnterpriseUri]: schemaProperty<string>({
117 > type: 'string',
118 > title: localize('agentHost.config.githubEnterpriseUri.title', "GitHub Enterprise URI"),
119 > description: localize('agentHost.config.githubEnterpriseUri.description', "Optional base URI of a GitHub Enterprise instance (for example \"https://ghe.example.com\" for GitHub Enterprise Server, or \"https://tenant.ghe.com\" for GitHub Enterprise Cloud). When set, the agent host authenticates and makes GitHub API calls against this instance instead of github.com. Normally pushed by the connected VS Code client from the `github-enterprise.uri` setting; remote agent host operators can set it directly in the remote `agent-host-config.json`."),
120 > }),
121 > });
122 >
123 > export const defaultAgentHostCustomizationConfigValues = {
124 > [AgentHostConfigKey.Customizations]: [] as IPersistedCustomizationConfigEntry[],
125 > };
126 >
127 > /**
128 > * Reads the persisted (legacy-shaped) plugin entries from the agent-host
129 > * root config and lifts them into the new {@link Customization} container
130 > * shape used by the rest of the platform.
131 > */
132 > export function getAgentHostConfiguredCustomizations(values: Record<string, unknown> | undefined): readonly Customization[] {
133 const raw = values?.[AgentHostConfigKey.Customizations];
134 const entries = agentHostCustomizationConfigSchema.validate(AgentHostConfigKey.Customizations, raw)
137 return entries.map(toContainerCustomization);
138 }
140 > /**
141 > * Lifts a persisted plugin config entry into the new
142 > * {@link Customization} container shape.
143 > */
144 > export function toContainerCustomization(entry: IPersistedCustomizationConfigEntry): PluginCustomization {
145 return {
146 type: CustomizationType.Plugin,
src/vs/platform/agentHost/common/meta/agentCompletionAttachmentMeta.ts 133 covered LOC · 7 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentCompletionAttachmentMeta.ts
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 > import type { SimpleMessageAttachment } from '../state/protocol/state.js';
7 >
8 > /**
9 > * Well-known typed views over a `SimpleMessageAttachment`'s `_meta` bag as
10 > * produced by the `completions` command, populated by the slash-command and
11 > * skill completion providers and read by the session handler. Read the bag
12 > * through {@link readCompletionAttachmentMeta} rather than indexing `_meta`
13 > * directly. Two variants are distinguished by which discriminating key is
14 > * present: a slash command (`command`) or a skill (`uri`).
15 > */
16 >
17 > /**
18 > * A client-side side effect a command completion carries. When present, the
19 > * workbench interprets it on accept (see the shared agent-host completion action
20 > * handler) rather than treating the item as a plain text/reference insertion.
21 > *
22 > * Used by Copilot agent-host permission/mode toggles (e.g. `/yolo`,
23 > * `/autopilot on`): the item applies a well-known session-config change. Whether
24 > * the item leaves text behind is expressed by its `insertText` (empty for a pure
25 > * toggle; `/command ` for an item that keeps the text so an argument can be
26 > * typed, with {@link ICommandCompletionAttachmentMeta.argumentHint} as ghost text).
27 > */
28 > export interface IAgentHostCompletionAction {
29 > /**
30 > * A partial agent-host session-config change to apply when the completion is
31 > * accepted, keyed by well-known session-config property (e.g. `autoApprove`,
32 > * `mode`) to the string-enum value. Applied via the active session's provider
33 > * so the corresponding picker updates reactively.
34 > */
35 > readonly applyConfig?: Readonly<Record<string, string>>;
36 > }
37 >
38 > /**
39 > * The `_meta` shape attached to a `completions` result that resolves to a slash
40 > * command.
41 > */
42 > export interface ICommandCompletionAttachmentMeta {
43 > /** The slash command name (without the leading `/`). */
44 > readonly command: string;
45 > /** Optional human-readable description of the command. */
46 > readonly description?: string;
47 > /**
48 > * Optional hint describing the argument the command expects. Rendered as
49 > * inline placeholder (ghost text) after an accepted command completion.
50 > */
51 > readonly argumentHint?: string;
52 > /**
53 > * Optional client-side action to run when the completion is accepted (e.g. a
54 > * permission/mode session-config toggle). See {@link IAgentHostCompletionAction}.
55 > */
56 > readonly action?: IAgentHostCompletionAction;
57 > }
58 >
59 > /**
60 > * The `_meta` shape attached to a `completions` result that resolves to a skill.
61 > */
62 > export interface ISkillCompletionAttachmentMeta {
63 > /** The skill resource URI as a string. */
64 > readonly uri: string;
65 > /** Optional internal name of the skill. */
66 > readonly name?: string;
67 > /** Optional human-readable display name (e.g. the slash-command name). */
68 > readonly displayName?: string;
69 > /** Optional human-readable description of the skill. */
70 > readonly description?: string;
71 > }
72 >
73 > /**
74 > * A typed, discriminated view over the well-known `completions` attachment
75 > * `_meta` variants. The `kind` discriminant is computed by
76 > * {@link readCompletionAttachmentMeta} from which key is present on the wire; it
77 > * is not itself carried in `_meta`.
78 > */
79 > export type CompletionAttachmentMeta =
80 > | ({ readonly kind: 'command' } & ICommandCompletionAttachmentMeta)
81 > | ({ readonly kind: 'skill' } & ISkillCompletionAttachmentMeta);
82 >
83 > /**
84 > * Reads the well-known `completions` attachment `_meta` keys, classifying the
85 > * bag into a {@link CompletionAttachmentMeta} variant by its discriminating key
86 > * (`command` for a slash command, `uri` for a skill). Returns `undefined` when
87 > * the bag is absent or matches neither variant; wrong-typed keys are dropped.
88 > */
89 > export function readCompletionAttachmentMeta(attachment: SimpleMessageAttachment): CompletionAttachmentMeta | undefined {
90 const meta = attachment._meta;
91 if (!meta || typeof meta !== 'object' || Array.isArray(meta)) {
113 return undefined;
114 }
116 > /**
117 > * Serializes a typed {@link ICommandCompletionAttachmentMeta} into the `_meta`
118 > * record, dropping `undefined` entries. Build a slash-command completion's
119 > * `_meta` through this so producers stay in lock-step with
120 > * {@link readCompletionAttachmentMeta}.
121 > */
122 > export function toCommandCompletionAttachmentMeta(meta: ICommandCompletionAttachmentMeta): Record<string, unknown> {
123 const result: Record<string, unknown> = { command: meta.command };
124 if (meta.description !== undefined) {
134 return result;
135 }
137 > /**
138 > * Reads the optional {@link IAgentHostCompletionAction} carried on a command
139 > * completion's raw `_meta` bag (under the `action` key). Kept as the single
140 > * seam consumers use to obtain the action, mirroring {@link getCommandArgumentHint}.
141 > * Returns `undefined` when absent or malformed; wrong-typed sub-fields are dropped.
142 > */
143 > export function getCompletionAction(meta: Record<string, unknown> | undefined): IAgentHostCompletionAction | undefined {
144 if (!meta || typeof meta !== 'object' || Array.isArray(meta)) {
145 return undefined;
147 return readCompletionActionMeta(meta['action']);
148 }
150 > /**
151 > * Parses an unknown value into an {@link IAgentHostCompletionAction}. Accepts an
152 > * object with a string-map `applyConfig`; returns `undefined` when no valid
153 > * `applyConfig` is present.
154 > */
155 function readCompletionActionMeta(value: unknown): IAgentHostCompletionAction | undefined {
156 if (!value || typeof value !== 'object' || Array.isArray(value)) {
173 return { applyConfig };
174 }
176 > /**
177 > * Serializes an {@link IAgentHostCompletionAction} into a plain record for the
178 > * `_meta` bag, dropping empty entries. Returns `undefined` when the action
179 > * carries nothing meaningful.
180 > */
181 function toCompletionActionMeta(action: IAgentHostCompletionAction | undefined): Record<string, unknown> | undefined {
182 if (!action?.applyConfig || Object.keys(action.applyConfig).length === 0) {
185 return { applyConfig: { ...action.applyConfig } };
186 }
188 > /**
189 > * Reads the well-known `argumentHint` from a raw completion attachment `_meta`
190 > * bag. Kept as the single seam that consumers use to obtain the hint, so a
191 > * future promotion of `argumentHint` to a first-class attachment field only
192 > * needs to change this reader. Returns `undefined` when absent or wrong-typed.
193 > */
194 > export function getCommandArgumentHint(meta: Record<string, unknown> | undefined): string | undefined {
195 if (!meta || typeof meta !== 'object' || Array.isArray(meta)) {
196 return undefined;
198 return typeof meta['argumentHint'] === 'string' ? meta['argumentHint'] : undefined;
199 }
201 > /**
202 > * Serializes a typed {@link ISkillCompletionAttachmentMeta} into the `_meta`
203 > * record, dropping `undefined` entries. Build a skill completion's `_meta`
204 > * through this so producers stay in lock-step with
205 > * {@link readCompletionAttachmentMeta}.
206 > */
207 > export function toSkillCompletionAttachmentMeta(meta: ISkillCompletionAttachmentMeta): Record<string, unknown> {
208 const result: Record<string, unknown> = { uri: meta.uri };
209 if (meta.name !== undefined) {
src/vs/platform/agentHost/node/agentHostRestrictedTelemetry.ts 132 covered LOC · 13 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostRestrictedTelemetry.ts
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 > import { generateUuid } from '../../../base/common/uuid.js';
7 > import { ILogService } from '../../log/common/log.js';
8 > import { ICommonProperties } from '../../telemetry/common/telemetry.js';
9 >
10 > /**
11 > * Public GitHub Copilot telemetry ingestion keys. These are instrumentation keys, not
12 > * secrets; the iKey selects the destination hydro table:
13 > * - standard -> `copilot_v0_copilot_event`
14 > * - enhanced -> `copilot_v0_restricted_copilot_event`
15 > */
16 > const GH_STANDARD_IKEY = '7d7048df-6dd0-4048-bb23-b716c1461f8f';
17 > const GH_ENHANCED_IKEY = '3fdd7f28-937a-48c8-9a21-ba337db23bd1';
18 >
19 > /**
20 > * Fallback Copilot telemetry endpoint (the dotcom value of the CAPI token's
21 > * `endpoints.telemetry`, with the `/telemetry` path the Copilot CLI/runtime appends).
22 > * Used until {@link IAgentHostRestrictedTelemetry.setRestrictedTelemetryEndpoint} supplies
23 > * the user's discovered endpoint (dotcom, GHE, or proxy). Accepts unauthenticated POSTs.
24 > */
25 > const GH_TELEMETRY_URL = 'https://copilot-telemetry.githubusercontent.com/telemetry';
26 >
27 > /** Event names are namespaced by client category; the CTS name filter requires this. */
28 > const NAMESPACE = 'copilot-chat';
29 >
30 > export type TelemetryProps = Record<string, string | undefined>;
31 > export type TelemetryMeasurements = Record<string, number | undefined>;
32 >
33 > export interface IAgentHostInternalTelemetryContext {
34 > readonly isInternal: boolean;
35 > readonly trackingId: string | undefined;
36 > readonly userName: string | undefined;
37 > readonly isVscodeTeamMember: boolean;
38 > }
39 >
40 > export interface IAgentHostRestrictedTelemetryContext extends IAgentHostInternalTelemetryContext {
41 > readonly restrictedTelemetryEnabled: boolean;
42 > readonly telemetryEndpoint: string | undefined;
43 > /** Whether content exclusion is enabled; undefined when account discovery could not determine it. */
44 > readonly copilotIgnoreEnabled?: boolean;
45 > }
46 >
47 > export interface IAgentHostInternalTelemetrySink {
48 > setContext(context: IAgentHostInternalTelemetryContext | undefined): void;
49 > send(eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void;
50 > sendForContext(context: IAgentHostInternalTelemetryContext, eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void;
51 > }
52 >
53 > /** The subset of the global `fetch` used to POST envelopes; injectable so tests avoid live network calls. */
54 > export type FetchFn = typeof globalThis.fetch;
55 >
56 > /**
57 > * App Insights caps a single property value at ~8192 chars. Long values are split across
58 > * numbered keys (`key`, `key_02`, `key_03`, …) so the Copilot Telemetry Service reassembles
59 > * them, mirroring the Copilot extension's `multiplexProperties` so events look identical on the
60 > * wire and downstream.
61 > */
62 > const MAX_PROPERTY_LENGTH = 8192;
63 > const MAX_CONCATENATED_PROPERTIES = 50;
64 >
65 > export function multiplexProperties(properties: TelemetryProps): TelemetryProps {
66 const newProperties: TelemetryProps = { ...properties };
67 for (const key in properties) {
89 return newProperties;
90 }
92 > /**
93 > * The restricted telemetry surface the agent host exposes, mirroring the Copilot extension's
94 > * `ITelemetryService` restricted methods so agent-host code can emit the same GH/MSFT events.
95 > */
96 > export interface IAgentHostRestrictedTelemetry {
97 > /** GH standard (non-restricted) telemetry -> `copilot_v0_copilot_event`. */
98 > sendGHTelemetryEvent(eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void;
99 > /** GH enhanced/restricted telemetry (prompts, tools, etc.) -> `copilot_v0_restricted_copilot_event`. */
100 > sendEnhancedGHTelemetryEvent(eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void;
101 > /** GH enhanced telemetry attributed and routed using an immutable per-session context. */
102 > sendEnhancedGHTelemetryEventForContext(context: IAgentHostRestrictedTelemetryContext, eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void;
103 > /** MSFT-internal telemetry -> Aria/Collector++ (internal-only table). No-op without an internal key. */
104 > sendInternalMSFTTelemetryEvent(eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void;
105 > /** MSFT-internal telemetry attributed using an immutable per-session context. */
106 > sendInternalMSFTTelemetryEventForContext(context: IAgentHostInternalTelemetryContext, eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void;
107 > /** Sets the Copilot user tracking id (`copilot_trackingId`) carried on every subsequent event. */
108 > setCopilotTrackingId(trackingId: string | undefined): void;
109 > /** Overrides the POST endpoint with the user's CAPI `endpoints.telemetry`; falsy restores the default. */
110 > setRestrictedTelemetryEndpoint(endpointUrl: string | undefined): void;
111 > /** Enables enhanced GH telemetry once the token opts in (`rt=1`); off by default and on flip/logout. */
112 > setRestrictedTelemetryEnabled(enabled: boolean): void;
113 > /** Sets the internal-user identity and enables the internal sink only for staff accounts. */
114 > setInternalTelemetryContext(context: IAgentHostInternalTelemetryContext | undefined): void;
115 > }
116 >
117 > /**
118 > * Emits GitHub Copilot restricted/enhanced telemetry from the agent-host process by POSTing
119 > * Application-Insights envelopes to the Copilot telemetry endpoint (the same wire format the
120 > * Copilot extension uses). Fire-and-forget; failures are logged, never thrown.
121 > */
122 > export class AgentHostRestrictedTelemetrySender implements IAgentHostRestrictedTelemetry {
123 >
124 > private readonly _commonProps: TelemetryProps;
125 >
126 > /**
127 > * Whether the current Copilot token opts into enhanced/restricted telemetry (`rt=1`). Off by
128 > * default so the sole writer to the restricted table never emits for public users — a hard
129 > * safety boundary that holds even if the enclosing service's gate is bypassed. Mirrors the
130 > * Copilot extension, which only creates the restricted reporter for opted-in users.
131 > */
132 > private _restrictedTelemetryEnabled = false;
133 > private _internalTelemetryEnabled = false;
134 >
135 > constructor(
136 commonProperties: ICommonProperties,
137 private readonly _logService: ILogService,
149 };
150 }
152 > sendGHTelemetryEvent(eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void {
153 this._post(GH_STANDARD_IKEY, eventName, properties, measurements);
154 }
156 > sendEnhancedGHTelemetryEvent(eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void {
157 // Hard safety boundary: enhanced/restricted telemetry is the pipeline that may carry prompt
158 // and tool content, so the only writer to the restricted table refuses to emit unless the
164 this._post(GH_ENHANCED_IKEY, eventName, properties, measurements);
165 }
167 > sendEnhancedGHTelemetryEventForContext(context: IAgentHostRestrictedTelemetryContext, eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void {
168 if (!context.restrictedTelemetryEnabled) {
169 return;
174 });
175 }
177 > sendInternalMSFTTelemetryEvent(eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void {
178 if (!this._internalTelemetryEnabled) {
179 return;
185 this._logService.trace(`[ahp-restricted] internal MSFT event (not sent, no internal key): ${eventName}`);
186 }
188 > sendInternalMSFTTelemetryEventForContext(context: IAgentHostInternalTelemetryContext, eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void {
189 if (!context.isInternal) {
190 return;
196 this._logService.trace(`[ahp-restricted] internal MSFT event (not sent, no internal key): ${eventName}`);
197 }
199 > setCopilotTrackingId(trackingId: string | undefined): void {
200 // `copilot_trackingId` is the current account's Copilot token `tid` claim. Exact runtime
201 // targets use their immutable per-session context instead; this mutable value remains for
203 this._commonProps.copilot_trackingId = trackingId || undefined;
204 }
206 > setRestrictedTelemetryEndpoint(endpointUrl: string | undefined): void {
207 // The user's telemetry host comes from the CAPI `endpoints.telemetry` discovery; fall back
208 // to the dotcom default when it is unknown so events are never sent to an empty URL.
209 this._endpointUrl = endpointUrl || GH_TELEMETRY_URL;
210 }
212 > setRestrictedTelemetryEnabled(enabled: boolean): void {
213 this._restrictedTelemetryEnabled = enabled;
214 }
216 > setInternalTelemetryContext(context: IAgentHostInternalTelemetryContext | undefined): void {
217 this._internalTelemetryEnabled = context?.isInternal === true;
218 this._internalSink?.setContext(context);
219 }
221 > private _post(iKey: string, eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements, context?: { readonly endpointUrl: string | undefined; readonly trackingId: string | undefined }): void {
222 const name = eventName.includes('/') ? eventName : `${NAMESPACE}/${eventName}`;
223 const commonProps = context
270 });
271 }
273 >
274 function asString(value: string | boolean | undefined): string | undefined {
275 return typeof value === 'string' ? value : value === undefined ? undefined : String(value);
src/vs/platform/agentHost/test/node/historyRecordFixtures.ts 132 covered LOC · 6 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- historyRecordFixtures.ts
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 > import { URI } from '../../../../base/common/uri.js';
7 > import { generateUuid } from '../../../../base/common/uuid.js';
8 > import { isString } from '../../../../base/common/types.js';
9 > import { stripRedundantCdPrefix } from '../../common/commandLineHelpers.js';
10 > import { IFileEditRecord, ISessionDatabase } from '../../common/sessionDataService.js';
11 > import { MessageKind, ResponsePartKind, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, TurnState, buildSubagentSessionUri, type Message, type ResponsePart, type StringOrMarkdown, type ToolCallCompletedState, type ToolResultContent, type Turn } from '../../common/state/sessionState.js';
12 > import { getInvocationMessage, getPastTenseMessage, getShellLanguage, getSubagentMetadata, getToolDisplayName, getToolInputString, getToolKind, isEditTool, isHiddenTool, synthesizeSkillToolCall } from '../../node/copilot/copilotToolDisplay.js';
13 > import { buildSessionDbUri } from '../../node/shared/fileEditTracker.js';
14 > import type { ISessionEvent, ISessionEventMessage, ISessionEventSkillInvoked, ISessionEventSubagentStarted, ISessionEventToolComplete, ISessionEventToolStart } from './copilotTestEvents.js';
15 >
16 > // =============================================================================
17 > // History-record test fixtures
18 > //
19 > // Flat, declarative DSL used by mock agents and unit tests to build session
20 > // history without manually constructing `Turn[]`. Records mirror the wire
21 > // shape of an SDK event stream — `message`, `tool_start`, `tool_complete`,
22 > // `subagent_started` — so transcripts read like the protocol they're
23 > // emulating.
24 > //
25 > // Production code does NOT depend on this module. The real
26 > // SDK-events-to-Turn[] pipeline in `node/copilot/mapSessionEvents.ts` runs
27 > // in a single pass without producing the intermediate record shape.
28 > // =============================================================================
29 >
30 > interface IHistoryRecordBase {
31 > readonly session: URI;
32 > }
33 >
34 > interface IHistoryMessageRecord extends IHistoryRecordBase {
35 > readonly type: 'message';
36 > readonly role: 'user' | 'assistant';
37 > readonly messageId: string;
38 > readonly content: string;
39 > readonly toolRequests?: readonly {
40 > readonly toolCallId: string;
41 > readonly name: string;
42 > readonly arguments?: string;
43 > readonly type?: 'function' | 'custom';
44 > }[];
45 > readonly reasoningOpaque?: string;
46 > readonly reasoningText?: string;
47 > readonly encryptedContent?: string;
48 > readonly parentToolCallId?: string;
49 > }
50 >
51 > export interface IHistoryToolStartRecord extends IHistoryRecordBase {
52 > readonly type: 'tool_start';
53 > readonly toolCallId: string;
54 > readonly toolName: string;
55 > readonly displayName: string;
56 > readonly invocationMessage: StringOrMarkdown;
57 > readonly toolInput?: string;
58 > readonly toolKind?: 'terminal' | 'subagent' | 'search';
59 > readonly language?: string;
60 > readonly toolArguments?: string;
61 > readonly subagentAgentName?: string;
62 > readonly subagentDescription?: string;
63 > readonly mcpServerName?: string;
64 > readonly mcpToolName?: string;
65 > readonly parentToolCallId?: string;
66 > }
67 >
68 > interface IHistoryToolCompleteRecord extends IHistoryRecordBase {
69 > readonly type: 'tool_complete';
70 > readonly toolCallId: string;
71 > readonly result: {
72 > readonly success: boolean;
73 > readonly pastTenseMessage: StringOrMarkdown;
74 > readonly content?: ToolResultContent[];
75 > readonly error?: { readonly message: string; readonly code?: string };
76 > };
77 > readonly isUserRequested?: boolean;
78 > readonly toolTelemetry?: string;
79 > readonly parentToolCallId?: string;
80 > }
81 >
82 > interface IHistorySubagentStartedRecord extends IHistoryRecordBase {
83 > readonly type: 'subagent_started';
84 > readonly toolCallId: string;
85 > readonly agentName: string;
86 > readonly agentDisplayName: string;
87 > readonly agentDescription?: string;
88 > }
89 >
90 > /** Test fixture record. Hand-constructed by tests to seed mock session histories. */
91 > export type IHistoryRecord =
92 > | IHistoryMessageRecord
93 > | IHistoryToolStartRecord
94 > | IHistoryToolCompleteRecord
95 > | IHistorySubagentStartedRecord;
96 >
97 function extractSubagentMeta(start: IHistoryToolStartRecord | undefined): { subagentDescription?: string; subagentAgentName?: string } {
98 if (!start) {
104 };
105 }
107 > /**
108 > * Builds a parent session's {@link Turn}s from a flat list of history
109 > * records.
110 > *
111 > * Each `user` message starts a new turn. Inner subagent records (those
112 > * carrying `parentToolCallId`) are skipped — see
113 > * {@link buildSubagentTurnsFromHistory}.
114 > */
115 > export function buildTurnsFromHistory(messages: readonly IHistoryRecord[]): Turn[] {
116 const turns: Turn[] = [];
117 const subagentsByToolCallId = new Map<string, IHistorySubagentStartedRecord>();
231 return turns;
232 }
234 > /**
235 > * Builds the {@link Turn}s for a subagent child session by filtering the
236 > * parent's history for records carrying the matching `parentToolCallId`.
237 > * Returns a single turn containing all inner tool calls and assistant
238 > * messages.
239 > */
240 > export function buildSubagentTurnsFromHistory(
241 parentMessages: readonly IHistoryRecord[],
242 parentToolCallId: string,
345 }];
346 }
348 > // =============================================================================
349 > // SDK-events-to-history-records (test fixture loader)
350 > //
351 > // Translates raw Copilot SDK session events into a flat IHistoryRecord
352 > // stream. This is the test-side equivalent of the production single-pass
353 > // `mapSessionEvents` (which goes directly to Turn[]). It exists so JSONL
354 > // fixtures captured from real `~/.copilot/session-state/` files can be
355 > // loaded into the test DSL without forcing tests to also adopt Turn[].
356 > // =============================================================================
357 >
358 function tryStringify(value: unknown): string | undefined {
359 try {
363 }
364 }
366 function isSyntheticUserMessage(event: ISessionEvent): boolean {
367 if (event.type !== 'user.message') {
371 return !!source && source.toLowerCase() !== 'user';
372 }
374 > /**
375 > * Maps raw SDK session events into a flat list of {@link IHistoryRecord}s,
376 > * restoring stored file-edit metadata from the session database when
377 > * available. Test-fixture-only.
378 > */
379 export async function mapSessionEventsToHistoryRecords(
380 session: URI,
src/vs/platform/agentHost/node/agentPeerChats.ts 131 covered LOC · 25 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentPeerChats.ts
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 > import { Disposable, DisposableMap, IDisposable } from '../../../base/common/lifecycle.js';
7 > import { renderResponseMarkdown, truncateMiddle } from '../common/agentHostConversationContext.js';
8 > import { type ActiveTurn, type ModelSelection, type Turn } from '../common/state/protocol/state.js';
9 >
10 > const SIDE_CHAT_CONTEXT_START = '<side-chat-context>';
11 > const SIDE_CHAT_CONTEXT_END = '</side-chat-context>';
12 > const SIDE_CHAT_CONTEXT_LENGTH_PREFIX = 'length=';
13 > const SIDE_CHAT_GUIDANCE = 'This is a side conversation. Prefer explanation over action; do not make changes or carry out work unless the user explicitly asks.';
14 > export const MAX_SIDE_CHAT_CONTEXT_CHARS = 20_000;
15 >
16 > export interface IPersistedSideChat {
17 > readonly source: string;
18 > readonly turnId: string;
19 > readonly selection?: { readonly text: string; readonly responsePartId?: string };
20 > readonly providerAnchorTurnId?: string;
21 > readonly inheritedTurnCount: number;
22 > readonly partialResponse?: string;
23 > readonly context?: string;
24 > }
25 >
26 > export function buildSideChatSourceContext(turns: readonly Turn[], activeTurn?: ActiveTurn): string | undefined {
27 const blocks: string[] = [];
28 for (const turn of turns) {
44 return conversation.length > MAX_SIDE_CHAT_CONTEXT_CHARS ? truncateMiddle(conversation, MAX_SIDE_CHAT_CONTEXT_CHARS) : conversation;
45 }
47 > export function getSideChatPartialResponse(activeTurn: ActiveTurn | undefined): string | undefined {
48 if (!activeTurn) {
49 return undefined;
52 return responseMarkdown ? truncateMiddle(responseMarkdown, MAX_SIDE_CHAT_CONTEXT_CHARS) : undefined;
53 }
55 > export function buildBoundedSideChatSourceContext(turns: readonly Turn[], turnId: string, activeTurn?: ActiveTurn): string | undefined {
56 if (activeTurn?.id === turnId) {
57 return buildSideChatSourceContext(turns, activeTurn);
60 return turnIndex === -1 ? undefined : buildSideChatSourceContext(turns.slice(0, turnIndex + 1));
61 }
63 > export function injectSideChatContext(prompt: string, partialResponse?: string, sourceContext?: string, selectionText?: string): string {
64 const context = [SIDE_CHAT_GUIDANCE];
65 if (selectionText) {
91 return [SIDE_CHAT_CONTEXT_START, `${SIDE_CHAT_CONTEXT_LENGTH_PREFIX}${contextBody.length}`, contextBody, SIDE_CHAT_CONTEXT_END, '', prompt].join('\n');
92 }
94 > export function prepareSideChatPrompt(prompt: string, turns: readonly Turn[], sideChat: IPersistedSideChat | undefined): string {
95 if (!sideChat || turns.length > sideChat.inheritedTurnCount) {
96 return prompt;
107 return injectSideChatContext(prompt, partialResponse, sourceContext, sideChat.selection?.text);
108 }
110 function buildSideChatContextBlock(message: string, response: string | undefined): string | undefined {
111 const userText = message.trim();
118 : `User request:\n${userText}`;
119 }
121 > export function stripSideChatContext(turns: readonly Turn[], sideChat: IPersistedSideChat | undefined): readonly Turn[] {
122 if (!sideChat || turns.length === 0) {
123 return turns;
150 return [{ ...first, message: { ...first.message, text: userPrompt } }, ...turns.slice(1)];
151 }
153 > /**
154 > * In-memory backing for an additional (non-default) peer chat. Records the SDK
155 > * chat id that backs the chat so it can be re-resumed after a process restart,
156 > * along with any model override chosen at creation time. This is also the shape
157 > * serialized into the opaque, agent-owned `providerData` blob the orchestrator
158 > * persists in its chat catalog and hands back on restore.
159 > */
160 > export interface IPersistedChat {
161 > readonly sdkSessionId: string;
162 > readonly model?: ModelSelection;
163 > readonly sideChat?: IPersistedSideChat;
164 > }
165 >
166 > export interface IResolvedAgentChat<TSession extends IDisposable> {
167 > readonly chatSession: TSession;
168 > readonly isDefault: boolean;
169 > }
170 >
171 > /**
172 > * Serializes a peer-chat backing into the opaque `providerData` token the
173 > * orchestrator persists verbatim. The encoding is the agent's private business
174 > * — today it is the JSON of {@link IPersistedChat}.
175 > */
176 > export function encodeProviderData(backing: IPersistedChat): string {
177 return JSON.stringify(backing);
178 }
180 > /**
181 > * Decodes an opaque `providerData` token produced by {@link encodeProviderData}
182 > * back into a peer-chat backing, tolerating corrupt/foreign blobs by returning
183 > * `undefined` (the same drop-on-corrupt policy as the legacy chat catalog read).
184 > */
185 > export function decodeProviderData(providerData: string): IPersistedChat | undefined {
186 try {
187 const value = JSON.parse(providerData) as { sdkSessionId?: unknown; model?: unknown; sideChat?: unknown };
231 }
232 }
234 > /**
235 > * Per-session container shared by the multi-chat agents. Keeps ALL chats of a
236 > * session — the default (main) chat and any additional peer chats — together in
237 > * ONE per-agent map keyed by each chat's channel URI string (no parallel maps,
238 > * no default-vs-peer storage split). The default chat is just the entry marked
239 > * as default, so send/abort/model/agent/history operations resolve any chat by a
240 > * single uniform {@link getChat} lookup with no default-chat resolution branch.
241 > *
242 > * Each entry can act as a leaf (wrapping one {@link ownSession} plus its
243 > * event-forwarding disposables) or as the container (holding the chat map).
244 > * Disposing the container disposes every chat leaf it holds.
245 > */
246 > export class AgentSessionEntry<TSession extends IDisposable> extends Disposable {
247 > /** All chats of the session (default + peers) as leaf entries, keyed by chat-URI string. */
248 > private readonly _chats = this._register(new DisposableMap<string, AgentSessionEntry<TSession>>());
249 > /** The key of the session's default (main) chat within {@link _chats}. */
250 > private _defaultChatKey: string | undefined;
251 > /** This leaf's own chat session (set when the entry wraps a single chat). */
252 > private _ownSession: TSession | undefined;
253 >
254 > constructor(session?: TSession) {
255 super();
256 if (session) {
259 }
260 }
262 > /** This leaf's own chat session, or `undefined` for a bare container. */
263 > get ownSession(): TSession | undefined {
264 return this._ownSession;
265 }
267 > addDisposable(disposable: IDisposable): void {
268 this._register(disposable);
269 }
271 > // ---- Uniform chat map (default + peers) --------------------------------
272 >
273 > /** Register the session's default (main) chat leaf under its chat-URI key. */
274 > setDefaultChat(chatKey: string, entry: AgentSessionEntry<TSession>): void {
275 this._chats.set(chatKey, entry);
276 this._defaultChatKey = chatKey;
277 }
279 > /** Dispose the default chat leaf (e.g. a config-driven restart) while keeping peer chats. */
280 > clearDefaultChat(): void {
281 if (this._defaultChatKey !== undefined) {
282 this._chats.deleteAndDispose(this._defaultChatKey);
284 }
285 }
287 > /** The session's materialized default (main) chat, or `undefined` while provisional. */
288 > get defaultChat(): TSession | undefined {
289 return this._defaultChatKey !== undefined ? this._chats.get(this._defaultChatKey)?.ownSession : undefined;
290 }
292 > /** Uniform lookup: the chat's session (default OR peer) by its chat-URI key. */
293 > getChat(chatKey: string): TSession | undefined {
294 return this._chats.get(chatKey)?.ownSession;
295 }
297 > /** Uniform lookup with default-vs-peer identity from the entry that resolved the chat. */
298 > resolveChat(chatKey: string): IResolvedAgentChat<TSession> | undefined {
299 const chatSession = this._chats.get(chatKey)?.ownSession;
300 if (!chatSession) {
303 return { chatSession, isDefault: chatKey === this._defaultChatKey };
304 }
306 > /** Every live chat session — the default chat plus all peers. */
307 > allChatSessions(): TSession[] {
308 const sessions: TSession[] = [];
309 for (const entry of this._chats.values()) {
314 return sessions;
315 }
317 > // ---- Peer chats (every chat except the default) ------------------------
318 >
319 > getPeerChat(chatKey: string): TSession | undefined {
320 return chatKey === this._defaultChatKey ? undefined : this._chats.get(chatKey)?.ownSession;
321 }
323 > hasPeerChat(chatKey: string): boolean {
324 return chatKey !== this._defaultChatKey && this._chats.has(chatKey);
325 }
327 > registerPeerChat(chatKey: string, entry: AgentSessionEntry<TSession>): void {
328 this._chats.set(chatKey, entry);
329 }
331 > disposePeerChat(chatKey: string): void {
332 if (chatKey !== this._defaultChatKey) {
333 this._chats.deleteAndDispose(chatKey);
334 }
335 }
337 > peerChatKeys(): string[] {
338 return [...this._chats.keys()].filter(key => key !== this._defaultChatKey);
339 }
341 > peerChatSessions(): TSession[] {
342 const sessions: TSession[] = [];
343 for (const key of this._chats.keys()) {
src/vs/platform/agentHost/common/agentHostChangesetOperationService.ts 130 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostChangesetOperationService.ts
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 > import { CancellationToken } from '../../../base/common/cancellation.js';
7 > import type { IDisposable } from '../../../base/common/lifecycle.js';
8 > import { createDecorator } from '../../instantiation/common/instantiation.js';
9 > import type { ChangesetKind } from './changesetUri.js';
10 > import type { InvokeChangesetOperationParams, InvokeChangesetOperationResult } from './state/protocol/channels-changeset/commands.js';
11 > import type { ChangesetOperation, ISessionGitHubState, ISessionGitState, URI } from './state/sessionState.js';
12 >
13 > export const IAgentHostChangesetOperationService = createDecorator<IAgentHostChangesetOperationService>('agentHostChangesetOperationService');
14 >
15 > /**
16 > * Server-side handler for a changeset operation advertised via
17 > * `changeset/operationsChanged`.
18 > *
19 > * The agent service validates the request shape (changeset exists, operation id
20 > * known, target scope matches) before invoking the handler; the handler is only
21 > * responsible for executing the operation.
22 > */
23 > export interface IChangesetOperationHandler {
24 > /**
25 > * Executes a previously advertised changeset operation.
26 > *
27 > * The handler receives the original protocol params so it can inspect the
28 > * changeset channel and optional target. Validation that the operation exists
29 > * on the changeset and supports the requested target scope happens before this
30 > * method is called.
31 > */
32 > invoke(params: InvokeChangesetOperationParams, token: CancellationToken): Promise<InvokeChangesetOperationResult>;
33 > }
34 >
35 > /**
36 > * Context used by changeset operation contributions to decide which operations
37 > * to advertise for a session changeset.
38 > *
39 > * Keep this interface intentionally small. Add new fields here only when a
40 > * contribution genuinely needs them to compute operation availability. Likely
41 > * future additions include the concrete changeset URI, the session state, the
42 > * changeset state, or the working directory URI.
43 > */
44 > export interface IChangesetOperationContext {
45 > /** String form of the session URI that owns the changeset. */
46 > readonly sessionKey: string;
47 > /** Expanded changeset URI whose operations are being computed. */
48 > readonly changesetUri: URI;
49 > /** Well-known changeset kind for {@link changesetUri}. */
50 > readonly changesetKind: ChangesetKind;
51 > /** Current git metadata for the session used to compute operation availability. */
52 > readonly gitState?: ISessionGitState;
53 > /** Current GitHub metadata for the session used to compute operation availability. */
54 > readonly gitHubState?: ISessionGitHubState;
55 > }
56 >
57 > /**
58 > * Registration surface handed to changeset operation contributions.
59 > *
60 > * Contributions use this object to install operation handlers and request a
61 > * refresh when external state changes which operations should be advertised.
62 > */
63 > export interface IChangesetOperationRegistry {
64 > /**
65 > * Registers the server-side handler for one {@link ChangesetOperation.id}.
66 > * The returned disposable removes only this registration.
67 > */
68 > registerChangesetOperationHandler(operationId: string, handler: IChangesetOperationHandler): IDisposable;
69 > /**
70 > * Notifies the contribution service that advertised operations for all static
71 > * changesets in `sessionKey` should be recomputed from current session state.
72 > */
73 > onDidChangeOperations(sessionKey: string): void;
74 > /**
75 > * Recomputes the session's git metadata and then refreshes advertised
76 > * operations if that metadata can be resolved.
77 > */
78 > refreshSessionGitState(sessionKey: string): Promise<void>;
79 > }
80 >
81 > /**
82 > * Provider of changeset operations for one feature area.
83 > *
84 > * A contribution owns the decision about which operations are available for a
85 > * changeset and registers the handlers that execute those operations.
86 > */
87 > export interface IChangesetOperationContribution extends IDisposable {
88 > /**
89 > * Registers every operation handler owned by this contribution. Called once
90 > * when the contribution is added to the service.
91 > */
92 > registerHandlers(registry: IChangesetOperationRegistry): IDisposable;
93 > /**
94 > * Returns operations that should be advertised for the given changeset, or
95 > * `undefined` when this contribution has nothing to offer in the context.
96 > */
97 > getOperations(context: IChangesetOperationContext): readonly ChangesetOperation[] | undefined;
98 > }
99 >
100 > /**
101 > * Coordinates changeset operation contributions, advertised operation state,
102 > * and client-triggered invocation.
103 > */
104 > export interface IAgentHostChangesetOperationService extends IDisposable {
105 > readonly _serviceBrand: undefined;
106 >
107 > /**
108 > * Adds a contribution and registers its handlers. Disposing the returned value
109 > * unregisters the handlers and disposes the contribution.
110 > */
111 > registerContribution(contribution: IChangesetOperationContribution): IDisposable;
112 > /**
113 > * Recomputes and publishes operations for the changesets for a given
114 > * session. If `gitState` is not provided, the current git state will
115 > * be used.
116 > */
117 > updateOperations(sessionKey: string, changeset?: string, gitState?: ISessionGitState, gitHubState?: ISessionGitHubState): void;
118 >
119 > /**
120 > * Returns the operations that should be advertised for the given changeset, or
121 > * `undefined` when no operations are available.
122 > */
123 > getOperations(sessionKey: string, changeset?: string, gitState?: ISessionGitState, gitHubState?: ISessionGitHubState): readonly ChangesetOperation[] | undefined;
124 >
125 > /**
126 > * Invokes an advertised operation after validating the changeset, operation id,
127 > * and requested target scope.
128 > */
129 > invokeChangesetOperation(params: InvokeChangesetOperationParams): Promise<InvokeChangesetOperationResult>;
130 > }
src/vs/platform/agentHost/common/copilotCliConfig.ts 130 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- copilotCliConfig.ts
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 > import { localize } from '../../../nls.js';
7 > import { createSchema, schemaProperty } from './agentHostSchema.js';
8 > import type { ModelSelection } from './state/protocol/state.js';
9 >
10 > /**
11 > * Root-config keys consumed exclusively by the Copilot CLI provider
12 > * (`CopilotSessionLauncher` / `CopilotAgent`) — kept out of the
13 > * provider-agnostic `agentHostCustomizationConfigSchema`.
14 > */
15 > export const enum CopilotCliConfigKey {
16 > /** Use Agent Host's custom terminal tool instead of the SDK's default. Off by default. */
17 > EnableCustomTerminalTool = 'enableCustomTerminalTool',
18 > /** Log level passed to the Copilot SDK client. */
19 > CopilotSdkLogLevel = 'copilotSdkLogLevel',
20 > /** Enable the rubber duck critic subagent. */
21 > RubberDuck = 'rubberDuck',
22 > /** Apply Opus 4.8-tuned system-prompt overrides on Opus 4.8 models. Off by default. */
23 > Opus48Prompt = 'opus48Prompt',
24 > /** Enable runtime tool search (deferred-tool loading) for Copilot SDK sessions. Off by default. */
25 > ToolSearchEnabled = 'toolSearchEnabled',
26 > /** Override reasoning effort regardless of the picker value; unsupported values are ignored. */
27 > ReasoningEffortOverride = 'reasoningEffortOverride',
28 > /** Per-model capability overrides (family aliases) keyed by model id. */
29 > ModelCapabilityOverrides = 'modelCapabilityOverrides',
30 > }
31 >
32 > // VS Code `chat.agentHost.*` setting IDs that feed the root-config keys above,
33 > // kept beside the keys they forward to. Registered in `chat.shared.contribution.ts`
34 > // and forwarded into the host's root config by `AgentHostCopilotCliSettingsContribution`
35 > // (and, for the terminal-tool toggle, `AgentHostTerminalContribution`).
36 >
37 > export const AgentHostCustomTerminalToolEnabledSettingId = 'chat.agentHost.customTerminalTool.enabled';
38 >
39 > export const AgentHostCopilotSdkLogLevelSettingId = 'chat.agentHost.copilotSdk.logLevel';
40 >
41 > export const AgentHostOpus48PromptEnabledSettingId = 'chat.agentHost.opus48Prompt.enabled';
42 >
43 > export const AgentHostToolSearchEnabledSettingId = 'chat.agentHost.copilot.toolSearch.enabled';
44 >
45 > export const AgentHostReasoningEffortOverrideSettingId = 'chat.agentHost.reasoningEffortOverride';
46 >
47 > export const AgentHostModelCapabilityOverridesSettingId = 'chat.agentHost.modelCapabilityOverrides';
48 >
49 > export const copilotSdkLogLevelSettingValues = ['info', 'trace'] as const;
50 > export type CopilotSdkLogLevelSetting = typeof copilotSdkLogLevelSettingValues[number];
51 >
52 > /** Per-model capability override; the agent-host equivalent of the extension's `IModelCapabilityOverride`. */
53 > interface ICopilotCliModelCapabilityOverride {
54 > /** Alias the model's family for prompt/capability routing (e.g. `"claude-opus-4-8"`). */
55 > readonly family?: string;
56 > }
57 >
58 > /** Map of model id → capability override. */
59 > export type CopilotCliModelCapabilityOverrides = Record<string, ICopilotCliModelCapabilityOverride>;
60 >
61 > export const copilotCliConfigSchema = createSchema({
62 > [CopilotCliConfigKey.EnableCustomTerminalTool]: schemaProperty<boolean>({
63 > type: 'boolean',
64 > title: localize('agentHost.config.enableCustomTerminalTool.title', "Use Agent Host Terminal Tool"),
65 > description: localize('agentHost.config.enableCustomTerminalTool.description', "When enabled, Copilot SDK sessions use Agent Host's terminal tool override instead of the SDK's default terminal behavior."),
66 > default: false,
67 > }),
68 > [CopilotCliConfigKey.CopilotSdkLogLevel]: schemaProperty<CopilotSdkLogLevelSetting>({
69 > type: 'string',
70 > title: localize('agentHost.config.copilotSdkLogLevel.title', "Copilot SDK Log Level"),
71 > description: localize('agentHost.config.copilotSdkLogLevel.description', "Controls logging from the Copilot SDK runtime. Agent host trace logging always enables trace output."),
72 > enum: [...copilotSdkLogLevelSettingValues],
73 > enumLabels: [
74 > localize('agentHost.config.copilotSdkLogLevel.info', "Info"),
75 > localize('agentHost.config.copilotSdkLogLevel.trace', "Trace"),
76 > ],
77 > default: 'info',
78 > }),
79 > [CopilotCliConfigKey.RubberDuck]: schemaProperty<boolean>({
80 > type: 'boolean',
81 > title: localize('agentHost.config.rubberDuck.title', "Rubber Duck Agent"),
82 > description: localize('agentHost.config.rubberDuck.description', "When enabled, the coding agent uses a rubber duck critic subagent to review code changes using a complementary model."),
83 > default: false,
84 > }),
85 > [CopilotCliConfigKey.Opus48Prompt]: schemaProperty<boolean>({
86 > type: 'boolean',
87 > title: localize('agentHost.config.opus48Prompt.title', "Opus 4.8 Agent Prompt"),
88 > description: localize('agentHost.config.opus48Prompt.description', "When enabled, Copilot SDK sessions running a Claude Opus 4.8 model apply Opus 4.8-tuned system-prompt section overrides on top of the default system message."),
89 > default: false,
90 > }),
91 > [CopilotCliConfigKey.ToolSearchEnabled]: schemaProperty<boolean>({
92 > type: 'boolean',
93 > title: localize('agentHost.config.toolSearchEnabled.title', "Agent Host Tool Search"),
94 > description: localize('agentHost.config.toolSearchEnabled.description', "When enabled, Copilot SDK sessions defer MCP and non-core VS Code tools behind a tool-search tool so the model discovers them on demand instead of loading every tool definition up front."),
95 > default: false,
96 > }),
97 > [CopilotCliConfigKey.ReasoningEffortOverride]: schemaProperty<string>({
98 > type: 'string',
99 > title: localize('agentHost.config.reasoningEffortOverride.title', "Reasoning Effort Override"),
100 > description: localize('agentHost.config.reasoningEffortOverride.description', "Overrides the reasoning effort for Copilot SDK sessions regardless of the per-model picker value. Set it to a level the selected model supports (e.g. `low`, `medium`, `high`, `xhigh`); a value that isn't a recognized effort level is ignored and the session falls back to the picker value. Only affects Copilot SDK sessions; intended for experimentation."),
101 > default: '',
102 > }),
103 > [CopilotCliConfigKey.ModelCapabilityOverrides]: schemaProperty<CopilotCliModelCapabilityOverrides>({
104 > type: 'object',
105 > title: localize('agentHost.config.modelCapabilityOverrides.title', "Model Capability Overrides"),
106 > description: localize('agentHost.config.modelCapabilityOverrides.description', "Per-model capability overrides for Copilot SDK sessions, keyed by model id. Aliasing a model id to a known `family` routes it to that family's tuned system prompt without changing the model id sent to the runtime. Only affects Copilot SDK sessions; intended for experimentation."),
107 > additionalProperties: {
108 > type: 'object',
109 > title: localize('agentHost.config.modelCapabilityOverrides.entry.title', "Capability Override"),
110 > description: localize('agentHost.config.modelCapabilityOverrides.entry.description', "A single capability override. The property key is the model id."),
111 > properties: {
112 > family: {
113 > type: 'string',
114 > title: localize('agentHost.config.modelCapabilityOverrides.family.title', "Family"),
115 > description: localize('agentHost.config.modelCapabilityOverrides.family.description', "Alias the model's family for prompt/capability routing (e.g. `claude-opus-4-8`)."),
116 > },
117 > },
118 > },
119 > default: {},
120 > }),
121 > });
122 >
123 > /** Returns the configured family alias for `modelId`, or `undefined`. Malformed entries are treated as unset. */
124 function getModelFamilyAlias(overrides: CopilotCliModelCapabilityOverrides | undefined, modelId: string): string | undefined {
125 const family = overrides?.[modelId]?.family;
126 return typeof family === 'string' && family.length > 0 ? family : undefined;
127 }
129 > /**
130 > * Substitutes a configured family alias for the model id so an aliased preview model
131 > * routes to a known family's prompt contributor. `model.config` picker values are
132 > * preserved; returns the input unchanged when no alias applies.
133 > */
134 > export function applyModelFamilyAlias(model: ModelSelection | undefined, overrides: CopilotCliModelCapabilityOverrides | undefined): ModelSelection | undefined {
135 if (!model) {
136 return undefined;
src/vs/platform/agentHost/node/agentHostChangesetFileMonitorCoordinator.ts 130 covered LOC · 29 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostChangesetFileMonitorCoordinator.ts
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 > import { SequencerByKey } from '../../../base/common/async.js';
7 > import { Disposable, DisposableMap, IReference, ReferenceCollection } from '../../../base/common/lifecycle.js';
8 > import { URI } from '../../../base/common/uri.js';
9 > import { buildBranchChangesetUri, buildSessionChangesetUri, buildUncommittedChangesetUri } from '../common/changesetUri.js';
10 > import { parseSubagentSessionUri } from '../common/state/sessionState.js';
11 > import { IAgentConfigurationService } from './agentConfigurationService.js';
12 > import { DEFAULT_AGENT_HOST_WATCH_EXCLUDES, IAgentHostFileMonitorService } from './agentHostFileMonitorService.js';
13 > import { IAgentHostGitService } from '../common/agentHostGitService.js';
14 > import { AgentHostStateManager, IAgentHostStateManager } from './agentHostStateManager.js';
15 > import { ILogService } from '../../log/common/log.js';
16 > import { IAgentHostGitStateService } from '../common/agentHostGitStateService.js';
17 >
18 > class WatchInterestReferenceCollection extends ReferenceCollection<string> {
19 > constructor(
20 > private readonly _create: (sessionStr: string) => void, agentHostChangesetFileMonitorCoordinator.ts
21 > private readonly _destroy: (sessionStr: string) => void,
22 > ) {
23 > super();
24 > }
26 > protected createReferencedObject(sessionStr: string): string {
27 this._create(sessionStr);
28 return sessionStr;
29 }
31 > protected destroyReferencedObject(sessionStr: string): void {
32 this._destroy(sessionStr);
33 }
35 >
36 > /**
37 > * Keeps static changeset catalogue entries fresh while a client is observing a
38 > * session or one of its static changeset resources.
39 > *
40 > * The generic {@link IAgentHostFileMonitorService} owns folder watching and
41 > * debounce mechanics; this coordinator owns the changeset-specific lifecycle:
42 > * subscription interest, session materialization, repository-root resolution,
43 > * root-level watcher sharing, and refresh fanout.
44 > *
45 > * We only monitor roots while at least one client is subscribed to a session or
46 > * static changeset that needs fresh changeset counts. We do not monitor while a
47 > * session on that root is actively running a turn: agent/tool edits made during
48 > * the turn are captured by the turn lifecycle, and the static changesets are
49 > * recomputed once when the turn completes. Watching during the turn would add
50 > * duplicate file-system noise without improving correctness.
51 > */
52 > export class ChangesetFileMonitorCoordinator extends Disposable {
53 >
54 > /** Per-subscription references into the per-session watch-interest collection. */
55 > private readonly _watchInterestReferences = this._register(new DisposableMap<string, IReference<string>>());
56 > private readonly _watchInterestCollection = new WatchInterestReferenceCollection(
57 > sessionStr => this._attachWatcherIfPossible(sessionStr),
58 > sessionStr => this._destroyWatchInterest(sessionStr),
59 > );
60 > /** Sessions waiting for materialization before a root watcher can attach. */
61 > private readonly _pendingWatchInterest = new Set<string>();
62 > /** Session URI string to the working directory that produced the current root attachment. */
63 > private readonly _sessionWorkingDirectory = new Map<string, string>();
64 > /** Session URI string to repository-root URI string. */
65 > private readonly _sessionRoot = new Map<string, string>();
66 > /** Repository-root URI string to sessions currently fanned out from that root. */
67 > private readonly _rootSessions = new Map<string, Set<string>>();
68 > /** Repository-root URI string to the shared monitor acquisition. */
69 > private readonly _rootWatchAcquisitions = this._register(new DisposableMap<string>());
70 > /** Repository-root URI string to the canonical repository root URI. */
71 > private readonly _rootUris = new Map<string, URI>();
72 > /** Active session URI string to repository-root URI string. */
73 > private readonly _activeSessionRoots = new Map<string, string>();
74 > /** Repository-root URI string to sessions currently active against that root. */
75 > private readonly _rootActiveSessions = new Map<string, Set<string>>();
76 > /** Active sessions whose repository root cannot yet be resolved. */
77 > private readonly _unresolvedActiveSessions = new Set<string>();
78 > private readonly _watchAttachmentSequencer = new SequencerByKey<string>();
79 > private readonly _activeTurnSequencer = new SequencerByKey<string>();
80 >
81 > constructor(
82 > @IAgentHostStateManager private readonly _stateManager: AgentHostStateManager, agentHostChangesetFileMonitorCoordinator.ts
83 > @IAgentConfigurationService private readonly _configurationService: IAgentConfigurationService,
84 > @IAgentHostFileMonitorService private readonly _fileMonitorService: IAgentHostFileMonitorService,
85 > @IAgentHostGitService private readonly _gitService: IAgentHostGitService,
86 > @IAgentHostGitStateService private readonly _gitStateService: IAgentHostGitStateService,
87 > @ILogService private readonly _logService: ILogService,
88 > ) {
89 > super();
90 > }
92 > trackSessionChanges(subscriptionKey: string, sessionStr: string): void {
93 if (!this._watchInterestReferences.has(subscriptionKey)) {
94 this._watchInterestReferences.set(subscriptionKey, this._watchInterestCollection.acquire(sessionStr));
95 }
96 }
98 > untrackSessionChanges(subscriptionKey: string): void {
99 this._watchInterestReferences.deleteAndDispose(subscriptionKey);
100 }
102 > onSessionRestored(sessionStr: string): void {
103 this._retryWatchAttachment(sessionStr);
104 }
106 > onSessionMaterialized(sessionStr: string): void {
107 this._retryWatchAttachment(sessionStr);
108 }
110 > onSessionDisposed(sessionStr: string): void {
111 this.untrackSessionChanges(buildUncommittedChangesetUri(sessionStr));
112 this.untrackSessionChanges(buildSessionChangesetUri(sessionStr));
115 this._destroyWatchInterest(sessionStr);
116 }
118 > onSessionTurnActiveChanged(sessionStr: string, active: boolean): void {
119 this._activeTurnSequencer.queue(sessionStr, async () => {
120 if (active) {
125 });
126 }
128 > private _destroyWatchInterest(sessionStr: string): void {
129 this._pendingWatchInterest.delete(sessionStr);
130 this._releaseSessionRoot(sessionStr);
131 }
133 > private _retryWatchAttachment(sessionStr: string): void {
134 if (this._shouldAttachSession(sessionStr) || this._pendingWatchInterest.has(sessionStr)) {
135 this._attachWatcherIfPossible(sessionStr);
136 }
137 }
139 > private _hasWatchInterest(sessionStr: string): boolean {
140 return this._watchInterestReferences.has(sessionStr)
141 || this._watchInterestReferences.has(buildBranchChangesetUri(sessionStr))
143 || this._watchInterestReferences.has(buildSessionChangesetUri(sessionStr));
144 }
146 > private _attachWatcherIfPossible(sessionStr: string): void {
147 this._watchAttachmentSequencer.queue(sessionStr, async () => {
148 if (!this._shouldAttachSession(sessionStr)) {
181 });
182 }
184 > private _attachSessionToRoot(sessionStr: string, repositoryRoot: URI, workingDirectory: string): void {
185 const rootStr = repositoryRoot.toString();
186 if (this._sessionRoot.get(sessionStr) === rootStr) {
201 this._ensureRootWatcher(rootStr, repositoryRoot);
202 }
204 > private _releaseSessionRoot(sessionStr: string): void {
205 const rootStr = this._sessionRoot.get(sessionStr);
206 if (!rootStr) {
250 }
251 }
253 > private _shouldAttachSession(sessionStr: string): boolean {
254 return this._hasWatchInterest(sessionStr)
255 && !this._activeSessionRoots.has(sessionStr)
256 && !this._unresolvedActiveSessions.has(sessionStr);
257 }
259 > private _isRootActive(rootStr: string): boolean {
260 return (this._rootActiveSessions.get(rootStr)?.size ?? 0) > 0;
261 }
263 > private _ensureRootWatcher(rootStr: string, repositoryRoot: URI): void {
264 if (this._isRootActive(rootStr) || this._rootWatchAcquisitions.has(rootStr)) {
265 return;
281 this._rootWatchAcquisitions.set(rootStr, rootWatchAcquisition);
282 }
284 > private _suspendRootWatcher(rootStr: string): void {
285 this._rootWatchAcquisitions.deleteAndDispose(rootStr);
286 }
288 > private async _markSessionActive(sessionStr: string): Promise<void> {
289 this._removeActiveSession(sessionStr);
290 this._pendingWatchInterest.delete(sessionStr);
322 }
323 }
325 > private _removeActiveSession(sessionStr: string): string | undefined {
326 this._unresolvedActiveSessions.delete(sessionStr);
327 const rootStr = this._activeSessionRoots.get(sessionStr);
339 return rootStr;
340 }
342 > private async _resolveActivityRepositoryRoot(sessionStr: string): Promise<URI | undefined> {
343 const workingDirectory = this._getActivityWorkingDirectory(sessionStr);
344 if (!workingDirectory) {
354 return this._gitService.getRepositoryRoot(workingDirectoryUri);
355 }
357 > private _getActivityWorkingDirectory(sessionStr: string): string | undefined {
358 const workingDirectory = this._configurationService.getEffectiveWorkingDirectory(sessionStr);
359 if (workingDirectory) {
src/vs/platform/agentHost/node/shared/agentServerToolHost.ts 129 covered LOC · 7 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentServerToolHost.ts
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 > import type { IAgentServerToolHost } from '../../common/agentServerTools.js';
7 > import { ActionType } from '../../common/state/protocol/common/actions.js';
8 > import type { StringOrMarkdown, ToolDefinition, URI } from '../../common/state/sessionState.js';
9 > import type { AgentHostStateManager } from '../agentHostStateManager.js';
10 >
11 > /**
12 > * Result of a server tool, passed to {@link IServerToolGroup.getDisplay} so the
13 > * owning group can tailor its past-tense message to what the tool returned
14 > * (for example a count parsed from the textual result). Absent while the tool
15 > * is still running.
16 > */
17 > export interface IServerToolDisplayResult {
18 > /** The textual tool result (the string the group's `execute` returned). */
19 > readonly text?: string;
20 > /** Whether the tool completed successfully. */
21 > readonly success: boolean;
22 > }
23 >
24 > /**
25 > * Display strings for a server tool, authored by the group that owns the tool
26 > * so every provider renders it identically (instead of each provider's display
27 > * layer re-deriving the strings from the tool name). Each field is optional: a
28 > * provider uses the returned value where present and falls back to its own
29 > * generic display otherwise.
30 > */
31 > export interface IServerToolDisplay {
32 > /** Human-readable tool name (e.g. "List Comments"). */
33 > readonly displayName?: string;
34 > /** Present-tense message shown while the tool runs (e.g. "Checking comments"). */
35 > readonly invocationMessage?: StringOrMarkdown;
36 > /** Past-tense message shown once the tool completes (e.g. "Checked 3 comments"). */
37 > readonly pastTenseMessage?: StringOrMarkdown;
38 > }
39 >
40 > /**
41 > * A group of related server tools owned and executed by the agent host. Each
42 > * group bundles the {@link ToolDefinition}s it advertises with an executor
43 > * that runs one of its tools by name against the session's state.
44 > *
45 > * Groups are the unit of extension and are **contributed from outside** — they
46 > * are passed to {@link AgentServerToolHost} at construction (startup), so this
47 > * module stays provider- and feature-agnostic (it knows nothing about
48 > * feedback, annotations, etc.). The feedback group, for example, lives in
49 > * `agentFeedbackServerTools.ts` and is wired in by the agent host. Everything
50 > * downstream — advertising, the Claude in-process MCP server and allow-list,
51 > * and the Copilot SDK tools and auto-approval — derives from the host's
52 > * contributed groups, so no provider code changes are needed to add a group.
53 > */
54 > export interface IServerToolGroup {
55 > /** Tool definitions this group advertises on the session's `serverTools`. */
56 > readonly definitions: readonly ToolDefinition[];
57 > /**
58 > * Whether {@link toolName} (one of this group's {@link definitions}) must be
59 > * confirmed by the user before it runs. Providers exclude such tools from
60 > * their server-tool auto-approve lists so the call surfaces a confirmation.
61 > * Absent or `false` means the tool is auto-approved like every other server
62 > * tool.
63 > */
64 > requiresConfirmation?(toolName: string): boolean;
65 > /**
66 > * Executes {@link toolName} (one of this group's {@link definitions})
67 > * against the session's state, dispatching any resulting actions through
68 > * the state manager (the single writer), and returns the textual tool
69 > * result.
70 > *
71 > * @throws if {@link toolName} is not owned by this group or the arguments
72 > * are invalid.
73 > */
74 > execute(stateManager: AgentHostStateManager, sessionUri: URI, toolName: string, rawArgs: unknown): string | Promise<string>;
75 >
76 > /**
77 > * Display strings for {@link toolName} (one of this group's
78 > * {@link definitions}), authored here so every provider renders this tool
79 > * identically rather than re-deriving the strings from the tool name. The
80 > * caller passes the parsed tool arguments and, once the tool has completed,
81 > * its {@link IServerToolDisplayResult result}. Returns `undefined` (or
82 > * individually-absent fields) to let the provider fall back to its generic
83 > * display. Optional: a group without bespoke display omits this.
84 > *
85 > * `toolName` is the bare tool name (the provider strips any transport
86 > * prefix such as Claude's `mcp__<server>__` before calling).
87 > */
88 > getDisplay?(toolName: string, args: unknown, result?: IServerToolDisplayResult): IServerToolDisplay | undefined;
89 > }
90 >
91 > /**
92 > * Bridges the agent host's server tools to the authoritative state tree.
93 > * Agents execute a server tool by name; the host routes it to the owning
94 > * {@link IServerToolGroup}, which reads the relevant session state, applies the
95 > * tool, dispatches any resulting actions through the state manager (the single
96 > * writer), and returns the textual tool result to the agent.
97 > *
98 > * The groups are contributed at construction; the host itself is generic and
99 > * has no knowledge of any specific tool group. It also advertises every server
100 > * tool on a session's {@link SessionState.serverTools} so clients see them as
101 > * server-provided.
102 > */
103 > export class AgentServerToolHost implements IAgentServerToolHost {
104 >
105 > private readonly _groupByToolName = new Map<string, IServerToolGroup>();
106 >
107 > readonly definitions: readonly ToolDefinition[];
108 > readonly toolNames: readonly string[];
109 >
110 > constructor(
111 > private readonly _stateManager: AgentHostStateManager, agentServerToolHost.ts
112 > groups: readonly IServerToolGroup[],
113 > ) {
114 > for (const group of groups) {
115 > for (const def of group.definitions) {
116 > if (this._groupByToolName.has(def.name)) {
117 throw new Error(`Duplicate server tool registered: ${def.name}`);
118 }
119 > this._groupByToolName.set(def.name, group); agentServerToolHost.ts
120 > }
121 > }
122 > this.definitions = groups.flatMap(group => group.definitions);
123 > this.toolNames = this.definitions.map(def => def.name);
124 > }
126 > advertise(sessionUri: URI): void {
127 this._stateManager.dispatchServerAction(sessionUri, {
128 type: ActionType.SessionServerToolsChanged,
130 });
131 }
133 > requiresConfirmation(toolName: string): boolean {
134 return this._groupByToolName.get(toolName)?.requiresConfirmation?.(toolName) ?? false;
135 }
137 > executeTool(sessionUri: URI, toolName: string, rawArgs: unknown): string | Promise<string> {
138 const group = this._groupByToolName.get(toolName);
139 if (!group) {
142 return group.execute(this._stateManager, sessionUri, toolName, rawArgs);
143 }
src/vs/base/common/observableInternal/observables/baseObservable.ts 128 covered LOC · 24 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- baseObservable.ts
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 > import { IObservableWithChange, IObserver, IReader, IObservable } from '../base.js';
7 > import { DisposableStore } from '../commonFacade/deps.js';
8 > import { DebugLocation } from '../debugLocation.js';
9 > import { DebugOwner, getFunctionName } from '../debugName.js';
10 > import { debugGetObservableGraph } from '../logging/debugGetDependencyGraph.js';
11 > import { getLogger, logObservable } from '../logging/logging.js';
12 > import type { keepObserved, recomputeInitiallyAndOnChange } from '../utils/utils.js';
13 > import { derivedOpts } from './derived.js';
14 >
15 > let _derived: typeof derivedOpts;
16 > /**
17 > * @internal
18 > * This is to allow splitting files.
19 > */
20 > export function _setDerivedOpts(derived: typeof _derived) {
21 > _derived = derived;
22 > }
23 >
24 > let _recomputeInitiallyAndOnChange: typeof recomputeInitiallyAndOnChange;
25 > export function _setRecomputeInitiallyAndOnChange(recomputeInitiallyAndOnChange: typeof _recomputeInitiallyAndOnChange) {
26 > _recomputeInitiallyAndOnChange = recomputeInitiallyAndOnChange;
27 > }
28 >
29 > let _keepObserved: typeof keepObserved;
30 > export function _setKeepObserved(keepObserved: typeof _keepObserved) {
31 > _keepObserved = keepObserved;
32 > }
33 >
34 > let _debugGetObservableGraph: typeof debugGetObservableGraph;
35 > export function _setDebugGetObservableGraph(debugGetObservableGraph: typeof _debugGetObservableGraph) {
36 > _debugGetObservableGraph = debugGetObservableGraph;
37 > }
38 >
39 > export abstract class ConvenientObservable<T, TChange> implements IObservableWithChange<T, TChange> {
40 > get TChange(): TChange { return null!; }
41 >
42 > public abstract get(): T;
43 >
44 > public reportChanges(): void {
45 this.get();
46 }
48 > public abstract addObserver(observer: IObserver): void;
49 > public abstract removeObserver(observer: IObserver): void;
50 >
51 > /** @sealed */
52 > public read(reader: IReader | undefined): T {
53 > if (reader) { baseObservable.ts
54 > return reader.readObservable(this); baseObservable.ts
55 > } else { baseObservable.ts
56 return this.get();
57 }
60 > /** @sealed */
61 > public map<TNew>(fn: (value: T, reader: IReader) => TNew): IObservable<TNew>;
62 > public map<TNew>(owner: DebugOwner, fn: (value: T, reader: IReader) => TNew): IObservable<TNew>;
63 > public map<TNew>(fnOrOwner: DebugOwner | ((value: T, reader: IReader) => TNew), fnOrUndefined?: (value: T, reader: IReader) => TNew, debugLocation: DebugLocation = DebugLocation.ofCaller()): IObservable<TNew> {
64 const owner = fnOrUndefined === undefined ? undefined : fnOrOwner as DebugOwner;
65 const fn = fnOrUndefined === undefined ? fnOrOwner as (value: T, reader: IReader) => TNew : fnOrUndefined;
91 );
92 }
94 > public abstract log(): IObservableWithChange<T, TChange>;
95 >
96 > /**
97 > * @sealed
98 > * Converts an observable of an observable value into a direct observable of the value.
99 > */
100 > public flatten<TNew>(this: IObservable<IObservableWithChange<TNew, any>>): IObservable<TNew> {
101 return _derived(
102 {
107 );
108 }
110 > public recomputeInitiallyAndOnChange(store: DisposableStore, handleValue?: (value: T) => void): IObservable<T> {
111 store.add(_recomputeInitiallyAndOnChange!(this, handleValue));
112 return this;
113 }
115 > /**
116 > * Ensures that this observable is observed. This keeps the cache alive.
117 > * However, in case of deriveds, it does not force eager evaluation (only when the value is read/get).
118 > * Use `recomputeInitiallyAndOnChange` for eager evaluation.
119 > */
120 > public keepObserved(store: DisposableStore): IObservable<T> {
121 store.add(_keepObserved!(this));
122 return this;
123 }
125 > public abstract get debugName(): string;
126 >
127 > protected get debugValue() {
128 return this.get();
129 }
131 > get debug(): DebugHelper {
132 return new DebugHelper(this);
133 }
135 >
136 > class DebugHelper {
137 > constructor(public readonly observable: IObservableWithChange<any, any>) {
138 }
140 > getDependencyGraph(): string {
141 return _debugGetObservableGraph(this.observable, { type: 'dependencies' });
142 }
144 > getObserverGraph(): string {
145 return _debugGetObservableGraph(this.observable, { type: 'observers' });
146 }
148 >
149 > export abstract class BaseObservable<T, TChange = void> extends ConvenientObservable<T, TChange> {
150 > protected readonly _observers = new Set<IObserver>();
151 >
152 > constructor(debugLocation: DebugLocation) {
153 > super(); baseObservable.ts
154 > getLogger()?.handleObservableCreated(this, debugLocation);
155 > }
157 > public addObserver(observer: IObserver): void {
158 > const len = this._observers.size; baseObservable.ts
159 > this._observers.add(observer);
160 > if (len === 0) {
161 > this.onFirstObserverAdded();
162 > }
163 > if (len !== this._observers.size) {
164 > getLogger()?.handleOnListenerCountChanged(this, this._observers.size);
165 > }
166 > }
168 > public removeObserver(observer: IObserver): void {
169 > const deleted = this._observers.delete(observer); baseObservable.ts
170 > if (deleted && this._observers.size === 0) {
171 > this.onLastObserverRemoved();
172 > }
173 > if (deleted) {
174 > getLogger()?.handleOnListenerCountChanged(this, this._observers.size);
175 > }
176 > }
178 > protected onFirstObserverAdded(): void { }
179 > protected onLastObserverRemoved(): void { }
180 >
181 > public override log(): IObservableWithChange<T, TChange> {
182 const hadLogger = !!getLogger();
183 logObservable(this);
src/vs/platform/instantiation/common/instantiation.ts 126 covered LOC · 12 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- instantiation.ts
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 > import { DisposableStore } from '../../../base/common/lifecycle.js';
7 > import * as descriptors from './descriptors.js';
8 > import { ServiceCollection } from './serviceCollection.js';
9 >
10 > // ------ internal util
11 >
12 > export namespace _util {
13 >
14 > export const serviceIds = new Map<string, ServiceIdentifier<any>>();
15 >
16 > export const DI_TARGET = '$di$target';
17 > export const DI_DEPENDENCIES = '$di$dependencies';
18 >
19 > export function getServiceDependencies(ctor: DI_TARGET_OBJ): { id: ServiceIdentifier<any>; index: number }[] {
20 > return ctor[DI_DEPENDENCIES] || []; instantiation.ts
21 > }
23 > export interface DI_TARGET_OBJ extends Function {
24 > [DI_TARGET]: Function;
25 > [DI_DEPENDENCIES]: { id: ServiceIdentifier<any>; index: number }[];
26 > }
27 > }
28 >
29 > // --- interfaces ------
30 >
31 > export type BrandedService = { _serviceBrand: undefined };
32 >
33 > export interface IConstructorSignature<T, Args extends any[] = []> {
34 > new <Services extends BrandedService[]>(...args: [...Args, ...Services]): T;
35 > }
36 >
37 > export interface ServicesAccessor {
38 > get<T>(id: ServiceIdentifier<T>): T;
39 > }
40 >
41 > export const IInstantiationService = createDecorator<IInstantiationService>('instantiationService');
42 >
43 > /**
44 > * Given a list of arguments as a tuple, attempt to extract the leading, non-service arguments
45 > * to their own tuple.
46 > */
47 > export type GetLeadingNonServiceArgs<TArgs extends any[]> =
48 > TArgs extends [] ? []
49 > : TArgs extends [...infer TFirst, BrandedService] ? GetLeadingNonServiceArgs<TFirst>
50 > : TArgs;
51 >
52 > export interface IInstantiationService {
53 >
54 > readonly _serviceBrand: undefined;
55 >
56 > /**
57 > * Synchronously creates an instance that is denoted by the descriptor
58 > */
59 > createInstance<T>(descriptor: descriptors.SyncDescriptor0<T>): T;
60 > createInstance<Ctor extends new (...args: any[]) => unknown, R extends InstanceType<Ctor>>(ctor: Ctor, ...args: GetLeadingNonServiceArgs<ConstructorParameters<Ctor>>): R;
61 >
62 > /**
63 > * Calls a function with a service accessor.
64 > */
65 > invokeFunction<R, TS extends any[] = []>(fn: (accessor: ServicesAccessor, ...args: TS) => R, ...args: TS): R;
66 >
67 > /**
68 > * Creates a child of this service which inherits all current services
69 > * and adds/overwrites the given services.
70 > *
71 > * NOTE that the returned child is `disposable` and should be disposed when not used
72 > * anymore. This will also dispose all the services that this service has created.
73 > */
74 > createChild(services: ServiceCollection, store?: DisposableStore): IInstantiationService;
75 >
76 > /**
77 > * Disposes this instantiation service.
78 > *
79 > * - Will dispose all services that this instantiation service has created.
80 > * - Will dispose all its children but not its parent.
81 > * - Will NOT dispose services-instances that this service has been created with
82 > * - Will NOT dispose consumer-instances this service has created
83 > */
84 > dispose(): void;
85 > }
86 >
87 >
88 > /**
89 > * Identifies a service of type `T`.
90 > */
91 > export interface ServiceIdentifier<T> {
92 > (...args: any[]): void;
93 > type: T;
94 > }
95 >
96 >
97 > function storeServiceDependency(id: ServiceIdentifier<unknown>, target: Function, index: number): void { instantiation.ts
98 > if ((target as _util.DI_TARGET_OBJ)[_util.DI_TARGET] === target) {
99 > (target as _util.DI_TARGET_OBJ)[_util.DI_DEPENDENCIES].push({ id, index }); instantiation.ts
100 > } else { instantiation.ts
101 > (target as _util.DI_TARGET_OBJ)[_util.DI_DEPENDENCIES] = [{ id, index }];
102 > (target as _util.DI_TARGET_OBJ)[_util.DI_TARGET] = target;
103 > }
104 > }
106 > /**
107 > * The *only* valid way to create a {{ServiceIdentifier}}.
108 > */
109 > export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {
110 >
111 > if (_util.serviceIds.has(serviceId)) {
112 return _util.serviceIds.get(serviceId)!;
113 }
115 > const id = function (target: Function, key: string, index: number) {
116 > if (arguments.length !== 3) { instantiation.ts
117 throw new Error('@IServiceName-decorator can only be used to decorate a parameter');
118 }
119 > storeServiceDependency(id, target, index); instantiation.ts
120 > } as ServiceIdentifier<T>;
122 > id.toString = () => serviceId;
123 >
124 > _util.serviceIds.set(serviceId, id);
125 > return id;
126 > }
127 >
128 > export function refineServiceDecorator<T1, T extends T1>(serviceIdentifier: ServiceIdentifier<T1>): ServiceIdentifier<T> {
129 > return <ServiceIdentifier<T>>serviceIdentifier; instantiation.ts
130 > }
src/vs/platform/agentHost/node/agentHostPullRequestOperationHandler.ts 124 covered LOC · 17 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostPullRequestOperationHandler.ts
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 > import { CancellationToken } from '../../../base/common/cancellation.js';
7 > import { URI } from '../../../base/common/uri.js';
8 > import { localize } from '../../../nls.js';
9 > import { IAgentService } from '../common/agentService.js';
10 > import { IAgentHostGitHubEndpointService } from './agentHostGitHubEndpointService.js';
11 > import { parseChangesetUri } from '../common/changesetUri.js';
12 > import { AHP_AUTH_REQUIRED, AHP_SESSION_NOT_FOUND, JsonRpcErrorCodes, ProtocolError } from '../common/state/sessionProtocol.js';
13 > import { readSessionGitHubState, readSessionGitState, type ChangesetOperationFollowUp, type ISessionFileDiff, type ISessionWithDefaultChat } from '../common/state/sessionState.js';
14 > import { ILogService } from '../../log/common/log.js';
15 > import { IAgentHostGitService } from '../common/agentHostGitService.js';
16 > import { type IChangesetOperationHandler } from '../common/agentHostChangesetOperationService.js';
17 > import { type AutoMergeMethod, type CreatedPullRequest, IAgentHostOctoKitService } from './shared/agentHostOctoKitService.js';
18 > import type { InvokeChangesetOperationParams, InvokeChangesetOperationResult } from '../common/state/protocol/channels-changeset/commands.js';
19 > import { ICopilotApiService, type ICopilotUtilityChatMessage } from './shared/copilotApiService.js';
20 > import { buildConversationContext } from '../common/agentHostConversationContext.js';
21 >
22 > /**
23 > * Soft upper bound, in characters, for the conversation context fed to the
24 > * utility model when generating a PR title and description. Sized to stay
25 > * within the small model's context window while leaving room for the changed
26 > * file summary and prompt scaffolding.
27 > */
28 > const MAX_PR_CONVERSATION_CONTEXT_CHARS = 12_000;
29 >
30 > /**
31 > * Soft upper bound, in characters, for the changed-file summary fed to the
32 > * utility model when generating a PR title and description.
33 > */
34 > const MAX_PR_CHANGE_SUMMARY_CHARS = 4_000;
35 >
36 > export interface PullRequestCreatedEvent {
37 > readonly sessionKey: string;
38 > readonly pullRequestUrl: string;
39 > }
40 >
41 > /**
42 > * Server-side handler for the `create-pr` and `create-draft-pr` changeset
43 > * operations advertised on git-backed sessions whose working directory has
44 > * a GitHub remote. Operation availability is recomputed by
45 > * `AgentHostChangesetOperationService.updateOperations`.
46 > *
47 > * The flow mirrors the Copilot CLI extension's `createPullRequest` helper
48 > * (`extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts`):
49 > *
50 > * 1. Resolve session → working directory + current/base branch from
51 > * {@link ISessionGitState}.
52 > * 2. Commit any uncommitted working-tree changes.
53 > * 3. Push the current branch to `origin` (with `--set-upstream` when missing).
54 > * 4. Resolve `owner` / `repo` from {@link ISessionGitState.githubOwner}
55 > * / {@link ISessionGitState.githubRepo} (populated by the git probe).
56 > * 5. Reuse an existing PR for the branch, or POST `/repos/{owner}/{repo}/pulls`
57 > * via {@link IAgentHostOctoKitService}.
58 > * 6. Return the PR URL as an {@link InvokeChangesetOperationResult.followUp}.
59 > */
60 > export class AgentHostPullRequestOperationHandler implements IChangesetOperationHandler {
61 >
62 > public static readonly OPERATION_CREATE_PR = 'create-pr';
63 > public static readonly OPERATION_CREATE_DRAFT_PR = 'create-draft-pr';
64 > public static readonly OPERATION_CREATE_PR_AUTO_MERGE = 'create-pr-auto-merge';
65 > public static readonly OPERATION_CREATE_PR_AUTO_SQUASH = 'create-pr-auto-squash';
66 > public static readonly OPERATION_CREATE_PR_AUTO_REBASE = 'create-pr-auto-rebase';
67 >
68 > constructor(
69 > private readonly _draft: boolean, agentHostPullRequestOperationHandler.ts
70 > private readonly _autoMergeMethod: AutoMergeMethod | undefined,
71 > private readonly _getSessionState: (sessionKey: string) => ISessionWithDefaultChat | undefined,
72 > private readonly _onPullRequestCreated: (event: PullRequestCreatedEvent) => void,
73 > @IAgentService private readonly _agentService: IAgentService,
74 > @IAgentHostGitService private readonly _gitService: IAgentHostGitService,
75 > @IAgentHostOctoKitService private readonly _octoKitService: IAgentHostOctoKitService,
76 > @IAgentHostGitHubEndpointService private readonly _gitHubEndpointService: IAgentHostGitHubEndpointService,
77 > @ICopilotApiService private readonly _copilotApiService: ICopilotApiService,
78 > @ILogService private readonly _logService: ILogService,
79 > ) { }
81 > async invoke(params: InvokeChangesetOperationParams, token: CancellationToken): Promise<InvokeChangesetOperationResult> {
82 const abortController = new AbortController();
83 if (token.isCancellationRequested) {
91 }
92 }
94 > private async _invoke(params: InvokeChangesetOperationParams, token: CancellationToken, signal: AbortSignal): Promise<InvokeChangesetOperationResult> {
95 const parsed = parseChangesetUri(params.channel);
96 if (!parsed) {
221 return await this._finalize(created, false, sessionUri, gitHubState.owner, gitHubState.repo, authToken, signal, token);
222 }
224 > /**
225 > * Notifies listeners that the pull request now exists, optionally enables
226 > * auto-merge with the configured {@link AutoMergeMethod} (best-effort: a
227 > * failure to enable auto-merge does not fail the operation), and builds the
228 > * result message describing what happened.
229 > */
230 > private async _finalize(
231 pr: CreatedPullRequest,
232 isExisting: boolean,
266 return this._createResult(pr, this._buildMessage(pr, isExisting, autoMergeOutcome, autoMergeError));
267 }
269 > private _buildMessage(pr: CreatedPullRequest, isExisting: boolean, autoMergeOutcome: 'none' | 'enabled' | 'failed', autoMergeError: string | undefined): string {
270 let mergeMethodLabel: string | undefined;
271 switch (this._autoMergeMethod) {
303 }
304 }
306 > private _throwIfCancelled(token: CancellationToken): void {
307 if (token.isCancellationRequested) {
308 throw new ProtocolError(JsonRpcErrorCodes.InternalError, localize('agentHost.changeset.pr.cancelled', "Pull request operation was cancelled."));
309 }
310 }
312 > private _formatTitle(branchName: string): string {
313 // Beautify a branch name like `feat/foo-bar` into `feat: foo bar`.
314 const idx = branchName.indexOf('/');
320 return branchName.replace(/[-_]+/g, ' ');
321 }
323 > private _formatCommitMessage(branchName: string): string {
324 return localize('agentHost.changeset.pr.commitMessage', "Agent Host changes for {0}", branchName);
325 }
327 > private _formatBody(branchName: string, baseBranchName: string): string {
328 return localize('agentHost.changeset.pr.body', "Created from `{0}` targeting `{1}`.", branchName, baseBranchName);
329 }
331 > /**
332 > * Best-effort generation of a PR title and description using the utility
333 > * model. The model is given the main session conversation (only the
334 > * markdown text of user requests and agent responses — tool calls,
335 > * subagents, and reasoning are excluded and the text is character-bounded)
336 > * along with a summary of the changed files. Returns `undefined` when no
337 > * Copilot token is available or generation fails, so the caller can fall
338 > * back to the branch-name based title/description. PR creation must never
339 > * fail just because the model is unavailable.
340 > */
341 > private async _generateTitleAndDescription(
342 sessionState: ISessionWithDefaultChat,
343 branchName: string,
376 }
377 }
379 > private _buildTitleAndDescriptionPrompt(branchName: string, base: string, conversation: string | undefined, changeSummary: string): ICopilotUtilityChatMessage[] {
380 const userSections: string[] = [
381 `Branch: ${branchName}`,
405 ];
406 }
408 > private _summarizeDiffsForPrompt(diffs: readonly ISessionFileDiff[]): string {
409 const lines: string[] = [];
410 let length = 0;
441 }
442 }
444 > private _parseTitleAndDescription(raw: string): { title: string; description: string } | undefined {
445 let text = raw.trim().replace(/\r\n/g, '\n');
446 const fenced = /^```(?:markdown|md|text)?\s*([\s\S]*?)\s*```$/i.exec(text);
474 return { title, description };
475 }
477 > private _createResult(created: { readonly url: string; readonly number: number }, message: string): InvokeChangesetOperationResult {
478 const followUp: ChangesetOperationFollowUp = {
479 content: { uri: created.url, contentType: 'text/html' },
src/vs/platform/agentHost/node/shared/shellCommandExecution.ts 123 covered LOC · 12 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- shellCommandExecution.ts
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 > import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
7 > import * as platform from '../../../../base/common/platform.js';
8 > import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js';
9 > import { generateUuid } from '../../../../base/common/uuid.js';
10 > import { ILogService } from '../../../log/common/log.js';
11 > import { TerminalClaimKind } from '../../common/state/protocol/state.js';
12 > import { IAgentHostTerminalManager } from '../agentHostTerminalManager.js';
13 >
14 > /**
15 > * Maximum scrollback content (in bytes) returned to the model / caller in
16 > * command results.
17 > */
18 > export const SHELL_COMMAND_MAX_OUTPUT_BYTES = 80_000;
19 >
20 > /**
21 > * Default command timeout in milliseconds (120 seconds).
22 > */
23 > export const DEFAULT_SHELL_COMMAND_TIMEOUT_MS = 120_000;
24 >
25 > /**
26 > * The sentinel prefix used to detect command completion in terminal output
27 > * when shell integration is unavailable. The full sentinel format is:
28 > * `<<<COPILOT_SENTINEL_<uuid>_EXIT_<code>>>`.
29 > */
30 > const SENTINEL_PREFIX = '<<<COPILOT_SENTINEL_';
31 >
32 > /**
33 > * The kind of shell a command runs in. Determines sentinel syntax, history
34 > * suppression and bracketed-paste heuristics.
35 > */
36 > export type ShellType = 'bash' | 'powershell';
37 >
38 > /**
39 > * Routes a resolved shell executable to a {@link ShellType}. Falls back to the
40 > * platform default for unknown shells.
41 > */
42 > export function shellTypeForExecutable(shellPath: string): ShellType {
43 // Strip path on either separator and the .exe suffix.
44 const lastSep = Math.max(shellPath.lastIndexOf('/'), shellPath.lastIndexOf('\\'));
74 }
75 }
77 > /**
78 > * For POSIX shells (bash/zsh) that honor `HISTCONTROL=ignorespace` /
79 > * `HIST_IGNORE_SPACE`, prepending a single space prevents the command from
80 > * being recorded in shell history. The shell integration scripts opt these
81 > * settings in via the `VSCODE_PREVENT_SHELL_HISTORY` env var (set when the
82 > * terminal is created with `preventShellHistory: true`). PowerShell
83 > * suppresses history through PSReadLine instead, so no prefix is needed.
84 > */
85 > export function prefixForHistorySuppression(shellType: ShellType): string {
86 return shellType === 'powershell' ? '' : ' ';
87 }
89 > export function isMultilineCommand(command: string): boolean {
90 const normalized = command.replace(/\r\n|\r/g, '\n');
91 return /(?<!\\)\n/.test(normalized);
92 }
94 function shouldUseBracketedPasteMode(command: string): boolean {
95 return platform.isMacintosh || isMultilineCommand(command);
96 }
98 function makeSentinelId(): string {
99 return generateUuid().replace(/-/g, '');
100 }
102 function buildSentinelCommand(sentinelId: string, shellType: ShellType): string {
103 if (shellType === 'powershell') {
106 return `echo "${SENTINEL_PREFIX}${sentinelId}_EXIT_$?>>>"`;
107 }
109 function parseSentinel(content: string, sentinelId: string): { found: boolean; exitCode: number; outputBeforeSentinel: string } {
110 const marker = `${SENTINEL_PREFIX}${sentinelId}_EXIT_`;
131 return { found: false, exitCode: -1, outputBeforeSentinel: content };
132 }
134 > /**
135 > * Strips ANSI escape codes and trims the terminal output to the last
136 > * {@link SHELL_COMMAND_MAX_OUTPUT_BYTES} bytes so it is safe to surface to a
137 > * model or the transcript.
138 > */
139 > export function prepareOutputForModel(rawOutput: string): string {
140 let text = removeAnsiEscapeCodes(rawOutput).trim();
141 if (text.length > SHELL_COMMAND_MAX_OUTPUT_BYTES) {
144 return text;
145 }
147 > /**
148 > * Terminal against which a shell command is executed.
149 > */
150 > export interface IShellCommandTarget {
151 > /** URI of the managed terminal the command runs in. */
152 > readonly terminalUri: string;
153 > /** The kind of shell backing the terminal. */
154 > readonly shellType: ShellType;
155 > }
156 >
157 > /**
158 > * How a shell command execution finished.
159 > *
160 > * - `completed` — the command finished; {@link IShellCommandResult.exitCode} holds the exit code.
161 > * - `timeout` — the command did not finish within the timeout; output is partial.
162 > * - `background` — the terminal claim was narrowed (user chose to continue in background).
163 > * - `altBuffer` — the command switched to the terminal's alternate buffer (interactive UI).
164 > * - `shellExited` — the shell process exited unexpectedly.
165 > */
166 > export type ShellCommandStatus = 'completed' | 'timeout' | 'background' | 'altBuffer' | 'shellExited';
167 >
168 > /**
169 > * Neutral, agent-agnostic result of executing a shell command. Callers map this
170 > * to their own result shape (e.g. an SDK `ToolResultObject` or an AHP tool call
171 > * completion).
172 > */
173 > export interface IShellCommandResult {
174 > /** How the command execution finished. */
175 > readonly status: ShellCommandStatus;
176 > /** Exit code, when known (`completed` and `shellExited`). */
177 > readonly exitCode?: number;
178 > /** Cleaned command output (empty for `background`/`altBuffer`). */
179 > readonly output: string;
180 > }
181 >
182 > /**
183 > * Execute a command on an already-created managed terminal, resolving once the
184 > * command finishes, times out, backgrounds, enters the alternate buffer, or the
185 > * shell exits. Uses shell integration (OSC 633) for completion detection when
186 > * available and falls back to a sentinel echo otherwise.
187 > *
188 > * This is the shared shell-integration primitive used by both the Copilot SDK
189 > * shell tools and the agent-host `!command` runner.
190 > */
191 > export function executeShellCommand(
192 target: IShellCommandTarget,
193 command: string,
200 : executeCommandWithSentinel(target, command, timeoutMs, terminalManager, logService);
201 }
203 function registerAltBufferHandler(
204 target: IShellCommandTarget,
213 });
214 }
216 > /**
217 > * Execute a command using shell integration (OSC 633) for completion detection.
218 > * No sentinel echo is injected — the shell's own command-finished signal
219 > * provides the exit code and cleanly delineated output.
220 > */
221 async function executeCommandWithShellIntegration(
222 target: IShellCommandTarget,
282 return result;
283 }
285 > /**
286 > * Fallback: execute a command using a sentinel echo to detect completion.
287 > * Used when shell integration is not available.
288 > */
289 async function executeCommandWithSentinel(
290 target: IShellCommandTarget,
src/vs/platform/terminal/node/terminalEnvironment.ts 123 covered LOC · 6 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- terminalEnvironment.ts
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 > import * as os from 'os';
7 > import { FileAccess } from '../../../base/common/network.js';
8 > import * as path from '../../../base/common/path.js';
9 > import { IProcessEnvironment, isMacintosh, isWindows } from '../../../base/common/platform.js';
10 > import * as process from '../../../base/common/process.js';
11 > import { format } from '../../../base/common/strings.js';
12 > import { ILogService } from '../../log/common/log.js';
13 > import { IProductService } from '../../product/common/productService.js';
14 > import { IShellLaunchConfig, ITerminalEnvironment, ITerminalProcessOptions, ShellIntegrationInjectionFailureReason } from '../common/terminal.js';
15 > import { EnvironmentVariableMutatorType } from '../common/environmentVariable.js';
16 > import { deserializeEnvironmentVariableCollections } from '../common/environmentVariableShared.js';
17 > import { MergedEnvironmentVariableCollection } from '../common/environmentVariableCollection.js';
18 > import { chmod, realpathSync, mkdirSync } from 'fs';
19 > import { promisify } from 'util';
20 > import { isString, SingleOrMany } from '../../../base/common/types.js';
21 > import { getWindowsBuildNumberAsync } from '../../../base/node/windowsVersion.js';
22 >
23 > export interface IShellIntegrationConfigInjection {
24 > readonly type: 'injection';
25 > /**
26 > * A new set of arguments to use.
27 > */
28 > readonly newArgs: string[] | undefined;
29 > /**
30 > * An optional environment to mixing to the real environment.
31 > */
32 > readonly envMixin?: IProcessEnvironment;
33 > /**
34 > * An optional array of files to copy from `source` to `dest`.
35 > */
36 > readonly filesToCopy?: {
37 > source: string;
38 > dest: string;
39 > }[];
40 > }
41 >
42 > export interface IShellIntegrationInjectionFailure {
43 > readonly type: 'failure';
44 > readonly reason: ShellIntegrationInjectionFailureReason;
45 > }
46 >
47 > /**
48 > * For a given shell launch config, returns arguments to replace and an optional environment to
49 > * mixin to the SLC's environment to enable shell integration. This must be run within the context
50 > * that creates the process to ensure accuracy. Returns undefined if shell integration cannot be
51 > * enabled.
52 > */
53 export async function getShellIntegrationInjection(
54 shellLaunchConfig: IShellLaunchConfig,
279 return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.UnsupportedShell };
280 }
282 > /**
283 > * There are a few situations where some directories are added to the beginning of the PATH.
284 > * 1. On macOS when the profile calls path_helper.
285 > * 2. For fish terminals, which always prepend "$fish_user_paths" to the PATH.
286 > *
287 > * This causes significant problems for the environment variable
288 > * collection API as the custom paths added to the end will now be somewhere in the middle of
289 > * the PATH. To combat this, VSCODE_PATH_PREFIX is used to re-apply any prefix after the profile
290 > * has run. This will cause duplication in the PATH but should fix the issue.
291 > *
292 > * See #99878 for more information.
293 > */
294 function addEnvMixinPathPrefix(options: ITerminalProcessOptions, envMixin: IProcessEnvironment, shell: string): void {
295 if ((isMacintosh || shell === 'fish') && options.environmentVariableCollections) {
315 }
316 }
318 > enum ShellIntegrationExecutable {
319 > WindowsPwsh = 'windows-pwsh',
320 > WindowsPwshLogin = 'windows-pwsh-login',
321 > Pwsh = 'pwsh',
322 > PwshLogin = 'pwsh-login',
323 > Zsh = 'zsh',
324 > ZshLogin = 'zsh-login',
325 > Bash = 'bash',
326 > Fish = 'fish',
327 > FishLogin = 'fish-login',
328 > }
329 >
330 > const shellIntegrationArgs: Map<ShellIntegrationExecutable, string[]> = new Map();
331 > // The try catch swallows execution policy errors in the case of the archive distributable
332 > shellIntegrationArgs.set(ShellIntegrationExecutable.WindowsPwsh, ['-noexit', '-command', 'try { . \"{0}\\out\\vs\\workbench\\contrib\\terminal\\common\\scripts\\shellIntegration.ps1\" } catch {}{1}']);
333 > shellIntegrationArgs.set(ShellIntegrationExecutable.WindowsPwshLogin, ['-l', '-noexit', '-command', 'try { . \"{0}\\out\\vs\\workbench\\contrib\\terminal\\common\\scripts\\shellIntegration.ps1\" } catch {}{1}']);
334 > shellIntegrationArgs.set(ShellIntegrationExecutable.Pwsh, ['-noexit', '-command', '. "{0}/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1"{1}']);
335 > shellIntegrationArgs.set(ShellIntegrationExecutable.PwshLogin, ['-l', '-noexit', '-command', '. "{0}/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1"']);
336 > shellIntegrationArgs.set(ShellIntegrationExecutable.Zsh, ['-i']);
337 > shellIntegrationArgs.set(ShellIntegrationExecutable.ZshLogin, ['-il']);
338 > shellIntegrationArgs.set(ShellIntegrationExecutable.Bash, ['--init-file', '{0}/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh']);
339 > shellIntegrationArgs.set(ShellIntegrationExecutable.Fish, ['--init-command', 'source "{0}/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish"']);
340 > shellIntegrationArgs.set(ShellIntegrationExecutable.FishLogin, ['-l', '--init-command', 'source "{0}/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish"']);
341 > const pwshLoginArgs = ['-login', '-l'];
342 > const shLoginArgs = ['--login', '-l'];
343 > const shInteractiveArgs = ['-i', '--interactive'];
344 > const pwshImpliedArgs = ['-nol', '-nologo'];
345 >
346 function arePwshLoginArgs(originalArgs: SingleOrMany<string>): boolean {
347 if (isString(originalArgs)) {
354 }
355 }
357 function arePwshImpliedArgs(originalArgs: SingleOrMany<string>): boolean {
358 if (isString(originalArgs)) {
362 }
363 }
365 function areZshBashFishLoginArgs(originalArgs: SingleOrMany<string>): boolean {
366 if (!isString(originalArgs)) {
370 || !isString(originalArgs) && originalArgs.length === 1 && shLoginArgs.includes(originalArgs[0].toLowerCase());
371 }
373 > /**
374 > * Patterns that indicate sensitive environment variable names.
375 > */
376 > const sensitiveEnvVarNames = /^(?:.*_)?(?:API_?KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|CREDENTIAL|AUTH|PRIVATE_?KEY|ACCESS_?KEY|CLIENT_?SECRET|APIKEY)(?:_.*)?$/i;
377 >
378 > /**
379 > * Patterns for detecting secret values in environment variables.
380 > */
381 > const secretValuePatterns = [
382 > // JWT tokens
383 > /^eyJ[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+$/,
384 > // GitHub tokens
385 > /^gh[psuro]_[a-zA-Z0-9]{36}$/,
386 > /^github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}$/,
387 > // Google API keys
388 > /^AIza[A-Za-z0-9_\-]{35}$/,
389 > // Slack tokens
390 > /^xox[pbar]\-[A-Za-z0-9\-]+$/,
391 > // Azure/MS tokens (common patterns)
392 > /^[a-zA-Z0-9]{32,}$/,
393 > ];
394 >
395 > /**
396 > * Sanitizes environment variables for logging by redacting sensitive values.
397 > */
398 > export function sanitizeEnvForLogging(env: IProcessEnvironment | undefined): IProcessEnvironment | undefined {
399 if (!env) {
400 return env;
src/vs/base/test/common/virtualScheduling/trace.ts 122 covered LOC · 7 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- trace.ts
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 > import { BugIndicatingError } from '../../../common/errors.js';
7 >
8 > /**
9 > * # Trace — causal-chain attribution for scheduled work
10 > *
11 > * A {@link Trace} is an immutable value identifying a causal chain. Every
12 > * non-root trace carries a `parent`; the head of the chain has no parent.
13 > * Use {@link child} to extend a chain when scheduling follow-up work.
14 > *
15 > * Traces are used to answer "who caused this?" for any virtual event:
16 > * useful for debugging, for per-owner termination, and for attribution in
17 > * error messages.
18 > */
19 > export class Trace {
20 > private static _idCounter = 0;
21 > public readonly id: number = ++Trace._idCounter;
22 > public readonly root: Trace;
23 > public readonly depth: number;
24 >
25 > constructor(
26 > public readonly parent: Trace | undefined,
27 > public readonly label: string,
28 > public readonly stack: string | undefined = undefined,
29 > ) {
30 > this.root = parent?.root ?? this;
31 > this.depth = (parent?.depth ?? -1) + 1;
32 > }
33 >
34 > child(label: string, stack?: string): Trace {
35 return new Trace(this, label, stack);
36 }
37 > trace.ts
38 > /** "#id label ← #id label ← … ← #id label" */
39 > describe(): string {
40 const parts: string[] = [];
41 for (let t: Trace | undefined = this; t; t = t.parent) {
44 return parts.join(' ← ');
45 }
46 > trace.ts
47 > toString(): string { return this.describe(); }
48 > }
49 >
50 > /** Sentinel for "no known causal predecessor". */
51 > export const ROOT_TRACE: Trace = new Trace(undefined, '<root>');
52 >
53 > export function createTraceRoot(label: string, stack?: string): Trace {
54 return new Trace(undefined, label, stack);
55 }
56 > trace.ts
57 > interface Frame {
58 > readonly trace: Trace;
59 > readonly prev: Frame | undefined;
60 > }
61 >
62 > const ROOT_FRAME: Frame = { trace: ROOT_TRACE, prev: undefined };
63 >
64 > /**
65 > * Options for {@link TraceContext.runAsHandler}.
66 > *
67 > * # Why this is a per-call option
68 > *
69 > * `runAsHandler` cannot restore the previous trace synchronously: microtasks
70 > * enqueued by `fn` (including awaited continuations) must observe the new
71 > * trace. So the reset is deferred — but it must fire after the *closure* of
72 > * the microtask queue (the current microtask plus every microtask it
73 > * recursively enqueues), not just one drain.
74 > *
75 > * Per spec, the host doesn't run a macrotask until the microtask queue is
76 > * empty, so any macrotask primitive (`setTimeout(0)`, `setImmediate`, the
77 > * `setTimeout0` shim) achieves this. Letting the *caller* supply the sink
78 > * means:
79 > *
80 > * - the {@link VirtualTimeProcessor} can route the reset through the same
81 > * primitive its embedding uses for its own host hops, eliminating any
82 > * race between the processor's hops and the trace-reset timer;
83 > *
84 > * - production code without a processor can still use a real
85 > * `setTimeout(0)`-based sink and get the same semantics;
86 > *
87 > * - tests can install a deterministic sink (e.g. a hand-driven queue) for
88 > * fully synchronous assertions.
89 > */
90 > export interface RunAsHandlerOptions {
91 > /**
92 > * Sink for the deferred trace-reset.
93 > *
94 > * Must invoke `reset` after the microtask closure that follows the
95 > * `runAsHandler` call returns — i.e. on the next host macrotask.
96 > */
97 > readonly afterMicrotaskClosure: (reset: () => void) => void;
98 > }
99 >
100 > /**
101 > * Holds the mutable "current trace frame" slot. Construct fresh instances
102 > * for test isolation, or use {@link TraceContext.instance} for shared state.
103 > */
104 > export class TraceContext {
105 > public static readonly instance = new TraceContext();
106 >
107 > private _current: Frame = ROOT_FRAME;
108 > private _isHandlerRunning = false;
109 >
110 > currentTrace(): Trace { return this._current.trace; }
111 >
112 > /**
113 > * Install `t` as current for the synchronous duration of `fn`, then
114 > * restore. Nestable. Microtasks enqueued by fn that run after fn returns
115 > * see the *restored* trace — use {@link runAsHandler} when continuation
116 > * inheritance is wanted.
117 > */
118 > runWithTrace<T>(t: Trace, fn: () => T): T {
119 const prev = this._current;
120 const next: Frame = { trace: t, prev };
132 }
133 }
134 > trace.ts
135 > /**
136 > * Install `t` as current and run `fn`. The trace stays current through
137 > * the microtask closure that follows `fn`, so awaited continuations
138 > * inside fn observe `t`. The reset is dispatched via
139 > * `opts.afterMicrotaskClosure`.
140 > *
141 > * Throws on synchronous re-entry: timer callbacks never nest on the
142 > * same JS stack frame, so this only fires for misuse.
143 > */
144 > runAsHandler<T>(t: Trace, fn: () => T, opts: RunAsHandlerOptions): T {
145 if (this._isHandlerRunning) {
146 throw new Error(
165 }
166 }
167 > trace.ts
168 > _resetForTesting(): void {
169 this._current = ROOT_FRAME;
170 this._isHandlerRunning = false;
171 }
172 > } trace.ts
src/vs/platform/agentHost/node/agentHostTelemetryService.ts 121 covered LOC · 32 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostTelemetryService.ts
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 > import { hostname, release } from 'os';
7 > import { Disposable, isDisposable, toDisposable, type DisposableStore } from '../../../base/common/lifecycle.js';
8 > import { joinPath } from '../../../base/common/resources.js';
9 > import { URI } from '../../../base/common/uri.js';
10 > import { getDevDeviceId, getMachineId, getSqmMachineId } from '../../../base/node/id.js';
11 > import { ConfigurationService } from '../../configuration/common/configurationService.js';
12 > import { INativeEnvironmentService } from '../../environment/common/environment.js';
13 > import { IFileService } from '../../files/common/files.js';
14 > import { ILogService, ILoggerService } from '../../log/common/log.js';
15 > import { NullPolicyService } from '../../policy/common/policy.js';
16 > import { IProductService } from '../../product/common/productService.js';
17 > import { IRequestService } from '../../request/common/request.js';
18 > import { OneDataSystemAppender } from '../../telemetry/node/1dsAppender.js';
19 > import { resolveCommonProperties } from '../../telemetry/common/commonProperties.js';
20 > import { ClassifiedEvent, IGDPRProperty, OmitMetadata, StrictPropertyCheck } from '../../telemetry/common/gdprTypings.js';
21 > import { ITelemetryData, ITelemetryService, TelemetryLevel } from '../../telemetry/common/telemetry.js';
22 > import { TelemetryLogAppender } from '../../telemetry/common/telemetryLogAppender.js';
23 > import { TelemetryService } from '../../telemetry/common/telemetryService.js';
24 > import { getPiiPathsFromEnvironment, isInternalTelemetry, isLoggingOnly, NullTelemetryService, supportsTelemetry, type ITelemetryAppender } from '../../telemetry/common/telemetryUtils.js';
25 > import { AgentHostTelemetryLevelConfigKey, agentHostConfigValueToTelemetryLevel } from '../common/agentHostSchema.js';
26 > import { AgentHostDevDeviceIdEnvKey, AgentHostMachineIdEnvKey, AgentHostSqmIdEnvKey } from '../common/agentHostTelemetryEnv.js';
27 > import { AgentHostRestrictedTelemetrySender, IAgentHostRestrictedTelemetry, IAgentHostInternalTelemetryContext, IAgentHostRestrictedTelemetryContext, TelemetryMeasurements, TelemetryProps } from './agentHostRestrictedTelemetry.js';
28 > import { AgentHostInternalTelemetrySender } from './agentHostMicrosoftTelemetry.js';
29 >
30 > export interface IAgentHostTelemetryServiceOptions {
31 > readonly environmentService: INativeEnvironmentService;
32 > readonly productService: IProductService;
33 > readonly fileService: IFileService;
34 > readonly loggerService: ILoggerService | undefined;
35 > readonly logService: ILogService;
36 > readonly disposables: DisposableStore;
37 > readonly disableTelemetry?: boolean;
38 > readonly fetchFn?: typeof globalThis.fetch;
39 > readonly requestService?: IRequestService;
40 > }
41 >
42 > export interface IAgentHostTelemetryService extends ITelemetryService, IAgentHostRestrictedTelemetry {
43 > updateTelemetryLevel(telemetryLevel: TelemetryLevel): void;
44 > }
45 >
46 > export class AgentHostTelemetryService extends Disposable implements IAgentHostTelemetryService {
47 > declare readonly _serviceBrand: undefined;
48 >
49 > private _telemetryLevel = TelemetryLevel.USAGE;
50 >
51 > /**
52 > * Whether the current Copilot token opts into enhanced/restricted telemetry (`rt=1`). Defaults
53 > * to `false` so nothing restricted is sent until an authenticated token confirms the opt-in,
54 > * keeping public users off the enhanced pipeline the way the Copilot extension does.
55 > */
56 > private _restrictedTelemetryEnabled = false;
57 > private _internalTelemetryEnabled = false;
58 >
59 > constructor(
60 private readonly _delegate: ITelemetryService,
61 private readonly _restricted?: IAgentHostRestrictedTelemetry,
66 }
67 }
69 > get telemetryLevel(): TelemetryLevel {
70 return Math.min(this._delegate.telemetryLevel, this._telemetryLevel);
71 }
73 > get sendErrorTelemetry(): boolean {
74 return this.telemetryLevel >= TelemetryLevel.ERROR && this._delegate.sendErrorTelemetry;
75 }
77 > get sessionId(): string {
78 return this._delegate.sessionId;
79 }
81 > get machineId(): string {
82 return this._delegate.machineId;
83 }
85 > get sqmId(): string {
86 return this._delegate.sqmId;
87 }
89 > get devDeviceId(): string {
90 return this._delegate.devDeviceId;
91 }
93 > get firstSessionDate(): string {
94 return this._delegate.firstSessionDate;
95 }
97 > get msftInternal(): boolean | undefined {
98 return this._delegate.msftInternal;
99 }
101 > publicLog(eventName: string, data?: ITelemetryData): void {
102 if (this.telemetryLevel < TelemetryLevel.USAGE) {
103 return;
105 this._delegate.publicLog(eventName, data);
106 }
108 > publicLogError(eventName: string, data?: ITelemetryData): void {
109 if (this.telemetryLevel < TelemetryLevel.ERROR) {
110 return;
112 this._delegate.publicLogError(eventName, data);
113 }
115 > publicLog2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>): void {
116 if (this.telemetryLevel < TelemetryLevel.USAGE) {
117 return;
119 this._delegate.publicLog2(eventName, data);
120 }
122 > publicLogError2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>): void {
123 if (this.telemetryLevel < TelemetryLevel.ERROR) {
124 return;
126 this._delegate.publicLogError2(eventName, data);
127 }
129 > sendGHTelemetryEvent(eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void {
130 if (this.telemetryLevel < TelemetryLevel.USAGE) {
131 return;
133 this._restricted?.sendGHTelemetryEvent(eventName, properties, measurements);
134 }
136 > sendEnhancedGHTelemetryEvent(eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void {
137 if (this.telemetryLevel < TelemetryLevel.USAGE || !this._restrictedTelemetryEnabled) {
138 return;
140 this._restricted?.sendEnhancedGHTelemetryEvent(eventName, properties, measurements);
141 }
143 > sendEnhancedGHTelemetryEventForContext(context: IAgentHostRestrictedTelemetryContext, eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void {
144 if (this.telemetryLevel < TelemetryLevel.USAGE || !context.restrictedTelemetryEnabled) {
145 return;
147 this._restricted?.sendEnhancedGHTelemetryEventForContext(context, eventName, properties, measurements);
148 }
150 > sendInternalMSFTTelemetryEvent(eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void {
151 if (this.telemetryLevel < TelemetryLevel.USAGE || !this._internalTelemetryEnabled) {
152 return;
154 this._restricted?.sendInternalMSFTTelemetryEvent(eventName, properties, measurements);
155 }
157 > sendInternalMSFTTelemetryEventForContext(context: IAgentHostInternalTelemetryContext, eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void {
158 if (this.telemetryLevel < TelemetryLevel.USAGE || !context.isInternal) {
159 return;
161 this._restricted?.sendInternalMSFTTelemetryEventForContext(context, eventName, properties, measurements);
162 }
164 > setCopilotTrackingId(trackingId: string | undefined): void {
165 this._restricted?.setCopilotTrackingId(trackingId);
166 }
168 > setRestrictedTelemetryEndpoint(endpointUrl: string | undefined): void {
169 this._restricted?.setRestrictedTelemetryEndpoint(endpointUrl);
170 }
172 > setRestrictedTelemetryEnabled(enabled: boolean): void {
173 this._restrictedTelemetryEnabled = enabled;
174 // Mirror onto the sender so the restricted-table writer enforces the same `rt` gate
176 this._restricted?.setRestrictedTelemetryEnabled(enabled);
177 }
179 > setInternalTelemetryContext(context: IAgentHostInternalTelemetryContext | undefined): void {
180 this._internalTelemetryEnabled = context?.isInternal === true;
181 this._restricted?.setInternalTelemetryContext(context);
182 }
184 > setExperimentProperty(name: string, value: string): void {
185 this._delegate.setExperimentProperty(name, value);
186 }
188 > setCommonProperty(name: string, value: string | boolean): void {
189 this._delegate.setCommonProperty(name, value);
190 }
192 > updateTelemetryLevel(telemetryLevel: TelemetryLevel): void {
193 this._telemetryLevel = Math.min(this._telemetryLevel, telemetryLevel);
194 }
196 >
197 > export function updateAgentHostTelemetryLevelFromConfig(telemetryService: ITelemetryService, config: Record<string, unknown> | undefined): void {
198 > const telemetryLevel = config?.[AgentHostTelemetryLevelConfigKey]; agentHostTelemetryService.ts
199 > const telemetryLevelValue = agentHostConfigValueToTelemetryLevel(telemetryLevel);
200 > if (!isAgentHostTelemetryService(telemetryService) || telemetryLevelValue === undefined) {
202 > }
203 telemetryService.updateTelemetryLevel(telemetryLevelValue);
204 }
206 > export function isAgentHostTelemetryService(telemetryService: ITelemetryService): telemetryService is IAgentHostTelemetryService {
207 > return typeof (telemetryService as IAgentHostTelemetryService).updateTelemetryLevel === 'function'; agentHostTelemetryService.ts
208 > }
210 async function resolveCopilotExtensionVersion(environmentService: INativeEnvironmentService, fileService: IFileService, logService: ILogService): Promise<string | undefined> {
211 if (!environmentService.builtinExtensionsPath) {
220 }
221 }
223 export async function createAgentHostTelemetryService(options: IAgentHostTelemetryServiceOptions): Promise<IAgentHostTelemetryService> {
224 const { environmentService, productService, fileService, loggerService, logService, disposables } = options;
src/vs/platform/agentHost/node/sessionDiffAggregator.ts 116 covered LOC · 5 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- sessionDiffAggregator.ts
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 > import { URI } from '../../../base/common/uri.js';
7 > import type { IFileEditRecord, ISessionDatabase } from '../common/sessionDataService.js';
8 > import type { IDiffComputeService } from '../common/diffComputeService.js';
9 > import { FileEditKind, type ISessionFileDiff } from '../common/state/sessionState.js';
10 > import { buildSessionDbUri } from './shared/fileEditTracker.js';
11 >
12 function getFileEditUri(diff: ISessionFileDiff): string | undefined {
13 return diff.after?.uri ?? diff.before?.uri;
14 }
16 function createSessionFileDiff(beforeSessionUri: string, afterSessionUri: string, identity: IFileIdentity, added: number, removed: number): ISessionFileDiff {
17 const hasBefore = identity.firstKind !== FileEditKind.Create;
33 };
34 }
36 > /**
37 > * Represents a file's identity across renames, tracking its first and last
38 > * snapshots in the session for diff computation.
39 > */
40 > interface IFileIdentity {
41 > /** The last known URI for this file. */
42 > terminalPath: string;
43 > /** Tool call ID of the first edit (for fetching "before" content). */
44 > firstToolCallId: string;
45 > /** File path used in the first edit's database record. */
46 > firstFilePath: string;
47 > /** The kind of the first edit (Create means no "before" content). */
48 > firstKind: FileEditKind;
49 > /** Index into the sources array of the DB that owns the first edit. */
50 > firstSourceIdx: number;
51 > /** Tool call ID of the last edit (for fetching "after" content). */
52 > lastToolCallId: string;
53 > /** File path used in the last edit's database record. */
54 > lastFilePath: string;
55 > /** The kind of the last edit (Delete means no "after" content). */
56 > lastKind: FileEditKind;
57 > /** Index into the sources array of the DB that owns the last edit. */
58 > lastSourceIdx: number;
59 > }
60 >
61 > /**
62 > * A single database whose file edits contribute to a session's aggregated
63 > * diff. For single-chat sessions there is one source (the session DB); for
64 > * multi-chat sessions each peer chat records edits into its own DB, so the
65 > * session changeset unions the session DB with every peer chat DB.
66 > */
67 > export interface ISessionDiffSource {
68 > /**
69 > * The session / peer-chat URI that owns {@link db}. Encoded into the
70 > * `session-db:` content URIs so the resource resolver opens the correct
71 > * database when fetching before/after blobs.
72 > */
73 > sessionUri: string;
74 > /** The database holding this source's file edits. */
75 > db: ISessionDatabase;
76 > }
77 >
78 > /**
79 > * Options for incremental diff computation. When provided,
80 > * {@link computeSessionDiffs} reuses previous diff results for file
81 > * identities that were not touched in the given turn.
82 > */
83 > export interface IIncrementalDiffOptions {
84 > /** The turn ID that just completed — only identities touched by edits
85 > * in this turn will be recomputed. */
86 > changedTurnId: string;
87 > /** Previously computed diffs (from the last dispatch). Entries for
88 > * untouched identities are carried over without recomputation. */
89 > previousDiffs: ISessionFileDiff[];
90 > }
91 >
92 > /**
93 > * Computes aggregated diff statistics for a session by comparing each file's
94 > * first snapshot to its last snapshot, tracking renames across the chain.
95 > *
96 > * When {@link incremental} is provided, only identities that were touched
97 > * by edits in the given turn are recomputed; all other identities reuse
98 > * the previous diff results. This avoids expensive content fetches and
99 > * diff computations for unchanged files.
100 > *
101 > * Returns an {@link ISessionFileDiff} array with the "last known URI" for each
102 > * file and the total lines added/removed across the session.
103 > */
104 export async function computeSessionDiffs(
105 sessionUri: string,
259 return results;
260 }
262 > /**
263 > * Computes aggregated diff statistics across one or more {@link ISessionDiffSource}
264 > * databases by unioning their file edits and comparing each file's first
265 > * snapshot to its last snapshot, tracking renames across the chain.
266 > *
267 > * Single-chat sessions pass one source (the session DB). Multi-chat sessions
268 > * pass the session DB plus every peer chat DB so peer-chat edits (recorded into
269 > * their own databases) roll up into the session-level changes. Each file
270 > * identity remembers which source owns its first and last snapshots so the
271 > * before/after content is read from — and its `session-db:` content URI encodes —
272 > * the correct database.
273 > *
274 > * Sources are unioned in array order (session first, peers next); within a
275 > * source, edits keep their insertion order. When a file is touched by more than
276 > * one source the "before" comes from the earliest source that touched it and the
277 > * "after" from the latest, which matches the shared working tree the chats edit.
278 > *
279 > * TODO (debt): this always does a full recompute — it ignores the
280 > * {@link IIncrementalDiffOptions} fast/slow paths that {@link computeSessionDiffs}
281 > * uses for single-source sessions. An incremental union is a safe follow-up:
282 > * the per-identity `firstSourceIdx`/`lastSourceIdx` already carry the provenance
283 > * needed to recompute only the turn's owning source plus cross-source files and
284 > * carry over the rest. Requires plumbing the owning source of `changedTurnId`
285 > * through `onTurnComplete` → `_doComputeStaticChangeset`. See tracking issue.
286 > */
287 export async function computeUnionedDiffs(
288 sources: readonly ISessionDiffSource[],
376 return results;
377 }
379 > /**
380 > * Computes the diff statistics for a single turn — files touched only
381 > * within `turnId`, with their `before` snapshot taken from the first edit
382 > * record in that turn and their `after` snapshot from the last. Used by
383 > * the per-turn changeset (`<session>/changeset/turn/<turnId>`).
384 > *
385 > * Returns an empty array when the turn touched no files.
386 > */
387 export async function computeTurnDiffs(
388 sessionUri: string,
src/vs/platform/agentHost/node/shared/editSurvivalTracker.ts 116 covered LOC · 8 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- editSurvivalTracker.ts
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 > /*
7 > * Edit-survival math for agent-host file edits.
8 > *
9 > * Sister implementation of the chat extension's `EditSurvivalTracker`
10 > * (`extensions/copilot/src/platform/editSurvivalTracking/common/editSurvivalTracker.ts`).
11 > * The extension version operates on multi-range `StringEdit`s with a
12 > * live `TextModel`; here we only have whole-file snapshots and (when
13 > * the tool input is recognisable) the explicit text the AI wrote. The
14 > * whole-file path is the baseline; the chunked path uses asymmetric
15 > * "fraction of AI 4-grams still present in the file" scoring so an
16 > * edit's score doesn't decay as the file grows around it.
17 > *
18 > * Mostly carried over from the chat extension's version.
19 > */
20 >
21 > /**
22 > * Computes a number between 0 and 1 that reflects how similar the two
23 > * texts are by counting how many 4-grams are shared between them.
24 > */
25 > export function compute4GramTextSimilarity(text1: string, text2: string): number {
26 const n = 4;
27
55 return equalNGramCount / totalNGramCount;
56 }
58 > /**
59 > * Computes the share of `chunk`'s 4-grams that appear anywhere in
60 > * `currentText`. Unlike {@link compute4GramTextSimilarity}, this is
61 > * asymmetric: the denominator is the chunk's n-gram count, not the
62 > * combined corpus. That makes the result stable as `currentText` grows
63 > * around the chunk — appending unrelated content does not drag the
64 > * score down. Returns a number in [0, 1].
65 > *
66 > * Used to ask "is the text the AI wrote still present in the file?"
67 > * when we have an explicit chunk (the `new_string` from `Edit`, each
68 > * entry of `MultiEdit.edits[*].new_string`, or `Write.content`) rather
69 > * than a whole-file before/after pair.
70 > *
71 > * For multi-chunk scoring against the same file, prefer building the
72 > * file n-gram set once via {@link buildNGramSet} and passing it to
73 > * {@link computeFractionPresentInSet} to avoid rebuilding the set per
74 > * chunk — see {@link computeChunkedFourGramSurvival}.
75 > */
76 > export function computeFractionPresentIn(chunk: string, currentText: string): number {
77 const n = 4;
78 if (chunk.length === 0) {
87 return computeFractionPresentInSet(chunk, buildNGramSet(currentText, n), n);
88 }
90 > /** Builds the set of length-`n` substrings of `text`. */
91 function buildNGramSet(text: string, n: number): Set<string> {
92 const set = new Set<string>();
96 return set;
97 }
99 > /**
100 > * {@link computeFractionPresentIn} with a precomputed file n-gram set.
101 > * `chunk.length >= n` is the caller's responsibility; short/empty
102 > * chunks are handled by {@link computeFractionPresentIn}.
103 > */
104 function computeFractionPresentInSet(chunk: string, fileNGrams: ReadonlySet<string>, n: number): number {
105 const total = chunk.length - n + 1;
112 return present / total;
113 }
115 > /**
116 > * Length-weighted average of {@link computeFractionPresentIn} across
117 > * multiple AI-written chunks. The weight is the chunk's n-gram count
118 > * (approx its character length), so a 200-char chunk counts ~10x as much
119 > * as a 20-char chunk. Returns 0 when there are no chunks (callers
120 > * should branch on that and fall back to whole-file scoring).
121 > *
122 > * Builds the file n-gram set exactly once and reuses it for every
123 > * chunk, so cost is O(|currentText| + sum(|chunk|)) rather than
124 > * O(|chunks| × |currentText|).
125 > */
126 > export function computeChunkedFourGramSurvival(aiChunks: readonly string[], currentText: string): number {
127 if (aiChunks.length === 0) {
128 return 0;
154 return weightedSum / totalWeight;
155 }
157 > /**
158 > * Result of {@link computeWholeFileEditSurvival}.
159 > */
160 > export interface IEditSurvivalScore {
161 > /**
162 > * 4-gram similarity between the current file content and the
163 > * text the AI wrote. 1 = current text is identical to AI text,
164 > * 0 = nothing in common.
165 > */
166 > readonly fourGram: number;
167 > /**
168 > * 1 minus the fraction by which the user moved the text back
169 > * toward the original. 1 = no revert (user kept or refined AI
170 > * output), 0 = full revert to original.
171 > */
172 > readonly noRevert: number;
173 > }
174 >
175 > /**
176 > * Computes the whole-file revert score. 1 = file did not move back
177 > * toward the original, 0 = file is back to the original. Used by both
178 > * the whole-file and the chunked code paths, since revert detection is
179 > * intrinsically a whole-file question (we want to know whether the
180 > * user undid the change, not whether each AI-written region is still
181 > * present).
182 > */
183 > export function computeNoRevertScore(beforeText: string, afterText: string, currentText: string): number {
184 const aiSimilarity = compute4GramTextSimilarity(afterText, beforeText);
185 if (aiSimilarity === 1) {
191 return 1 - Math.max(userSimilarity - aiSimilarity, 0) / (1 - aiSimilarity);
192 }
194 > /**
195 > * Computes survival scores for a whole-file edit.
196 > *
197 > * @param beforeText - File content before the AI edit was applied.
198 > * @param afterText - File content the AI wrote.
199 > * @param currentText - File content right now.
200 > */
201 > export function computeWholeFileEditSurvival(
202 beforeText: string,
203 afterText: string,
209 };
210 }
212 > /**
213 > * Computes survival scores for an edit when we know the explicit
214 > * AI-written chunks. `fourGram` uses the chunked, search-within scoring
215 > * so the denominator is bounded by the AI's written text (immune to
216 > * file-growth artifacts); `noRevert` continues to use the whole-file
217 > * comparison so reverts are still detectable.
218 > *
219 > * Falls back to whole-file scoring when `aiChunks` is empty (e.g. tool
220 > * input was unrecognised or malformed) so callers can pass through
221 > * uniformly.
222 > */
223 > export function computeChunkedEditSurvival(
224 beforeText: string,
225 afterText: string,
src/vs/platform/telemetry/common/telemetry.ts 115 covered LOC · 3 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- telemetry.ts
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 > import { createDecorator } from '../../instantiation/common/instantiation.js';
7 > import { ClassifiedEvent, IGDPRProperty, OmitMetadata, StrictPropertyCheck } from './gdprTypings.js';
8 >
9 > export const ITelemetryService = createDecorator<ITelemetryService>('telemetryService');
10 >
11 > export interface ITelemetryData {
12 > from?: string;
13 > target?: string;
14 > [key: string]: string | unknown | undefined;
15 > }
16 >
17 > export interface ITelemetryService {
18 >
19 > readonly _serviceBrand: undefined;
20 >
21 > readonly telemetryLevel: TelemetryLevel;
22 >
23 > readonly sessionId: string;
24 > readonly machineId: string;
25 > readonly sqmId: string;
26 > readonly devDeviceId: string;
27 > readonly firstSessionDate: string;
28 > readonly msftInternal?: boolean;
29 >
30 > /**
31 > * Whether error telemetry will get sent. If false, `publicLogError` will no-op.
32 > */
33 > readonly sendErrorTelemetry: boolean;
34 >
35 > /**
36 > * @deprecated Use publicLog2 and the typescript GDPR annotation where possible
37 > */
38 > publicLog(eventName: string, data?: ITelemetryData): void;
39 >
40 > /**
41 > * Sends a telemetry event that has been privacy approved.
42 > * Do not call this unless you have been given approval.
43 > */
44 > publicLog2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>): void;
45 >
46 > /**
47 > * @deprecated Use publicLogError2 and the typescript GDPR annotation where possible
48 > */
49 > publicLogError(errorEventName: string, data?: ITelemetryData): void;
50 >
51 > publicLogError2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>): void;
52 >
53 > setExperimentProperty(name: string, value: string): void;
54 >
55 > /**
56 > * Sets a common property that will be attached to all telemetry events.
57 > * Common properties are added after PII cleaning and cannot be overridden by event data.
58 > */
59 > setCommonProperty(name: string, value: string | boolean): void;
60 > }
61 >
62 > export function telemetryLevelEnabled(service: ITelemetryService, level: TelemetryLevel): boolean {
63 return service.telemetryLevel >= level;
64 }
66 > /**
67 > * Replaces `/` and `\` with `|` in model identifiers to prevent the
68 > * telemetry pipeline from redacting them as file paths.
69 > */
70 > export function escapeModelIdForTelemetry(modelId: string | undefined): string | undefined {
71 return modelId?.replace(/[\/\\]/g, '|');
72 }
74 > export interface ITelemetryEndpoint {
75 > id: string;
76 > aiKey: string;
77 > sendErrorTelemetry: boolean;
78 > }
79 >
80 > export const ICustomEndpointTelemetryService = createDecorator<ICustomEndpointTelemetryService>('customEndpointTelemetryService');
81 >
82 > export interface ICustomEndpointTelemetryService {
83 > readonly _serviceBrand: undefined;
84 >
85 > publicLog(endpoint: ITelemetryEndpoint, eventName: string, data?: ITelemetryData): void;
86 > publicLogError(endpoint: ITelemetryEndpoint, errorEventName: string, data?: ITelemetryData): void;
87 > }
88 >
89 > // Keys
90 > export const currentSessionDateStorageKey = 'telemetry.currentSessionDate';
91 > export const firstSessionDateStorageKey = 'telemetry.firstSessionDate';
92 > export const lastSessionDateStorageKey = 'telemetry.lastSessionDate';
93 > export const machineIdKey = 'telemetry.machineId';
94 > export const sqmIdKey = 'telemetry.sqmId';
95 > export const devDeviceIdKey = 'telemetry.devDeviceId';
96 >
97 > // Configuration Keys
98 > export const TELEMETRY_SECTION_ID = 'telemetry';
99 > export const TELEMETRY_SETTING_ID = 'telemetry.telemetryLevel';
100 > export const TELEMETRY_CRASH_REPORTER_SETTING_ID = 'telemetry.enableCrashReporter';
101 > export const TELEMETRY_OLD_SETTING_ID = 'telemetry.enableTelemetry';
102 >
103 > export const enum TelemetryLevel {
104 > NONE = 0,
105 > CRASH = 1,
106 > ERROR = 2,
107 > USAGE = 3
108 > }
109 >
110 > export const enum TelemetryConfiguration {
111 > OFF = 'off',
112 > CRASH = 'crash',
113 > ERROR = 'error',
114 > ON = 'all'
115 > }
116 >
117 > export interface ICommonProperties {
118 > [name: string]: string | boolean | undefined;
119 > }
src/vs/base/common/observableInternal/observables/derivedImpl.ts 112 covered LOC · 23 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- derivedImpl.ts
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 > import { IObservable, IObservableWithChange, IObserver, IReaderWithStore, ISettableObservable, ITransaction, } from '../base.js';
7 > import { BaseObservable } from './baseObservable.js';
8 > import { DebugNameData } from '../debugName.js';
9 > import { BugIndicatingError, DisposableStore, EqualityComparer, assertFn, onBugIndicatingError } from '../commonFacade/deps.js';
10 > import { getLogger } from '../logging/logging.js';
11 > import { IChangeTracker } from '../changeTracker.js';
12 > import { DebugLocation } from '../debugLocation.js';
13 >
14 > export interface IDerivedReader<TChange = void> extends IReaderWithStore {
15 > /**
16 > * Call this to report a change delta or to force report a change, even if the new value is the same as the old value.
17 > */
18 > reportChange(change: TChange): void;
19 > }
20 >
21 > export const enum DerivedState {
22 > /** Initial state, no previous value, recomputation needed */
23 > initial = 0,
24 >
25 > /**
26 > * A dependency could have changed.
27 > * We need to explicitly ask them if at least one dependency changed.
28 > */
29 > dependenciesMightHaveChanged = 1,
30 >
31 > /**
32 > * A dependency changed and we need to recompute.
33 > * After recomputation, we need to check the previous value to see if we changed as well.
34 > */
35 > stale = 2,
36 >
37 > /**
38 > * No change reported, our cached value is up to date.
39 > */
40 > upToDate = 3,
41 > }
42 >
43 function derivedStateToString(state: DerivedState): string {
44 switch (state) {
50 }
51 }
53 > export class Derived<T, TChangeSummary = any, TChange = void> extends BaseObservable<T, TChange> implements IDerivedReader<TChange>, IObserver {
54 > private _state = DerivedState.initial;
55 > private _value: T | undefined = undefined;
56 > private _updateCount = 0;
57 > private _dependencies = new Set<IObservable<any>>();
58 > private _dependenciesToBeRemoved = new Set<IObservable<any>>();
59 > private _changeSummary: TChangeSummary | undefined = undefined;
60 > private _isUpdating = false;
61 > private _isComputing = false;
62 > private _didReportChange = false;
63 > private _isInBeforeUpdate = false;
64 > private _isReaderValid = false;
65 > private _store: DisposableStore | undefined = undefined;
66 > private _delayedStore: DisposableStore | undefined = undefined;
67 > private _removedObserverToCallEndUpdateOn: Set<IObserver> | null = null;
68 >
69 > public override get debugName(): string {
70 > return this._debugNameData.getDebugName(this) ?? '(anonymous)';
71 > }
72 >
73 > constructor(
74 public readonly _debugNameData: DebugNameData,
75 public readonly _computeFn: (reader: IDerivedReader<TChange>, changeSummary: TChangeSummary) => T,
82 this._changeSummary = this._changeTracker?.createChangeSummary(undefined);
83 }
85 > protected override onLastObserverRemoved(): void {
86 /**
87 * We are not tracking changes anymore, thus we have to assume
107 this._handleLastObserverRemoved?.();
108 }
110 > public override get(): T {
111 const checkEnabled = false; // TODO set to true
112 if (this._isComputing && checkEnabled) {
164 }
165 }
167 > private _recompute() {
168 let didChange = false;
169 this._isComputing = true;
238 }
239 }
241 > public override toString(): string {
242 return `LazyDerived<${this.debugName}>`;
243 }
245 > // IObserver Implementation
246 >
247 > public beginUpdate<T>(_observable: IObservable<T>): void {
248 if (this._isUpdating) {
249 throw new BugIndicatingError('Cyclic deriveds are not supported yet!');
272 }
273 }
275 > public endUpdate<T>(_observable: IObservable<T>): void {
276 this._updateCount--;
277 if (this._updateCount === 0) {
291 assertFn(() => this._updateCount >= 0);
292 }
294 > public handlePossibleChange<T>(observable: IObservable<T>): void {
295 // In all other states, observers already know that we might have changed.
296 if (this._state === DerivedState.upToDate && this._dependencies.has(observable) && !this._dependenciesToBeRemoved.has(observable)) {
301 }
302 }
304 > public handleChange<T, TChange>(observable: IObservableWithChange<T, TChange>, change: TChange): void {
305 if (this._dependencies.has(observable) && !this._dependenciesToBeRemoved.has(observable) || this._isInBeforeUpdate) {
306 getLogger()?.handleDerivedDependencyChanged(this, observable, change);
329 }
330 }
332 > // IReader Implementation
333 >
334 > private _ensureReaderValid(): void {
335 if (!this._isReaderValid) { throw new BugIndicatingError('The reader object cannot be used outside its compute function!'); }
336 }
338 > public readObservable<T>(observable: IObservable<T>): T {
339 this._ensureReaderValid();
340
348 return value;
349 }
351 > public reportChange(change: TChange): void {
352 this._ensureReaderValid();
353
358 }
359 }
361 > get store(): DisposableStore {
362 this._ensureReaderValid();
363
367 return this._store;
368 }
370 > get delayedStore(): DisposableStore {
371 this._ensureReaderValid();
372
376 return this._delayedStore;
377 }
379 > public override addObserver(observer: IObserver): void {
380 const shouldCallBeginUpdate = !this._observers.has(observer) && this._updateCount > 0;
381 super.addObserver(observer);
387 }
388 }
390 > public override removeObserver(observer: IObserver): void {
391 if (this._observers.has(observer) && this._updateCount > 0) {
392 if (!this._removedObserverToCallEndUpdateOn) {
397 super.removeObserver(observer);
398 }
400 > public debugGetState() {
401 return {
402 state: this._state,
408 };
409 }
411 > public debugSetValue(newValue: unknown) {
412 // eslint-disable-next-line local/code-no-any-casts
413 this._value = newValue as any;
414 }
416 > public debugRecompute(): void {
417 this.beginUpdate(this);
418 try {
426 }
427 }
429 > public setValue(newValue: T, tx: ITransaction, change: TChange): void {
430 this._value = newValue;
431 const observers = this._observers;
435 }
436 }
437 > } derivedImpl.ts
438 >
439 >
440 > export class DerivedWithSetter<T, TChangeSummary = any, TOutChanges = any> extends Derived<T, TChangeSummary, TOutChanges> implements ISettableObservable<T, TOutChanges> {
441 > constructor(
442 debugNameData: DebugNameData,
443 computeFn: (reader: IDerivedReader<TOutChanges>, changeSummary: TChangeSummary) => T,
src/vs/base/common/processes.ts 112 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- processes.ts
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 > import { IProcessEnvironment, isLinux } from './platform.js';
7 >
8 > /**
9 > * Options to be passed to the external program or shell.
10 > */
11 > export interface CommandOptions {
12 > /**
13 > * The current working directory of the executed program or shell.
14 > * If omitted VSCode's current workspace root is used.
15 > */
16 > cwd?: string;
17 >
18 > /**
19 > * The environment of the executed program or shell. If omitted
20 > * the parent process' environment is used.
21 > */
22 > env?: { [key: string]: string };
23 > }
24 >
25 > export interface Executable {
26 > /**
27 > * The command to be executed. Can be an external program or a shell
28 > * command.
29 > */
30 > command: string;
31 >
32 > /**
33 > * Specifies whether the command is a shell command and therefore must
34 > * be executed in a shell interpreter (e.g. cmd.exe, bash, ...).
35 > */
36 > isShellCommand: boolean;
37 >
38 > /**
39 > * The arguments passed to the command.
40 > */
41 > args: string[];
42 >
43 > /**
44 > * The command options used when the command is executed. Can be omitted.
45 > */
46 > options?: CommandOptions;
47 > }
48 >
49 > export interface ForkOptions extends CommandOptions {
50 > execArgv?: string[];
51 > }
52 >
53 > export const enum Source {
54 > stdout,
55 > stderr
56 > }
57 >
58 > /**
59 > * The data send via a success callback
60 > */
61 > export interface SuccessData {
62 > error?: Error;
63 > cmdCode?: number;
64 > terminated?: boolean;
65 > }
66 >
67 > /**
68 > * The data send via a error callback
69 > */
70 > export interface ErrorData {
71 > error?: Error;
72 > terminated?: boolean;
73 > stdout?: string;
74 > stderr?: string;
75 > }
76 >
77 > export interface TerminateResponse {
78 > success: boolean;
79 > code?: TerminateResponseCode;
80 > error?: any;
81 > }
82 >
83 > export const enum TerminateResponseCode {
84 > Success = 0,
85 > Unknown = 1,
86 > AccessDenied = 2,
87 > ProcessNotFound = 3,
88 > }
89 >
90 > export interface ProcessItem {
91 > name: string;
92 > cmd: string;
93 > pid: number;
94 > ppid: number;
95 > load: number;
96 > mem: number;
97 >
98 > children?: ProcessItem[];
99 > }
100 >
101 > /**
102 > * Sanitizes a VS Code process environment by removing all Electron/VS Code-related values.
103 > */
104 > export function sanitizeProcessEnvironment(env: IProcessEnvironment, ...preserve: string[]): void {
105 const set = preserve.reduce<Record<string, boolean>>((set, key) => {
106 set[key] = true;
125 });
126 }
127 > processes.ts
128 > /**
129 > * Remove dangerous environment variables that have caused crashes
130 > * in forked processes (i.e. in ELECTRON_RUN_AS_NODE processes)
131 > *
132 > * @param env The env object to change
133 > */
134 > export function removeDangerousEnvVariables(env: IProcessEnvironment | undefined): void {
135 if (!env) {
136 return;
src/vs/platform/agentHost/common/agentHostCheckpointService.ts 112 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostCheckpointService.ts
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 > import { URI } from '../../../base/common/uri.js';
7 > import { createDecorator } from '../../instantiation/common/instantiation.js';
8 >
9 > export const IAgentHostCheckpointService = createDecorator<IAgentHostCheckpointService>('agentHostCheckpointService');
10 >
11 > /**
12 > * `session_metadata` key under which the per-session baseline (turn/0)
13 > * checkpoint ref is stored.
14 > */
15 > export const META_CHECKPOINT_BASE_REF = 'checkpoint.baseRef';
16 >
17 > /**
18 > * Returns the canonical name for a per-turn checkpoint ref.
19 > * Distinct from the chat extension's `refs/sessions/...` so the two can
20 > * coexist safely in the same repository.
21 > */
22 > export function buildCheckpointRefName(sanitizedSessionId: string, turnNumber: number): string {
23 return `refs/agents/${sanitizedSessionId}/checkpoints/turn/${turnNumber}`;
24 }
26 > /**
27 > * Captures per-turn git **checkpoint refs** for Agent Host sessions so
28 > * end-of-turn diffs reflect the entire working-tree delta (including
29 > * terminal-tool edits that are invisible to the FileEditTracker pipeline).
30 > *
31 > * Each checkpoint is a parentless or parent-chained commit (commit-tree)
32 > * pointing at a tree captured via the temp-index trick, anchored under
33 > * `refs/agents/<sid>/checkpoints/turn/<N>`. The session-private ref
34 > * namespace means the commits stay reachable for the lifetime of the
35 > * session and survive process restarts (refs live on disk in
36 > * `<repo>/.git/refs/`), while never appearing as branches/tags to the
37 > * user. Cleanup is driven by `ISessionDataService.onWillDeleteSessionData`
38 > * — the service deletes every ref it created for the destroyed session
39 > * before the data directory is removed.
40 > */
41 > export interface IAgentHostCheckpointService {
42 > readonly _serviceBrand: undefined;
43 >
44 > /**
45 > * Captures the session's baseline (turn/0) checkpoint. Idempotent: if
46 > * a baseline already exists for the session, returns the existing ref.
47 > * Returns `undefined` when the working directory is not a git work tree
48 > * (folder-isolation against a non-git folder) or when checkpoint
49 > * capture fails.
50 > *
51 > * Called once per session, immediately after the session's working
52 > * directory has been resolved and any worktree metadata has been
53 > * persisted (e.g. `CopilotAgent._materializeProvisional`).
54 > */
55 > captureBaseline(sessionUri: URI, workingDirectory: URI | undefined): Promise<string | undefined>;
56 >
57 > /**
58 > * Captures an end-of-turn checkpoint, chained to the previous turn's
59 > * checkpoint (or the baseline for turn 1). Persists the ref against
60 > * the turn via `ISessionDatabase.setTurnCheckpointRef`. Returns
61 > * `undefined` when the session is not git-backed, the baseline is
62 > * missing, or capture fails.
63 > *
64 > * If the captured tree OID matches the parent's tree OID (no-op turn)
65 > * the parent ref is recorded against the turn rather than creating a
66 > * redundant commit / new ref.
67 > *
68 > * Called from `AgentSideEffects` when a `ChatTurnComplete` action
69 > * fires, BEFORE the changeset service's `onTurnComplete` hook so the
70 > * per-turn changeset compute can pick up the new refs.
71 > */
72 > captureTurnCheckpoint(sessionUri: URI, turnId: string): Promise<string | undefined>;
73 >
74 > /**
75 > * Returns the `{ parent, current }` checkpoint refs for a turn, or
76 > * `undefined` when either is missing. Used by the changeset service
77 > * to decide whether to take the git-diff fast path for per-turn diffs.
78 > */
79 > getTurnCheckpointPair(sessionUri: URI, turnId: string): Promise<{ parent: string; current: string } | undefined>;
80 >
81 > /**
82 > * Returns the session's baseline checkpoint ref, or `undefined` when
83 > * the baseline was never captured (non-git-backed session, or capture
84 > * failed). Used by the changeset service to resolve compare-turns
85 > * URIs whose `originalTurnId` is the `BASELINE_TURN_ID` sentinel.
86 > */
87 > getBaselineCheckpointRef(sessionUri: URI): Promise<string | undefined>;
88 >
89 > /**
90 > * Deletes every checkpoint ref this service created for the session
91 > * (baseline + all turn refs), reading the precise list from the
92 > * session database. Tolerates missing refs.
93 > *
94 > * Called from a subscriber to `ISessionDataService.onWillDeleteSessionData`
95 > * before the session's data directory is removed.
96 > */
97 > disposeSessionData(sessionUri: URI): Promise<void>;
98 > }
99 >
100 > /**
101 > * A no-op implementation of {@link IAgentHostCheckpointService} used as a
102 > * fallback in test fixtures that don't exercise checkpoint capture, and
103 > * as the default value for the optional `_checkpointService` parameter
104 > * on `AgentService` so existing test callsites keep compiling without
105 > * forced fixture updates.
106 > */
107 > export const NULL_CHECKPOINT_SERVICE: IAgentHostCheckpointService = {
108 > _serviceBrand: undefined,
109 > captureBaseline: async () => undefined,
110 > captureTurnCheckpoint: async () => undefined,
111 > getTurnCheckpointPair: async () => undefined,
112 > getBaselineCheckpointRef: async () => undefined,
113 > disposeSessionData: async () => { },
114 > };
src/vs/base/common/equals.ts 110 covered LOC · 15 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- equals.ts
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 > import * as arrays from './arrays.js';
7 >
8 > /*
9 > * Each function in this file which offers an equality comparison, has an accompanying
10 > * `*C` variant which returns an EqualityComparer function.
11 > *
12 > * The `*C` variant allows for easier composition of equality comparers and improved type-inference.
13 > */
14 >
15 >
16 > /** Represents a function that decides if two values are equal. */
17 > export type EqualityComparer<T> = (a: T, b: T) => boolean;
18 >
19 > export interface IEquatable<T> {
20 > equals(other: T): boolean;
21 > }
22 >
23 > /**
24 > * Compares two items for equality using strict equality.
25 > */
26 > export function strictEquals<T>(a: T, b: T): boolean {
27 > return a === b; equals.ts
28 > }
29 > equals.ts
30 > export function strictEqualsC<T>(): EqualityComparer<T> {
31 return (a, b) => a === b;
32 }
33 > equals.ts
34 > /**
35 > * Checks if the items of two arrays are equal.
36 > * By default, strict equality is used to compare elements, but a custom equality comparer can be provided.
37 > */
38 > export function arrayEquals<T>(a: readonly T[], b: readonly T[], itemEquals?: EqualityComparer<T>): boolean {
39 return arrays.equals(a, b, itemEquals ?? strictEquals);
40 }
41 > equals.ts
42 > /**
43 > * Checks if the items of two arrays are equal.
44 > * By default, strict equality is used to compare elements, but a custom equality comparer can be provided.
45 > */
46 > export function arrayEqualsC<T>(itemEquals?: EqualityComparer<T>): EqualityComparer<readonly T[]> {
47 return (a, b) => arrays.equals(a, b, itemEquals ?? strictEquals);
48 }
49 > equals.ts
50 > /**
51 > * Drills into arrays (items ordered) and objects (keys unordered) and uses strict equality on everything else.
52 > */
53 > export function structuralEquals<T>(a: T, b: T): boolean {
54 if (a === b) {
55 return true;
95 return false;
96 }
97 > equals.ts
98 > export function structuralEqualsC<T>(): EqualityComparer<T> {
99 return (a, b) => structuralEquals(a, b);
100 }
101 > equals.ts
102 > /**
103 > * `getStructuralKey(a) === getStructuralKey(b) <=> structuralEquals(a, b)`
104 > * (assuming that a and b are not cyclic structures and nothing extends globalThis Array).
105 > */
106 > export function getStructuralKey(t: unknown): string {
107 return JSON.stringify(toNormalizedJsonStructure(t));
108 }
109 > equals.ts
110 > let objectId = 0;
111 > const objIds = new WeakMap<object, number>();
112 >
113 function toNormalizedJsonStructure(t: unknown): unknown {
114 if (Array.isArray(t)) {
136 return t;
137 }
138 > equals.ts
139 >
140 > /**
141 > * Two items are considered equal, if their stringified representations are equal.
142 > */
143 > export function jsonStringifyEquals<T>(a: T, b: T): boolean {
144 return JSON.stringify(a) === JSON.stringify(b);
145 }
146 > equals.ts
147 > /**
148 > * Two items are considered equal, if their stringified representations are equal.
149 > */
150 > export function jsonStringifyEqualsC<T>(): EqualityComparer<T> {
151 return (a, b) => JSON.stringify(a) === JSON.stringify(b);
152 }
153 > equals.ts
154 > /**
155 > * Uses `item.equals(other)` to determine equality.
156 > */
157 > export function thisEqualsC<T extends IEquatable<T>>(): EqualityComparer<T> {
158 return (a, b) => a.equals(b);
159 }
160 > equals.ts
161 > /**
162 > * Checks if two items are both null or undefined, or are equal according to the provided equality comparer.
163 > */
164 > export function equalsIfDefined<T>(v1: T | undefined | null, v2: T | undefined | null, equals: EqualityComparer<T>): boolean {
165 if (v1 === undefined || v1 === null || v2 === undefined || v2 === null) {
166 return v2 === v1;
168 return equals(v1, v2);
169 }
170 > equals.ts
171 > /**
172 > * Returns an equality comparer that checks if two items are both null or undefined, or are equal according to the provided equality comparer.
173 > */
174 > export function equalsIfDefinedC<T>(equals: EqualityComparer<T>): EqualityComparer<T | undefined | null> {
175 return (v1, v2) => {
176 if (v1 === undefined || v1 === null || v2 === undefined || v2 === null) {
180 };
181 }
182 > equals.ts
183 > /**
184 > * Each function in this file which offers an equality comparison, has an accompanying
185 > * `*C` variant which returns an EqualityComparer function.
186 > *
187 > * The `*C` variant allows for easier composition of equality comparers and improved type-inference.
188 > */
189 > export namespace equals {
190 > export const strict = strictEquals;
191 > export const strictC = strictEqualsC;
192 >
193 > export const array = arrayEquals;
194 > export const arrayC = arrayEqualsC;
195 >
196 > export const structural = structuralEquals;
197 > export const structuralC = structuralEqualsC;
198 >
199 > export const jsonStringify = jsonStringifyEquals;
200 > export const jsonStringifyC = jsonStringifyEqualsC;
201 >
202 > export const thisC = thisEqualsC;
203 >
204 > export const ifDefined = equalsIfDefined;
205 > export const ifDefinedC = equalsIfDefinedC;
206 > }
src/vs/platform/agentHost/common/meta/agentFeedbackAnnotations.ts 108 covered LOC · 5 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentFeedbackAnnotations.ts
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 > import type { Mutable } from '../../../../base/common/types.js';
7 > import type { Annotation } from '../state/protocol/state.js';
8 >
9 > /**
10 > * Shared convention for carrying agent-feedback semantics inside an
11 > * {@link Annotation._meta} on the agent host annotations channel.
12 > *
13 > * Feedback items round-trip as annotations on `<session>/annotations`; the
14 > * annotation's own fields cover id / resource / range / resolved, and
15 > * everything else (lifecycle state, origin kind, code context, PR linkage)
16 > * lives under {@link FEEDBACK_ANNOTATION_META_KEY}. This module is the single
17 > * place both the server (agent host, which writes feedback annotations from
18 > * its server tools) and the client (agents window, which reads them back)
19 > * agree on the key and shape, so the two sides cannot drift.
20 > */
21 >
22 > /** Namespaced key under {@link Annotation._meta} carrying feedback semantics. */
23 > export const FEEDBACK_ANNOTATION_META_KEY = 'vscode.agentFeedback';
24 >
25 > /**
26 > * Name of the agent host server tool that reveals review comments the user has
27 > * not accepted yet. Shared here (in the layer-neutral `common` module) so the
28 > * node-side server tool implementation and the browser-side chat adapter that
29 > * renders its confirmation agree on the name without drifting. The agent sees
30 > * this name directly (Copilot) or prefixed as `mcp__host__<name>` (Claude).
31 > */
32 > export const VIEW_UNREVIEWED_COMMENTS_TOOL_NAME = 'viewUnreviewedComments';
33 >
34 > /**
35 > * Name of the agent host server tool that adds a comment (agent feedback) to a
36 > * file range. Shared here (in the layer-neutral `common` module) so the
37 > * node-side server tool implementation and the browser-side chat adapter that
38 > * renders its tool call agree on the name without drifting. The agent sees this
39 > * name directly (Copilot) or prefixed as `mcp__host__<name>` (Claude).
40 > */
41 > export const ADD_COMMENT_TOOL_NAME = 'addComment';
42 >
43 > /**
44 > * Whether {@link toolName} (a tool name as seen on a tool call) refers to the
45 > * {@link VIEW_UNREVIEWED_COMMENTS_TOOL_NAME} server tool. Accepts both the bare
46 > * name and the Claude `mcp__<server>__<name>` prefixed form.
47 > */
48 > export function isViewUnreviewedCommentsTool(toolName: string): boolean {
49 return toolName === VIEW_UNREVIEWED_COMMENTS_TOOL_NAME || toolName.endsWith(`__${VIEW_UNREVIEWED_COMMENTS_TOOL_NAME}`);
50 }
52 > /**
53 > * Whether {@link toolName} (a tool name as seen on a tool call) refers to the
54 > * {@link ADD_COMMENT_TOOL_NAME} server tool. Accepts both the bare name and the
55 > * Claude `mcp__<server>__<name>` prefixed form.
56 > */
57 > export function isAddCommentTool(toolName: string): boolean {
58 return toolName === ADD_COMMENT_TOOL_NAME || toolName.endsWith(`__${ADD_COMMENT_TOOL_NAME}`);
59 }
61 > /**
62 > * Origin of a feedback item. String values match the client-side
63 > * `AgentFeedbackKind` enum so a value written by either side decodes on the
64 > * other without translation.
65 > */
66 > export type AgentFeedbackKindValue = 'user' | 'codeReview' | 'prReview';
67 >
68 > /**
69 > * Lifecycle state of a feedback item. String values match the client-side
70 > * `AgentFeedbackState` enum.
71 > */
72 > export type AgentFeedbackStateValue = 'created' | 'accepted' | 'submitted' | 'resolved';
73 >
74 > /**
75 > * Feedback semantics carried in an annotation's {@link Annotation._meta}.
76 > *
77 > * The optional client-only fields ({@link suggestion}, {@link codeSelection},
78 > * {@link diffHunks}, {@link sourcePRReviewCommentId}) are populated when a
79 > * feedback item is converted from a code- or PR-review comment on the client;
80 > * server tools only ever write {@link kind} / {@link state} /
81 > * {@link sessionResource}. {@link suggestion} is typed loosely here because
82 > * its concrete shape lives in the client (sessions) layer.
83 > */
84 > export interface IFeedbackAnnotationMeta {
85 > readonly kind: AgentFeedbackKindValue;
86 > readonly state: AgentFeedbackStateValue;
87 > readonly sessionResource: string;
88 > readonly suggestion?: unknown;
89 > readonly codeSelection?: string;
90 > readonly diffHunks?: string;
91 > readonly sourcePRReviewCommentId?: string;
92 > /**
93 > * Transient marker set by the client when the user reveals this comment to
94 > * the agent via the `viewUnreviewedComments` tool. The server tool returns
95 > * exactly the comments carrying this flag (so the result is scoped to the
96 > * comments selected for that invocation rather than every accepted review
97 > * comment) and clears it once they have been delivered, so a later
98 > * invocation does not re-return them.
99 > */
100 > readonly pendingAgentReveal?: boolean;
101 > }
102 >
103 function isAgentFeedbackKindValue(value: unknown): value is AgentFeedbackKindValue {
104 return value === 'user' || value === 'codeReview' || value === 'prReview';
105 }
107 function isAgentFeedbackStateValue(value: unknown): value is AgentFeedbackStateValue {
108 return value === 'created' || value === 'accepted' || value === 'submitted' || value === 'resolved';
109 }
111 > /**
112 > * Reads the well-known {@link IFeedbackAnnotationMeta} from an annotation's
113 > * `_meta` bag (under {@link FEEDBACK_ANNOTATION_META_KEY}). The annotations
114 > * channel is shared, so this validates the required `kind` / `state` /
115 > * `sessionResource` fields and returns `undefined` for annotations that aren't
116 > * feedback items. Read through this rather than casting the namespaced slot.
117 > */
118 > export function readFeedbackAnnotationMeta(annotation: Annotation): IFeedbackAnnotationMeta | undefined {
119 const meta = annotation._meta;
120 const slot = meta?.[FEEDBACK_ANNOTATION_META_KEY];
src/vs/base/common/cancellation.ts 107 covered LOC · 28 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- cancellation.ts
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 > import { Emitter, Event } from './event.js';
7 > import { DisposableStore, IDisposable } from './lifecycle.js';
8 >
9 > export interface CancellationToken {
10 >
11 > /**
12 > * A flag signalling is cancellation has been requested.
13 > */
14 > readonly isCancellationRequested: boolean;
15 >
16 > /**
17 > * An event which fires when cancellation is requested. This event
18 > * only ever fires `once` as cancellation can only happen once. Listeners
19 > * that are registered after cancellation will be called (next event loop run),
20 > * but also only once.
21 > *
22 > * @event
23 > */
24 > readonly onCancellationRequested: (listener: (e: void) => unknown, thisArgs?: unknown, disposables?: IDisposable[]) => IDisposable;
25 > }
26 >
27 > const shortcutEvent: Event<void> = Object.freeze(function (callback, context?): IDisposable {
28 const handle = setTimeout(callback.bind(context), 0);
29 return { dispose() { clearTimeout(handle); } };
30 });
32 > export namespace CancellationToken {
33 >
34 > export function isCancellationToken(thing: unknown): thing is CancellationToken {
35 if (thing === CancellationToken.None || thing === CancellationToken.Cancelled) {
36 return true;
45 && typeof (thing as CancellationToken).onCancellationRequested === 'function';
46 }
48 >
49 > export const None = Object.freeze<CancellationToken>({
50 > isCancellationRequested: false,
51 > onCancellationRequested: Event.None
52 > });
53 >
54 > export const Cancelled = Object.freeze<CancellationToken>({
55 > isCancellationRequested: true,
56 > onCancellationRequested: shortcutEvent
57 > });
58 > }
59 >
60 class MutableToken implements CancellationToken {
61
62 private _isCancelled: boolean = false;
63 private _emitter: Emitter<void> | null = null;
65 > public cancel() {
66 if (!this._isCancelled) {
67 this._isCancelled = true;
72 }
73 }
75 > get isCancellationRequested(): boolean {
76 return this._isCancelled;
77 }
79 > get onCancellationRequested(): Event<void> {
80 if (this._isCancelled) {
81 return shortcutEvent;
86 return this._emitter.event;
87 }
89 > public dispose(): void {
90 if (this._emitter) {
91 this._emitter.dispose();
93 }
94 }
96 >
97 > export class CancellationTokenSource {
98 >
99 > private _token?: CancellationToken = undefined;
100 > private _parentListener?: IDisposable = undefined;
101 >
102 > constructor(parent?: CancellationToken) {
103 > this._parentListener = parent && parent.onCancellationRequested(this.cancel, this); cancellation.ts
104 > }
106 > get token(): CancellationToken {
107 if (!this._token) {
108 // be lazy and create the token only when
112 return this._token;
113 }
115 > cancel(): void {
116 > if (!this._token) { cancellation.ts
117 > // save an object by returning the default cancellation.ts
118 > // cancelled token when cancellation happens
119 > // before someone asks for the token
120 > this._token = CancellationToken.Cancelled;
121 >
122 > } else if (this._token instanceof MutableToken) { cancellation.ts
123 // actually cancel
124 this._token.cancel();
125 }
126 > } cancellation.ts
128 > dispose(cancel: boolean = false): void {
129 > if (cancel) { cancellation.ts
130 > this.cancel(); cancellation.ts
131 > }
132 > this._parentListener?.dispose(); cancellation.ts
133 > if (!this._token) {
134 // ensure to initialize with an empty token if we had none
135 this._token = CancellationToken.None;
136
137 > } else if (this._token instanceof MutableToken) { cancellation.ts
138 // actually dispose
139 this._token.dispose();
140 }
141 > } cancellation.ts
142 > } cancellation.ts
143 >
144 > export function cancelOnDispose(store: DisposableStore): CancellationToken {
145 const source = new CancellationTokenSource();
146 store.add({ dispose() { source.cancel(); } });
147 return source.token;
148 }
150 > /**
151 > * A pool that aggregates multiple cancellation tokens. The pool's own token
152 > * (accessible via `pool.token`) is cancelled only after every token added
153 > * to the pool has been cancelled. Adding tokens after the pool token has
154 > * been cancelled has no effect.
155 > */
156 > export class CancellationTokenPool {
157
158 private readonly _source = new CancellationTokenSource();
162 private _cancelled: number = 0;
163 private _isDone: boolean = false;
165 > get token(): CancellationToken {
166 return this._source.token;
167 }
169 > /**
170 > * Add a token to the pool. If the token is already cancelled it is counted
171 > * immediately. Tokens added after the pool token has been cancelled are ignored.
172 > */
173 > add(token: CancellationToken): void {
174 if (this._isDone) {
175 return;
191 this._listeners.add(d);
192 }
194 > private _check(): void {
195 if (!this._isDone && this._total > 0 && this._total === this._cancelled) {
196 this._isDone = true;
src/vs/platform/agentHost/common/state/protocol/channels-changeset/commands.ts 105 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- commands.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > import type { URI, ContentRef, StringOrMarkdown, TextRange } from '../common/state.js';
10 > import type { BaseParams } from '../common/commands.js';
11 >
12 > // ─── invokeChangesetOperation ────────────────────────────────────────────────
13 >
14 > /**
15 > * Discriminator for {@link ChangesetOperationTarget}. Mirrors the
16 > * non-`Changeset` members of {@link ChangesetOperationScope} — the
17 > * `Changeset` scope has no target.
18 > *
19 > * @category Commands
20 > */
21 > export const enum ChangesetOperationTargetKind {
22 > /** Operation acts on a single file. */
23 > Resource = 'resource',
24 > /** Operation acts on a line range within a single file. */
25 > Range = 'range',
26 > }
27 >
28 > /**
29 > * Identifies the file or range a {@link ChangesetOperation} should act on.
30 > *
31 > * The `kind` MUST match one of the operation's declared
32 > * {@link ChangesetOperation.scopes}.
33 > *
34 > * @category Commands
35 > */
36 > export type ChangesetOperationTarget =
37 > | { kind: ChangesetOperationTargetKind.Resource; resource: URI; side?: 'before' | 'after' }
38 > | { kind: ChangesetOperationTargetKind.Range; resource: URI; side?: 'before' | 'after'; range: TextRange };
39 >
40 > /**
41 > * Optional follow-up surfaced by the server after an operation completes —
42 > * a {@link ContentRef} the client can fetch and display.
43 > *
44 > * Set `external` to `true` to open the content in the user's preferred
45 > * external handler (e.g. browser); otherwise the client is expected to
46 > * surface it inline.
47 > *
48 > * @category Commands
49 > */
50 > export interface ChangesetOperationFollowUp {
51 > content: ContentRef;
52 > /** When `true`, open in an external handler rather than inline. */
53 > external?: boolean;
54 > }
55 >
56 > /**
57 > * Invokes a server-defined {@link ChangesetOperation} against a changeset,
58 > * a single file, or a line range.
59 > *
60 > * The server validates that `operationId` exists in the changeset's
61 > * current `operations` list and that the requested `target.kind` is
62 > * contained in the operation's `scopes`. Invalid combinations result in a
63 > * JSON-RPC error.
64 > *
65 > * State changes resulting from invocation flow back through the normal
66 > * `changeset/*` action stream on the relevant changeset URIs. Clients
67 > * SHOULD NOT synthesise local optimistic changes for invocations unless
68 > * the server explicitly opts in via a future capability.
69 > *
70 > * @category Commands
71 > * @method invokeChangesetOperation
72 > * @direction Client → Server
73 > * @messageType Request
74 > * @version 2
75 > */
76 > export interface InvokeChangesetOperationParams extends BaseParams {
77 > /** The expanded changeset URI. */
78 > channel: URI;
79 > /** Matches {@link ChangesetOperation.id} from the changeset's `operations` list. */
80 > operationId: string;
81 > /**
82 > * Target of the operation. Required iff the chosen scope is
83 > * `'resource'` or `'range'`. Omit for changeset-scoped operations.
84 > */
85 > target?: ChangesetOperationTarget;
86 > }
87 >
88 > /**
89 > * Result of the {@link InvokeChangesetOperationParams | `invokeChangesetOperation`}
90 > * command.
91 > *
92 > * Success is implicit: the server returns this result when it accepted
93 > * the operation. Failure is signalled by rejecting the JSON-RPC request
94 > * with an appropriate error code, not by any field on this result. The
95 > * operation MAY still produce subsequent failure feedback through the
96 > * {@link ChangesetStatusChangedAction | `changeset/statusChanged`} stream.
97 > *
98 > * @category Commands
99 > */
100 > export interface InvokeChangesetOperationResult {
101 > /** Optional human-readable message describing the result. */
102 > message?: StringOrMarkdown;
103 > /** Optional follow-up: a URI to open (e.g. a PR), a content ref, etc. */
104 > followUp?: ChangesetOperationFollowUp;
105 > }
src/vs/platform/agentHost/node/agentHostCompletions.ts 105 covered LOC · 7 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostCompletions.ts
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 > import { CancellationToken } from '../../../base/common/cancellation.js';
7 > import { Disposable, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
8 > import { ILogService } from '../../log/common/log.js';
9 > import { createDecorator } from '../../instantiation/common/instantiation.js';
10 > import type { CompletionItem, CompletionItemKind, CompletionsParams, CompletionsResult } from '../common/state/protocol/commands.js';
11 >
12 > export const IAgentHostCompletions = createDecorator<IAgentHostCompletions>('agentHostCompletions');
13 >
14 > /**
15 > * Well-known completion trigger characters announced to clients in the
16 > * `initialize` handshake. Clients SHOULD issue a `completions` request when
17 > * the user types one of these characters in a {@link UserMessage} input.
18 > */
19 > export const enum CompletionTriggerCharacter {
20 > /** File reference, used for `@`-mentions handled by the file completion provider. */
21 > File = '@',
22 > /** File reference, used for `#`-mentions handled by the file completion provider. */
23 > Hash = '#',
24 > /** Leading slash command or skill reference. */
25 > Slash = '/',
26 > }
27 >
28 > /**
29 > * Pluggable provider that contributes {@link CompletionItem}s for one or
30 > * more {@link CompletionItemKind}s.
31 > *
32 > * Providers are registered via {@link IAgentHostCompletions.registerProvider}
33 > * and may be agent-specific (e.g. registered alongside an `IAgent`) or
34 > * generic (e.g. the built-in workspace file completion provider).
35 > */
36 > export interface IAgentHostCompletionItemProvider {
37 > /** Completion kinds this provider handles. Providers are skipped for any other kind. */
38 > readonly kinds: ReadonlySet<CompletionItemKind>;
39 >
40 > /**
41 > * Characters that, when typed by the user, should trigger a request for
42 > * this provider's completions. Aggregated across all registered providers
43 > * and announced to clients via `InitializeResult.completionTriggerCharacters`.
44 > */
45 > readonly triggerCharacters?: readonly string[];
46 >
47 > /**
48 > * Compute completion items for the given input.
49 > *
50 > * Implementations SHOULD respect `token` and return promptly.
51 > * Throwing or rejecting fails this provider only; other providers'
52 > * results are still returned by {@link IAgentHostCompletions.completions}.
53 > */
54 > provideCompletionItems(params: CompletionsParams, token: CancellationToken): Promise<readonly CompletionItem[]>;
55 > }
56 >
57 > /**
58 > * Server-side completions service. Owns a set of pluggable providers and
59 > * fans out a single `completions` request to every provider whose
60 > * {@link IAgentHostCompletionItemProvider.kinds} includes the requested kind.
61 > *
62 > * Provider results are concatenated in registration order; a single failing
63 > * provider does not prevent other providers' results from being returned.
64 > */
65 > export interface IAgentHostCompletions {
66 > readonly _serviceBrand: undefined;
67 >
68 > /**
69 > * Aggregated, deduplicated trigger characters from every registered
70 > * provider. Used to populate `InitializeResult.completionTriggerCharacters`.
71 > */
72 > readonly triggerCharacters: readonly string[];
73 >
74 > /**
75 > * Register a completion provider. The returned {@link IDisposable} unregisters
76 > * the provider when disposed.
77 > */
78 > registerProvider(provider: IAgentHostCompletionItemProvider): IDisposable;
79 >
80 > /**
81 > * Compute completion items by fanning out to all matching providers.
82 > */
83 > completions(params: CompletionsParams, token?: CancellationToken): Promise<CompletionsResult>;
84 > }
85 >
86 > export class AgentHostCompletions extends Disposable implements IAgentHostCompletions {
87 > declare readonly _serviceBrand: undefined;
88 >
89 > private readonly _providers = new Set<IAgentHostCompletionItemProvider>();
90 >
91 > constructor(
92 > @ILogService private readonly _logService: ILogService, agentHostCompletions.ts
93 > ) {
94 > super();
95 > }
97 > get triggerCharacters(): readonly string[] {
98 const seen = new Set<string>();
99 for (const provider of this._providers) {
106 return [...seen];
107 }
109 > registerProvider(provider: IAgentHostCompletionItemProvider): IDisposable {
110 > this._providers.add(provider); agentHostCompletions.ts
111 > return toDisposable(() => this._providers.delete(provider));
112 > }
114 > async completions(params: CompletionsParams, token: CancellationToken = CancellationToken.None): Promise<CompletionsResult> {
115 const matching = [...this._providers].filter(p => p.kinds.has(params.kind));
116 if (matching.length === 0) {
src/vs/base/common/observableInternal/utils/promise.ts 104 covered LOC · 12 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- promise.ts
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 > import { DisposableStore } from '../../lifecycle.js';
6 > import { IObservable, ISettableObservable } from '../base.js';
7 > import { autorun } from '../reactions/autorun.js';
8 > import { transaction } from '../transaction.js';
9 > import { derived } from '../observables/derived.js';
10 > import { observableValue } from '../observables/observableValue.js';
11 >
12 > export class ObservableLazy<T> {
13 > private readonly _value = observableValue<T | undefined>(this, undefined);
14 >
15 > /**
16 > * The cached value.
17 > * Does not force a computation of the value.
18 > */
19 > public get cachedValue(): IObservable<T | undefined> { return this._value; }
20 >
21 > constructor(private readonly _computeValue: () => T) {
22 }
23 > promise.ts
24 > /**
25 > * Returns the cached value.
26 > * Computes the value if the value has not been cached yet.
27 > */
28 > public getValue(): T {
29 let v = this._value.get();
30 if (!v) {
34 return v;
35 }
36 > } promise.ts
37 >
38 > /**
39 > * A promise whose state is observable.
40 > */
41 > export class ObservablePromise<T> {
42 > public static fromFn<T>(fn: () => Promise<T>): ObservablePromise<T> {
43 > return new ObservablePromise(fn());
44 > }
45 >
46 > public static resolved<T>(value: T): ObservablePromise<T> {
47 return new ObservablePromise(Promise.resolve(value));
48 }
49 > promise.ts
50 > private readonly _value = observableValue<PromiseResult<T> | undefined>(this, undefined);
51 >
52 > /**
53 > * The promise that this object wraps.
54 > */
55 > public readonly promise: Promise<T>;
56 >
57 > /**
58 > * The current state of the promise.
59 > * Is `undefined` if the promise didn't resolve yet.
60 > */
61 > public readonly promiseResult: IObservable<PromiseResult<T> | undefined> = this._value;
62 >
63 > constructor(promise: Promise<T>) {
64 this.promise = promise.then(value => {
65 transaction(tx => {
76 });
77 }
78 > promise.ts
79 > public readonly resolvedValue = derived(this, reader => {
80 > const result = this.promiseResult.read(reader); promise.ts
81 > if (!result) {
82 > return undefined;
83 > }
84 > return result.getDataOrThrow();
85 > }); promise.ts
86 > }
87 >
88 > export class PromiseResult<T> {
89 > constructor(
90 /**
91 * The value of the resolved promise.
101 ) {
102 }
103 > promise.ts
104 > /**
105 > * Returns the value if the promise resolved, otherwise throws the error.
106 > */
107 > public getDataOrThrow(): T {
108 if (this.error) {
109 throw this.error;
111 return this.data!;
112 }
113 > } promise.ts
114 >
115 > /**
116 > * Tracks a changing {@link ObservablePromise}, exposing the last resolved value
117 > * and whether a newer promise is still pending.
118 > */
119 > export class ObservableResolvedPromise<T> {
120 > private readonly _lastResolved: ISettableObservable<T>;
121 > public readonly lastResolved: IObservable<T>;
122 >
123 > private readonly _isResolving = observableValue<boolean>(this, false);
124 > public readonly isResolving: IObservable<boolean> = this._isResolving;
125 >
126 > private _runningPromise: ObservablePromise<T> | undefined;
127 >
128 > constructor(
129 source: IObservable<ObservablePromise<T>>,
130 initialValue: T,
149 }));
150 }
151 > } promise.ts
152 >
153 > /**
154 > * A lazy promise whose state is observable.
155 > */
156 > export class ObservableLazyPromise<T> {
157 > private readonly _lazyValue = new ObservableLazy(() => new ObservablePromise(this._computePromise()));
158 >
159 > /**
160 > * Does not enforce evaluation of the promise compute function.
161 > * Is undefined if the promise has not been computed yet.
162 > */
163 > public readonly cachedPromiseResult = derived(this, reader => this._lazyValue.cachedValue.read(reader)?.promiseResult.read(reader));
164 >
165 > constructor(private readonly _computePromise: () => Promise<T>) {
166 }
167 > promise.ts
168 > public getPromise(): Promise<T> {
169 return this._lazyValue.getValue().promise;
170 }
171 > } promise.ts
src/vs/base/node/powershell.ts 103 covered LOC · 16 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- powershell.ts
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 > import * as os from 'os';
7 > import * as path from '../common/path.js';
8 > import * as pfs from './pfs.js';
9 >
10 > // This is required, since parseInt("7-preview") will return 7.
11 > const IntRegex: RegExp = /^\d+$/;
12 >
13 > const PwshMsixRegex: RegExp = /^Microsoft.PowerShell_.*/;
14 > const PwshPreviewMsixRegex: RegExp = /^Microsoft.PowerShellPreview_.*/;
15 >
16 > const enum Arch {
17 > x64,
18 > x86,
19 > ARM
20 > }
21 >
22 > let processArch: Arch;
23 > switch (process.arch) {
24 > case 'ia32':
25 processArch = Arch.x86;
26 break;
27 > case 'arm': powershell.ts
28 > case 'arm64':
29 processArch = Arch.ARM;
30 break;
31 > default: powershell.ts
32 > processArch = Arch.x64;
33 > break;
34 > }
35 >
36 > /*
37 > Currently, here are the values for these environment variables on their respective archs:
38 >
39 > On x86 process on x86:
40 > PROCESSOR_ARCHITECTURE is X86
41 > PROCESSOR_ARCHITEW6432 is undefined
42 >
43 > On x86 process on x64:
44 > PROCESSOR_ARCHITECTURE is X86
45 > PROCESSOR_ARCHITEW6432 is AMD64
46 >
47 > On x64 process on x64:
48 > PROCESSOR_ARCHITECTURE is AMD64
49 > PROCESSOR_ARCHITEW6432 is undefined
50 >
51 > On ARM process on ARM:
52 > PROCESSOR_ARCHITECTURE is ARM64
53 > PROCESSOR_ARCHITEW6432 is undefined
54 >
55 > On x86 process on ARM:
56 > PROCESSOR_ARCHITECTURE is X86
57 > PROCESSOR_ARCHITEW6432 is ARM64
58 >
59 > On x64 process on ARM:
60 > PROCESSOR_ARCHITECTURE is ARM64
61 > PROCESSOR_ARCHITEW6432 is undefined
62 > */
63 > let osArch: Arch;
64 > if (process.env['PROCESSOR_ARCHITEW6432']) {
65 osArch = process.env['PROCESSOR_ARCHITEW6432'] === 'ARM64'
66 ? Arch.ARM
67 : Arch.x64;
68 > } else if (process.env['PROCESSOR_ARCHITECTURE'] === 'ARM64') { powershell.ts
69 osArch = Arch.ARM;
70 > } else if (process.env['PROCESSOR_ARCHITECTURE'] === 'X86') { powershell.ts
71 osArch = Arch.x86;
72 > } else { powershell.ts
73 > osArch = Arch.x64;
74 > }
75 >
76 > export interface IPowerShellExeDetails {
77 > readonly displayName: string;
78 > readonly exePath: string;
79 > }
80 >
81 > interface IPossiblePowerShellExe extends IPowerShellExeDetails {
82 > exists(): Promise<boolean>;
83 > }
84 >
85 > class PossiblePowerShellExe implements IPossiblePowerShellExe {
86 > constructor(
87 public readonly exePath: string,
88 public readonly displayName: string,
89 private knownToExist?: boolean) { }
91 > public async exists(): Promise<boolean> {
92 if (this.knownToExist === undefined) {
93 this.knownToExist = await pfs.SymlinkSupport.existsFile(this.exePath);
95 return this.knownToExist;
96 }
97 > } powershell.ts
98 >
99 function getProgramFilesPath(
100 { useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): string | null {
118 return null;
119 }
121 async function findPSCoreWindowsInstallation(
122 { useAlternateBitness = false, findPreview = false }:
190 return new PossiblePowerShellExe(pwshExePath, `PowerShell${preview}${bitness}`, true);
191 }
193 async function findPSCoreMsix({ findPreview }: { findPreview?: boolean } = {}): Promise<IPossiblePowerShellExe | null> {
194 // We can't proceed if there's no LOCALAPPDATA path
220 return null;
221 }
223 function findPSCoreDotnetGlobalTool(): IPossiblePowerShellExe {
224 const dotnetGlobalToolExePath: string = path.join(os.homedir(), '.dotnet', 'tools', 'pwsh.exe');
226 return new PossiblePowerShellExe(dotnetGlobalToolExePath, '.NET Core PowerShell Global Tool');
227 }
229 function findPSCoreScoopInstallation(): IPossiblePowerShellExe {
230 const scoopAppsDir = path.join(os.homedir(), 'scoop', 'apps');
233 return new PossiblePowerShellExe(scoopPwsh, 'PowerShell (Scoop)');
234 }
236 function findWinPS(): IPossiblePowerShellExe | null {
237 const winPSPath = path.join(
242 return new PossiblePowerShellExe(winPSPath, 'Windows PowerShell', true);
243 }
245 > /**
246 > * Iterates through all the possible well-known PowerShell installations on a machine.
247 > * Returned values may not exist, but come with an .exists property
248 > * which will check whether the executable exists.
249 > */
250 async function* enumerateDefaultPowerShellInstallations(): AsyncIterable<IPossiblePowerShellExe> {
251 // Find PSCore stable first
304 }
305 }
307 > /**
308 > * Iterates through PowerShell installations on the machine according
309 > * to configuration passed in through the constructor.
310 > * PowerShell items returned by this object are verified
311 > * to exist on the filesystem.
312 > */
313 export async function* enumeratePowerShellInstallations(): AsyncIterable<IPowerShellExeDetails> {
314 // Get the default PowerShell installations first
319 }
320 }
322 > /**
323 > * Returns the first available PowerShell executable found in the search order.
324 > */
325 export async function getFirstAvailablePowerShellInstallation(): Promise<IPowerShellExeDetails | null> {
326 for await (const pwsh of enumeratePowerShellInstallations()) {
src/vs/platform/agentHost/node/agentHostGitHubEndpointService.ts 103 covered LOC · 12 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostGitHubEndpointService.ts
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 > import { Emitter, Event } from '../../../base/common/event.js';
7 > import { Disposable } from '../../../base/common/lifecycle.js';
8 > import { createDecorator } from '../../instantiation/common/instantiation.js';
9 > import { ILogService } from '../../log/common/log.js';
10 > import { AgentHostConfigKey, agentHostCustomizationConfigSchema } from '../common/agentHostCustomizationConfig.js';
11 > import { deriveGitHubEndpoints, gitHubCopilotResource, gitHubRepoResource, IGitHubEndpoints } from '../common/githubEndpoints.js';
12 > import { ProtectedResourceMetadata } from '../common/state/protocol/state.js';
13 > import { IAgentConfigurationService } from './agentConfigurationService.js';
14 >
15 > export const IAgentHostGitHubEndpointService = createDecorator<IAgentHostGitHubEndpointService>('agentHostGitHubEndpointService');
16 >
17 > /**
18 > * Single source of truth for the GitHub endpoints (protected resources + REST /
19 > * GraphQL hosts) the agent host talks to. Computed from the optional
20 > * `githubEnterpriseUri` root config so that every consumer — agent
21 > * `authenticate` / `getProtectedResources`, changeset operation `getAuthToken`
22 > * lookups, and the REST client — agrees on the same resource identifiers and API
23 > * base. With no enterprise URI configured, the values are byte-for-byte the
24 > * github.com defaults.
25 > */
26 > export interface IAgentHostGitHubEndpointService {
27 > readonly _serviceBrand: undefined;
28 >
29 > /**
30 > * Fires when the configured GitHub endpoints change (e.g. `githubEnterpriseUri`
31 > * was set, cleared, or repointed). Does NOT fire for unrelated root-config
32 > * changes.
33 > */
34 > readonly onDidChange: Event<void>;
35 >
36 > /** The GitHub Copilot protected resource, computed against the configured endpoints. */
37 > getCopilotResource(): ProtectedResourceMetadata;
38 >
39 > /** The GitHub repository protected resource, computed against the configured endpoints. */
40 > getRepoResource(): ProtectedResourceMetadata;
41 >
42 > /** The REST API base URI (no trailing slash), e.g. `https://api.github.com`. */
43 > getApiBaseUri(): string;
44 >
45 > /** The GraphQL endpoint URI, e.g. `https://api.github.com/graphql`. */
46 > getGraphQlUri(): string;
47 >
48 > /**
49 > * The configured GitHub Enterprise host (authority only, e.g. `acme.ghe.com`),
50 > * or `undefined` for github.com. Used to set `COPILOT_GH_HOST` for the Copilot CLI.
51 > */
52 > getEnterpriseHost(): string | undefined;
53 >
54 > /**
55 > * The raw configured GitHub Enterprise base URI (e.g. `https://acme.ghe.com`),
56 > * or `undefined` for github.com. This is the value the `@vscode/copilot-api`
57 > * `CAPIClient.updateDomains(..., enterpriseUrlConfig)` expects: it derives the
58 > * GitHub API host (`api.<host>`) used for `copilot_internal` endpoints (token
59 > * mint, etc.) from it. Distinct from {@link getApiBaseUri} (the already-derived
60 > * `api.` host) - the package does that derivation itself.
61 > */
62 > getEnterpriseUri(): string | undefined;
63 > }
64 >
65 > export class AgentHostGitHubEndpointService extends Disposable implements IAgentHostGitHubEndpointService {
66 >
67 > declare readonly _serviceBrand: undefined;
68 >
69 > private readonly _onDidChange = this._register(new Emitter<void>());
70 > readonly onDidChange = this._onDidChange.event;
71 >
72 > private _endpoints: IGitHubEndpoints;
73 > private _enterpriseUri: string | undefined;
74 >
75 > constructor(
76 > @IAgentConfigurationService private readonly _configurationService: IAgentConfigurationService, agentHostGitHubEndpointService.ts
77 > @ILogService private readonly _logService: ILogService,
78 > ) {
79 > super();
80 > const resolved = this._resolve();
81 > this._endpoints = resolved.endpoints;
82 > this._enterpriseUri = resolved.enterpriseUri;
83 > this._register(this._configurationService.onDidRootConfigChange(() => {
84 const next = this._resolve();
85 // `onDidRootConfigChange` fires for every root-config key; only react
94 this._enterpriseUri = next.enterpriseUri;
95 this._onDidChange.fire();
97 > }
99 > private _resolve(): { endpoints: IGitHubEndpoints; enterpriseUri: string | undefined } {
100 > const enterpriseUri = this._configurationService.getRootValue(agentHostCustomizationConfigSchema, AgentHostConfigKey.GithubEnterpriseUri); agentHostGitHubEndpointService.ts
101 > return { endpoints: deriveGitHubEndpoints(enterpriseUri), enterpriseUri: enterpriseUri || undefined };
102 > }
104 > getApiBaseUri(): string {
105 return this._endpoints.apiBaseUri;
106 }
108 > getGraphQlUri(): string {
109 return this._endpoints.graphQlUri;
110 }
112 > getEnterpriseHost(): string | undefined {
113 return this._endpoints.enterpriseHost;
114 }
116 > getEnterpriseUri(): string | undefined {
117 return this._enterpriseUri;
118 }
120 > getCopilotResource(): ProtectedResourceMetadata {
121 return gitHubCopilotResource(this._endpoints);
122 }
124 > getRepoResource(): ProtectedResourceMetadata {
125 return gitHubRepoResource(this._endpoints);
126 }
src/vs/base/common/htmlContent.ts 102 covered LOC · 20 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- htmlContent.ts
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 > import { illegalArgument } from './errors.js';
7 > import { escapeIcons } from './iconLabels.js';
8 > import { Schemas } from './network.js';
9 > import { isEqual } from './resources.js';
10 > import { escapeRegExpCharacters } from './strings.js';
11 > import { URI, UriComponents } from './uri.js';
12 >
13 > export interface MarkdownStringTrustedOptions {
14 > readonly enabledCommands: readonly string[];
15 > }
16 >
17 > export interface IMarkdownString {
18 > readonly value: string;
19 > readonly isTrusted?: boolean | MarkdownStringTrustedOptions;
20 > readonly supportThemeIcons?: boolean;
21 > readonly supportHtml?: boolean;
22 > /** @internal */
23 > readonly supportAlertSyntax?: boolean;
24 > readonly baseUri?: UriComponents;
25 > uris?: { [href: string]: UriComponents };
26 > }
27 >
28 > export const enum MarkdownStringTextNewlineStyle {
29 > Paragraph = 0,
30 > Break = 1,
31 > }
32 >
33 > export class MarkdownString implements IMarkdownString {
34 >
35 > public value: string;
36 > public isTrusted?: boolean | MarkdownStringTrustedOptions;
37 > public supportThemeIcons?: boolean;
38 > public supportHtml?: boolean;
39 > public supportAlertSyntax?: boolean;
40 > public baseUri?: URI;
41 > public uris?: { [href: string]: UriComponents } | undefined;
42 >
43 > public static lift(dto: IMarkdownString): MarkdownString {
44 const markdownString = new MarkdownString(dto.value, dto);
45 markdownString.uris = dto.uris;
47 return markdownString;
48 }
50 > constructor(
51 value: string = '',
52 isTrustedOrOptions: boolean | { isTrusted?: boolean | MarkdownStringTrustedOptions; supportThemeIcons?: boolean; supportHtml?: boolean; supportAlertSyntax?: boolean } = false,
70 }
71 }
73 > appendText(value: string, newlineStyle: MarkdownStringTextNewlineStyle = MarkdownStringTextNewlineStyle.Paragraph): MarkdownString {
74 this.value += escapeMarkdownSyntaxTokens(this.supportThemeIcons ? escapeIcons(value) : value) // CodeQL [SM02383] The Markdown is fully sanitized after being rendered.
75 .replace(/([ \t]+)/g, (_match, g1) => '&nbsp;'.repeat(g1.length)) // CodeQL [SM02383] The Markdown is fully sanitized after being rendered.
79 return this;
80 }
82 > appendMarkdown(value: string): MarkdownString {
83 this.value += value;
84 return this;
85 }
87 > appendCodeblock(langId: string, code: string): MarkdownString {
88 this.value += `\n${appendEscapedMarkdownCodeBlockFence(code, langId)}\n`;
89 return this;
90 }
92 > appendLink(target: URI | string, label: string, title?: string): MarkdownString {
93 this.value += '[';
94 this.value += this._escape(label, ']');
101 return this;
102 }
104 > private _escape(value: string, ch: string): string {
105 const r = new RegExp(escapeRegExpCharacters(ch), 'g');
106 return value.replace(r, (match, offset) => {
112 });
113 }
114 > } htmlContent.ts
115 >
116 > export function isEmptyMarkdownString(oneOrMany: IMarkdownString | IMarkdownString[] | null | undefined): boolean {
117 if (isMarkdownString(oneOrMany)) {
118 return !oneOrMany.value;
123 }
124 }
126 > export function isMarkdownString(thing: unknown): thing is IMarkdownString {
127 if (thing instanceof MarkdownString) {
128 return true;
135 return false;
136 }
138 > export function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean {
139 if (a === b) {
140 return true;
150 }
151 }
153 > export function escapeMarkdownSyntaxTokens(text: string): string {
154 // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash
155 return text
157 .replace(/^([ \t]*)-/gm, '$1\\-'); // CodeQL [SM02383] Backslash is escaped in the character class
158 }
160 > /**
161 > * Escapes only the characters that would break out of markdown link text
162 > * (`[label](url)`) syntax: `\` and `]`. Use this when the escaped string is
163 > * displayed as the visible label of a link, since renderers that extract the
164 > * link text without re-parsing markdown (e.g. the chat inline anchor / skill
165 > * pill) would otherwise show full `escapeMarkdownSyntaxTokens` backslashes
166 > * (`\-`, `\.`, ...) verbatim.
167 > */
168 > export function escapeMarkdownLinkLabel(text: string): string {
169 return text.replace(/[\\\]]/g, '\\$&');
170 }
172 > /**
173 > * @see https://github.com/microsoft/vscode/issues/193746
174 > */
175 > export function appendEscapedMarkdownCodeBlockFence(code: string, langId: string) {
176 const longestFenceLength =
177 code.match(/^`+/gm)?.reduce((a, b) => (a.length > b.length ? a : b)).length ??
187 ].join('\n');
188 }
190 > /**
191 > * Wraps arbitrary text in a markdown inline code span using a backtick fence
192 > * long enough to safely contain any backtick sequences present in the text.
193 > *
194 > * Backticks inside an inline code span cannot be backslash-escaped per the
195 > * CommonMark spec — the only safe way is to choose a delimiter run longer
196 > * than any run of backticks in the content (and pad with spaces if the
197 > * content begins or ends with a backtick).
198 > */
199 > export function appendEscapedMarkdownInlineCode(text: string): string {
200 const longestBacktickRun = Math.max(0, ...(text.match(/`+/g) ?? []).map(m => m.length));
201 const fence = '`'.repeat(longestBacktickRun + 1);
204 return `${fence}${content}${fence}`;
205 }
207 > export function escapeDoubleQuotes(input: string) {
208 return input.replace(/"/g, '&quot;');
209 }
211 > export function removeMarkdownEscapes(text: string): string {
212 if (!text) {
213 return text;
215 return text.replace(/\\([\\`*_{}[\]()#+\-.!~])/g, '$1');
216 }
218 > export function parseHrefAndDimensions(href: string): { href: string; dimensions: string[] } {
219 const dimensions: string[] = [];
220 const splitted = href.split('|').map(s => s.trim());
237 return { href, dimensions };
238 }
240 > export function createMarkdownLink(text: string, href: string, title?: string, escapeTokens = true): string {
241 return `[${escapeTokens ? escapeMarkdownSyntaxTokens(text) : text}](${href}${title ? ` "${escapeMarkdownSyntaxTokens(title)}"` : ''})`;
242 }
244 > export function createMarkdownCommandLink(command: { text: string; id: string; arguments?: unknown[]; tooltip: string }, escapeTokens = true): string {
245 const uri = createCommandUri(command.id, ...(command.arguments || [])).toString();
246 return createMarkdownLink(command.text, uri, command.tooltip, escapeTokens);
247 }
249 > export function createCommandUri(commandId: string, ...commandArgs: unknown[]): URI {
250 return URI.from({
251 scheme: Schemas.command,
src/vs/base/common/mime.ts 102 covered LOC · 5 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- mime.ts
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 > import { extname } from './path.js';
7 >
8 > export const Mimes = Object.freeze({
9 > text: 'text/plain',
10 > binary: 'application/octet-stream',
11 > unknown: 'application/unknown',
12 > markdown: 'text/markdown',
13 > latex: 'text/latex',
14 > uriList: 'text/uri-list',
15 > html: 'text/html',
16 > });
17 >
18 > interface MapExtToMediaMimes {
19 > [index: string]: string | string[];
20 > }
21 >
22 > const mapExtToTextMimes: Record<string, string> = {
23 > '.css': 'text/css',
24 > '.csv': 'text/csv',
25 > '.htm': 'text/html',
26 > '.html': 'text/html',
27 > '.ics': 'text/calendar',
28 > '.js': 'text/javascript',
29 > '.mjs': 'text/javascript',
30 > '.txt': 'text/plain',
31 > '.xml': 'text/xml'
32 > };
33 >
34 > // Known media mimes that we can handle
35 > const mapExtToMediaMimes: MapExtToMediaMimes = {
36 > '.aac': 'audio/x-aac',
37 > '.avi': 'video/x-msvideo',
38 > '.bmp': 'image/bmp',
39 > '.flv': 'video/x-flv',
40 > '.gif': 'image/gif',
41 > '.ico': 'image/x-icon',
42 > '.jpe': ['image/jpg', 'image/jpeg'],
43 > '.jpeg': ['image/jpg', 'image/jpeg'],
44 > '.jpg': ['image/jpg', 'image/jpeg'],
45 > '.m1v': 'video/mpeg',
46 > '.m2a': 'audio/mpeg',
47 > '.m2v': 'video/mpeg',
48 > '.m3a': 'audio/mpeg',
49 > '.mid': 'audio/midi',
50 > '.midi': 'audio/midi',
51 > '.mk3d': 'video/x-matroska',
52 > '.mks': 'video/x-matroska',
53 > '.mkv': 'video/x-matroska',
54 > '.mov': 'video/quicktime',
55 > '.movie': 'video/x-sgi-movie',
56 > '.mp2': 'audio/mpeg',
57 > '.mp2a': 'audio/mpeg',
58 > '.mp3': 'audio/mpeg',
59 > '.mp4': 'video/mp4',
60 > '.mp4a': 'audio/mp4',
61 > '.mp4v': 'video/mp4',
62 > '.mpe': 'video/mpeg',
63 > '.mpeg': 'video/mpeg',
64 > '.mpg': 'video/mpeg',
65 > '.mpg4': 'video/mp4',
66 > '.mpga': 'audio/mpeg',
67 > '.oga': 'audio/ogg',
68 > '.ogg': 'audio/ogg',
69 > '.opus': 'audio/opus',
70 > '.ogv': 'video/ogg',
71 > '.png': 'image/png',
72 > '.psd': 'image/vnd.adobe.photoshop',
73 > '.qt': 'video/quicktime',
74 > '.spx': 'audio/ogg',
75 > '.svg': 'image/svg+xml',
76 > '.tga': 'image/x-tga',
77 > '.tif': 'image/tiff',
78 > '.tiff': 'image/tiff',
79 > '.wav': 'audio/x-wav',
80 > '.webm': 'video/webm',
81 > '.webp': 'image/webp',
82 > '.wma': 'audio/x-ms-wma',
83 > '.wmv': 'video/x-ms-wmv',
84 > '.woff': 'application/font-woff',
85 > };
86 >
87 > export function getMediaOrTextMime(path: string): string | undefined {
88 const ext = extname(path);
89 const textMime = mapExtToTextMimes[ext.toLowerCase()];
94 }
95 }
96 > mime.ts
97 > export function getMediaMime(path: string): string | undefined {
98 const ext = extname(path);
99 const mimeType = mapExtToMediaMimes[ext.toLowerCase()];
100 return Array.isArray(mimeType) ? mimeType[0] : mimeType;
101 }
102 > mime.ts
103 > export function getExtensionForMimeType(mimeType: string): string | undefined {
104 for (const extension in mapExtToMediaMimes) {
105 const value = mapExtToMediaMimes[extension];
111 return undefined;
112 }
113 > mime.ts
114 > const _simplePattern = /^(.+)\/(.+?)(;.+)?$/;
115 >
116 > export function normalizeMimeType(mimeType: string): string;
117 > export function normalizeMimeType(mimeType: string, strict: true): string | undefined;
118 > export function normalizeMimeType(mimeType: string, strict?: true): string | undefined {
119
120 const match = _simplePattern.exec(mimeType);
128 return `${match[1].toLowerCase()}/${match[2].toLowerCase()}${match[3] ?? ''}`;
129 }
130 > mime.ts
131 > /**
132 > * Whether the provided mime type is a text stream like `stdout`, `stderr`.
133 > */
134 > export function isTextStreamMime(mimeType: string) {
135 return ['application/vnd.code.notebook.stdout', 'application/vnd.code.notebook.stderr'].includes(mimeType);
136 }
src/vs/platform/agentHost/common/agentHostUri.ts 102 covered LOC · 5 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostUri.ts
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 > import { decodeBase64, encodeBase64, VSBuffer } from '../../../base/common/buffer.js';
7 > import { Schemas } from '../../../base/common/network.js';
8 > import { URI } from '../../../base/common/uri.js';
9 > import type { ResourceLabelFormatter } from '../../label/common/label.js';
10 >
11 > /**
12 > * The URI scheme for accessing files on a remote agent host.
13 > *
14 > * The original file path is kept verbatim as the URI path so resource
15 > * labels, language detection, and path comparisons see a real path. The
16 > * original scheme, authority, and query are carried in a single
17 > * url-safe-base64 `_ah` query parameter so any remote resource can be
18 > * represented without assuming `file://`:
19 > *
20 > * ```
21 > * vscode-agent-host://[connectionAuthority][originalPath]?_ah=[meta]#[originalFragment]
22 > * ```
23 > *
24 > * where `meta` is {@link IAgentHostUriMeta} as url-safe-base64-encoded
25 > * JSON. Encoding the metadata as a single opaque parameter (rather than
26 > * raw JSON) keeps the query a well-formed parameter list, so unrelated
27 > * query parameters such as `vscodeLinkType` can coexist on the wrapped
28 > * URI without corrupting the metadata. For example,
29 > * `file:///home/user/foo.ts` on remote `my-server` becomes:
30 > * ```
31 > * vscode-agent-host://my-server/home/user/foo.ts?_ah=eyJzY2hlbWUiOiJmaWxlIn0
32 > * ```
33 > */
34 > export const AGENT_HOST_SCHEME = 'vscode-agent-host';
35 >
36 > /**
37 > * Query parameter that carries the {@link IAgentHostUriMeta} payload.
38 > */
39 > const AGENT_HOST_META_PARAM = '_ah';
40 >
41 > /**
42 > * Metadata carried in the query of a {@link AGENT_HOST_SCHEME} URI so the
43 > * original URI can be reconstructed while keeping the path label-friendly.
44 > */
45 > interface IAgentHostUriMeta {
46 > /** Original URI scheme (e.g. `file`, `git-blob`). */
47 > readonly scheme: string;
48 > /** Original URI authority, omitted when empty. */
49 > readonly authority?: string;
50 > /** Original URI query, omitted when empty. */
51 > readonly query?: string;
52 > }
53 >
54 > /**
55 > * Wraps a remote URI into a {@link AGENT_HOST_SCHEME} URI that can be
56 > * resolved through the agent host filesystem provider.
57 > *
58 > * @param originalUri The URI on the remote (e.g. `file:///path` or
59 > * `agenthost-content:///sessionId/...`)
60 > * @param connectionAuthority The sanitized connection identifier used as
61 > * the URI authority (from {@link agentHostAuthority}).
62 > */
63 > export function toAgentHostUri(originalUri: URI, connectionAuthority: string): URI {
64 if (connectionAuthority === 'local' && originalUri.scheme === Schemas.file) {
65 return originalUri;
81 });
82 }
84 > /**
85 > * Extracts the original URI from a {@link AGENT_HOST_SCHEME} URI.
86 > *
87 > * The inverse of {@link toAgentHostUri}.
88 > */
89 > export function fromAgentHostUri(agentHostUri: URI): URI {
90 if (agentHostUri.scheme !== AGENT_HOST_SCHEME) {
91 return agentHostUri;
116 });
117 }
119 > /**
120 > * Strips the redundant `ws://` scheme from an address. The transport layer
121 > * already defaults to `ws://`, so only `wss://` needs to be preserved.
122 > */
123 > export function normalizeRemoteAgentHostAddress(address: string): string {
124 if (address.startsWith('ws://')) {
125 return address.slice('ws://'.length);
127 return address;
128 }
130 > /**
131 > * Encode a remote address into an identifier that is safe for use in
132 > * both URI schemes and URI authorities, and is collision-free.
133 > *
134 > * Three tiers:
135 > * 1. Purely alphanumeric addresses are returned as-is.
136 > * 2. "Normal" addresses containing only `[a-zA-Z0-9.:-]` get colons
137 > * replaced with `__` (double underscore) for human readability.
138 > * Addresses containing `_` skip this tier to keep the encoding
139 > * collision-free (`__` can only appear from colon replacement).
140 > * 3. Everything else is url-safe base64-encoded with a `b64-` prefix.
141 > */
142 > export function agentHostAuthority(address: string): string {
143 const normalized = normalizeRemoteAgentHostAddress(address);
144 if (/^[a-zA-Z0-9]+$/.test(normalized)) {
150 return `b64-${encodeBase64(VSBuffer.fromString(normalized), false, true)}`;
151 }
153 > /**
154 > * Label formatter for {@link AGENT_HOST_SCHEME} URIs. The URI path is
155 > * already the original resource path, so the label is the path verbatim.
156 > */
157 > export const AGENT_HOST_LABEL_FORMATTER: ResourceLabelFormatter = {
158 > scheme: AGENT_HOST_SCHEME,
159 > formatting: {
160 > label: '${path}',
161 > separator: '/',
162 > },
163 > };
src/vs/base/common/observableInternal/observables/observableValue.ts 101 covered LOC · 30 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- observableValue.ts
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 > import { ISettableObservable, ITransaction } from '../base.js';
7 > import { TransactionImpl } from '../transaction.js';
8 > import { BaseObservable } from './baseObservable.js';
9 > import { EqualityComparer, IDisposable, strictEquals } from '../commonFacade/deps.js';
10 > import { DebugNameData } from '../debugName.js';
11 > import { getLogger } from '../logging/logging.js';
12 > import { DebugLocation } from '../debugLocation.js';
13 >
14 > /**
15 > * Creates an observable value.
16 > * Observers get informed when the value changes.
17 > * @template TChange An arbitrary type to describe how or why the value changed. Defaults to `void`.
18 > * Observers will receive every single change value.
19 > */
20 >
21 > export function observableValue<T, TChange = void>(name: string, initialValue: T): ISettableObservable<T, TChange>;
22 > export function observableValue<T, TChange = void>(owner: object, initialValue: T): ISettableObservable<T, TChange>;
23 > export function observableValue<T, TChange = void>(nameOrOwner: string | object, initialValue: T, debugLocation = DebugLocation.ofCaller()): ISettableObservable<T, TChange> {
24 > let debugNameData: DebugNameData; observableValue.ts
25 > if (typeof nameOrOwner === 'string') {
26 > debugNameData = new DebugNameData(undefined, nameOrOwner, undefined); observableValue.ts
27 > } else { observableValue.ts
28 > debugNameData = new DebugNameData(nameOrOwner, undefined, undefined); observableValue.ts
29 > }
30 > return new ObservableValue(debugNameData, initialValue, strictEquals, debugLocation); observableValue.ts
31 > }
33 > export class ObservableValue<T, TChange = void>
34 > extends BaseObservable<T, TChange>
35 > implements ISettableObservable<T, TChange> {
36 > protected _value: T;
37 >
38 > get debugName() {
39 > return this._debugNameData.getDebugName(this) ?? 'ObservableValue';
40 > }
41 >
42 > constructor(
43 > private readonly _debugNameData: DebugNameData, observableValue.ts
44 > initialValue: T,
45 > private readonly _equalityComparator: EqualityComparer<T>,
46 > debugLocation: DebugLocation
47 > ) {
48 > super(debugLocation);
49 > this._value = initialValue;
50 >
51 > getLogger()?.handleObservableUpdated(this, { hadValue: false, newValue: initialValue, change: undefined, didChange: true, oldValue: undefined });
52 > }
53 > public override get(): T { observableValue.ts
54 > return this._value; observableValue.ts
55 > }
57 > public set(value: T, tx: ITransaction | undefined, change: TChange): void {
58 > if (change === undefined && this._equalityComparator(this._value, value)) { observableValue.ts
59 return;
60 }
62 > let _tx: TransactionImpl | undefined;
63 > if (!tx) {
64 > tx = _tx = new TransactionImpl(() => { }, () => `Setting ${this.debugName}`); observableValue.ts
65 > }
66 > try { observableValue.ts
67 > const oldValue = this._value;
68 > this._setValue(value);
69 > getLogger()?.handleObservableUpdated(this, { oldValue, newValue: value, change, didChange: true, hadValue: true }); observableValue.ts
70 >
71 > for (const observer of this._observers) {
72 > tx.updateObserver(observer, this); observableValue.ts
73 > observer.handleChange(this, change);
74 > }
75 > } finally { observableValue.ts
76 > if (_tx) {
77 > _tx.finish(); observableValue.ts
78 > }
82 > override toString(): string {
83 return `${this.debugName}: ${this._value}`;
84 }
86 > protected _setValue(newValue: T): void {
87 > this._value = newValue; observableValue.ts
88 > }
90 > public debugGetState() {
91 return {
92 value: this._value,
93 };
94 }
96 > public debugSetValue(value: unknown) {
97 this._value = value as T;
98 }
100 > /**
101 > * A disposable observable. When disposed, its value is also disposed.
102 > * When a new value is set, the previous value is disposed.
103 > */
104 >
105 > export function disposableObservableValue<T extends IDisposable | undefined, TChange = void>(nameOrOwner: string | object, initialValue: T, debugLocation = DebugLocation.ofCaller()): ISettableObservable<T, TChange> & IDisposable {
106 let debugNameData: DebugNameData;
107 if (typeof nameOrOwner === 'string') {
112 return new DisposableObservableValue(debugNameData, initialValue, strictEquals, debugLocation);
113 }
115 > export class DisposableObservableValue<T extends IDisposable | undefined, TChange = void> extends ObservableValue<T, TChange> implements IDisposable {
116 > protected override _setValue(newValue: T): void {
117 if (this._value === newValue) {
118 return;
src/vs/platform/jsonschemas/common/jsonContributionRegistry.ts 100 covered LOC · 9 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- jsonContributionRegistry.ts
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 > import { Emitter, Event } from '../../../base/common/event.js';
7 > import { getCompressedContent, IJSONSchema } from '../../../base/common/jsonSchema.js';
8 > import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
9 > import * as platform from '../../registry/common/platform.js';
10 >
11 > export const Extensions = {
12 > JSONContribution: 'base.contributions.json'
13 > };
14 >
15 > export interface ISchemaContributions {
16 > schemas: { [id: string]: IJSONSchema };
17 > }
18 >
19 > export interface IJSONContributionRegistry {
20 >
21 > readonly onDidChangeSchema: Event<string>;
22 > readonly onDidChangeSchemaAssociations: Event<void>;
23 >
24 > /**
25 > * Register a schema to the registry.
26 > */
27 > registerSchema(uri: string, unresolvedSchemaContent: IJSONSchema, store?: DisposableStore): void;
28 >
29 > registerSchemaAssociation(uri: string, glob: string): IDisposable;
30 >
31 > /**
32 > * Notifies all listeners that the content of the given schema has changed.
33 > * @param uri The id of the schema
34 > */
35 > notifySchemaChanged(uri: string): void;
36 >
37 > /**
38 > * Get all schemas
39 > */
40 > getSchemaContributions(): ISchemaContributions;
41 >
42 > getSchemaAssociations(): { [uri: string]: string[] };
43 >
44 > /**
45 > * Gets the (compressed) content of the schema with the given schema ID (if any)
46 > * @param uri The id of the schema
47 > */
48 > getSchemaContent(uri: string): string | undefined;
49 >
50 > /**
51 > * Returns true if there's a schema that matches the given schema ID
52 > * @param uri The id of the schema
53 > */
54 > hasSchemaContent(uri: string): boolean;
55 > }
56 >
57 >
58 >
59 > function normalizeId(id: string) {
60 > if (id.length > 0 && id.charAt(id.length - 1) === '#') {
61 return id.substring(0, id.length - 1);
62 }
63 > return id; jsonContributionRegistry.ts
64 > }
65 >
66 >
67 >
68 > class JSONContributionRegistry extends Disposable implements IJSONContributionRegistry {
69 >
70 > private readonly schemasById: { [id: string]: IJSONSchema } = {};
71 > private readonly schemaAssociations: { [uri: string]: string[] } = {};
72 >
73 > private readonly _onDidChangeSchema = this._register(new Emitter<string>());
74 > readonly onDidChangeSchema: Event<string> = this._onDidChangeSchema.event;
75 >
76 > private readonly _onDidChangeSchemaAssociations = this._register(new Emitter<void>());
77 > readonly onDidChangeSchemaAssociations: Event<void> = this._onDidChangeSchemaAssociations.event;
78 >
79 > public registerSchema(uri: string, unresolvedSchemaContent: IJSONSchema, store?: DisposableStore): void {
80 > const normalizedUri = normalizeId(uri);
81 > this.schemasById[normalizedUri] = unresolvedSchemaContent;
82 > this._onDidChangeSchema.fire(uri);
83 >
84 > if (store) {
85 store.add(toDisposable(() => {
86 delete this.schemasById[normalizedUri];
88 }));
89 }
91 >
92 > public registerSchemaAssociation(uri: string, glob: string): IDisposable {
93 const normalizedUri = normalizeId(uri);
94 if (!this.schemaAssociations[normalizedUri]) {
114 });
115 }
117 > public notifySchemaChanged(uri: string): void {
118 this._onDidChangeSchema.fire(uri);
119 }
121 > public getSchemaContributions(): ISchemaContributions {
122 return {
123 schemas: this.schemasById,
124 };
125 }
127 > public getSchemaContent(uri: string): string | undefined {
128 const schema = this.schemasById[uri];
129 return schema ? getCompressedContent(schema) : undefined;
130 }
132 > public hasSchemaContent(uri: string): boolean {
133 return !!this.schemasById[uri];
134 }
136 > public getSchemaAssociations(): { [uri: string]: string[] } {
137 return this.schemaAssociations;
138 }
140 > }
141 >
142 > const jsonContributionRegistry = new JSONContributionRegistry();
143 > platform.Registry.add(Extensions.JSONContribution, jsonContributionRegistry);
src/vs/base/common/observableInternal/reactions/autorun.ts 98 covered LOC · 11 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- autorun.ts
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 > import { IReaderWithStore, IReader, IObservable, ISettableObservable } from '../base.js';
7 > import { IChangeTracker } from '../changeTracker.js';
8 > import { DisposableStore, IDisposable, toDisposable } from '../commonFacade/deps.js';
9 > import { DebugNameData, IDebugNameData } from '../debugName.js';
10 > import { AutorunObserver } from './autorunImpl.js';
11 > import { DebugLocation } from '../debugLocation.js';
12 > import { observableValue } from '../observables/observableValue.js';
13 > import { transaction } from '../transaction.js';
14 >
15 > /**
16 > * Runs immediately and whenever a transaction ends and an observed observable changed.
17 > * {@link fn} should start with a JS Doc using `@description` to name the autorun.
18 > */
19 > export function autorun(fn: (reader: IReaderWithStore) => void, debugLocation = DebugLocation.ofCaller()): IDisposable {
20 > return new AutorunObserver( autorun.ts
21 > new DebugNameData(undefined, undefined, fn),
22 > fn,
23 > undefined,
24 > debugLocation
25 > );
26 > }
27 > autorun.ts
28 > /**
29 > * Runs immediately and whenever a transaction ends and an observed observable changed.
30 > * {@link fn} should start with a JS Doc using `@description` to name the autorun.
31 > */
32 > export function autorunOpts(options: IDebugNameData & {}, fn: (reader: IReaderWithStore) => void, debugLocation = DebugLocation.ofCaller()): IDisposable {
33 return new AutorunObserver(
34 new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? fn),
38 );
39 }
40 > autorun.ts
41 > /**
42 > * Runs immediately and whenever a transaction ends and an observed observable changed.
43 > * {@link fn} should start with a JS Doc using `@description` to name the autorun.
44 > *
45 > * Use `changeTracker.createChangeSummary` to create a "change summary" that can collect the changes.
46 > * Use `changeTracker.handleChange` to add a reported change to the change summary.
47 > * The run function is given the last change summary.
48 > * The change summary is discarded after the run function was called.
49 > *
50 > * @see autorun
51 > */
52 > export function autorunHandleChanges<TChangeSummary>(
53 options: IDebugNameData & {
54 changeTracker: IChangeTracker<TChangeSummary>;
64 );
65 }
66 > autorun.ts
67 > /**
68 > * @see autorunHandleChanges (but with a disposable store that is cleared before the next run or on dispose)
69 > */
70 > export function autorunWithStoreHandleChanges<TChangeSummary>(
71 options: IDebugNameData & {
72 changeTracker: IChangeTracker<TChangeSummary>;
92 });
93 }
94 > autorun.ts
95 > /**
96 > * @see autorun (but with a disposable store that is cleared before the next run or on dispose)
97 > *
98 > * @deprecated Use `autorun(reader => { reader.store.add(...) })` instead!
99 > */
100 > export function autorunWithStore(fn: (reader: IReader, store: DisposableStore) => void): IDisposable {
101 const store = new DisposableStore();
102 const disposable = autorunOpts(
116 });
117 }
118 > autorun.ts
119 > export function autorunDelta<T>(
120 observable: IObservable<T>,
121 handler: (args: { lastValue: T | undefined; newValue: T }) => void
129 });
130 }
131 > autorun.ts
132 > export function autorunIterableDelta<T>(
133 getValue: (reader: IReader) => Iterable<T>,
134 handler: (args: { addedValues: T[]; removedValues: T[] }) => void,
157 });
158 }
159 > autorun.ts
160 > /**
161 > * For each key-stable item in {@link items}, runs {@link setup} once when the
162 > * key is first observed and disposes the per-key {@link DisposableStore} when
163 > * the key is no longer present in the array (or when the returned disposable
164 > * is disposed).
165 > *
166 > * The {@link IObservable} handed to {@link setup} fires whenever the array
167 > * still contains an item with the same key but the item value itself has
168 > * changed (e.g. because the upstream state is immutable and produced a new
169 > * object with the same id). All per-key value updates triggered by a single
170 > * change to {@link items} are batched into one transaction, so dependent
171 > * autoruns observe a consistent snapshot.
172 > *
173 > * Per-key state should be stored in closures or in disposables registered
174 > * against the per-key {@link DisposableStore}. {@link setup} should not call
175 > * `.read()` on the outer {@link items} observable from its body (use the
176 > * provided per-key value observable, or create inner autoruns).
177 > */
178 > export function autorunPerKeyedItem<TIn, TKey>(
179 items: IObservable<readonly TIn[]>,
180 keyFn: (input: TIn) => TKey,
227 });
228 }
229 > autorun.ts
230 > export interface IReaderWithDispose extends IReaderWithStore, IDisposable { }
231 >
232 > /**
233 > * An autorun with a `dispose()` method on its `reader` which cancels the autorun.
234 > * It it safe to call `dispose()` synchronously.
235 > * @deprecated Use autorunSelfDisposable2
236 > */
237 > export function autorunSelfDisposable(fn: (reader: IReaderWithDispose) => void, debugLocation = DebugLocation.ofCaller()): IDisposable {
238 let ar: IDisposable | undefined;
239 let disposed = false;
258 return ar;
259 }
260 > autorun.ts
261 >
262 > /**
263 > * An autorun with a `dispose()` method on its `reader` which cancels the autorun.
264 > * It it safe to call `dispose()` synchronously.
265 > * TODO@hediet/copilot: rename to delete autorunSelfDisposable, and rename autorunSelfDisposable2 to autorunSelfDisposable.
266 > */
267 > export function registerAutorunSelfDisposable(store: DisposableStore, fn: (reader: IReaderWithDispose) => void, debugLocation = DebugLocation.ofCaller()): void {
268 let ar: IDisposable | undefined;
269 let disposeSync = false;
src/vs/platform/agentHost/node/shared/editChunkExtractor.ts 97 covered LOC · 5 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- editChunkExtractor.ts
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 > /*
7 > * Extracts the explicit AI-written text chunks from a file-edit tool's
8 > * input payload. Both Claude (via @anthropic-ai/claude-agent-sdk) and
9 > * Copilot CLI (via @github/copilot-sdk) accept canonical tool schemas
10 > * whose shapes we can read structurally — Claude uses PascalCase names
11 > * (`Write`, `Edit`, `MultiEdit`) with `_string` fields, Copilot uses
12 > * snake_case (`create`, `edit`, `str_replace`, `insert`,
13 > * `str_replace_editor` command-dispatched, `apply_patch` /
14 > * `git_apply_patch` V4A patch body) with `_str` / `file_text` fields.
15 > *
16 > * Returning an empty array means "we couldn't read this — fall back to
17 > * whole-file scoring." Defensive against malformed SDK input: every
18 > * branch checks the value shape before reading.
19 > *
20 > * Coverage invariant: every tool the agent host currently treats as a
21 > * file-edit tool (`isClaudeFileEditTool`, `isEditTool` in Copilot) has
22 > * a matching case below — so in practice every edit gets chunked
23 > * scoring. The whole-file fallback is a safety net for SDK shape drift
24 > * (a tool input changes shape) and for newly added tools (a new edit
25 > * tool added to one of those gates without a matching case here). If
26 > * you add a new file-edit tool to either gate, add a case here too so
27 > * the survival reporter keeps producing chunked scores.
28 > */
29 >
30 > /**
31 > * Returns the AI-written text chunks for a known file-edit tool, or
32 > * `[]` if the tool / input shape is not recognised. Callers should
33 > * treat `[]` as "fall back to whole-file scoring."
34 > *
35 > * Supported Claude SDK tools (one file per call):
36 > * - `Write { content }` → `[content]`
37 > * - `Edit { new_string }` → `[new_string]`
38 > * - `MultiEdit { edits: [{ new_string }] }` → one chunk per edit
39 > *
40 > * Supported Copilot CLI tools (one file per call unless noted):
41 > * - `create { file_text }` → `[file_text]`
42 > * - `edit`, `str_replace { new_str }` → `[new_str]`
43 > * - `insert { new_str }` → `[new_str]`
44 > * - `str_replace_editor { command, ... }` → dispatch on command
45 > * - `apply_patch`, `git_apply_patch` → `+` lines from the
46 > * V4A patch body, scoped to {@link forFilePath} when supplied
47 > * (the patch may touch multiple files; we only want chunks for
48 > * the file we're sampling).
49 > *
50 > * `NotebookEdit` is not handled here: the reporter currently skips
51 > * `.ipynb` files at launch time, so notebook tool inputs never reach
52 > * the survival math. Add a branch here if we extend tracking to
53 > * notebooks.
54 > *
55 > * @param toolName Tool identifier (Claude PascalCase or Copilot
56 > * snake_case; tools we don't recognise just return `[]`).
57 > * @param input The tool input. May be `unknown`, a JSON object,
58 > * or — for `apply_patch` — a bare V4A patch string. Defensive
59 > * against all three.
60 > * @param forFilePath Optional. When supplied and the tool is a
61 > * multi-file patch (`apply_patch` / `git_apply_patch`), only the
62 > * `+` lines under that file's header contribute. Single-file tools
63 > * ignore this argument.
64 > */
65 > export function extractAiChunks(toolName: string, input: unknown, forFilePath?: string): string[] {
66 switch (toolName) {
67 // ---- Claude SDK -------------------------------------------------
90 }
91 }
93 function readStringField(input: unknown, field: string): string[] {
94 if (typeof input !== 'object' || input === null) {
98 return typeof value === 'string' ? [value] : [];
99 }
101 function readMultiEdit(input: unknown, field: string): string[] {
102 if (typeof input !== 'object' || input === null) {
118 return chunks;
119 }
121 > /**
122 > * `str_replace_editor` dispatches on `command`. We extract chunks per
123 > * command type — `view` and `undo_edit` produce no chunks.
124 > */
125 function readStrReplaceEditor(input: unknown): string[] {
126 if (typeof input !== 'object' || input === null) {
138 }
139 }
141 > /**
142 > * Headers of the V4A patch format the Copilot `apply_patch` tool
143 > * accepts. Mirrors {@link copilotToolDisplay.APPLY_PATCH_FILE_HEADERS};
144 > * kept duplicated here so this module stays free of cross-provider
145 > * imports.
146 > */
147 > const APPLY_PATCH_FILE_HEADERS = [
148 > /^\s*\*\*\*\s+Update File:\s*(.+?)\s*$/,
149 > /^\s*\*\*\*\s+Add File:\s*(.+?)\s*$/,
150 > /^\s*\*\*\*\s+Delete File:\s*(.+?)\s*$/,
151 > /^\s*\*\*\*\s+Move to:\s*(.+?)\s*$/,
152 > ];
153 >
154 > /**
155 > * Extracts the AI-written additions from a V4A patch body, grouped by
156 > * the file header that introduced them. When `forFilePath` is set,
157 > * only that file's additions are returned (joined into a single
158 > * chunk); otherwise every file's additions are returned, in document
159 > * order.
160 > *
161 > * The Copilot SDK delivers `apply_patch` with `arguments` as a raw
162 > * patch string (custom tool format), not as a JSON object, so the
163 > * string fallback is the common case for apply_patch.
164 > */
165 function readApplyPatch(input: unknown, forFilePath?: string): string[] {
166 let text: string | undefined;
src/vs/platform/agentHost/node/shared/fileEditTracker.ts 96 covered LOC · 9 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- fileEditTracker.ts
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 > import { decodeHex, encodeHex, VSBuffer } from '../../../../base/common/buffer.js';
7 > import { basename } from '../../../../base/common/path.js';
8 > import { URI } from '../../../../base/common/uri.js';
9 > import { IFileService } from '../../../files/common/files.js';
10 > import { ILogService } from '../../../log/common/log.js';
11 > import { IDiffComputeService } from '../../common/diffComputeService.js';
12 > import { ISessionDatabase } from '../../common/sessionDataService.js';
13 > import { FileEditKind, ToolResultContentType, type ToolResultFileEditContent } from '../../common/state/sessionState.js';
14 > import { extractAiChunks } from './editChunkExtractor.js';
15 > import { IEditSurvivalReporterFactory } from './editSurvivalReporter.js';
16 >
17 > const SESSION_DB_SCHEME = 'session-db';
18 >
19 > /**
20 > * Builds a `session-db:` URI that references a file-edit content blob
21 > * stored in the session database. Parsed by {@link parseSessionDbUri}.
22 > */
23 > export function buildSessionDbUri(sessionUri: string, toolCallId: string, filePath: string, part: 'before' | 'after'): string {
24 return URI.from({
25 scheme: SESSION_DB_SCHEME,
28 }).toString();
29 }
31 > /** Parsed fields from a `session-db:` content URI. */
32 > export interface ISessionDbUriFields {
33 > sessionUri: string;
34 > toolCallId: string;
35 > filePath: string;
36 > part: 'before' | 'after';
37 > }
38 >
39 > /**
40 > * Parses a `session-db:` URI produced by {@link buildSessionDbUri}.
41 > * Returns `undefined` if the URI is not a valid `session-db:` URI.
42 > */
43 > export function parseSessionDbUri(raw: string): ISessionDbUriFields | undefined {
44 const parsed = URI.parse(raw);
45 if (parsed.scheme !== SESSION_DB_SCHEME) {
61 }
62 }
64 > /**
65 > * Tracks file edits made by tools in a session by snapshotting file content
66 > * before and after each edit tool invocation, persisting snapshots into the
67 > * session database.
68 > */
69 > export class FileEditTracker {
70 >
71 > /**
72 > * Pending edits keyed by file path. Populated by {@link trackEditStart}
73 > * before the edit tool runs; popped by {@link completeEdit} when it
74 > * finishes.
75 > */
76 > private readonly _pendingEdits = new Map<string, { beforeContent: VSBuffer; beforeExisted: boolean; snapshotDone: Promise<void> }>();
77 >
78 > /**
79 > * Completed edits keyed by file path. Populated by {@link completeEdit};
80 > * drained by {@link takeCompletedEdit}, which persists the entry to
81 > * the database.
82 > */
83 > private readonly _completedEdits = new Map<string, { beforeContent: VSBuffer; beforeExisted: boolean; afterContent: VSBuffer }>();
84 >
85 > constructor(
86 private readonly _sessionUri: string,
87 private readonly _db: ISessionDatabase,
91 @IEditSurvivalReporterFactory private readonly _editSurvivalReporterFactory: IEditSurvivalReporterFactory,
92 ) { }
94 > /**
95 > * Call before an edit tool runs. Reads the file's current content
96 > * into memory as the "before" state. Callers should await this so
97 > * the snapshot captures pre-edit content before the tool writes to
98 > * disk.
99 > *
100 > * @param filePath - Absolute path of the file being edited.
101 > */
102 > async trackEditStart(filePath: string): Promise<void> {
103 const snapshotDone = this._readFileWithExistence(filePath);
104 const entry = {
113 await entry.snapshotDone;
114 }
116 > /**
117 > * Call after an edit tool finishes. Reads the file content again as
118 > * the "after" state and stores the result for later retrieval via
119 > * {@link takeCompletedEdit}.
120 > *
121 > * @param filePath - Absolute path of the file that was edited.
122 > */
123 > async completeEdit(filePath: string): Promise<void> {
124 const pending = this._pendingEdits.get(filePath);
125 if (!pending) {
137 });
138 }
140 > /**
141 > * Retrieves and removes a completed edit for the given file path,
142 > * persists it to the session database with computed diff counts,
143 > * and returns the result as an {@link ToolResultFileEditContent}
144 > * for inclusion in the tool result.
145 > *
146 > * `toolName` and `toolInput` are forwarded to {@link extractAiChunks}
147 > * for region-based survival scoring; unknown shapes fall back to
148 > * whole-file scoring.
149 > */
150 > async takeCompletedEdit(turnId: string, toolCallId: string, filePath: string, toolName: string, toolInput: unknown, modelId: string | undefined): Promise<ToolResultFileEditContent | undefined> {
151 const edit = this._completedEdits.get(filePath);
152 if (!edit) {
217 };
218 }
220 > private async _readFile(filePath: string): Promise<VSBuffer> {
221 try {
222 const content = await this._fileService.readFile(URI.file(filePath));
227 }
228 }
230 > private async _readFileWithExistence(filePath: string): Promise<{ content: VSBuffer; existed: boolean }> {
231 try {
232 const content = await this._fileService.readFile(URI.file(filePath));
src/vs/base/common/arraysFind.ts 95 covered LOC · 16 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- arraysFind.ts
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 > import { Comparator } from './arrays.js';
7 >
8 > export function findLast<T, R extends T>(array: readonly T[], predicate: (item: T, index: number) => item is R, fromIndex?: number): R | undefined;
9 > export function findLast<T>(array: readonly T[], predicate: (item: T, index: number) => unknown, fromIndex?: number): T | undefined;
10 > export function findLast<T>(array: readonly T[], predicate: (item: T, index: number) => unknown, fromIndex = array.length - 1): T | undefined {
11 const idx = findLastIdx(array, predicate, fromIndex);
12 if (idx === -1) {
15 return array[idx];
16 }
18 > export function findLastIdx<T>(array: readonly T[], predicate: (item: T, index: number) => unknown, fromIndex = array.length - 1): number {
19 for (let i = fromIndex; i >= 0; i--) {
20 const element = array[i];
27 return -1;
28 }
30 > export function findFirst<T, R extends T>(array: readonly T[], predicate: (item: T, index: number) => item is R, fromIndex?: number): R | undefined;
31 > export function findFirst<T>(array: readonly T[], predicate: (item: T, index: number) => unknown, fromIndex?: number): T | undefined;
32 > export function findFirst<T>(array: readonly T[], predicate: (item: T, index: number) => unknown, fromIndex = 0): T | undefined {
33 const idx = findFirstIdx(array, predicate, fromIndex);
34 if (idx === -1) {
37 return array[idx];
38 }
40 > export function findFirstIdx<T>(array: readonly T[], predicate: (item: T, index: number) => unknown, fromIndex = 0): number {
41 for (let i = fromIndex; i < array.length; i++) {
42 const element = array[i];
49 return -1;
50 }
52 > /**
53 > * Finds the last item where predicate is true using binary search.
54 > * `predicate` must be monotonous, i.e. `arr.map(predicate)` must be like `[true, ..., true, false, ..., false]`!
55 > *
56 > * @returns `undefined` if no item matches, otherwise the last item that matches the predicate.
57 > */
58 > export function findLastMonotonous<T>(array: readonly T[], predicate: (item: T) => boolean): T | undefined {
59 const idx = findLastIdxMonotonous(array, predicate);
60 return idx === -1 ? undefined : array[idx];
61 }
63 > /**
64 > * Finds the last item where predicate is true using binary search.
65 > * `predicate` must be monotonous, i.e. `arr.map(predicate)` must be like `[true, ..., true, false, ..., false]`!
66 > *
67 > * @returns `startIdx - 1` if predicate is false for all items, otherwise the index of the last item that matches the predicate.
68 > */
69 > export function findLastIdxMonotonous<T>(array: readonly T[], predicate: (item: T) => boolean, startIdx = 0, endIdxEx = array.length): number {
70 let i = startIdx;
71 let j = endIdxEx;
80 return i - 1;
81 }
83 > /**
84 > * Finds the first item where predicate is true using binary search.
85 > * `predicate` must be monotonous, i.e. `arr.map(predicate)` must be like `[false, ..., false, true, ..., true]`!
86 > *
87 > * @returns `undefined` if no item matches, otherwise the first item that matches the predicate.
88 > */
89 > export function findFirstMonotonous<T>(array: readonly T[], predicate: (item: T) => boolean): T | undefined {
90 const idx = findFirstIdxMonotonousOrArrLen(array, predicate);
91 return idx === array.length ? undefined : array[idx];
92 }
94 > /**
95 > * Finds the first item where predicate is true using binary search.
96 > * `predicate` must be monotonous, i.e. `arr.map(predicate)` must be like `[false, ..., false, true, ..., true]`!
97 > *
98 > * @returns `endIdxEx` if predicate is false for all items, otherwise the index of the first item that matches the predicate.
99 > */
100 > export function findFirstIdxMonotonousOrArrLen<T>(array: readonly T[], predicate: (item: T) => boolean, startIdx = 0, endIdxEx = array.length): number {
101 let i = startIdx;
102 let j = endIdxEx;
111 return i;
112 }
114 > export function findFirstIdxMonotonous<T>(array: readonly T[], predicate: (item: T) => boolean, startIdx = 0, endIdxEx = array.length): number {
115 const idx = findFirstIdxMonotonousOrArrLen(array, predicate, startIdx, endIdxEx);
116 return idx === array.length ? -1 : idx;
117 }
119 > /**
120 > * Use this when
121 > * * You have a sorted array
122 > * * You query this array with a monotonous predicate to find the last item that has a certain property.
123 > * * You query this array multiple times with monotonous predicates that get weaker and weaker.
124 > */
125 > export class MonotonousArray<T> {
126 > public static assertInvariants = false;
127 >
128 > private _findLastMonotonousLastIdx = 0;
129 > private _prevFindLastPredicate: ((item: T) => boolean) | undefined;
130 >
131 > constructor(private readonly _array: readonly T[]) {
132 }
134 > /**
135 > * The predicate must be monotonous, i.e. `arr.map(predicate)` must be like `[true, ..., true, false, ..., false]`!
136 > * For subsequent calls, current predicate must be weaker than (or equal to) the previous predicate, i.e. more entries must be `true`.
137 > */
138 > findLastMonotonous(predicate: (item: T) => boolean): T | undefined {
139 if (MonotonousArray.assertInvariants) {
140 if (this._prevFindLastPredicate) {
152 return idx === -1 ? undefined : this._array[idx];
153 }
154 > } arraysFind.ts
155 >
156 > /**
157 > * Returns the first item that is equal to or greater than every other item.
158 > */
159 > export function findFirstMax<T>(array: readonly T[], comparator: Comparator<T>): T | undefined {
160 if (array.length === 0) {
161 return undefined;
171 return max;
172 }
174 > /**
175 > * Returns the last item that is equal to or greater than every other item.
176 > */
177 > export function findLastMax<T>(array: readonly T[], comparator: Comparator<T>): T | undefined {
178 if (array.length === 0) {
179 return undefined;
189 return max;
190 }
192 > /**
193 > * Returns the first item that is equal to or less than every other item.
194 > */
195 > export function findFirstMin<T>(array: readonly T[], comparator: Comparator<T>): T | undefined {
196 return findFirstMax(array, (a, b) => -comparator(a, b));
197 }
199 > export function findMaxIdx<T>(array: readonly T[], comparator: Comparator<T>): number {
200 if (array.length === 0) {
201 return -1;
211 return maxIdx;
212 }
214 > /**
215 > * Returns the first mapped value of the array which is not undefined.
216 > */
217 > export function mapFindFirst<T, R>(items: Iterable<T>, mapFn: (value: T) => R | undefined): R | undefined {
218 for (const value of items) {
219 const mapped = mapFn(value);
src/vs/base/test/common/virtualScheduling/virtualClock.ts 94 covered LOC · 7 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- virtualClock.ts
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 > import { compareBy, numberComparator, tieBreakComparators } from '../../../common/arrays.js';
7 > import { Emitter } from '../../../common/event.js';
8 > import { IDisposable } from '../../../common/lifecycle.js';
9 > import { Trace } from './trace.js';
10 >
11 > export type VirtualTime = number;
12 >
13 > /** Debug source description for an event. */
14 > export interface EventSource {
15 > toString(): string;
16 > readonly stackTrace?: string;
17 > }
18 >
19 > /**
20 > * A unit of work scheduled at a point in virtual time.
21 > *
22 > * Timer callbacks are events. External completions (e.g. fake fs reads) can
23 > * also be modelled as events whose virtual completion time is chosen by a
24 > * scheduling policy — to the {@link VirtualClock} they are indistinguishable.
25 > */
26 > export interface VirtualEvent {
27 > readonly time: VirtualTime;
28 > readonly source: EventSource;
29 > readonly trace?: Trace;
30 > /**
31 > * Hint for the {@link Embedding}: this event prefers to run on a real
32 > * animation frame (e.g. so DOM measurements after it observe a real
33 > * reflow). Pure-time tests can ignore the hint.
34 > */
35 > readonly preferRealAnimationFrame?: boolean;
36 > run(): void;
37 > }
38 >
39 > interface QueuedEvent extends VirtualEvent { readonly id: number }
40 >
41 > const eventComparator = tieBreakComparators<QueuedEvent>(
42 > compareBy(e => e.time, numberComparator),
43 > compareBy(e => e.id, numberComparator),
44 > );
45 >
46 > /**
47 > * A pure data structure: a virtual clock + a priority queue of events.
48 > *
49 > * The clock has no concept of "real time". It is advanced exclusively by
50 > * {@link runNext}, which sets `now` to the next event's `time` before running
51 > * it. The {@link VirtualTimeProcessor} is the only intended driver, but the
52 > * clock is useful in isolation (e.g. for unit-testing a scheduler or for
53 > * stepping a scenario manually).
54 > */
55 > export class VirtualClock {
56 > private _now: VirtualTime;
57 > private _idCounter = 0;
58 > private readonly _queue = new SimplePriorityQueue<QueuedEvent>(eventComparator);
59 > private readonly _onEventScheduled = new Emitter<VirtualEvent>();
60 >
61 > public readonly onEventScheduled = this._onEventScheduled.event;
62 >
63 > constructor(startTime: VirtualTime = 0) {
64 this._now = startTime;
65 }
67 > get now(): VirtualTime { return this._now; }
68 > get hasEvents(): boolean { return this._queue.length > 0; }
69 >
70 > schedule(event: VirtualEvent): IDisposable {
71 if (event.time < this._now) {
72 throw new Error(`Scheduled time (${event.time}) must be >= now (${this._now}).`);
77 return { dispose: () => this._queue.remove(queued) };
78 }
80 > peekNext(): VirtualEvent | undefined { return this._queue.getMin(); }
81 >
82 > runNext(): VirtualEvent | undefined {
83 const e = this._queue.removeMin();
84 if (e) {
88 return e;
89 }
91 > getEvents(): readonly VirtualEvent[] { return this._queue.toSortedArray(); }
92 > }
93 >
94 > class SimplePriorityQueue<T> {
95 > private _items: T[] = [];
96 > private _sorted = true;
97 >
98 > constructor(private readonly _compare: (a: T, b: T) => number) { }
99 >
100 > get length(): number { return this._items.length; }
101 >
102 > add(value: T): void {
103 this._items.push(value);
104 this._sorted = false;
105 }
107 > remove(value: T): void {
108 const i = this._items.indexOf(value);
109 if (i !== -1) { this._items.splice(i, 1); }
110 }
112 > getMin(): T | undefined { this._ensureSorted(); return this._items[0]; }
113 > removeMin(): T | undefined { this._ensureSorted(); return this._items.shift(); }
114 > toSortedArray(): T[] { this._ensureSorted(); return [...this._items]; }
115 >
116 > private _ensureSorted(): void {
117 if (this._sorted) { return; }
118 this._items.sort(this._compare);
119 this._sorted = true;
120 }
121 > } virtualClock.ts
src/vs/platform/agentHost/node/agentHostToolCallTracker.ts 94 covered LOC · 13 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostToolCallTracker.ts
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 > import { disposableTimeout } from '../../../base/common/async.js';
7 > import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js';
8 > import { StopWatch } from '../../../base/common/stopwatch.js';
9 > import type { SessionToolAuthenticationRequest, SessionToolClientExecutionRequest, SessionToolConfirmationRequest } from '../common/state/protocol/state.js';
10 > import { ToolCallContributorKind, type ToolCallContributor, type ToolCallResult } from '../common/state/sessionState.js';
11 > import type { AgentHostTelemetryReporter } from './agentHostTelemetryReporter.js';
12 >
13 > export type ToolInvokedResult = 'success' | 'error' | 'userCancelled';
14 >
15 > const TOOL_CALL_STALL_THRESHOLD_MS = 5 * 60 * 1000;
16 >
17 > type ToolCallBlockerRequest = SessionToolConfirmationRequest | SessionToolClientExecutionRequest | SessionToolAuthenticationRequest;
18 >
19 > /**
20 > * Maps a completed tool call's result to the telemetry result bucket. Mirrors
21 > * the derivation previously done inline in `CopilotAgentSession`: a denied,
22 > * rejected, or cancelled tool call counts as `userCancelled`; any other
23 > * failure counts as `error`.
24 > */
25 > export function deriveToolInvokedResult(result: ToolCallResult): ToolInvokedResult {
26 if (result.success) {
27 return 'success';
33 return 'error';
34 }
36 > /**
37 > * Maps a tool call's contributor to the telemetry `toolSourceKind`. A tool with
38 > * no contributor is provided by the agent host itself; an MCP contributor maps
39 > * to `mcp` and a client contributor to `client`.
40 > */
41 > export function toolSourceKindFromContributor(contributor: ToolCallContributor | undefined): string {
42 if (!contributor) {
43 return 'agentHost';
55 }
56 }
58 > /** Per-tool-call timing state, keyed by `session:toolCallId`. */
59 > interface IToolCallTiming {
60 > readonly stopWatch: StopWatch;
61 > readonly provider: string;
62 > readonly session: string;
63 > readonly toolId: string;
64 > readonly toolSourceKind: string;
65 > }
66 >
67 > interface IStalledToolCall {
68 > readonly blockerKind: ToolCallBlockerRequest['kind'];
69 > readonly completionStopWatch: StopWatch;
70 > }
71 >
72 > /**
73 > * Tracks completed and stalled tool calls for agent host sessions.
74 > *
75 > * Lifecycle per tool call:
76 > * 1. {@link toolCallStarted} — begins a stopwatch and records the tool's
77 > * name and source kind (only the start action carries these)
78 > * 2. {@link toolCallCompleted} — emits the telemetry event and clears state
79 > * 3. {@link toolCallBlocked} / {@link toolCallUnblocked} — emits once when a
80 > * confirmation or client execution remains unresolved past the threshold
81 > *
82 > * In-flight tool calls that never complete (e.g. the turn is cancelled mid
83 > * tool call) are dropped via {@link clearSession} / {@link clear} so the
84 > * tracking map cannot leak.
85 > */
86 > export class AgentHostToolCallTracker extends Disposable {
87 >
88 > private readonly _toolCalls = new Map<string, IToolCallTiming>();
89 > private readonly _toolCallStallTimers = this._register(new DisposableMap<string>());
90 > private readonly _stalledToolCalls = new Map<string, IStalledToolCall>();
91 >
92 > constructor(private readonly _reporter: AgentHostTelemetryReporter) {
94 > }
96 > toolCallStarted(provider: string, session: string, toolCallId: string, toolName: string, contributor: ToolCallContributor | undefined): void {
97 this._toolCalls.set(this._key(session, toolCallId), {
98 stopWatch: StopWatch.create(true),
103 });
104 }
106 > toolCallCompleted(session: string, toolCallId: string, result: ToolCallResult): void {
107 const key = this._key(session, toolCallId);
108 const timing = this._toolCalls.get(key);
141 }
142 }
144 > toolCallBlocked(provider: string, session: string, request: ToolCallBlockerRequest): void {
145 const key = this._key(session, request.id);
146 const toolCallKey = this._key(session, request.toolCall.toolCallId);
163 }, TOOL_CALL_STALL_THRESHOLD_MS));
164 }
166 > toolCallUnblocked(session: string, requestId: string): void {
167 this._toolCallStallTimers.deleteAndDispose(this._key(session, requestId));
168 }
170 > /**
171 > * Drops any in-flight (never-completed) tool calls for a session. Called
172 > * when a turn ends or a session is torn down so the tracking map cannot
173 > * leak. A no-op in the normal case where every tool call completes.
174 > */
175 > clearSession(session: string): void {
176 const prefix = `${session}\0`;
177 for (const key of this._toolCalls.keys()) {
src/vs/platform/configuration/common/configurations.ts 94 covered LOC · 19 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- configurations.ts
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 > import { IStringDictionary } from '../../../base/common/collections.js';
7 > import { Emitter, Event } from '../../../base/common/event.js';
8 > import { Disposable } from '../../../base/common/lifecycle.js';
9 > import { deepClone, equals } from '../../../base/common/objects.js';
10 > import { isEmptyObject, isString } from '../../../base/common/types.js';
11 > import { ConfigurationModel } from './configurationModels.js';
12 > import { Extensions, IConfigurationRegistry, IRegisteredConfigurationPropertySchema } from './configurationRegistry.js';
13 > import { ILogService, NullLogService } from '../../log/common/log.js';
14 > import { IPolicyService, PolicyDefinition, PolicyValue } from '../../policy/common/policy.js';
15 > import { Registry } from '../../registry/common/platform.js';
16 > import { getErrorMessage } from '../../../base/common/errors.js';
17 > import * as json from '../../../base/common/json.js';
18 > import { PolicyName } from '../../../base/common/policy.js';
19 >
20 > export class DefaultConfiguration extends Disposable {
21 >
22 > private readonly _onDidChangeConfiguration = this._register(new Emitter<{ defaults: ConfigurationModel; properties: string[] }>());
23 > readonly onDidChangeConfiguration = this._onDidChangeConfiguration.event;
24 >
25 > private _configurationModel: ConfigurationModel;
26 > get configurationModel(): ConfigurationModel {
27 > return this._configurationModel;
28 > }
29 >
30 > constructor(private readonly logService: ILogService) {
31 super();
32 this._configurationModel = ConfigurationModel.createEmptyModel(logService);
33 }
35 > async initialize(): Promise<ConfigurationModel> {
36 this.resetConfigurationModel();
37 this._register(Registry.as<IConfigurationRegistry>(Extensions.Configuration).onDidUpdateConfiguration(({ properties, defaultsOverrides }) => this.onDidUpdateConfiguration(Array.from(properties), defaultsOverrides)));
38 return this.configurationModel;
39 }
41 > reload(): ConfigurationModel {
42 this.resetConfigurationModel();
43 return this.configurationModel;
44 }
46 > protected onDidUpdateConfiguration(properties: string[], defaultsOverrides?: boolean): void {
47 this.updateConfigurationModel(properties, Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties());
48 this._onDidChangeConfiguration.fire({ defaults: this.configurationModel, properties });
49 }
51 > protected getConfigurationDefaultOverrides(): IStringDictionary<unknown> {
52 return {};
53 }
55 > private resetConfigurationModel(): void {
56 this._configurationModel = ConfigurationModel.createEmptyModel(this.logService);
57 const properties = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties();
58 this.updateConfigurationModel(Object.keys(properties), properties);
59 }
61 > private updateConfigurationModel(properties: string[], configurationProperties: IStringDictionary<IRegisteredConfigurationPropertySchema>): void {
62 const configurationDefaultsOverrides = this.getConfigurationDefaultOverrides();
63 for (const key of properties) {
73 }
74 }
76 > protected getDefaultValue(_key: string, propertySchema: IRegisteredConfigurationPropertySchema): unknown {
77 return deepClone(propertySchema.default);
78 }
80 > }
81 >
82 > export interface IPolicyConfiguration {
83 > readonly onDidChangeConfiguration: Event<ConfigurationModel>;
84 > readonly configurationModel: ConfigurationModel;
85 > initialize(): Promise<ConfigurationModel>;
86 > }
87 >
88 > export class NullPolicyConfiguration implements IPolicyConfiguration {
89 readonly onDidChangeConfiguration = Event.None;
90 readonly configurationModel = ConfigurationModel.createEmptyModel(new NullLogService());
91 > async initialize() { return this.configurationModel; } configurations.ts
92 > }
93 >
94 > type ParsedType = IStringDictionary<unknown> | Array<unknown>;
95 >
96 > export class PolicyConfiguration extends Disposable implements IPolicyConfiguration {
97 >
98 > private readonly _onDidChangeConfiguration = this._register(new Emitter<ConfigurationModel>());
99 > readonly onDidChangeConfiguration = this._onDidChangeConfiguration.event;
100 >
101 > private readonly configurationRegistry: IConfigurationRegistry;
102 >
103 > private _configurationModel: ConfigurationModel;
104 > get configurationModel() { return this._configurationModel; }
105 >
106 > /** Last definition submitted per policy name; avoids redundant re-registration. */
107 > private readonly _submittedPolicyDefinitions = new Map<PolicyName, PolicyDefinition>();
108 >
109 > /** Maps each policy-controlled setting key to its policy name, so removed keys can be re-resolved. */
110 > private readonly _policyNameByKey = new Map<string, PolicyName>();
111 >
112 > constructor(
113 private readonly defaultConfiguration: DefaultConfiguration,
114 @IPolicyService private readonly policyService: IPolicyService,
119 this.configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
120 }
122 > async initialize(): Promise<ConfigurationModel> {
123 this.logService.trace('PolicyConfiguration#initialize');
124
129 return this._configurationModel;
130 }
132 > private toPolicyDefinitionType(configType: unknown, policyName: PolicyName): 'string' | 'number' | 'boolean' | undefined {
133 // `configType` may be a single type or a union (e.g. `['array', 'null']`).
134 // Normalize to an array and keep only the types we can represent as policies.
141 return supportedTypes.includes('number') ? 'number' : supportedTypes.includes('boolean') ? 'boolean' : 'string';
142 }
144 > private async updatePolicyDefinitions(properties: string[]): Promise<string[]> {
145 this.logService.trace('PolicyConfiguration#updatePolicyDefinitions', properties);
146 const keys: string[] = [];
183 return keys;
184 }
186 > private isSamePolicyDefinition(a: PolicyDefinition | undefined, b: PolicyDefinition): boolean {
187 return !!a && a.type === b.type && a.value === b.value && a.managedSettings === b.managedSettings && a.restrictedValue === b.restrictedValue;
188 }
190 > /** Resolve the authoritative definition: owner wins; references provide a bare type fallback. */
191 > private resolvePolicyDefinition(policyName: PolicyName): PolicyDefinition | undefined {
192 const configurationProperties = this.configurationRegistry.getConfigurationProperties();
193 const excludedConfigurationProperties = this.configurationRegistry.getExcludedConfigurationProperties();
214 return undefined;
215 }
217 > private onDidChangePolicies(policyNames: readonly PolicyName[]): void {
218 this.logService.trace('PolicyConfiguration#onDidChangePolicies', policyNames);
219 const policyConfigurations = this.configurationRegistry.getPolicyConfigurations();
232 this.update(keys, true);
233 }
235 > private update(keys: string[], trigger: boolean): void {
236 this.logService.trace('PolicyConfiguration#update', keys);
237 const configurationProperties = this.configurationRegistry.getConfigurationProperties();
286 }
287 }
289 > private parse(content: string): ParsedType {
290 let raw: ParsedType = {};
291 let currentProperty: string | null = null;
src/vs/platform/terminal/common/environmentVariable.ts 93 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- environmentVariable.ts
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 > import { IProcessEnvironment } from '../../../base/common/platform.js';
7 > import { IWorkspaceFolderData } from '../../workspace/common/workspace.js';
8 >
9 > export enum EnvironmentVariableMutatorType {
10 > Replace = 1,
11 > Append = 2,
12 > Prepend = 3
13 > }
14 > export interface IEnvironmentVariableMutator {
15 > readonly variable: string;
16 > readonly value: string;
17 > readonly type: EnvironmentVariableMutatorType;
18 > readonly scope?: EnvironmentVariableScope;
19 > readonly options?: IEnvironmentVariableMutatorOptions;
20 > }
21 >
22 > export interface IEnvironmentVariableCollectionDescription {
23 > readonly description: string | undefined;
24 > readonly scope?: EnvironmentVariableScope;
25 > }
26 >
27 > export interface IEnvironmentVariableMutatorOptions {
28 > applyAtProcessCreation?: boolean;
29 > applyAtShellIntegration?: boolean;
30 > }
31 >
32 > export type EnvironmentVariableScope = {
33 > workspaceFolder?: IWorkspaceFolderData;
34 > };
35 >
36 > export interface IEnvironmentVariableCollection {
37 > readonly map: ReadonlyMap<string, IEnvironmentVariableMutator>;
38 > readonly descriptionMap?: ReadonlyMap<string, IEnvironmentVariableCollectionDescription>;
39 > }
40 >
41 > /** [variable, mutator] */
42 > export type ISerializableEnvironmentVariableCollection = [string, IEnvironmentVariableMutator][];
43 >
44 > export type ISerializableEnvironmentDescriptionMap = [string, IEnvironmentVariableCollectionDescription][];
45 > export interface IExtensionOwnedEnvironmentDescriptionMutator extends IEnvironmentVariableCollectionDescription {
46 > readonly extensionIdentifier: string;
47 > }
48 >
49 > /** [extension, collection, description] */
50 > export type ISerializableEnvironmentVariableCollections = [string, ISerializableEnvironmentVariableCollection, ISerializableEnvironmentDescriptionMap][];
51 >
52 > export interface IExtensionOwnedEnvironmentVariableMutator extends IEnvironmentVariableMutator {
53 > readonly extensionIdentifier: string;
54 > }
55 >
56 > export interface IMergedEnvironmentVariableCollectionDiff {
57 > added: ReadonlyMap<string, IExtensionOwnedEnvironmentVariableMutator[]>;
58 > changed: ReadonlyMap<string, IExtensionOwnedEnvironmentVariableMutator[]>;
59 > removed: ReadonlyMap<string, IExtensionOwnedEnvironmentVariableMutator[]>;
60 > }
61 >
62 > type VariableResolver = (str: string) => Promise<string>;
63 >
64 > /**
65 > * Represents an environment variable collection that results from merging several collections
66 > * together.
67 > */
68 > export interface IMergedEnvironmentVariableCollection {
69 > readonly collections: ReadonlyMap<string, IEnvironmentVariableCollection>;
70 > /**
71 > * Gets the variable map for a given scope.
72 > * @param scope The scope to get the variable map for. If undefined, the global scope is used.
73 > */
74 > getVariableMap(scope: EnvironmentVariableScope | undefined): Map<string, IExtensionOwnedEnvironmentVariableMutator[]>;
75 > /**
76 > * Gets the description map for a given scope.
77 > * @param scope The scope to get the description map for. If undefined, description map for the
78 > * global scope is returned.
79 > */
80 > getDescriptionMap(scope: EnvironmentVariableScope | undefined): Map<string, string | undefined>;
81 > /**
82 > * Applies this collection to a process environment.
83 > * @param variableResolver An optional function to use to resolve variables within the
84 > * environment values.
85 > */
86 > applyToProcessEnvironment(env: IProcessEnvironment, scope: EnvironmentVariableScope | undefined, variableResolver?: VariableResolver): Promise<void>;
87 >
88 > /**
89 > * Generates a diff of this collection against another. Returns undefined if the collections are
90 > * the same.
91 > */
92 > diff(other: IMergedEnvironmentVariableCollection, scope: EnvironmentVariableScope | undefined): IMergedEnvironmentVariableCollectionDiff | undefined;
93 > }
src/vs/platform/agentHost/common/meta/agentToolCallMeta.ts 92 covered LOC · 5 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentToolCallMeta.ts
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 > import type { Mutable } from '../../../../base/common/types.js';
7 >
8 > /** Anything carrying a tool call's `_meta` bag (persisted state or wire actions). */
9 > interface IHasToolCallMeta {
10 > readonly _meta?: Record<string, unknown>;
11 > }
12 >
13 > /**
14 > * Well-known typed view over a tool call's open `_meta` bag. Producers and
15 > * consumers agree on these keys here so the two sides can't drift; always read
16 > * the bag through {@link readToolCallMeta}, which validates each field and drops
17 > * wrong-typed values.
18 > */
19 > export interface IToolCallMeta {
20 > /**
21 > * VS Code rendering hint. `terminal` routes the call to the command/output
22 > * renderer, `subagent` to the subagent UI, `search` to the search renderer;
23 > * everything else falls through to the generic invocation renderer. Set by
24 > * the agent adapter, never matched on raw tool name by the renderer.
25 > */
26 > readonly toolKind?: ToolKind;
27 > /** Shell language for a `terminal` tool call (drives syntax highlighting). */
28 > readonly language?: string;
29 > /** Short task description for a `subagent` tool call (e.g. "Find related files"). */
30 > readonly subagentDescription?: string;
31 > /** Agent name for a `subagent` tool call (e.g. "explore"). */
32 > readonly subagentAgentName?: string;
33 > /** Chat URI of the subagent this tool call spawns, stamped by the host (see {@link buildSubagentChatUri}); the resource may not be registered yet. */
34 > readonly subagentChatUri?: string;
35 > /** Raw, pre-stringified tool arguments captured for display/debugging. */
36 > readonly toolArguments?: unknown;
37 > /** Originating MCP server name, when the call came from an MCP server. */
38 > readonly mcpServerName?: string;
39 > /** Originating MCP tool name, when the call came from an MCP server. */
40 > readonly mcpToolName?: string;
41 > /** MCP App render data, when the call exposes an interactive App surface. */
42 > readonly ui?: IToolCallUiMeta;
43 > /**
44 > * Set by the host's side-effect layer when the call was auto-approved
45 > * because of an `autoApprove` session-config setting (rather than an
46 > * explicit user action), so the client can render it as setting-driven.
47 > */
48 > readonly autoApproveBySetting?: boolean;
49 > /** Transient runtime corpus for the local client tool-search invocation. */
50 > readonly toolSearchCandidates?: readonly IToolSearchCandidate[];
51 > }
52 >
53 > /** Minimal metadata needed to embed and rank a deferred tool. */
54 > export interface IToolSearchCandidate {
55 > readonly name: string;
56 > readonly description: string;
57 > }
58 >
59 > /**
60 > * The set of VS Code-recognized tool-call rendering kinds. Add a new value here
61 > * (and teach the renderer to handle it) rather than matching on tool name.
62 > */
63 > export type ToolKind = 'terminal' | 'subagent' | 'search';
64 >
65 > /**
66 > * MCP App render data carried under {@link IToolCallMeta.ui}. Clients gate
67 > * mounting the App webview on both a `resourceUri` and a `channel` being
68 > * present.
69 > */
70 > export interface IToolCallUiMeta {
71 > /** The MCP App's UI resource URI (an `ui://` resource the App renders). */
72 > readonly resourceUri: string;
73 > /** AHP `mcp://` channel the App's sub-RPCs route back through, when ready. */
74 > readonly channel?: string;
75 > }
76 >
77 function isToolKind(value: unknown): value is ToolKind {
78 return value === 'terminal' || value === 'subagent' || value === 'search';
79 }
81 function readToolCallUiMeta(value: unknown): IToolCallUiMeta | undefined {
82 if (!value || typeof value !== 'object' || Array.isArray(value)) {
93 return result;
94 }
96 function readToolSearchCandidates(value: unknown): readonly IToolSearchCandidate[] | undefined {
97 if (!Array.isArray(value)) {
114 return result;
115 }
117 > /**
118 > * Reads the well-known {@link IToolCallMeta} keys from a tool call's `_meta`
119 > * bag, dropping unknown keys and wrong-typed values.
120 > */
121 > export function readToolCallMeta(source: IHasToolCallMeta): IToolCallMeta {
122 const meta = source._meta;
123 if (!meta) {
140 return result;
141 }
143 > /**
144 > * Serializes a typed {@link IToolCallMeta} into the `_meta` record, dropping
145 > * `undefined` entries and returning `undefined` when empty. Build a tool call's
146 > * `_meta` through this so producers stay in lock-step with
147 > * {@link readToolCallMeta}.
148 > */
149 > export function toToolCallMeta(meta: IToolCallMeta): Record<string, unknown> | undefined {
150 const result: Record<string, unknown> = {};
151 for (const [key, value] of Object.entries(meta)) {
src/vs/base/common/comparers.ts 91 covered LOC · 24 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- comparers.ts
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 > import { safeIntl } from './date.js';
7 > import { Lazy } from './lazy.js';
8 > import { sep } from './path.js';
9 >
10 > // When comparing large numbers of strings it's better for performance to create an
11 > // Intl.Collator object and use the function provided by its compare property
12 > // than it is to use String.prototype.localeCompare()
13 >
14 > // A collator with numeric sorting enabled, and no sensitivity to case, accents or diacritics.
15 > const intlFileNameCollatorBaseNumeric: Lazy<{ collator: Intl.Collator; collatorIsNumeric: boolean }> = new Lazy(() => {
16 const collator = safeIntl.Collator(undefined, { numeric: true, sensitivity: 'base' }).value;
17 return {
20 };
21 });
23 > // A collator with numeric sorting enabled.
24 > const intlFileNameCollatorNumeric: Lazy<{ collator: Intl.Collator }> = new Lazy(() => {
25 const collator = safeIntl.Collator(undefined, { numeric: true }).value;
26 return {
28 };
29 });
31 > // A collator with numeric sorting enabled, and sensitivity to accents and diacritics but not case.
32 > const intlFileNameCollatorNumericCaseInsensitive: Lazy<{ collator: Intl.Collator }> = new Lazy(() => {
33 const collator = safeIntl.Collator(undefined, { numeric: true, sensitivity: 'accent' }).value;
34 return {
36 };
37 });
39 > /** Compares filenames without distinguishing the name from the extension. Disambiguates by unicode comparison. */
40 > export function compareFileNames(one: string | null, other: string | null, caseSensitive = false): number {
41 const a = one || '';
42 const b = other || '';
50 return result;
51 }
53 > /** Compares full filenames without grouping by case. */
54 > export function compareFileNamesDefault(one: string | null, other: string | null): number {
55 const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
56 one = one || '';
59 return compareAndDisambiguateByLength(collatorNumeric, one, other);
60 }
62 > /** Compares full filenames grouping uppercase names before lowercase. */
63 > export function compareFileNamesUpper(one: string | null, other: string | null) {
64 const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
65 one = one || '';
68 return compareCaseUpperFirst(one, other) || compareAndDisambiguateByLength(collatorNumeric, one, other);
69 }
71 > /** Compares full filenames grouping lowercase names before uppercase. */
72 > export function compareFileNamesLower(one: string | null, other: string | null) {
73 const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
74 one = one || '';
77 return compareCaseLowerFirst(one, other) || compareAndDisambiguateByLength(collatorNumeric, one, other);
78 }
80 > /** Compares full filenames by unicode value. */
81 > export function compareFileNamesUnicode(one: string | null, other: string | null) {
82 one = one || '';
83 other = other || '';
89 return one < other ? -1 : 1;
90 }
92 > /** Compares filenames by extension, then by name. Disambiguates by unicode comparison. */
93 > export function compareFileExtensions(one: string | null, other: string | null): number {
94 const [oneName, oneExtension] = extractNameAndExtension(one);
95 const [otherName, otherExtension] = extractNameAndExtension(other);
113 return result;
114 }
115 > comparers.ts
116 > /** Compares filenames by extension, then by full filename. Mixes uppercase and lowercase names together. */
117 > export function compareFileExtensionsDefault(one: string | null, other: string | null): number {
118 one = one || '';
119 other = other || '';
126 compareAndDisambiguateByLength(collatorNumeric, one, other);
127 }
128 > comparers.ts
129 > /** Compares filenames by extension, then case, then full filename. Groups uppercase names before lowercase. */
130 > export function compareFileExtensionsUpper(one: string | null, other: string | null): number {
131 one = one || '';
132 other = other || '';
140 compareAndDisambiguateByLength(collatorNumeric, one, other);
141 }
142 > comparers.ts
143 > /** Compares filenames by extension, then case, then full filename. Groups lowercase names before uppercase. */
144 > export function compareFileExtensionsLower(one: string | null, other: string | null): number {
145 one = one || '';
146 other = other || '';
154 compareAndDisambiguateByLength(collatorNumeric, one, other);
155 }
156 > comparers.ts
157 > /** Compares filenames by case-insensitive extension unicode value, then by full filename unicode value. */
158 > export function compareFileExtensionsUnicode(one: string | null, other: string | null) {
159 one = one || '';
160 other = other || '';
174 return 0;
175 }
176 > comparers.ts
177 > const FileNameMatch = /^(.*?)(\.([^.]*))?$/;
178 >
179 > /** Extracts the name and extension from a full filename, with optional special handling for dotfiles */
180 function extractNameAndExtension(str?: string | null, dotfilesAsNames = false): [string, string] {
181 const match = str ? FileNameMatch.exec(str) as Array<string> : ([] as Array<string>);
191 return result;
192 }
193 > comparers.ts
194 > /** Extracts the extension from a full filename. Treats dotfiles as names, not extensions. */
195 function extractExtension(str?: string | null): string {
196 const match = str ? FileNameMatch.exec(str) as Array<string> : ([] as Array<string>);
198 return (match && match[1] && match[1].charAt(0) !== '.' && match[3]) || '';
199 }
200 > comparers.ts
201 function compareAndDisambiguateByLength(collator: Intl.Collator, one: string, other: string) {
202 // Check for differences
214 return 0;
215 }
216 > comparers.ts
217 > /** @returns `true` if the string is starts with a lowercase letter. Otherwise, `false`. */
218 function startsWithLower(string: string) {
219 const character = string.charAt(0);
221 return (character.toLocaleUpperCase() !== character) ? true : false;
222 }
223 > comparers.ts
224 > /** @returns `true` if the string starts with an uppercase letter. Otherwise, `false`. */
225 function startsWithUpper(string: string) {
226 const character = string.charAt(0);
228 return (character.toLocaleLowerCase() !== character) ? true : false;
229 }
230 > comparers.ts
231 > /**
232 > * Compares the case of the provided strings - lowercase before uppercase
233 > *
234 > * @returns
235 > * ```text
236 > * -1 if one is lowercase and other is uppercase
237 > * 1 if one is uppercase and other is lowercase
238 > * 0 otherwise
239 > * ```
240 > */
241 function compareCaseLowerFirst(one: string, other: string): number {
242 if (startsWithLower(one) && startsWithUpper(other)) {
245 return (startsWithUpper(one) && startsWithLower(other)) ? 1 : 0;
246 }
247 > comparers.ts
248 > /**
249 > * Compares the case of the provided strings - uppercase before lowercase
250 > *
251 > * @returns
252 > * ```text
253 > * -1 if one is uppercase and other is lowercase
254 > * 1 if one is lowercase and other is uppercase
255 > * 0 otherwise
256 > * ```
257 > */
258 function compareCaseUpperFirst(one: string, other: string): number {
259 if (startsWithUpper(one) && startsWithLower(other)) {
262 return (startsWithLower(one) && startsWithUpper(other)) ? 1 : 0;
263 }
264 > comparers.ts
265 function comparePathComponents(one: string, other: string, caseSensitive = false): number {
266 if (!caseSensitive) {
275 return one < other ? -1 : 1;
276 }
277 > comparers.ts
278 > export function comparePaths(one: string, other: string, caseSensitive = false): number {
279 const oneParts = one.split(sep);
280 const otherParts = other.split(sep);
303 }
304 }
305 > comparers.ts
306 > export function compareAnything(one: string, other: string, lookFor: string): number {
307 const elementAName = one.toLowerCase();
308 const elementBName = other.toLowerCase();
330 return elementAName.localeCompare(elementBName);
331 }
332 > comparers.ts
333 > export function compareByPrefix(one: string, other: string, lookFor: string): number {
334 const elementAName = one.toLowerCase();
335 const elementBName = other.toLowerCase();
src/vs/base/common/extpath.ts 91 covered LOC · 17 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- extpath.ts
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 > import { CharCode } from './charCode.js';
7 > import { isAbsolute, join, normalize, posix, sep } from './path.js';
8 > import { isWindows } from './platform.js';
9 > import { equalsIgnoreCase, rtrim, startsWithIgnoreCase } from './strings.js';
10 > import { isNumber } from './types.js';
11 >
12 > export function isPathSeparator(code: number) {
13 return code === CharCode.Slash || code === CharCode.Backslash;
14 }
15 > extpath.ts
16 > /**
17 > * Takes a Windows OS path and changes backward slashes to forward slashes.
18 > * This should only be done for OS paths from Windows (or user provided paths potentially from Windows).
19 > * Using it on a Linux or MaxOS path might change it.
20 > */
21 > export function toSlashes(osPath: string) {
22 return osPath.replace(/[\\/]/g, posix.sep);
23 }
24 > extpath.ts
25 > /**
26 > * Takes a Windows OS path (using backward or forward slashes) and turns it into a posix path:
27 > * - turns backward slashes into forward slashes
28 > * - makes it absolute if it starts with a drive letter
29 > * This should only be done for OS paths from Windows (or user provided paths potentially from Windows).
30 > * Using it on a Linux or MaxOS path might change it.
31 > */
32 > export function toPosixPath(osPath: string) {
33 if (osPath.indexOf('/') === -1) {
34 osPath = toSlashes(osPath);
39 return osPath;
40 }
41 > extpath.ts
42 > /**
43 > * Computes the _root_ this path, like `getRoot('c:\files') === c:\`,
44 > * `getRoot('files:///files/path') === files:///`,
45 > * or `getRoot('\\server\shares\path') === \\server\shares\`
46 > */
47 > export function getRoot(path: string, sep: string = posix.sep): string {
48 if (!path) {
49 return '';
111 return '';
112 }
113 > extpath.ts
114 > /**
115 > * Check if the path follows this pattern: `\\hostname\sharename`.
116 > *
117 > * @see https://msdn.microsoft.com/en-us/library/gg465305.aspx
118 > * @return A boolean indication if the path is a UNC path, on none-windows
119 > * always false.
120 > */
121 > export function isUNC(path: string): boolean {
122 if (!isWindows) {
123 // UNC is a windows concept
162 return true;
163 }
164 > extpath.ts
165 > // Reference: https://en.wikipedia.org/wiki/Filename
166 > const WINDOWS_INVALID_FILE_CHARS = /[\\/:\*\?"<>\|]/g;
167 > const UNIX_INVALID_FILE_CHARS = /[/]/g;
168 > const WINDOWS_FORBIDDEN_NAMES = /^(con|prn|aux|clock\$|nul|lpt[0-9]|com[0-9])(\.(.*?))?$/i;
169 > export function isValidBasename(name: string | null | undefined, isWindowsOS: boolean = isWindows): boolean {
170 const invalidFileChars = isWindowsOS ? WINDOWS_INVALID_FILE_CHARS : UNIX_INVALID_FILE_CHARS;
171
201 return true;
202 }
203 > extpath.ts
204 > /**
205 > * @deprecated please use `IUriIdentityService.extUri.isEqual` instead. If you are
206 > * in a context without services, consider to pass down the `extUri` from the outside
207 > * or use `extUriBiasedIgnorePathCase` if you know what you are doing.
208 > */
209 > export function isEqual(pathA: string, pathB: string, ignoreCase?: boolean): boolean {
210 const identityEquals = (pathA === pathB);
211 if (!ignoreCase || identityEquals) {
219 return equalsIgnoreCase(pathA, pathB);
220 }
221 > extpath.ts
222 > /**
223 > * @deprecated please use `IUriIdentityService.extUri.isEqualOrParent` instead. If
224 > * you are in a context without services, consider to pass down the `extUri` from the
225 > * outside, or use `extUriBiasedIgnorePathCase` if you know what you are doing.
226 > */
227 > export function isEqualOrParent(base: string, parentCandidate: string, ignoreCase?: boolean, forcePosixSemantics = false): boolean {
228 const separator = forcePosixSemantics ? posix.sep : sep;
229
272 return base.indexOf(parentCandidate) === 0;
273 }
274 > extpath.ts
275 > export function isWindowsDriveLetter(char0: number): boolean {
276 return char0 >= CharCode.A && char0 <= CharCode.Z || char0 >= CharCode.a && char0 <= CharCode.z;
277 }
278 > extpath.ts
279 > export function sanitizeFilePath(candidate: string, cwd: string): string {
280
281 // Special case: allow to open a drive letter without trailing backslash
295 return removeTrailingPathSeparator(candidate);
296 }
297 > extpath.ts
298 > export function removeTrailingPathSeparator(candidate: string): string {
299 if (isWindows) {
300 candidate = rtrim(candidate, sep);
316 return candidate;
317 }
318 > extpath.ts
319 > export function isRootOrDriveLetter(path: string): boolean {
320 const pathNormalized = normalize(path);
321
331 return pathNormalized === posix.sep;
332 }
333 > extpath.ts
334 > export function hasDriveLetter(path: string, isWindowsOS: boolean = isWindows): boolean {
335 if (isWindowsOS) {
336 return isWindowsDriveLetter(path.charCodeAt(0)) && path.charCodeAt(1) === CharCode.Colon;
339 return false;
340 }
341 > extpath.ts
342 > export function getDriveLetter(path: string, isWindowsOS: boolean = isWindows): string | undefined {
343 return hasDriveLetter(path, isWindowsOS) ? path[0] : undefined;
344 }
345 > extpath.ts
346 > export function indexOfPath(path: string, candidate: string, ignoreCase?: boolean): number {
347 if (candidate.length > path.length) {
348 return -1;
360 return path.indexOf(candidate);
361 }
362 > extpath.ts
363 > export interface IPathWithLineAndColumn {
364 > path: string;
365 > line?: number;
366 > column?: number;
367 > }
368 >
369 > export function parseLineAndColumnAware(rawPath: string): IPathWithLineAndColumn {
370 const segments = rawPath.split(':'); // C:\file.txt:<line>:<column>
371
395 };
396 }
397 > extpath.ts
398 > const pathChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
399 > const windowsSafePathFirstChars = 'BDEFGHIJKMOQRSTUVWXYZbdefghijkmoqrstuvwxyz0123456789';
400 >
401 > export function randomPath(parent?: string, prefix?: string, randomLength = 8): string {
402 let suffix = '';
403 for (let i = 0; i < randomLength; i++) {
src/vs/base/common/observableInternal/utils/utils.ts 89 covered LOC · 21 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- utils.ts
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 > import { autorun } from '../reactions/autorun.js';
7 > import { IObservable, IObservableWithChange, IObserver, IReader, ITransaction } from '../base.js';
8 > import { observableValue } from '../observables/observableValue.js';
9 > import { DebugOwner } from '../debugName.js';
10 > import { DisposableStore, Event, IDisposable, toDisposable } from '../commonFacade/deps.js';
11 > import { derived, derivedOpts } from '../observables/derived.js';
12 > import { observableFromEvent } from '../observables/observableFromEvent.js';
13 > import { observableSignal } from '../observables/observableSignal.js';
14 > import { _setKeepObserved, _setRecomputeInitiallyAndOnChange } from '../observables/baseObservable.js';
15 > import { DebugLocation } from '../debugLocation.js';
16 >
17 > export function observableFromPromise<T>(promise: Promise<T>): IObservable<{ value?: T }> {
18 const observable = observableValue<{ value?: T }>('promiseValue', {});
19 promise.then((value) => {
22 return observable;
23 }
24 > utils.ts
25 > export function signalFromObservable<T>(owner: DebugOwner | undefined, observable: IObservable<T>): IObservable<void> {
26 return derivedOpts({
27 owner,
31 });
32 }
33 > utils.ts
34 > /**
35 > * Creates an observable that debounces the input observable.
36 > */
37 > export function debouncedObservable<T>(observable: IObservable<T>, debounceMs: number | ((lastValue: T | undefined, newValue: T) => number), debugLocation = DebugLocation.ofCaller()): IObservable<T> {
38 let hasValue = false;
39 let lastValue: T | undefined;
79 }, debugLocation);
80 }
81 > utils.ts
82 > /**
83 > * Creates an observable that throttles the input observable.
84 > * Unlike {@link debouncedObservable}, the timer starts on the first change
85 > * and is not reset by subsequent changes, preventing starvation.
86 > */
87 > export function throttledObservable<T>(observable: IObservable<T>, throttleMs: number, debugLocation = DebugLocation.ofCaller()): IObservable<T> {
88 let hasValue = false;
89 let lastValue: T | undefined;
126 }, debugLocation);
127 }
128 > utils.ts
129 > /**
130 > * Creates an observable that debounces the input observable.
131 > */
132 > export function debouncedObservable2<T>(observable: IObservable<T>, debounceMs: number | ((currentValue: T | undefined, newValue: T) => number), debugLocation = DebugLocation.ofCaller()): IObservable<T> {
133 const s = observableSignal('handleTimeout');
134
167 return d;
168 }
169 > utils.ts
170 > export function wasEventTriggeredRecently(event: Event<any>, timeoutMs: number, disposableStore: DisposableStore): IObservable<boolean> {
171 const observable = observableValue('triggeredRecently', false);
172
186 return observable;
187 }
188 > utils.ts
189 > /**
190 > * This makes sure the observable is being observed and keeps its cache alive.
191 > */
192 > export function keepObserved<T>(observable: IObservable<T>): IDisposable {
193 const o = new KeepAliveObserver(false, undefined);
194 observable.addObserver(o);
197 });
198 }
199 > utils.ts
200 > _setKeepObserved(keepObserved);
201 >
202 > /**
203 > * This converts the given observable into an autorun.
204 > */
205 > export function recomputeInitiallyAndOnChange<T>(observable: IObservable<T>, handleValue?: (value: T) => void): IDisposable {
206 const o = new KeepAliveObserver(true, handleValue);
207 observable.addObserver(o);
216 });
217 }
218 > utils.ts
219 > _setRecomputeInitiallyAndOnChange(recomputeInitiallyAndOnChange);
220 >
221 > export class KeepAliveObserver implements IObserver {
222 > private _counter = 0;
223 >
224 > constructor(
225 private readonly _forceRecompute: boolean,
226 private readonly _handleValue: ((value: any) => void) | undefined,
227 ) { }
228 > utils.ts
229 > beginUpdate<T>(observable: IObservable<T>): void {
230 this._counter++;
231 }
232 > utils.ts
233 > endUpdate<T>(observable: IObservable<T>): void {
234 if (this._counter === 1 && this._forceRecompute) {
235 if (this._handleValue) {
241 this._counter--;
242 }
243 > utils.ts
244 > handlePossibleChange<T>(observable: IObservable<T>): void {
245 // NO OP
246 }
247 > utils.ts
248 > handleChange<T, TChange>(observable: IObservableWithChange<T, TChange>, change: TChange): void {
249 // NO OP
250 }
251 > } utils.ts
252 >
253 > export function derivedObservableWithCache<T>(owner: DebugOwner, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable<T> {
254 let lastValue: T | undefined = undefined;
255 const observable = derivedOpts({ owner, debugReferenceFn: computeFn }, reader => {
259 return observable;
260 }
261 > utils.ts
262 > export function derivedObservableWithWritableCache<T>(owner: object, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable<T>
263 & { clearCache(transaction: ITransaction): void; setCache(newValue: T | undefined, tx: ITransaction | undefined): void } {
264 let lastValue: T | undefined = undefined;
280 });
281 }
282 > utils.ts
283 > /**
284 > * When the items array changes, referential equal items are not mapped again.
285 > */
286 > export function mapObservableArrayCached<TIn, TOut, TKey = TIn>(owner: DebugOwner, items: IObservable<readonly TIn[]>, map: (input: TIn, store: DisposableStore) => TOut, keySelector?: (input: TIn) => TKey): IObservable<readonly TOut[]> {
287 let m = new ArrayMap(map, keySelector);
288 const self = derivedOpts({
300 return self;
301 }
302 > utils.ts
303 > class ArrayMap<TIn, TOut, TKey> implements IDisposable {
304 > private readonly _cache = new Map<TKey, { out: TOut; store: DisposableStore }>();
305 > private _items: TOut[] = [];
306 > constructor(
307 private readonly _map: (input: TIn, store: DisposableStore) => TOut,
308 private readonly _keySelector?: (input: TIn) => TKey,
309 ) {
310 }
311 > utils.ts
312 > public dispose(): void {
313 this._cache.forEach(entry => entry.store.dispose());
314 this._cache.clear();
315 }
316 > utils.ts
317 > public setItems(items: readonly TIn[]): void {
318 const newItems: TOut[] = [];
319 const itemsToRemove = new Set(this._cache.keys());
342 this._items = newItems;
343 }
344 > utils.ts
345 > public getItems(): TOut[] {
346 return this._items;
347 }
348 > } utils.ts
349 >
350 > export function isObservable<T>(obj: unknown): obj is IObservable<T> {
351 return !!obj && (<IObservable<T>>obj).read !== undefined && (<IObservable<T>>obj).reportChanges !== undefined;
352 }
src/vs/platform/configuration/common/configurationService.ts 89 covered LOC · 18 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- configurationService.ts
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 > import { distinct, equals as arrayEquals } from '../../../base/common/arrays.js';
7 > import { Queue, RunOnceScheduler } from '../../../base/common/async.js';
8 > import { VSBuffer } from '../../../base/common/buffer.js';
9 > import { Emitter, Event } from '../../../base/common/event.js';
10 > import { JSONPath, ParseError, parse } from '../../../base/common/json.js';
11 > import { applyEdits, setProperty } from '../../../base/common/jsonEdit.js';
12 > import { Edit, FormattingOptions } from '../../../base/common/jsonFormatter.js';
13 > import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
14 > import { ResourceMap } from '../../../base/common/map.js';
15 > import { equals } from '../../../base/common/objects.js';
16 > import { OS, OperatingSystem } from '../../../base/common/platform.js';
17 > import { extUriBiasedIgnorePathCase } from '../../../base/common/resources.js';
18 > import { URI } from '../../../base/common/uri.js';
19 > import { ConfigurationTarget, IConfigurationChange, IConfigurationChangeEvent, IConfigurationData, IConfigurationOverrides, IConfigurationService, IConfigurationUpdateOptions, IConfigurationUpdateOverrides, IConfigurationValue, isConfigurationOverrides, isConfigurationUpdateOverrides } from './configuration.js';
20 > import { Configuration, ConfigurationChangeEvent, ConfigurationModel, UserSettings } from './configurationModels.js';
21 > import { keyFromOverrideIdentifiers } from './configurationRegistry.js';
22 > import { DefaultConfiguration, IPolicyConfiguration, NullPolicyConfiguration, PolicyConfiguration } from './configurations.js';
23 > import { FileOperationError, FileOperationResult, IFileService } from '../../files/common/files.js';
24 > import { ILogService } from '../../log/common/log.js';
25 > import { IPolicyService, NullPolicyService } from '../../policy/common/policy.js';
26 >
27 > export class ConfigurationService extends Disposable implements IConfigurationService, IDisposable {
28 >
29 > declare readonly _serviceBrand: undefined;
30 >
31 > private configuration: Configuration;
32 > private readonly defaultConfiguration: DefaultConfiguration;
33 > private readonly policyConfiguration: IPolicyConfiguration;
34 > private readonly userConfiguration: UserSettings;
35 > private readonly reloadConfigurationScheduler: RunOnceScheduler;
36 >
37 > private readonly _onDidChangeConfiguration: Emitter<IConfigurationChangeEvent> = this._register(new Emitter<IConfigurationChangeEvent>());
38 > readonly onDidChangeConfiguration: Event<IConfigurationChangeEvent> = this._onDidChangeConfiguration.event;
39 >
40 > private readonly configurationEditing: ConfigurationEditing;
41 >
42 > constructor(
43 private readonly settingsResource: URI,
44 fileService: IFileService,
69 this._register(this.userConfiguration.onDidChange(() => this.reloadConfigurationScheduler.schedule()));
70 }
72 > async initialize(): Promise<void> {
73 const [defaultModel, policyModel, userModel] = await Promise.all([this.defaultConfiguration.initialize(), this.policyConfiguration.initialize(), this.userConfiguration.loadConfiguration()]);
74 this.configuration = new Configuration(
85 );
86 }
88 > getConfigurationData(): IConfigurationData {
89 return this.configuration.toData();
90 }
92 > getValue<T>(): T;
93 > getValue<T>(section: string): T;
94 > getValue<T>(overrides: IConfigurationOverrides): T;
95 > getValue<T>(section: string, overrides: IConfigurationOverrides): T;
96 > getValue(arg1?: unknown, arg2?: unknown): unknown {
97 const section = typeof arg1 === 'string' ? arg1 : undefined;
98 const overrides = isConfigurationOverrides(arg1) ? arg1 : isConfigurationOverrides(arg2) ? arg2 : {};
99 return this.configuration.getValue(section, overrides, undefined);
100 }
102 > updateValue(key: string, value: unknown): Promise<void>;
103 > updateValue(key: string, value: unknown, overrides: IConfigurationOverrides | IConfigurationUpdateOverrides): Promise<void>;
104 > updateValue(key: string, value: unknown, target: ConfigurationTarget): Promise<void>;
105 > updateValue(key: string, value: unknown, overrides: IConfigurationOverrides | IConfigurationUpdateOverrides, target: ConfigurationTarget, options?: IConfigurationUpdateOptions): Promise<void>;
106 > async updateValue(key: string, value: unknown, arg3?: unknown, arg4?: unknown, options?: IConfigurationUpdateOptions): Promise<void> {
107 const overrides: IConfigurationUpdateOverrides | undefined = isConfigurationUpdateOverrides(arg3) ? arg3
108 : isConfigurationOverrides(arg3) ? { resource: arg3.resource, overrideIdentifiers: arg3.overrideIdentifier ? [arg3.overrideIdentifier] : undefined } : undefined;
143 await this.reloadConfiguration();
144 }
146 > inspect<T>(key: string, overrides: IConfigurationOverrides = {}): IConfigurationValue<T> {
147 return this.configuration.inspect<T>(key, overrides, undefined);
148 }
150 > keys(): {
151 default: string[];
152 policy: string[];
157 return this.configuration.keys(undefined);
158 }
160 > async reloadConfiguration(): Promise<void> {
161 const configurationModel = await this.userConfiguration.loadConfiguration();
162 this.onDidChangeUserConfiguration(configurationModel);
163 }
165 > private onDidChangeUserConfiguration(userConfigurationModel: ConfigurationModel): void {
166 const previous = this.configuration.toData();
167 const change = this.configuration.compareAndUpdateLocalUserConfiguration(userConfigurationModel);
168 this.trigger(change, previous, ConfigurationTarget.USER);
169 }
171 > private onDidDefaultConfigurationChange(defaultConfigurationModel: ConfigurationModel, properties: string[]): void {
172 const previous = this.configuration.toData();
173 const change = this.configuration.compareAndUpdateDefaultConfiguration(defaultConfigurationModel, properties);
174 this.trigger(change, previous, ConfigurationTarget.DEFAULT);
175 }
177 > private onDidPolicyConfigurationChange(policyConfiguration: ConfigurationModel): void {
178 const previous = this.configuration.toData();
179 const change = this.configuration.compareAndUpdatePolicyConfiguration(policyConfiguration);
180 this.trigger(change, previous, ConfigurationTarget.DEFAULT);
181 }
183 > private trigger(configurationChange: IConfigurationChange, previous: IConfigurationData, source: ConfigurationTarget): void {
184 const event = new ConfigurationChangeEvent(configurationChange, { data: previous }, this.configuration, undefined, this.logService);
185 event.source = source;
186 this._onDidChangeConfiguration.fire(event);
187 }
189 >
190 > class ConfigurationEditing {
191 >
192 > private readonly queue: Queue<void>;
193 >
194 > constructor(
195 private readonly settingsResource: URI,
196 private readonly fileService: IFileService,
199 this.queue = new Queue<void>();
200 }
202 > write(path: JSONPath, value: unknown): Promise<void> {
203 return this.queue.queue(() => this.doWriteConfiguration(path, value)); // queue up writes to prevent race conditions
204 }
206 > private async doWriteConfiguration(path: JSONPath, value: unknown): Promise<void> {
207 let content: string;
208 try {
228 await this.fileService.writeFile(this.settingsResource, VSBuffer.fromString(content));
229 }
231 > private getEdits(content: string, path: JSONPath, value: unknown): Edit[] {
232 const { tabSize, insertSpaces, eol } = this.formattingOptions;
233
244 return setProperty(content, path, value, { tabSize, insertSpaces, eol });
245 }
247 > private _formattingOptions: Required<FormattingOptions> | undefined;
248 > private get formattingOptions(): Required<FormattingOptions> {
249 if (!this._formattingOptions) {
250 let eol = OS === OperatingSystem.Linux || OS === OperatingSystem.Macintosh ? '\n' : '\r\n';
src/vs/base/common/cache.ts 88 covered LOC · 9 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- cache.ts
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 > import { CancellationToken, CancellationTokenSource } from './cancellation.js';
7 > import { IDisposable } from './lifecycle.js';
8 >
9 > export interface CacheResult<T> extends IDisposable {
10 > promise: Promise<T>;
11 > }
12 >
13 > export class Cache<T> {
14 >
15 > private result: CacheResult<T> | null = null;
16 > constructor(private task: (ct: CancellationToken) => Promise<T>) { }
17 >
18 > get(): CacheResult<T> {
19 if (this.result) {
20 return this.result;
35 return this.result;
36 }
37 > } cache.ts
38 >
39 > export function identity<T>(t: T): T {
40 return t;
41 }
42 > cache.ts
43 > interface ICacheOptions<TArg> {
44 > /**
45 > * The cache key is used to identify the cache entry.
46 > * Strict equality is used to compare cache keys.
47 > */
48 > getCacheKey: (arg: TArg) => unknown;
49 > }
50 >
51 > /**
52 > * Uses a LRU cache to make a given parametrized function cached.
53 > * Caches just the last key/value.
54 > */
55 > export class LRUCachedFunction<TArg, TComputed> {
56 > private lastCache: TComputed | undefined = undefined;
57 > private lastArgKey: unknown | undefined = undefined;
58 >
59 > private readonly _fn: (arg: TArg) => TComputed;
60 > private readonly _computeKey: (arg: TArg) => unknown;
61 >
62 > constructor(fn: (arg: TArg) => TComputed);
63 > constructor(options: ICacheOptions<TArg>, fn: (arg: TArg) => TComputed);
64 > constructor(arg1: ICacheOptions<TArg> | ((arg: TArg) => TComputed), arg2?: (arg: TArg) => TComputed) {
65 > if (typeof arg1 === 'function') {
66 > this._fn = arg1;
67 > this._computeKey = identity;
68 > } else {
69 this._fn = arg2!;
70 this._computeKey = arg1.getCacheKey;
71 }
72 > } cache.ts
73 >
74 > public get(arg: TArg): TComputed {
75 const key = this._computeKey(arg);
76 if (this.lastArgKey !== key) {
80 return this.lastCache!;
81 }
82 > } cache.ts
83 >
84 > /**
85 > * Uses an unbounded cache to memoize the results of the given function.
86 > */
87 > export class CachedFunction<TArg, TComputed> {
88 > private readonly _map = new Map<TArg, TComputed>();
89 > private readonly _map2 = new Map<unknown, TComputed>();
90 > public get cachedValues(): ReadonlyMap<TArg, TComputed> {
91 > return this._map;
92 > }
93 >
94 > private readonly _fn: (arg: TArg) => TComputed;
95 > private readonly _computeKey: (arg: TArg) => unknown;
96 >
97 > constructor(fn: (arg: TArg) => TComputed);
98 > constructor(options: ICacheOptions<TArg>, fn: (arg: TArg) => TComputed);
99 > constructor(arg1: ICacheOptions<TArg> | ((arg: TArg) => TComputed), arg2?: (arg: TArg) => TComputed) {
100 if (typeof arg1 === 'function') {
101 this._fn = arg1;
106 }
107 }
108 > cache.ts
109 > public get(arg: TArg): TComputed {
110 const key = this._computeKey(arg);
111 if (this._map2.has(key)) {
118 return value;
119 }
120 > } cache.ts
121 >
122 > /**
123 > * Uses an unbounded cache to memoize the results of the given function.
124 > */
125 > export class WeakCachedFunction<TArg, TComputed> {
126 > private readonly _map = new WeakMap<WeakKey, TComputed>();
127 >
128 > private readonly _fn: (arg: TArg) => TComputed;
129 > private readonly _computeKey: (arg: TArg) => unknown;
130 >
131 > constructor(fn: (arg: TArg) => TComputed);
132 > constructor(options: ICacheOptions<TArg>, fn: (arg: TArg) => TComputed);
133 > constructor(arg1: ICacheOptions<TArg> | ((arg: TArg) => TComputed), arg2?: (arg: TArg) => TComputed) {
134 if (typeof arg1 === 'function') {
135 this._fn = arg1;
140 }
141 }
142 > cache.ts
143 > public get(arg: TArg): TComputed {
144 const key = this._computeKey(arg) as WeakKey;
145 if (this._map.has(key)) {
151 return value;
152 }
153 > } cache.ts
src/vs/platform/agentHost/common/agentHostReviewService.ts 87 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostReviewService.ts
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 > import { URI } from '../../../base/common/uri.js';
7 > import type { URI as ProtocolURI } from './state/sessionState.js';
8 > import { createDecorator } from '../../instantiation/common/instantiation.js';
9 >
10 > export const IAgentHostReviewService = createDecorator<IAgentHostReviewService>('agentHostReviewService');
11 >
12 > /**
13 > * Returns the canonical name for a session's synthetic **reviewed** ref.
14 > * Lives under the same `refs/agents/<sid>/…` namespace as checkpoint refs so
15 > * the two coexist safely and never surface to the user as branches/tags.
16 > */
17 > export function buildReviewedRefName(sanitizedSessionId: string): string {
18 return `refs/agents/${sanitizedSessionId}/reviewed`;
19 }
21 > /**
22 > * Tracks which files in a session's **Branch Changes** the user has reviewed,
23 > * as a session-private synthetic git ref (`refs/agents/<sid>/reviewed`) whose
24 > * tree snapshots the reviewed content. A file is reviewed when its content in
25 > * the reviewed tree matches the current working tree; re-editing a reviewed
26 > * file therefore auto-unreviews it.
27 > *
28 > * All operations are keyed on the Branch Changes baseline (the merge-base of
29 > * `HEAD` and the session's base branch). The `baseBranch` argument is the
30 > * already-resolved base-branch **name** (see `resolveDiffBaseBranchName`),
31 > * shared with the changeset service so both agree on the baseline.
32 > *
33 > * Operations are no-ops when the working directory is not inside a git
34 > * repository; a future milestone will add a non-git fallback (see the
35 > * DB-backed reviewed-file store on `ISessionDatabase`).
36 > */
37 > export interface IAgentHostReviewService {
38 > readonly _serviceBrand: undefined;
39 >
40 > /**
41 > * Persists the review state for files in a local Branch Changes changeset.
42 > */
43 > setReviewState(channel: ProtocolURI, resources: readonly ProtocolURI[], reviewed: boolean): Promise<void>;
44 >
45 > /**
46 > * Marks a single file reviewed at its current working-tree content by
47 > * overlaying that content into the reviewed tree and advancing the
48 > * reviewed ref. No-op when the file is already reviewed at that content.
49 > */
50 > markFileReviewed(session: ProtocolURI, workingDirectory: URI, baseBranch: string | undefined, resource: URI): Promise<void>;
51 >
52 > /**
53 > * Marks a single file as unreviewed by resetting its entry in the
54 > * reviewed tree back to the baseline content and advancing the reviewed
55 > * ref. No-op when the file is not currently reviewed.
56 > */
57 > markFileUnreviewed(session: ProtocolURI, workingDirectory: URI, baseBranch: string | undefined, resource: URI): Promise<void>;
58 >
59 > /**
60 > * Returns the set of reviewed repo-relative paths within the current Branch
61 > * Changes: the changed files whose reviewed-tree content matches the
62 > * working tree. Empty when nothing is reviewed or the directory is not a
63 > * git work tree.
64 > */
65 > getReviewedPaths(session: ProtocolURI, workingDirectory: URI, baseBranch: string | undefined): Promise<ReadonlySet<string>>;
66 >
67 > /**
68 > * Copies the reviewed ref from `sourceSessionUri` to `targetSessionUri` so a
69 > * forked session starts with the parent's review progress. Points the
70 > * target's reviewed ref at the same commit as the source's (git objects are
71 > * shared within the repository). No-op when the source has no reviewed ref or
72 > * the directory is not a git work tree.
73 > */
74 > copyReviewedRef(sourceSession: ProtocolURI, targetSession: ProtocolURI, workingDirectory: URI): Promise<void>;
75 > }
76 >
77 > /**
78 > * A no-op {@link IAgentHostReviewService} used as the default for the optional
79 > * `_reviewService` parameter on `AgentService` so existing test callsites keep
80 > * compiling without forced fixture updates.
81 > */
82 > export const NULL_REVIEW_SERVICE: IAgentHostReviewService = {
83 > _serviceBrand: undefined,
84 > setReviewState: async () => { },
85 > markFileReviewed: async () => { },
86 > markFileUnreviewed: async () => { },
87 > getReviewedPaths: async () => new Set(),
88 > copyReviewedRef: async () => { },
89 > };
src/vs/platform/agentHost/node/agentHostFileMonitorService.ts 87 covered LOC · 18 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostFileMonitorService.ts
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 > import { disposableTimeout } from '../../../base/common/async.js';
7 > import { IExpression, ParsedExpression, parse } from '../../../base/common/glob.js';
8 > import { Disposable, DisposableMap, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../base/common/lifecycle.js';
9 > import { extUriBiasedIgnorePathCase } from '../../../base/common/resources.js';
10 > import { URI } from '../../../base/common/uri.js';
11 > import { FileChangesEvent, IFileService } from '../../files/common/files.js';
12 > import { createDecorator } from '../../instantiation/common/instantiation.js';
13 > import { ILogService } from '../../log/common/log.js';
14 >
15 > export const IAgentHostFileMonitorService = createDecorator<IAgentHostFileMonitorService>('agentHostFileMonitorService');
16 >
17 > export const DEFAULT_AGENT_HOST_WATCH_EXCLUDES: readonly string[] = Object.freeze([
18 > '**/.git',
19 > '**/.git/lfs/**',
20 > '**/.git/logs/**',
21 > '**/.git/objects/**',
22 > '**/.git/subtree-cache/**',
23 > '**/.git/**/*.lock',
24 > '**/.git/**/FETCH_HEAD',
25 > '**/.git/**/fsmonitor--daemon/**',
26 > '**/*.watchman-cookie-*',
27 > ]);
28 >
29 > export interface IAgentHostFileMonitorOptions {
30 > readonly excludes?: readonly string[];
31 > readonly debounceMs?: number;
32 > }
33 >
34 > export interface IAgentHostFileMonitorService extends IDisposable {
35 > readonly _serviceBrand: undefined;
36 > acquire(folder: URI, callback: () => void, options?: IAgentHostFileMonitorOptions): IDisposable | undefined;
37 > }
38 >
39 > interface IMonitorEntry extends IDisposable {
40 > readonly folder: URI;
41 > readonly callbacks: Set<() => void>;
42 > readonly debounce: MutableDisposable<IDisposable>;
43 > readonly debounceMs: number;
44 > readonly excludeMatcher: ParsedExpression;
45 > }
46 >
47 function normalizeExcludes(excludes: readonly string[]): readonly string[] {
48 return [...excludes].sort();
49 }
51 function parseExcludes(excludes: readonly string[]): ParsedExpression {
52 const expression: IExpression = Object.create(null);
56 return parse(expression);
57 }
59 > export class AgentHostFileMonitorService extends Disposable implements IAgentHostFileMonitorService {
60 > declare readonly _serviceBrand: undefined;
61 >
62 > private static readonly _DEFAULT_DEBOUNCE_MS = 750;
63 >
64 > private readonly _entries = this._register(new DisposableMap<string, IMonitorEntry>());
65 >
66 > constructor(
67 > @IFileService private readonly _fileService: IFileService, agentHostFileMonitorService.ts
68 > @ILogService private readonly _logService: ILogService,
69 > ) {
70 > super();
71 > this._register(this._fileService.onDidFilesChange(event => this._onDidFilesChange(event)));
72 > this._register(this._fileService.onDidWatchError(error => {
73 this._logService.warn('[AgentHostFileMonitorService] File watcher error', error);
75 > }
77 > acquire(folder: URI, callback: () => void, options: IAgentHostFileMonitorOptions = {}): IDisposable | undefined {
78 const canonicalFolder = this._canonicalizeFolder(folder);
79 const excludes = normalizeExcludes(options.excludes ?? DEFAULT_AGENT_HOST_WATCH_EXCLUDES);
104 });
105 }
107 > private _createEntry(_key: string, folder: URI, excludes: readonly string[], debounceMs: number): IMonitorEntry {
108 const disposable = new DisposableStore();
109 try {
144 }, entry.debounceMs);
145 }
147 > private _hasRelevantRawChange(entry: IMonitorEntry, event: FileChangesEvent): boolean {
148 return this._hasRelevantRawResources(entry, event.rawAdded)
149 || this._hasRelevantRawResources(entry, event.rawUpdated)
150 || this._hasRelevantRawResources(entry, event.rawDeleted);
151 }
153 > private _hasRelevantRawResources(entry: IMonitorEntry, resources: readonly URI[]): boolean {
154 for (const resource of resources) {
155 if (!extUriBiasedIgnorePathCase.isEqualOrParent(resource, entry.folder)) {
162 return false;
163 }
165 > private _isExcluded(entry: IMonitorEntry, resource: URI): boolean {
166 const basename = extUriBiasedIgnorePathCase.basename(resource);
167 const relativePath = extUriBiasedIgnorePathCase.relativePath(entry.folder, resource);
171 return this._matchesExclude(entry, resource.path, basename);
172 }
174 > private _matchesExclude(entry: IMonitorEntry, path: string, basename: string): boolean {
175 return typeof entry.excludeMatcher(path, basename) === 'string';
176 }
178 > private _canonicalizeFolder(folder: URI): URI {
179 return extUriBiasedIgnorePathCase.removeTrailingPathSeparator(extUriBiasedIgnorePathCase.normalizePath(folder));
180 }
182 > private _key(folder: URI, excludes: readonly string[], debounceMs: number): string {
183 return `${extUriBiasedIgnorePathCase.getComparisonKey(folder)}\u0000${debounceMs}\u0000${excludes.join('\n')}`;
184 }
src/vs/base/common/performance.ts 86 covered LOC · 7 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- performance.ts
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 > import type { INodeProcess } from './platform.js';
7 >
8 > function _definePolyfillMarks(timeOrigin?: number) {
9 > const _data: [string?, number?] = [];
10 > if (typeof timeOrigin === 'number') {
11 > _data.push('code/timeOrigin', timeOrigin);
12 > }
13 >
14 > function mark(name: string, markOptions?: { startTime?: number }) {
15 > _data.push(name, markOptions?.startTime ?? Date.now()); performance.ts
16 > }
17 > function getMarks() { performance.ts
18 const result = [];
19 for (let i = 0; i < _data.length; i += 2) {
25 return result;
26 }
27 > function clearMarks(name?: string) { performance.ts
28 if (typeof name === 'undefined') {
29 const hasTimeOrigin = _data.length >= 2 && _data[0] === 'code/timeOrigin';
41 }
42 }
43 > return { mark, getMarks, clearMarks }; performance.ts
44 > }
45 >
46 > declare const process: INodeProcess;
47 >
48 > interface IPerformanceEntry {
49 > readonly name: string;
50 > readonly startTime: number;
51 > }
52 >
53 > interface IPerformanceTiming {
54 > readonly navigationStart?: number;
55 > readonly redirectStart?: number;
56 > readonly fetchStart?: number;
57 > }
58 >
59 > interface IPerformance {
60 > mark(name: string, markOptions?: { startTime?: number }): void;
61 > clearMarks(name?: string): void;
62 > getEntriesByType(type: string): IPerformanceEntry[];
63 > readonly timeOrigin: number;
64 > readonly timing: IPerformanceTiming;
65 > readonly nodeTiming?: any;
66 > }
67 >
68 > declare const performance: IPerformance;
69 >
70 > function _define() {
71 >
72 > // Identify browser environment when following property is not present
73 > // https://nodejs.org/dist/latest-v16.x/docs/api/perf_hooks.html#performancenodetiming
74 > // @ts-ignore
75 > if (typeof performance === 'object' && typeof performance.mark === 'function' && !performance.nodeTiming) {
76 // in a browser context, reuse performance-util
77
109 }
110
111 > } else if (typeof process === 'object') { performance.ts
112 > // node.js: use the normal polyfill but add the timeOrigin
113 > // from the node perf_hooks API as very first mark
114 > const timeOrigin = performance?.timeOrigin;
115 > return _definePolyfillMarks(timeOrigin);
116 >
117 > } else {
118 // unknown environment
119 console.trace('perf-util loaded in UNKNOWN environment');
120 return _definePolyfillMarks();
121 }
122 > } performance.ts
123 >
124 > function _factory(sharedObj: any) {
125 > if (!sharedObj.MonacoPerformanceMarks) {
126 > sharedObj.MonacoPerformanceMarks = _define();
127 > }
128 > return sharedObj.MonacoPerformanceMarks;
129 > }
130 >
131 > const perf = _factory(globalThis);
132 >
133 > export const mark: (name: string, markOptions?: { startTime?: number }) => void = perf.mark;
134 >
135 > /**
136 > * Clears performance marks. If a name is given, only marks with that exact
137 > * name are removed. If no name is given, all marks are removed.
138 > */
139 > export const clearMarks: (name?: string) => void = perf.clearMarks;
140 >
141 > export interface PerformanceMark {
142 > readonly name: string;
143 > readonly startTime: number;
144 > }
145 >
146 > /**
147 > * Returns all marks, sorted by `startTime`.
148 > */
149 > export const getMarks: () => PerformanceMark[] = perf.getMarks;
src/vs/platform/agentHost/node/agentHostReviewService.ts 83 covered LOC · 16 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostReviewService.ts
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 > import { SequencerByKey } from '../../../base/common/async.js';
7 > import { Disposable } from '../../../base/common/lifecycle.js';
8 > import { relativePath } from '../../../base/common/resources.js';
9 > import { URI } from '../../../base/common/uri.js';
10 > import { ILogService } from '../../log/common/log.js';
11 > import { AgentSession } from '../common/agentService.js';
12 > import { ChangesetKind, parseChangesetUri } from '../common/changesetUri.js';
13 > import { EMPTY_TREE_OBJECT, IAgentHostGitService, META_DIFF_BASE_BRANCH, resolveDiffBaseBranchName } from '../common/agentHostGitService.js';
14 > import { buildReviewedRefName, IAgentHostReviewService } from '../common/agentHostReviewService.js';
15 > import { ISessionDataService } from '../common/sessionDataService.js';
16 > import { readSessionGitState, type URI as ProtocolURI } from '../common/state/sessionState.js';
17 > import { AgentHostStateManager, IAgentHostStateManager } from './agentHostStateManager.js';
18 >
19 > /**
20 > * Resolved git context shared by the review operations: the repository root,
21 > * the Branch Changes baseline tree, and the current reviewed ref/tree.
22 > */
23 > interface IReviewContext {
24 > readonly repoRoot: URI;
25 > /** Tree object of the baseline. */
26 > readonly baselineTree: string;
27 > /** Name of the session's reviewed ref. */
28 > readonly reviewedRef: string;
29 > /** Current reviewed commit, or `undefined` when the ref does not exist yet. */
30 > readonly reviewedCommit: string | undefined;
31 > /** Current reviewed tree; equals `baselineTree` when the ref does not exist. */
32 > readonly reviewedTree: string;
33 > }
34 >
35 > export class AgentHostReviewService extends Disposable implements IAgentHostReviewService {
36 > declare readonly _serviceBrand: undefined;
37 >
38 > /**
39 > * Serializes mark/unmark/read per session so back-to-back mutations don't
40 > * race on the reviewed ref rebuild and reads observe a consistent ref.
41 > */
42 > private readonly _sequencer = new SequencerByKey<string>();
43 >
44 > constructor(
45 > @IAgentHostStateManager private readonly _stateManager: AgentHostStateManager, agentHostReviewService.ts
46 > @IAgentHostGitService private readonly _gitService: IAgentHostGitService,
47 > @ISessionDataService private readonly _sessionDataService: ISessionDataService,
48 > @ILogService private readonly _logService: ILogService,
49 > ) {
50 > super();
51 >
52 > // When a session's data directory is about to be deleted, delete the
53 > // reviewed ref we created for it. The working directory needed to
54 > // resolve the repository root is supplied by the event (resolved from
55 > // live session state) so we don't persist our own copy.
56 > this._register(this._sessionDataService.onWillDeleteSessionData(e => {
57 e.waitUntil(this.disposeSessionData(e.session.toString()));
59 > }
61 > async setReviewState(channel: ProtocolURI, resources: readonly ProtocolURI[], reviewed: boolean): Promise<void> {
62 const parsed = parseChangesetUri(channel);
63 if (!parsed || parsed.kind !== ChangesetKind.Branch) {
89 });
90 }
92 > markFileReviewed(session: ProtocolURI, workingDirectory: URI, baseBranch: string | undefined, resource: URI): Promise<void> {
93 return this._sequencer.queue(session, () => this._setReviewed(session, workingDirectory, baseBranch, resource, true));
94 }
96 > markFileUnreviewed(session: ProtocolURI, workingDirectory: URI, baseBranch: string | undefined, resource: URI): Promise<void> {
97 return this._sequencer.queue(session, () => this._setReviewed(session, workingDirectory, baseBranch, resource, false));
98 }
100 > getReviewedPaths(session: ProtocolURI, workingDirectory: URI, baseBranch: string | undefined): Promise<ReadonlySet<string>> {
101 return this._sequencer.queue(session, () => this._getReviewedPaths(session, workingDirectory, baseBranch));
102 }
104 > copyReviewedRef(sourceSession: ProtocolURI, targetSession: ProtocolURI, workingDirectory: URI): Promise<void> {
105 return this._sequencer.queue(targetSession, () => this._copyReviewedRef(sourceSession, targetSession, workingDirectory));
106 }
108 > private async _copyReviewedRef(sourceSession: ProtocolURI, targetSession: ProtocolURI, workingDirectory: URI): Promise<void> {
109 const repoRoot = await this._gitService.getRepositoryRoot(workingDirectory);
110 if (!repoRoot) {
122 this._logService.trace(`[AgentHostReview][_copyReviewedRef] Copied reviewed ref ${sourceRef} -> ${targetRef} for fork`);
123 }
125 > private async _setReviewed(session: ProtocolURI, workingDirectory: URI, baseBranch: string | undefined, resource: URI, reviewed: boolean): Promise<void> {
126 const context = await this._resolveContext(session, workingDirectory, baseBranch);
127 if (!context) {
172 this._logService.trace(`[AgentHostReview][_setReviewed] ${message} for ${session.toString()} -> ${context.reviewedRef}@${commit}`);
173 }
175 > private async _getReviewedPaths(session: ProtocolURI, workingDirectory: URI, baseBranch: string | undefined): Promise<ReadonlySet<string>> {
176 const context = await this._resolveContext(session, workingDirectory, baseBranch);
177 if (!context?.reviewedCommit) {
202 return new Set(changed.filter(path => !unreviewedSet.has(path)));
203 }
205 > private async _resolveContext(session: ProtocolURI, workingDirectory: URI, baseBranch: string | undefined): Promise<IReviewContext | undefined> {
206 const repoRoot = await this._gitService.getRepositoryRoot(workingDirectory);
207 if (!repoRoot) {
229 return { repoRoot, baselineTree, reviewedRef, reviewedCommit, reviewedTree };
230 }
232 > async disposeSessionData(session: ProtocolURI): Promise<void> {
233 await this._sequencer.queue(session, () => this._disposeSessionData(session));
234 }
236 > private async _disposeSessionData(session: ProtocolURI): Promise<void> {
237 const workingDirectory = this._stateManager.getSessionState(session)?.workingDirectories?.[0];
238 if (!workingDirectory) {
src/vs/base/common/objects.ts 82 covered LOC · 23 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- objects.ts
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 > import { isTypedArray, isObject, isUndefinedOrNull } from './types.js';
7 >
8 > export function deepClone<T>(obj: T): T {
9 if (!obj || typeof obj !== 'object') {
10 return obj;
19 return result;
20 }
21 > objects.ts
22 > export function deepFreeze<T>(obj: T): T {
23 if (!obj || typeof obj !== 'object') {
24 return obj;
39 return obj;
40 }
41 > objects.ts
42 > const _hasOwnProperty = Object.prototype.hasOwnProperty;
43 >
44 >
45 > export function cloneAndChange(obj: any, changer: (orig: any) => any): any {
46 return _cloneAndChange(obj, changer, new Set());
47 }
48 > objects.ts
49 function _cloneAndChange(obj: any, changer: (orig: any) => any, seen: Set<any>): any {
50 if (isUndefinedOrNull(obj)) {
82 return obj;
83 }
84 > objects.ts
85 > /**
86 > * Copies all properties of source into destination. The optional parameter "overwrite" allows to control
87 > * if existing properties on the destination should be overwritten or not. Defaults to true (overwrite).
88 > */
89 > export function mixin(destination: any, source: any, overwrite: boolean = true): any {
90 if (!isObject(destination)) {
91 return source;
109 return destination;
110 }
111 > objects.ts
112 > export function equals(one: any, other: any): boolean {
113 > if (one === other) { objects.ts
114 return true;
115 }
116 > if (one === null || one === undefined || other === null || other === undefined) { objects.ts
117 return false;
118 }
119 > if (typeof one !== typeof other) { objects.ts
120 return false;
121 }
122 > if (typeof one !== 'object') { objects.ts
123 return false;
124 }
125 > if ((Array.isArray(one)) !== (Array.isArray(other))) { objects.ts
126 return false;
127 }
128 > objects.ts
129 > let i: number;
130 > let key: string;
131 >
132 > if (Array.isArray(one)) {
133 > if (one.length !== other.length) {
134 > return false; objects.ts
135 > }
136 > for (i = 0; i < one.length; i++) { objects.ts
137 if (!equals(one[i], other[i])) {
138 return false;
139 }
140 }
141 > } else { objects.ts
142 const oneKeys: string[] = [];
143
160 }
161 }
162 > return true; objects.ts
163 > }
164 > objects.ts
165 > /**
166 > * Calls `JSON.Stringify` with a replacer to break apart any circular references.
167 > * This prevents `JSON`.stringify` from throwing the exception
168 > * "Uncaught TypeError: Converting circular structure to JSON"
169 > */
170 > export function safeStringify(obj: any): string {
171 const seen = new Set<any>();
172 return JSON.stringify(obj, (key, value) => {
184 });
185 }
186 > objects.ts
187 > /**
188 > * Like `JSON.stringify`, but with deterministic ordering of object keys so that
189 > * structurally equal inputs always produce the same string. Useful for cache
190 > * keys derived from arbitrary object payloads.
191 > *
192 > * - Object keys are sorted at every level of nesting.
193 > * - Properties whose value is `undefined` are omitted (matching `JSON.stringify`).
194 > * - Circular references are replaced with the string `"[Circular]"` to avoid
195 > * throwing.
196 > * - A top-level `undefined` returns the string `'undefined'`; any other
197 > * stringification failure returns the empty string.
198 > */
199 > export function stableStringify(value: unknown): string {
200 if (value === undefined) {
201 return 'undefined';
207 }
208 }
209 > objects.ts
210 function _stableStringify(value: unknown, seen: WeakSet<object>): string {
211 if (value === null || typeof value !== 'object') {
230 return '{' + parts.join(',') + '}';
231 }
232 > objects.ts
233 > type obj = { [key: string]: any };
234 > /**
235 > * Returns an object that has keys for each value that is different in the base object. Keys
236 > * that do not exist in the target but in the base object are not considered.
237 > *
238 > * Note: This is not a deep-diffing method, so the values are strictly taken into the resulting
239 > * object if they differ.
240 > *
241 > * @param base the object to diff against
242 > * @param obj the object to use for diffing
243 > */
244 > export function distinct(base: obj, target: obj): obj {
245 const result = Object.create(null);
246
261 return result;
262 }
263 > objects.ts
264 > export function getCaseInsensitive(target: obj, key: string): unknown {
265 const lowercaseKey = key.toLowerCase();
266 const equivalentKey = Object.keys(target).find(k => k.toLowerCase() === lowercaseKey);
267 return equivalentKey ? target[equivalentKey] : target[key];
268 }
269 > objects.ts
270 > export function filter(obj: obj, predicate: (key: string, value: any) => boolean): obj {
271 const result = Object.create(null);
272 for (const [key, value] of Object.entries(obj)) {
277 return result;
278 }
279 > objects.ts
280 > export function mapValues<T extends {}, R>(obj: T, fn: (value: T[keyof T], key: string) => R): { [K in keyof T]: R } {
281 const result: { [key: string]: R } = {};
282 for (const [key, value] of Object.entries(obj)) {
src/vs/platform/agentHost/node/agentHostLocalTurns.ts 81 covered LOC · 13 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostLocalTurns.ts
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 > import type { IReference } from '../../../base/common/lifecycle.js';
7 > import { URI } from '../../../base/common/uri.js';
8 > import { ILogService } from '../../log/common/log.js';
9 > import type { ILocalTurnRecord, ISessionDatabase, ISessionDataService } from '../common/sessionDataService.js';
10 > import type { Turn } from '../common/state/sessionState.js';
11 >
12 > /**
13 > * Tracks host-injected ("local") turns — completed protocol turns the agent SDK
14 > * never saw, such as the `/rename` acknowledgement or a `!command` terminal run.
15 > *
16 > * These turns exist only in the agent host: they are never forwarded to the
17 > * agent SDK, so they are absent from the SDK transcript that
18 > * {@link AgentService} replays on restore. This registry persists them (so they
19 > * survive reload) and remembers, for each, the id of the preceding concrete
20 > * (SDK-backed) turn — the *anchor* — so that fork/truncate operations targeting
21 > * a local turn can be redirected to the concrete SDK message before it.
22 > *
23 > * Everything is scoped to a **chat** (its channel URI): a session's default
24 > * chat and each of its peer chats are handled identically. Persistence lives in
25 > * the owning session's database (one per session, shared across its chats),
26 > * discriminated by {@link ILocalTurnRecord.chatUri}.
27 > */
28 > export class AgentHostLocalTurns {
29 >
30 > /** chat URI → (localTurnId → { anchorTurnId, seq }). */
31 > private readonly _byChat = new Map<string, Map<string, { readonly anchorTurnId: string | undefined; readonly seq: number }>>();
32 > /** session URI → highest `seq` assigned so far (seq is session-global for stable ordering). */
33 > private readonly _seqBySession = new Map<string, number>();
34 >
35 > constructor(
36 > private readonly _sessionDataService: ISessionDataService, agentHostLocalTurns.ts
37 > private readonly _logService: ILogService,
38 > ) { }
40 > /** Whether `turnId` is a known host-injected local turn in `chat`. */
41 > isLocal(chat: string, turnId: string): boolean {
42 return this._byChat.get(chat)?.has(turnId) ?? false;
43 }
45 > /** All known local turn ids for `chat`. */
46 > getLocalTurnIds(chat: string): string[] {
47 const map = this._byChat.get(chat);
48 return map ? [...map.keys()] : [];
49 }
51 > /**
52 > * Resolves `turnId` to the concrete (SDK-backed) turn a fork/truncate should
53 > * operate on within `chat`. For a local turn this is its anchor (the
54 > * preceding real turn, or `undefined` when it precedes any real turn); for a
55 > * concrete turn it is the turn itself.
56 > */
57 > resolveConcreteTurnId(chat: string, turnId: string): string | undefined {
58 const entry = this._byChat.get(chat)?.get(turnId);
59 return entry ? entry.anchorTurnId : turnId;
60 }
62 > /**
63 > * Persist a local turn and remember it in memory. `anchorTurnId` is the id
64 > * of the preceding concrete turn in `chat` (or `undefined` when there is
65 > * none). `session` identifies the database to persist into.
66 > */
67 > record(session: string, chat: string, turn: Turn, anchorTurnId: string | undefined): void {
68 const seq = (this._seqBySession.get(session) ?? 0) + 1;
69 this._noteInMemory(session, chat, turn.id, anchorTurnId, seq);
80 }).finally(() => ref.dispose());
81 }
83 > /**
84 > * Loads persisted local turns for `session`, populating the in-memory index
85 > * (keyed by each record's chat), and returns the records for `chat` in
86 > * `seq` order so the caller can interleave them into that chat's SDK-derived
87 > * turns during restore.
88 > */
89 > async loadForChat(session: string, chat: string): Promise<ILocalTurnRecord[]> {
90 const records = await this._load(session);
91 return records.filter(r => r.chatUri === chat);
92 }
94 > /** Note a local turn in memory only (used by fork seeding). */
95 > noteInMemory(session: string, chat: string, turnId: string, anchorTurnId: string | undefined, seq: number): void {
96 this._noteInMemory(session, chat, turnId, anchorTurnId, seq);
97 }
99 > /** Delete the given local turns from memory and the session database. */
100 > deleteLocals(session: string, turnIds: readonly string[]): void {
101 if (turnIds.length === 0) {
102 return;
119 }).finally(() => ref.dispose());
120 }
122 > /** Drop all in-memory state for a chat. */
123 > forgetChat(chat: string): void {
124 this._byChat.delete(chat);
125 }
127 > private async _load(session: string): Promise<ILocalTurnRecord[]> {
128 const ref = this._sessionDataService.tryOpenDatabase?.(URI.parse(session));
129 if (!ref) {
149 }
150 }
152 > private _noteInMemory(session: string, chat: string, turnId: string, anchorTurnId: string | undefined, seq: number): void {
153 let map = this._byChat.get(chat);
154 if (!map) {
159 this._seqBySession.set(session, Math.max(this._seqBySession.get(session) ?? 0, seq));
160 }
src/vs/platform/agentHost/common/githubEndpoints.ts 79 covered LOC · 8 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- githubEndpoints.ts
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 > import { URI } from '../../../base/common/uri.js';
7 > import { ProtectedResourceMetadata } from './state/protocol/state.js';
8 >
9 > /**
10 > * The GitHub endpoints an agent host talks to, derived from an optional
11 > * GitHub Enterprise base URI. All values are string URIs with no trailing slash.
12 > */
13 > export interface IGitHubEndpoints {
14 > /** REST API base (e.g. `https://api.github.com`), used as the resource identifier and the REST host. */
15 > readonly apiBaseUri: string;
16 > /** GraphQL endpoint (distinct from `apiBaseUri` for on-prem: `/api/graphql`, not `/api/v3/graphql`). */
17 > readonly graphQlUri: string;
18 > /** OAuth authorization server URI, advertised in `authorization_servers`. */
19 > readonly oauthServer: string;
20 > /**
21 > * The configured GitHub Enterprise host (authority only, e.g. `acme.ghe.com`),
22 > * or `undefined` for github.com. Used to point the Copilot CLI at an enterprise
23 > * host via `COPILOT_GH_HOST`.
24 > */
25 > readonly enterpriseHost: string | undefined;
26 > }
27 >
28 > const GITHUB_DOT_COM_COPILOT_API_BASE_URI = 'https://api.githubcopilot.com';
29 >
30 > /** Canonical github.com endpoints, used when no enterprise URI is configured. */
31 > const GITHUB_DOT_COM_ENDPOINTS: IGitHubEndpoints = {
32 > apiBaseUri: 'https://api.github.com',
33 > graphQlUri: 'https://api.github.com/graphql',
34 > oauthServer: 'https://github.com/login/oauth',
35 > enterpriseHost: undefined,
36 > };
37 >
38 > /**
39 > * Derives the {@link IGitHubEndpoints} for a GitHub Enterprise base URI, mirroring
40 > * the URL derivation in the built-in `github-authentication` extension
41 > * (`githubServer.ts` / `common/env.ts`):
42 > *
43 > * - unset / empty / unparseable → github.com defaults (byte-for-byte, preserving
44 > * the resource identifiers used by every non-enterprise install).
45 > * - GitHub Enterprise **Cloud** (authority ends in `.ghe.com`) → API on an `api.`
46 > * subdomain: `https://api.<authority>`.
47 > * - GitHub Enterprise **Server** (on-prem) → API under `/api/v3`, GraphQL under
48 > * `/api/graphql`.
49 > *
50 > * The OAuth server is always `<scheme>://<authority>/login/oauth` for enterprise.
51 > */
52 > export function deriveGitHubEndpoints(enterpriseUri: string | undefined): IGitHubEndpoints {
53 > if (!enterpriseUri) { githubEndpoints.ts
54 > return GITHUB_DOT_COM_ENDPOINTS; githubEndpoints.ts
55 > }
56
57 let uri: URI;
70 // default rather than deriving a nonsensical `github.com/api/v3`. Guards the
71 // case where the enterprise host can't be resolved and falls back to github.com.
72 > if (authority === 'github.com' || authority === 'www.github.com' || authority === 'api.github.com') { githubEndpoints.ts
73 return GITHUB_DOT_COM_ENDPOINTS;
74 }
75
76 const scheme = uri.scheme || 'https';
77 > const isCloud = /\.ghe\.com$/.test(authority); githubEndpoints.ts
78 > return {
79 > apiBaseUri: isCloud ? `${scheme}://api.${authority}` : `${scheme}://${authority}/api/v3`,
80 > graphQlUri: isCloud ? `${scheme}://api.${authority}/graphql` : `${scheme}://${authority}/api/graphql`,
81 > oauthServer: `${scheme}://${authority}/login/oauth`,
82 > enterpriseHost: authority,
83 > };
84 > }
86 > /**
87 > * Derives the official GitHub MCP server URL from the per-user Copilot API
88 > * endpoint returned by `/copilot_internal/user`.
89 > */
90 > export function gitHubMcpServerUrl(copilotApiBaseUri: string | undefined): string | undefined {
91 try {
92 const uri = URI.parse(copilotApiBaseUri ?? GITHUB_DOT_COM_COPILOT_API_BASE_URI, true);
99 }
100 }
102 > /**
103 > * The GitHub Copilot protected resource for the given endpoints. Shared by the
104 > * endpoint service and tests so the resource identity is defined once.
105 > */
106 > export function gitHubCopilotResource(endpoints: IGitHubEndpoints): ProtectedResourceMetadata {
107 return {
108 resource: endpoints.apiBaseUri,
113 };
114 }
116 > /** The GitHub repository protected resource for the given endpoints. */
117 > export function gitHubRepoResource(endpoints: IGitHubEndpoints): ProtectedResourceMetadata {
118 return {
119 resource: `${endpoints.apiBaseUri}/repos`,
src/vs/platform/agentHost/node/agentHostFileCompletionProvider.ts 79 covered LOC · 8 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostFileCompletionProvider.ts
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 > import { CancellationToken } from '../../../base/common/cancellation.js';
7 > import { isCancellationError } from '../../../base/common/errors.js';
8 > import { compareItemsByFuzzyScore, FuzzyScorerCache, IItemAccessor, prepareQuery, scoreItemFuzzy } from '../../../base/common/fuzzyScorer.js';
9 > import { Schemas } from '../../../base/common/network.js';
10 > import { basename, relativePath } from '../../../base/common/resources.js';
11 > import { URI } from '../../../base/common/uri.js';
12 > import { CompletionItem, CompletionItemKind, CompletionsParams } from '../common/state/protocol/commands.js';
13 > import { MessageAttachmentKind } from '../common/state/protocol/state.js';
14 > import { CompletionTriggerCharacter, IAgentHostCompletionItemProvider } from './agentHostCompletions.js';
15 > import { AgentHostStateManager } from './agentHostStateManager.js';
16 > import { AgentHostWorkspaceFiles } from './agentHostWorkspaceFiles.js';
17 >
18 > /** Maximum number of completion items returned per call. */
19 > const MAX_RESULTS = 50;
20 >
21 > /**
22 > * Result of {@link extractAtToken}.
23 > */
24 > interface IAtToken {
25 > readonly token: string;
26 > readonly triggerChar: string;
27 > readonly rangeStart: number;
28 > readonly rangeEnd: number;
29 > }
30 >
31 > /**
32 > * Walk back from `offset` to find the most recent `@` that is preceded by
33 > * whitespace (or start-of-string) and not interrupted by whitespace. Returns
34 > * the substring after `@` together with the range to replace, or `undefined`
35 > * if no `@`-token is being typed at `offset`.
36 > *
37 > * Exported for unit testing.
38 > */
39 > export function extractAtToken(text: string, offset: number): IAtToken | undefined {
40 if (offset < 0 || offset > text.length) {
41 return undefined;
61 return undefined;
62 }
64 > /**
65 > * Item-accessor that exposes a {@link URI} as basename / parent-directory /
66 > * relative path for the {@link scoreItemFuzzy} family.
67 > */
68 > class UriAccessor implements IItemAccessor<URI> {
69 > constructor(private readonly _workingDirectory: URI) { }
70 >
71 > getItemLabel(item: URI): string {
72 return basename(item);
73 }
75 > getItemDescription(item: URI): string | undefined {
76 const rel = relativePath(this._workingDirectory, item);
77 if (!rel) {
81 return idx > 0 ? rel.slice(0, idx) : undefined;
82 }
84 > getItemPath(item: URI): string | undefined {
85 const rel = relativePath(this._workingDirectory, item);
86 return rel ?? item.fsPath;
87 }
89 >
90 > /**
91 > * Generic completion provider that contributes workspace file references
92 > * for a {@link CompletionItemKind.UserMessage} input — typically used for
93 > * `@`-mentions in the user message composer.
94 > *
95 > * When the user has typed an `@`-prefixed token at the cursor position,
96 > * this provider enumerates files under the session's working directory
97 > * (via {@link AgentHostWorkspaceFiles}, which uses ripgrep and respects
98 > * `.gitignore`), ranks them with the same fuzzy scorer used by the
99 > * VS Code Quick Open file picker, and returns up to {@link MAX_RESULTS}
100 > * matches.
101 > */
102 > export class AgentHostFileCompletionProvider implements IAgentHostCompletionItemProvider {
103 >
104 > readonly kinds: ReadonlySet<CompletionItemKind> = new Set([CompletionItemKind.UserMessage]);
105 >
106 > readonly triggerCharacters: readonly string[] = [CompletionTriggerCharacter.File, CompletionTriggerCharacter.Hash];
107 >
108 > constructor(
109 > private readonly _stateManager: AgentHostStateManager, agentHostFileCompletionProvider.ts
110 > private readonly _workspaceFiles: AgentHostWorkspaceFiles,
111 > ) { }
113 > async provideCompletionItems(params: CompletionsParams, token: CancellationToken): Promise<readonly CompletionItem[]> {
114 const workingDirectoryStr = this._stateManager.getSessionState(params.channel)?.workingDirectories?.[0];
115 if (!workingDirectoryStr) {
src/vs/platform/agentHost/node/agentHostChangesetStateCache.ts 78 covered LOC · 12 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostChangesetStateCache.ts
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 > import { LinkedMap, Touch } from '../../../base/common/map.js';
7 > import { ChangesetStatus, type ChangesetState, type URI } from '../common/state/sessionState.js';
8 >
9 > /**
10 > * Default number of expanded changeset states kept hot in memory.
11 > *
12 > * This cache only stores the subscribable `ChangesetState` payloads. The
13 > * lightweight catalogue on `SessionSummary.changesets` remains on the session
14 > * summary, and static changesets can be rehydrated from persisted metadata or
15 > * recomputed on demand. The limit is intentionally a soft cap: subscribed or
16 > * actively-computing changesets may pin the cache above this value until they
17 > * become evictable.
18 > */
19 > const DEFAULT_CHANGESET_STATE_SOFT_LIMIT = 500;
20 >
21 > export interface IAgentHostChangesetStateRetentionOptions {
22 > /**
23 > * Number of expanded changeset states kept hot in memory. The limit is soft:
24 > * entries for which {@link canEvict} returns false may temporarily keep the
25 > * cache above this value.
26 > */
27 > readonly softLimit?: number;
28 >
29 > /**
30 > * Returns whether a changeset state can be silently evicted from the cache.
31 > * Production callers should provide this from `AgentService`, which owns
32 > * protocol subscription refcounts and can ask the changeset service about
33 > * active producers. Return false for changesets that are subscribed or have
34 > * an active producer that may still publish into the changeset URI.
35 > */
36 > readonly canEvict?: (changeset: URI) => boolean;
37 > }
38 >
39 > /**
40 > * Owns the memory policy for expanded changeset states.
41 > *
42 > * The state manager owns protocol sequencing and reducer application; this
43 > * helper owns the cache mechanics needed to keep dormant changesets bounded.
44 > * Eviction here is deliberately silent: protocol-visible teardown still goes
45 > * through `AgentHostStateManager.disposeChangeset`, which emits
46 > * `ChangesetCleared` before removing state.
47 > */
48 > export class AgentHostChangesetStateCache {
49 >
50 > private readonly _states = new Map<string, ChangesetState>();
51 > private readonly _lru = new LinkedMap<string, true>();
52 > private readonly _softLimit: number;
53 > private readonly _canEvict: (changeset: URI) => boolean;
54 >
55 > constructor(options: IAgentHostChangesetStateRetentionOptions = {}) {
56 > this._softLimit = Math.max(0, options.softLimit ?? DEFAULT_CHANGESET_STATE_SOFT_LIMIT); agentHostChangesetStateCache.ts
57 > this._canEvict = options.canEvict ?? (() => true);
58 > }
60 > keys(): IterableIterator<string> {
61 return this._states.keys();
62 }
64 > has(changeset: URI): boolean {
65 return this._states.has(changeset);
66 }
68 > get(changeset: URI): ChangesetState | undefined {
69 this._touch(changeset);
70 return this._states.get(changeset);
71 }
73 > set(changeset: URI, state: ChangesetState): void {
74 this._states.set(changeset, state);
75 this._touch(changeset);
76 this._evictIfOverLimit();
77 }
79 > delete(changeset: URI): void {
80 this._states.delete(changeset);
81 this._lru.delete(changeset);
82 }
84 > register(changeset: URI, initialStatus: ChangesetStatus = ChangesetStatus.Computing): void {
85 if (this._states.has(changeset)) {
86 this._touch(changeset);
89 this.set(changeset, { status: initialStatus, files: [] });
90 }
92 > /** Re-runs eviction after external liveness changes, such as unsubscribe or compute completion. */
93 > trimEvictableEntries(): void {
94 this._evictIfOverLimit();
95 }
97 > private _touch(changeset: URI): void {
98 if (this._states.has(changeset)) {
99 this._lru.set(changeset, true, Touch.AsNew);
100 }
101 }
103 > private _evictIfOverLimit(): void {
104 if (this._softLimit === 0) {
105 for (const changeset of [...this._lru.keys()]) {
src/vs/base/test/common/virtualScheduling/embedding.ts 76 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- embedding.ts
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 > import { setTimeout0, setTimeout0IsFaster } from '../../../common/platform.js';
7 > import { TimeApi } from './timeApi.js';
8 > import { VirtualEvent } from './virtualClock.js';
9 >
10 > /**
11 > * # The processor/host embedding
12 > *
13 > * An {@link Embedding} is the contract between the processor's pure state
14 > * machine and the host event loop. It is invoked once per virtual step that
15 > * produced progress, and decides *how* the processor reaches the host before
16 > * the next step.
17 > *
18 > * ## Contract
19 > *
20 > * On each invocation the embedding MUST do exactly one of:
21 > *
22 > * 1. Return `'continueSync'` **without** calling `then`. The processor will
23 > * loop in place on the same host stack frame.
24 > *
25 > * 2. Schedule `then` on a host primitive (microtask, macrotask, paint frame)
26 > * and return `'cbScheduled'`. The processor will return and wait for the
27 > * callback to re-enter the trampoline.
28 > *
29 > * The embedding MUST NOT call `then` synchronously and also return
30 > * `'cbScheduled'` (that would re-enter the trampoline before this call
31 > * completed). Likewise, returning `'continueSync'` while having scheduled
32 > * `then` async would cause `then` to fire after the trampoline already
33 > * looped — also a bug.
34 > *
35 > * ## Why a callback contract instead of async/await
36 > *
37 > * Every `await` is an implicit microtask hop. For code whose job is to
38 > * decide host hops, that's the wrong abstraction: the reader has to mentally
39 > * compile the `await` to a boundary. With this contract, every host hop is
40 > * a named call to a single primitive (`api.setTimeout`, `setTimeout0`,
41 > * `api.requestAnimationFrame`, …) at exactly one site in this file.
42 > */
43 > export type Embedding = (
44 > nextEvent: VirtualEvent,
45 > then: () => void,
46 > ) => 'continueSync' | 'cbScheduled';
47 >
48 > /**
49 > * Tasks never schedule via promise chains. The processor runs virtual events
50 > * back-to-back on a single host stack frame — fastest possible, but starves
51 > * the host event loop for the duration of the run.
52 > *
53 > * Use only for tests where no `await` / `.then` chains are involved between
54 > * scheduling and execution of virtual events.
55 > */
56 > export const syncEmbedding: Embedding = () => 'continueSync';
57 >
58 > /**
59 > * Tasks may schedule via `await` / `.then`. Between virtual events, yield to
60 > * the host so the *microtask closure* — the current microtask plus every
61 > * microtask it transitively enqueues — drains before the next event runs.
62 > *
63 > * This is the embedding to use for almost all integration-style tests.
64 > */
65 > export function drainMicrotasksEmbedding(realApi: TimeApi): Embedding {
66 return (next, then) => {
67 if (next.preferRealAnimationFrame && realApi.requestAnimationFrame) {
73 };
74 }
76 > /**
77 > * Schedule `cb` after the closure of the current microtask queue: `cb`
78 > * fires only after the current microtask AND every microtask it
79 > * (recursively, transitively) enqueues has settled.
80 > *
81 > * Per the HTML spec, a macrotask runs only when the microtask queue is
82 > * empty, so any macrotask primitive achieves this. We pick the fastest
83 > * one available on the host.
84 > */
85 > export function nextMacrotask(api: TimeApi, cb: () => void): void {
86 if (setTimeout0IsFaster) { setTimeout0(cb); return; }
87 if (api.setImmediate) { api.setImmediate(cb); return; }
src/vs/base/common/observableInternal/logging/debugGetDependencyGraph.ts 75 covered LOC · 10 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- debugGetDependencyGraph.ts
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 > import { IObservable, IObserver } from '../base.js';
7 > import { Derived } from '../observables/derivedImpl.js';
8 > import { FromEventObservable } from '../observables/observableFromEvent.js';
9 > import { ObservableValue } from '../observables/observableValue.js';
10 > import { AutorunObserver } from '../reactions/autorunImpl.js';
11 > import { formatValue } from './consoleObservableLogger.js';
12 >
13 > interface IOptions {
14 > type: 'dependencies' | 'observers';
15 > debugNamePostProcessor?: (name: string) => string;
16 > }
17 >
18 > export function debugGetObservableGraph(obs: IObservable<any> | IObserver, options: IOptions): string {
19 const debugNamePostProcessor = options?.debugNamePostProcessor ?? ((str: string) => str);
20 const info = Info.from(obs, debugNamePostProcessor);
31 }
32 }
34 function formatObservableInfoWithDependencies(info: Info, indentLevel: number, alreadyListed: Set<IObservable<any> | IObserver>, options: IOptions): string {
35 const indent = '\t\t'.repeat(indentLevel);
58 return lines.join('\n');
59 }
61 function formatObservableInfoWithObservers(info: Info, indentLevel: number, alreadyListed: Set<IObservable<any> | IObserver>, options: IOptions): string {
62 const indent = '\t\t'.repeat(indentLevel);
85 return lines.join('\n');
86 }
88 > class Info {
89 > public static from(obs: IObservable<any> | IObserver, debugNamePostProcessor: (name: string) => string): Info | undefined {
90 > if (obs instanceof AutorunObserver) {
91 > const state = obs.debugGetState(); debugGetDependencyGraph.ts
92 > return new Info(
93 > obs,
94 > debugNamePostProcessor(obs.debugName),
95 > 'autorun',
96 > undefined,
97 > state.stateStr,
98 > Array.from(state.dependencies),
99 > []
100 > );
101 > } else if (obs instanceof Derived) { debugGetDependencyGraph.ts
102 > const state = obs.debugGetState();
103 > return new Info(
104 > obs,
105 > debugNamePostProcessor(obs.debugName),
106 > 'derived',
107 > state.value,
108 > state.stateStr,
109 > Array.from(state.dependencies),
110 > Array.from(obs.debugGetObservers())
111 > );
112 > } else if (obs instanceof ObservableValue) {
113 > const state = obs.debugGetState();
114 > return new Info(
115 > obs,
116 > debugNamePostProcessor(obs.debugName),
117 > 'observableValue',
118 > state.value,
119 > 'upToDate',
120 > [],
121 > Array.from(obs.debugGetObservers())
122 > );
123 > } else if (obs instanceof FromEventObservable) {
124 > const state = obs.debugGetState(); debugGetDependencyGraph.ts
125 > return new Info(
126 > obs,
127 > debugNamePostProcessor(obs.debugName),
128 > 'fromEvent',
129 > state.value,
130 > state.hasValue ? 'upToDate' : 'initial',
131 > [],
132 > Array.from(obs.debugGetObservers())
133 > );
134 > }
135 > return undefined;
137 >
138 > public static unknown(obs: IObservable<any> | IObserver): Info {
139 return new Info(
140 obs,
147 );
148 }
150 > constructor(
151 public readonly sourceObj: IObservable<any> | IObserver,
152 public readonly name: string,
src/vs/platform/label/common/label.ts 75 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- label.ts
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 > import { Event } from '../../../base/common/event.js';
7 > import { IDisposable } from '../../../base/common/lifecycle.js';
8 > import { URI } from '../../../base/common/uri.js';
9 > import { createDecorator } from '../../instantiation/common/instantiation.js';
10 > import { IWorkspace, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from '../../workspace/common/workspace.js';
11 >
12 > export const ILabelService = createDecorator<ILabelService>('labelService');
13 >
14 > export interface ILabelService {
15 >
16 > readonly _serviceBrand: undefined;
17 >
18 > /**
19 > * Gets the human readable label for a uri.
20 > * If `relative` is passed returns a label relative to the workspace root that the uri belongs to.
21 > * If `noPrefix` is passed does not tildify the label and also does not prepand the root name for relative labels in a multi root scenario.
22 > * If `separator` is passed, will use that over the defined path separator of the formatter.
23 > * If `appendWorkspaceSuffix` is passed, will append the name of the workspace to the label.
24 > */
25 > getUriLabel(resource: URI, options?: { relative?: boolean; noPrefix?: boolean; separator?: '/' | '\\'; appendWorkspaceSuffix?: boolean }): string;
26 > getUriBasenameLabel(resource: URI): string;
27 > getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | IWorkspace), options?: { verbose: Verbosity }): string;
28 > getHostLabel(scheme: string, authority?: string): string;
29 > getHostTooltip(scheme: string, authority?: string): string | undefined;
30 > getSeparator(scheme: string, authority?: string): '/' | '\\';
31 >
32 > registerFormatter(formatter: ResourceLabelFormatter): IDisposable;
33 > readonly onDidChangeFormatters: Event<IFormatterChangeEvent>;
34 >
35 > /**
36 > * Registers a formatter that's cached for the machine beyond the lifecycle
37 > * of the current window. Disposing the formatter _will not_ remove it from
38 > * the cache.
39 > */
40 > registerCachedFormatter(formatter: ResourceLabelFormatter): IDisposable;
41 > }
42 >
43 > export const enum Verbosity {
44 > SHORT,
45 > MEDIUM,
46 > LONG
47 > }
48 >
49 > export interface IFormatterChangeEvent {
50 > scheme: string;
51 > }
52 >
53 > export interface ResourceLabelFormatter {
54 > scheme: string;
55 > authority?: string;
56 > priority?: boolean;
57 > formatting: ResourceLabelFormatting;
58 > }
59 >
60 > export interface ResourceLabelFormatting {
61 > label: string; // myLabel:/${path}
62 > separator: '/' | '\\' | '';
63 > tildify?: boolean;
64 > normalizeDriveLetter?: boolean;
65 > workspaceSuffix?: string;
66 > workspaceTooltip?: string;
67 > authorityPrefix?: string;
68 > stripPathStartingSeparator?: boolean;
69 > /**
70 > * Number of leading path segments to strip from `${path}` before
71 > * substitution. For example, a value of `2` turns
72 > * `/scheme/authority/rest/of/path` into `/rest/of/path`.
73 > */
74 > stripPathSegments?: number;
75 > }
src/vs/base/common/collections.ts 74 covered LOC · 18 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- collections.ts
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 > /**
7 > * An interface for a JavaScript object that
8 > * acts a dictionary. The keys are strings.
9 > */
10 > export type IStringDictionary<V> = Record<string, V>;
11 >
12 > /**
13 > * An interface for a JavaScript object that
14 > * acts a dictionary. The keys are numbers.
15 > */
16 > export type INumberDictionary<V> = Record<number, V>;
17 >
18 > /**
19 > * Groups the collection into a dictionary based on the provided
20 > * group function.
21 > */
22 > export function groupBy<K extends string | number | symbol, V>(data: readonly V[], groupFn: (element: V) => K): Partial<Record<K, V[]>> {
23 const result: Partial<Record<K, V[]>> = Object.create(null);
24 for (const element of data) {
32 return result;
33 }
35 > export function groupByMap<K, V>(data: V[], groupFn: (element: V) => K): Map<K, V[]> {
36 const result = new Map<K, V[]>();
37 for (const element of data) {
46 return result;
47 }
49 > export function diffSets<T>(before: ReadonlySet<T>, after: ReadonlySet<T>): { removed: T[]; added: T[] } {
50 const removed: T[] = [];
51 const added: T[] = [];
62 return { removed, added };
63 }
65 > /**
66 > * Checks whether two sets contain exactly the same elements.
67 > *
68 > * @param a - The first set.
69 > * @param b - The second set.
70 > * @returns `true` if both sets have the same size and every element of `a` is also in `b`.
71 > */
72 > export function equalSets<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): boolean {
73 if (a === b) {
74 return true;
84 return true;
85 }
87 > export function diffMaps<K, V>(before: Map<K, V>, after: Map<K, V>): { removed: V[]; added: V[] } {
88 const removed: V[] = [];
89 const added: V[] = [];
100 return { removed, added };
101 }
103 > /**
104 > * Computes the intersection of two sets.
105 > *
106 > * @param setA - The first set.
107 > * @param setB - The second iterable.
108 > * @returns A new set containing the elements that are in both `setA` and `setB`.
109 > */
110 > export function intersection<T>(setA: Set<T>, setB: Iterable<T>): Set<T> {
111 const result = new Set<T>();
112 for (const elem of setB) {
117 return result;
118 }
120 > export class SetWithKey<T> implements Set<T> {
121 > private _map = new Map<unknown, T>();
122 >
123 > constructor(values: T[], private toKey: (t: T) => unknown) {
124 for (const value of values) {
125 this.add(value);
126 }
127 }
129 > get size(): number {
130 return this._map.size;
131 }
133 > add(value: T): this {
134 const key = this.toKey(value);
135 this._map.set(key, value);
136 return this;
137 }
139 > delete(value: T): boolean {
140 return this._map.delete(this.toKey(value));
141 }
143 > has(value: T): boolean {
144 return this._map.has(this.toKey(value));
145 }
147 > *entries(): SetIterator<[T, T]> {
148 for (const entry of this._map.values()) {
149 yield [entry, entry];
150 }
151 }
153 > keys(): SetIterator<T> {
154 return this.values();
155 }
157 > *values(): SetIterator<T> {
158 for (const entry of this._map.values()) {
159 yield entry;
160 }
161 }
163 > clear(): void {
164 this._map.clear();
165 }
167 > forEach(callbackfn: (value: T, value2: T, set: Set<T>) => void, thisArg?: unknown): void {
168 this._map.forEach(entry => callbackfn.call(thisArg, entry, entry, this));
169 }
171 > [Symbol.iterator](): SetIterator<T> {
172 return this.values();
173 }
175 > [Symbol.toStringTag]: string = 'SetWithKey';
176 > }
src/vs/base/common/hash.ts 74 covered LOC · 17 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- hash.ts
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 > import { encodeHex, VSBuffer } from './buffer.js';
7 > import * as strings from './strings.js';
8 >
9 > type NotSyncHashable = ArrayBufferLike | ArrayBufferView;
10 >
11 > /**
12 > * Return a hash value for an object.
13 > *
14 > * Note that this should not be used for binary data types. Instead,
15 > * prefer {@link hashAsync}.
16 > */
17 > export function hash<T>(obj: T extends NotSyncHashable ? never : T): number {
18 return doHash(obj, 0);
19 }
20 > hash.ts
21 > export function doHash(obj: unknown, hashVal: number): number {
22 switch (typeof obj) {
23 case 'object':
40 }
41 }
42 > hash.ts
43 > export function numberHash(val: number, initialHashVal: number): number {
44 return (((initialHashVal << 5) - initialHashVal) + val) | 0; // hashVal * 31 + ch, keep as int32
45 }
46 > hash.ts
47 function booleanHash(b: boolean, initialHashVal: number): number {
48 return numberHash(b ? 433 : 863, initialHashVal);
49 }
50 > hash.ts
51 > export function stringHash(s: string, hashVal: number) {
52 hashVal = numberHash(149417, hashVal);
53 for (let i = 0, length = s.length; i < length; i++) {
56 return hashVal;
57 }
58 > hash.ts
59 function arrayHash(arr: unknown[], initialHashVal: number): number {
60 initialHashVal = numberHash(104579, initialHashVal);
61 return arr.reduce<number>((hashVal, item) => doHash(item, hashVal), initialHashVal);
62 }
63 > hash.ts
64 function objectHash(obj: object, initialHashVal: number): number {
65 initialHashVal = numberHash(181387, initialHashVal);
69 }, initialHashVal);
70 }
71 > hash.ts
72 >
73 >
74 > /** Hashes the input as SHA-1, returning a hex-encoded string. */
75 > export const hashAsync = (input: string | ArrayBufferView | VSBuffer) => {
76 // Note: I would very much like to expose a streaming interface for hashing
77 // generally, but this is not available in web crypto yet, see
96 return crypto.subtle.digest('sha-1', buff as ArrayBufferView<ArrayBuffer>).then(toHexString); // CodeQL [SM04514] we use sha1 here for validating old stored client state, not for security
97 };
98 > hash.ts
99 > const enum SHA1Constant {
100 > BLOCK_SIZE = 64, // 512 / 8
101 > UNICODE_REPLACEMENT = 0xFFFD,
102 > }
103 >
104 function leftRotate(value: number, bits: number, totalBits: number = 32): number {
105 // delta + bits = totalBits
112 return ((value << bits) | ((mask & value) >>> delta)) >>> 0;
113 }
114 > hash.ts
115 > function toHexString(buffer: ArrayBuffer): string;
116 > function toHexString(value: number, bitsize?: number): string;
117 function toHexString(bufferOrValue: ArrayBuffer | number, bitsize: number = 32): string {
118 if (bufferOrValue instanceof ArrayBuffer) {
122 return (bufferOrValue >>> 0).toString(16).padStart(bitsize / 4, '0');
123 }
124 > hash.ts
125 > /**
126 > * A SHA1 implementation that works with strings and does not allocate.
127 > *
128 > * Prefer to use {@link hashAsync} in async contexts
129 > */
130 > export class StringSHA1 {
131 > private static _bigBlock32 = new DataView(new ArrayBuffer(320)); // 80 * 4 = 320
132 >
133 > private _h0 = 0x67452301;
134 > private _h1 = 0xEFCDAB89;
135 > private _h2 = 0x98BADCFE;
136 > private _h3 = 0x10325476;
137 > private _h4 = 0xC3D2E1F0;
138 >
139 > private readonly _buff: Uint8Array;
140 > private readonly _buffDV: DataView;
141 > private _buffLen: number;
142 > private _totalLen: number;
143 > private _leftoverHighSurrogate: number;
144 > private _finished: boolean;
145 >
146 > constructor() {
147 this._buff = new Uint8Array(SHA1Constant.BLOCK_SIZE + 3 /* to fit any utf-8 */);
148 this._buffDV = new DataView(this._buff.buffer);
152 this._finished = false;
153 }
154 > hash.ts
155 > public update(str: string): void {
156 const strLen = str.length;
157 if (strLen === 0) {
208 this._leftoverHighSurrogate = leftoverHighSurrogate;
209 }
210 > hash.ts
211 > private _push(buff: Uint8Array, buffLen: number, codePoint: number): number {
212 if (codePoint < 0x0080) {
213 buff[buffLen++] = codePoint;
238 return buffLen;
239 }
240 > hash.ts
241 > public digest(): string {
242 if (!this._finished) {
243 this._finished = true;
253 return toHexString(this._h0) + toHexString(this._h1) + toHexString(this._h2) + toHexString(this._h3) + toHexString(this._h4);
254 }
255 > hash.ts
256 > private _wrapUp(): void {
257 this._buff[this._buffLen++] = 0x80;
258 this._buff.subarray(this._buffLen).fill(0);
271 this._step();
272 }
273 > hash.ts
274 > private _step(): void {
275 const bigBlock32 = StringSHA1._bigBlock32;
276 const data = this._buffDV;
322 this._h4 = (this._h4 + e) & 0xffffffff;
323 }
324 > } hash.ts
src/vs/platform/agentHost/common/state/protocol/channels-resource-watch/state.ts 73 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- state.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > import type { URI } from '../common/state.js';
10 >
11 > // ─── Resource Watch Types ────────────────────────────────────────────────────
12 >
13 > /**
14 > * Full state for a single resource watch, returned when a client subscribes
15 > * to an `ahp-resource-watch:` URI.
16 > *
17 > * Watches are otherwise stateless: the watcher exists to deliver
18 > * {@link ResourceWatchChangedAction} events. The state carries only the
19 > * descriptor of what is being watched so a re-subscribing client can
20 > * recover the watch configuration after reconnecting.
21 > *
22 > * @category Resource Watch Types
23 > */
24 > export interface ResourceWatchState {
25 > /**
26 > * The URI being watched. For recursive watches this is the root of the
27 > * subtree; for non-recursive watches this is the single file or
28 > * directory.
29 > */
30 > root: URI;
31 > /**
32 > * `true` if the watcher reports changes for descendants of `root`;
33 > * `false` if it only reports changes to `root` itself (and, when
34 > * `root` is a directory, its direct children).
35 > */
36 > recursive: boolean;
37 > /**
38 > * Optional glob patterns or paths relative to `root` to exclude from
39 > * change reporting.
40 > */
41 > excludes?: { items: string[] };
42 > /**
43 > * Optional glob patterns or paths relative to `root` to restrict
44 > * change reporting to. Omit to report every change under `root`
45 > * subject to `excludes`.
46 > */
47 > includes?: { items: string[] };
48 > }
49 >
50 > // ─── Resource Change ─────────────────────────────────────────────────────────
51 >
52 > /**
53 > * Discriminant for {@link ResourceChange.type}.
54 > *
55 > * @category Resource Watch Types
56 > */
57 > export const enum ResourceChangeType {
58 > Added = 'added',
59 > Updated = 'updated',
60 > Deleted = 'deleted',
61 > }
62 >
63 > /**
64 > * A single change observed by a resource watcher.
65 > *
66 > * @category Resource Watch Types
67 > */
68 > export interface ResourceChange {
69 > /** The URI of the resource that changed. */
70 > uri: URI;
71 > /** The kind of change observed. */
72 > type: ResourceChangeType;
73 > }
src/vs/platform/agentHost/common/state/protocol/channels-chat/reducer.ts 70 covered LOC · 14 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- reducer.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > import { ActionType } from '../common/actions.js';
10 > import { TurnState, ToolCallStatus, ToolCallConfirmationReason, ToolCallCancellationReason, ToolCallContributorKind, ResponsePartKind, PendingMessageKind, type ChatState, type ToolCallState, type ResponsePart, type ToolCallResponsePart, type InputRequestResponsePart, type Turn, type PendingMessage, type ConfirmationOption } from './state.js';
11 > import { SessionStatus } from '../channels-session/state.js';
12 > import type { ChatAction } from '../action-origin.generated.js';
13 > import { softAssertNever } from '../common/reducer-helpers.js';
14 >
15 > // ─── Helpers ─────────────────────────────────────────────────────────────────
16 >
17 > /** Extracts the common base fields shared by all tool call lifecycle states. */
18 function tcBase(tc: ToolCallState) {
19 return {
26 };
27 }
28 > reducer.ts
29 function tcBaseWithMeta(tc: ToolCallState, meta: Record<string, unknown> | undefined) {
30 return {
33 };
34 }
35 > reducer.ts
36 > /** Resolves a selected option from the confirmation options array by ID. */
37 function resolveSelectedOption(options: ConfirmationOption[] | undefined, id: string | undefined): ConfirmationOption | undefined {
38 if (!id || !options) {
41 return options.find(o => o.id === id);
42 }
43 > reducer.ts
44 > /**
45 > * Returns `true` if the active turn has any tool call blocking on something
46 > * external to the turn itself — a pending confirmation/result-confirmation,
47 > * or a tool call paused on MCP authentication.
48 > */
49 function hasBlockingToolCall(state: ChatState): boolean {
50 if (!state.activeTurn) {
58 );
59 }
60 > reducer.ts
61 > /** Returns whether the active turn contains an input request awaiting submission. */
62 function hasOpenInputRequest(state: ChatState): boolean {
63 return state.activeTurn?.responseParts.some(part =>
65 ) ?? false;
66 }
67 > reducer.ts
68 function findOpenInputRequestPart(
69 responseParts: readonly ResponsePart[],
81 return part.kind === ResponsePartKind.InputRequest ? { index, part } : undefined;
82 }
83 > reducer.ts
84 > /** Bitmask covering the mutually-exclusive activity bits (bits 0–4). */
85 > const STATUS_ACTIVITY_MASK = (1 << 5) - 1;
86 >
87 > /** Sets or clears a metadata flag on a status value. */
88 function withStatusFlag(status: SessionStatus, flag: SessionStatus, set: boolean): SessionStatus {
89 return set ? status | flag : status & ~flag;
90 }
91 > reducer.ts
92 > /** Derives the summary status from live session work, preserving orthogonal flags. */
93 function summaryStatus(state: ChatState, terminalStatus?: SessionStatus.Error): SessionStatus {
94 let activity: SessionStatus;
105 return state.status & ~STATUS_ACTIVITY_MASK | activity;
106 }
107 > reducer.ts
108 > /**
109 > * Returns a state with `status` recomputed. Use this after reducers
110 > * that change data which feeds into {@link summaryStatus} (e.g. tool call
111 > * lifecycle transitions that may enter or leave a pending-confirmation state).
112 > */
113 function refreshSummaryStatus(state: ChatState): ChatState {
114 const status = summaryStatus(state);
118 return { ...state, status };
119 }
120 > reducer.ts
121 > /**
122 > * Ends the active turn, finalizing it into a completed turn record.
123 > *
124 > * Tool call parts with non-terminal states are forced to cancelled.
125 > * Pending permissions are stripped from tool call parts.
126 > */
127 function endTurn(
128 state: ChatState,
183 };
184 }
185 > reducer.ts
186 function upsertInputRequestPart(state: ChatState, request: InputRequestResponsePart['request']): ChatState {
187 const activeTurn = state.activeTurn;
213 return { ...next, status: withStatusFlag(summaryStatus(next), SessionStatus.IsRead, false), modifiedAt: new Date(Date.now()).toISOString() };
214 }
215 > reducer.ts
216 > /**
217 > * Immutably updates the tool call inside a `ToolCall` response part in the
218 > * active turn's `responseParts` array. Returns `state` unchanged if the
219 > * active turn or tool call doesn't match.
220 > */
221 function updateToolCallInParts(
222 state: ChatState,
252 };
253 }
254 > reducer.ts
255 > /**
256 > * Immutably updates a response part by `partId` in the active turn.
257 > * For markdown/reasoning parts, matches on `id`. For tool call parts,
258 > * matches on `toolCall.toolCallId`.
259 > */
260 function updateResponsePart(
261 state: ChatState,
292 };
293 }
294 > reducer.ts
295 >
296 > // ─── Chat Reducer ────────────────────────────────────────────────────────────
297 >
298 > /**
299 > * Pure reducer for chat state. Handles all {@link ChatAction} variants.
300 > */
301 > export function chatReducer(state: ChatState, action: ChatAction, log?: (msg: string) => void): ChatState {
302 switch (action.type) {
303 // ── Turn Lifecycle ────────────────────────────────────────────────────
src/vs/platform/agentHost/node/agentHostChangesetOperationService.ts 70 covered LOC · 14 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostChangesetOperationService.ts
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 > import { CancellationToken } from '../../../base/common/cancellation.js';
7 > import { toErrorMessage } from '../../../base/common/errorMessage.js';
8 > import { Disposable, DisposableMap, toDisposable, type IDisposable } from '../../../base/common/lifecycle.js';
9 > import { parseChangesetUri } from '../common/changesetUri.js';
10 > import type { InvokeChangesetOperationParams, InvokeChangesetOperationResult } from '../common/state/protocol/channels-changeset/commands.js';
11 > import { AHP_SESSION_NOT_FOUND, JsonRpcErrorCodes, ProtocolError } from '../common/state/sessionProtocol.js';
12 > import { ActionType } from '../common/state/sessionActions.js';
13 > import { ChangesetOperationScope, ChangesetOperationStatus, ChangesetOperationTargetKind, ISessionGitHubState, readSessionGitHubState, readSessionGitState, type ChangesetOperation, type ErrorInfo, type ISessionGitState } from '../common/state/sessionState.js';
14 > import type { IChangesetOperationContribution, IAgentHostChangesetOperationService, IChangesetOperationContext, IChangesetOperationHandler, IChangesetOperationRegistry } from '../common/agentHostChangesetOperationService.js';
15 > import { AgentHostStateManager, IAgentHostStateManager } from './agentHostStateManager.js';
16 > import { IAgentHostChangesetSubscriptionService } from '../common/agentHostChangesetSubscriptionService.js';
17 > import { IAgentHostGitStateService } from '../common/agentHostGitStateService.js';
18 >
19 > export class AgentHostChangesetOperationService extends Disposable implements IAgentHostChangesetOperationService {
20 > declare readonly _serviceBrand: undefined;
21 >
22 > private readonly _registry: IChangesetOperationRegistry;
23 > private readonly _handlerRegistrations = this._register(new DisposableMap<IChangesetOperationContribution>());
24 > private readonly _changesetOperationHandlers = new Map<string, IChangesetOperationHandler>();
25 > private readonly _inFlightOperations = new Map<string, Promise<InvokeChangesetOperationResult>>();
26 >
27 > constructor(
28 > @IAgentHostStateManager private readonly _stateManager: AgentHostStateManager, agentHostChangesetOperationService.ts
29 > @IAgentHostGitStateService private readonly _gitStateService: IAgentHostGitStateService,
30 > @IAgentHostChangesetSubscriptionService private readonly _changesetSubscriptions: IAgentHostChangesetSubscriptionService,
31 > ) {
32 > super();
33 >
34 > this._registry = {
35 > registerChangesetOperationHandler: (operationId, handler) => this._registerChangesetOperationHandler(operationId, handler),
36 > refreshSessionGitState: sessionKey => this._gitStateService.refreshSessionGitState(sessionKey),
37 > onDidChangeOperations: sessionKey => this.updateOperations(sessionKey),
38 > };
39 > }
41 > registerContribution(contribution: IChangesetOperationContribution): IDisposable {
42 > if (this._handlerRegistrations.has(contribution)) { agentHostChangesetOperationService.ts
43 throw new Error('Changeset operation contribution already registered');
44 }
45 > this._handlerRegistrations.set(contribution, contribution.registerHandlers(this._registry)); agentHostChangesetOperationService.ts
46 > return toDisposable(() => {
47 > this._handlerRegistrations.deleteAndDispose(contribution);
48 > contribution.dispose();
49 > });
50 > }
52 > getOperations(sessionKey: string, changeset: string, gitState?: ISessionGitState, gitHubState?: ISessionGitHubState): readonly ChangesetOperation[] {
53 if (!gitState) {
54 const sessionState = this._stateManager.getSessionState(sessionKey);
76 });
77 }
79 > private _getOperations(context: IChangesetOperationContext): readonly ChangesetOperation[] {
80 const operations: ChangesetOperation[] = [];
81 for (const contribution of this._handlerRegistrations.keys()) {
97 return operations;
98 }
100 > updateOperations(sessionKey: string, changeset?: string, gitState?: ISessionGitState, gitHubState?: ISessionGitHubState): void {
101 if (!gitState) {
102 const sessionState = this._stateManager.getSessionState(sessionKey);
125 }
126 }
128 > async invokeChangesetOperation(params: InvokeChangesetOperationParams): Promise<InvokeChangesetOperationResult> {
129 const state = this._stateManager.getChangesetState(params.channel);
130 if (!state) {
166 return this._invokeChangesetOperation(handler, params);
167 }
169 > private _invokeChangesetOperation(
170 handler: IChangesetOperationHandler,
171 params: InvokeChangesetOperationParams,
213 return operationPromise;
214 }
216 > private _registerChangesetOperationHandler(operationId: string, handler: IChangesetOperationHandler): IDisposable {
217 > if (this._changesetOperationHandlers.has(operationId)) { agentHostChangesetOperationService.ts
218 throw new Error(`Changeset operation handler already registered for '${operationId}'`);
219 }
220 > this._changesetOperationHandlers.set(operationId, handler); agentHostChangesetOperationService.ts
221 > return toDisposable(() => {
222 > if (this._changesetOperationHandlers.get(operationId) === handler) {
223 > this._changesetOperationHandlers.delete(operationId);
224 > }
225 > });
226 > }
228 >
229 function toChangesetOperationError(error: unknown): ErrorInfo {
230 const message = toErrorMessage(error);
src/vs/base/common/jsonFormatter.ts 69 covered LOC · 6 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- jsonFormatter.ts
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 > import { createScanner, ScanError, SyntaxKind } from './json.js';
7 >
8 > export interface FormattingOptions {
9 > /**
10 > * If indentation is based on spaces (`insertSpaces` = true), then what is the number of spaces that make an indent?
11 > */
12 > tabSize?: number;
13 > /**
14 > * Is indentation based on spaces?
15 > */
16 > insertSpaces?: boolean;
17 > /**
18 > * The default 'end of line' character. If not set, '\n' is used as default.
19 > */
20 > eol?: string;
21 > }
22 >
23 > /**
24 > * Represents a text modification
25 > */
26 > export interface Edit {
27 > /**
28 > * The start offset of the modification.
29 > */
30 > offset: number;
31 > /**
32 > * The length of the modification. Must not be negative. Empty length represents an *insert*.
33 > */
34 > length: number;
35 > /**
36 > * The new content. Empty content represents a *remove*.
37 > */
38 > content: string;
39 > }
40 >
41 > /**
42 > * A text range in the document
43 > */
44 > export interface Range {
45 > /**
46 > * The start offset of the range.
47 > */
48 > offset: number;
49 > /**
50 > * The length of the range. Must not be negative.
51 > */
52 > length: number;
53 > }
54 >
55 >
56 > export function format(documentText: string, range: Range | undefined, options: FormattingOptions): Edit[] {
57 let initialIndentLevel: number;
58 let formatText: string;
202 return editOperations;
203 }
205 > /**
206 > * Creates a formatted string out of the object passed as argument, using the given formatting options
207 > * @param any The object to stringify and format
208 > * @param options The formatting options to use
209 > */
210 > export function toFormattedString(obj: unknown, options: FormattingOptions) {
211 const content = JSON.stringify(obj, undefined, options.insertSpaces ? options.tabSize || 4 : '\t');
212 if (options.eol !== undefined) {
215 return content;
216 }
218 function repeat(s: string, count: number): string {
219 let result = '';
223 return result;
224 }
226 function computeIndentLevel(content: string, options: FormattingOptions): number {
227 let i = 0;
241 return Math.floor(nChars / tabSize);
242 }
244 > export function getEOL(options: FormattingOptions, text: string): string {
245 for (let i = 0; i < text.length; i++) {
246 const ch = text.charAt(i);
256 return (options && options.eol) || '\n';
257 }
259 > export function isEOL(text: string, offset: number) {
260 return '\r\n'.indexOf(text.charAt(offset)) !== -1;
261 }
src/vs/base/common/observableInternal/logging/logging.ts 69 covered LOC · 18 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- logging.ts
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 > import { AutorunObserver } from '../reactions/autorunImpl.js';
7 > import { IObservable } from '../base.js';
8 > import { TransactionImpl } from '../transaction.js';
9 > import type { Derived } from '../observables/derivedImpl.js';
10 > import { DebugLocation } from '../debugLocation.js';
11 >
12 > let globalObservableLogger: IObservableLogger | undefined;
13 >
14 > export function addLogger(logger: IObservableLogger): void {
15 if (!globalObservableLogger) {
16 globalObservableLogger = logger;
21 }
22 }
23 > logging.ts
24 > export function getLogger(): IObservableLogger | undefined {
25 > return globalObservableLogger; logging.ts
26 > }
27 > logging.ts
28 > let globalObservableLoggerFn: ((obs: IObservable<any>) => void) | undefined = undefined;
29 > export function setLogObservableFn(fn: (obs: IObservable<any>) => void): void {
30 > globalObservableLoggerFn = fn;
31 > }
32 >
33 > export function logObservable(obs: IObservable<any>): void {
34 if (globalObservableLoggerFn) {
35 globalObservableLoggerFn(obs);
36 }
37 }
38 > logging.ts
39 > export interface IChangeInformation {
40 > oldValue: unknown;
41 > newValue: unknown;
42 > change: unknown;
43 > didChange: boolean;
44 > hadValue: boolean;
45 > }
46 >
47 > export interface IObservableLogger {
48 > handleObservableCreated(observable: IObservable<any>, location: DebugLocation): void;
49 > handleOnListenerCountChanged(observable: IObservable<any>, newCount: number): void;
50 >
51 > handleObservableUpdated(observable: IObservable<any>, info: IChangeInformation): void;
52 >
53 > handleAutorunCreated(autorun: AutorunObserver, location: DebugLocation): void;
54 > handleAutorunDisposed(autorun: AutorunObserver): void;
55 > handleAutorunDependencyChanged(autorun: AutorunObserver, observable: IObservable<any>, change: unknown): void;
56 > handleAutorunStarted(autorun: AutorunObserver): void;
57 > handleAutorunFinished(autorun: AutorunObserver): void;
58 >
59 > handleDerivedDependencyChanged(derived: Derived<any, any, any>, observable: IObservable<any>, change: unknown): void;
60 > handleDerivedCleared(observable: Derived<any, any, any>): void;
61 >
62 > handleBeginTransaction(transaction: TransactionImpl): void;
63 > handleEndTransaction(transaction: TransactionImpl): void;
64 > }
65 >
66 > class ComposedLogger implements IObservableLogger {
67 > constructor(
68 public readonly loggers: IObservableLogger[],
69 ) { }
70 > logging.ts
71 > handleObservableCreated(observable: IObservable<any>, location: DebugLocation): void {
72 for (const logger of this.loggers) {
73 logger.handleObservableCreated(observable, location);
74 }
75 }
76 > handleOnListenerCountChanged(observable: IObservable<any>, newCount: number): void { logging.ts
77 for (const logger of this.loggers) {
78 logger.handleOnListenerCountChanged(observable, newCount);
79 }
80 }
81 > handleObservableUpdated(observable: IObservable<any>, info: IChangeInformation): void { logging.ts
82 for (const logger of this.loggers) {
83 logger.handleObservableUpdated(observable, info);
84 }
85 }
86 > handleAutorunCreated(autorun: AutorunObserver, location: DebugLocation): void { logging.ts
87 for (const logger of this.loggers) {
88 logger.handleAutorunCreated(autorun, location);
89 }
90 }
91 > handleAutorunDisposed(autorun: AutorunObserver): void { logging.ts
92 for (const logger of this.loggers) {
93 logger.handleAutorunDisposed(autorun);
94 }
95 }
96 > handleAutorunDependencyChanged(autorun: AutorunObserver, observable: IObservable<any>, change: unknown): void { logging.ts
97 for (const logger of this.loggers) {
98 logger.handleAutorunDependencyChanged(autorun, observable, change);
99 }
100 }
101 > handleAutorunStarted(autorun: AutorunObserver): void { logging.ts
102 for (const logger of this.loggers) {
103 logger.handleAutorunStarted(autorun);
104 }
105 }
106 > handleAutorunFinished(autorun: AutorunObserver): void { logging.ts
107 for (const logger of this.loggers) {
108 logger.handleAutorunFinished(autorun);
109 }
110 }
111 > handleDerivedDependencyChanged(derived: Derived<any>, observable: IObservable<any>, change: unknown): void { logging.ts
112 for (const logger of this.loggers) {
113 logger.handleDerivedDependencyChanged(derived, observable, change);
114 }
115 }
116 > handleDerivedCleared(observable: Derived<any>): void { logging.ts
117 for (const logger of this.loggers) {
118 logger.handleDerivedCleared(observable);
119 }
120 }
121 > handleBeginTransaction(transaction: TransactionImpl): void { logging.ts
122 for (const logger of this.loggers) {
123 logger.handleBeginTransaction(transaction);
124 }
125 }
126 > handleEndTransaction(transaction: TransactionImpl): void { logging.ts
127 for (const logger of this.loggers) {
128 logger.handleEndTransaction(transaction);
129 }
130 }
131 > } logging.ts
src/vs/platform/agentHost/node/agentHostWorkspaceFiles.ts 67 covered LOC · 8 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostWorkspaceFiles.ts
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 > import * as cp from 'child_process';
7 > import { CancellationToken } from '../../../base/common/cancellation.js';
8 > import { CancellationError } from '../../../base/common/errors.js';
9 > import { Disposable } from '../../../base/common/lifecycle.js';
10 > import { Schemas } from '../../../base/common/network.js';
11 > import { URI } from '../../../base/common/uri.js';
12 > import { ILogService } from '../../log/common/log.js';
13 > import { rgDiskPath } from '../../../base/node/ripgrep.js';
14 >
15 > /** Maximum number of files cached per working directory. */
16 > const MAX_FILES = 50_000;
17 >
18 > /** TTL for a cached file list before we re-enumerate. */
19 > const CACHE_TTL_MS = 30_000;
20 >
21 > interface ICacheEntry {
22 > readonly promise: Promise<readonly URI[]>;
23 > expiresAt: number;
24 > }
25 >
26 > /**
27 > * Enumerates files under a working directory using ripgrep, with results
28 > * cached per working directory for a short TTL.
29 > *
30 > * Mirrors the workbench's file-search invocation pattern (see
31 > * `ripgrepFileSearch.ts` in `vs/workbench/services/search/node/`) but does
32 > * not depend on the workbench layer — the agent host runs in a separate
33 > * node process that may not import from `vs/workbench/`.
34 > *
35 > * Files are returned as absolute {@link URI}s relative to the working
36 > * directory. `.gitignore` and other `.ignore` files are honoured by
37 > * ripgrep. Symlinks are followed.
38 > */
39 > export class AgentHostWorkspaceFiles extends Disposable {
40 >
41 > private readonly _cache = new Map<string, ICacheEntry>();
42 > /** Active ripgrep child processes, killed on dispose. */
43 > private readonly _activeChildren = new Set<cp.ChildProcessWithoutNullStreams>();
44 >
45 > constructor(
46 > @ILogService private readonly _logService: ILogService, agentHostWorkspaceFiles.ts
47 > ) {
48 > super();
49 > }
51 > override dispose(): void {
52 > for (const child of this._activeChildren) { agentHostWorkspaceFiles.ts
53 try {
54 child.kill();
57 }
58 }
59 > this._activeChildren.clear(); agentHostWorkspaceFiles.ts
60 > this._cache.clear();
61 > super.dispose();
62 > }
64 > /**
65 > * Return the list of files under `workingDirectory`. Concurrent calls
66 > * with the same working directory share an in-flight enumeration.
67 > *
68 > * Only `file://` URIs are supported. Other schemes return an empty list.
69 > */
70 > async getFiles(workingDirectory: URI, token: CancellationToken): Promise<readonly URI[]> {
71 if (workingDirectory.scheme !== Schemas.file) {
72 return [];
115 });
116 }
118 > private async _enumerate(workingDirectory: URI): Promise<readonly URI[]> {
119 const resolvedRgDiskPath = await rgDiskPath();
120 return new Promise<readonly URI[]>(resolve => {
src/vs/platform/agentHost/node/agentHostAuthenticationService.ts 66 covered LOC · 16 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostAuthenticationService.ts
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 > import type { ILogService } from '../../log/common/log.js';
7 > import type { AuthenticateParams, AuthenticateResult, IAgent, IAgentHostAuthTokenRequest } from '../common/agentService.js';
8 >
9 > interface IStoredAuthToken {
10 > readonly resource: string;
11 > readonly scopes: readonly string[];
12 > readonly token: string;
13 > }
14 >
15 > export class AgentHostAuthenticationService {
16 >
17 > private readonly _tokens = new Map<string, IStoredAuthToken>();
18 >
19 > constructor(
20 > private readonly _logService: ILogService, agentHostAuthenticationService.ts
21 > ) { }
23 > async authenticate(params: AuthenticateParams, providers: Iterable<IAgent>): Promise<AuthenticateResult> {
24 > this._logService.trace(`[AgentHostAuthenticationService] authenticate called: resource=${params.resource}`); agentHostAuthenticationService.ts
25 > const providerList = [...providers];
26 > // Multiple providers may share the same protected resource (e.g.
27 > // both Copilot CLI and Claude consume the GitHub Copilot token).
28 > // Fan out to every matching provider in parallel; the request is
29 > // considered authenticated if at least one accepts. Provider
30 > // failures are isolated -- one provider rejecting (e.g. proxy
31 > // server bind failure) MUST NOT prevent another provider from
32 > // accepting the same token.
33 > const matching = providerList.filter(
34 > p => p.getProtectedResources().some(r => r.resource === params.resource),
35 > );
36 > const settled = await Promise.allSettled(
37 > matching.map(p => p.authenticate(params.resource, params.token)),
38 > );
39 > let authenticated = false;
40 > for (let i = 0; i < settled.length; i++) {
41 > const result = settled[i]; agentHostAuthenticationService.ts
42 > if (result.status === 'fulfilled') {
43 authenticated ||= result.value;
45 > this._logService.error( agentHostAuthenticationService.ts
46 > result.reason,
47 > `[AgentHostAuthenticationService] Provider '${matching[i].id}' authenticate threw for resource=${params.resource}`,
48 > );
49 > }
51 > const sessionResourceHandlers = providerList.filter(p => p.handleAuthenticationToken); agentHostAuthenticationService.ts
52 > const sessionResourceSettled = await Promise.allSettled(
53 > sessionResourceHandlers.map(p => p.handleAuthenticationToken ? p.handleAuthenticationToken(params) : Promise.resolve(false)),
54 > );
55 > for (let i = 0; i < sessionResourceSettled.length; i++) {
56 const result = sessionResourceSettled[i];
57 if (result.status === 'fulfilled') {
64 }
65 }
66 > if (authenticated) { agentHostAuthenticationService.ts
67 const scopes = this._normalizeScopes(params.scopes);
68 this._tokens.set(this._key(params.resource, scopes), { resource: params.resource, scopes, token: params.token });
69 }
70 > return { authenticated }; agentHostAuthenticationService.ts
71 > }
73 > getAuthToken(request: IAgentHostAuthTokenRequest): string | undefined {
74 const scopes = this._normalizeScopes(request.scopes);
75 const exact = this._tokens.get(this._key(request.resource, scopes));
102 return this._tokens.get(this._key(request.resource, []))?.token;
103 }
105 > private _containsAll(scopes: readonly string[], requested: ReadonlySet<string>): boolean {
106 for (const scope of requested) {
107 if (!scopes.includes(scope)) {
src/vs/platform/agentHost/node/agentHostHeadlessTerminal.ts 66 covered LOC · 11 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostHeadlessTerminal.ts
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 > import { Emitter, Event } from '../../../base/common/event.js';
7 > import { DeferredPromise } from '../../../base/common/async.js';
8 > import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js';
9 > import type { ILogService } from '../../log/common/log.js';
10 > import pkg from '@xterm/headless';
11 >
12 > type XtermTerminal = pkg.Terminal;
13 > const { Terminal: XtermTerminal } = pkg;
14 >
15 > export interface IAgentHostHeadlessTerminalOptions {
16 > cols: number;
17 > rows: number;
18 > scrollback: number;
19 > logService: ILogService;
20 > terminalFactory?: (options: IXtermTerminalOptions) => XtermTerminal;
21 > }
22 >
23 > /**
24 > * Mirrors an agent-host PTY into xterm's interpreted terminal model.
25 > *
26 > * The mirror is intentionally internal to the agent host. Protocol-visible
27 > * terminal data still flows through the existing OSC 633 parser and content
28 > * model; this class provides terminal responses for programs that query
29 > * terminal state.
30 > */
31 > export class AgentHostHeadlessTerminal extends Disposable {
32 >
33 > private readonly _terminal: XtermTerminal;
34 > private readonly _logService: ILogService;
35 > private readonly _onResponseData = this._register(new Emitter<string>());
36 > readonly onResponseData: Event<string> = this._onResponseData.event;
37 > private _writeBarrier: Promise<void> = Promise.resolve();
38 > private _isDisposed = false;
39 >
40 > constructor(options: IAgentHostHeadlessTerminalOptions) {
41 super();
42 this._logService = options.logService;
64 });
65 }
67 > writePtyData(data: string): Promise<void> {
68 this._writeBarrier = this._writeBarrier.catch(() => undefined).then(() => {
69 if (this._isDisposed) {
80 return this._writeBarrier;
81 }
83 > whenPtyDataFlushed(): Promise<void> {
84 return this._writeBarrier.catch(() => undefined);
85 }
87 > resize(cols: number, rows: number): void {
88 this._terminal.resize(cols, rows);
89 }
91 > isBracketedPasteMode(): boolean {
92 return this._terminal.modes.bracketedPasteMode;
93 }
95 > isInAltBuffer(): boolean {
96 return this._terminal.buffer.active === this._terminal.buffer.alternate;
97 }
99 > createAltBufferPromise(store: DisposableStore): Promise<void> {
100 const deferred = new DeferredPromise<void>();
101 const complete = () => {
118 return deferred.p;
119 }
121 > clear(): void {
122 // xterm.clear() preserves the visible line content; emulate a terminal
123 // clear sequence so future terminal-state reads match a user-visible clear.
124 void this.writePtyData('\x1b[2J\x1b[3J\x1b[H');
125 }
127 > override dispose(): void {
128 this._isDisposed = true;
129 super.dispose();
130 }
132 > private _isCursorPositionReportResponse(data: string): boolean {
133 // Only forward cursor position reports for now. xterm can also answer
134 // device attribute queries, but workbench only forwards those in narrow
136 return /^(?:\x1b\[\??\d+;\d+R)+$/.test(data);
137 }
139 >
140 > interface IXtermTerminalOptions {
141 > cols: number;
142 > rows: number;
143 > scrollback: number;
144 > allowProposedApi: boolean;
145 > }
src/vs/base/common/iterator.ts 65 covered LOC · 23 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- iterator.ts
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 > import { isIterable } from './types.js';
7 >
8 > export namespace Iterable {
9 >
10 > export function is<T = unknown>(thing: unknown): thing is Iterable<T> {
11 > return !!thing && typeof thing === 'object' && typeof (thing as Iterable<T>)[Symbol.iterator] === 'function'; iterator.ts
12 > }
14 > const _empty: Iterable<never> = Object.freeze([]);
15 > export function empty<T = never>(): readonly never[] {
16 return _empty as readonly never[];
17 }
19 > export function* single<T>(element: T): Iterable<T> {
20 yield element;
21 }
23 > export function wrap<T>(iterableOrElement: Iterable<T> | T): Iterable<T> {
24 if (is(iterableOrElement)) {
25 return iterableOrElement;
28 }
29 }
31 > export function from<T>(iterable: Iterable<T> | undefined | null): Iterable<T> {
32 return iterable ?? (_empty as Iterable<T>);
33 }
35 > export function* reverse<T>(array: ReadonlyArray<T>): Iterable<T> {
36 for (let i = array.length - 1; i >= 0; i--) {
37 yield array[i];
38 }
39 }
41 > export function isEmpty<T>(iterable: Iterable<T> | undefined | null): boolean {
42 return !iterable || iterable[Symbol.iterator]().next().done === true;
43 }
45 > export function first<T>(iterable: Iterable<T>): T | undefined {
46 return iterable[Symbol.iterator]().next().value;
47 }
49 > export function some<T>(iterable: Iterable<T>, predicate: (t: T, i: number) => unknown): boolean {
50 let i = 0;
51 for (const element of iterable) {
56 return false;
57 }
59 > export function every<T>(iterable: Iterable<T>, predicate: (t: T, i: number) => unknown): boolean {
60 let i = 0;
61 for (const element of iterable) {
66 return true;
67 }
69 > export function find<T, R extends T>(iterable: Iterable<T>, predicate: (t: T) => t is R): R | undefined;
70 > export function find<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): T | undefined;
71 > export function find<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): T | undefined {
72 for (const element of iterable) {
73 if (predicate(element)) {
78 return undefined;
79 }
81 > export function filter<T, R extends T>(iterable: Iterable<T>, predicate: (t: T) => t is R): Iterable<R>;
82 > export function filter<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): Iterable<T>;
83 > export function* filter<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): Iterable<T> {
84 for (const element of iterable) {
85 if (predicate(element)) {
88 }
89 }
91 > export function* map<T, R>(iterable: Iterable<T>, fn: (t: T, index: number) => R): Iterable<R> {
92 let index = 0;
93 for (const element of iterable) {
95 }
96 }
98 > export function* flatMap<T, R>(iterable: Iterable<T>, fn: (t: T, index: number) => Iterable<R>): Iterable<R> {
99 let index = 0;
100 for (const element of iterable) {
102 }
103 }
104 > iterator.ts
105 > export function* concat<T>(...iterables: (Iterable<T> | T)[]): Iterable<T> {
106 for (const item of iterables) {
107 if (isIterable(item)) {
112 }
113 }
114 > iterator.ts
115 > export function reduce<T, R>(iterable: Iterable<T>, reducer: (previousValue: R, currentValue: T) => R, initialValue: R): R {
116 let value = initialValue;
117 for (const element of iterable) {
120 return value;
121 }
122 > iterator.ts
123 > export function length<T>(iterable: Iterable<T>): number {
124 let count = 0;
125 for (const _ of iterable) {
128 return count;
129 }
130 > iterator.ts
131 > /**
132 > * Returns an iterable slice of the array, with the same semantics as `array.slice()`.
133 > */
134 > export function* slice<T>(arr: ReadonlyArray<T>, from: number, to = arr.length): Iterable<T> {
135 if (from < -arr.length) {
136 from = 0;
150 }
151 }
152 > iterator.ts
153 > /**
154 > * Consumes `atMost` elements from iterable and returns the consumed elements,
155 > * and an iterable for the rest of the elements.
156 > */
157 > export function consume<T>(iterable: Iterable<T>, atMost: number = Number.POSITIVE_INFINITY): [T[], Iterable<T>] {
158 const consumed: T[] = [];
159
176 return [consumed, { [Symbol.iterator]() { return iterator; } }];
177 }
178 > iterator.ts
179 > export async function asyncToArray<T>(iterable: AsyncIterable<T>): Promise<T[]> {
180 const result: T[] = [];
181 for await (const item of iterable) {
184 return result;
185 }
186 > iterator.ts
187 > export async function asyncToArrayFlat<T>(iterable: AsyncIterable<T[]>): Promise<T[]> {
188 let result: T[] = [];
189 for await (const item of iterable) {
src/vs/base/common/themables.ts 65 covered LOC · 14 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- themables.ts
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 > import { Codicon } from './codicons.js';
7 >
8 > export type ColorIdentifier = string;
9 >
10 > export type IconIdentifier = string;
11 >
12 > export interface ThemeColor {
13 > id: string;
14 > }
15 >
16 > export namespace ThemeColor {
17 > export function isThemeColor(obj: unknown): obj is ThemeColor {
18 return !!obj && typeof obj === 'object' && typeof (<ThemeColor>obj).id === 'string';
19 }
20 > } themables.ts
21 >
22 > export function themeColorFromId(id: ColorIdentifier) {
23 return { id };
24 }
26 >
27 > export interface ThemeIcon {
28 > readonly id: string;
29 > readonly color?: ThemeColor;
30 > }
31 >
32 > export namespace ThemeIcon {
33 > export const iconNameSegment = '[A-Za-z0-9]+';
34 > export const iconNameExpression = '[A-Za-z0-9-]+';
35 > export const iconModifierExpression = '~[A-Za-z]+';
36 > export const iconNameCharacter = '[A-Za-z0-9~-]';
37 >
38 > const ThemeIconIdRegex = new RegExp(`^(${iconNameExpression})(${iconModifierExpression})?$`);
39 >
40 > export function asClassNameArray(icon: ThemeIcon): string[] {
41 const match = ThemeIconIdRegex.exec(icon.id);
42 if (!match) {
50 return classNames;
51 }
53 > export function asClassName(icon: ThemeIcon): string {
54 return asClassNameArray(icon).join(' ');
55 }
57 > export function asCSSSelector(icon: ThemeIcon): string {
58 return '.' + asClassNameArray(icon).join('.');
59 }
61 > export function isThemeIcon(obj: unknown): obj is ThemeIcon {
62 return !!obj && typeof obj === 'object' && typeof (<ThemeIcon>obj).id === 'string' && (typeof (<ThemeIcon>obj).color === 'undefined' || ThemeColor.isThemeColor((<ThemeIcon>obj).color));
63 }
65 > const _regexFromString = new RegExp(`^\\$\\((${ThemeIcon.iconNameExpression}(?:${ThemeIcon.iconModifierExpression})?)\\)$`);
66 >
67 > export function fromString(str: string): ThemeIcon | undefined {
68 const match = _regexFromString.exec(str);
69 if (!match) {
73 return { id: name };
74 }
76 > export function fromId(id: string): ThemeIcon {
77 return { id };
78 }
80 > export function modify(icon: ThemeIcon, modifier: 'disabled' | 'spin' | undefined): ThemeIcon {
81 let id = icon.id;
82 const tildeIndex = id.lastIndexOf('~');
89 return { id };
90 }
92 > export function getModifier(icon: ThemeIcon): string | undefined {
93 const tildeIndex = icon.id.lastIndexOf('~');
94 if (tildeIndex !== -1) {
97 return undefined;
98 }
100 > export function isEqual(ti1: ThemeIcon, ti2: ThemeIcon): boolean {
101 return ti1.id === ti2.id && ti1.color?.id === ti2.color?.id;
102 }
103 > themables.ts
104 > /**
105 > * Returns whether specified icon is defined and has 'file' ID.
106 > */
107 > export function isFile(icon: ThemeIcon | undefined): boolean {
108 return icon?.id === Codicon.file.id;
109 }
110 > themables.ts
111 > /**
112 > * Returns whether specified icon is defined and has 'folder' ID.
113 > */
114 > export function isFolder(icon: ThemeIcon | undefined): boolean {
115 return icon?.id === Codicon.folder.id;
116 }
117 > } themables.ts
src/vs/base/common/codicons.ts 64 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- codicons.ts
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 > import { ThemeIcon } from './themables.js';
6 > import { register } from './codiconsUtil.js';
7 > import { codiconsLibrary } from './codiconsLibrary.js';
8 >
9 >
10 > /**
11 > * Only to be used by the iconRegistry.
12 > */
13 > export function getAllCodicons(): ThemeIcon[] {
14 return Object.values(Codicon);
15 }
17 > /**
18 > * Derived icons, that could become separate icons.
19 > * These mappings should be moved into the mapping file in the vscode-codicons repo at some point.
20 > */
21 > export const codiconsDerived = {
22 > dialogError: register('dialog-error', 'error'),
23 > dialogWarning: register('dialog-warning', 'warning'),
24 > dialogInfo: register('dialog-info', 'info'),
25 > dialogClose: register('dialog-close', 'close'),
26 > treeItemExpanded: register('tree-item-expanded', 'chevron-down'), // collapsed is done with rotation
27 > treeFilterOnTypeOn: register('tree-filter-on-type-on', 'list-filter'),
28 > treeFilterOnTypeOff: register('tree-filter-on-type-off', 'list-selection'),
29 > treeFilterClear: register('tree-filter-clear', 'close'),
30 > treeItemLoading: register('tree-item-loading', 'loading'),
31 > menuSelection: register('menu-selection', 'check'),
32 > menuSubmenu: register('menu-submenu', 'chevron-right'),
33 > menuBarMore: register('menubar-more', 'more'),
34 > scrollbarButtonLeft: register('scrollbar-button-left', 'triangle-left'),
35 > scrollbarButtonRight: register('scrollbar-button-right', 'triangle-right'),
36 > scrollbarButtonUp: register('scrollbar-button-up', 'triangle-up'),
37 > scrollbarButtonDown: register('scrollbar-button-down', 'triangle-down'),
38 > toolBarMore: register('toolbar-more', 'more'),
39 > quickInputBack: register('quick-input-back', 'arrow-left'),
40 > dropDownButton: register('drop-down-button', 0xeab4),
41 > symbolCustomColor: register('symbol-customcolor', 0xeb5c),
42 > exportIcon: register('export', 0xebac),
43 > workspaceUnspecified: register('workspace-unspecified', 0xebc3),
44 > newLine: register('newline', 0xebea),
45 > thumbsDownFilled: register('thumbsdown-filled', 0xec13),
46 > thumbsUpFilled: register('thumbsup-filled', 0xec14),
47 > gitFetch: register('git-fetch', 0xec1d),
48 > lightbulbSparkleAutofix: register('lightbulb-sparkle-autofix', 0xec1f),
49 > debugBreakpointPending: register('debug-breakpoint-pending', 0xebd9),
50 > chatImport: register('chat-import', 0xec86),
51 > chatExport: register('chat-export', 0xec87),
52 >
53 > } as const;
54 >
55 > /**
56 > * The Codicon library is a set of default icons that are built-in in VS Code.
57 > *
58 > * In the product (outside of base) Codicons should only be used as defaults. In order to have all icons in VS Code
59 > * themeable, component should define new, UI component specific icons using `iconRegistry.registerIcon`.
60 > * In that call a Codicon can be named as default.
61 > */
62 > export const Codicon = {
63 > ...codiconsLibrary,
64 > ...codiconsDerived
65 >
66 > } as const;
src/vs/platform/agentHost/common/state/protocol/common/notifications.ts 63 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- notifications.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > import type { URI } from './state.js';
10 >
11 > /**
12 > * Reason why authentication is required.
13 > *
14 > * @category Protocol Notifications
15 > */
16 > export const enum AuthRequiredReason {
17 > /** The client has not yet authenticated for the resource */
18 > Required = 'required',
19 > /** A previously valid token has expired or been revoked */
20 > Expired = 'expired',
21 > }
22 >
23 > // ─── auth/required ───────────────────────────────────────────────────────────
24 >
25 > /**
26 > * Sent by the server when a protected resource requires (re-)authentication.
27 > *
28 > * This notification MAY be associated with any channel — for example, an
29 > * agent advertised on the root channel, or a per-session resource. The
30 > * `channel` field identifies the subscription the auth requirement belongs
31 > * to; the `resource` field carries the OAuth-protected resource identifier
32 > * (per RFC 9728).
33 > *
34 > * Clients should obtain a fresh token and push it via the `authenticate`
35 > * command.
36 > *
37 > * @category Protocol Notifications
38 > * @method auth/required
39 > * @direction Server → Client
40 > * @messageType Notification
41 > * @version 1
42 > * @see {@link /specification/authentication | Authentication}
43 > * @example
44 > * ```json
45 > * {
46 > * "jsonrpc": "2.0",
47 > * "method": "auth/required",
48 > * "params": {
49 > * "channel": "ahp-root://",
50 > * "resource": "https://api.github.com",
51 > * "reason": "expired"
52 > * }
53 > * }
54 > * ```
55 > */
56 > export interface AuthRequiredParams {
57 > /** Channel URI this notification belongs to */
58 > channel: URI;
59 > /** The protected resource identifier that requires authentication */
60 > resource: string;
61 > /** Why authentication is required */
62 > reason?: AuthRequiredReason;
63 > }
src/vs/platform/agentHost/node/agentHostCheckpointService.ts 62 covered LOC · 13 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostCheckpointService.ts
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 > import { SequencerByKey } from '../../../base/common/async.js';
7 > import { Disposable } from '../../../base/common/lifecycle.js';
8 > import { URI } from '../../../base/common/uri.js';
9 > import { ILogService } from '../../log/common/log.js';
10 > import { IAgentHostCheckpointService, META_CHECKPOINT_BASE_REF, buildCheckpointRefName } from '../common/agentHostCheckpointService.js';
11 > import { AgentSession } from '../common/agentService.js';
12 > import { ISessionDatabase, ISessionDataService } from '../common/sessionDataService.js';
13 > import { IAgentHostGitService } from '../common/agentHostGitService.js';
14 >
15 > /**
16 > * `session_metadata` key under which the working directory used for
17 > * checkpoint capture is persisted (set when the baseline is created).
18 > * Stored as `URI.toString()`. Read by `captureTurnCheckpoint` /
19 > * `disposeSessionData` so they can resolve the repo without per-call
20 > * working-directory plumbing.
21 > */
22 > export const META_CHECKPOINT_WORKING_DIR = 'checkpoint.workingDir';
23 >
24 > export class AgentHostCheckpointService extends Disposable implements IAgentHostCheckpointService {
25 > declare readonly _serviceBrand: undefined;
26 >
27 > /**
28 > * Serializes capture/dispose per session so back-to-back end-of-turn
29 > * captures don't race on the temp-index files or the `setTurnCheckpointRef`
30 > * write, and a dispose can't run concurrently with an in-flight capture.
31 > * Keyed by session URI string.
32 > */
33 > private readonly _sequencer = new SequencerByKey<string>();
34 >
35 > constructor(
36 @ISessionDataService private readonly _sessionDataService: ISessionDataService,
37 @IAgentHostGitService private readonly _gitService: IAgentHostGitService,
48 }));
49 }
51 > captureBaseline(sessionUri: URI, workingDirectory: URI | undefined): Promise<string | undefined> {
52 return this._sequencer.queue(sessionUri.toString(), () => this._captureBaseline(sessionUri, workingDirectory));
53 }
55 > private async _captureBaseline(sessionUri: URI, workingDirectory: URI | undefined): Promise<string | undefined> {
56 if (!workingDirectory) {
57 return undefined;
85 }
86 }
88 > captureTurnCheckpoint(sessionUri: URI, turnId: string): Promise<string | undefined> {
89 return this._sequencer.queue(sessionUri.toString(), () => this._captureTurnCheckpoint(sessionUri, turnId));
90 }
92 > private async _captureTurnCheckpoint(sessionUri: URI, turnId: string): Promise<string | undefined> {
93 const ref = this._sessionDataService.openDatabase(sessionUri);
94 try {
153 }
154 }
156 > async getTurnCheckpointPair(sessionUri: URI, turnId: string): Promise<{ parent: string; current: string } | undefined> {
157 const ref = this._sessionDataService.openDatabase(sessionUri);
158 try {
174 }
175 }
177 > async getBaselineCheckpointRef(sessionUri: URI): Promise<string | undefined> {
178 const ref = this._sessionDataService.openDatabase(sessionUri);
179 try {
183 }
184 }
186 > async disposeSessionData(sessionUri: URI): Promise<void> {
187 await this._sequencer.queue(sessionUri.toString(), () => this._disposeSessionData(sessionUri));
188 }
190 > private async _disposeSessionData(sessionUri: URI): Promise<void> {
191 const refHandle = await this._sessionDataService.tryOpenDatabase(sessionUri);
192 if (!refHandle) {
228 }
229 }
231 > private async _writeCheckpointCommit(
232 workingDirectory: URI,
233 parentOid: string | undefined,
248 return { commitOid };
249 }
251 > /**
252 > * Parses the highest turn number from the existing refs and returns
253 > * the next one. Falls back to 1 (baseline is always 0).
254 > */
255 > private async _nextTurnNumber(db: ISessionDatabase): Promise<number> {
256 const refs = await db.getAllCheckpointRefs();
257 let max = 0;
src/vs/platform/agentHost/node/shared/agentBranchNameGenerator.ts 62 covered LOC · 7 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentBranchNameGenerator.ts
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 > import { ILogService } from '../../../log/common/log.js';
7 > import { ICopilotApiService, type ICopilotUtilityChatMessage } from './copilotApiService.js';
8 >
9 > /**
10 > * Branch-name prefix for worktree-isolated agent sessions, e.g.
11 > * `agents/add-feature`. Shared by every agent-host provider (Copilot, Codex,
12 > * Claude) via {@link WorktreeIsolation}.
13 > */
14 > export const AGENT_BRANCH_PREFIX = 'agents/';
15 > const AGENT_BRANCH_SESSION_ID_SUFFIX_LENGTH = 8;
16 > const MAX_BRANCH_NAME_HINT_LENGTH = 48;
17 > const MIN_GENERATED_BRANCH_NAME_LENGTH = 8;
18 > const MAX_BRANCH_NAME_CANDIDATES = 100;
19 >
20 > export interface IAgentBranchNameGeneratorRequest {
21 > readonly sessionId: string;
22 > readonly message?: string;
23 > readonly githubToken?: string;
24 > readonly signal?: AbortSignal;
25 > /**
26 > * Optional prefix prepended before the built-in {@link AGENT_BRANCH_PREFIX}
27 > * when constructing the branch name (e.g. the user's `git.branchPrefix`
28 > * setting). An empty or omitted value preserves the historical
29 > * `agents/<hint>` naming.
30 > */
31 > readonly branchPrefix?: string;
32 > /**
33 > * Optional predicate used to check whether a candidate branch name collides
34 > * with an existing branch or its corresponding worktree path.
35 > */
36 > readonly branchNameCollides?: (branchName: string) => Promise<boolean>;
37 > }
38 >
39 > export interface IAgentBranchNameGenerator {
40 > generateBranchName(request: IAgentBranchNameGeneratorRequest): Promise<string>;
41 > }
42 >
43 > export class AgentBranchNameGenerator implements IAgentBranchNameGenerator {
44 >
45 > constructor(
46 @ICopilotApiService private readonly _copilotApiService: ICopilotApiService,
47 @ILogService private readonly _logService: ILogService,
48 ) { }
50 > async generateBranchName(request: IAgentBranchNameGeneratorRequest): Promise<string> {
51 const branchNameHint = (await this._generateBranchNameHint(request)) ?? getAgentBranchNameHintFromMessage(request.message ?? '');
52 return this._buildBranchName(request, branchNameHint);
53 }
55 > private async _generateBranchNameHint(request: IAgentBranchNameGeneratorRequest): Promise<string | undefined> {
56 const message = request.message?.trim();
57 if (!message || !request.githubToken) {
90 }
91 }
93 > private _buildBranchNamePrompt(userRequest: string): ICopilotUtilityChatMessage[] {
94 return [
95 {
111 ];
112 }
114 > private async _buildBranchName(request: IAgentBranchNameGeneratorRequest, branchNameHint: string | undefined): Promise<string> {
115 // Prepend the caller-supplied prefix (e.g. `git.branchPrefix`) ahead of
116 // the built-in `agents/` prefix. An empty/omitted value keeps the
135 throw new Error(`Unable to find an available branch name after checking ${MAX_BRANCH_NAME_CANDIDATES} candidates`);
136 }
138 >
139 > export function normalizeAgentBranchName(branchName: string): string {
140 // Only support alphanumeric characters and dashes for simplicity.
141 let normalized = branchName.replace(/[^a-zA-Z0-9\-]/g, '').toLowerCase();
151 return normalized;
152 }
154 > /**
155 > * Derive a slug-style branch-name hint from the user's first message. Used as
156 > * a local fallback when the utility branch name generation is unavailable.
157 > */
158 > export function getAgentBranchNameHintFromMessage(message: string): string | undefined {
159 const words = message
160 .toLowerCase()
src/vs/platform/agentHost/common/commandLineHelpers.ts 60 covered LOC · 3 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- commandLineHelpers.ts
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 > import { extUriBiasedIgnorePathCase } from '../../../base/common/resources.js';
7 > import { URI } from '../../../base/common/uri.js';
8 >
9 > /**
10 > * Result of {@link extractCdPrefix}: the directory the `cd` jumps to and the
11 > * remaining command after the chain operator.
12 > */
13 > export interface IExtractedCdPrefix {
14 > readonly directory: string;
15 > readonly command: string;
16 > }
17 >
18 > /**
19 > * Extracts a `cd <dir> &&` (or PowerShell equivalent) prefix from a command
20 > * line, returning the directory and remaining command. Does not check whether
21 > * the directory matches anything — callers do that comparison themselves.
22 > *
23 > * The separator between the `cd` and the remaining command may be `&&` or a
24 > * newline (bash treats a bare newline as a command separator like `;`).
25 > * PowerShell additionally accepts `;`. The remaining command may span multiple
26 > * lines (the model frequently emits `cd <dir>` on its own line followed by a
27 > * multi-line script).
28 > *
29 > * Recognized forms:
30 > * - bash: `cd <dir> && <suffix>`, `cd <dir>\n<suffix>`
31 > * - powershell: `cd <dir> && <suffix>`, `cd <dir>; <suffix>`, `cd <dir>\n<suffix>`
32 > * `cd /d <dir> && <suffix>`, `cd /d <dir>; <suffix>`
33 > * `Set-Location <dir> && <suffix>`, `Set-Location <dir>; <suffix>`
34 > * `Set-Location -Path <dir> && <suffix>`, `Set-Location -Path <dir>; <suffix>`
35 > *
36 > * Surrounding double quotes around `<dir>` are stripped.
37 > */
38 > export function extractCdPrefix(commandLine: string, isPowerShell: boolean): IExtractedCdPrefix | undefined {
39 const cdPrefixMatch = commandLine.match(
40 isPowerShell
53 return undefined;
54 }
56 > /**
57 > * If `toolName` is a shell tool (`bash` or `powershell`) and
58 > * `parameters.command` starts with a `cd <workingDirectory> && …` (or
59 > * PowerShell equivalent) prefix, mutate `parameters.command` to drop the
60 > * prefix and return `true`. Returns `false` otherwise.
61 > *
62 > * Path comparison normalizes trailing slashes and is case-insensitive on
63 > * Windows.
64 > */
65 > export function stripRedundantCdPrefix(
66 toolName: string,
67 parameters: Record<string, unknown> | undefined,
90 return true;
91 }
93 > /**
94 > * Compares an extracted `cd <dir>` argument (a raw filesystem path string,
95 > * possibly using either `/` or `\` separators) to a working-directory URI.
96 > * Normalizes separators by routing the extracted string through `URI.file`,
97 > * which converts to the platform-native `fsPath` shape, so that e.g.
98 > * `cd C:/repo` matches a working directory of `C:\repo` on Windows.
99 > *
100 > * Path comparison uses {@link extUriBiasedIgnorePathCase}, which is
101 > * case-insensitive on Windows / macOS.
102 > */
103 function sameDirectory(extractedDir: string, workingDirectory: URI): boolean {
104 if (!extractedDir) {
src/vs/amdX.ts 59 covered LOC · 8 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- amdX.ts
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 > import { AppResourcePath, FileAccess, nodeModulesAsarPath, nodeModulesPath, Schemas, VSCODE_AUTHORITY } from './base/common/network.js';
7 > import * as platform from './base/common/platform.js';
8 > import { IProductConfiguration } from './base/common/product.js';
9 > import { URI } from './base/common/uri.js';
10 > import { generateUuid } from './base/common/uuid.js';
11 >
12 > declare const window: any;
13 > declare const document: any;
14 > declare const self: any;
15 > declare const globalThis: any;
16 >
17 > class DefineCall {
18 > constructor(
19 public readonly id: string | null | undefined,
20 public readonly dependencies: string[] | null | undefined,
21 public readonly callback: any
22 ) { }
23 > } amdX.ts
24 >
25 > enum AMDModuleImporterState {
26 > Uninitialized = 1,
27 > InitializedInternal,
28 > InitializedExternal
29 > }
30 >
31 > class AMDModuleImporter {
32 > public static INSTANCE = new AMDModuleImporter();
33 >
34 > private readonly _isWebWorker = (typeof self === 'object' && self.constructor && self.constructor.name === 'DedicatedWorkerGlobalScope');
35 > private readonly _isRenderer = typeof document === 'object';
36 >
37 > private readonly _defineCalls: DefineCall[] = [];
38 > private _state = AMDModuleImporterState.Uninitialized;
39 > private _amdPolicy: Pick<TrustedTypePolicy, 'name' | 'createScriptURL'> | undefined;
40 >
41 > constructor() { }
42 >
43 > private _initialize(): void {
44 if (this._state === AMDModuleImporterState.Uninitialized) {
45 if (globalThis.define) {
91 }
92 }
93 > amdX.ts
94 > public async load<T>(scriptSrc: string): Promise<T> {
95 this._initialize();
96
134 }
135 }
136 > amdX.ts
137 > private _rendererLoadScript(scriptSrc: string): Promise<DefineCall | undefined> {
138 return new Promise<DefineCall | undefined>((resolve, reject) => {
139 const scriptElement = document.createElement('script');
165 });
166 }
167 > amdX.ts
168 > private async _workerLoadScript(scriptSrc: string): Promise<DefineCall | undefined> {
169 if (this._amdPolicy) {
170 scriptSrc = this._amdPolicy.createScriptURL(scriptSrc) as unknown as string;
173 return this._defineCalls.pop();
174 }
175 > amdX.ts
176 > private async _nodeJSLoadScript(scriptSrc: string): Promise<DefineCall | undefined> {
177 try {
178 // `import('module')` is not remapped (only `fs` is), so it yields the real
198 }
199 }
200 > } amdX.ts
201 >
202 > const cache = new Map<string, Promise<any>>();
203 >
204 > /**
205 > * Utility for importing an AMD node module. This util supports AMD and ESM contexts and should be used while the ESM adoption
206 > * is on its way.
207 > *
208 > * e.g. pass in `vscode-textmate/release/main.js`
209 > */
210 export async function importAMDNodeModule<T>(nodeModuleName: string, pathInsideNodeModule: string, isBuilt?: boolean): Promise<T> {
211 if (isBuilt === undefined) {
233 return result;
234 }
235 > amdX.ts
236 > export function resolveAmdNodeModulePath(nodeModuleName: string, pathInsideNodeModule: string): string {
237 const product = globalThis._VSCODE_PRODUCT_JSON as unknown as IProductConfiguration;
238 const isBuilt = Boolean((product ?? globalThis.vscode?.context?.configuration()?.product)?.commit);
src/vs/base/common/observableInternal/transaction.ts 58 covered LOC · 17 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- transaction.ts
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 > import { handleBugIndicatingErrorRecovery, IObservable, IObserver, ITransaction } from './base.js';
7 > import { getFunctionName } from './debugName.js';
8 > import { getLogger } from './logging/logging.js';
9 >
10 > /**
11 > * Starts a transaction in which many observables can be changed at once.
12 > * {@link fn} should start with a JS Doc using `@description` to give the transaction a debug name.
13 > * Reaction run on demand or when the transaction ends.
14 > */
15 >
16 > export function transaction(fn: (tx: ITransaction) => void, getDebugName?: () => string): void {
17 const tx = new TransactionImpl(fn, getDebugName);
18 try {
22 }
23 }
24 > let _globalTransaction: ITransaction | undefined = undefined; transaction.ts
25 >
26 > export function globalTransaction(fn: (tx: ITransaction) => void) {
27 if (_globalTransaction) {
28 fn(_globalTransaction);
40 }
41 }
42 > /** @deprecated */ transaction.ts
43 >
44 export async function asyncTransaction(fn: (tx: ITransaction) => Promise<void>, getDebugName?: () => string): Promise<void> {
45 const tx = new TransactionImpl(fn, getDebugName);
50 }
51 }
52 > /** transaction.ts
53 > * Allows to chain transactions.
54 > */
55 >
56 > export function subtransaction(tx: ITransaction | undefined, fn: (tx: ITransaction) => void, getDebugName?: () => string): void {
57 if (!tx) {
58 transaction(fn, getDebugName);
60 fn(tx);
61 }
62 > } export class TransactionImpl implements ITransaction { transaction.ts
63 > private _updatingObservers: { observer: IObserver; observable: IObservable<any> }[] | null = [];
64 >
65 > constructor(public readonly _fn: Function, private readonly _getDebugName?: () => string) {
66 > getLogger()?.handleBeginTransaction(this); transaction.ts
67 > }
69 > public getDebugName(): string | undefined {
70 if (this._getDebugName) {
71 return this._getDebugName();
73 return getFunctionName(this._fn);
74 }
76 > public updateObserver(observer: IObserver, observable: IObservable<any>): void {
77 > if (!this._updatingObservers) { transaction.ts
78 // This happens when a transaction is used in a callback or async function.
79 // If an async transaction is used, make sure the promise awaits all users of the transaction (e.g. no race).
85 return;
86 }
88 > // When this gets called while finish is active, they will still get considered
89 > this._updatingObservers.push({ observer, observable });
90 > observer.beginUpdate(observable);
91 > }
93 > public finish(): void {
94 > const updatingObservers = this._updatingObservers; transaction.ts
95 > if (!updatingObservers) {
96 handleBugIndicatingErrorRecovery('transaction.finish() has already been called!');
97 return;
98 }
100 > for (let i = 0; i < updatingObservers.length; i++) {
101 > const { observer, observable } = updatingObservers[i]; transaction.ts
102 > observer.endUpdate(observable);
103 > }
104 > // Prevent anyone from updating observers from now on. transaction.ts
105 > this._updatingObservers = null;
106 > getLogger()?.handleEndTransaction(this);
107 > }
109 > public debugGetUpdatingObservers() {
110 return this._updatingObservers;
111 }
112 > } transaction.ts
113
src/vs/platform/agentHost/common/openSessionLink.ts 58 covered LOC · 7 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- openSessionLink.ts
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 > import { URI } from '../../../base/common/uri.js';
7 > import { AgentSession } from './agentService.js';
8 >
9 > /**
10 > * Dedicated URI scheme for "open this session" links surfaced in agent/tool
11 > * output (e.g. the `create_session` server tool result). A single stable
12 > * scheme keeps the chat markdown allow-list minimal and lets the Agents window
13 > * register one opener, rather than allow-listing every dynamic provider scheme.
14 > *
15 > * Shape: `agent-host-session://<provider>/<rawSessionId>` — the backend session
16 > * URI (`<provider>:/<rawSessionId>`) rearranged so the provider is the
17 > * authority and the id is the path.
18 > */
19 > export const AGENT_HOST_SESSION_LINK_SCHEME = 'agent-host-session';
20 >
21 > /** Name of the `create_session` server tool. */
22 > export const CREATE_SESSION_TOOL_NAME = 'create_session';
23 >
24 > /** Name of the `create_chat` server tool. */
25 > export const CREATE_CHAT_TOOL_NAME = 'create_chat';
26 >
27 > /** Name of the `send_message` server tool. */
28 > export const SEND_MESSAGE_TOOL_NAME = 'send_message';
29 >
30 > /**
31 > * Whether {@link toolName} (as seen on a tool call) matches {@link bareName}.
32 > * Accepts the bare name and a transport prefix such as Claude's
33 > * `mcp__<server>__<name>` (matched as a `__`-delimited suffix).
34 > */
35 function matchesToolName(toolName: string, bareName: string): boolean {
36 return toolName === bareName || toolName.endsWith(`__${bareName}`);
37 }
39 > /** Whether {@link toolName} refers to the `create_session` server tool. */
40 > export function isCreateSessionTool(toolName: string): boolean {
41 return matchesToolName(toolName, CREATE_SESSION_TOOL_NAME);
42 }
44 > /** Whether {@link toolName} refers to the `create_chat` server tool. */
45 > export function isCreateChatTool(toolName: string): boolean {
46 return matchesToolName(toolName, CREATE_CHAT_TOOL_NAME);
47 }
49 > /** Whether {@link toolName} refers to the `send_message` server tool. */
50 > export function isSendMessageTool(toolName: string): boolean {
51 return matchesToolName(toolName, SEND_MESSAGE_TOOL_NAME);
52 }
54 > /** Builds an {@link AGENT_HOST_SESSION_LINK_SCHEME} link for a backend session URI. */
55 > export function buildOpenSessionLinkUri(backendSession: URI | string, chatId?: string): string {
56 const provider = AgentSession.provider(backendSession);
57 const rawId = AgentSession.id(backendSession);
62 return chatId ? `${base}?chat=${encodeURIComponent(chatId)}` : base;
63 }
65 > /**
66 > * Recovers the backend session URI from an {@link AGENT_HOST_SESSION_LINK_SCHEME}
67 > * link, or `undefined` when the URI is not such a link.
68 > */
69 > export function parseOpenSessionLinkUri(uri: URI | string): URI | undefined {
70 const parsed = typeof uri === 'string' ? URI.parse(uri) : uri;
71 if (parsed.scheme !== AGENT_HOST_SESSION_LINK_SCHEME || !parsed.authority) {
78 return AgentSession.uri(parsed.authority, rawId);
79 }
81 > /**
82 > * Recovers the target chat id carried by an {@link AGENT_HOST_SESSION_LINK_SCHEME}
83 > * link (from `create_chat`), or `undefined` when the link targets a whole session.
84 > */
85 > export function parseOpenSessionLinkChatId(uri: URI | string): string | undefined {
86 const parsed = typeof uri === 'string' ? URI.parse(uri) : uri;
87 if (parsed.scheme !== AGENT_HOST_SESSION_LINK_SCHEME) {
src/vs/base/common/observableInternal/index.ts 57 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- index.ts
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 > // This is a facade for the observable implementation. Only import from here!
7 >
8 > export { observableValueOpts } from './observables/observableValueOpts.js';
9 > export { autorun, autorunDelta, autorunHandleChanges, autorunOpts, autorunWithStore, autorunWithStoreHandleChanges, autorunIterableDelta, autorunPerKeyedItem, autorunSelfDisposable, registerAutorunSelfDisposable } from './reactions/autorun.js';
10 > export { type IObservable, type IObservableWithChange, type IObserver, type IReader, type ISettable, type IReaderWithStore, type ISettableObservable, type ITransaction } from './base.js';
11 > export { disposableObservableValue } from './observables/observableValue.js';
12 > export { derived, derivedDisposable, derivedHandleChanges, derivedOpts, derivedWithSetter, derivedWithStore } from './observables/derived.js';
13 > export { type IDerivedReader } from './observables/derivedImpl.js';
14 > export { ObservableLazy, ObservableLazyPromise, ObservablePromise, ObservableResolvedPromise, PromiseResult, } from './utils/promise.js';
15 > export { derivedWithCancellationToken, waitForState } from './utils/utilsCancellation.js';
16 > export {
17 > debouncedObservable, debouncedObservable2, derivedObservableWithCache,
18 > derivedObservableWithWritableCache, keepObserved, mapObservableArrayCached, observableFromPromise,
19 > recomputeInitiallyAndOnChange,
20 > signalFromObservable, throttledObservable, wasEventTriggeredRecently,
21 > isObservable,
22 > } from './utils/utils.js';
23 > export { type DebugOwner } from './debugName.js';
24 > export { type IChangeContext, type IChangeTracker, recordChanges, recordChangesLazy } from './changeTracker.js';
25 > export { constObservable } from './observables/constObservable.js';
26 > export { type IObservableSignal, observableSignal } from './observables/observableSignal.js';
27 > export { observableFromEventOpts } from './observables/observableFromEvent.js';
28 > export { observableSignalFromEvent } from './observables/observableSignalFromEvent.js';
29 > export { asyncTransaction, globalTransaction, subtransaction, transaction, TransactionImpl } from './transaction.js';
30 > export { observableFromValueWithChangeEvent, ValueWithChangeEventFromObservable } from './utils/valueWithChangeEvent.js';
31 > export { runOnChange, runOnChangeWithCancellationToken, runOnChangeWithStore, type RemoveUndefined } from './utils/runOnChange.js';
32 > export { derivedConstOnceDefined, latestChangedValue } from './experimental/utils.js';
33 > export { observableFromEvent } from './observables/observableFromEvent.js';
34 > export { observableValue } from './observables/observableValue.js';
35 >
36 > export { ObservableSet } from './set.js';
37 > export { ObservableMap } from './map.js';
38 > export { DebugLocation } from './debugLocation.js';
39 >
40 > import { addLogger, setLogObservableFn } from './logging/logging.js';
41 > import { ConsoleObservableLogger, logObservableToConsole } from './logging/consoleObservableLogger.js';
42 > import { DevToolsLogger } from './logging/debugger/devToolsLogger.js';
43 > import { env } from '../process.js';
44 > import { _setDebugGetObservableGraph } from './observables/baseObservable.js';
45 > import { debugGetObservableGraph } from './logging/debugGetDependencyGraph.js';
46 >
47 > _setDebugGetObservableGraph(debugGetObservableGraph);
48 > setLogObservableFn(logObservableToConsole);
49 >
50 > // Remove "//" in the next line to enable logging
51 > const enableLogging = false
52 > // || Boolean("true") // done "weirdly" so that a lint warning prevents you from pushing this
53 > ;
54 >
55 > if (enableLogging) {
56 addLogger(new ConsoleObservableLogger());
57 }
58 > index.ts
59 > if (env && env['VSCODE_DEV_DEBUG_OBSERVABLES']) {
60 // To debug observables you also need the extension "ms-vscode.debug-value-editor"
61 addLogger(DevToolsLogger.getInstance());
src/vs/base/common/observableInternal/observables/derived.ts 57 covered LOC · 6 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- derived.ts
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 > import { IObservable, IReader, ITransaction, ISettableObservable, IObservableWithChange } from '../base.js';
7 > import { IChangeTracker } from '../changeTracker.js';
8 > import { DisposableStore, EqualityComparer, IDisposable, strictEquals } from '../commonFacade/deps.js';
9 > import { DebugLocation } from '../debugLocation.js';
10 > import { DebugOwner, DebugNameData, IDebugNameData } from '../debugName.js';
11 > import { _setDerivedOpts } from './baseObservable.js';
12 > import { IDerivedReader, Derived, DerivedWithSetter } from './derivedImpl.js';
13 >
14 > /**
15 > * Creates an observable that is derived from other observables.
16 > * The value is only recomputed when absolutely needed.
17 > *
18 > * {@link computeFn} should start with a JS Doc using `@description` to name the derived.
19 > */
20 > export function derived<T, TChange = void>(computeFn: (reader: IDerivedReader<TChange>, debugLocation?: DebugLocation) => T): IObservableWithChange<T, TChange>;
21 > export function derived<T, TChange = void>(owner: DebugOwner, computeFn: (reader: IDerivedReader<TChange>) => T, debugLocation?: DebugLocation): IObservableWithChange<T, TChange>;
22 > export function derived<T, TChange = void>(
23 computeFnOrOwner: ((reader: IDerivedReader<TChange>) => T) | DebugOwner,
24 computeFn?: ((reader: IDerivedReader<TChange>) => T) | undefined,
46 );
47 }
48 > derived.ts
49 > export function derivedWithSetter<T>(owner: DebugOwner | undefined, computeFn: (reader: IReader) => T, setter: (value: T, transaction: ITransaction | undefined) => void, debugLocation = DebugLocation.ofCaller()): ISettableObservable<T> {
50 return new DerivedWithSetter(
51 new DebugNameData(owner, undefined, computeFn),
58 );
59 }
60 > derived.ts
61 > export function derivedOpts<T>(
62 options: IDebugNameData & {
63 equalsFn?: EqualityComparer<T>;
76 );
77 }
78 > _setDerivedOpts(derivedOpts); derived.ts
79 >
80 > /**
81 > * Represents an observable that is derived from other observables.
82 > * The value is only recomputed when absolutely needed.
83 > *
84 > * {@link computeFn} should start with a JS Doc using `@description` to name the derived.
85 > *
86 > * Use `createEmptyChangeSummary` to create a "change summary" that can collect the changes.
87 > * Use `handleChange` to add a reported change to the change summary.
88 > * The compute function is given the last change summary.
89 > * The change summary is discarded after the compute function was called.
90 > *
91 > * @see derived
92 > */
93 > export function derivedHandleChanges<T, TDelta, TChangeSummary>(
94 options: IDebugNameData & {
95 changeTracker: IChangeTracker<TChangeSummary>;
108 );
109 }
110 > derived.ts
111 > /**
112 > * @deprecated Use `derived(reader => { reader.store.add(...) })` instead!
113 > */
114 > export function derivedWithStore<T>(computeFn: (reader: IReader, store: DisposableStore) => T): IObservable<T>;
115 >
116 > /**
117 > * @deprecated Use `derived(reader => { reader.store.add(...) })` instead!
118 > */
119 > export function derivedWithStore<T>(owner: DebugOwner, computeFn: (reader: IReader, store: DisposableStore) => T): IObservable<T>;
120 > export function derivedWithStore<T>(computeFnOrOwner: ((reader: IReader, store: DisposableStore) => T) | DebugOwner, computeFnOrUndefined?: ((reader: IReader, store: DisposableStore) => T), debugLocation = DebugLocation.ofCaller()): IObservable<T> {
121 let computeFn: (reader: IReader, store: DisposableStore) => T;
122 let owner: DebugOwner;
151 );
152 }
153 > derived.ts
154 > export function derivedDisposable<T extends IDisposable | undefined>(computeFn: (reader: IReader) => T): IObservable<T>;
155 > export function derivedDisposable<T extends IDisposable | undefined>(owner: DebugOwner, computeFn: (reader: IReader) => T): IObservable<T>;
156 > export function derivedDisposable<T extends IDisposable | undefined>(computeFnOrOwner: ((reader: IReader) => T) | DebugOwner, computeFnOrUndefined?: ((reader: IReader) => T), debugLocation = DebugLocation.ofCaller()): IObservable<T> {
157 let computeFn: (reader: IReader) => T;
158 let owner: DebugOwner;
src/vs/platform/agentHost/common/agentHostConversationContext.ts 57 covered LOC · 3 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostConversationContext.ts
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 > import { ResponsePartKind, type ResponsePart, type Turn } from './state/sessionState.js';
7 >
8 > /**
9 > * Options for {@link buildConversationContext}.
10 > */
11 > export interface IConversationContextOptions {
12 > /**
13 > * Soft upper bound, in characters, for the conversation portion of the
14 > * produced context string. When the conversation exceeds this budget its
15 > * middle is removed (marked with `...`) via {@link truncateMiddle}. The
16 > * optional {@link framing} is always preserved in full and does not count
17 > * against this budget.
18 > */
19 > readonly maxChars: number;
20 >
21 > /**
22 > * Optional framing text prepended to the conversation (e.g. a note that the
23 > * conversation was branched from an earlier chat). Always preserved in full
24 > * — only the conversation is truncated to {@link maxChars}.
25 > */
26 > readonly framing?: string;
27 > }
28 >
29 > /**
30 > * Concatenates the normal textual (markdown) response parts of a turn into a
31 > * single string. Tool calls, reasoning, content references, and other
32 > * non-markdown parts are intentionally ignored so that only the assistant's
33 > * user-facing prose is included — this keeps utility-model prompts focused and
34 > * free of large tool payloads or subagent traces.
35 > */
36 > export function renderResponseMarkdown(parts: readonly ResponsePart[]): string {
37 const segments: string[] = [];
38 for (const part of parts) {
46 return segments.join('\n\n');
47 }
49 > /**
50 > * Builds a plain-text conversation context string from the given turns by
51 > * concatenating each turn's user request and the assistant's textual
52 > * (markdown) response. Only normal text response parts are considered — tool
53 > * calls, reasoning, subagent traces, and other parts are ignored (see
54 > * {@link renderResponseMarkdown}). The conversation is middle-truncated to
55 > * {@link IConversationContextOptions.maxChars} to bound model cost; any
56 > * {@link IConversationContextOptions.framing} is prepended afterwards and is
57 > * always preserved in full.
58 > *
59 > * @returns the context string, or `undefined` when no turn carries any text
60 > * worth including.
61 > */
62 > export function buildConversationContext(turns: readonly Turn[], options: IConversationContextOptions): string | undefined {
63 const blocks: string[] = [];
64 for (const turn of turns) {
79 return `${options.framing ?? ''}${truncatedConversation}`;
80 }
82 > /**
83 > * Truncates `text` to at most `maxChars` characters by removing the middle and
84 > * inserting a `...` marker, preserving the start and end.
85 > */
86 > export function truncateMiddle(text: string, maxChars: number): string {
87 if (text.length <= maxChars) {
88 return text;
src/vs/platform/agentHost/common/agentHostFileSystemService.ts 57 covered LOC · 4 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostFileSystemService.ts
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 > import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
7 > import { IFileService } from '../../files/common/files.js';
8 > import { InMemoryFileSystemProvider } from '../../files/common/inMemoryFilesystemProvider.js';
9 > import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
10 > import { createDecorator } from '../../instantiation/common/instantiation.js';
11 > import { ILabelService } from '../../label/common/label.js';
12 > import { AgentHostFileSystemProvider, type IRemoteFilesystemConnection } from './agentHostFileSystemProvider.js';
13 > import { AGENT_HOST_LABEL_FORMATTER, AGENT_HOST_SCHEME } from './agentHostUri.js';
14 >
15 > export type { IRemoteFilesystemConnection } from './agentHostFileSystemProvider.js';
16 >
17 > /**
18 > * Scheme used for the in-memory plugin filesystem backing synced customizations.
19 > *
20 > * URIs under this scheme are served by a registered {@link InMemoryFileSystemProvider}
21 > * and are reachable by the agent host via `fetchContent`.
22 > */
23 > export const SYNCED_CUSTOMIZATION_SCHEME = 'vscode-synced-customization';
24 >
25 > export const IAgentHostFileSystemService = createDecorator<IAgentHostFileSystemService>('agentHostFileSystemService');
26 >
27 > export interface IAgentHostFileSystemService {
28 > readonly _serviceBrand: undefined;
29 >
30 > /**
31 > * Register a mapping from a URI authority to a connection so that
32 > * `vscode-agent-host://[authority]/…` URIs resolve through this connection.
33 > */
34 > registerAuthority(authority: string, connection: IRemoteFilesystemConnection): IDisposable;
35 >
36 > /**
37 > * Ensures the in-memory filesystem provider for synced customizations
38 > * (`vscode-synced-customization:` scheme) is registered. Safe to call
39 > * multiple times — only the first call registers the provider.
40 > */
41 > ensureSyncedCustomizationProvider(): void;
42 > }
43 >
44 > class AgentHostFileSystemService extends Disposable implements IAgentHostFileSystemService {
45 > declare readonly _serviceBrand: undefined;
46 >
47 > private readonly _fsProvider: AgentHostFileSystemProvider;
48 > private _syncedCustomizationProviderRegistered = false;
49 >
50 > constructor(
51 @IFileService private readonly _fileService: IFileService,
52 @ILabelService labelService: ILabelService,
58 this._register(labelService.registerFormatter(AGENT_HOST_LABEL_FORMATTER));
59 }
61 > registerAuthority(authority: string, connection: IRemoteFilesystemConnection): IDisposable {
62 return this._fsProvider.registerAuthority(authority, connection);
63 }
65 > ensureSyncedCustomizationProvider(): void {
66 if (!this._syncedCustomizationProviderRegistered) {
67 this._syncedCustomizationProviderRegistered = true;
70 }
71 }
73 >
74 > registerSingleton(IAgentHostFileSystemService, AgentHostFileSystemService, InstantiationType.Delayed);
src/vs/platform/agentHost/node/shared/serverToolGroups.ts 57 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- serverToolGroups.ts
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 > import { feedbackServerToolGroup } from './agentFeedbackServerTools.js';
7 > import { createSessionServerToolGroup, type ISessionServerToolAccessor } from './sessionServerTools.js';
8 > import type { IServerToolDisplay, IServerToolDisplayResult, IServerToolGroup } from './agentServerToolHost.js';
9 >
10 > /**
11 > * Builds the server-tool groups contributed to every agent host session, in
12 > * priority order. This is the single source of truth wired into the
13 > * {@link AgentServerToolHost} at startup (see `agentService.ts`) and — via
14 > * {@link getServerToolDisplay} — consulted by each provider's display layer.
15 > *
16 > * Adding a group here makes its tools available to all providers (Copilot,
17 > * Claude, Codex, …) and — if the group implements
18 > * {@link IServerToolGroup.getDisplay} — gives them nice display everywhere for
19 > * free.
20 > *
21 > * `sessionAccessor` is the runtime dependency of the session-management group
22 > * (list/create/delete sessions); it is provided by the host at construction.
23 > * When omitted (the pure display path) the session group's `execute` is inert,
24 > * but its definitions and display remain available.
25 > */
26 > export function buildServerToolGroups(sessionAccessor?: ISessionServerToolAccessor): readonly IServerToolGroup[] {
27 > return [feedbackServerToolGroup, createSessionServerToolGroup(sessionAccessor)];
28 > }
29 >
30 > /**
31 > * The groups used by the pure {@link getServerToolDisplay} path. Built without a
32 > * session accessor since display never invokes `execute`.
33 > */
34 > const serverToolGroupsForDisplay: readonly IServerToolGroup[] = buildServerToolGroups();
35 >
36 > /**
37 > * Whether {@link toolName} (a tool name as seen on a tool call) refers to the
38 > * server tool {@link bareName}. Accepts both the bare name and a transport
39 > * prefix such as Claude's `mcp__<server>__<name>` (matched as a `__`-delimited
40 > * suffix), mirroring the convention in `agentFeedbackAnnotations.ts`.
41 > */
42 function matchesServerToolName(toolName: string, bareName: string): boolean {
43 return toolName === bareName || toolName.endsWith(`__${bareName}`);
44 }
46 > /**
47 > * Resolves the {@link IServerToolDisplay} for a server tool call, authored by
48 > * the group that owns the tool. Returns `undefined` when no contributed group
49 > * owns {@link toolName} or the owning group has no bespoke display, so each
50 > * provider's display layer can fall back to its generic behavior.
51 > *
52 > * Pure over the contributed groups (it does not need the constructed
53 > * {@link AgentServerToolHost}) so the providers' history-replay paths — which
54 > * build display from pure functions without a host instance — can call it too.
55 > *
56 > * @param toolName The tool name as seen on the call (bare or transport-prefixed).
57 > * @param args The parsed tool arguments.
58 > * @param result The tool result, once it has completed; absent while running.
59 > */
60 > export function getServerToolDisplay(toolName: string, args: unknown, result?: IServerToolDisplayResult): IServerToolDisplay | undefined {
61 for (const group of serverToolGroupsForDisplay) {
62 if (!group.getDisplay) {
src/vs/base/common/observableInternal/debugName.ts 56 covered LOC · 9 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- debugName.ts
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 > export interface IDebugNameData {
7 > /**
8 > * The owner object of an observable.
9 > * Used for debugging only, such as computing a name for the observable by iterating over the fields of the owner.
10 > */
11 > readonly owner?: DebugOwner | undefined;
12 >
13 > /**
14 > * A string or function that returns a string that represents the name of the observable.
15 > * Used for debugging only.
16 > */
17 > readonly debugName?: DebugNameSource | undefined;
18 >
19 > /**
20 > * A function that points to the defining function of the object.
21 > * Used for debugging only.
22 > */
23 > readonly debugReferenceFn?: Function | undefined;
24 > }
25 >
26 > export class DebugNameData {
27 > constructor(
28 > public readonly owner: DebugOwner | undefined, debugName.ts
29 > public readonly debugNameSource: DebugNameSource | undefined,
30 > public readonly referenceFn: Function | undefined,
31 > ) { }
33 > public getDebugName(target: object): string | undefined {
34 return getDebugName(target, this);
35 }
36 > } debugName.ts
37 >
38 > /**
39 > * The owning object of an observable.
40 > * Is only used for debugging purposes, such as computing a name for the observable by iterating over the fields of the owner.
41 > */
42 > export type DebugOwner = object | undefined;
43 > export type DebugNameSource = string | (() => string | undefined);
44 >
45 > const countPerName = new Map<string, number>();
46 > const cachedDebugName = new WeakMap<object, string>();
47 >
48 > export function getDebugName(target: object, data: DebugNameData): string | undefined {
49 const cached = cachedDebugName.get(target);
50 if (cached) {
63 return undefined;
64 }
66 function computeDebugName(self: object, data: DebugNameData): string | undefined {
67 const cached = cachedDebugName.get(self);
101 return undefined;
102 }
103 > debugName.ts
104 function findKey(obj: object, value: object): string | undefined {
105 for (const key in obj) {
110 return undefined;
111 }
112 > debugName.ts
113 > const countPerClassName = new Map<string, number>();
114 > const ownerId = new WeakMap<object, string>();
115 >
116 function formatOwner(owner: object): string {
117 const id = ownerId.get(owner);
127 return result;
128 }
129 > debugName.ts
130 > export function getClassName(obj: object): string | undefined {
131 const ctor = obj.constructor;
132 if (ctor) {
138 return undefined;
139 }
140 > debugName.ts
141 > export function getFunctionName(fn: Function): string | undefined {
142 const fnSrc = fn.toString();
143 // Pattern: /** @description ... */
src/vs/base/test/common/utils.ts 56 covered LOC · 13 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- utils.ts
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 > import { DisposableStore, DisposableTracker, IDisposable, setDisposableTracker } from '../../common/lifecycle.js';
7 > import { join } from '../../common/path.js';
8 > import { isWindows } from '../../common/platform.js';
9 > import { URI } from '../../common/uri.js';
10 >
11 > export type ValueCallback<T = any> = (value: T | Promise<T>) => void;
12 >
13 > export function toResource(this: any, path: string): URI {
14 if (isWindows) {
15 return URI.file(join('C:\\', btoa(this.test.fullTitle()), path));
18 return URI.file(join('/', btoa(this.test.fullTitle()), path));
19 }
20 > utils.ts
21 > export function suiteRepeat(n: number, description: string, callback: (this: any) => void): void {
22 for (let i = 0; i < n; i++) {
23 suite(`${description} (iteration ${i})`, callback);
24 }
25 }
26 > utils.ts
27 > export function testRepeat(n: number, description: string, callback: (this: any) => any): void {
28 for (let i = 0; i < n; i++) {
29 test(`${description} (iteration ${i})`, callback);
30 }
31 }
32 > utils.ts
33 export async function assertThrowsAsync(block: () => any, message: string | Error = 'Missing expected exception'): Promise<void> {
34 try {
41 throw err;
42 }
43 > utils.ts
44 > /**
45 > * Use this function to ensure that all disposables are cleaned up at the end of each test in the current suite.
46 > *
47 > * Use `markAsSingleton` if disposable singletons are created lazily that are allowed to outlive the test.
48 > * Make sure that the singleton properly registers all child disposables so that they are excluded too.
49 > *
50 > * @returns A {@link DisposableStore} that can optionally be used to track disposables in the test.
51 > * This will be automatically disposed on test teardown.
52 > */
53 > export function ensureNoDisposablesAreLeakedInTestSuite(): Pick<DisposableStore, 'add'> {
54 > let tracker: DisposableTracker | undefined;
55 > let store: DisposableStore;
56 > setup(() => {
57 > store = new DisposableStore(); utils.ts
58 > tracker = new DisposableTracker();
59 > setDisposableTracker(tracker);
60 > }); utils.ts
61 >
62 > teardown(function (this: import('mocha').Context) {
63 > store.dispose(); utils.ts
64 > setDisposableTracker(null);
65 > if (this.currentTest?.state !== 'failed') {
66 > const result = tracker!.computeLeakingDisposables();
67 > if (result) {
68 console.error(result.details);
69 throw new Error(`There are ${result.leaks.length} undisposed disposables!${result.details}`);
70 }
71 > } utils.ts
72 > }); utils.ts
73 >
74 > // Wrap store as the suite function is called before it's initialized
75 > const testContext = {
76 > add<T extends IDisposable>(o: T): T {
77 return store.add(o);
78 }
79 > }; utils.ts
80 > return testContext;
81 > }
82 >
83 > export function throwIfDisposablesAreLeaked(body: () => void, logToConsole = true): void {
84 const tracker = new DisposableTracker();
85 setDisposableTracker(tracker);
88 computeLeakingDisposables(tracker, logToConsole);
89 }
90 > utils.ts
91 export async function throwIfDisposablesAreLeakedAsync(body: () => Promise<void>): Promise<void> {
92 const tracker = new DisposableTracker();
96 computeLeakingDisposables(tracker);
97 }
98 > utils.ts
99 function computeLeakingDisposables(tracker: DisposableTracker, logToConsole = true) {
100 const result = tracker.computeLeakingDisposables();
src/vs/platform/policy/common/policy.ts 56 covered LOC · 8 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- policy.ts
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 > import { IStringDictionary } from '../../../base/common/collections.js';
7 > import { IPolicyData } from '../../../base/common/defaultAccount.js';
8 > import { Emitter, Event } from '../../../base/common/event.js';
9 > import { Iterable } from '../../../base/common/iterator.js';
10 > import { Disposable } from '../../../base/common/lifecycle.js';
11 > import { IManagedSettingsPolicyDefinitions, PolicyName } from '../../../base/common/policy.js';
12 > import { createDecorator } from '../../instantiation/common/instantiation.js';
13 >
14 > export type PolicyValue = string | number | boolean;
15 > export type PolicyDefinition = {
16 > type: 'string' | 'number' | 'boolean';
17 > value?: (policyData: IPolicyData) => string | number | boolean | undefined;
18 > managedSettings?: IManagedSettingsPolicyDefinitions;
19 > restrictedValue?: PolicyValue;
20 > };
21 >
22 > /** Returns a structured-clone-safe copy of `definition`, dropping the non-cloneable `value` callback. */
23 > export function toSerializablePolicyDefinition(definition: PolicyDefinition): PolicyDefinition {
24 return { type: definition.type, managedSettings: definition.managedSettings, restrictedValue: definition.restrictedValue };
25 }
26 > policy.ts
27 > /**
28 > * Returns the value to apply for `definition` when the account-policy gate is active
29 > * but not satisfied. Uses `definition.restrictedValue` when specified, otherwise falls
30 > * back to a type-driven safe default.
31 > */
32 > export function getRestrictedPolicyValue(definition: PolicyDefinition): PolicyValue {
33 if (definition.restrictedValue !== undefined) {
34 return definition.restrictedValue;
40 }
41 }
42 > policy.ts
43 > export const IPolicyService = createDecorator<IPolicyService>('policy');
44 >
45 > export interface IPolicyService {
46 > readonly _serviceBrand: undefined;
47 >
48 > readonly onDidChange: Event<readonly PolicyName[]>;
49 > updatePolicyDefinitions(policyDefinitions: IStringDictionary<PolicyDefinition>): Promise<IStringDictionary<PolicyValue>>;
50 > getPolicyValue(name: PolicyName): PolicyValue | undefined;
51 > serialize(): IStringDictionary<{ definition: PolicyDefinition; value: PolicyValue }> | undefined;
52 > readonly policyDefinitions: IStringDictionary<PolicyDefinition>;
53 > }
54 >
55 > export abstract class AbstractPolicyService extends Disposable implements IPolicyService {
56 readonly _serviceBrand: undefined;
57
61 protected readonly _onDidChange = this._register(new Emitter<readonly PolicyName[]>());
62 readonly onDidChange = this._onDidChange.event;
63 > policy.ts
64 > async updatePolicyDefinitions(policyDefinitions: IStringDictionary<PolicyDefinition>): Promise<IStringDictionary<PolicyValue>> {
65 // Replace existing definitions; identity comparison avoids redundant watcher churn.
66 let changed = false;
78 return Iterable.reduce(this.policies.entries(), (r, [name, value]) => ({ ...r, [name]: value }), {});
79 }
80 > policy.ts
81 > getPolicyValue(name: PolicyName): PolicyValue | undefined {
82 return this.policies.get(name);
83 }
84 > policy.ts
85 > serialize(): IStringDictionary<{ definition: PolicyDefinition; value: PolicyValue }> {
86 return Iterable.reduce<[PolicyName, PolicyDefinition], IStringDictionary<{ definition: PolicyDefinition; value: PolicyValue }>>(Object.entries(this.policyDefinitions), (r, [name, definition]) => ({ ...r, [name]: { definition: toSerializablePolicyDefinition(definition), value: this.policies.get(name)! } }), {});
87 }
88 > policy.ts
89 > protected abstract _updatePolicyDefinitions(policyDefinitions: IStringDictionary<PolicyDefinition>): Promise<void>;
90 > }
91 >
92 > export class NullPolicyService implements IPolicyService {
93 readonly _serviceBrand: undefined;
94 readonly onDidChange = Event.None;
97 serialize() { return undefined; }
98 policyDefinitions: IStringDictionary<PolicyDefinition> = {};
99 > } policy.ts
src/vs/platform/agentHost/node/agentHostGitStateService.ts 55 covered LOC · 8 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostGitStateService.ts
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 > import { equals as objectEquals } from '../../../base/common/objects.js';
7 > import { URI } from '../../../base/common/uri.js';
8 > import { Emitter } from '../../../base/common/event.js';
9 > import { ILogService } from '../../log/common/log.js';
10 > import { IAgentHostGitStateService, META_GIT_STATE, META_GITHUB_STATE } from '../common/agentHostGitStateService.js';
11 > import { ISessionGitHubState, readSessionGitHubState, readSessionGitState, SessionLifecycle, withSessionGitHubState, withSessionGitState, type ISessionGitState } from '../common/state/sessionState.js';
12 > import { IAgentHostGitService } from '../common/agentHostGitService.js';
13 > import { AgentHostStateManager, IAgentHostStateManager } from './agentHostStateManager.js';
14 > import { ISessionDataService } from '../common/sessionDataService.js';
15 > import { IAgentHostOctoKitService } from './shared/agentHostOctoKitService.js';
16 > import { IAgentService } from '../common/agentService.js';
17 > import { IAgentHostGitHubEndpointService } from './agentHostGitHubEndpointService.js';
18 > import { Disposable, toDisposable } from '../../../base/common/lifecycle.js';
19 > import { CancellationTokenSource } from '../../../base/common/cancellation.js';
20 > import { ThrottlerByKey, timeout } from '../../../base/common/async.js';
21 > import { isCancellationError } from '../../../base/common/errors.js';
22 >
23 > export class AgentHostGitStateService extends Disposable implements IAgentHostGitStateService {
24 > declare readonly _serviceBrand: undefined;
25 >
26 > private readonly _onDidRefreshSessionGitState = this._register(new Emitter<string>());
27 > readonly onDidRefreshSessionGitState = this._onDidRefreshSessionGitState.event;
28 >
29 > private readonly _gitStateRefreshThrottler = this._register(new ThrottlerByKey<string>());
30 > private readonly _gitStateRefreshCancellationTokenSource = new CancellationTokenSource();
31 >
32 > constructor(
33 > @IAgentHostStateManager private readonly _stateManager: AgentHostStateManager, agentHostGitStateService.ts
34 > @IAgentHostGitService private readonly _gitService: IAgentHostGitService,
35 > @IAgentHostOctoKitService private readonly _octoKitService: IAgentHostOctoKitService,
36 > @IAgentService private readonly _agentService: IAgentService,
37 > @IAgentHostGitHubEndpointService private readonly _gitHubEndpointService: IAgentHostGitHubEndpointService,
38 > @ILogService private readonly _logService: ILogService,
39 > @ISessionDataService private readonly _sessionDataService: ISessionDataService,
40 > ) {
41 > super();
42 >
43 > this._register(toDisposable(() => this._gitStateRefreshCancellationTokenSource.dispose(true)));
44 > }
46 > async attachSessionGitHubPullRequest(sessionKey: string): Promise<void> {
47 const state = this._stateManager.getSessionState(sessionKey);
48 if (!state) {
93 }
94 }
96 > async refreshSessionGitState(sessionKey: string, workingDirectory: URI | undefined): Promise<void> {
97 const sessionState = this._stateManager.getSessionState(sessionKey);
98 if (sessionState?.lifecycle === SessionLifecycle.CreationFailed) {
148 });
149 }
151 > async setSessionGitHubState(sessionKey: string, state: ISessionGitHubState): Promise<void> {
152 const currentMeta = this._stateManager.getSessionState(sessionKey)?._meta;
153
166 await this._saveSessionState(sessionKey, META_GITHUB_STATE, JSON.stringify(nextState));
167 }
169 > private async _setSessionGitState(sessionKey: string, gitState: ISessionGitState): Promise<void> {
170 // Update session state manager
171 const currentMeta = this._stateManager.getSessionState(sessionKey)?._meta;
176 await this._saveSessionState(sessionKey, META_GIT_STATE, JSON.stringify(gitState));
177 }
179 > private async _saveSessionState(sessionKey: string, key: string, value: string): Promise<void> {
180 // Skip saving session state if the session is not materialized
181 const state = this._stateManager.getSessionState(sessionKey);
src/vs/platform/registry/common/platform.ts 54 covered LOC · 7 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- platform.ts
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 > import * as Assert from '../../../base/common/assert.js';
7 > import * as Types from '../../../base/common/types.js';
8 >
9 > export interface IRegistry {
10 >
11 > /**
12 > * Adds the extension functions and properties defined by data to the
13 > * platform. The provided id must be unique.
14 > * @param id a unique identifier
15 > * @param data a contribution
16 > */
17 > add(id: string, data: any): void;
18 >
19 > /**
20 > * Returns true iff there is an extension with the provided id.
21 > * @param id an extension identifier
22 > */
23 > knows(id: string): boolean;
24 >
25 > /**
26 > * Returns the extension functions and properties defined by the specified key or null.
27 > * @param id an extension identifier
28 > */
29 > as<T>(id: string): T;
30 > }
31 >
32 > class RegistryImpl implements IRegistry {
33 >
34 > private readonly data = new Map<string, any>();
35 >
36 > public add(id: string, data: any): void {
37 > Assert.ok(Types.isString(id)); platform.ts
38 > Assert.ok(Types.isObject(data));
39 > Assert.ok(!this.data.has(id), 'There is already an extension with this id');
40 >
41 > this.data.set(id, data);
42 > }
44 > public knows(id: string): boolean {
45 return this.data.has(id);
46 }
48 > public as(id: string): any {
49 > return this.data.get(id) || null; platform.ts
50 > }
52 > public dispose() {
53 this.data.forEach((value) => {
54 if (Types.isFunction(value.dispose)) {
58 this.data.clear();
59 }
61 > }
62 >
63 > export const Registry: IRegistry = new RegistryImpl();
src/vs/base/common/observableInternal/logging/debugger/rpc.ts 53 covered LOC · 3 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- rpc.ts
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 > export type ChannelFactory = (handler: IChannelHandler) => IChannel;
7 >
8 > export interface IChannel {
9 > sendNotification(data: unknown): void;
10 > sendRequest(data: unknown): Promise<RpcRequestResult>;
11 > }
12 >
13 > export interface IChannelHandler {
14 > handleNotification(notificationData: unknown): void;
15 > handleRequest(requestData: unknown): Promise<RpcRequestResult> | RpcRequestResult;
16 > }
17 >
18 > export type RpcRequestResult = { type: 'result'; value: unknown } | { type: 'error'; value: unknown };
19 >
20 > export type API = {
21 > host: Side;
22 > client: Side;
23 > };
24 >
25 > export type Side = {
26 > notifications: Record<string, (...args: any[]) => void>;
27 > requests: Record<string, (...args: any[]) => Promise<unknown> | unknown>;
28 > };
29 >
30 > type MakeAsyncIfNot<TFn> = TFn extends (...args: infer TArgs) => infer TResult ? TResult extends Promise<unknown> ? TFn : (...args: TArgs) => Promise<TResult> : never;
31 >
32 > export type MakeSideAsync<T extends Side> = {
33 > notifications: T['notifications'];
34 > requests: { [K in keyof T['requests']]: MakeAsyncIfNot<T['requests'][K]> };
35 > };
36 >
37 > export class SimpleTypedRpcConnection<T extends Side> {
38 > public static createHost<T extends API>(channelFactory: ChannelFactory, getHandler: () => T['host']): SimpleTypedRpcConnection<MakeSideAsync<T['client']>> {
39 > return new SimpleTypedRpcConnection(channelFactory, getHandler);
40 > }
41 >
42 > public static createClient<T extends API>(channelFactory: ChannelFactory, getHandler: () => T['client']): SimpleTypedRpcConnection<MakeSideAsync<T['host']>> {
43 return new SimpleTypedRpcConnection(channelFactory, getHandler);
44 }
45 > rpc.ts
46 > public readonly api: T;
47 > private readonly _channel: IChannel;
48 >
49 > private constructor(
50 private readonly _channelFactory: ChannelFactory,
51 private readonly _getHandler: () => Side,
95 this.api = { notifications: notifications, requests: requests } as any;
96 }
97 > } rpc.ts
98 >
99 > type OutgoingMessage = [
100 > method: string,
101 > args: unknown[],
102 > ];
src/vs/base/common/process.ts 53 covered LOC · 3 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- process.ts
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 > import { INodeProcess, isMacintosh, isWindows } from './platform.js';
7 >
8 > let safeProcess: Omit<INodeProcess, 'arch'> & { arch: string | undefined };
9 > declare const process: INodeProcess;
10 >
11 > // Native sandbox environment
12 > const vscodeGlobal = (globalThis as { vscode?: { process?: INodeProcess } }).vscode;
13 > if (typeof vscodeGlobal !== 'undefined' && typeof vscodeGlobal.process !== 'undefined') {
14 const sandboxProcess: INodeProcess = vscodeGlobal.process;
15 safeProcess = {
20 };
21 }
22 > process.ts
23 > // Native node.js environment
24 > else if (typeof process !== 'undefined' && typeof process?.versions?.node === 'string') {
25 > safeProcess = {
26 > get platform() { return process.platform; },
27 > get arch() { return process.arch; },
28 > get env() { return process.env; },
29 > cwd() { return process.env['VSCODE_CWD'] || process.cwd(); }
30 > };
31 }
32
44 };
45 }
46 > process.ts
47 > /**
48 > * Provides safe access to the `cwd` property in node.js, sandboxed or web
49 > * environments.
50 > *
51 > * Note: in web, this property is hardcoded to be `/`.
52 > *
53 > * @skipMangle
54 > */
55 > export const cwd = safeProcess.cwd;
56 >
57 > /**
58 > * Provides safe access to the `env` property in node.js, sandboxed or web
59 > * environments.
60 > *
61 > * Note: in web, this property is hardcoded to be `{}`.
62 > */
63 > export const env = safeProcess.env;
64 >
65 > /**
66 > * Provides safe access to the `platform` property in node.js, sandboxed or web
67 > * environments.
68 > */
69 > export const platform = safeProcess.platform;
70 >
71 > /**
72 > * Provides safe access to the `arch` method in node.js, sandboxed or web
73 > * environments.
74 > * Note: `arch` is `undefined` in web
75 > */
76 > export const arch = safeProcess.arch;
src/vs/base/common/assert.ts 52 covered LOC · 11 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- assert.ts
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 > import { BugIndicatingError, onUnexpectedError } from './errors.js';
7 >
8 > /**
9 > * Throws an error with the provided message if the provided value does not evaluate to a true Javascript value.
10 > *
11 > * @deprecated Use `assert(...)` instead.
12 > * This method is usually used like this:
13 > * ```ts
14 > * import * as assert from 'vs/base/common/assert';
15 > * assert.ok(...);
16 > * ```
17 > *
18 > * However, `assert` in that example is a user chosen name.
19 > * There is no tooling for generating such an import statement.
20 > * Thus, the `assert(...)` function should be used instead.
21 > */
22 > export function ok(value?: unknown, message?: string) {
23 > if (!value) { assert.ts
24 throw new Error(message ? `Assertion failed (${message})` : 'Assertion Failed');
25 }
26 > } assert.ts
27 > assert.ts
28 > export function assertNever(value: never, message = 'Unreachable'): never {
29 throw new Error(message);
30 }
31 > assert.ts
32 > export function softAssertNever(value: never): void {
33 // no-op
34 }
35 > assert.ts
36 > /**
37 > * Asserts that a condition is `truthy`.
38 > *
39 > * @throws provided {@linkcode messageOrError} if the {@linkcode condition} is `falsy`.
40 > *
41 > * @param condition The condition to assert.
42 > * @param messageOrError An error message or error object to throw if condition is `falsy`.
43 > */
44 > export function assert(
45 condition: boolean,
46 messageOrError: string | Error = 'unexpected state',
55 }
56 }
57 > assert.ts
58 > /**
59 > * Like assert, but doesn't throw.
60 > */
61 > export function softAssert(condition: boolean, message = 'Soft Assertion Failed'): void {
62 if (!condition) {
63 onUnexpectedError(new BugIndicatingError(message));
64 }
65 }
66 > assert.ts
67 > /**
68 > * condition must be side-effect free!
69 > */
70 > export function assertFn(condition: () => boolean): void {
71 > if (!condition()) { assert.ts
72 // eslint-disable-next-line no-debugger
73 debugger;
76 onUnexpectedError(new BugIndicatingError('Assertion Failed'));
77 }
78 > } assert.ts
79 > assert.ts
80 > export function checkAdjacentItems<T>(items: readonly T[], predicate: (item1: T, item2: T) => boolean): boolean {
81 let i = 0;
82 while (i < items.length - 1) {
src/vs/base/common/observableInternal/logging/consoleObservableLogger.ts 52 covered LOC · 20 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- consoleObservableLogger.ts
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 > import { IObservable } from '../base.js';
7 > import { TransactionImpl } from '../transaction.js';
8 > import { IObservableLogger, IChangeInformation, addLogger } from './logging.js';
9 > import { FromEventObservable } from '../observables/observableFromEvent.js';
10 > import { getClassName } from '../debugName.js';
11 > import { Derived } from '../observables/derivedImpl.js';
12 > import { AutorunObserver } from '../reactions/autorunImpl.js';
13 >
14 > let consoleObservableLogger: ConsoleObservableLogger | undefined;
15 >
16 > export function logObservableToConsole(obs: IObservable<any>): void {
17 if (!consoleObservableLogger) {
18 consoleObservableLogger = new ConsoleObservableLogger();
21 consoleObservableLogger.addFilteredObj(obs);
22 }
24 > export class ConsoleObservableLogger implements IObservableLogger {
25 private indentation = 0;
26
118
119 private readonly changedObservablesSets = new WeakMap<object, Set<IObservable<any>>>();
121 > formatChanges(changes: Set<IObservable<any>>): ConsoleText | undefined {
122 if (changes.size === 0) {
123 return undefined;
130 );
131 }
133 > handleDerivedDependencyChanged(derived: Derived<any>, observable: IObservable<any>, change: unknown): void {
134 if (!this._isIncluded(derived)) { return; }
135
136 this.changedObservablesSets.get(derived)?.add(observable);
137 }
139 > _handleDerivedRecomputed(derived: Derived<unknown>, info: IChangeInformation): void {
140 if (!this._isIncluded(derived)) { return; }
141
151 changedObservables.clear();
152 }
154 > handleDerivedCleared(derived: Derived<unknown>): void {
155 if (!this._isIncluded(derived)) { return; }
156
160 ]));
161 }
163 > handleFromEventObservableTriggered(observable: FromEventObservable<any, any>, info: IChangeInformation): void {
164 if (!this._isIncluded(observable)) { return; }
165
171 ]));
172 }
174 > handleAutorunCreated(autorun: AutorunObserver): void {
175 if (!this._isIncluded(autorun)) { return; }
176
177 this.changedObservablesSets.set(autorun, new Set());
178 }
180 > handleAutorunDisposed(autorun: AutorunObserver): void {
181 }
183 > handleAutorunDependencyChanged(autorun: AutorunObserver, observable: IObservable<any>, change: unknown): void {
184 if (!this._isIncluded(autorun)) { return; }
185
186 this.changedObservablesSets.get(autorun)!.add(observable);
187 }
189 > handleAutorunStarted(autorun: AutorunObserver): void {
190 const changedObservables = this.changedObservablesSets.get(autorun);
191 if (!changedObservables) { return; }
202 this.indentation++;
203 }
205 > handleAutorunFinished(autorun: AutorunObserver): void {
206 this.indentation--;
207 }
209 > handleBeginTransaction(transaction: TransactionImpl): void {
210 let transactionName = transaction.getDebugName();
211 if (transactionName === undefined) {
221 this.indentation++;
222 }
224 > handleEndTransaction(): void {
225 this.indentation--;
226 }
228 > type ConsoleText = (ConsoleText | undefined)[] |
229 > { text: string; style: string; data?: unknown[] } |
230 > { data: unknown[] };
231 function consoleTextToArgs(text: ConsoleText): unknown[] {
232 const styles = new Array<any>();
294 };
295 }
297 > export function formatValue(value: unknown, availableLen: number): string {
298 switch (typeof value) {
299 case 'number':
325 }
326 }
328 function formatArray(value: unknown[], availableLen: number): string {
329 let result = '[ ';
343 return result;
344 }
346 function formatObject(value: object, availableLen: number): string {
347 if (typeof value.toString === 'function' && value.toString !== Object.prototype.toString) {
371 return result;
372 }
374 function repeat(str: string, count: number): string {
375 let result = '';
379 return result;
380 }
382 function padStr(str: string, length: number): string {
383 while (str.length < length) {
src/vs/platform/agentHost/common/sessionConfigKeys.ts 52 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- sessionConfigKeys.ts
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 > /**
7 > * Well-known keys used in the agent-host configuration value bag.
8 > *
9 > * The Agent Host Protocol's config schema is intentionally generic — agents
10 > * are free to advertise any property names. These constants capture the
11 > * names that the platform itself consumes (e.g. {@link SessionConfigKey.AutoApprove}
12 > * drives tool auto-approval) or that clients interpret via convention
13 > * (e.g. {@link SessionConfigKey.Branch}, {@link SessionConfigKey.Isolation}).
14 > *
15 > * Provider-owned platform properties use these names in an agent's
16 > * `resolveSessionConfig` response. Worktree properties are owned and
17 > * contributed by the host and are not passed to agents.
18 > */
19 > export const enum SessionConfigKey {
20 > /** `'autoApprove'` — tool auto-approval level. */
21 > AutoApprove = 'autoApprove',
22 > /** `'permissions'` — per-tool session allow/deny lists. */
23 > Permissions = 'permissions',
24 > /** `'isolation'` — host-owned `'folder'` or `'worktree'` selection. */
25 > Isolation = 'isolation',
26 > /** `'branch'` — host-owned base branch to work from. */
27 > Branch = 'branch',
28 > /** `'mode'` — agent execution mode (interactive / plan / autopilot). */
29 > Mode = 'mode',
30 > /** `'worktreeBranchPrefix'` — host-owned prefix for the worktree branch name. */
31 > WorktreeBranchPrefix = 'worktreeBranchPrefix',
32 > /** `'worktreeIncludeFiles'` — host-owned glob patterns for files copied into a new worktree. */
33 > WorktreeIncludeFiles = 'worktreeIncludeFiles',
34 > }
35 >
36 > /**
37 > * The set of enum values the unified permission picker *tolerates* for the
38 > * {@link SessionConfigKey.AutoApprove} property when deciding whether a
39 > * session's schema is "well-known" (and therefore handled by the dedicated
40 > * permission picker rather than the generic per-property fallback).
41 > *
42 > * `default` is the required baseline level; `assisted` and `autoApprove` are
43 > * offered elevated levels. `autopilot` is retained for backward compatibility
44 > * with sessions created before it moved onto the mode axis.
45 > */
46 > export const KNOWN_AUTO_APPROVE_VALUES: ReadonlySet<string> = new Set(['default', 'assisted', 'autoApprove', 'autopilot']);
47 >
48 > /**
49 > * The set of enum values understood for the {@link SessionConfigKey.Mode}
50 > * property: the agent execution mode axis.
51 > */
52 > export const KNOWN_MODE_VALUES: ReadonlySet<string> = new Set(['interactive', 'plan', 'autopilot']);
src/vs/platform/agentHost/node/agentHostCommitOperationHandler.ts 52 covered LOC · 11 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostCommitOperationHandler.ts
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 > import { basename } from '../../../base/common/resources.js';
7 > import { CancellationToken } from '../../../base/common/cancellation.js';
8 > import { URI } from '../../../base/common/uri.js';
9 > import { localize } from '../../../nls.js';
10 > import { IAgentService } from '../common/agentService.js';
11 > import { IAgentHostGitHubEndpointService } from './agentHostGitHubEndpointService.js';
12 > import { parseChangesetUri } from '../common/changesetUri.js';
13 > import { type IChangesetOperationHandler } from '../common/agentHostChangesetOperationService.js';
14 > import type { InvokeChangesetOperationParams, InvokeChangesetOperationResult } from '../common/state/protocol/channels-changeset/commands.js';
15 > import { AHP_AUTH_REQUIRED, AHP_SESSION_NOT_FOUND, JsonRpcErrorCodes, ProtocolError } from '../common/state/sessionProtocol.js';
16 > import { readSessionGitState, type ISessionFileDiff, type SessionState } from '../common/state/sessionState.js';
17 > import { ILogService } from '../../log/common/log.js';
18 > import { IAgentHostGitService } from '../common/agentHostGitService.js';
19 > import { CopilotApiError, ICopilotApiService } from './shared/copilotApiService.js';
20 >
21 > const MAX_CHANGE_SUMMARY_PROMPT_CHARS = 20_000;
22 >
23 > export class AgentHostCommitOperationHandler implements IChangesetOperationHandler {
24 >
25 > public static readonly OPERATION_COMMIT = 'commit';
26 >
27 > constructor(
28 > private readonly _getSessionState: (sessionKey: string) => SessionState | undefined, agentHostCommitOperationHandler.ts
29 > private readonly _onCommitted: (sessionKey: string) => Promise<void>,
30 > @IAgentService private readonly _agentService: IAgentService,
31 > @IAgentHostGitHubEndpointService private readonly _gitHubEndpointService: IAgentHostGitHubEndpointService,
32 > @IAgentHostGitService private readonly _gitService: IAgentHostGitService,
33 > @ICopilotApiService private readonly _copilotApiService: ICopilotApiService,
34 > @ILogService private readonly _logService: ILogService,
35 > ) { }
37 > async invoke(params: InvokeChangesetOperationParams, token: CancellationToken): Promise<InvokeChangesetOperationResult> {
38 const abortController = new AbortController();
39 if (token.isCancellationRequested) {
47 }
48 }
50 > private async _invoke(params: InvokeChangesetOperationParams, token: CancellationToken, signal: AbortSignal): Promise<InvokeChangesetOperationResult> {
51 const parsed = parseChangesetUri(params.channel);
52 if (!parsed) {
134 return { message: { markdown: localize('agentHost.changeset.commit.committed', "Committed changes with message: `{0}`", message.split('\n')[0]) } };
135 }
137 > private _buildCommitMessagePrompt(workingDirectory: URI, branchName: string | undefined, diffs: readonly ISessionFileDiff[]): { role: 'system' | 'user'; content: string }[] {
138 const changeSummary = this._summarizeDiffsForPrompt(diffs);
139 return [
158 ];
159 }
161 > private _summarizeDiffsForPrompt(diffs: readonly ISessionFileDiff[]): string {
162 const lines: string[] = [];
163 for (const diff of diffs) {
181 return lines.join('\n');
182 }
184 > private _displayUri(uri: string): string {
185 try {
186 const parsed = URI.parse(uri);
190 }
191 }
193 > private _cleanCommitMessage(raw: string): string {
194 let text = raw.trim().replace(/\r\n/g, '\n');
195 const fenced = /^```(?:text|gitcommit)?\s*([\s\S]*?)\s*```$/i.exec(text);
199 return text;
200 }
202 > private _isAuthFailure(err: unknown): boolean {
203 if (err instanceof CopilotApiError) {
204 return err.status === 401 || err.status === 403;
208 && /\b(auth|authorization|unauthorized|forbidden|token|copilot endpoint discovery|copilot session token mint)\b/i.test(message);
209 }
211 > private _throwIfCancelled(token: CancellationToken): void {
212 if (token.isCancellationRequested) {
213 throw new ProtocolError(JsonRpcErrorCodes.InternalError, localize('agentHost.changeset.commit.cancelled', "Commit operation was cancelled."));
214 }
215 }
src/vs/platform/remote/common/remoteHosts.ts 52 covered LOC · 7 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- remoteHosts.ts
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 > import { Schemas } from '../../../base/common/network.js';
7 > import { URI } from '../../../base/common/uri.js';
8 >
9 > export function getRemoteAuthority(uri: URI): string | undefined {
10 return uri.scheme === Schemas.vscodeRemote ? uri.authority : undefined;
11 }
13 > export function getRemoteName(authority: string): string;
14 > export function getRemoteName(authority: undefined): undefined;
15 > export function getRemoteName(authority: string | undefined): string | undefined;
16 > export function getRemoteName(authority: string | undefined): string | undefined {
17 if (!authority) {
18 return undefined;
25 return authority.substr(0, pos);
26 }
28 > /**
29 > * Returns the suffix part of the authority after the '+' character.
30 > * For remote connections, this is typically the server/tunnel identifier.
31 > * Examples:
32 > * - For tunnels: `tunnel+myTunnel` returns `myTunnel`
33 > * - For SSH: `ssh+myserver` returns `myserver`
34 > * - For localhost: `localhost:8000` returns `undefined`
35 > * @param authority The remote authority string.
36 > * @returns The suffix after the '+' character, or undefined if there is no '+' character.
37 > */
38 > export function getRemoteServerRootPath(authority: string): string | undefined;
39 > export function getRemoteServerRootPath(authority: undefined): undefined;
40 > export function getRemoteServerRootPath(authority: string | undefined): string | undefined;
41 > export function getRemoteServerRootPath(authority: string | undefined): string | undefined {
42 if (!authority) {
43 return undefined;
49 return authority.substring(pos + 1);
50 }
52 > export function parseAuthorityWithPort(authority: string): { host: string; port: number } {
53 const { host, port } = parseAuthority(authority);
54 if (typeof port === 'undefined') {
57 return { host, port };
58 }
60 > export function parseAuthorityWithOptionalPort(authority: string, defaultPort: number): { host: string; port: number } {
61 let { host, port } = parseAuthority(authority);
62 if (typeof port === 'undefined') {
65 return { host, port };
66 }
68 function parseAuthority(authority: string): { host: string; port: number | undefined } {
69 // check for ipv6 with port
88 return { host: authority, port: undefined };
89 }
91 > const loopbackHosts = new Set([
92 > 'localhost',
93 > '127.0.0.1',
94 > '::1',
95 > '[::1]',
96 > '0000:0000:0000:0000:0000:0000:0000:0001',
97 > '[0000:0000:0000:0000:0000:0000:0000:0001]'
98 > ]);
99 >
100 > /**
101 > * Returns whether the given host (as found in a direct `<host>:<port>` remote
102 > * authority) refers to the local loopback interface. The check is intentionally
103 > * strict: only `localhost` and the IPv4/IPv6 loopback literals are considered
104 > * local. Any other host (a routable IP address or a hostname) is treated as a
105 > * connection that leaves the local machine.
106 > */
107 > export function isLoopbackHost(host: string): boolean {
108 return loopbackHosts.has(host.toLowerCase());
109 }
src/vs/base/common/observableInternal/observables/observableFromEvent.ts 51 covered LOC · 9 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- observableFromEvent.ts
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 > import { IObservable, ITransaction } from '../base.js';
7 > import { subtransaction } from '../transaction.js';
8 > import { EqualityComparer, Event, IDisposable, strictEquals } from '../commonFacade/deps.js';
9 > import { DebugOwner, DebugNameData, IDebugNameData } from '../debugName.js';
10 > import { getLogger } from '../logging/logging.js';
11 > import { BaseObservable } from './baseObservable.js';
12 > import { DebugLocation } from '../debugLocation.js';
13 >
14 >
15 > export function observableFromEvent<T, TArgs = unknown>(
16 > owner: DebugOwner,
17 > event: Event<TArgs>,
18 > getValue: (args: TArgs | undefined) => T,
19 > debugLocation?: DebugLocation,
20 > ): IObservable<T>;
21 > export function observableFromEvent<T, TArgs = unknown>(
22 > event: Event<TArgs>,
23 > getValue: (args: TArgs | undefined) => T,
24 > ): IObservable<T>;
25 > export function observableFromEvent(...args:
26 [owner: DebugOwner, event: Event<any>, getValue: (args: any | undefined) => any, debugLocation?: DebugLocation] |
27 [event: Event<any>, getValue: (args: any | undefined) => any]
45 );
46 }
48 > export function observableFromEventOpts<T, TArgs = unknown>(
49 options: IDebugNameData & {
50 equalsFn?: EqualityComparer<T>;
64 );
65 }
67 > export class FromEventObservable<TArgs, T> extends BaseObservable<T> {
68 > public static globalTransaction: ITransaction | undefined;
69 >
70 > private _value: T | undefined;
71 > private _hasValue = false;
72 > private _subscription: IDisposable | undefined;
73 >
74 > constructor(
75 private readonly _debugNameData: DebugNameData,
76 private readonly event: Event<TArgs>,
131 }
132 };
134 > protected override onLastObserverRemoved(): void {
135 this._subscription!.dispose();
136 this._subscription = undefined;
138 this._value = undefined;
139 }
141 > public get(): T {
142 if (this._subscription) {
143 if (!this._hasValue) {
151 }
152 }
154 > public debugSetValue(value: unknown): void {
155 // eslint-disable-next-line local/code-no-any-casts
156 this._value = value as any;
157 }
159 > public debugGetState() {
160 return { value: this._value, hasValue: this._hasValue };
161 }
163 >
164 > export namespace observableFromEvent {
165 > export const Observer = FromEventObservable;
166 >
167 > export function batchEventsGlobally(tx: ITransaction, fn: () => void): void {
168 let didSet = false;
169 if (FromEventObservable.globalTransaction === undefined) {
src/vs/platform/agentHost/node/localCommands/bangLocalCommand.ts 51 covered LOC · 7 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- bangLocalCommand.ts
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 > import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js';
7 > import { URI } from '../../../../base/common/uri.js';
8 > import { generateUuid } from '../../../../base/common/uuid.js';
9 > import { localize } from '../../../../nls.js';
10 > import type { CreateTerminalParams } from '../../common/state/protocol/commands.js';
11 > import { TerminalClaimKind, type TerminalSessionClaim } from '../../common/state/protocol/state.js';
12 > import { ActionType } from '../../common/state/sessionActions.js';
13 > import { isAhpChatChannel, parseRequiredSessionUriFromChatUri, ToolCallConfirmationReason, ToolResultContentType, type ToolResultContent, type URI as ProtocolURI } from '../../common/state/sessionState.js';
14 > import { parseBangCommand } from '../agentHostBangCommand.js';
15 > import { DEFAULT_SHELL_COMMAND_TIMEOUT_MS, executeShellCommand, shellTypeForExecutable, type IShellCommandResult } from '../shared/shellCommandExecution.js';
16 > import { ILocalChatCommand, ILocalChatCommandContext, ILocalChatCommandHandling, ILocalChatCommandRequest, LocalChatCommandRegistry } from './localChatCommand.js';
17 >
18 > /**
19 > * The generic `!command` command: runs the message as a terminal command via
20 > * the {@link IAgentHostTerminalManager} shell integration and surfaces it as a
21 > * terminal tool call in the transcript, instead of forwarding it to the agent
22 > * SDK. Runs immediately (the user typed it explicitly — no confirmation).
23 > */
24 > export class BangLocalCommand extends Disposable implements ILocalChatCommand {
25 >
26 > readonly name = 'bang';
27 > readonly recordsLocalTurn = true;
28 >
29 > /** Terminals kept alive for transcript output; disposed with this command. */
30 > private readonly _terminals = new Set<string>();
31 >
32 > constructor(private readonly _context: ILocalChatCommandContext) {
33 > super(); bangLocalCommand.ts
34 > this._register(toDisposable(() => {
35 > for (const terminalUri of this._terminals) {
36 this._context.terminalManager.disposeTerminal(terminalUri);
37 }
38 > this._terminals.clear(); bangLocalCommand.ts
39 > }));
40 > }
42 > tryHandle(request: ILocalChatCommandRequest): ILocalChatCommandHandling | undefined {
43 const command = parseBangCommand(request.text);
44 if (command === undefined) {
49 return { run: () => this._run(request.turnChannel, request.turnId, command), suggestedTitle: command };
50 }
52 > private async _run(turnChannel: ProtocolURI, turnId: string, command: string): Promise<void> {
53 const ctx = this._context;
54 const sessionChannel = isAhpChatChannel(turnChannel) ? parseRequiredSessionUriFromChatUri(turnChannel) : turnChannel;
133 }
134 }
136 > /**
137 > * Maps a shell command result to a success flag and past-tense summary for
138 > * the completed tool call.
139 > */
140 > private _summarizeResult(result: IShellCommandResult): { success: boolean; pastTenseMessage: string } {
141 switch (result.status) {
142 case 'completed': {
156 }
157 }
159 >
160 > LocalChatCommandRegistry.register(BangLocalCommand);
src/vs/platform/agentHost/node/agentHostRenameCommand.ts 50 covered LOC · 3 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostRenameCommand.ts
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 > import { CancellationToken } from '../../../base/common/cancellation.js';
7 > import { localize } from '../../../nls.js';
8 > import type { URI } from '../common/state/protocol/common/state.js';
9 > import { CompletionItem, CompletionItemKind, CompletionsParams } from '../common/state/protocol/commands.js';
10 > import { MessageAttachmentKind } from '../common/state/protocol/state.js';
11 > import { toCommandCompletionAttachmentMeta } from '../common/meta/agentCompletionAttachmentMeta.js';
12 > import { CompletionTriggerCharacter, IAgentHostCompletionItemProvider } from './agentHostCompletions.js';
13 > import { extractLeadingSlashToken, matchesSlashCompletion } from './agentHostSlashCompletion.js';
14 >
15 > /** The generic, agent-agnostic `/rename` slash command name. */
16 > export const RENAME_SLASH_COMMAND = 'rename';
17 >
18 > /**
19 > * Parses a leading `/rename [title]` command at the very start of `prompt`.
20 > *
21 > * Mirrors `parseLeadingSlashCommand` (the Copilot CLI slash parser): the
22 > * command must be `/rename`, followed either by end-of-input or at least one
23 > * whitespace character. `/renamed`, `/rename-foo`, or a leading-space
24 > * `/rename` all return `undefined`. Match is case-sensitive.
25 > *
26 > * Returns the trimmed new title (possibly an empty string when no title is
27 > * supplied) when the prompt is a rename command, or `undefined` when it is not.
28 > * Callers MUST distinguish "not a rename command" (`undefined`) from "rename
29 > * with empty title" (`''`).
30 > */
31 > export function parseRenameCommand(prompt: string): string | undefined {
32 const match = /^\/rename(?:$|\s+([\s\S]*))/.exec(prompt);
33 if (!match) {
36 return (match[1] ?? '').trim();
37 }
39 > /**
40 > * Generic completion provider that contributes the `/rename` slash command
41 > * for every agent-host session type. Unlike agent-specific slash commands
42 > * (e.g. Copilot's `/compact`), `/rename` is not forwarded to any agent SDK;
43 > * it is intercepted in the agent-host send path and redirected to a
44 > * `SessionTitleChanged` action (see the rename handling in `AgentSideEffects`).
45 > *
46 > * The completion is only offered for sessions that already have history —
47 > * renaming a session before the first turn has no meaningful target.
48 > */
49 > export class AgentHostRenameCompletionProvider implements IAgentHostCompletionItemProvider {
50 > readonly kinds: ReadonlySet<CompletionItemKind> = new Set([CompletionItemKind.UserMessage]);
51 > readonly triggerCharacters = [CompletionTriggerCharacter.Slash] as const;
52 >
53 > constructor(private readonly _hasHistory: (session: URI) => boolean) { }
54 >
55 > async provideCompletionItems(params: CompletionsParams, _token: CancellationToken): Promise<readonly CompletionItem[]> {
56 const leading = extractLeadingSlashToken(params.text, params.offset);
57 if (!leading) {
src/vs/base/common/observableInternal/observables/lazyObservableValue.ts 49 covered LOC · 11 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- lazyObservableValue.ts
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 > import { EqualityComparer } from '../commonFacade/deps.js';
7 > import { IObserver, ISettableObservable, ITransaction } from '../base.js';
8 > import { TransactionImpl } from '../transaction.js';
9 > import { DebugNameData } from '../debugName.js';
10 > import { getLogger } from '../logging/logging.js';
11 > import { BaseObservable } from './baseObservable.js';
12 > import { DebugLocation } from '../debugLocation.js';
13 >
14 > /**
15 > * Holds off updating observers until the value is actually read.
16 > */
17 > export class LazyObservableValue<T, TChange = void>
18 > extends BaseObservable<T, TChange>
19 > implements ISettableObservable<T, TChange> {
20 > protected _value: T;
21 > private _isUpToDate = true;
22 > private readonly _deltas: TChange[] = [];
23 >
24 > get debugName() {
25 > return this._debugNameData.getDebugName(this) ?? 'LazyObservableValue';
26 > }
27 >
28 > constructor(
29 private readonly _debugNameData: DebugNameData,
30 initialValue: T,
35 this._value = initialValue;
36 }
38 > public override get(): T {
39 this._update();
40 return this._value;
41 }
43 > private _update(): void {
44 if (this._isUpToDate) {
45 return;
62 }
63 }
65 > private _updateCounter = 0;
66 >
67 > private _beginUpdate(): void {
68 this._updateCounter++;
69 if (this._updateCounter === 1) {
73 }
74 }
76 > private _endUpdate(): void {
77 this._updateCounter--;
78 if (this._updateCounter === 0) {
86 }
87 }
89 > public override addObserver(observer: IObserver): void {
90 const shouldCallBeginUpdate = !this._observers.has(observer) && this._updateCounter > 0;
91 super.addObserver(observer);
95 }
96 }
98 > public override removeObserver(observer: IObserver): void {
99 const shouldCallEndUpdate = this._observers.has(observer) && this._updateCounter > 0;
100 super.removeObserver(observer);
105 }
106 }
108 > public set(value: T, tx: ITransaction | undefined, change: TChange): void {
109 if (change === undefined && this._equalityComparator(this._value, value)) {
110 return;
src/vs/platform/agentHost/node/agentHostPullRequestOperationProvider.ts 49 covered LOC · 7 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostPullRequestOperationProvider.ts
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 > import { Disposable, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';
7 > import { localize } from '../../../nls.js';
8 > import { IInstantiationService } from '../../instantiation/common/instantiation.js';
9 > import type { IChangesetOperationContribution, IChangesetOperationContext, IChangesetOperationRegistry } from '../common/agentHostChangesetOperationService.js';
10 > import { IAgentHostGitStateService } from '../common/agentHostGitStateService.js';
11 > import { ChangesetOperationScope, ChangesetOperationStatus, SessionLifecycle, type ChangesetOperation } from '../common/state/sessionState.js';
12 > import { AgentHostPullRequestOperationHandler, type PullRequestCreatedEvent } from './agentHostPullRequestOperationHandler.js';
13 > import { AgentHostStateManager, IAgentHostStateManager } from './agentHostStateManager.js';
14 >
15 > export class AgentHostPullRequestOperationContribution extends Disposable implements IChangesetOperationContribution {
16 >
17 > private _registry: IChangesetOperationRegistry | undefined;
18 >
19 > constructor(
20 > @IAgentHostStateManager private readonly _stateManager: AgentHostStateManager, agentHostPullRequestOperationProvider.ts
21 > @IInstantiationService private readonly _instantiationService: IInstantiationService,
22 > @IAgentHostGitStateService private readonly _gitStateService: IAgentHostGitStateService
23 > ) {
24 > super();
25 > }
27 > registerHandlers(registry: IChangesetOperationRegistry): IDisposable {
28 > this._registry = registry; agentHostPullRequestOperationProvider.ts
29 > const store = new DisposableStore();
30 > const getSessionState = (sessionKey: string) => this._stateManager.getSessionState(sessionKey);
31 > const onCreated = (event: PullRequestCreatedEvent) => this._onPullRequestCreated(event);
32 > const createPrHandler = this._instantiationService.createInstance(AgentHostPullRequestOperationHandler, false, undefined, getSessionState, onCreated);
33 > const createDraftPrHandler = this._instantiationService.createInstance(AgentHostPullRequestOperationHandler, true, undefined, getSessionState, onCreated);
34 > const createAutoMergePrHandler = this._instantiationService.createInstance(AgentHostPullRequestOperationHandler, false, 'MERGE', getSessionState, onCreated);
35 > const createAutoSquashPrHandler = this._instantiationService.createInstance(AgentHostPullRequestOperationHandler, false, 'SQUASH', getSessionState, onCreated);
36 > const createAutoRebasePrHandler = this._instantiationService.createInstance(AgentHostPullRequestOperationHandler, false, 'REBASE', getSessionState, onCreated);
37 > store.add(registry.registerChangesetOperationHandler(AgentHostPullRequestOperationHandler.OPERATION_CREATE_PR, createPrHandler));
38 > store.add(registry.registerChangesetOperationHandler(AgentHostPullRequestOperationHandler.OPERATION_CREATE_DRAFT_PR, createDraftPrHandler));
39 > store.add(registry.registerChangesetOperationHandler(AgentHostPullRequestOperationHandler.OPERATION_CREATE_PR_AUTO_MERGE, createAutoMergePrHandler));
40 > store.add(registry.registerChangesetOperationHandler(AgentHostPullRequestOperationHandler.OPERATION_CREATE_PR_AUTO_SQUASH, createAutoSquashPrHandler));
41 > store.add(registry.registerChangesetOperationHandler(AgentHostPullRequestOperationHandler.OPERATION_CREATE_PR_AUTO_REBASE, createAutoRebasePrHandler));
42 > store.add({ dispose: () => { this._registry = undefined; } });
43 > return store;
44 > }
46 > getOperations({ sessionKey, gitState, gitHubState }: IChangesetOperationContext): ChangesetOperation[] | undefined {
47 // New Session
48 const state = this._stateManager.getSessionState(sessionKey);
104 }] satisfies ChangesetOperation[];
105 }
107 > private _onPullRequestCreated(event: PullRequestCreatedEvent): void {
108 const sessionKey = event.sessionKey;
109
src/vs/base/node/windowsVersion.ts 48 covered LOC · 6 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- windowsVersion.ts
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 > import * as os from 'os';
7 > import { isWindows } from '../common/platform.js';
8 >
9 > let versionInfo: { release: string; buildNumber: number } | undefined;
10 >
11 > /**
12 > * Initializes the Windows version cache by reading from the registry.
13 > *
14 > * On Windows 8.1+, the `os.release()` function may return incorrect version numbers
15 > * due to the deprecated GetVersionEx API returning compatibility-shimmed values
16 > * when the application doesn't have a proper manifest. Reading from the registry
17 > * gives us the real version.
18 > *
19 > * See: https://github.com/microsoft/vscode/issues/197444
20 > */
21 export async function initWindowsVersionInfo() {
22 if (versionInfo) {
57 }
58 }
60 > /**
61 > * Gets Windows version information from the registry.
62 > * @returns The Windows version in Major.Minor.Build format (e.g., "10.0.19041")
63 > */
64 export async function getWindowsRelease(): Promise<string> {
65 if (!versionInfo) {
68 return versionInfo!.release;
69 }
71 > /**
72 > * Gets the Windows build number from the registry.
73 > * @returns The Windows build number (e.g., 19041 for Windows 10 2004)
74 > */
75 export async function getWindowsBuildNumberAsync(): Promise<number> {
76 if (!versionInfo) {
79 return versionInfo!.buildNumber;
80 }
82 > /**
83 > * Synchronous version of getWindowsBuildNumberAsync().
84 > * @returns The Windows build number (e.g., 19041 for Windows 10 2004)
85 > */
86 > export function getWindowsBuildNumberSync(): number {
87 if (versionInfo) {
88 return versionInfo.buildNumber;
91 }
92 }
94 > /**
95 > * Gets the cached Windows release string synchronously.
96 > * Falls back to os.release() if the cache hasn't been initialized yet.
97 > * @returns The Windows version in Major.Minor.Build format (e.g., "10.0.19041")
98 > */
99 > export function getWindowsReleaseSync(): string {
100 return versionInfo?.release ?? os.release();
101 }
103 > /**
104 > * Parses the Windows build number from os.release().
105 > * This is used as a fallback when registry reading is not available.
106 > */
107 function getWindowsBuildNumberFromOsRelease(): number {
108 const osVersion = (/(\d+)\.(\d+)\.(\d+)/g).exec(os.release());
src/vs/base/test/common/virtualScheduling/timeApi.ts 48 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- timeApi.ts
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 > export interface TimeoutId { readonly _timeoutIdBrand: void }
7 > export interface IntervalId { readonly _intervalIdBrand: void }
8 > export interface ImmediateId { readonly _immediateIdBrand: void }
9 > export type AnimationFrameId = number & { readonly _animationFrameIdBrand: void };
10 >
11 > /**
12 > * The subset of host time APIs the processor and embeddings need.
13 > *
14 > * Used both for the real host API (captured via {@link captureGlobalTimeApi})
15 > * and for the virtual replacement that runs through a {@link VirtualClock}.
16 > *
17 > * Keeping this as a plain interface means the processor never reaches into
18 > * `globalThis` directly: the boundary between "real time" and "virtual time"
19 > * is exactly which `TimeApi` instance is in use.
20 > */
21 > export interface TimeApi {
22 > setTimeout(handler: () => void, timeout?: number): TimeoutId;
23 > clearTimeout(id: TimeoutId): void;
24 > setInterval(handler: () => void, interval: number): IntervalId;
25 > clearInterval(id: IntervalId): void;
26 > setImmediate?: ((handler: () => void) => ImmediateId);
27 > clearImmediate?: ((id: ImmediateId) => void);
28 > requestAnimationFrame?: ((cb: (time: number) => void) => AnimationFrameId);
29 > cancelAnimationFrame?: ((id: AnimationFrameId) => void);
30 > Date: DateConstructor;
31 > }
32 >
33 > export function captureGlobalTimeApi(): TimeApi {
34 > return {
35 > setTimeout: globalThis.setTimeout.bind(globalThis) as unknown as TimeApi['setTimeout'],
36 > clearTimeout: globalThis.clearTimeout.bind(globalThis) as unknown as TimeApi['clearTimeout'],
37 > setInterval: globalThis.setInterval.bind(globalThis) as unknown as TimeApi['setInterval'],
38 > clearInterval: globalThis.clearInterval.bind(globalThis) as unknown as TimeApi['clearInterval'],
39 > setImmediate: globalThis.setImmediate?.bind(globalThis) as unknown as TimeApi['setImmediate'],
40 > clearImmediate: globalThis.clearImmediate?.bind(globalThis) as unknown as TimeApi['clearImmediate'],
41 > requestAnimationFrame: globalThis.requestAnimationFrame?.bind(globalThis) as unknown as TimeApi['requestAnimationFrame'],
42 > cancelAnimationFrame: globalThis.cancelAnimationFrame?.bind(globalThis) as unknown as TimeApi['cancelAnimationFrame'],
43 > Date: globalThis.Date,
44 > };
45 > }
46 >
47 > /** A snapshot of the real host time API at module-load time. */
48 > export const realTimeApi: TimeApi = captureGlobalTimeApi();
src/vs/platform/agentHost/common/agentHostGitStateService.ts 48 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostGitStateService.ts
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 > import { URI } from '../../../base/common/uri.js';
7 > import { Event } from '../../../base/common/event.js';
8 > import { createDecorator } from '../../instantiation/common/instantiation.js';
9 > import { ISessionGitHubState } from './state/sessionState.js';
10 >
11 > export const META_GIT_STATE = 'agentHost.git';
12 > export const META_GITHUB_STATE = 'agentHost.github';
13 >
14 > export const GIT_DB_METADATA_KEYS: Record<string, true> = {
15 > [META_GIT_STATE]: true,
16 > [META_GITHUB_STATE]: true,
17 > };
18 >
19 > export const IAgentHostGitStateService = createDecorator<IAgentHostGitStateService>('agentHostGitStateService');
20 >
21 > export interface IAgentHostGitStateService {
22 > readonly _serviceBrand: undefined;
23 >
24 > /**
25 > * Fires when the git state for a session is refreshed.
26 > */
27 > readonly onDidRefreshSessionGitState: Event<string>;
28 >
29 > /**
30 > * Refreshes the git state for a given session.
31 > * @param sessionKey The key of the session for which to refresh the git state.
32 > * @param workingDirectory Optional working directory override; when omitted, the session summary's working directory is used.
33 > */
34 > refreshSessionGitState(sessionKey: string, workingDirectory?: URI): Promise<void>;
35 >
36 > /**
37 > * Sets the GitHub state for a given session.
38 > * @param sessionKey The key of the session for which to set the GitHub state.
39 > * @param state The GitHub state to set.
40 > */
41 > setSessionGitHubState(sessionKey: string, state: ISessionGitHubState): Promise<void>;
42 >
43 > /**
44 > * Find a GitHub pull request for the given session and save it to the session state.
45 > * @param sessionKey The key of the session for which to check the GitHub pull request.
46 > */
47 > attachSessionGitHubPullRequest(sessionKey: string): Promise<void>;
48 > }
src/vs/platform/terminal/common/environmentVariableCollection.ts 48 covered LOC · 11 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- environmentVariableCollection.ts
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 > import { IProcessEnvironment, isWindows } from '../../../base/common/platform.js';
7 > import { EnvironmentVariableMutatorType, EnvironmentVariableScope, IEnvironmentVariableCollection, IExtensionOwnedEnvironmentDescriptionMutator, IExtensionOwnedEnvironmentVariableMutator, IMergedEnvironmentVariableCollection, IMergedEnvironmentVariableCollectionDiff } from './environmentVariable.js';
8 >
9 > type VariableResolver = (str: string) => Promise<string>;
10 >
11 > const mutatorTypeToLabelMap: Map<EnvironmentVariableMutatorType, string> = new Map([
12 > [EnvironmentVariableMutatorType.Append, 'APPEND'],
13 > [EnvironmentVariableMutatorType.Prepend, 'PREPEND'],
14 > [EnvironmentVariableMutatorType.Replace, 'REPLACE']
15 > ]);
16 > const PYTHON_ACTIVATION_VARS_PATTERN = /^VSCODE_PYTHON_(PWSH|ZSH|BASH|FISH)_ACTIVATE/;
17 > const PYTHON_ENV_EXTENSION_ID = 'ms-python.vscode-python-envs';
18 >
19 > export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVariableCollection {
20 > private readonly map: Map<string, IExtensionOwnedEnvironmentVariableMutator[]> = new Map();
21 > private readonly descriptionMap: Map<string, IExtensionOwnedEnvironmentDescriptionMutator[]> = new Map();
22 >
23 > constructor(
24 readonly collections: ReadonlyMap<string, IEnvironmentVariableCollection>,
25 ) {
68 });
69 }
71 > async applyToProcessEnvironment(env: IProcessEnvironment, scope: EnvironmentVariableScope | undefined, variableResolver?: VariableResolver): Promise<void> {
72 let lowerToActualVariableNames: { [lowerKey: string]: string | undefined } | undefined;
73 if (isWindows) {
106 }
107 }
109 > private _encodeColons(value: string): string {
110 return value.replaceAll(':', '\\x3a');
111 }
113 > private blockPythonActivationVar(variable: string, extensionIdentifier: string): boolean {
114 // Only Python env extension can modify Python activate env var.
115 if (PYTHON_ACTIVATION_VARS_PATTERN.test(variable) && PYTHON_ENV_EXTENSION_ID !== extensionIdentifier) {
118 return false;
119 }
121 > diff(other: IMergedEnvironmentVariableCollection, scope: EnvironmentVariableScope | undefined): IMergedEnvironmentVariableCollectionDiff | undefined {
122 const added: Map<string, IExtensionOwnedEnvironmentVariableMutator[]> = new Map();
123 const changed: Map<string, IExtensionOwnedEnvironmentVariableMutator[]> = new Map();
157 return { added, changed, removed };
158 }
160 > getVariableMap(scope: EnvironmentVariableScope | undefined): Map<string, IExtensionOwnedEnvironmentVariableMutator[]> {
161 const result = new Map<string, IExtensionOwnedEnvironmentVariableMutator[]>();
162 for (const mutators of this.map.values()) {
169 return result;
170 }
172 > getDescriptionMap(scope: EnvironmentVariableScope | undefined): Map<string, string | undefined> {
173 const result = new Map<string, string | undefined>();
174 for (const mutators of this.descriptionMap.values()) {
180 return result;
181 }
183 > private populateDescriptionMap(collection: IEnvironmentVariableCollection, extensionIdentifier: string): void {
184 if (!collection.descriptionMap) {
185 return;
209
210 }
212 >
213 > /**
214 > * Returns whether a mutator matches with the scope provided.
215 > * @param mutator Mutator to filter
216 > * @param scope Scope to be used for querying
217 > * @param strictFilter If true, mutators with global scope is not returned when querying for workspace scope.
218 > * i.e whether mutator scope should always exactly match with query scope.
219 > */
220 function filterScope(
221 mutator: IExtensionOwnedEnvironmentVariableMutator | IExtensionOwnedEnvironmentDescriptionMutator,
236 return false;
237 }
239 function getMissingMutatorsFromArray(
240 current: IExtensionOwnedEnvironmentVariableMutator[],
260 return result.length === 0 ? undefined : result;
261 }
263 function getChangedMutatorsFromArray(
264 current: IExtensionOwnedEnvironmentVariableMutator[],
src/vs/base/common/iconLabels.ts 47 covered LOC · 6 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- iconLabels.ts
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 > import { IMatch, matchesFuzzy } from './filters.js';
7 > import { ltrim } from './strings.js';
8 > import { ThemeIcon } from './themables.js';
9 >
10 > const iconStartMarker = '$(';
11 >
12 > const iconsRegex = new RegExp(`\\$\\(${ThemeIcon.iconNameExpression}(?:${ThemeIcon.iconModifierExpression})?\\)`, 'g'); // no capturing groups
13 >
14 > const escapeIconsRegex = new RegExp(`(\\\\)?${iconsRegex.source}`, 'g');
15 > export function escapeIcons(text: string): string {
16 return text.replace(escapeIconsRegex, (match, escaped) => escaped ? match : `\\${match}`);
17 }
19 > const markdownEscapedIconsRegex = new RegExp(`\\\\${iconsRegex.source}`, 'g');
20 > export function markdownEscapeEscapedIcons(text: string): string {
21 // Need to add an extra \ for escaping in markdown
22 return text.replace(markdownEscapedIconsRegex, match => `\\${match}`);
23 }
25 > const stripIconsRegex = new RegExp(`(\\s)?(\\\\)?${iconsRegex.source}(\\s)?`, 'g');
26 >
27 > /**
28 > * Takes a label with icons (`$(iconId)xyz`) and strips the icons out (`xyz`)
29 > */
30 > export function stripIcons(text: string): string {
31 if (text.indexOf(iconStartMarker) === -1) {
32 return text;
35 return text.replace(stripIconsRegex, (match, preWhitespace, escaped, postWhitespace) => escaped ? match : preWhitespace || postWhitespace || '');
36 }
38 >
39 > /**
40 > * Takes a label with icons (`$(iconId)xyz`), removes the icon syntax adds whitespace so that screen readers can read the text better.
41 > */
42 > export function getCodiconAriaLabel(text: string | undefined) {
43 if (!text) {
44 return '';
47 return text.replace(/\$\((.*?)\)/g, (_match, codiconName) => ` ${codiconName} `).trim();
48 }
50 >
51 > export interface IParsedLabelWithIcons {
52 > readonly text: string;
53 > readonly iconOffsets?: readonly number[];
54 > }
55 >
56 > const _parseIconsRegex = new RegExp(`\\$\\(${ThemeIcon.iconNameCharacter}+\\)`, 'g');
57 >
58 > /**
59 > * Takes a label with icons (`abc $(iconId)xyz`) and returns the text (`abc xyz`) and the offsets of the icons (`[3]`)
60 > */
61 > export function parseLabelWithIcons(input: string): IParsedLabelWithIcons {
62
63 _parseIconsRegex.lastIndex = 0;
86 return { text, iconOffsets };
87 }
89 >
90 > export function matchesFuzzyIconAware(query: string, target: IParsedLabelWithIcons, enableSeparateSubstringMatching = false): IMatch[] | null {
91 const { text, iconOffsets } = target;
92
src/vs/base/test/common/virtualScheduling/traceLogger.ts 47 covered LOC · 3 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- traceLogger.ts
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 > import { LogEntryLike } from '../executionGraph.js';
7 > import { Trace, TraceContext } from './trace.js';
8 >
9 > /**
10 > * A minimal logger for tests that captures the active {@link Trace} at log
11 > * time so messages can later be woven into a swimlane diagram next to the
12 > * timer events that produced them.
13 > */
14 > export interface ITraceLogger {
15 > log(message: string): void;
16 > warn(message: string): void;
17 > error(message: string): void;
18 > /**
19 > * Run `fn` synchronously and log it as a marker in the trace. The
20 > * marker text is `fn.toString()` so call sites read naturally as e.g.
21 > * `logger.logRun(() => model.trigger())`. Returns whatever `fn`
22 > * returns.
23 > */
24 > logRun<T>(fn: () => T): T;
25 > }
26 >
27 > /**
28 > * One log entry produced by an {@link ITraceLogger}. Extends
29 > * {@link LogEntryLike} (consumed by `buildHistoryFromTasks`) with a level so
30 > * renderers can differentiate `log` / `warn` / `error`.
31 > */
32 > export interface ITraceLogEntry extends LogEntryLike {
33 > readonly trace: Trace;
34 > readonly level: 'log' | 'warn' | 'error';
35 > }
36 >
37 > /**
38 > * Build an {@link ITraceLogger} that pushes every call into `buffer`,
39 > * tagging each entry with the trace that's current at call time.
40 > *
41 > * Pass the same `buffer` to `buildHistoryFromTasks(history, startTime,
42 > * buffer)` to interleave log lines with the timer swimlane.
43 > */
44 > export function createTraceLogger(buffer: ITraceLogEntry[]): ITraceLogger {
45 const make = (level: 'log' | 'warn' | 'error') => (message: string) => {
46 buffer.push({
61 };
62 }
64 > /** Best-effort one-line description of `fn` for trace log markers. */
65 function _describeFn(fn: () => unknown): string {
66 const src = fn.toString();
72 return _collapseWhitespace(src);
73 }
75 function _collapseWhitespace(s: string): string {
76 return s.replace(/\s+/g, ' ').trim();
src/vs/platform/agentHost/node/agentHostMicrosoftTelemetry.ts 47 covered LOC · 7 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostMicrosoftTelemetry.ts
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 > import { Disposable, MutableDisposable, toDisposable } from '../../../base/common/lifecycle.js';
7 > import type { IRequestService } from '../../request/common/request.js';
8 > import type { ICommonProperties } from '../../telemetry/common/telemetry.js';
9 > import { OneDataSystemAppender } from '../../telemetry/node/1dsAppender.js';
10 > import type { IAgentHostInternalTelemetryContext, IAgentHostInternalTelemetrySink, TelemetryMeasurements, TelemetryProps } from './agentHostRestrictedTelemetry.js';
11 >
12 > // Public instrumentation key shipped as internalLargeStorageAriaKey in extensions/copilot/package.json.
13 > const INTERNAL_LARGE_STORAGE_ARIA_KEY = 'ec712b3202c5462fb6877acae7f1f9d7-c19ad55e-3e3c-4f99-984b-827f6d95bd9e-6917';
14 > const INTERNAL_EVENT_PREFIX = 'GitHub.copilot-chat';
15 > const INTERNAL_EXTENSION_ID = 'GitHub.copilot-chat';
16 >
17 > interface IInternalTelemetryAppender {
18 > log(eventName: string, data?: object): void;
19 > flush(): Promise<void>;
20 > }
21 >
22 > interface IAgentHostInternalTelemetrySenderOptions {
23 > readonly requestService?: IRequestService;
24 > readonly commonProperties?: ICommonProperties;
25 > readonly extensionVersion?: string;
26 > readonly createAppender?: (requestService: IRequestService | undefined, commonProperties: ICommonProperties | undefined, eventPrefix: string) => IInternalTelemetryAppender;
27 > }
28 >
29 function getInternalCommonProperties(commonProperties: ICommonProperties | undefined, extensionVersion: string | undefined): ICommonProperties | undefined {
30 if (!commonProperties) {
41 return result;
42 }
44 > class InternalTelemetryAppender extends Disposable {
45 >
46 > constructor(readonly appender: IInternalTelemetryAppender) {
47 super();
48 this._register(toDisposable(() => { void appender.flush(); }));
49 }
51 >
52 > export class AgentHostInternalTelemetrySender extends Disposable implements IAgentHostInternalTelemetrySink {
53 >
54 > private readonly _appender = this._register(new MutableDisposable<InternalTelemetryAppender>());
55 > private _context: IAgentHostInternalTelemetryContext | undefined;
56 >
57 > constructor(private readonly _options: IAgentHostInternalTelemetrySenderOptions = {}) {
58 super();
59 }
61 > setContext(context: IAgentHostInternalTelemetryContext | undefined): void {
62 this._context = context?.isInternal ? context : undefined;
63 if (!this._context) {
68 this._appender.value ??= new InternalTelemetryAppender(createAppender(this._options.requestService, getInternalCommonProperties(this._options.commonProperties, this._options.extensionVersion), INTERNAL_EVENT_PREFIX));
69 }
71 > send(eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void {
72 if (!this._context) {
73 return;
75 this.sendForContext(this._context, eventName, properties, measurements);
76 }
78 > sendForContext(context: IAgentHostInternalTelemetryContext, eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void {
79 if (!context.isInternal) {
80 return;
src/vs/platform/agentHost/node/agentHostSkillCompletionProvider.ts 47 covered LOC · 7 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostSkillCompletionProvider.ts
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 > import { CancellationToken } from '../../../base/common/cancellation.js';
7 > import { Disposable } from '../../../base/common/lifecycle.js';
8 > import { URI } from '../../../base/common/uri.js';
9 > import { SYNCED_CUSTOMIZATION_SCHEME } from '../common/agentHostFileSystemService.js';
10 > import type { IAgent } from '../common/agentService.js';
11 > import { CompletionItem, CompletionItemKind, CompletionsParams } from '../common/state/protocol/commands.js';
12 > import { MessageAttachmentKind } from '../common/state/protocol/state.js';
13 > import { toSkillCompletionAttachmentMeta } from '../common/meta/agentCompletionAttachmentMeta.js';
14 > import { CustomizationType, DirectoryCustomization, PluginCustomization, SkillCustomization } from '../common/state/sessionState.js';
15 > import { CompletionTriggerCharacter, IAgentHostCompletionItemProvider } from './agentHostCompletions.js';
16 > import { extractWhitespaceDelimitedSlashToken, matchesSlashCompletion } from './agentHostSlashCompletion.js';
17 >
18 >
19 > /**
20 > * Generic completion provider that contributes slash completions for skills
21 > * exposed through an agent's global and session-effective customizations.
22 > */
23 > export class AgentHostSkillCompletionProvider extends Disposable implements IAgentHostCompletionItemProvider {
24 >
25 > readonly kinds: ReadonlySet<CompletionItemKind> = new Set([CompletionItemKind.UserMessage]);
26 > readonly triggerCharacters = [CompletionTriggerCharacter.Slash] as const;
27 >
28 > constructor(
29 > private readonly _getAgent: (session: URI | string) => IAgent | undefined, agentHostSkillCompletionProvider.ts
30 > ) {
31 > super();
32 > }
34 > async provideCompletionItems(params: CompletionsParams, token: CancellationToken): Promise<readonly CompletionItem[]> {
35 const leading = extractWhitespaceDelimitedSlashToken(params.text, params.offset);
36 if (!leading) {
76 }));
77 }
79 > private async _getCandidates(agent: IAgent, session: URI): Promise<readonly SlashCommmandCandidate[]> {
80 if (!agent.getSessionCustomizations) {
81 return [];
95 return result;
96 }
98 > private _toSlashCommandCandidate(container: PluginCustomization | DirectoryCustomization, skill: SkillCustomization): SlashCommmandCandidate {
99 // see getCanonicalPluginCommandId
100 let slashCommandName = skill.name;
109 };
110 }
112 >
113 function isSyncedCustomization(container: PluginCustomization): boolean {
114 return container.uri.startsWith(SYNCED_CUSTOMIZATION_SCHEME + ':');
115 }
117 > interface SlashCommmandCandidate {
118 > readonly slashCommandName: string;
119 > readonly name: string;
120 > readonly description: string | undefined;
121 > readonly uri: string;
122 > }
src/vs/base/common/date.ts 46 covered LOC · 10 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- date.ts
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 > import { localize } from '../../nls.js';
7 > import { Lazy } from './lazy.js';
8 > import { LANGUAGE_DEFAULT } from './platform.js';
9 >
10 > const minute = 60;
11 > const hour = minute * 60;
12 > const day = hour * 24;
13 > const week = day * 7;
14 > const month = day * 30;
15 > const year = day * 365;
16 >
17 > /**
18 > * Create a localized difference of the time between now and the specified date.
19 > * @param date The date to generate the difference from.
20 > * @param appendAgoLabel Whether to append the " ago" to the end.
21 > * @param useFullTimeWords Whether to use full words (eg. seconds) instead of
22 > * shortened (eg. secs).
23 > * @param disallowNow Whether to disallow the string "now" when the difference
24 > * is less than 30 seconds.
25 > */
26 > export function fromNow(date: number | Date, appendAgoLabel?: boolean, useFullTimeWords?: boolean, disallowNow?: boolean): string {
27 if (typeof date === 'undefined') {
28 return localize('date.fromNow.unknown', 'unknown');
205 }
206 }
207 > date.ts
208 > export function fromNowByDay(date: number | Date, appendAgoLabel?: boolean, useFullTimeWords?: boolean): string {
209 if (typeof date !== 'number') {
210 date = date.getTime();
226 return fromNow(date, appendAgoLabel, useFullTimeWords);
227 }
228 > date.ts
229 > /**
230 > * Gets a readable duration with intelligent/lossy precision. For example "40ms" or "3.040s")
231 > * @param ms The duration to get in milliseconds.
232 > * @param useFullTimeWords Whether to use full words (eg. seconds) instead of
233 > * shortened (eg. secs).
234 > */
235 > export function getDurationString(ms: number, useFullTimeWords?: boolean) {
236 const seconds = Math.abs(ms / 1000);
237 if (seconds < 1) {
257 return localize('duration.d', '{0} days', Math.round(ms / (1000 * day)));
258 }
259 > date.ts
260 > export function toLocalISOString(date: Date): string {
261 return date.getFullYear() +
262 '-' + String(date.getMonth() + 1).padStart(2, '0') +
268 'Z';
269 }
270 > date.ts
271 > export const safeIntl = {
272 > DateTimeFormat(locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions): Lazy<Intl.DateTimeFormat> {
273 return new Lazy(() => {
274 try {
279 });
280 },
281 > Collator(locales?: Intl.LocalesArgument, options?: Intl.CollatorOptions): Lazy<Intl.Collator> { date.ts
282 return new Lazy(() => {
283 try {
288 });
289 },
290 > Segmenter(locales?: Intl.LocalesArgument, options?: Intl.SegmenterOptions): Lazy<Intl.Segmenter> { date.ts
291 return new Lazy(() => {
292 try {
297 });
298 },
299 > Locale(tag: Intl.Locale | string, options?: Intl.LocaleOptions): Lazy<Intl.Locale> { date.ts
300 return new Lazy(() => {
301 try {
306 });
307 },
308 > NumberFormat(locales?: Intl.LocalesArgument, options?: Intl.NumberFormatOptions): Lazy<Intl.NumberFormat> { date.ts
309 return new Lazy(() => {
310 try {
src/vs/base/common/observableInternal/debugLocation.ts 46 covered LOC · 6 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- debugLocation.ts
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 > export type DebugLocation = DebugLocationImpl | undefined;
7 >
8 > export namespace DebugLocation {
9 > let enabled = false;
10 >
11 > export function enable(): void {
12 enabled = true;
13 }
15 > export function ofCaller(): DebugLocation {
16 > if (!enabled) { debugLocation.ts
17 > return undefined;
18 > }
19 const Err = Error as ErrorConstructor & { stackTraceLimit: number };
20
25
26 return DebugLocationImpl.fromStack(stack, 2);
29 >
30 > class DebugLocationImpl implements ILocation {
31 > public static fromStack(stack: string, parentIdx: number): DebugLocationImpl | undefined {
32 > const lines = stack.split('\n');
33 > const location = parseLine(lines[parentIdx + 1]);
34 > if (location) {
35 > return new DebugLocationImpl(
36 > location.fileName,
37 > location.line,
38 > location.column,
39 > location.id
40 > );
41 > } else {
42 > return undefined;
43 > }
44 > }
45 >
46 > constructor(
47 public readonly fileName: string,
48 public readonly line: number,
51 ) {
52 }
54 >
55 >
56 > export interface ILocation {
57 > fileName: string;
58 > line: number;
59 > column: number;
60 > id: string;
61 > }
62 >
63 function parseLine(stackLine: string): ILocation | undefined {
64 const match = stackLine.match(/\((.*):(\d+):(\d+)\)/);
src/vs/platform/agentHost/common/agentCustomizationSettings.ts 46 covered LOC · 7 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentCustomizationSettings.ts
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 > import type { ConfigPropertySchema, RootState } from './state/protocol/state.js';
7 >
8 > export const AGENT_CUSTOMIZATION_SETTINGS_META_KEY = 'vscode.agentCustomizationSettings';
9 >
10 > export interface IAgentCustomizationSettingDescriptor {
11 > readonly key: string;
12 > readonly group: string;
13 > readonly kind?: 'multiline';
14 > readonly saveLabel?: string;
15 > }
16 >
17 > export interface IAgentCustomizationSettingsDescriptor {
18 > readonly provider: string;
19 > readonly title: string;
20 > readonly description: string;
21 > readonly settings: readonly IAgentCustomizationSettingDescriptor[];
22 > readonly configurationFile?: {
23 > readonly resource: string;
24 > readonly title: string;
25 > readonly description: string;
26 > readonly openLabel: string;
27 > readonly documentationUrl?: string;
28 > readonly documentationLabel?: string;
29 > };
30 > }
31 >
32 > export interface IAgentCustomizationSettingsRegistration extends IAgentCustomizationSettingsDescriptor {
33 > readonly properties: Readonly<Record<string, ConfigPropertySchema>>;
34 > }
35 >
36 function isAgentCustomizationSettingDescriptor(value: unknown): value is IAgentCustomizationSettingDescriptor {
37 if (!value || typeof value !== 'object') {
44 && (setting.saveLabel === undefined || typeof setting.saveLabel === 'string');
45 }
47 function isAgentCustomizationSettingsDescriptor(value: unknown): value is IAgentCustomizationSettingsDescriptor {
48 if (!value || typeof value !== 'object') {
62 && (file.documentationLabel === undefined || typeof file.documentationLabel === 'string'));
63 }
65 > export function getAgentCustomizationSettingsEntries(state: RootState | undefined): readonly IAgentCustomizationSettingsDescriptor[] {
66 const meta = state?._meta;
67 const value = meta?.[AGENT_CUSTOMIZATION_SETTINGS_META_KEY];
68 return Array.isArray(value) ? value.filter(isAgentCustomizationSettingsDescriptor) : [];
69 }
71 > export function withAgentCustomizationSettings(state: RootState | undefined, entries: readonly IAgentCustomizationSettingsDescriptor[]): Record<string, unknown> {
72 return { ...state?._meta, [AGENT_CUSTOMIZATION_SETTINGS_META_KEY]: entries };
73 }
75 > export function readAgentCustomizationSettings(state: RootState | undefined, provider: string): IAgentCustomizationSettingsDescriptor | undefined {
76 return getAgentCustomizationSettingsEntries(state).find(entry => entry.provider === provider);
77 }
79 > export function getProviderBackedRootConfigKeys(state: RootState | undefined): ReadonlySet<string> {
80 return new Set(getAgentCustomizationSettingsEntries(state).flatMap(entry => entry.settings.map(setting => setting.key)));
81 }
83 > export function preserveProviderBackedRootConfigValues(state: RootState | undefined, replacement: Readonly<Record<string, unknown>>): Record<string, unknown> {
84 const values = { ...replacement };
85 const current = state?.config?.values;
src/vs/platform/agentHost/common/state/chatAttachmentContext.ts 46 covered LOC · 3 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- chatAttachmentContext.ts
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 > import { MessageAttachmentKind, ResponsePartKind, type MessageChatAttachment, type SimpleMessageAttachment, type Turn } from './protocol/state.js';
7 >
8 > /**
9 > * Model-facing preamble that frames the resolved transcript for the SDK. It is
10 > * intentionally hard-coded English (like Claude's `<system-reminder>` text):
11 > * this string is consumed by the model, not shown in the UI, so it must not be
12 > * localized.
13 > */
14 > const CHAT_TRANSCRIPT_PREAMBLE =
15 > 'The user referenced another chat in the same session. ' +
16 > 'The transcript below is that chat up to the selected turn, provided as background context. ' +
17 > 'Treat it as reference material that may or may not be relevant to the new question.';
18 >
19 > /**
20 > * Returns the referenced chat's turns bounded through {@link endTurn}, inclusive.
21 > * Throws when {@link endTurn} is not a retained completed turn.
22 > */
23 > export function boundChatTranscriptTurns(turns: readonly Turn[], endTurn: string): readonly Turn[] {
24 const index = turns.findIndex(t => t.id === endTurn);
25 if (index < 0) {
28 return turns.slice(0, index + 1);
29 }
31 > /**
32 > * Formats bounded transcript turns into a plain-text conversation for the
33 > * model. Only user message text and assistant markdown are rendered; tool
34 > * calls, reasoning, and other parts are omitted to keep the context bounded.
35 > *
36 > * This never expands nested attachments, so a {@link MessageChatAttachment}
37 > * referenced inside the source transcript is not recursively resolved.
38 > */
39 > export function formatChatTranscript(turns: readonly Turn[]): string {
40 const blocks: string[] = [];
41 for (const turn of turns) {
54 return blocks.join('\n\n');
55 }
57 > /**
58 > * Resolves a {@link MessageChatAttachment} into an SDK-compatible
59 > * {@link SimpleMessageAttachment}: the bounded transcript rendered as the
60 > * attachment's {@link SimpleMessageAttachment.modelRepresentation}. Every
61 > * provider adapter already inlines a `Simple` attachment's model
62 > * representation, so this keeps transcript formatting in one place instead of
63 > * duplicating it per agent.
64 > *
65 > * The resolution is non-recursive: it renders {@link sourceTurns} directly and
66 > * never re-resolves chat attachments found within them.
67 > */
68 > export function resolveChatAttachment(attachment: MessageChatAttachment, sourceTurns: readonly Turn[]): SimpleMessageAttachment {
69 const bounded = boundChatTranscriptTurns(sourceTurns, attachment.endTurn);
70 const transcript = formatChatTranscript(bounded);
src/vs/platform/agentHost/node/gitDiffContent.ts 46 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- gitDiffContent.ts
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 > import { URI } from '../../../base/common/uri.js';
7 >
8 > const GIT_BLOB_SCHEME = 'git-blob';
9 >
10 > interface IGitBlobUriQuery {
11 > readonly sessionUri: string;
12 > readonly sha: string;
13 > readonly repoRelativePath: string;
14 > }
15 >
16 > /**
17 > * Builds a `git-blob:` URI that references a file blob at a specific git
18 > * commit, scoped to a given session. Resolved by reading the session's
19 > * working directory and shelling out to `git show <sha>:<path>`.
20 > *
21 > * The URI path is the absolute working-tree path so resource labels show a
22 > * recognizable file path and so the "before" side lines up with the
23 > * working-tree "after" side in diff editors. The session URI, SHA, and
24 > * repository-relative path needed to fetch the blob are carried in the
25 > * query.
26 > *
27 > * @param sessionUri Session the blob belongs to; used to find the working
28 > * directory.
29 > * @param sha Git commit/ref the blob is read from.
30 > * @param repoRelativePath Repository-relative path passed to `git show`.
31 > * @param absolutePath Absolute working-tree path used as the display path.
32 > */
33 > export function buildGitBlobUri(sessionUri: string, sha: string, repoRelativePath: string, absolutePath: string): string {
34 return URI.from({
35 scheme: GIT_BLOB_SCHEME,
38 }).toString();
39 }
41 > /** Parsed fields from a `git-blob:` content URI. */
42 > export interface IGitBlobUriFields {
43 > readonly sessionUri: string;
44 > readonly sha: string;
45 > readonly repoRelativePath: string;
46 > }
47 >
48 > /**
49 > * Parses a `git-blob:` URI produced by {@link buildGitBlobUri}.
50 > * Returns `undefined` if the URI is not a valid `git-blob:` URI.
51 > */
52 > export function parseGitBlobUri(raw: string): IGitBlobUriFields | undefined {
53 let parsed: URI;
54 try {
src/vs/base/node/processes.ts 45 covered LOC · 5 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- processes.ts
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 > import * as cp from 'child_process';
7 > import { Stats, promises } from 'fs';
8 > import { getCaseInsensitive } from '../common/objects.js';
9 > import * as path from '../common/path.js';
10 > import * as Platform from '../common/platform.js';
11 > import * as processCommon from '../common/process.js';
12 > import { CommandOptions, ForkOptions, Source, SuccessData, TerminateResponse, TerminateResponseCode } from '../common/processes.js';
13 > import * as Types from '../common/types.js';
14 > import * as pfs from './pfs.js';
15 > import { FileAccess } from '../common/network.js';
16 > import Stream from 'stream';
17 > export { Source, TerminateResponseCode, type CommandOptions, type ForkOptions, type SuccessData, type TerminateResponse };
18 >
19 > export type ValueCallback<T> = (value: T | Promise<T>) => void;
20 > export type ErrorCallback = (error?: any) => void;
21 > export type ProgressCallback<T> = (progress: T) => void;
22 >
23 >
24 > export function getWindowsShell(env = processCommon.env): string {
25 return env['comspec'] || 'cmd.exe';
26 }
28 > export interface IQueuedSender {
29 > send: (msg: any) => void;
30 > }
31 >
32 > // Wrapper around process.send() that will queue any messages if the internal node.js
33 > // queue is filled with messages and only continue sending messages when the internal
34 > // queue is free again to consume messages.
35 > // On Windows we always wait for the send() method to return before sending the next message
36 > // to workaround https://github.com/nodejs/node/issues/7657 (IPC can freeze process)
37 > export function createQueuedSender(childProcess: cp.ChildProcess): IQueuedSender {
38 let msgQueue: string[] = [];
39 let useQueue = false;
67 return { send };
68 }
70 async function fileExistsDefault(path: string): Promise<boolean> {
71 if (await pfs.Promises.exists(path)) {
83 return false;
84 }
86 export async function findExecutable(command: string, cwd?: string, paths?: string[], env: Platform.IProcessEnvironment = processCommon.env, fileExists: (path: string) => Promise<boolean> = fileExistsDefault): Promise<string | undefined> {
87 // If we have an absolute path then we take it.
140 return await fileExists(fullPath) ? fullPath : undefined;
141 }
142 > processes.ts
143 > /**
144 > * Kills a process and all its children.
145 > * @param pid the process id to kill
146 > * @param forceful whether to forcefully kill the process (default: false). Note
147 > * that on Windows, terminal processes can _only_ be killed forcefully and this
148 > * will throw when not forceful.
149 > */
150 export async function killTree(pid: number, forceful = false) {
151 let child: cp.ChildProcessByStdio<null, Stream.Readable, Stream.Readable>;
src/vs/base/test/common/virtualScheduling/virtualTimeApi.ts 45 covered LOC · 3 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- virtualTimeApi.ts
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 > import { IDisposable } from '../../../common/lifecycle.js';
7 > import { realTimeApi, TimeApi } from './timeApi.js';
8 > import { ROOT_TRACE, TraceContext } from './trace.js';
9 > import { VirtualClock } from './virtualClock.js';
10 >
11 > // V8 default `Error.stackTraceLimit` of 10 swallows everything past the
12 > // first async boundary in the stacks we capture for trace diagnostics.
13 > // Bump it so swimlane callers actually see the user code that scheduled a
14 > // timer rather than just the Promise wrapper.
15 > if (typeof Error.stackTraceLimit === 'number' && Error.stackTraceLimit < 50) {
16 > Error.stackTraceLimit = 50;
17 > }
18 >
19 > /** Virtual timer IDs are `IDisposable`s. Recover one from an opaque id. */
20 function asDisposable(id: unknown): IDisposable | undefined {
21 if (id === null || typeof id !== 'object') { return undefined; }
23 return typeof maybe.dispose === 'function' ? id as IDisposable : undefined;
24 }
26 > export interface CreateVirtualTimeApiOptions {
27 > /**
28 > * If `true`, `requestAnimationFrame` is faked: callbacks are scheduled
29 > * onto the virtual queue at `now + 16ms` and the resulting event hints
30 > * the embedding to use a real `requestAnimationFrame` so the host can
31 > * reflow before the callback runs. Useful for fixtures that need DOM
32 > * measurements after rAF callbacks.
33 > *
34 > * If `false` (default), `requestAnimationFrame` is left to the host.
35 > */
36 > readonly fakeRequestAnimationFrame?: boolean;
37 > }
38 >
39 > /**
40 > * Build a {@link TimeApi} that schedules every timer call into `clock`'s
41 > * virtual queue, capturing the current trace at schedule time so that
42 > * causal chains (`setTimeout` → `setTimeout`, etc.) are preserved.
43 > *
44 > * The returned API is suitable to install with {@link pushGlobalTimeApi},
45 > * which is what {@link runWithFakedTimers} does internally.
46 > */
47 > export function createVirtualTimeApi(
48 clock: VirtualClock,
49 options?: CreateVirtualTimeApiOptions,
190 return api;
191 }
193 > // Re-exported for convenience: many tests want to install both at once.
194 > export { pushGlobalTimeApi } from './globalTimeApi.js';
src/vs/base/common/linkedList.ts 44 covered LOC · 13 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- linkedList.ts
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 > class Node<E> {
7 >
8 > static readonly Undefined = new Node<unknown>(undefined);
9 >
10 > element: E;
11 > next: Node<E> | typeof Node.Undefined;
12 > prev: Node<E> | typeof Node.Undefined;
13 >
14 > constructor(element: E) {
15 > this.element = element;
16 > this.next = Node.Undefined;
17 > this.prev = Node.Undefined;
18 > }
19 > }
20 >
21 > export class LinkedList<E> {
22
23 private _first: Node<E> | typeof Node.Undefined = Node.Undefined;
24 private _last: Node<E> | typeof Node.Undefined = Node.Undefined;
25 private _size: number = 0;
27 > get size(): number {
28 return this._size;
29 }
31 > isEmpty(): boolean {
32 return this._first === Node.Undefined;
33 }
35 > clear(): void {
36 let node = this._first;
37 while (node !== Node.Undefined) {
46 this._size = 0;
47 }
49 > unshift(element: E): () => void {
50 return this._insert(element, false);
51 }
53 > push(element: E): () => void {
54 return this._insert(element, true);
55 }
57 > private _insert(element: E, atTheEnd: boolean): () => void {
58 const newNode = new Node(element);
59 if (this._first === Node.Undefined) {
85 };
86 }
88 > shift(): E | undefined {
89 if (this._first === Node.Undefined) {
90 return undefined;
95 }
96 }
98 > pop(): E | undefined {
99 if (this._last === Node.Undefined) {
100 return undefined;
105 }
106 }
108 > peek(): E | undefined {
109 if (this._last === Node.Undefined) {
110 return undefined;
114 }
115 }
117 > private _remove(node: Node<E> | typeof Node.Undefined): void {
118 if (node.prev !== Node.Undefined && node.next !== Node.Undefined) {
119 // middle
141 this._size -= 1;
142 }
144 > *[Symbol.iterator](): Iterator<E> {
145 let node = this._first;
146 while (node !== Node.Undefined) {
src/vs/base/test/common/virtualScheduling/runWithFakedTimers.ts 44 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- runWithFakedTimers.ts
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 > import { CancellationTokenSource } from '../../../common/cancellation.js';
7 > import { drainMicrotasksEmbedding } from './embedding.js';
8 > import { pushGlobalTimeApi } from './globalTimeApi.js';
9 > import { realTimeApi } from './timeApi.js';
10 > import { untilToken, VirtualTimeProcessor } from './processor.js';
11 > import { createRecordingRealTimeApi, RecordedTimerEvent } from './recordingTimeApi.js';
12 > import { VirtualClock } from './virtualClock.js';
13 > import { createVirtualTimeApi } from './virtualTimeApi.js';
14 >
15 > export interface RunWithFakedTimersOptions {
16 > readonly startTime?: number;
17 > /** Default `true`. Set `false` to bypass virtual time entirely (for
18 > * cases where the same test is parameterised over real/virtual time). */
19 > readonly useFakeTimers?: boolean;
20 > /** No effect in the new processor; accepted for legacy compatibility.
21 > * The drain-microtasks embedding picks the fastest available macrotask
22 > * primitive automatically. */
23 > readonly useSetImmediate?: boolean;
24 > /** Maximum number of virtual events the run is allowed to execute
25 > * before being rejected. Default 100. */
26 > readonly maxTaskCount?: number;
27 > /**
28 > * If set, called once `fn` resolves with the recorded timer events.
29 > * In virtual mode the events come from the {@link VirtualTimeProcessor}'s
30 > * own history; in real mode a recording wrapper around the host time
31 > * API is installed for the duration of `fn`. Useful for swimlane
32 > * diagnostics.
33 > */
34 > readonly onHistory?: (history: readonly RecordedTimerEvent[]) => void;
35 > }
36 >
37 > /**
38 > * Run `fn` with a virtual clock installed as the global time API.
39 > *
40 > * After `fn` resolves, the virtual queue is drained (so any timers `fn`
41 > * scheduled and `await`ed for, transitively, complete deterministically).
42 > * If `fn` throws, the queue is *not* drained — the original error is
43 > * re-thrown immediately.
44 > */
45 export async function runWithFakedTimers<T>(
46 options: RunWithFakedTimersOptions,
src/vs/platform/agentHost/common/agentClientUri.ts 44 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentClientUri.ts
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 > import { URI } from '../../../base/common/uri.js';
7 >
8 > /**
9 > * The URI scheme for accessing client-side files from the agent host.
10 > *
11 > * This is the inverse of {@link AGENT_HOST_SCHEME}: the agent host uses
12 > * this scheme to address files that live on the connected client.
13 > *
14 > * ```
15 > * vscode-agent-client://[clientId]/[originalScheme]/[originalAuthority]/[originalPath]
16 > * ```
17 > *
18 > * For example, `file:///Users/user/plugins/my-plugin` on client `client-1` becomes:
19 > * ```
20 > * vscode-agent-client://client-1/file/-/Users/user/plugins/my-plugin
21 > * ```
22 > */
23 > export const AGENT_CLIENT_SCHEME = 'vscode-agent-client';
24 >
25 > /**
26 > * Wraps a client-side URI into a {@link AGENT_CLIENT_SCHEME} URI that
27 > * can be resolved through the agent host's client filesystem provider.
28 > *
29 > * Opaque-path URIs (e.g. `untitled:Untitled-1`, where `path` has no
30 > * leading `/`) are marked with a `!` suffix on the encoded scheme slot
31 > * so the decoder can restore them faithfully. Scheme names are
32 > * alphanumeric + `+.-` per RFC 3986, so `!` cannot collide.
33 > *
34 > * @param originalUri The URI on the client (e.g. `file:///path`)
35 > * @param clientId The client identifier (from the protocol `clientId`)
36 > */
37 > export function toAgentClientUri(originalUri: URI, clientId: string): URI {
38 const originalAuthority = originalUri.authority || '-';
39 const isOpaque = originalUri.path.length > 0 && !originalUri.path.startsWith('/');
50 });
51 }
53 > /**
54 > * Extracts the original client-side URI from a {@link AGENT_CLIENT_SCHEME} URI.
55 > *
56 > * The inverse of {@link toAgentClientUri}.
57 > */
58 > export function fromAgentClientUri(agentClientUri: URI): URI {
59 const path = agentClientUri.path;
60 const query = agentClientUri.query || undefined;
src/vs/base/common/uint.ts 43 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- uint.ts
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 > export const enum Constants {
7 > /**
8 > * MAX SMI (SMall Integer) as defined in v8.
9 > * one bit is lost for boxing/unboxing flag.
10 > * one bit is lost for sign flag.
11 > * See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values
12 > */
13 > MAX_SAFE_SMALL_INTEGER = 1 << 30,
14 >
15 > /**
16 > * MIN SMI (SMall Integer) as defined in v8.
17 > * one bit is lost for boxing/unboxing flag.
18 > * one bit is lost for sign flag.
19 > * See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values
20 > */
21 > MIN_SAFE_SMALL_INTEGER = -(1 << 30),
22 >
23 > /**
24 > * Max unsigned integer that fits on 8 bits.
25 > */
26 > MAX_UINT_8 = 255, // 2^8 - 1
27 >
28 > /**
29 > * Max unsigned integer that fits on 16 bits.
30 > */
31 > MAX_UINT_16 = 65535, // 2^16 - 1
32 >
33 > /**
34 > * Max unsigned integer that fits on 32 bits.
35 > */
36 > MAX_UINT_32 = 4294967295, // 2^32 - 1
37 >
38 > UNICODE_SUPPLEMENTARY_PLANE_BEGIN = 0x010000
39 > }
40 >
41 > export function toUint8(v: number): number {
42 if (v < 0) {
43 return 0;
48 return v | 0;
49 }
50 > uint.ts
51 > export function toUint32(v: number): number {
52 if (v < 0) {
53 return 0;
src/vs/platform/agentHost/common/state/protocol/mcpAppDefaults.ts 43 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- mcpAppDefaults.ts
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 > import type { AhpMcpUiHostCapabilities, McpServerCustomizationApps } from './channels-session/state.js';
7 >
8 > /**
9 > * MCP App capabilities the agent host proxies for every MCP server it
10 > * advertises over the `mcp://` side-channel.
11 > *
12 > * - `serverTools.listChanged` is `true`: we forward
13 > * `notifications/tools/list_changed` whenever the SDK signals that the
14 > * tool inventory has refreshed (see `CopilotAgentSession`).
15 > * - `serverResources` is advertised as an empty object: we serve the
16 > * `resources/*` methods over the channel but do not promise
17 > * `notifications/resources/list_changed` forwarding (no `listChanged`).
18 > * - `sampling` is advertised as an empty object: we serve
19 > * `sampling/createMessage` requests from the App over the `mcp://`
20 > * channel (the agent host handler forwards them to
21 > * `session.rpc.mcp.executeSampling`). The SEP-1577 `tools`
22 > * sub-flag is NOT set — we don't pass through tool content blocks.
23 > *
24 > * Per the AHP spec, `mcpApp` is a static capability declaration —
25 > * "SHOULD be present whenever the server can host Apps" — so this
26 > * constant is set on every MCP customization at construction time,
27 > * regardless of the server's current lifecycle state.
28 > */
29 > export const DEFAULT_MCP_APP_CAPABILITIES: AhpMcpUiHostCapabilities = {
30 > serverTools: { listChanged: true },
31 > serverResources: {},
32 > sampling: {},
33 > };
34 >
35 > /**
36 > * The full `mcpApp` shape applied to a {@link McpServerCustomization}.
37 > * Wraps {@link DEFAULT_MCP_APP_CAPABILITIES} so callers can drop it in
38 > * directly without re-allocating the same wrapper object at every call
39 > * site.
40 > */
41 > export const DEFAULT_MCP_APP: McpServerCustomizationApps = {
42 > capabilities: DEFAULT_MCP_APP_CAPABILITIES,
43 > };
src/vs/platform/agentHost/node/agentHostTurnTracker.ts 43 covered LOC · 5 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostTurnTracker.ts
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 > import { StopWatch } from '../../../base/common/stopwatch.js';
7 > import type { AgentHostModelTelemetryKind, AgentHostTelemetryReporter, AgentHostTurnResult, IAgentHostTurnFailure } from './agentHostTelemetryReporter.js';
8 >
9 > /** Per-turn timing state, keyed by `session:turnId`. */
10 > interface ITurnTiming {
11 > readonly stopWatch: StopWatch;
12 > readonly provider: string;
13 > readonly session: string;
14 > readonly model: string | undefined;
15 > readonly modelTelemetryKind: AgentHostModelTelemetryKind | undefined;
16 > readonly permissionLevel: string | undefined;
17 > firstProgressMs: number | undefined;
18 > }
19 >
20 > /**
21 > * Tracks per-turn timing for agent host sessions and reports a completion
22 > * event via the provided {@link AgentHostTelemetryReporter} when a turn ends.
23 > *
24 > * Lifecycle per turn:
25 > * 1. {@link turnStarted} — begins a stopwatch for the turn
26 > * 2. {@link markFirstProgress} — records elapsed time to first visible output
27 > * (only the first call per turn has an effect)
28 > * 3. {@link turnCompleted} — emits the telemetry event and clears state
29 > */
30 > export class AgentHostTurnTracker {
31 >
32 > private readonly _turnTimings = new Map<string, ITurnTiming>();
33 >
34 > constructor(private readonly _reporter: AgentHostTelemetryReporter) { }
35 >
36 > turnStarted(provider: string, session: string, turnId: string, model: string | undefined, modelTelemetryKind: AgentHostModelTelemetryKind | undefined, permissionLevel: string | undefined): void {
37 const key = this._key(session, turnId);
38 this._turnTimings.set(key, {
46 });
47 }
49 > markFirstProgress(session: string, turnId: string): void {
50 const timing = this._turnTimings.get(this._key(session, turnId));
51 if (timing && timing.firstProgressMs === undefined) {
53 }
54 }
56 > turnCompleted(session: string, turnId: string, result: AgentHostTurnResult, failure?: IAgentHostTurnFailure): void {
57 const key = this._key(session, turnId);
58 const timing = this._turnTimings.get(key);
src/vs/base/test/common/virtualScheduling/globalTimeApi.ts 42 covered LOC · 3 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- globalTimeApi.ts
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 > import { IDisposable } from '../../../common/lifecycle.js';
7 > import { captureGlobalTimeApi, realTimeApi, TimeApi } from './timeApi.js';
8 >
9 > /** Cast through `unknown` so we don't widen our typed `TimeApi` shapes to `any`. */
10 > type AsGlobal<K extends keyof typeof globalThis> = (typeof globalThis)[K];
11 >
12 > /**
13 > * Ensure `fn` carries an `originalFn` back-door pointing at the real
14 > * (non-virtual) `setTimeout`. We prefer the existing tag on `fn`, then a tag
15 > * inherited from `previousFn` (which may itself be a wrapper that already
16 > * carried the back-door), and finally fall back to `realTimeApi.setTimeout`
17 > * — which has its own `originalFn` set at module load.
18 > */
19 function ensureSetTimeoutOriginalFn(fn: TimeApi['setTimeout'], previousFn: TimeApi['setTimeout']): TimeApi['setTimeout'] {
20 const tagged = fn as TimeApi['setTimeout'] & { originalFn?: TimeApi['setTimeout'] };
26 return tagged;
27 }
29 > /**
30 > * Replace the global time APIs (`setTimeout`, `setInterval`, …, `Date`,
31 > * optionally `requestAnimationFrame`) with the ones from `api`. Returns a
32 > * disposable that restores the previous globals.
33 > *
34 > * The previous globals are captured *at install time*, so nested installs
35 > * compose correctly (the disposable restores to whatever was current when
36 > * this call was made, not to the original real values).
37 > *
38 > * `setTimeout.originalFn` is preserved on the installed function so callers
39 > * like the component-explorer host can escape virtual time when polling.
40 > * If `api.setTimeout` does not already carry `originalFn`, it is copied from
41 > * the previous global (or defaulted to the real `setTimeout`) so wrapping
42 > * APIs such as a logging wrapper don't drop the back-door.
43 > */
44 > export function pushGlobalTimeApi(api: TimeApi): IDisposable {
45 const previous = captureGlobalTimeApi();
46
74 };
75 }
77 > // One-shot tag on the *real* setTimeout: lets callers (e.g. the
78 > // component-explorer host's polling loop) escape virtual time even after
79 > // pushGlobalTimeApi has installed a virtual version on top. The `originalFn`
80 > // property is not on the `setTimeout` signature by design — it's a back-door
81 > // convention shared with the polling code.
82 > (realTimeApi.setTimeout as unknown as { originalFn: TimeApi['setTimeout'] }).originalFn = realTimeApi.setTimeout;
src/vs/platform/agentHost/node/diffComputeService.ts 42 covered LOC · 9 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- diffComputeService.ts
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 > import { Worker } from 'worker_threads';
7 > import { Disposable } from '../../../base/common/lifecycle.js';
8 > import { FileAccess } from '../../../base/common/network.js';
9 > import { ILogService } from '../../log/common/log.js';
10 > import { DEFAULT_DIFF_TIMEOUT_MS, IDiffComputeService, type IDiffCountResult } from '../common/diffComputeService.js';
11 >
12 > /**
13 > * Node.js implementation of {@link IDiffComputeService} that runs
14 > * {@link DefaultLinesDiffComputer} in a worker thread to avoid blocking
15 > * the main thread.
16 > */
17 > export class NodeWorkerDiffComputeService extends Disposable implements IDiffComputeService {
18 >
19 > declare readonly _serviceBrand: undefined;
20 >
21 > private _worker: Worker | undefined;
22 > private _workerFailures = 0;
23 > private _nextId = 1;
24 > private readonly _pending = new Map<number, { resolve: (value: IDiffCountResult) => void; reject: (err: Error) => void }>();
25 >
26 > constructor(
27 > @ILogService private readonly _logService: ILogService, diffComputeService.ts
28 > ) {
29 > super();
30 > }
32 > async computeDiffCounts(original: string, modified: string, timeoutMs: number = DEFAULT_DIFF_TIMEOUT_MS): Promise<IDiffCountResult> {
33 const worker = this._ensureWorker();
34 const id = this._nextId++;
43 });
44 }
46 > private _ensureWorker(): Worker {
47 if (this._workerFailures >= 3) {
48 throw new Error('Diff compute worker failed too many times');
src/vs/platform/instantiation/common/graph.ts 42 covered LOC · 12 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- graph.ts
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 > export class Node<T> {
7 >
8 >
9 > readonly incoming = new Map<string, Node<T>>();
10 > readonly outgoing = new Map<string, Node<T>>();
11 >
12 > constructor(
13 readonly key: string,
14 readonly data: T
15 ) { }
16 > } graph.ts
17 >
18 > export class Graph<T> {
19 >
20 > private readonly _nodes = new Map<string, Node<T>>();
21 >
22 > constructor(private readonly _hashFn: (element: T) => string) {
23 // empty
24 }
25 > graph.ts
26 > roots(): Node<T>[] {
27 const ret: Node<T>[] = [];
28 for (const node of this._nodes.values()) {
33 return ret;
34 }
35 > graph.ts
36 > insertEdge(from: T, to: T): void {
37 const fromNode = this.lookupOrInsertNode(from);
38 const toNode = this.lookupOrInsertNode(to);
41 toNode.incoming.set(fromNode.key, fromNode);
42 }
43 > graph.ts
44 > removeNode(data: T): void {
45 const key = this._hashFn(data);
46 this._nodes.delete(key);
50 }
51 }
52 > graph.ts
53 > lookupOrInsertNode(data: T): Node<T> {
54 const key = this._hashFn(data);
55 let node = this._nodes.get(key);
62 return node;
63 }
64 > graph.ts
65 > lookup(data: T): Node<T> | undefined {
66 return this._nodes.get(this._hashFn(data));
67 }
68 > graph.ts
69 > isEmpty(): boolean {
70 return this._nodes.size === 0;
71 }
72 > graph.ts
73 > toString(): string {
74 const data: string[] = [];
75 for (const [key, value] of this._nodes) {
79 return data.join('\n');
80 }
81 > graph.ts
82 > /**
83 > * This is brute force and slow and **only** be used
84 > * to trouble shoot.
85 > */
86 > findCycleSlow() {
87 for (const [id, node] of this._nodes) {
88 const seen = new Set<string>([id]);
94 return undefined;
95 }
96 > graph.ts
97 > private _findCycle(node: Node<T>, seen: Set<string>): string | undefined {
98 for (const [id, outgoing] of node.outgoing) {
99 if (seen.has(id)) {
109 return undefined;
110 }
111 > } graph.ts
src/vs/platform/telemetry/common/1dsAppender.ts 41 covered LOC · 6 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- 1dsAppender.ts
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 > import type { IExtendedConfiguration, IExtendedTelemetryItem, ITelemetryItem, ITelemetryUnloadState } from '@microsoft/1ds-core-js';
7 > import type { IChannelConfiguration, IXHROverride, PostChannel } from '@microsoft/1ds-post-js';
8 > import { importAMDNodeModule } from '../../../amdX.js';
9 > import { onUnexpectedError } from '../../../base/common/errors.js';
10 > import { mixin } from '../../../base/common/objects.js';
11 > import { isWeb } from '../../../base/common/platform.js';
12 > import { ITelemetryAppender, validateTelemetryData } from './telemetryUtils.js';
13 >
14 > // Interface type which is a subset of @microsoft/1ds-core-js AppInsightsCore.
15 > // Allows us to more easily build mock objects for testing as the interface is quite large and we only need a few properties.
16 > export interface IAppInsightsCore {
17 > pluginVersionString: string;
18 > track(item: ITelemetryItem | IExtendedTelemetryItem): void;
19 > unload(isAsync: boolean, unloadComplete: (unloadState: ITelemetryUnloadState) => void): void;
20 > }
21 >
22 > const endpointUrl = 'https://mobile.events.data.microsoft.com/OneCollector/1.0';
23 > const endpointHealthUrl = 'https://mobile.events.data.microsoft.com/ping';
24 >
25 async function getClient(instrumentationKey: string, addInternalFlag?: boolean, xhrOverride?: IXHROverride): Promise<IAppInsightsCore> {
26 // eslint-disable-next-line local/code-amd-node-module
73 return appInsightsCore;
74 }
76 > // TODO @lramos15 maybe make more in line with src/vs/platform/telemetry/browser/appInsightsAppender.ts with caching support
77 > export abstract class AbstractOneDataSystemAppender implements ITelemetryAppender {
78 >
79 > protected _aiCoreOrKey: IAppInsightsCore | string | undefined;
80 > private _asyncAiCore: Promise<IAppInsightsCore> | null;
81 > protected readonly endPointUrl = endpointUrl;
82 > protected readonly endPointHealthUrl = endpointHealthUrl;
83 >
84 > constructor(
85 private readonly _isInternalTelemetry: boolean,
86 private _eventPrefix: string,
100 this._asyncAiCore = null;
101 }
103 > private _withAIClient(callback: (aiCore: IAppInsightsCore) => void): void {
104 if (!this._aiCoreOrKey) {
105 return;
125 );
126 }
128 > log(eventName: string, data?: unknown): void {
129 if (!this._aiCoreOrKey) {
130 return;
144 } catch { }
145 }
147 > flush(): Promise<void> {
148 if (this._aiCoreOrKey) {
149 return new Promise(resolve => {
src/vs/base/common/lazy.ts 39 covered LOC · 4 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- lazy.ts
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 > enum LazyValueState {
7 > Uninitialized,
8 > Running,
9 > Completed,
10 > }
11 >
12 > export class Lazy<T> {
13 >
14 > private _state = LazyValueState.Uninitialized;
15 > private _value?: T;
16 > private _error: Error | undefined;
17 >
18 > constructor(
19 > private readonly executor: () => T, lazy.ts
20 > ) { }
21 > lazy.ts
22 > /**
23 > * True if the lazy value has been resolved.
24 > */
25 > get hasValue(): boolean { return this._state === LazyValueState.Completed; }
26 >
27 > /**
28 > * Get the wrapped value.
29 > *
30 > * This will force evaluation of the lazy value if it has not been resolved yet. Lazy values are only
31 > * resolved once. `getValue` will re-throw exceptions that are hit while resolving the value
32 > */
33 > get value(): T {
34 if (this._state === LazyValueState.Uninitialized) {
35 this._state = LazyValueState.Running;
50 return this._value!;
51 }
52 > lazy.ts
53 > /**
54 > * Get the wrapped value without forcing evaluation.
55 > */
56 > get rawValue(): T | undefined { return this._value; }
57 > }
src/vs/platform/agentHost/node/agentHostSlashCompletion.ts 39 covered LOC · 4 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostSlashCompletion.ts
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 > import { matchesFuzzy2 } from '../../../base/common/filters.js';
7 >
8 > /**
9 > * A leading slash token in a user-message input.
10 > */
11 > export interface ILeadingSlashToken {
12 > /** The token including the leading slash. */
13 > readonly token: string;
14 > /** The typed token text after the slash. */
15 > readonly typed: string;
16 > /** The start offset of the token range to replace. */
17 > readonly rangeStart: number;
18 > /** The end offset of the token range to replace. */
19 > readonly rangeEnd: number;
20 > }
21 >
22 > /**
23 > * Extracts the leading `/word` token from the input, where the token is the
24 > * run of non-whitespace characters starting at offset 0. Returns `undefined`
25 > * if the input does not start with `/` or the cursor is past the token.
26 > */
27 > export function extractLeadingSlashToken(text: string, offset: number): ILeadingSlashToken | undefined {
28 if (text.length === 0 || text.charCodeAt(0) !== 0x2f /* / */) {
29 return undefined;
42 return { token, typed: token.slice(1), rangeStart: 0, rangeEnd: end };
43 }
45 > /**
46 > * Extracts the slash token containing the cursor when the slash is either at
47 > * the start of the input or immediately follows whitespace.
48 > */
49 > export function extractWhitespaceDelimitedSlashToken(text: string, offset: number): ILeadingSlashToken | undefined {
50 if (text.length === 0 || offset < 0 || offset > text.length) {
51 return undefined;
71 return { token, typed: token.slice(1), rangeStart: start, rangeEnd: end };
72 }
74 > /**
75 > * Tests whether a slash completion name fuzzy matches the typed token text.
76 > */
77 > export function matchesSlashCompletion(typed: string, name: string): boolean {
78 if (typed.length === 0 || name.toLowerCase().startsWith(typed.toLowerCase())) {
79 return true;
81 return typed.length > 1 && matchesFuzzy2(typed, name) !== null;
82 }
84 function isSlashTokenWhitespace(ch: number): boolean {
85 return ch === 0x20 /* space */ || ch === 0x09 /* tab */ || ch === 0x0a /* \n */ || ch === 0x0d /* \r */;
src/vs/platform/product/common/product.ts 39 covered LOC · 4 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- product.ts
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 > import { env } from '../../../base/common/process.js';
7 > import { IProductConfiguration } from '../../../base/common/product.js';
8 > import { ISandboxConfiguration } from '../../../base/parts/sandbox/common/sandboxTypes.js';
9 >
10 > /**
11 > * @deprecated It is preferred that you use `IProductService` if you can. This
12 > * allows web embedders to override our defaults. But for things like `product.quality`,
13 > * the use is fine because that property is not overridable.
14 > */
15 > let product: IProductConfiguration;
16 >
17 > // Native sandbox environment
18 > const vscodeGlobal = (globalThis as { vscode?: { context?: { configuration(): ISandboxConfiguration | undefined } } }).vscode;
19 > if (typeof vscodeGlobal !== 'undefined' && typeof vscodeGlobal.context !== 'undefined') {
20 const configuration: ISandboxConfiguration | undefined = vscodeGlobal.context.configuration();
21 if (configuration) {
25 }
26 }
27 > // _VSCODE environment product.ts
28 > else if (globalThis._VSCODE_PRODUCT_JSON && globalThis._VSCODE_PACKAGE_JSON) {
29 > // Obtain values from product.json and package.json-data
30 > product = globalThis._VSCODE_PRODUCT_JSON as unknown as IProductConfiguration;
31 >
32 > // Running out of sources
33 > if (env['VSCODE_DEV']) {
34 Object.assign(product, {
35 nameShort: `${product.nameShort} Dev`,
39 });
40 }
41 > product.ts
42 > // Version is added during built time, but we still
43 > // want to have it running out of sources so we
44 > // read it from package.json only when we need it.
45 > if (!product.version) {
46 > const pkg = globalThis._VSCODE_PACKAGE_JSON as { version: string };
47 >
48 > Object.assign(product, {
49 > version: pkg.version
50 > });
51 > }
52 }
53
90 }
91 }
92 > product.ts
93 > export default product;
src/vs/platform/telemetry/node/1dsAppender.ts 39 covered LOC · 5 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- 1dsAppender.ts
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 > import type { IPayloadData, IXHROverride } from '@microsoft/1ds-post-js';
7 > import { streamToBuffer } from '../../../base/common/buffer.js';
8 > import { CancellationToken } from '../../../base/common/cancellation.js';
9 > import { IRequestOptions } from '../../../base/parts/request/common/request.js';
10 > import { IRequestService, NO_FETCH_TELEMETRY } from '../../request/common/request.js';
11 > import { AbstractOneDataSystemAppender, IAppInsightsCore } from '../common/1dsAppender.js';
12 >
13 > type OnCompleteFunc = (status: number, headers: { [headerName: string]: string }, response?: string) => void;
14 >
15 > interface IResponseData {
16 > headers: { [headerName: string]: string };
17 > statusCode: number;
18 > responseData: string;
19 > }
20 >
21 > /**
22 > * Completes a request to submit telemetry to the server utilizing the request service
23 > * @param options The options which will be used to make the request
24 > * @param requestService The request service
25 > * @returns An object containing the headers, statusCode, and responseData
26 > */
27 async function makeTelemetryRequest(options: IRequestOptions, requestService: IRequestService): Promise<IResponseData> {
28 const response = await requestService.request(options, CancellationToken.None);
36 };
37 }
39 > /**
40 > * Complete a request to submit telemetry to the server utilizing the https module. Only used when the request service is not available
41 > * @param options The options which will be used to make the request
42 > * @returns An object containing the headers, statusCode, and responseData
43 > */
44 async function makeLegacyTelemetryRequest(options: IRequestOptions): Promise<IResponseData> {
45 const https = await import('https'); // Lazy due to https://github.com/nodejs/node/issues/59686
71 return responsePromise;
72 }
74 async function sendPostAsync(requestService: IRequestService | undefined, payload: IPayloadData, oncomplete: OnCompleteFunc) {
75 const telemetryRequestData = typeof payload.data === 'string' ? payload.data : new TextDecoder().decode(payload.data);
94 }
95 }
97 >
98 > export class OneDataSystemAppender extends AbstractOneDataSystemAppender {
99 >
100 > constructor(
101 requestService: IRequestService | undefined,
102 isInternalTelemetry: boolean,
115 super(isInternalTelemetry, eventPrefix, defaultData, iKeyOrClientFactory, customHttpXHROverride);
116 }
117 > } 1dsAppender.ts
src/vs/base/common/observableInternal/observables/observableSignalFromEvent.ts 38 covered LOC · 8 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- observableSignalFromEvent.ts
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 > import { IObservable } from '../base.js';
7 > import { transaction } from '../transaction.js';
8 > import { Event, IDisposable } from '../commonFacade/deps.js';
9 > import { DebugOwner, DebugNameData } from '../debugName.js';
10 > import { BaseObservable } from './baseObservable.js';
11 > import { DebugLocation } from '../debugLocation.js';
12 >
13 > export function observableSignalFromEvent(
14 owner: DebugOwner | string,
15 event: Event<any>,
18 return new FromEventObservableSignal(typeof owner === 'string' ? owner : new DebugNameData(owner, undefined, undefined), event, debugLocation);
19 }
21 > class FromEventObservableSignal extends BaseObservable<void> {
22 > private subscription: IDisposable | undefined;
23 >
24 > public readonly debugName: string;
25 > constructor(
26 debugNameDataOrName: DebugNameData | string,
27 private readonly event: Event<any>,
33 : debugNameDataOrName.getDebugName(this) ?? 'Observable Signal From Event';
34 }
36 > protected override onFirstObserverAdded(): void {
37 this.subscription = this.event(this.handleEvent);
38 }
40 > private readonly handleEvent = () => {
41 > transaction( observableSignalFromEvent.ts
42 > (tx) => {
43 > for (const o of this._observers) {
44 > tx.updateObserver(o, this);
45 > o.handleChange(this, undefined);
46 > }
47 > },
48 > () => this.debugName
49 > );
50 > };
52 > protected override onLastObserverRemoved(): void {
53 this.subscription!.dispose();
54 this.subscription = undefined;
55 }
57 > public override get(): void {
58 // NO OP
59 }
src/vs/platform/agentHost/common/agentHostChangesetSubscriptionService.ts 38 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostChangesetSubscriptionService.ts
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 > import type { URI as ProtocolURI } from './state/sessionState.js';
7 > import { createDecorator } from '../../instantiation/common/instantiation.js';
8 >
9 > export const IAgentHostChangesetSubscriptionService = createDecorator<IAgentHostChangesetSubscriptionService>('agentHostChangesetSubscriptionService');
10 >
11 > /**
12 > * Shared changeset subscription registry. The coordinator records subscription
13 > * lifecycle changes here; compute services read the current per-session set.
14 > */
15 > export interface IAgentHostChangesetSubscriptionService {
16 > readonly _serviceBrand: undefined;
17 >
18 > /**
19 > * Returns the set of changeset URIs currently subscribed for `session`.
20 > * Empty when the session has no active changeset subscribers.
21 > */
22 > getSessionSubscriptions(session: ProtocolURI): ReadonlySet<ProtocolURI>;
23 >
24 > /**
25 > * Adds `changeset` to the active subscription set for `session`.
26 > */
27 > addSubscription(session: ProtocolURI, changeset: ProtocolURI): void;
28 >
29 > /**
30 > * Removes `changeset` from the active subscription set for `session`.
31 > */
32 > removeSubscription(session: ProtocolURI, changeset: ProtocolURI): void;
33 >
34 > /**
35 > * Drops every active subscription for `session`.
36 > */
37 > clearSessionSubscriptions(session: ProtocolURI): void;
38 > }
src/vs/platform/agentHost/node/agentHostCommitOperationProvider.ts 38 covered LOC · 7 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostCommitOperationProvider.ts
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 > import { Disposable, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';
7 > import { localize } from '../../../nls.js';
8 > import { IInstantiationService } from '../../instantiation/common/instantiation.js';
9 > import type { IChangesetOperationContribution, IChangesetOperationContext, IChangesetOperationRegistry } from '../common/agentHostChangesetOperationService.js';
10 > import { ChangesetOperationScope, ChangesetOperationStatus, type ChangesetOperation } from '../common/state/sessionState.js';
11 > import { AgentHostCommitOperationHandler } from './agentHostCommitOperationHandler.js';
12 > import { AgentHostStateManager, IAgentHostStateManager } from './agentHostStateManager.js';
13 >
14 > export class AgentHostCommitOperationContribution extends Disposable implements IChangesetOperationContribution {
15 >
16 > private _registry: IChangesetOperationRegistry | undefined;
17 >
18 > constructor(
19 > @IAgentHostStateManager private readonly _stateManager: AgentHostStateManager, agentHostCommitOperationProvider.ts
20 > @IInstantiationService private readonly _instantiationService: IInstantiationService,
21 > ) {
22 > super();
23 > }
25 > registerHandlers(registry: IChangesetOperationRegistry): IDisposable {
26 > this._registry = registry; agentHostCommitOperationProvider.ts
27 > const store = new DisposableStore();
28 > const getSessionState = (sessionKey: string) => this._stateManager.getSessionState(sessionKey);
29 > const handler = this._instantiationService.createInstance(AgentHostCommitOperationHandler, getSessionState, (sessionKey: string) => this._onCommitted(sessionKey));
30 > store.add(registry.registerChangesetOperationHandler(AgentHostCommitOperationHandler.OPERATION_COMMIT, handler));
31 > store.add({ dispose: () => { this._registry = undefined; } });
32 > return store;
33 > }
35 > getOperations({ changesetKind, gitHubState, gitState }: IChangesetOperationContext): ChangesetOperation[] {
36 if ((gitState?.uncommittedChanges ?? 0) <= 0) {
37 return [];
src/vs/platform/agentHost/node/agentHostSyncOperationProvider.ts 38 covered LOC · 7 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostSyncOperationProvider.ts
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 > import { Disposable, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';
7 > import { localize } from '../../../nls.js';
8 > import { IInstantiationService } from '../../instantiation/common/instantiation.js';
9 > import type { IChangesetOperationContribution, IChangesetOperationContext, IChangesetOperationRegistry } from '../common/agentHostChangesetOperationService.js';
10 > import { ChangesetOperationScope, ChangesetOperationStatus, SessionLifecycle, type ChangesetOperation } from '../common/state/sessionState.js';
11 > import { AgentHostStateManager, IAgentHostStateManager } from './agentHostStateManager.js';
12 > import { AgentHostSyncOperationHandler } from './agentHostSyncOperationHandler.js';
13 >
14 > export class AgentHostSyncOperationContribution extends Disposable implements IChangesetOperationContribution {
15 >
16 > private _registry: IChangesetOperationRegistry | undefined;
17 >
18 > constructor(
19 > @IAgentHostStateManager private readonly _stateManager: AgentHostStateManager, agentHostSyncOperationProvider.ts
20 > @IInstantiationService private readonly _instantiationService: IInstantiationService,
21 > ) {
22 > super();
23 > }
25 > registerHandlers(registry: IChangesetOperationRegistry): IDisposable {
26 > this._registry = registry; agentHostSyncOperationProvider.ts
27 > const store = new DisposableStore();
28 > const getSessionState = (sessionKey: string) => this._stateManager.getSessionState(sessionKey);
29 > const handler = this._instantiationService.createInstance(AgentHostSyncOperationHandler, getSessionState, (sessionKey: string) => this._onSynced(sessionKey));
30 > store.add(registry.registerChangesetOperationHandler(AgentHostSyncOperationHandler.OPERATION_SYNC, handler));
31 > store.add({ dispose: () => { this._registry = undefined; } });
32 > return store;
33 > }
35 > getOperations({ sessionKey, gitState }: IChangesetOperationContext): ChangesetOperation[] | undefined {
36 // New Session
37 const state = this._stateManager.getSessionState(sessionKey);
src/vs/base/common/observableInternal/observables/observableSignal.ts 37 covered LOC · 6 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- observableSignal.ts
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 > import { IObservableWithChange, ITransaction } from '../base.js';
7 > import { transaction } from '../transaction.js';
8 > import { DebugNameData } from '../debugName.js';
9 > import { BaseObservable } from './baseObservable.js';
10 > import { DebugLocation } from '../debugLocation.js';
11 >
12 > /**
13 > * Creates a signal that can be triggered to invalidate observers.
14 > * Signals don't have a value - when they are triggered they indicate a change.
15 > * However, signals can carry a delta that is passed to observers.
16 > */
17 > export function observableSignal<TDelta = void>(debugName: string): IObservableSignal<TDelta>;
18 > export function observableSignal<TDelta = void>(owner: object): IObservableSignal<TDelta>;
19 > export function observableSignal<TDelta = void>(debugNameOrOwner: string | object, debugLocation = DebugLocation.ofCaller()): IObservableSignal<TDelta> {
20 if (typeof debugNameOrOwner === 'string') {
21 return new ObservableSignal<TDelta>(debugNameOrOwner, undefined, debugLocation);
24 }
25 }
27 > export interface IObservableSignal<TChange> extends IObservableWithChange<void, TChange> {
28 > trigger(tx: ITransaction | undefined, change: TChange): void;
29 > }
30 >
31 > class ObservableSignal<TChange> extends BaseObservable<void, TChange> implements IObservableSignal<TChange> {
32 > public get debugName() {
33 > return new DebugNameData(this._owner, this._debugName, undefined).getDebugName(this) ?? 'Observable Signal';
34 > }
35 >
36 > public override toString(): string {
37 return this.debugName;
38 }
40 > constructor(
41 private readonly _debugName: string | undefined,
42 private readonly _owner: object | undefined,
45 super(debugLocation);
46 }
48 > public trigger(tx: ITransaction | undefined, change: TChange): void {
49 if (!tx) {
50 transaction(tx => {
src/vs/base/node/id.ts 37 covered LOC · 6 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- id.ts
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 > import { networkInterfaces } from 'os';
7 > import { TernarySearchTree } from '../common/ternarySearchTree.js';
8 > import * as uuid from '../common/uuid.js';
9 > import { getMac } from './macAddress.js';
10 > import { isWindows } from '../common/platform.js';
11 > import { stripUTF8BOM } from '../common/strings.js';
12 >
13 > // http://www.techrepublic.com/blog/data-center/mac-address-scorecard-for-common-virtual-machine-platforms/
14 > // VMware ESX 3, Server, Workstation, Player 00-50-56, 00-0C-29, 00-05-69
15 > // Microsoft Hyper-V, Virtual Server, Virtual PC 00-03-FF
16 > // Parallels Desktop, Workstation, Server, Virtuozzo 00-1C-42
17 > // Virtual Iron 4 00-0F-4B
18 > // Red Hat Xen 00-16-3E
19 > // Oracle VM 00-16-3E
20 > // XenSource 00-16-3E
21 > // Novell Xen 00-16-3E
22 > // Sun xVM VirtualBox 08-00-27
23 > export const virtualMachineHint: { value(): number } = new class {
24 >
25 > private _virtualMachineOUIs?: TernarySearchTree<string, boolean>;
26 > private _value?: number;
27 >
28 > private _isVirtualMachineMacAddress(mac: string): boolean {
29 if (!this._virtualMachineOUIs) {
30 this._virtualMachineOUIs = TernarySearchTree.forStrings<boolean>();
50 return !!this._virtualMachineOUIs.findSubstr(mac);
51 }
52 > id.ts
53 > value(): number {
54 if (this._value === undefined) {
55 let vmOui = 0;
77 return this._value;
78 }
79 > }; id.ts
80 >
81 > let machineId: Promise<string>;
82 export async function getMachineId(errorLogger: (error: Error) => void): Promise<string> {
83 if (!machineId) {
91 return machineId;
92 }
93 > id.ts
94 async function getMacMachineId(errorLogger: (error: Error) => void): Promise<string | undefined> {
95 try {
102 }
103 }
104 > id.ts
105 > const SQM_KEY: string = 'Software\\Microsoft\\SQMClient';
106 export async function getSqmMachineId(errorLogger: (error: Error) => void): Promise<string> {
107 if (isWindows) {
116 return '';
117 }
118 > id.ts
119 export async function getDevDeviceId(errorLogger: (error: Error) => void): Promise<string> {
120 try {
src/vs/platform/agentHost/common/state/protocol/channels-session/reducer.ts 37 covered LOC · 4 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- reducer.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > import { ActionType } from '../common/actions.js';
10 > import { SessionLifecycle, SessionStatus, CustomizationType, McpServerStatus, type SessionState, type SessionInputRequest, type McpServerCustomization } from './state.js';
11 > import type { SessionAction } from '../action-origin.generated.js';
12 > import { softAssertNever } from '../common/reducer-helpers.js';
13 >
14 > // ─── Helpers ─────────────────────────────────────────────────────────────────
15 >
16 > /** Bitmask covering the mutually-exclusive activity bits (bits 0–4). */
17 > const STATUS_ACTIVITY_MASK = (1 << 5) - 1;
18 >
19 > /** Sets or clears a metadata flag on a status value. */
20 function withStatusFlag(status: SessionStatus, flag: SessionStatus, set: boolean): SessionStatus {
21 return set ? status | flag : status & ~flag;
22 }
23 > reducer.ts
24 > /**
25 > * Reflects the session-level {@link SessionState.inputNeeded | input queue}
26 > * into the activity bits of `status`. A non-empty queue promotes the activity
27 > * to {@link SessionStatus.InputNeeded}; emptying it clears the
28 > * input-needed-specific bit. Since `InputNeeded` implies
29 > * {@link SessionStatus.InProgress}, an unblocked turn falls back to
30 > * `InProgress` while an already-idle session stays idle. Orthogonal flags
31 > * (`IsRead` / `IsArchived`) are preserved.
32 > */
33 function withInputNeededStatus(status: SessionStatus, inputNeeded: readonly SessionInputRequest[]): SessionStatus {
34 if (inputNeeded.length > 0) {
37 return status & ~(SessionStatus.InputNeeded & ~SessionStatus.InProgress);
38 }
39 > reducer.ts
40 function updateMcpServerCustomization(
41 state: SessionState,
84 return { ...state, customizations: updated };
85 }
86 > reducer.ts
87 > // ─── Session Reducer ─────────────────────────────────────────────────────────
88 >
89 > /**
90 > * Pure reducer for session state. Handles all {@link SessionAction} variants.
91 > */
92 > export function sessionReducer(state: SessionState, action: SessionAction, log?: (msg: string) => void): SessionState {
93 switch (action.type) {
94 // ── Lifecycle ──────────────────────────────────────────────────────────
src/vs/base/test/common/virtualScheduling/index.ts 36 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- index.ts
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 > // Greenfield virtual scheduling primitives.
7 > //
8 > // This folder is the new home for virtual-time scheduling. It supersedes
9 > // `timeTravelScheduler.ts` and `traceableTimeApi.ts`, both of which are
10 > // retained as @deprecated re-export shims.
11 >
12 > export type { TimeApi } from './timeApi.js';
13 > export { captureGlobalTimeApi, realTimeApi } from './timeApi.js';
14 >
15 > export type { EventSource, VirtualEvent, VirtualTime } from './virtualClock.js';
16 > export { VirtualClock } from './virtualClock.js';
17 >
18 > export type { RunAsHandlerOptions } from './trace.js';
19 > export { ROOT_TRACE, Trace, TraceContext, createTraceRoot } from './trace.js';
20 >
21 > export type { Embedding } from './embedding.js';
22 > export { drainMicrotasksEmbedding, nextMacrotask, syncEmbedding } from './embedding.js';
23 >
24 > export type { RunOptions, TerminationPolicy, VirtualTimeProcessorOptions } from './processor.js';
25 > export { VirtualTimeProcessor, untilIdle, untilTime, untilToken } from './processor.js';
26 >
27 > export { pushGlobalTimeApi } from './globalTimeApi.js';
28 > export type { CreateVirtualTimeApiOptions } from './virtualTimeApi.js';
29 > export { createVirtualTimeApi } from './virtualTimeApi.js';
30 > export { createLoggingTimeApi } from './loggingTimeApi.js';
31 > export type { RecordedTimerEvent } from './recordingTimeApi.js';
32 > export { createRecordingRealTimeApi } from './recordingTimeApi.js';
33 > export type { ITraceLogEntry, ITraceLogger } from './traceLogger.js';
34 > export { createTraceLogger } from './traceLogger.js';
35 > export type { RunWithFakedTimersOptions } from './runWithFakedTimers.js';
36 > export { runWithFakedTimers } from './runWithFakedTimers.js';
src/vs/platform/agentHost/common/agentHostTelemetryEnv.ts 36 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostTelemetryEnv.ts
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 > /**
7 > * Environment variables used to forward the host's resolved telemetry
8 > * identifiers into the agent host process.
9 > *
10 > * The agent host runs in its own utility process and would otherwise compute
11 > * its own `machineId`/`devDeviceId` live from the MAC address / device-id store
12 > * on every launch. That can diverge from the workbench's persisted, state-backed
13 > * identifiers (e.g. when `state.json` was seeded by imaging/migration, or when
14 > * the "first valid MAC" changes), breaking per-user joins across event sources.
15 > *
16 > * To keep the identifiers consistent, the local starter
17 > * (`ElectronAgentHostStarter`, which runs in the main process where these are
18 > * already resolved) forwards them via these env vars, and
19 > * `createAgentHostTelemetryService` prefers them over recomputing.
20 > */
21 > export const AgentHostMachineIdEnvKey = 'VSCODE_AGENT_HOST_MACHINE_ID';
22 > export const AgentHostSqmIdEnvKey = 'VSCODE_AGENT_HOST_SQM_ID';
23 > export const AgentHostDevDeviceIdEnvKey = 'VSCODE_AGENT_HOST_DEV_DEVICE_ID';
24 >
25 > export interface IAgentHostForwardedTelemetryIds {
26 > readonly machineId: string;
27 > readonly sqmId: string;
28 > readonly devDeviceId: string;
29 > }
30 >
31 > /**
32 > * Builds the env var bag that forwards the resolved telemetry identifiers to
33 > * the agent host process. Empty identifiers are omitted so the host falls back
34 > * to computing them itself.
35 > */
36 > export function buildAgentHostTelemetryIdEnv(ids: IAgentHostForwardedTelemetryIds): Record<string, string> {
37 const env: Record<string, string> = {};
38 if (ids.machineId) {
src/vs/platform/sandbox/common/settings.ts 36 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- settings.ts
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 > /**
7 > * Setting IDs for agent sandboxing.
8 > */
9 > export const enum AgentSandboxSettingId {
10 > AgentSandboxEnabled = 'chat.agent.sandbox.enabled',
11 > AgentSandboxWindowsEnabled = 'chat.agent.sandbox.enabledWindows',
12 > AgentSandboxAllowNetwork = 'chat.agent.sandbox.allowNetwork',
13 > AgentSandboxAllowUnsandboxedCommands = 'chat.agent.sandbox.allowUnsandboxedCommands',
14 > AgentSandboxRetryWithAllowNetworkRequests = 'chat.agent.sandbox.retryWithAllowNetworkRequests',
15 > AgentSandboxAllowAutoApprove = 'chat.agent.sandbox.allowAutoApprove',
16 > AgentSandboxLinuxFileSystem = 'chat.agent.sandbox.fileSystem.linux',
17 > AgentSandboxMacFileSystem = 'chat.agent.sandbox.fileSystem.mac',
18 > AgentSandboxWindowsFileSystem = 'chat.agent.sandbox.fileSystem.windows',
19 > AgentSandboxWindowsSchemaVersion = 'chat.agent.sandbox.advanced.windows.schemaVersion',
20 > AgentSandboxAdvancedRuntime = 'chat.agent.sandbox.advanced.runtime',
21 > DeprecatedAgentSandboxEnabled = 'chat.agent.sandbox',
22 > DeprecatedAgentSandboxLinuxFileSystem = 'chat.agent.sandboxFileSystem.linux',
23 > DeprecatedAgentSandboxMacFileSystem = 'chat.agent.sandboxFileSystem.mac',
24 > }
25 >
26 > export const enum AgentSandboxEnabledValue {
27 > Off = 'off',
28 > On = 'on',
29 > AllowNetwork = 'allowNetwork',
30 > }
31 >
32 > export type AgentSandboxEnabledSettingValue = AgentSandboxEnabledValue | boolean;
33 >
34 > export function normalizeAgentSandboxEnabledValue(value: AgentSandboxEnabledSettingValue): AgentSandboxEnabledValue {
35 if (value === true) {
36 return AgentSandboxEnabledValue.On;
41 return value;
42 }
44 > export function isAgentSandboxEnabledValue(value: AgentSandboxEnabledSettingValue | undefined): boolean {
45 return value !== undefined && normalizeAgentSandboxEnabledValue(value) !== AgentSandboxEnabledValue.Off;
46 }
src/vs/base/common/observableInternal/changeTracker.ts 35 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- changeTracker.ts
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 > import { BugIndicatingError } from './commonFacade/deps.js';
7 > import { IObservableWithChange, IReader } from './base.js';
8 >
9 > export interface IChangeTracker<TChangeSummary> {
10 > createChangeSummary(previousChangeSummary: TChangeSummary | undefined): TChangeSummary;
11 > handleChange(ctx: IChangeContext, change: TChangeSummary): boolean;
12 > beforeUpdate?(reader: IReader, change: TChangeSummary): void;
13 > }
14 >
15 > export interface IChangeContext {
16 > readonly changedObservable: IObservableWithChange<any, any>;
17 > readonly change: unknown;
18 >
19 > /**
20 > * Returns if the given observable caused the change.
21 > */
22 > didChange<T, TChange>(observable: IObservableWithChange<T, TChange>): this is { change: TChange };
23 > }
24 >
25 > /**
26 > * Subscribes to and records changes and the last value of the given observables.
27 > * Don't use the key "changes", as it is reserved for the changes array!
28 > */
29 > export function recordChanges<TObs extends Record<any, IObservableWithChange<any, any>>>(obs: TObs):
30 IChangeTracker<{ [TKey in keyof TObs]: ReturnType<TObs[TKey]['get']> }
31 & { changes: readonly ({ [TKey in keyof TObs]: { key: TKey; change: TObs[TKey]['TChange'] } }[keyof TObs])[] }> {
56 };
57 }
59 > /**
60 > * Subscribes to and records changes and the last value of the given observables.
61 > * Don't use the key "changes", as it is reserved for the changes array!
62 > */
63 > export function recordChangesLazy<TObs extends Record<any, IObservableWithChange<any, any>>>(getObs: () => TObs):
64 IChangeTracker<{ [TKey in keyof TObs]: ReturnType<TObs[TKey]['get']> }
65 & { changes: readonly ({ [TKey in keyof TObs]: { key: TKey; change: TObs[TKey]['TChange'] } }[keyof TObs])[] }> {
src/vs/base/common/observableInternal/map.ts 35 covered LOC · 14 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- map.ts
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 > import { IObservable, ITransaction } from '../observable.js';
7 > import { observableValueOpts } from './observables/observableValueOpts.js';
8 >
9 >
10 > export class ObservableMap<K, V> implements Map<K, V> {
11 private readonly _data = new Map<K, V>();
12
14
15 readonly observable: IObservable<Map<K, V>> = this._obs;
16 > map.ts
17 > get size(): number {
18 return this._data.size;
19 }
20 > map.ts
21 > has(key: K): boolean {
22 return this._data.has(key);
23 }
24 > map.ts
25 > get(key: K): V | undefined {
26 return this._data.get(key);
27 }
28 > map.ts
29 > set(key: K, value: V, tx?: ITransaction): this {
30 const hadKey = this._data.has(key);
31 const oldValue = this._data.get(key);
36 return this;
37 }
38 > map.ts
39 > delete(key: K, tx?: ITransaction): boolean {
40 const result = this._data.delete(key);
41 if (result) {
44 return result;
45 }
46 > map.ts
47 > clear(tx?: ITransaction): void {
48 if (this._data.size > 0) {
49 this._data.clear();
51 }
52 }
53 > map.ts
54 > forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, thisArg?: unknown): void {
55 this._data.forEach((value, key, _map) => {
56 callbackfn.call(thisArg, value, key, this);
57 });
58 }
59 > map.ts
60 > *entries(): MapIterator<[K, V]> {
61 yield* this._data.entries();
62 }
63 > map.ts
64 > *keys(): MapIterator<K> {
65 yield* this._data.keys();
66 }
67 > map.ts
68 > *values(): MapIterator<V> {
69 yield* this._data.values();
70 }
71 > map.ts
72 > [Symbol.iterator](): MapIterator<[K, V]> {
73 return this.entries();
74 }
75 > map.ts
76 > get [Symbol.toStringTag](): string {
77 return 'ObservableMap';
78 }
79 > } map.ts
src/vs/base/test/common/timeTravelScheduler.ts 35 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- timeTravelScheduler.ts
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 > /**
7 > * @deprecated The contents of this file have moved to
8 > * `./virtualScheduling/index.js`. This re-export is kept for backwards
9 > * compatibility and will be removed once all callers have migrated.
10 > *
11 > * Notes for migration:
12 > * - `TimeTravelScheduler` is now {@link VirtualClock} (same constructor).
13 > * - `AsyncSchedulerProcessor` is now {@link VirtualTimeProcessor}; its
14 > * constructor takes an explicit {@link Embedding}, and {@link Run.options}
15 > * use `until` (a {@link TerminationPolicy}) instead of the implicit
16 > * "drain queue" behaviour. See {@link runWithFakedTimers} for a
17 > * drop-in helper.
18 > * - `originalGlobalValues` is now {@link realTimeApi}.
19 > */
20 >
21 > export {
22 > captureGlobalTimeApi,
23 > createLoggingTimeApi,
24 > createVirtualTimeApi,
25 > pushGlobalTimeApi,
26 > realTimeApi as originalGlobalValues,
27 > runWithFakedTimers,
28 > VirtualClock as TimeTravelScheduler,
29 > } from './virtualScheduling/index.js';
30 >
31 > export type {
32 > CreateVirtualTimeApiOptions,
33 > RunWithFakedTimersOptions,
34 > TimeApi,
35 > } from './virtualScheduling/index.js';
src/vs/platform/agentHost/common/annotationsUri.ts 35 covered LOC · 3 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- annotationsUri.ts
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 > import type { URI } from './state/sessionState.js';
7 >
8 > /**
9 > * Helpers for building / parsing the URI clients subscribe to in order to
10 > * receive an {@link import('./state/protocol/state.js').AnnotationsState}.
11 > *
12 > * Each session exposes exactly one annotations channel, nested under the
13 > * session URI namespace:
14 > *
15 > * <sessionUri>/annotations
16 > *
17 > * Keeping the annotations URI nested under the session URI lets the server
18 > * cleanly tear down a session's annotations when that session is disposed
19 > * (the reverse-lookup is just a string-prefix scan, mirroring changesets).
20 > */
21 >
22 > /** Marker injected into an annotations channel URI's path. */
23 > const ANNOTATIONS_PATH_SEGMENT = '/annotations';
24 >
25 > /** Returns the subscribable URI for a session's annotations channel. */
26 > export function buildAnnotationsUri(sessionUri: URI): URI {
27 return `${sessionUri}${ANNOTATIONS_PATH_SEGMENT}`;
28 }
30 > /**
31 > * Parses an annotations channel URI back into its owning `sessionUri`, or
32 > * returns `undefined` if `uri` is not an annotations channel URI.
33 > */
34 > export function parseAnnotationsUri(uri: URI): { sessionUri: URI } | undefined {
35 if (!uri.endsWith(ANNOTATIONS_PATH_SEGMENT)) {
36 return undefined;
42 return { sessionUri };
43 }
45 > /** Returns `true` iff `uri` is a session's annotations channel URI. */
46 > export function isAnnotationsUri(uri: URI): boolean {
47 return parseAnnotationsUri(uri) !== undefined;
48 }
src/vs/platform/instantiation/common/extensions.ts 35 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- extensions.ts
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 > import { SyncDescriptor } from './descriptors.js';
7 > import { BrandedService, ServiceIdentifier } from './instantiation.js';
8 >
9 > const _registry: [ServiceIdentifier<any>, SyncDescriptor<any>][] = [];
10 >
11 > export const enum InstantiationType {
12 > /**
13 > * Instantiate this service as soon as a consumer depends on it. _Note_ that this
14 > * is more costly as some upfront work is done that is likely not needed
15 > */
16 > Eager = 0,
17 >
18 > /**
19 > * Instantiate this service as soon as a consumer uses it. This is the _better_
20 > * way of registering a service.
21 > */
22 > Delayed = 1
23 > }
24 >
25 > export function registerSingleton<T, Services extends BrandedService[]>(id: ServiceIdentifier<T>, ctor: new (...services: Services) => T, supportsDelayedInstantiation: InstantiationType): void;
26 > export function registerSingleton<T, Services extends BrandedService[]>(id: ServiceIdentifier<T>, descriptor: SyncDescriptor<any>): void;
27 > export function registerSingleton<T, Services extends BrandedService[]>(id: ServiceIdentifier<T>, ctorOrDescriptor: { new(...services: Services): T } | SyncDescriptor<any>, supportsDelayedInstantiation?: boolean | InstantiationType): void {
28 > if (!(ctorOrDescriptor instanceof SyncDescriptor)) {
29 > ctorOrDescriptor = new SyncDescriptor<T>(ctorOrDescriptor as new (...args: unknown[]) => T, [], Boolean(supportsDelayedInstantiation));
30 > }
31 >
32 > _registry.push([id, ctorOrDescriptor]);
33 > }
34 >
35 > export function getSingletonServiceDescriptors(): [ServiceIdentifier<any>, SyncDescriptor<any>][] {
36 return _registry;
37 }
src/vs/platform/agentHost/node/agentHostDiscardChangesOperationProvider.ts 34 covered LOC · 6 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostDiscardChangesOperationProvider.ts
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 > import { Disposable, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';
7 > import { localize } from '../../../nls.js';
8 > import { IInstantiationService } from '../../instantiation/common/instantiation.js';
9 > import { ChangesetKind } from '../common/changesetUri.js';
10 > import type { IChangesetOperationContribution, IChangesetOperationContext, IChangesetOperationRegistry } from '../common/agentHostChangesetOperationService.js';
11 > import { ChangesetOperationScope, ChangesetOperationStatus, type ChangesetOperation } from '../common/state/sessionState.js';
12 > import { AgentHostDiscardChangesOperationHandler } from './agentHostDiscardChangesOperationHandler.js';
13 > import { AgentHostStateManager, IAgentHostStateManager } from './agentHostStateManager.js';
14 >
15 > export class AgentHostDiscardChangesOperationContribution extends Disposable implements IChangesetOperationContribution {
16 >
17 > constructor(
18 > @IAgentHostStateManager private readonly _stateManager: AgentHostStateManager, agentHostDiscardChangesOperationProvider.ts
19 > @IInstantiationService private readonly _instantiationService: IInstantiationService,
20 > ) {
21 > super();
22 > }
24 > registerHandlers(registry: IChangesetOperationRegistry): IDisposable {
25 > const store = new DisposableStore(); agentHostDiscardChangesOperationProvider.ts
26 > const getSessionState = (sessionKey: string) => this._stateManager.getSessionState(sessionKey);
27 > const handler = this._instantiationService.createInstance(AgentHostDiscardChangesOperationHandler, getSessionState);
28 > store.add(registry.registerChangesetOperationHandler(AgentHostDiscardChangesOperationHandler.OPERATION_DISCARD_CHANGES, handler));
29 >
30 > return store;
31 > }
33 > getOperations({ changesetKind, gitState }: IChangesetOperationContext): ChangesetOperation[] {
34 if (changesetKind !== ChangesetKind.Uncommitted || (gitState?.uncommittedChanges ?? 0) <= 0) {
35 return [];
src/vs/base/common/marshallingIds.ts 33 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- marshallingIds.ts
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 > export const enum MarshalledId {
7 > Uri = 1,
8 > Regexp,
9 > ScmResource,
10 > ScmResourceGroup,
11 > ScmProvider,
12 > CommentController,
13 > CommentThread,
14 > CommentThreadInstance,
15 > CommentThreadReply,
16 > CommentNode,
17 > CommentThreadNode,
18 > TimelineActionContext,
19 > NotebookCellActionContext,
20 > NotebookActionContext,
21 > TerminalContext,
22 > TestItemContext,
23 > Date,
24 > TestMessageMenuArgs,
25 > ChatViewContext,
26 > LanguageModelToolResult,
27 > LanguageModelTextPart,
28 > LanguageModelThinkingPart,
29 > LanguageModelPromptTsxPart,
30 > LanguageModelDataPart,
31 > AgentSessionContext,
32 > ChatResponsePullRequestPart,
33 > }
src/vs/base/test/common/virtualScheduling/recordingTimeApi.ts 33 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- recordingTimeApi.ts
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 > import { realTimeApi, TimeApi } from './timeApi.js';
7 > import { Trace, TraceContext } from './trace.js';
8 > import { EventSource } from './virtualClock.js';
9 >
10 > /**
11 > * One entry in a real-time trace recording. Structurally compatible with
12 > * `VirtualEvent` (and `ScheduledTaskLike` consumed by
13 > * `buildHistoryFromTasks`), so the same swimlane renderer can plot both.
14 > */
15 > export interface RecordedTimerEvent {
16 > readonly time: number;
17 > readonly source: EventSource;
18 > readonly trace?: Trace;
19 > }
20 >
21 > /**
22 > * Wrap the real host time API so every `setTimeout` / `setInterval` /
23 > * `requestAnimationFrame` call is tagged with a child {@link Trace} and
24 > * pushes a {@link RecordedTimerEvent} into `history` when the handler
25 > * actually runs.
26 > *
27 > * Handlers are invoked through {@link TraceContext.runAsHandler} so causal
28 > * chains carry across awaits inside a handler. Note: because each handler's
29 > * deferred trace-reset fires as its own real macrotask, attribution can
30 > * drift slightly when many handlers fire in quick succession — accurate
31 > * enough for diagnostics, not for assertions.
32 > */
33 > export function createRecordingRealTimeApi(history: RecordedTimerEvent[]): TimeApi {
34 const realSetTimeout = realTimeApi.setTimeout;
35
src/vs/platform/agentHost/common/diffComputeService.ts 33 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- diffComputeService.ts
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 > import { createDecorator } from '../../instantiation/common/instantiation.js';
7 >
8 > export interface IDiffCountResult {
9 > added: number;
10 > removed: number;
11 > }
12 >
13 > export const IDiffComputeService = createDecorator<IDiffComputeService>('diffComputeService');
14 >
15 > /** Default timeout for diff computation in milliseconds. */
16 > export const DEFAULT_DIFF_TIMEOUT_MS = 5000;
17 >
18 > /**
19 > * Service that computes line diff counts (added/removed) between two
20 > * text strings. Implementations may offload computation to a worker
21 > * thread to avoid blocking the main thread.
22 > */
23 > export interface IDiffComputeService {
24 > readonly _serviceBrand: undefined;
25 >
26 > /**
27 > * Computes line-level diff counts between two text strings.
28 > * @param original - The original text.
29 > * @param modified - The modified text to compare against the original.
30 > * @param timeoutMs - Maximum time in milliseconds before aborting. Defaults to {@link DEFAULT_DIFF_TIMEOUT_MS}.
31 > */
32 > computeDiffCounts(original: string, modified: string, timeoutMs?: number): Promise<IDiffCountResult>;
33 > }
src/vs/platform/agentHost/node/agentHostDiscardChangesOperationHandler.ts 33 covered LOC · 6 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostDiscardChangesOperationHandler.ts
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 > import { CancellationToken } from '../../../base/common/cancellation.js';
7 > import { basename } from '../../../base/common/resources.js';
8 > import { URI } from '../../../base/common/uri.js';
9 > import { localize } from '../../../nls.js';
10 > import { ChangesetKind, parseChangesetUri } from '../common/changesetUri.js';
11 > import { type IChangesetOperationHandler } from '../common/agentHostChangesetOperationService.js';
12 > import { ChangesetOperationTargetKind, type InvokeChangesetOperationParams, type InvokeChangesetOperationResult } from '../common/state/protocol/channels-changeset/commands.js';
13 > import { AHP_SESSION_NOT_FOUND, JsonRpcErrorCodes, ProtocolError } from '../common/state/sessionProtocol.js';
14 > import { type SessionState } from '../common/state/sessionState.js';
15 > import { ILogService } from '../../log/common/log.js';
16 > import { IAgentHostGitService } from '../common/agentHostGitService.js';
17 >
18 > export class AgentHostDiscardChangesOperationHandler implements IChangesetOperationHandler {
19 >
20 > public static readonly OPERATION_DISCARD_CHANGES = 'discard-changes';
21 >
22 > constructor(
23 > private readonly _getSessionState: (sessionKey: string) => SessionState | undefined, agentHostDiscardChangesOperationHandler.ts
24 > @IAgentHostGitService private readonly _agentHostGitService: IAgentHostGitService,
25 > @ILogService private readonly _logService: ILogService,
26 > ) { }
28 > async invoke(params: InvokeChangesetOperationParams, token: CancellationToken): Promise<InvokeChangesetOperationResult> {
29 const abortController = new AbortController();
30 if (token.isCancellationRequested) {
38 }
39 }
41 > private async _invoke(params: InvokeChangesetOperationParams, token: CancellationToken, _signal: AbortSignal): Promise<InvokeChangesetOperationResult> {
42 const parsed = parseChangesetUri(params.channel);
43 if (!parsed || parsed.kind !== ChangesetKind.Uncommitted) {
79 return { message: { markdown: localize('agentHost.changeset.discardChanges.discarded', "Discarded changes to `{0}`.", basename(resource)) } };
80 }
82 > private _throwIfCancelled(token: CancellationToken): void {
83 if (token.isCancellationRequested) {
84 throw new ProtocolError(JsonRpcErrorCodes.InternalError, localize('agentHost.changeset.discardChanges.cancelled', "Discard changes operation was cancelled."));
85 }
86 }
src/vs/platform/agentHost/node/localCommands/renameLocalCommand.ts 33 covered LOC · 5 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- renameLocalCommand.ts
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 > import { Disposable } from '../../../../base/common/lifecycle.js';
7 > import { generateUuid } from '../../../../base/common/uuid.js';
8 > import { localize } from '../../../../nls.js';
9 > import { ActionType } from '../../common/state/sessionActions.js';
10 > import { isAhpChatChannel, isDefaultChatUri, parseRequiredSessionUriFromChatUri, ResponsePartKind, type URI as ProtocolURI } from '../../common/state/sessionState.js';
11 > import { parseRenameCommand } from '../agentHostRenameCommand.js';
12 > import { ILocalChatCommand, ILocalChatCommandContext, ILocalChatCommandHandling, ILocalChatCommandRequest, LocalChatCommandRegistry } from './localChatCommand.js';
13 >
14 > /**
15 > * The generic `/rename [title]` command: renames the session (or an individual
16 > * peer chat) instead of forwarding the message to the agent SDK. Intercepted
17 > * for every agent-host session type.
18 > */
19 > export class RenameLocalCommand extends Disposable implements ILocalChatCommand {
20 >
21 > readonly name = 'rename';
22 > readonly recordsLocalTurn = true;
23 >
24 > constructor(private readonly _context: ILocalChatCommandContext) {
25 > super(); renameLocalCommand.ts
26 > }
28 > tryHandle(request: ILocalChatCommandRequest): ILocalChatCommandHandling | undefined {
29 const title = parseRenameCommand(request.text);
30 if (title === undefined) {
33 return { run: async () => this._run(request.turnChannel, request.turnId, title), suggestedTitle: title };
34 }
36 > private _run(channel: ProtocolURI, turnId: string, title: string): void {
37 if (title.length === 0) {
38 // `/rename` with no title: nothing to change; the dispatcher still
66 });
67 }
69 >
70 > LocalChatCommandRegistry.register(RenameLocalCommand);
src/vs/base/common/observableInternal/set.ts 32 covered LOC · 13 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- set.ts
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 > import { IObservable, ITransaction } from '../observable.js';
7 > import { observableValueOpts } from './observables/observableValueOpts.js';
8 >
9 > export class ObservableSet<T> implements Set<T> {
10
11 private readonly _data = new Set<T>();
14
15 readonly observable: IObservable<Set<T>> = this._obs;
16 > set.ts
17 > get size(): number {
18 return this._data.size;
19 }
20 > set.ts
21 > has(value: T): boolean {
22 return this._data.has(value);
23 }
24 > set.ts
25 > add(value: T, tx?: ITransaction): this {
26 const hadValue = this._data.has(value);
27 if (!hadValue) {
31 return this;
32 }
33 > set.ts
34 > delete(value: T, tx?: ITransaction): boolean {
35 const result = this._data.delete(value);
36 if (result) {
39 return result;
40 }
41 > set.ts
42 > clear(tx?: ITransaction): void {
43 if (this._data.size > 0) {
44 this._data.clear();
46 }
47 }
48 > set.ts
49 > forEach(callbackfn: (value: T, value2: T, set: Set<T>) => void, thisArg?: unknown): void {
50 this._data.forEach((value, value2, _set) => {
51 callbackfn.call(thisArg, value, value2, this);
52 });
53 }
54 > set.ts
55 > *entries(): SetIterator<[T, T]> {
56 for (const value of this._data) {
57 yield [value, value];
58 }
59 }
60 > set.ts
61 > *keys(): SetIterator<T> {
62 yield* this._data.keys();
63 }
64 > set.ts
65 > *values(): SetIterator<T> {
66 yield* this._data.values();
67 }
68 > set.ts
69 > [Symbol.iterator](): SetIterator<T> {
70 return this.values();
71 }
72 > set.ts
73 > get [Symbol.toStringTag](): string {
74 return 'ObservableSet';
75 }
76 > } set.ts
src/vs/platform/agentHost/common/agentModelByokMeta.ts 32 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentModelByokMeta.ts
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 > import type { SessionModelInfo } from './state/protocol/state.js';
7 > import type { IAgentModelInfo } from './agentService.js';
8 >
9 > /**
10 > * Well-known key for the renderer LM-service identifier of the BYOK model an
11 > * agent-host model is a copy of, carried under a model's open `_meta` bag (see
12 > * {@link IAgentModelInfo._meta} / {@link SessionModelInfo._meta}).
13 > *
14 > * A renderer BYOK model is registered under `<vendor>/<group>/<id>` (or `<vendor>/<id>`
15 > * without a configured group) — exactly the id the "Manage Models" view keys visibility
16 > * by. That identifier is not otherwise recoverable once the model round-trips the
17 > * agent-host bridge, so the renderer attaches it here so the chat model picker can honour
18 > * the model's visibility toggle.
19 > */
20 > export const BYOK_MODEL_IDENTIFIER_META_KEY = 'byokModelIdentifier';
21 >
22 > /**
23 > * Builds a `_meta` payload carrying the BYOK model identifier, or `undefined` when there
24 > * is none so callers can avoid attaching an empty `_meta` object.
25 > */
26 > export function createAgentModelByokMeta(modelIdentifier: string | undefined): Record<string, unknown> | undefined {
27 return modelIdentifier !== undefined ? { [BYOK_MODEL_IDENTIFIER_META_KEY]: modelIdentifier } : undefined;
28 }
30 > /**
31 > * Reads the BYOK model identifier from a model's open `_meta` bag, ignoring unrelated
32 > * keys and values of the wrong type.
33 > */
34 > export function readAgentModelByokIdentifier(model: IAgentModelInfo | SessionModelInfo): string | undefined {
35 const meta = model._meta;
36 if (!meta) {
src/vs/platform/files/common/io.ts 32 covered LOC · 4 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- io.ts
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 > import { VSBuffer } from '../../../base/common/buffer.js';
7 > import { CancellationToken } from '../../../base/common/cancellation.js';
8 > import { canceled } from '../../../base/common/errors.js';
9 > import { IDataTransformer, IErrorTransformer, WriteableStream } from '../../../base/common/stream.js';
10 > import { URI } from '../../../base/common/uri.js';
11 > import { localize } from '../../../nls.js';
12 > import { createFileSystemProviderError, ensureFileSystemProviderError, IFileReadStreamOptions, FileSystemProviderErrorCode, IFileSystemProviderWithOpenReadWriteCloseCapability } from './files.js';
13 >
14 > export interface ICreateReadStreamOptions extends IFileReadStreamOptions {
15 >
16 > /**
17 > * The size of the buffer to use before sending to the stream.
18 > */
19 > readonly bufferSize: number;
20 >
21 > /**
22 > * Allows to massage any possibly error that happens during reading.
23 > */
24 > readonly errorTransformer?: IErrorTransformer;
25 > }
26 >
27 > /**
28 > * A helper to read a file from a provider with open/read/close capability into a stream.
29 > */
30 export async function readFileIntoStream<T>(
31 provider: IFileSystemProviderWithOpenReadWriteCloseCapability,
54 }
55 }
56 > io.ts
57 async function doReadFileIntoStream<T>(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, target: WriteableStream<T>, transformer: IDataTransformer<VSBuffer, T>, options: ICreateReadStreamOptions, token: CancellationToken): Promise<void> {
58
114 }
115 }
116 > io.ts
117 function throwIfCancelled(token: CancellationToken): boolean {
118 if (token.isCancellationRequested) {
122 return true;
123 }
124 > io.ts
125 function throwIfTooLarge(totalBytesRead: number, options: ICreateReadStreamOptions): boolean {
126
src/vs/platform/agentHost/common/state/protocol/channels-resource-watch/reducer.ts 31 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- reducer.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > import { ActionType } from '../common/actions.js';
10 > import type { ResourceWatchState } from './state.js';
11 > import type { ResourceWatchAction } from '../action-origin.generated.js';
12 >
13 > /**
14 > * Pure reducer for resource-watch state. Handles every
15 > * {@link ResourceWatchAction} variant.
16 > *
17 > * Watches are intentionally event-pass-through: change events are
18 > * delivered via `resourceWatch/changed` actions but the reducer keeps no
19 > * history of them. The state therefore tracks only the watch descriptor,
20 > * which is set at subscription time and never mutates over the life of
21 > * the watch.
22 > *
23 > * The reducer uses an `if`/else shape rather than `switch`/`softAssertNever`
24 > * because `ResourceWatchAction` currently has a single variant — TypeScript
25 > * does not narrow single-variant discriminated unions to `never` after the
26 > * sole case branch, so the usual exhaustiveness pattern would not compile.
27 > * Unknown action types degrade gracefully (mirroring `softAssertNever`'s
28 > * runtime behaviour) so a client speaking an older protocol stays correct
29 > * if the server adds new `resourceWatch/*` actions in a future version.
30 > */
31 > export function resourceWatchReducer(state: ResourceWatchState, action: ResourceWatchAction, log?: (msg: string) => void): ResourceWatchState {
32 if (action.type === ActionType.ResourceWatchChanged) {
33 return state;
src/vs/platform/agentHost/node/agentHostSyncOperationHandler.ts 31 covered LOC · 5 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostSyncOperationHandler.ts
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 > import { CancellationToken } from '../../../base/common/cancellation.js';
7 > import { URI } from '../../../base/common/uri.js';
8 > import { localize } from '../../../nls.js';
9 > import { parseChangesetUri } from '../common/changesetUri.js';
10 > import type { InvokeChangesetOperationParams, InvokeChangesetOperationResult } from '../common/state/protocol/channels-changeset/commands.js';
11 > import { AHP_SESSION_NOT_FOUND, JsonRpcErrorCodes, ProtocolError } from '../common/state/sessionProtocol.js';
12 > import { readSessionGitState, type SessionState } from '../common/state/sessionState.js';
13 > import { ILogService } from '../../log/common/log.js';
14 > import { IChangesetOperationHandler } from '../common/agentHostChangesetOperationService.js';
15 > import { IAgentHostGitService } from '../common/agentHostGitService.js';
16 >
17 > export class AgentHostSyncOperationHandler implements IChangesetOperationHandler {
18 >
19 > public static readonly OPERATION_SYNC = 'sync';
20 >
21 > constructor(
22 > private readonly _getSessionState: (sessionKey: string) => SessionState | undefined, agentHostSyncOperationHandler.ts
23 > private readonly _onSynced: (sessionKey: string) => Promise<void>,
24 > @IAgentHostGitService private readonly _gitService: IAgentHostGitService,
25 > @ILogService private readonly _logService: ILogService,
26 > ) { }
28 > async invoke(params: InvokeChangesetOperationParams, token: CancellationToken): Promise<InvokeChangesetOperationResult> {
29 const parsed = parseChangesetUri(params.channel);
30 if (!parsed) {
72 return { message: { markdown: localize('agentHost.changeset.sync.synced', "Synced changes.") } };
73 }
75 > private _throwIfCancelled(token: CancellationToken): void {
76 if (token.isCancellationRequested) {
77 throw new ProtocolError(JsonRpcErrorCodes.InternalError, localize('agentHost.changeset.sync.cancelled', "Sync operation was cancelled."));
78 }
79 }
src/vs/platform/agentHost/common/state/protocol/channels-root/reducer.ts 30 covered LOC · 8 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- reducer.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > import { ActionType } from '../common/actions.js';
10 > import type { RootState } from './state.js';
11 > import type { RootAction } from '../action-origin.generated.js';
12 > import { softAssertNever } from '../common/reducer-helpers.js';
13 >
14 > /**
15 > * Pure reducer for root state. Handles all {@link RootAction} variants.
16 > */
17 > export function rootReducer(state: RootState, action: RootAction, log?: (msg: string) => void): RootState {
18 > switch (action.type) { reducer.ts
19 > case ActionType.RootAgentsChanged:
20 > return { ...state, agents: action.agents }; reducer.ts
21 > reducer.ts
22 > case ActionType.RootActiveSessionsChanged:
23 return { ...state, activeSessions: action.activeSessions };
24 > reducer.ts
25 > case ActionType.RootTerminalsChanged:
26 return { ...state, terminals: action.terminals };
27 > reducer.ts
28 > case ActionType.RootConfigChanged:
29 if (!state.config) {
30 return state;
37 },
38 };
39 > reducer.ts
40 > default:
41 softAssertNever(action, log);
42 return state;
43 > } reducer.ts
44 > }
src/vs/platform/instantiation/common/serviceCollection.ts 30 covered LOC · 10 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- serviceCollection.ts
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 > import { ServiceIdentifier } from './instantiation.js';
7 > import { SyncDescriptor } from './descriptors.js';
8 >
9 > export class ServiceCollection {
10 >
11 > private _entries = new Map<ServiceIdentifier<any>, any>();
12 >
13 > constructor(...entries: [ServiceIdentifier<any>, any][]) {
14 > for (const [id, service] of entries) { serviceCollection.ts
15 > this.set(id, service); serviceCollection.ts
16 > }
19 > set<T>(id: ServiceIdentifier<T>, instanceOrDescriptor: T | SyncDescriptor<T>): T | SyncDescriptor<T> {
20 > const result = this._entries.get(id); serviceCollection.ts
21 > this._entries.set(id, instanceOrDescriptor);
22 > return result;
23 > }
25 > has(id: ServiceIdentifier<any>): boolean {
26 return this._entries.has(id);
27 }
29 > get<T>(id: ServiceIdentifier<T>): T | SyncDescriptor<T> {
30 > return this._entries.get(id); serviceCollection.ts
31 > }
src/vs/base/common/errorMessage.ts 29 covered LOC · 6 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- errorMessage.ts
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 > import * as arrays from './arrays.js';
7 > import * as types from './types.js';
8 > import * as nls from '../../nls.js';
9 > import { IAction } from './actions.js';
10 >
11 function exceptionToErrorMessage(exception: any, verbose: boolean): string {
12 if (verbose && (exception.stack || exception.stacktrace)) {
16 return detectSystemErrorMessage(exception);
17 }
19 function stackToString(stack: string[] | string | undefined): string | undefined {
20 if (Array.isArray(stack)) {
24 return stack;
25 }
27 function detectSystemErrorMessage(exception: any): string {
28
39 return exception.message || nls.localize('error.defaultMessage', "An unknown error occurred. Please consult the log for more details.");
40 }
42 > /**
43 > * Tries to generate a human readable error message out of the error. If the verbose parameter
44 > * is set to true, the error message will include stacktrace details if provided.
45 > *
46 > * @returns A string containing the error message.
47 > */
48 > export function toErrorMessage(error: any = null, verbose: boolean = false): string {
49 if (!error) {
50 return nls.localize('error.defaultMessage', "An unknown error occurred. Please consult the log for more details.");
88 return nls.localize('error.defaultMessage', "An unknown error occurred. Please consult the log for more details.");
89 }
91 >
92 > export interface IErrorWithActions extends Error {
93 > actions: IAction[];
94 > }
95 >
96 > export function isErrorWithActions(obj: unknown): obj is IErrorWithActions {
97 const candidate = obj as IErrorWithActions | undefined;
98
99 return candidate instanceof Error && Array.isArray(candidate.actions);
100 }
102 > export function createErrorWithActions(messageOrError: string | Error, actions: IAction[]): IErrorWithActions {
103 let error: IErrorWithActions;
104 if (typeof messageOrError === 'string') {
src/vs/platform/agentHost/common/state/protocol/channels-annotations/reducer.ts 29 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- reducer.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > import { ActionType } from '../common/actions.js';
10 > import type { AnnotationEntry, Annotation, AnnotationsState } from './state.js';
11 > import type { AnnotationsAction } from '../action-origin.generated.js';
12 > import { softAssertNever } from '../common/reducer-helpers.js';
13 >
14 > /**
15 > * Pure reducer for annotations state. Handles every {@link AnnotationsAction}
16 > * variant.
17 > *
18 > * Per the spec, every annotations action is client-dispatchable; the reducer
19 > * runs identically on the client (optimistic, write-ahead) and the server. It
20 > * preserves the dispatch order of annotations (and of entries within an
21 > * annotation): new entries are appended; `*Set` actions with a matching id
22 > * replace in place, while actions whose target id is unknown are no-ops
23 > * (mirroring `changeset/fileRemoved` semantics). The single-entry
24 > * minimum invariant is enforced by producers, not the reducer — removing an
25 > * annotation's last entry via {@link AnnotationsEntryRemovedAction} (instead
26 > * of {@link AnnotationsRemovedAction}) would leave an empty annotation,
27 > * which is observable but not catastrophic.
28 > */
29 > export function annotationsReducer(state: AnnotationsState, action: AnnotationsAction, log?: (msg: string) => void): AnnotationsState {
30 switch (action.type) {
31 case ActionType.AnnotationsSet: {
src/vs/platform/agentHost/common/state/protocol/common/reducer-helpers.ts 29 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- reducer-helpers.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > import { IS_CLIENT_DISPATCHABLE, type RootAction, type ClientRootAction, type SessionAction, type ClientSessionAction, type TerminalAction, type ClientTerminalAction, type ChangesetAction, type ClientChangesetAction, type AnnotationsAction, type ClientAnnotationsAction } from '../action-origin.generated.js';
10 >
11 > /**
12 > * Soft assertion for exhaustiveness checking. Place in the `default` branch of
13 > * a switch on a discriminated union so the compiler errors when a new variant
14 > * is added but not handled.
15 > *
16 > * At runtime, logs a warning instead of throwing so that forward-compatible
17 > * clients receiving unknown actions from a newer server degrade gracefully.
18 > */
19 > export function softAssertNever(value: never, log?: (msg: string) => void): void {
20 const msg = `Unhandled action type: ${JSON.stringify(value)}`;
21 (log ?? console.warn)(msg);
22 }
24 > // ─── Dispatch Validation ─────────────────────────────────────────────────────
25 >
26 > /**
27 > * Type guard that checks whether an action may be dispatched by a client.
28 > *
29 > * Servers SHOULD call this to validate incoming `dispatchAction` requests
30 > * and reject any action the client is not allowed to originate.
31 > */
32 > export function isClientDispatchable(action: RootAction | SessionAction | TerminalAction | ChangesetAction | AnnotationsAction): action is ClientRootAction | ClientSessionAction | ClientTerminalAction | ClientChangesetAction | ClientAnnotationsAction {
33 return IS_CLIENT_DISPATCHABLE[action.type];
34 }
src/vs/platform/agentHost/node/agentHostManagementService.ts 29 covered LOC · 10 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostManagementService.ts
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 > import { URI } from '../../../base/common/uri.js';
7 > import { IAgentCreateChatOptions, IAgentCreateSessionConfig, IAgentHostInspectInfo, IAgentHostManagedSettingsDiagnostics, IAgentHostManagementService, IAgentHostNetworkDiagnosticsInfo, IAgentHostNetworkFetchResult, IAgentHostSocketInfo, IAgentService, IConnectionTrackerService } from '../common/agentService.js';
8 >
9 > export class AgentHostManagementService implements IAgentHostManagementService {
10 > declare readonly _serviceBrand: undefined;
11 >
12 > constructor(
13 private readonly _agentService: IAgentService,
14 private readonly _connectionTrackerService: IConnectionTrackerService,
15 ) { }
17 > createSessionWithExtensions(config: IAgentCreateSessionConfig): Promise<URI> {
18 return this._agentService.createSession(config);
19 }
21 > createChatWithExtensions(session: URI, chat: URI, options: IAgentCreateChatOptions): Promise<void> {
22 return this._agentService.createChat(session, chat, options);
23 }
25 > shutdown(): Promise<void> {
26 return this._agentService.shutdown();
27 }
29 > getNetworkDiagnosticsInfo(): Promise<IAgentHostNetworkDiagnosticsInfo> {
30 return this._agentService.getNetworkDiagnosticsInfo();
31 }
33 > getManagedSettingsDiagnostics(): Promise<readonly IAgentHostManagedSettingsDiagnostics[]> {
34 return this._agentService.getManagedSettingsDiagnostics();
35 }
37 > diagnosticsFetch(url: string): Promise<IAgentHostNetworkFetchResult> {
38 return this._agentService.diagnosticsFetch(url);
39 }
41 > startWebSocketServer(): Promise<IAgentHostSocketInfo> {
42 return this._connectionTrackerService.startWebSocketServer();
43 }
45 > getInspectInfo(tryEnable: boolean): Promise<IAgentHostInspectInfo | undefined> {
46 return this._connectionTrackerService.getInspectInfo(tryEnable);
47 }
src/vs/base/common/normalization.ts 27 covered LOC · 5 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- normalization.ts
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 > import { LRUCache } from './map.js';
7 >
8 > const nfcCache = new LRUCache<string, string>(10000); // bounded to 10000 elements
9 > export function normalizeNFC(str: string): string {
10 return normalize(str, 'NFC', nfcCache);
11 }
13 > const nfdCache = new LRUCache<string, string>(10000); // bounded to 10000 elements
14 > export function normalizeNFD(str: string): string {
15 return normalize(str, 'NFD', nfdCache);
16 }
18 > const nonAsciiCharactersPattern = /[^\u0000-\u0080]/;
19 function normalize(str: string, form: string, normalizedCache: LRUCache<string, string>): string {
20 if (!str) {
39 return res;
40 }
42 > /**
43 > * Attempts to normalize the string to Unicode base format (NFD -> remove accents -> lower case).
44 > * When original string contains accent characters directly, only lower casing will be performed.
45 > * This is done so as to keep the string length the same and not affect indices.
46 > *
47 > * @see https://stackoverflow.com/questions/990904/remove-accents-diacritics-in-a-string-in-javascript/37511463#37511463
48 > */
49 > export const tryNormalizeToBase: (str: string) => string = function () {
50 > const cache = new LRUCache<string, string>(10000); // bounded to 10000 elements
51 > const accentsRegex = /[\u0300-\u036f]/g;
52 > return function (str: string): string {
53 const cached = cache.get(str);
54 if (cached) {
src/vs/base/common/observableInternal/experimental/utils.ts 26 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- utils.ts
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 > import { IObservable, IReader } from '../base.js';
7 > import { BugIndicatingError, DisposableStore } from '../commonFacade/deps.js';
8 > import { DebugOwner, getDebugName, DebugNameData } from '../debugName.js';
9 > import { observableFromEvent } from '../observables/observableFromEvent.js';
10 > import { autorunOpts } from '../reactions/autorun.js';
11 > import { derivedObservableWithCache } from '../utils/utils.js';
12 >
13 > /**
14 > * Creates an observable that has the latest changed value of the given observables.
15 > * Initially (and when not observed), it has the value of the last observable.
16 > * When observed and any of the observables change, it has the value of the last changed observable.
17 > * If multiple observables change in the same transaction, the last observable wins.
18 > */
19 > export function latestChangedValue<T extends IObservable<any>[]>(owner: DebugOwner, observables: T): IObservable<ReturnType<T[number]['get']>> {
20 if (observables.length === 0) {
21 throw new BugIndicatingError();
50 return result;
51 }
52 > utils.ts
53 > /**
54 > * Works like a derived.
55 > * However, if the value is not undefined, it is cached and will not be recomputed anymore.
56 > * In that case, the derived will unsubscribe from its dependencies.
57 > */
58 > export function derivedConstOnceDefined<T>(owner: DebugOwner, fn: (reader: IReader) => T): IObservable<T | undefined> {
59 return derivedObservableWithCache<T | undefined>(owner, (reader, lastValue) => lastValue ?? fn(reader));
60 }
src/vs/base/common/observableInternal/observables/constObservable.ts 26 covered LOC · 9 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- constObservable.ts
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 > import { IObservable, IObserver, IObservableWithChange } from '../base.js';
7 > import { ConvenientObservable } from './baseObservable.js';
8 >
9 > /**
10 > * Represents an efficient observable whose value never changes.
11 > */
12 >
13 > export function constObservable<T>(value: T): IObservable<T> {
14 return new ConstObservable(value);
15 }
16 > class ConstObservable<T> extends ConvenientObservable<T, void> { constObservable.ts
17 > constructor(private readonly value: T) {
18 super();
19 }
21 > public override get debugName(): string {
22 return this.toString();
23 }
25 > public get(): T {
26 return this.value;
27 }
28 > public addObserver(observer: IObserver): void { constObservable.ts
29 // NO OP
30 }
31 > public removeObserver(observer: IObserver): void { constObservable.ts
32 // NO OP
33 }
35 > override log(): IObservableWithChange<T, void> {
36 return this;
37 }
39 > override toString(): string {
40 return `Const: ${this.value}`;
41 }
src/vs/base/common/stopwatch.ts 25 covered LOC · 6 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- stopwatch.ts
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 > declare const globalThis: { performance: { now(): number } };
7 > const performanceNow = globalThis.performance.now.bind(globalThis.performance);
8 >
9 > export class StopWatch {
10 >
11 > private _startTime: number;
12 > private _stopTime: number;
13 >
14 > private readonly _now: () => number;
15 >
16 > public static create(highResolution?: boolean): StopWatch {
17 return new StopWatch(highResolution);
18 }
20 > constructor(highResolution?: boolean) {
21 this._now = highResolution === false ? Date.now : performanceNow;
22 this._startTime = this._now();
23 this._stopTime = -1;
24 }
26 > public stop(): void {
27 this._stopTime = this._now();
28 }
30 > public reset(): void {
31 this._startTime = this._now();
32 this._stopTime = -1;
33 }
35 > public elapsed(): number {
36 if (this._stopTime !== -1) {
37 return this._stopTime - this._startTime;
39 return this._now() - this._startTime;
40 }
41 > } stopwatch.ts
src/vs/base/common/uuid.ts 25 covered LOC · 3 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- uuid.ts
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 >
7 > const _UUIDPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
8 >
9 > export function isUUID(value: string): boolean {
10 return _UUIDPattern.test(value);
11 }
12 > uuid.ts
13 > export const generateUuid = (function (): () => string {
14 >
15 > // use `randomUUID` if possible
16 > if (typeof crypto.randomUUID === 'function') {
17 > // see https://developer.mozilla.org/en-US/docs/Web/API/Window/crypto
18 > // > Although crypto is available on all windows, the returned Crypto object only has one
19 > // > usable feature in insecure contexts: the getRandomValues() method.
20 > // > In general, you should use this API only in secure contexts.
21 >
22 > return crypto.randomUUID.bind(crypto);
23 > }
24
25 // prep-work
63 return result;
64 };
65 > })(); uuid.ts
66 >
67 > /** Namespace should be 3 letters, e.g. `abc-<uuid>`. */
68 > export function prefixedUuid(namespace: string): string {
69 return `${namespace}-${generateUuid()}`;
70 }
src/vs/platform/agentHost/node/appNodeModules.ts 25 covered LOC · 5 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- appNodeModules.ts
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 > import { AppResourcePath, nodeModulesAsarUnpackedPath, nodeModulesPath } from '../../../base/common/network.js';
7 > import product from '../../product/common/product.js';
8 >
9 > function hasUnpackedNodeModulesArchive(): boolean { appNodeModules.ts
10 > return !!process.versions['electron'] && !!product.commit && !process.env['VSCODE_DEV'];
11 > }
13 > /**
14 > * The {@link AppResourcePath} of the `node_modules` root that actually holds VS
15 > * Code's bundled modules, suitable for passing to `FileAccess.asFileUri`.
16 > */
17 > export function getAppNodeModulesPath(): AppResourcePath {
18 > return hasUnpackedNodeModulesArchive() ? nodeModulesAsarUnpackedPath : nodeModulesPath; appNodeModules.ts
19 > }
21 > /**
22 > * The bare directory name (`node_modules` or `node_modules.asar.unpacked`) of the
23 > * resolved root, for callers that build paths from an app root themselves.
24 > */
25 > export function getAppNodeModulesDirName(): 'node_modules' | 'node_modules.asar.unpacked' {
26 return hasUnpackedNodeModulesArchive() ? 'node_modules.asar.unpacked' : 'node_modules';
27 }
src/vs/base/common/codiconsUtil.ts 24 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- codiconsUtil.ts
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 > import { ThemeIcon } from './themables.js';
6 > import { isString } from './types.js';
7 >
8 >
9 > const _codiconFontCharacters: { [id: string]: number } = Object.create(null);
10 >
11 > export function register(id: string, fontCharacter: number | string): ThemeIcon {
12 > if (isString(fontCharacter)) {
13 > const val = _codiconFontCharacters[fontCharacter];
14 > if (val === undefined) {
15 throw new Error(`${id} references an unknown codicon: ${fontCharacter}`);
16 }
17 > fontCharacter = val; codiconsUtil.ts
18 > }
19 > _codiconFontCharacters[id] = fontCharacter;
20 > return { id };
21 > }
22 >
23 > /**
24 > * Only to be used by the iconRegistry.
25 > */
26 > export function getCodiconFontCharacters(): { [id: string]: number } {
27 return _codiconFontCharacters;
28 }
src/vs/base/common/observableInternal/logging/debugger/utils.ts 24 covered LOC · 8 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- utils.ts
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 > import { IDisposable } from '../../../lifecycle.js';
7 >
8 > export class Debouncer implements IDisposable {
9 private _timeout: Timeout | undefined = undefined;
10 > utils.ts
11 > public debounce(fn: () => void, timeoutMs: number): void {
12 if (this._timeout !== undefined) {
13 clearTimeout(this._timeout);
18 }, timeoutMs);
19 }
20 > utils.ts
21 > dispose(): void {
22 if (this._timeout !== undefined) {
23 clearTimeout(this._timeout);
24 }
25 }
26 > } utils.ts
27 >
28 > export class Throttler implements IDisposable {
29 private _timeout: Timeout | undefined = undefined;
30 > utils.ts
31 > public throttle(fn: () => void, timeoutMs: number): void {
32 if (this._timeout === undefined) {
33 this._timeout = setTimeout(() => {
37 }
38 }
39 > utils.ts
40 > dispose(): void {
41 if (this._timeout !== undefined) {
42 clearTimeout(this._timeout);
43 }
44 }
45 > } utils.ts
46 >
47 > export function deepAssign<T>(target: T, source: T): void {
48 for (const key in source) {
49 if (!!target[key] && typeof target[key] === 'object' && !!source[key] && typeof source[key] === 'object') {
54 }
55 }
56 > utils.ts
57 > export function deepAssignDeleteNulls<T>(target: T, source: T): void {
58 for (const key in source) {
59 if (source[key] === null) {
src/vs/base/common/observableInternal/utils/utilsCancellation.ts 24 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- utilsCancellation.ts
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 > import { IReader, IObservable } from '../base.js';
7 > import { DebugOwner, DebugNameData } from '../debugName.js';
8 > import { CancellationError, CancellationToken, CancellationTokenSource } from '../commonFacade/cancellation.js';
9 > import { strictEquals } from '../commonFacade/deps.js';
10 > import { autorun } from '../reactions/autorun.js';
11 > import { Derived } from '../observables/derivedImpl.js';
12 > import { DebugLocation } from '../debugLocation.js';
13 >
14 > /**
15 > * Resolves the promise when the observables state matches the predicate.
16 > */
17 > export function waitForState<T>(observable: IObservable<T | null | undefined>): Promise<T>;
18 > export function waitForState<T, TState extends T>(observable: IObservable<T>, predicate: (state: T) => state is TState, isError?: (state: T) => boolean | unknown | undefined, cancellationToken?: CancellationToken): Promise<TState>;
19 > export function waitForState<T>(observable: IObservable<T>, predicate: (state: T) => boolean, isError?: (state: T) => boolean | unknown | undefined, cancellationToken?: CancellationToken): Promise<T>;
20 > export function waitForState<T>(observable: IObservable<T>, predicate?: (state: T) => boolean, isError?: (state: T) => boolean | unknown | undefined, cancellationToken?: CancellationToken): Promise<T> {
21 if (!predicate) {
22 predicate = state => state !== null && state !== undefined;
69 });
70 }
72 > export function derivedWithCancellationToken<T>(computeFn: (reader: IReader, cancellationToken: CancellationToken) => T): IObservable<T>;
73 > export function derivedWithCancellationToken<T>(owner: object, computeFn: (reader: IReader, cancellationToken: CancellationToken) => T): IObservable<T>;
74 > export function derivedWithCancellationToken<T>(computeFnOrOwner: ((reader: IReader, cancellationToken: CancellationToken) => T) | object, computeFnOrUndefined?: ((reader: IReader, cancellationToken: CancellationToken) => T)): IObservable<T> {
75 let computeFn: (reader: IReader, store: CancellationToken) => T;
76 let owner: DebugOwner;
src/vs/platform/agentHost/common/state/protocol/channels-changeset/reducer.ts 24 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- reducer.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > import { ActionType } from '../common/actions.js';
10 > import { ChangesetStatus, ChangesetOperationStatus, type ChangesetState, type ChangesetFile, type ChangesetOperation } from './state.js';
11 > import type { ChangesetAction } from '../action-origin.generated.js';
12 > import { softAssertNever } from '../common/reducer-helpers.js';
13 >
14 > /**
15 > * Pure reducer for changeset state. Handles all {@link ChangesetAction}
16 > * variants.
17 > *
18 > * The reducer preserves a stable file order by appending new files via
19 > * {@link ActionType.ChangesetFileSet} when the id is unknown, and replacing in
20 > * place when it matches an existing entry. Per-file review lives on
21 > * {@link ChangesetFile.reviewed} and is toggled (per file, in batches) by the
22 > * client-dispatchable {@link ActionType.ChangesetFilesReviewChanged}.
23 > */
24 > export function changesetReducer(state: ChangesetState, action: ChangesetAction, log?: (msg: string) => void): ChangesetState {
25 switch (action.type) {
26 case ActionType.ChangesetStatusChanged: {
src/vs/platform/chat/common/aiAgentEnv.ts 24 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- aiAgentEnv.ts
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 > /**
7 > * Cross-vendor convention env var that announces which AI agent is driving a
8 > * process. Child processes inherit it; `gh` in particular reads it and reports
9 > * the value in its `User-Agent`, which lets downstream activity be attributed to
10 > * the originating AI experience without touching the user's global environment.
11 > */
12 > export const AiAgentEnvVar = 'AI_AGENT';
13 >
14 > /**
15 > * The value VS Code announces for processes it spawns on behalf of an agent
16 > * session, following the `github_copilot_<surface>` form asked of every GitHub
17 > * Copilot surface. Must stay stable: it is a wire contract with the reporting
18 > * pipeline, and `gh` only accepts values matching `[a-zA-Z0-9_-]+`.
19 > *
20 > * This must be set for *every* process an agent session spawns — the local
21 > * (in-workbench) harness terminal tool, the agent host process and every agent
22 > * SDK subprocess it launches — otherwise the surface is under-counted.
23 > */
24 > export const AiAgentEnvValue = 'github_copilot_vscode_agent';
src/vs/platform/agentHost/node/agentHostChangesetSubscriptionService.ts 23 covered LOC · 7 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostChangesetSubscriptionService.ts
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 > import type { URI as ProtocolURI } from '../common/state/sessionState.js';
7 > import { IAgentHostChangesetSubscriptionService } from '../common/agentHostChangesetSubscriptionService.js';
8 >
9 > const EMPTY_SUBSCRIPTIONS: ReadonlySet<ProtocolURI> = new Set<ProtocolURI>();
10 >
11 > export class AgentHostChangesetSubscriptionService implements IAgentHostChangesetSubscriptionService {
12 > declare readonly _serviceBrand: undefined; agentHostChangesetSubscriptionService.ts
13 >
14 > private readonly _subscriptions = new Map<ProtocolURI, Set<ProtocolURI>>();
16 > getSessionSubscriptions(session: ProtocolURI): ReadonlySet<ProtocolURI> {
17 return this._subscriptions.get(session) ?? EMPTY_SUBSCRIPTIONS;
18 }
20 > addSubscription(session: ProtocolURI, changeset: ProtocolURI): void {
21 let subscriptions = this._subscriptions.get(session);
22 if (!subscriptions) {
26 subscriptions.add(changeset);
27 }
29 > removeSubscription(session: ProtocolURI, changeset: ProtocolURI): void {
30 const subscriptions = this._subscriptions.get(session);
31 if (!subscriptions) {
src/vs/platform/telemetry/common/telemetryLogAppender.ts 22 covered LOC · 4 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- telemetryLogAppender.ts
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 > import { Disposable } from '../../../base/common/lifecycle.js';
7 > import { localize } from '../../../nls.js';
8 > import { IEnvironmentService } from '../../environment/common/environment.js';
9 > import { ILogger, ILoggerService } from '../../log/common/log.js';
10 > import { IProductService } from '../../product/common/productService.js';
11 > import { ITelemetryAppender, TelemetryLogGroup, isLoggingOnly, telemetryLogId, validateTelemetryData } from './telemetryUtils.js';
12 >
13 > export class TelemetryLogAppender extends Disposable implements ITelemetryAppender {
14 >
15 > private readonly logger: ILogger;
16 >
17 > constructor(
18 private readonly prefix: string,
19 remote: boolean,
src/vs/platform/agentHost/node/agentHostBangCommand.ts 21 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostBangCommand.ts
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 > /**
7 > * The leading character that marks a chat message as a terminal command.
8 > */
9 > export const BANG_COMMAND_PREFIX = '!';
10 >
11 > /**
12 > * Parses a leading `!<command>` at the very start of `prompt`.
13 > *
14 > * Like {@link parseRenameCommand}, the marker must be at position 0 (no leading
15 > * whitespace). A lone `!` or `!` followed only by whitespace is not treated as
16 > * a bang command — the caller should forward such messages normally.
17 > *
18 > * Returns the trimmed command string when the prompt is a bang command, or
19 > * `undefined` when it is not.
20 > */
21 > export function parseBangCommand(prompt: string): string | undefined {
22 if (!prompt.startsWith(BANG_COMMAND_PREFIX)) {
23 return undefined;
src/vs/platform/instantiation/common/descriptors.ts 21 covered LOC · 3 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- descriptors.ts
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 > export class SyncDescriptor<T> {
7 >
8 > readonly ctor: any;
9 > readonly staticArguments: unknown[];
10 > readonly supportsDelayedInstantiation: boolean;
11 >
12 > constructor(ctor: new (...args: any[]) => T, staticArguments: unknown[] = [], supportsDelayedInstantiation: boolean = false) {
13 > this.ctor = ctor; descriptors.ts
14 > this.staticArguments = staticArguments;
15 > this.supportsDelayedInstantiation = supportsDelayedInstantiation;
16 > }
18 >
19 > export interface SyncDescriptor0<T> {
20 > readonly ctor: new () => T;
21 > }
src/vs/platform/networkFilter/common/settings.ts 21 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- settings.ts
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 > /**
7 > * Setting IDs for agent network domain filtering.
8 > */
9 > export const enum AgentNetworkDomainSettingId {
10 > NetworkFilter = 'chat.agent.networkFilter',
11 > AllowedNetworkDomains = 'chat.agent.allowedNetworkDomains',
12 > DeniedNetworkDomains = 'chat.agent.deniedNetworkDomains',
13 >
14 > // Deprecated: renamed from sandbox-scoped to agent-scoped
15 > DeprecatedSandboxAllowedNetworkDomains = 'chat.agent.sandbox.allowedNetworkDomains',
16 > DeprecatedSandboxDeniedNetworkDomains = 'chat.agent.sandbox.deniedNetworkDomains',
17 >
18 > // Deprecated: older names before the sandbox rename
19 > DeprecatedOldAllowedNetworkDomains = 'chat.agent.sandboxNetwork.allowedDomains',
20 > DeprecatedOldDeniedNetworkDomains = 'chat.agent.sandboxNetwork.deniedDomains',
21 > }
src/vs/platform/agentHost/common/state/sessionReducers.ts 20 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- sessionReducers.ts
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 > // Re-exports the protocol reducers and adds VS Code-specific helpers.
7 > // The actual reducer logic lives in the auto-generated protocol layer.
8 >
9 > // Re-export reducers from the protocol layer
10 > export { rootReducer, sessionReducer, chatReducer, changesetReducer, annotationsReducer, softAssertNever, isClientDispatchable } from './protocol/reducers.js';
11 >
12 > import { readToolCallMeta, type ToolKind } from '../meta/agentToolCallMeta.js';
13 > import type { ICompletedToolCall, ToolCallState } from './sessionState.js';
14 >
15 > /**
16 > * Extracts the VS Code-specific `toolKind` hint from a tool call's `_meta`
17 > * bag. This is not part of the protocol and is injected by the agent adapter
18 > * (e.g. `copilotEventMapper`).
19 > */
20 > export function getToolKind(tc: ToolCallState | ICompletedToolCall): ToolKind | undefined {
21 return readToolCallMeta(tc).toolKind;
22 }
src/vs/platform/terminal/common/environmentVariableShared.ts 20 covered LOC · 6 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- environmentVariableShared.ts
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 > import { IEnvironmentVariableCollectionDescription, IEnvironmentVariableCollection, IEnvironmentVariableMutator, ISerializableEnvironmentDescriptionMap, ISerializableEnvironmentVariableCollection, ISerializableEnvironmentVariableCollections } from './environmentVariable.js';
7 >
8 > // This file is shared between the renderer and extension host
9 >
10 > export function serializeEnvironmentVariableCollection(collection: ReadonlyMap<string, IEnvironmentVariableMutator>): ISerializableEnvironmentVariableCollection {
11 return [...collection.entries()];
12 }
14 > export function serializeEnvironmentDescriptionMap(descriptionMap: ReadonlyMap<string, IEnvironmentVariableCollectionDescription> | undefined): ISerializableEnvironmentDescriptionMap {
15 return descriptionMap ? [...descriptionMap.entries()] : [];
16 }
18 > export function deserializeEnvironmentVariableCollection(
19 serializedCollection: ISerializableEnvironmentVariableCollection
20 ): Map<string, IEnvironmentVariableMutator> {
21 return new Map<string, IEnvironmentVariableMutator>(serializedCollection);
22 }
24 > export function deserializeEnvironmentDescriptionMap(
25 serializableEnvironmentDescription: ISerializableEnvironmentDescriptionMap | undefined
26 ): Map<string, IEnvironmentVariableCollectionDescription> {
27 return new Map<string, IEnvironmentVariableCollectionDescription>(serializableEnvironmentDescription ?? []);
28 }
30 > export function serializeEnvironmentVariableCollections(collections: ReadonlyMap<string, IEnvironmentVariableCollection>): ISerializableEnvironmentVariableCollections {
31 return Array.from(collections.entries()).map(e => {
32 return [e[0], serializeEnvironmentVariableCollection(e[1].map), serializeEnvironmentDescriptionMap(e[1].descriptionMap)];
33 });
34 }
36 > export function deserializeEnvironmentVariableCollections(
37 serializedCollection: ISerializableEnvironmentVariableCollections
38 ): Map<string, IEnvironmentVariableCollection> {
src/vs/base/common/observableInternal/utils/valueWithChangeEvent.ts 19 covered LOC · 4 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- valueWithChangeEvent.ts
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 > import { IObservable } from '../base.js';
7 > import { Event, IValueWithChangeEvent } from '../commonFacade/deps.js';
8 > import { DebugOwner } from '../debugName.js';
9 > import { observableFromEvent } from '../observables/observableFromEvent.js';
10 >
11 > export class ValueWithChangeEventFromObservable<T> implements IValueWithChangeEvent<T> {
12 > constructor(public readonly observable: IObservable<T>) {
13 }
15 > get onDidChange(): Event<void> {
16 return Event.fromObservableLight(this.observable);
17 }
19 > get value(): T {
20 return this.observable.get();
21 }
23 >
24 > export function observableFromValueWithChangeEvent<T>(owner: DebugOwner, value: IValueWithChangeEvent<T>): IObservable<T> {
25 if (value instanceof ValueWithChangeEventFromObservable) {
26 return value.observable;
src/vs/base/node/shell.ts 19 covered LOC · 3 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- shell.ts
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 > import { userInfo } from 'os';
7 > import * as platform from '../common/platform.js';
8 > import { getFirstAvailablePowerShellInstallation } from './powershell.js';
9 > import * as processes from './processes.js';
10 >
11 > /**
12 > * Gets the detected default shell for the _system_, not to be confused with VS Code's _default_
13 > * shell that the terminal uses by default.
14 > * @param os The platform to detect the shell of.
15 > */
16 export async function getSystemShell(os: platform.OperatingSystem, env: platform.IProcessEnvironment): Promise<string> {
17 if (os === platform.OperatingSystem.Windows) {
25 return getSystemShellUnixLike(os, env);
26 }
27 > shell.ts
28 > let _TERMINAL_DEFAULT_SHELL_UNIX_LIKE: string | null = null;
29 function getSystemShellUnixLike(os: platform.OperatingSystem, env: platform.IProcessEnvironment): string {
30 // Only use $SHELL for the current OS
61 return _TERMINAL_DEFAULT_SHELL_UNIX_LIKE;
62 }
63 > shell.ts
64 > let _TERMINAL_DEFAULT_SHELL_WINDOWS: string | null = null;
65 async function getSystemShellWindows(): Promise<string> {
66 if (!_TERMINAL_DEFAULT_SHELL_WINDOWS) {
src/vs/platform/agentHost/node/shared/persistSessionMetadata.ts 19 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- persistSessionMetadata.ts
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 > import { URI } from '../../../../base/common/uri.js';
7 > import { ILogService } from '../../../log/common/log.js';
8 > import type { ISessionDataService } from '../../common/sessionDataService.js';
9 >
10 > /**
11 > * Fire-and-forget persistence of a single session-metadata key/value pair to a
12 > * session's database. Opens the database, writes the value, and disposes the
13 > * handle; failures are logged, not thrown.
14 > *
15 > * Used for host-owned fields that must survive restart (custom titles, isRead /
16 > * isArchived flags, merged config values, …). Shared so callers do not each
17 > * re-implement the open/write/dispose dance.
18 > */
19 > export function persistSessionMetadata(sessionDataService: ISessionDataService, logService: ILogService, session: string, key: string, value: string): void {
20 const ref = sessionDataService.openDatabase(URI.parse(session));
21 ref.object.setMetadata(key, value).catch(err => {
src/vs/base/common/jsonEdit.ts 18 covered LOC · 5 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- jsonEdit.ts
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 > import { findNodeAtLocation, JSONPath, Node, ParseError, parseTree, Segment } from './json.js';
7 > import { Edit, format, FormattingOptions, isEOL } from './jsonFormatter.js';
8 >
9 >
10 > export function removeProperty(text: string, path: JSONPath, formattingOptions: FormattingOptions): Edit[] {
11 return setProperty(text, path, undefined, formattingOptions);
12 }
14 > export function setProperty(text: string, originalPath: JSONPath, value: unknown, formattingOptions: FormattingOptions, getInsertionIndex?: (properties: string[]) => number): Edit[] {
15 const path = originalPath.slice();
16 const errors: ParseError[] = [];
119 }
120 }
121 > jsonEdit.ts
122 > export function withFormatting(text: string, edit: Edit, formattingOptions: FormattingOptions): Edit[] {
123 // apply the edit
124 let newText = applyEdit(text, edit);
150 return [{ offset: begin, length: editLength, content: newText.substring(begin, end) }];
151 }
152 > jsonEdit.ts
153 > export function applyEdit(text: string, edit: Edit): string {
154 return text.substring(0, edit.offset) + edit.content + text.substring(edit.offset + edit.length);
155 }
156 > jsonEdit.ts
157 > export function applyEdits(text: string, edits: Edit[]): string {
158 const sortedEdits = edits.slice(0).sort((a, b) => {
159 const diff = a.offset - b.offset;
src/vs/base/common/observableInternal/logging/debugger/debuggerRpc.ts 17 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- debuggerRpc.ts
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 > import { ChannelFactory, IChannelHandler, API, SimpleTypedRpcConnection, MakeSideAsync } from './rpc.js';
7 >
8 > export function registerDebugChannel<T extends { channelId: string } & API>(
9 channelId: T['channelId'],
10 createClient: () => T['client'],
43 });
44 }
46 > interface GlobalObj {
47 > $$debugValueEditor_debugChannels: Record<string, (host: IHost) => { handleRequest: (data: unknown) => unknown }>;
48 > }
49 >
50 > interface IHost {
51 > sendNotification: (data: unknown) => void;
52 > }
53 >
54 function createChannelFactoryFromDebugChannel(host: IHost): { channel: ChannelFactory; handler: { handleRequest: (data: unknown) => unknown } } {
55 let h: IChannelHandler | undefined;
src/vs/base/common/observableInternal/utils/runOnChange.ts 17 covered LOC · 3 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- runOnChange.ts
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 > import { IObservableWithChange } from '../base.js';
7 > import { CancellationToken, cancelOnDispose } from '../commonFacade/cancellation.js';
8 > import { DisposableStore, IDisposable } from '../commonFacade/deps.js';
9 > import { autorunWithStoreHandleChanges } from '../reactions/autorun.js';
10 >
11 > export type RemoveUndefined<T> = T extends undefined ? never : T;
12 >
13 > export function runOnChange<T, TChange>(observable: IObservableWithChange<T, TChange>, cb: (value: T, previousValue: T, deltas: RemoveUndefined<TChange>[]) => void): IDisposable {
14 let _previousValue: T | undefined;
15 let _firstRun = true;
42 });
43 }
45 > export function runOnChangeWithStore<T, TChange>(observable: IObservableWithChange<T, TChange>, cb: (value: T, previousValue: T, deltas: RemoveUndefined<TChange>[], store: DisposableStore) => void): IDisposable {
46 const store = new DisposableStore();
47 const disposable = runOnChange(observable, (value, previousValue: T, deltas) => {
56 };
57 }
59 > export function runOnChangeWithCancellationToken<T, TChange>(observable: IObservableWithChange<T, TChange>, cb: (value: T, previousValue: T, deltas: RemoveUndefined<TChange>[], token: CancellationToken) => Promise<void>): IDisposable {
60 return runOnChangeWithStore(observable, (value, previousValue, deltas, store) => {
61 cb(value, previousValue, deltas, cancelOnDispose(store));
src/vs/platform/agentHost/common/state/protocol/channels-terminal/reducer.ts 17 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- reducer.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > import { ActionType } from '../common/actions.js';
10 > import type { TerminalState, TerminalContentPart } from './state.js';
11 > import type { TerminalAction } from '../action-origin.generated.js';
12 > import { softAssertNever } from '../common/reducer-helpers.js';
13 >
14 > /**
15 > * Pure reducer for terminal state. Handles all {@link TerminalAction} variants.
16 > */
17 > export function terminalReducer(state: TerminalState, action: TerminalAction, log?: (msg: string) => void): TerminalState {
18 switch (action.type) {
19 case ActionType.TerminalData: {
src/vs/platform/agentHost/common/state/protocol/state.ts 17 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- state.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > export * from './common/state.js';
10 > export * from './channels-root/state.js';
11 > export * from './channels-session/state.js';
12 > export * from './channels-chat/state.js';
13 > export * from './channels-terminal/state.js';
14 > export * from './channels-changeset/state.js';
15 > export * from './channels-annotations/state.js';
16 > export * from './channels-otlp/state.js';
17 > export * from './channels-resource-watch/state.js';
src/vs/platform/product/common/productService.ts 17 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- productService.ts
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 > import { IProductConfiguration } from '../../../base/common/product.js';
7 > import { createDecorator } from '../../instantiation/common/instantiation.js';
8 >
9 > export const IProductService = createDecorator<IProductService>('productService');
10 >
11 > export interface IProductService extends Readonly<IProductConfiguration> {
12 >
13 > readonly _serviceBrand: undefined;
14 >
15 > }
16 >
17 > export const productSchemaId = 'vscode://schemas/vscode-product';
src/vs/platform/agentHost/common/state/protocol/actions.ts 16 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- actions.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > export * from './common/actions.js';
10 > export * from './channels-root/actions.js';
11 > export * from './channels-session/actions.js';
12 > export * from './channels-chat/actions.js';
13 > export * from './channels-terminal/actions.js';
14 > export * from './channels-changeset/actions.js';
15 > export * from './channels-annotations/actions.js';
16 > export * from './channels-resource-watch/actions.js';
src/vs/platform/agentHost/common/state/protocol/reducers.ts 16 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- reducers.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > export { rootReducer } from './channels-root/reducer.js';
10 > export { sessionReducer } from './channels-session/reducer.js';
11 > export { chatReducer } from './channels-chat/reducer.js';
12 > export { terminalReducer } from './channels-terminal/reducer.js';
13 > export { changesetReducer } from './channels-changeset/reducer.js';
14 > export { annotationsReducer } from './channels-annotations/reducer.js';
15 > export { resourceWatchReducer } from './channels-resource-watch/reducer.js';
16 > export { softAssertNever, isClientDispatchable } from './common/reducer-helpers.js';
src/vs/base/node/macAddress.ts 15 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- macAddress.ts
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 > import { networkInterfaces } from 'os';
7 >
8 > const invalidMacAddresses = new Set([
9 > '00:00:00:00:00:00',
10 > 'ff:ff:ff:ff:ff:ff',
11 > 'ac:de:48:00:11:22'
12 > ]);
13 >
14 function validateMacAddress(candidate: string): boolean {
15 const tempCandidate = candidate.replace(/\-/g, ':').toLowerCase();
16 return !invalidMacAddresses.has(tempCandidate);
17 }
19 > export function getMac(): string {
20 const ifaces = networkInterfaces();
21 for (const name in ifaces) {
src/vs/base/test/common/virtualScheduling/loggingTimeApi.ts 15 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- loggingTimeApi.ts
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 > import { TimeApi } from './timeApi.js';
7 >
8 > /**
9 > * Wrap `underlying` so that every call to `setTimeout`, `setInterval`,
10 > * `setImmediate` or `requestAnimationFrame` invokes `onCall` first.
11 > *
12 > * Useful for diagnostics — e.g. logging timer registrations made outside of
13 > * virtual time, to find leaks of real-time scheduling into a fixture.
14 > */
15 > export function createLoggingTimeApi(
16 underlying: TimeApi,
17 onCall: (name: string, stack: string | undefined, handler?: () => void) => void,
src/vs/platform/agentHost/common/state/protocol/commands.ts 15 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- commands.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > export * from './common/commands.js';
10 > export * from './channels-root/commands.js';
11 > export * from './channels-session/commands.js';
12 > export * from './channels-chat/commands.js';
13 > export * from './channels-terminal/commands.js';
14 > export * from './channels-changeset/commands.js';
15 > export * from './channels-resource-watch/commands.js';
src/vs/platform/telemetry/common/commonProperties.ts 14 covered LOC · 3 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- commonProperties.ts
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 > import { isLinuxSnap, platform, Platform, PlatformToString } from '../../../base/common/platform.js';
7 > import { env, platform as nodePlatform } from '../../../base/common/process.js';
8 > import { generateUuid } from '../../../base/common/uuid.js';
9 > import { ICommonProperties } from './telemetry.js';
10 >
11 function getPlatformDetail(hostname: string): string | undefined {
12 if (platform === Platform.Linux && /^penguin(\.|$)/i.test(hostname)) {
16 return undefined;
17 }
19 > export function resolveCommonProperties(
20 release: string,
21 hostname: string,
97 return result;
98 }
100 > export function verifyMicrosoftInternalDomain(domainList: readonly string[]): boolean {
101 const userDnsDomain = env['USERDNSDOMAIN'];
102 if (!userDnsDomain) {
src/vs/base/common/observableInternal/observables/observableValueOpts.ts 13 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- observableValueOpts.ts
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 > import { ISettableObservable } from '../base.js';
7 > import { DebugNameData, IDebugNameData } from '../debugName.js';
8 > import { EqualityComparer, strictEquals } from '../commonFacade/deps.js';
9 > import { ObservableValue } from './observableValue.js';
10 > import { LazyObservableValue } from './lazyObservableValue.js';
11 > import { DebugLocation } from '../debugLocation.js';
12 >
13 > export function observableValueOpts<T, TChange = void>(
14 options: IDebugNameData & {
15 equalsFn?: EqualityComparer<T>;
src/vs/platform/agentHost/node/localCommands/localChatCommands.contribution.ts 12 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- localChatCommands.contribution.ts
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 > // Importing this module registers all built-in local chat commands with the
7 > // LocalChatCommandRegistry (via each command module's bottom-of-file
8 > // `register(...)` side effect). Import it wherever the registry must be
9 > // populated (e.g. AgentSideEffects) so adding a new command is just a new file
10 > // plus an import here.
11 > import './renameLocalCommand.js';
12 > import './bangLocalCommand.js';
src/vs/platform/endpoint/common/licenseAgreement.ts 12 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- licenseAgreement.ts
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 > /**
7 > * This file is modified as part of the production build.
8 > *
9 > * WARNING: Do not move or rename this file.
10 > */
11 > export const COPILOT_LICENSE_AGREEMENT: string | undefined = undefined;
12 > export const COPILOT_INTEGRATION_ID: string = 'code-oss';
src/vs/platform/agentHost/common/state/protocol/notifications.ts 11 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- notifications.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > export * from './common/notifications.js';
10 > export * from './channels-root/notifications.js';
11 > export * from './channels-otlp/notifications.js';
src/vs/base/common/observableInternal/commonFacade/deps.ts 10 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- deps.ts
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 > export { assertFn } from '../../assert.js';
7 > export { type EqualityComparer, strictEquals } from '../../equals.js';
8 > export { BugIndicatingError, onBugIndicatingError, onUnexpectedError } from '../../errors.js';
9 > export { Event, type IValueWithChangeEvent } from '../../event.js';
10 > export { DisposableStore, type IDisposable, markAsDisposed, toDisposable, trackDisposable } from '../../lifecycle.js';
src/vs/base/node/ripgrep.ts 10 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- ripgrep.ts
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 > import { Lazy } from '../common/lazy.js';
7 >
8 > const _rgDiskPath = new Lazy(async () => {
9 const m = await import('@vscode/ripgrep-universal');
10 return m.rgPath.replace(/\bnode_modules\.asar\b/, 'node_modules.asar.unpacked');
11 });
12 > ripgrep.ts
13 > export function rgDiskPath(): Promise<string> {
14 return _rgDiskPath.value;
15 }
src/vs/base/common/functional.ts 9 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- functional.ts
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 > /**
7 > * Given a function, returns a function that is only calling that function once.
8 > */
9 > export function createSingleCallFunction<T extends Function>(this: unknown, fn: T, fnDidRunCallback?: () => void): T {
10 const _this = this;
11 let didCall = false;
src/vs/base/common/symbols.ts 9 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- symbols.ts
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 > /**
7 > * Can be passed into the Delayed to defer using a microtask
8 > * */
9 > export const MicrotaskDelay = Symbol('MicrotaskDelay');
src/vs/platform/agentHost/common/state/protocol/errors.ts 9 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- errors.ts
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 > // allow-any-unicode-comment-file
7 > // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
8 >
9 > export * from './common/errors.js';
src/vs/platform/agentHost/node/agentHostShellUtils.ts 9 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostShellUtils.ts
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 > import { posix as pathPosix, win32 as pathWin32 } from '../../../base/common/path.js';
7 > import * as platform from '../../../base/common/platform.js';
8 >
9 > export function isZsh(shell: string): boolean {
10 if (platform.OS === platform.OperatingSystem.Windows) {
11 return /^zsh(?:\.exe)?$/i.test(pathWin32.basename(shell));
src/vs/platform/agentHost/node/copilot/copilotTokenFields.ts 9 covered LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- copilotTokenFields.ts
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 > /** Parses the `key=value;...` field map from the leading colon-delimited segment of a Copilot token (e.g. `tid=abc;exp=123;rt=1:HMAC...`). */
7 > export function parseCopilotTokenFields(token: string | undefined): ReadonlyMap<string, string> {
8 const result = new Map<string, string>();
9 if (!token) {
21 return result;
22 }
24 > export function isRestrictedTelemetryEnabled(token: string | undefined): boolean {
25 return parseCopilotTokenFields(token).get('rt') === '1';
26 }
src/vs/base/common/observable.ts 8 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- observable.ts
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 > // This is a facade for the observable implementation. Only import from here!
7 >
8 > export * from './observableInternal/index.js';
src/vs/base/common/observableInternal/commonFacade/cancellation.ts 7 covered LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- cancellation.ts
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 > export { CancellationError } from '../../errors.js';
7 > export { CancellationToken, CancellationTokenSource, cancelOnDispose } from '../../cancellation.js';