agentSideEffects.ts ×51

Frontier kind: Code frontier

unlabeled · c_d8dc10c24211

506 tests · 39963 LOC · 233 files · introduces 0 tests · 730 LOC · 7 files

Introduces — evidence that enters the hierarchy at this concept

Code
73 ranges730 lines · 7 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
3433 ranges39963 lines · 233 files · Browse complete extent
All tests (intent)
506 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

7 files ranked by introduced lines: 730 introduced LOC across 73 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentHost/node/agentSideEffects.ts 422 introduced LOC · 51 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,
169 private readonly _options: IAgentSideEffectsOptions,
233 }));
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 => {
241 const d = a.getDescriptor();
267 this._stateManager.dispatchServerAction(ROOT_STATE_URI, { type: ActionType.RootAgentsChanged, agents: infos });
268 }
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();
512 disposables.add(agent.onDidSessionProgress(signal => {
524 return disposables;
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();
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/platform/agentHost/node/localCommands/localChatCommand.ts 158 introduced LOC · 6 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));
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,
134 private readonly _localTurns: AgentHostLocalTurns,
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/agentHost/node/localCommands/bangLocalCommand.ts 45 introduced LOC · 5 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();
34 this._register(toDisposable(() => {
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/agentHostTurnTracker.ts 43 introduced 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/platform/agentHost/node/localCommands/renameLocalCommand.ts 31 introduced LOC · 4 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();
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/platform/agentHost/node/shared/persistSessionMetadata.ts 19 introduced 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/platform/agentHost/node/localCommands/localChatCommands.contribution.ts 12 introduced 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';