@vybestack/llxprt-code 0.10.0 → 0.11.0-nightly.260728.ffaa8d5d7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -13
- package/src/commands/mcp/add.ts +8 -4
- package/src/config/cliArgParser.ts +2 -0
- package/src/config/config.ts +2 -0
- package/src/config/configBuilder.ts +3 -0
- package/src/config/configError.ts +23 -0
- package/src/config/extensions/extensionEnablement.ts +62 -1
- package/src/config/interactiveContext.ts +2 -1
- package/src/config/intermediateConfig.ts +4 -0
- package/src/config/keyBindings.ts +29 -3
- package/src/config/policy.ts +7 -1
- package/src/config/postConfigRuntime.ts +20 -14
- package/src/config/settings-schema/schema-ui.ts +9 -0
- package/src/config/settingsLoader.ts +21 -17
- package/src/config/settingsSchema.ts +2 -2
- package/src/config/trustedFolders.ts +275 -58
- package/src/config/yargsOptions.ts +7 -0
- package/src/generated/git-commit.json +1 -1
- package/src/nonInteractiveCli.ts +1 -0
- package/src/nonInteractiveCliSupport.ts +171 -51
- package/src/runtime/interactiveToolScheduler.ts +6 -6
- package/src/services/BuiltinCommandLoader.ts +33 -25
- package/src/services/FileCommandLoader.ts +34 -7
- package/src/test-utils/async.ts +20 -0
- package/src/test-utils/render.tsx +10 -1
- package/src/ui/App.tsx +17 -10
- package/src/ui/AppContainerRuntime.tsx +7 -9
- package/src/ui/cliUiRuntime.ts +28 -0
- package/src/ui/commands/authCommand.ts +240 -3
- package/src/ui/commands/compressCommand.ts +9 -1
- package/src/ui/commands/directoryCommand.tsx +1 -1
- package/src/ui/commands/logoutCommand.ts +2 -2
- package/src/ui/commands/permissionsCommand.ts +77 -20
- package/src/ui/commands/policiesCommand.ts +6 -1
- package/src/ui/commands/quotaCommand.ts +501 -0
- package/src/ui/commands/statsCommand.ts +2 -2
- package/src/ui/commands/statsQuota.ts +1 -1
- package/src/ui/commands/subagentCommand.ts +59 -27
- package/src/ui/components/AuthDialog.tsx +3 -3
- package/src/ui/components/Composer.tsx +1 -0
- package/src/ui/components/DialogManager.tsx +9 -2
- package/src/ui/components/FolderTrustDialog.tsx +71 -60
- package/src/ui/components/PermissionsModifyTrustDialog.tsx +211 -146
- package/src/ui/components/ProfileCreateWizard/constants.ts +13 -1
- package/src/ui/components/QueuedMessagesPanel.tsx +388 -0
- package/src/ui/components/inputPromptHooks.ts +2 -0
- package/src/ui/components/inputPromptKeyHandlers.ts +14 -0
- package/src/ui/components/inputPromptTypes.ts +6 -0
- package/src/ui/components/messages/AiMessage.tsx +24 -12
- package/src/ui/components/messages/ToolMessage.tsx +26 -40
- package/src/ui/containers/AppContainer/builders/buildUIActions.ts +3 -1
- package/src/ui/containers/AppContainer/builders/buildUIState.ts +5 -4
- package/src/ui/containers/AppContainer/hooks/useAppBootstrap.ts +3 -33
- package/src/ui/containers/AppContainer/hooks/useAppDialogs.ts +2 -14
- package/src/ui/containers/AppContainer/hooks/useAppInput.ts +31 -25
- package/src/ui/containers/AppContainer/hooks/useAppLayout.ts +90 -60
- package/src/ui/containers/AppContainer/hooks/useDisplayPreferences.ts +10 -0
- package/src/ui/containers/AppContainer/hooks/useInputHandling.ts +3 -17
- package/src/ui/containers/AppContainer/hooks/useKeybindings.ts +51 -38
- package/src/ui/containers/AppContainer/hooks/useSteer.ts +51 -0
- package/src/ui/contexts/KeypressContext.tsx +13 -2
- package/src/ui/contexts/ShellCommandDisplayContext.tsx +103 -0
- package/src/ui/contexts/TodoContext.tsx +0 -13
- package/src/ui/contexts/TodoProvider.tsx +1 -34
- package/src/ui/contexts/UIActionsContext.tsx +2 -1
- package/src/ui/contexts/UIStateContext.tsx +3 -2
- package/src/ui/hooks/agentStream/streamUtils.ts +0 -24
- package/src/ui/hooks/agentStream/thoughtState.ts +110 -30
- package/src/ui/hooks/agentStream/types.ts +1 -0
- package/src/ui/hooks/agentStream/useAgentEventStream.ts +8 -17
- package/src/ui/hooks/agentStream/useAgentStream.ts +2 -2
- package/src/ui/hooks/agentStream/useAgentStreamLifecycle.ts +4 -5
- package/src/ui/hooks/agentStream/useAgentStreamOrchestration.ts +12 -5
- package/src/ui/hooks/agentStream/useQueuedSubmissions.ts +99 -0
- package/src/ui/hooks/agentStream/useStreamEventHandlers.ts +4 -13
- package/src/ui/hooks/agentStream/useStreamState.ts +60 -41
- package/src/ui/hooks/agentStream/useSubmitQuery.ts +284 -59
- package/src/ui/hooks/slashCommandProcessorSupport.ts +4 -0
- package/src/ui/hooks/useAgentStream-test-helpers.ts +1 -0
- package/src/ui/hooks/useAutoAcceptIndicator.ts +13 -0
- package/src/ui/hooks/useFolderTrust.ts +118 -81
- package/src/ui/hooks/useGitBranchName.ts +79 -33
- package/src/ui/hooks/useIdeTrustListener.ts +5 -19
- package/src/ui/hooks/usePermissionsModifyTrust.ts +223 -85
- package/src/ui/hooks/useReactToolScheduler.ts +13 -17
- package/src/ui/hooks/useTodoPausePreserver.ts +0 -55
- package/src/ui/layouts/DefaultAppLayout.tsx +2 -0
- package/src/ui/layouts/DefaultAppLayoutHelpers.tsx +37 -162
- package/src/ui/layouts/InlineContent.tsx +198 -0
- package/src/ui/trustDialogHelpers.ts +190 -0
- package/src/utils/cleanup-state.ts +115 -0
- package/src/utils/cleanup.ts +32 -34
- package/src/utils/sandbox-containers.ts +183 -53
- package/src/utils/sandbox-exec.ts +9 -3
- package/src/zed-integration/acp-terminal-shell-host.ts +112 -0
- package/src/zed-integration/acp-types.ts +35 -0
- package/src/zed-integration/fileSystemService.ts +1 -1
- package/src/zed-integration/runZedIntegration.ts +142 -0
- package/src/zed-integration/zed-agent-event-handler.ts +179 -0
- package/src/zed-integration/zed-agent-setup.ts +43 -0
- package/src/zed-integration/zed-command-registry.ts +167 -0
- package/src/zed-integration/zed-config-options.ts +280 -0
- package/src/zed-integration/zed-content-utils.ts +2 -2
- package/src/zed-integration/zed-helpers.ts +132 -0
- package/src/zed-integration/zed-initialize.ts +64 -0
- package/src/zed-integration/zed-plan-update.ts +20 -0
- package/src/zed-integration/zed-prompt-command.ts +77 -0
- package/src/zed-integration/zed-session-config.ts +119 -0
- package/src/zed-integration/zed-session-errors.ts +337 -0
- package/src/zed-integration/zed-session-events.ts +216 -0
- package/src/zed-integration/zed-session-info.ts +255 -0
- package/src/zed-integration/zed-session-lifecycle.ts +186 -0
- package/src/zed-integration/zed-session-listing.ts +153 -0
- package/src/zed-integration/zed-session-loader.ts +193 -0
- package/src/zed-integration/zed-session-pagination.ts +240 -0
- package/src/zed-integration/zed-session-replay.ts +564 -0
- package/src/zed-integration/zed-stream-batcher.ts +236 -0
- package/src/zed-integration/zed-terminal-manager.ts +416 -0
- package/src/zed-integration/zed-terminal-setup.ts +72 -0
- package/src/zed-integration/zed-test-helpers.ts +349 -4
- package/src/zed-integration/zed-tool-handler.ts +47 -5
- package/src/zed-integration/zedIntegration.ts +538 -587
- package/src/services/todo-continuation/todoContinuationService.ts +0 -610
- package/src/ui/containers/AppContainer/hooks/useIdeRestartHotkey.ts +0 -33
- package/src/ui/containers/AppContainer/hooks/useTodoContinuationFlow.ts +0 -160
- package/src/ui/hooks/useTodoContinuation.ts +0 -299
|
@@ -3,270 +3,423 @@
|
|
|
3
3
|
* Copyright 2025 Vybestack LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
6
|
import {
|
|
8
7
|
type Config,
|
|
9
|
-
getErrorStatus,
|
|
10
8
|
todoEvents,
|
|
11
9
|
DEFAULT_AGENT_ID,
|
|
12
|
-
isWithinRoot,
|
|
13
|
-
createInkStdio,
|
|
14
|
-
type ContentBlock,
|
|
15
|
-
EmojiFilter,
|
|
16
10
|
type FilterConfiguration,
|
|
11
|
+
type IContent,
|
|
17
12
|
type TodoUpdateEvent,
|
|
18
|
-
type Todo,
|
|
19
13
|
type ApprovalMode,
|
|
20
|
-
type RuntimeProviderManager,
|
|
21
14
|
} from '@vybestack/llxprt-code-core';
|
|
22
15
|
import { debugLogger, DebugLogger } from '@vybestack/llxprt-code-telemetry';
|
|
23
|
-
import * as acp from '@agentclientprotocol/sdk';
|
|
16
|
+
import type * as acp from '@agentclientprotocol/sdk';
|
|
24
17
|
import {
|
|
25
18
|
fromConfig,
|
|
19
|
+
getTokenLimitForConfiguredContext,
|
|
26
20
|
type Agent,
|
|
27
21
|
type AgentEvent,
|
|
28
|
-
type DoneReason,
|
|
29
|
-
type ThoughtSummary,
|
|
30
|
-
type AgentInput,
|
|
31
22
|
} from '@vybestack/llxprt-code-agents';
|
|
32
|
-
import { Readable, Writable } from 'node:stream';
|
|
33
|
-
import * as path from 'node:path';
|
|
34
23
|
import { type LoadedSettings } from '../config/settings.js';
|
|
35
24
|
import { randomUUID } from 'crypto';
|
|
36
|
-
import {
|
|
37
|
-
loadProfileByName,
|
|
38
|
-
setCliRuntimeContext,
|
|
39
|
-
} from '@vybestack/llxprt-code-providers/runtime.js';
|
|
40
|
-
import { runExitCleanup } from '../utils/cleanup.js';
|
|
41
25
|
import { AcpFileSystemService } from './fileSystemService.js';
|
|
42
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
buildAvailableModes,
|
|
28
|
+
buildSessionModes,
|
|
29
|
+
buildUsageUpdate,
|
|
30
|
+
describeSessionUpdateForLog,
|
|
31
|
+
} from './zed-helpers.js';
|
|
43
32
|
import { ZedPathResolver } from './zed-path-resolver.js';
|
|
44
33
|
import { ToolConfirmationOutcome } from '@vybestack/llxprt-code-tools';
|
|
45
34
|
import {
|
|
46
|
-
emitToolCallStart,
|
|
47
|
-
emitToolStatus,
|
|
48
|
-
emitToolResult,
|
|
49
35
|
requestToolConfirmation,
|
|
50
36
|
type PermissionRoundTripResult,
|
|
51
37
|
} from './zed-tool-handler.js';
|
|
52
|
-
|
|
38
|
+
import type { TerminalManager } from './zed-terminal-manager.js';
|
|
39
|
+
import { buildZedTerminalSetup } from './zed-terminal-setup.js';
|
|
40
|
+
import { mapHistoryToSessionUpdates } from './zed-session-replay.js';
|
|
41
|
+
import { wrapReplayFailure } from './zed-session-errors.js';
|
|
42
|
+
import {
|
|
43
|
+
resumeAgentHistory,
|
|
44
|
+
toLoadRequestError,
|
|
45
|
+
hasRecordedSessionFile,
|
|
46
|
+
readAgentHistoryForReplay,
|
|
47
|
+
nodeChatSessionFileLister,
|
|
48
|
+
type ChatSessionFileLister,
|
|
49
|
+
} from './zed-session-loader.js';
|
|
50
|
+
import { SessionLifecycle } from './zed-session-lifecycle.js';
|
|
51
|
+
import type { LifecycleSession } from './zed-session-pagination.js';
|
|
52
|
+
import { authenticateZedAgent, initializeZedAgent } from './zed-initialize.js';
|
|
53
|
+
import {
|
|
54
|
+
createSessionScopedConfig,
|
|
55
|
+
resolveSessionTargetDir,
|
|
56
|
+
} from './zed-session-config.js';
|
|
57
|
+
import { buildAvailableCommandsUpdate } from './zed-command-registry.js';
|
|
58
|
+
import { tryHandleZedCommand } from './zed-prompt-command.js';
|
|
59
|
+
import {
|
|
60
|
+
buildZedConfigOptions,
|
|
61
|
+
dispatchZedConfigOption,
|
|
62
|
+
observeZedConfigOptions,
|
|
63
|
+
setZedConfigOption,
|
|
64
|
+
zedConfigOptionsForClient,
|
|
65
|
+
zedSessionConfigOptions,
|
|
66
|
+
} from './zed-config-options.js';
|
|
67
|
+
import {
|
|
68
|
+
buildZedSession,
|
|
69
|
+
enableZedSessionRecording,
|
|
70
|
+
} from './zed-agent-setup.js';
|
|
71
|
+
import {
|
|
72
|
+
runPromptTurn as executePromptTurn,
|
|
73
|
+
type SessionStreamDeps,
|
|
74
|
+
} from './zed-session-events.js';
|
|
75
|
+
import { buildZedPlanUpdate } from './zed-plan-update.js';
|
|
76
|
+
import {
|
|
77
|
+
SessionTitleTracker,
|
|
78
|
+
buildSessionInfoUpdate,
|
|
79
|
+
} from './zed-session-info.js';
|
|
80
|
+
import type {
|
|
81
|
+
CloseSessionRequest,
|
|
82
|
+
CloseSessionResponse,
|
|
83
|
+
DeleteSessionRequest,
|
|
84
|
+
DeleteSessionResponse,
|
|
85
|
+
ClientCapabilitiesWithSession,
|
|
86
|
+
} from './acp-types.js';
|
|
53
87
|
export { parseZedAuthMethodId } from './zed-helpers.js';
|
|
54
|
-
|
|
55
|
-
export
|
|
56
|
-
config: Config,
|
|
57
|
-
settings: LoadedSettings,
|
|
58
|
-
): Promise<void> {
|
|
59
|
-
const logger = new DebugLogger('llxprt:zed-integration');
|
|
60
|
-
logger.debug(() => 'Starting Zed integration');
|
|
61
|
-
|
|
62
|
-
const { stdout: workingStdout } = createInkStdio();
|
|
63
|
-
const stdout = Writable.toWeb(workingStdout) as WritableStream;
|
|
64
|
-
const stdin = Readable.toWeb(process.stdin) as ReadableStream<Uint8Array>;
|
|
65
|
-
|
|
66
|
-
logger.debug(() => 'Streams created');
|
|
67
|
-
|
|
68
|
-
// Deliberate foreground hand-off: the Zed integration replaces the CLI
|
|
69
|
-
// bootstrap runtime as the process default. allowDefaultHandoff opts past the
|
|
70
|
-
// write-once guard for this one intentional transition (issue #2300).
|
|
71
|
-
setCliRuntimeContext(config.getSettingsService(), config, {
|
|
72
|
-
runtimeId: 'cli.runtime.zed',
|
|
73
|
-
metadata: { source: 'zed-integration', stage: 'bootstrap' },
|
|
74
|
-
allowDefaultHandoff: true,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
let zedAgent: ZedAgent | undefined;
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
const stream = acp.ndJsonStream(stdout, stdin);
|
|
81
|
-
const connection = new acp.AgentSideConnection((conn) => {
|
|
82
|
-
logger.debug(() => 'Creating ZedAgent');
|
|
83
|
-
zedAgent = new ZedAgent(config, settings, conn);
|
|
84
|
-
return zedAgent;
|
|
85
|
-
}, stream);
|
|
86
|
-
logger.debug(() => 'AgentSideConnection created successfully');
|
|
87
|
-
|
|
88
|
-
try {
|
|
89
|
-
await connection.closed;
|
|
90
|
-
} finally {
|
|
91
|
-
await zedAgent?.disposeAll();
|
|
92
|
-
await runExitCleanup();
|
|
93
|
-
}
|
|
94
|
-
} catch (e) {
|
|
95
|
-
logger.debug(() => `ERROR: Failed to create AgentSideConnection: ${e}`);
|
|
96
|
-
throw e;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function resolveSessionTargetDir(
|
|
101
|
-
config: Config,
|
|
102
|
-
cwd: string | undefined,
|
|
103
|
-
): string {
|
|
104
|
-
if (cwd === undefined || cwd.trim().length === 0) {
|
|
105
|
-
return config.getTargetDir();
|
|
106
|
-
}
|
|
107
|
-
const candidate = path.isAbsolute(cwd)
|
|
108
|
-
? cwd
|
|
109
|
-
: path.resolve(config.getTargetDir(), cwd);
|
|
110
|
-
return isWithinRoot(candidate, config.getTargetDir())
|
|
111
|
-
? candidate
|
|
112
|
-
: config.getTargetDir();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function createSessionScopedConfig(
|
|
116
|
-
config: Config,
|
|
117
|
-
initialFileSystemService: ReturnType<Config['getFileSystemService']>,
|
|
118
|
-
targetDir: string = config.getTargetDir(),
|
|
119
|
-
): Config {
|
|
120
|
-
let fileSystemService = initialFileSystemService;
|
|
121
|
-
let providerManager: RuntimeProviderManager | undefined =
|
|
122
|
-
config.getProviderManager();
|
|
123
|
-
return new Proxy(config, {
|
|
124
|
-
get(target, property, receiver) {
|
|
125
|
-
if (property === 'getFileSystemService') {
|
|
126
|
-
return () => fileSystemService;
|
|
127
|
-
}
|
|
128
|
-
if (property === 'setFileSystemService') {
|
|
129
|
-
return (nextFileSystemService: typeof fileSystemService) => {
|
|
130
|
-
fileSystemService = nextFileSystemService;
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
if (property === 'getProviderManager') {
|
|
134
|
-
return () => providerManager;
|
|
135
|
-
}
|
|
136
|
-
if (property === 'setProviderManager') {
|
|
137
|
-
return (nextProviderManager: RuntimeProviderManager) => {
|
|
138
|
-
providerManager = nextProviderManager;
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
if (property === 'getProjectRoot') {
|
|
142
|
-
return () => targetDir;
|
|
143
|
-
}
|
|
144
|
-
if (property === 'getTargetDir') {
|
|
145
|
-
return () => targetDir;
|
|
146
|
-
}
|
|
147
|
-
return Reflect.get(target, property, receiver);
|
|
148
|
-
},
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
88
|
+
export { createSessionScopedConfig } from './zed-session-config.js';
|
|
89
|
+
export { runZedIntegration } from './runZedIntegration.js';
|
|
152
90
|
export class ZedAgent {
|
|
153
91
|
private sessions: Map<string, Session> = new Map();
|
|
154
|
-
private clientCapabilities:
|
|
155
|
-
private logger
|
|
156
|
-
|
|
92
|
+
private clientCapabilities: ClientCapabilitiesWithSession | undefined;
|
|
93
|
+
private readonly logger = new DebugLogger('llxprt:zed-integration');
|
|
94
|
+
private readonly lifecycle: SessionLifecycle;
|
|
157
95
|
constructor(
|
|
158
96
|
private config: Config,
|
|
159
97
|
_settings: LoadedSettings,
|
|
160
98
|
private connection: acp.AgentSideConnection,
|
|
99
|
+
private readonly sessionFileLister: ChatSessionFileLister = nodeChatSessionFileLister,
|
|
161
100
|
) {
|
|
162
|
-
this.
|
|
101
|
+
this.lifecycle = new SessionLifecycle(
|
|
102
|
+
config,
|
|
103
|
+
this.sessions,
|
|
104
|
+
(sessionId, cwd) => this.buildAndResumeSession(sessionId, cwd),
|
|
105
|
+
(session) => zedSessionConfigOptions(this.clientCapabilities, session),
|
|
106
|
+
);
|
|
163
107
|
}
|
|
164
|
-
|
|
165
108
|
async initialize(
|
|
166
109
|
args: acp.InitializeRequest,
|
|
167
110
|
): Promise<acp.InitializeResponse> {
|
|
168
|
-
this.clientCapabilities = args.clientCapabilities
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
: [];
|
|
173
|
-
const authMethods = profileNames.map((name) => ({
|
|
174
|
-
id: name,
|
|
175
|
-
name,
|
|
176
|
-
description: null,
|
|
177
|
-
}));
|
|
178
|
-
|
|
179
|
-
return {
|
|
180
|
-
protocolVersion: acp.PROTOCOL_VERSION,
|
|
181
|
-
authMethods,
|
|
182
|
-
agentCapabilities: {
|
|
183
|
-
loadSession: false,
|
|
184
|
-
promptCapabilities: {
|
|
185
|
-
image: true,
|
|
186
|
-
audio: true,
|
|
187
|
-
embeddedContext: true,
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
};
|
|
111
|
+
this.clientCapabilities = args.clientCapabilities as
|
|
112
|
+
| ClientCapabilitiesWithSession
|
|
113
|
+
| undefined;
|
|
114
|
+
return initializeZedAgent(this.config);
|
|
191
115
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
await loadProfileByName(profileName);
|
|
116
|
+
listSessions(
|
|
117
|
+
params: acp.ListSessionsRequest,
|
|
118
|
+
): Promise<acp.ListSessionsResponse> {
|
|
119
|
+
return this.lifecycle.list(params);
|
|
120
|
+
}
|
|
121
|
+
authenticate({ methodId }: acp.AuthenticateRequest): Promise<void> {
|
|
122
|
+
return authenticateZedAgent(this.config, methodId);
|
|
201
123
|
}
|
|
202
|
-
|
|
203
124
|
async newSession({
|
|
204
125
|
cwd,
|
|
205
126
|
mcpServers: _mcpServers,
|
|
206
127
|
}: acp.NewSessionRequest): Promise<acp.NewSessionResponse> {
|
|
207
128
|
try {
|
|
208
129
|
const sessionId = randomUUID();
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
? new AcpFileSystemService(
|
|
212
|
-
this.connection,
|
|
213
|
-
sessionId,
|
|
214
|
-
this.clientCapabilities.fs,
|
|
215
|
-
baseFileSystemService,
|
|
216
|
-
)
|
|
217
|
-
: baseFileSystemService;
|
|
218
|
-
const sessionConfig = createSessionScopedConfig(
|
|
219
|
-
this.config,
|
|
220
|
-
sessionFileSystemService,
|
|
221
|
-
resolveSessionTargetDir(this.config, cwd),
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
this.logger.debug(() => `newSession - creating session ${sessionId}`);
|
|
225
|
-
|
|
226
|
-
const agent = await fromConfig({
|
|
130
|
+
const {
|
|
131
|
+
agent,
|
|
227
132
|
config: sessionConfig,
|
|
133
|
+
terminals,
|
|
134
|
+
} = await this.buildSessionAgent(sessionId, cwd);
|
|
135
|
+
let session: Session;
|
|
136
|
+
try {
|
|
137
|
+
await enableZedSessionRecording(agent, (error) =>
|
|
138
|
+
this.logger.debug(
|
|
139
|
+
() => `Recording failed for ${sessionId}: ${error}`,
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
session = await this.createSession(
|
|
143
|
+
sessionId,
|
|
144
|
+
agent,
|
|
145
|
+
sessionConfig,
|
|
146
|
+
terminals,
|
|
147
|
+
);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
await agent.dispose().catch(() => undefined);
|
|
150
|
+
await terminals?.settleAll().catch(() => undefined);
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
await session.sendAvailableCommands();
|
|
155
|
+
} catch (error) {
|
|
156
|
+
await session.dispose();
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
let configOptions: acp.NewSessionResponse['configOptions'];
|
|
160
|
+
try {
|
|
161
|
+
({ configOptions } = await zedConfigOptionsForClient(
|
|
162
|
+
this.clientCapabilities,
|
|
163
|
+
agent,
|
|
164
|
+
sessionConfig,
|
|
165
|
+
));
|
|
166
|
+
} catch (error) {
|
|
167
|
+
await session.dispose();
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
this.sessions.set(sessionId, session);
|
|
171
|
+
return {
|
|
228
172
|
sessionId,
|
|
229
|
-
|
|
230
|
-
|
|
173
|
+
modes: buildSessionModes(agent.getApprovalMode()),
|
|
174
|
+
...(configOptions === undefined ? {} : { configOptions }),
|
|
175
|
+
};
|
|
176
|
+
} catch (error) {
|
|
177
|
+
this.logger.debug(() => `ERROR in newSession: ${error}`);
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
resumeSession(
|
|
182
|
+
params: acp.ResumeSessionRequest,
|
|
183
|
+
): Promise<acp.ResumeSessionResponse> {
|
|
184
|
+
return this.lifecycle.resume(params);
|
|
185
|
+
}
|
|
186
|
+
private supportsConfigOptions(): boolean {
|
|
187
|
+
return this.clientCapabilities?.session?.configOptions === true;
|
|
188
|
+
}
|
|
189
|
+
private supportsTerminal(): boolean {
|
|
190
|
+
return this.clientCapabilities?.terminal === true;
|
|
191
|
+
}
|
|
192
|
+
private createSession(
|
|
193
|
+
id: string,
|
|
194
|
+
agent: Agent,
|
|
195
|
+
config: Config,
|
|
196
|
+
terminals: TerminalManager | null,
|
|
197
|
+
) {
|
|
198
|
+
return buildZedSession(
|
|
199
|
+
agent,
|
|
200
|
+
() =>
|
|
201
|
+
new Session(
|
|
202
|
+
id,
|
|
203
|
+
agent,
|
|
204
|
+
config,
|
|
205
|
+
this.connection,
|
|
206
|
+
this.supportsConfigOptions(),
|
|
207
|
+
terminals,
|
|
208
|
+
),
|
|
209
|
+
(error) => this.logger.debug(() => `Session cleanup failed: ${error}`),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
async loadSession(
|
|
213
|
+
params: acp.LoadSessionRequest,
|
|
214
|
+
): Promise<acp.LoadSessionResponse> {
|
|
215
|
+
return this.lifecycle.runSerialized(params.sessionId, () =>
|
|
216
|
+
this.performLoadSession(params),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
private async performLoadSession(
|
|
220
|
+
params: acp.LoadSessionRequest,
|
|
221
|
+
): Promise<acp.LoadSessionResponse> {
|
|
222
|
+
const { sessionId } = params;
|
|
223
|
+
const reattached = await this.tryReattachLiveSession(sessionId);
|
|
224
|
+
if (reattached !== null) {
|
|
225
|
+
try {
|
|
226
|
+
await reattached.sendAvailableCommands();
|
|
227
|
+
} catch (error) {
|
|
228
|
+
await this.rollbackSession(sessionId, reattached);
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
return {
|
|
233
|
+
modes: buildSessionModes(reattached.getApprovalMode()),
|
|
234
|
+
...(await zedSessionConfigOptions(
|
|
235
|
+
this.clientCapabilities,
|
|
236
|
+
reattached,
|
|
237
|
+
)),
|
|
238
|
+
};
|
|
239
|
+
} catch (error) {
|
|
240
|
+
await this.rollbackSession(sessionId, reattached);
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
await this.disposePriorSession(sessionId);
|
|
245
|
+
const session = await this.installResumedSession(sessionId, params.cwd);
|
|
246
|
+
try {
|
|
247
|
+
await session.sendAvailableCommands();
|
|
248
|
+
} catch (error) {
|
|
249
|
+
await this.rollbackSession(sessionId, session);
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
modes: buildSessionModes(session.getApprovalMode()),
|
|
254
|
+
...(await zedSessionConfigOptions(this.clientCapabilities, session)),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
private async tryReattachLiveSession(
|
|
258
|
+
sessionId: string,
|
|
259
|
+
): Promise<Session | null> {
|
|
260
|
+
const existing = this.sessions.get(sessionId);
|
|
261
|
+
if (existing === undefined) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
const recordingExists = await hasRecordedSessionFile(
|
|
265
|
+
this.config,
|
|
266
|
+
sessionId,
|
|
267
|
+
this.sessionFileLister,
|
|
268
|
+
);
|
|
269
|
+
if (recordingExists) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
this.logger.debug(
|
|
273
|
+
() => `loadSession - re-attaching live session ${sessionId}`,
|
|
274
|
+
);
|
|
275
|
+
try {
|
|
276
|
+
await existing.replayLiveHistory();
|
|
277
|
+
return existing;
|
|
278
|
+
} catch (error) {
|
|
279
|
+
await this.rollbackSession(sessionId, existing);
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
private async rollbackSession(
|
|
284
|
+
sessionId: string,
|
|
285
|
+
session: Session,
|
|
286
|
+
): Promise<void> {
|
|
287
|
+
this.sessions.delete(sessionId);
|
|
288
|
+
await session.dispose().catch(() => undefined);
|
|
289
|
+
}
|
|
290
|
+
private async disposePriorSession(sessionId: string): Promise<void> {
|
|
291
|
+
const existing = this.sessions.get(sessionId);
|
|
292
|
+
if (existing !== undefined) await this.rollbackSession(sessionId, existing);
|
|
293
|
+
}
|
|
294
|
+
private async installResumedSession(
|
|
295
|
+
sessionId: string,
|
|
296
|
+
cwd: string | undefined,
|
|
297
|
+
): Promise<Session> {
|
|
298
|
+
const { session, history } = await this.buildAndResumeSession(
|
|
299
|
+
sessionId,
|
|
300
|
+
cwd,
|
|
301
|
+
);
|
|
302
|
+
this.sessions.set(sessionId, session);
|
|
303
|
+
try {
|
|
304
|
+
await session.streamHistory(history);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
await this.rollbackSession(sessionId, session);
|
|
307
|
+
this.logger.debug(() => `loadSession - replay failed: ${error}`);
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
return session;
|
|
311
|
+
}
|
|
312
|
+
private async buildAndResumeSession(
|
|
313
|
+
sessionId: string,
|
|
314
|
+
cwd: string | undefined,
|
|
315
|
+
): Promise<{ session: Session; history: readonly IContent[] }> {
|
|
316
|
+
const {
|
|
317
|
+
agent,
|
|
318
|
+
config: sessionConfig,
|
|
319
|
+
terminals,
|
|
320
|
+
} = await this.buildSessionAgent(sessionId, cwd);
|
|
321
|
+
try {
|
|
322
|
+
const history = await resumeAgentHistory(
|
|
323
|
+
agent,
|
|
324
|
+
sessionId,
|
|
325
|
+
sessionConfig,
|
|
326
|
+
this.sessionFileLister,
|
|
327
|
+
);
|
|
231
328
|
const session = new Session(
|
|
232
329
|
sessionId,
|
|
233
330
|
agent,
|
|
234
331
|
sessionConfig,
|
|
235
332
|
this.connection,
|
|
333
|
+
this.supportsConfigOptions(),
|
|
334
|
+
terminals,
|
|
236
335
|
);
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
336
|
+
return { session, history };
|
|
337
|
+
} catch (error) {
|
|
338
|
+
await agent.dispose().catch(() => undefined);
|
|
339
|
+
await terminals?.settleAll().catch(() => undefined);
|
|
340
|
+
this.logger.debug(() => `loadSession - build/resume failed: ${error}`);
|
|
341
|
+
throw toLoadRequestError(sessionId, error);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
private async buildSessionAgent(
|
|
345
|
+
sessionId: string,
|
|
346
|
+
cwd: string | undefined,
|
|
347
|
+
): Promise<{
|
|
348
|
+
agent: Agent;
|
|
349
|
+
config: Config;
|
|
350
|
+
terminals: TerminalManager | null;
|
|
351
|
+
}> {
|
|
352
|
+
const baseFileSystemService = this.config.getFileSystemService();
|
|
353
|
+
const sessionFileSystemService = this.clientCapabilities?.fs
|
|
354
|
+
? new AcpFileSystemService(
|
|
355
|
+
this.connection,
|
|
356
|
+
sessionId,
|
|
357
|
+
this.clientCapabilities.fs,
|
|
358
|
+
baseFileSystemService,
|
|
359
|
+
)
|
|
360
|
+
: baseFileSystemService;
|
|
361
|
+
let terminalSetup: ReturnType<typeof buildZedTerminalSetup> | undefined;
|
|
362
|
+
const sessionConfig = createSessionScopedConfig(
|
|
363
|
+
this.config,
|
|
364
|
+
sessionFileSystemService,
|
|
365
|
+
resolveSessionTargetDir(this.config, cwd),
|
|
366
|
+
() => terminalSetup?.registry,
|
|
367
|
+
);
|
|
368
|
+
let agent: Agent | undefined;
|
|
369
|
+
try {
|
|
370
|
+
agent = await fromConfig({
|
|
371
|
+
config: sessionConfig,
|
|
240
372
|
sessionId,
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
373
|
+
});
|
|
374
|
+
if (this.supportsTerminal()) {
|
|
375
|
+
terminalSetup = buildZedTerminalSetup(
|
|
376
|
+
sessionId,
|
|
377
|
+
sessionConfig,
|
|
378
|
+
this.config.getToolRegistry(),
|
|
379
|
+
this.connection,
|
|
380
|
+
this.logger,
|
|
381
|
+
agent.getMessageBus(),
|
|
382
|
+
);
|
|
383
|
+
}
|
|
246
384
|
} catch (error) {
|
|
247
|
-
|
|
385
|
+
// buildZedTerminalSetup can throw after fromConfig succeeded; dispose the
|
|
386
|
+
// already-constructed agent (and any partially-built terminals) so it is
|
|
387
|
+
// not leaked when this method aborts before returning ownership to caller.
|
|
388
|
+
await agent?.dispose().catch(() => undefined);
|
|
389
|
+
await terminalSetup?.terminals.settleAll().catch(() => undefined);
|
|
248
390
|
throw error;
|
|
249
391
|
}
|
|
392
|
+
return {
|
|
393
|
+
agent,
|
|
394
|
+
config: sessionConfig,
|
|
395
|
+
terminals: terminalSetup?.terminals ?? null,
|
|
396
|
+
};
|
|
250
397
|
}
|
|
251
|
-
|
|
252
|
-
|
|
398
|
+
deleteSession(params: DeleteSessionRequest): Promise<DeleteSessionResponse> {
|
|
399
|
+
return this.lifecycle.delete(params);
|
|
400
|
+
}
|
|
401
|
+
closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse> {
|
|
402
|
+
return this.lifecycle.close(params);
|
|
403
|
+
}
|
|
404
|
+
setSessionMode(
|
|
253
405
|
params: acp.SetSessionModeRequest,
|
|
254
406
|
): Promise<acp.SetSessionModeResponse> {
|
|
255
407
|
const session = this.sessions.get(params.sessionId);
|
|
256
408
|
if (!session) {
|
|
257
409
|
throw new Error(`Session not found: ${params.sessionId}`);
|
|
258
410
|
}
|
|
259
|
-
return session.setMode(params.modeId);
|
|
411
|
+
return Promise.resolve(session.setMode(params.modeId));
|
|
260
412
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
413
|
+
setSessionConfigOption(params: acp.SetSessionConfigOptionRequest) {
|
|
414
|
+
return this.lifecycle.runSerialized(params.sessionId, () =>
|
|
415
|
+
dispatchZedConfigOption(this.clientCapabilities, this.sessions, params),
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
async cancel({ sessionId }: acp.CancelNotification): Promise<void> {
|
|
419
|
+
const session = this.sessions.get(sessionId);
|
|
420
|
+
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
267
421
|
await session.cancelPendingPrompt();
|
|
268
422
|
}
|
|
269
|
-
|
|
270
423
|
async prompt(params: acp.PromptRequest): Promise<acp.PromptResponse> {
|
|
271
424
|
const session = this.sessions.get(params.sessionId);
|
|
272
425
|
if (!session) {
|
|
@@ -274,41 +427,15 @@ export class ZedAgent {
|
|
|
274
427
|
}
|
|
275
428
|
return session.prompt(params);
|
|
276
429
|
}
|
|
277
|
-
|
|
278
430
|
async disposeAll(): Promise<void> {
|
|
279
431
|
const sessions = [...this.sessions.values()];
|
|
280
432
|
this.sessions.clear();
|
|
281
433
|
await Promise.allSettled(sessions.map((session) => session.dispose()));
|
|
282
434
|
}
|
|
283
435
|
}
|
|
284
|
-
|
|
285
|
-
function mapDoneReasonToStopReason(reason: DoneReason): acp.StopReason {
|
|
286
|
-
switch (reason) {
|
|
287
|
-
case 'stop':
|
|
288
|
-
case 'loop-detected':
|
|
289
|
-
return 'end_turn';
|
|
290
|
-
case 'aborted':
|
|
291
|
-
return 'cancelled';
|
|
292
|
-
case 'max-turns':
|
|
293
|
-
return 'max_turn_requests';
|
|
294
|
-
case 'context-overflow':
|
|
295
|
-
return 'max_tokens';
|
|
296
|
-
case 'refusal':
|
|
297
|
-
return 'refusal';
|
|
298
|
-
case 'error':
|
|
299
|
-
case 'hook-stopped':
|
|
300
|
-
throw new Error(`Agent stopped with terminal reason: ${reason}`);
|
|
301
|
-
default: {
|
|
302
|
-
const exhaustive: never = reason;
|
|
303
|
-
return exhaustive;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
436
|
export class Session {
|
|
309
437
|
private pendingPrompt: AbortController | null = null;
|
|
310
|
-
private
|
|
311
|
-
private logger: DebugLogger;
|
|
438
|
+
private readonly logger = new DebugLogger('llxprt:zed-integration');
|
|
312
439
|
private pathResolver: ZedPathResolver;
|
|
313
440
|
private activeConfirmations = new Map<
|
|
314
441
|
string,
|
|
@@ -320,25 +447,29 @@ export class Session {
|
|
|
320
447
|
>();
|
|
321
448
|
private promptGeneration = 0;
|
|
322
449
|
private readonly todoListener: (event: TodoUpdateEvent) => void;
|
|
450
|
+
private readonly stopConfigUpdates: () => void;
|
|
451
|
+
private readonly sessionInfo = new SessionTitleTracker();
|
|
452
|
+
private readonly createdAt = new Date().toISOString();
|
|
453
|
+
private readonly terminals: TerminalManager | null;
|
|
323
454
|
|
|
324
455
|
constructor(
|
|
325
456
|
private readonly id: string,
|
|
326
457
|
private readonly agent: Agent,
|
|
327
458
|
private readonly config: Config,
|
|
328
459
|
private readonly connection: acp.AgentSideConnection,
|
|
460
|
+
configOptionsEnabled = false,
|
|
461
|
+
terminals: TerminalManager | null = null,
|
|
329
462
|
) {
|
|
330
|
-
this.
|
|
331
|
-
const configuredEmojiFilterMode = this.config.getEphemeralSetting(
|
|
332
|
-
'emojifilter',
|
|
333
|
-
) as 'allowed' | 'auto' | 'warn' | 'error' | undefined;
|
|
334
|
-
const emojiFilterMode = configuredEmojiFilterMode ?? 'auto';
|
|
335
|
-
const filterConfig: FilterConfiguration = { mode: emojiFilterMode };
|
|
336
|
-
this.emojiFilter = new EmojiFilter(filterConfig);
|
|
337
|
-
|
|
463
|
+
this.terminals = terminals;
|
|
338
464
|
this.pathResolver = new ZedPathResolver(this.config, (msg) =>
|
|
339
465
|
this.debug(msg),
|
|
340
466
|
);
|
|
341
|
-
|
|
467
|
+
const recordedTitle = config
|
|
468
|
+
.getSessionRecordingService()
|
|
469
|
+
?.getSessionMetadataTitle();
|
|
470
|
+
if (recordedTitle !== undefined) {
|
|
471
|
+
this.sessionInfo.hydrateFromMetadata(recordedTitle);
|
|
472
|
+
}
|
|
342
473
|
this.todoListener = (event: TodoUpdateEvent) => {
|
|
343
474
|
const eventAgentId = event.agentId ?? DEFAULT_AGENT_ID;
|
|
344
475
|
if (event.sessionId === this.id && eventAgentId === DEFAULT_AGENT_ID) {
|
|
@@ -348,8 +479,15 @@ export class Session {
|
|
|
348
479
|
}
|
|
349
480
|
};
|
|
350
481
|
todoEvents.onTodoUpdated(this.todoListener);
|
|
482
|
+
this.stopConfigUpdates = configOptionsEnabled
|
|
483
|
+
? observeZedConfigOptions(
|
|
484
|
+
this.agent,
|
|
485
|
+
this.config,
|
|
486
|
+
(update) => this.sendUpdateStrict(update),
|
|
487
|
+
(error) => this.logger.debug(() => `Config update failed: ${error}`),
|
|
488
|
+
)
|
|
489
|
+
: () => undefined;
|
|
351
490
|
}
|
|
352
|
-
|
|
353
491
|
setMode(modeId: acp.SessionModeId): acp.SetSessionModeResponse {
|
|
354
492
|
const availableModes = buildAvailableModes();
|
|
355
493
|
const mode = availableModes.find((m) => m.id === modeId);
|
|
@@ -359,46 +497,43 @@ export class Session {
|
|
|
359
497
|
this.agent.setApprovalMode(mode.id as ApprovalMode);
|
|
360
498
|
return {};
|
|
361
499
|
}
|
|
362
|
-
|
|
500
|
+
getApprovalMode(): ApprovalMode {
|
|
501
|
+
return this.agent.getApprovalMode();
|
|
502
|
+
}
|
|
503
|
+
setConfigOption(configId: string, value: string) {
|
|
504
|
+
return setZedConfigOption(this.agent, this.config, configId, value);
|
|
505
|
+
}
|
|
506
|
+
getConfigOptions = () => buildZedConfigOptions(this.agent, this.config);
|
|
507
|
+
getLifecycleInfo(): LifecycleSession {
|
|
508
|
+
const title = this.sessionInfo.getTitle();
|
|
509
|
+
return {
|
|
510
|
+
sessionId: this.id,
|
|
511
|
+
cwd: this.config.getProjectRoot(),
|
|
512
|
+
updatedAt: this.sessionInfo.getUpdatedAt() ?? this.createdAt,
|
|
513
|
+
createdAt: this.createdAt,
|
|
514
|
+
...(title === undefined ? {} : { title }),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
363
517
|
async cancelPendingPrompt(): Promise<void> {
|
|
364
518
|
this.settleActiveConfirmation();
|
|
365
|
-
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
this.pendingPrompt.abort();
|
|
519
|
+
this.pendingPrompt?.abort();
|
|
369
520
|
this.pendingPrompt = null;
|
|
521
|
+
await this.terminals
|
|
522
|
+
?.settleAll()
|
|
523
|
+
.catch((e: unknown) =>
|
|
524
|
+
this.logger.debug(() => `Terminal settleAll failed: ${e}`),
|
|
525
|
+
);
|
|
370
526
|
}
|
|
371
|
-
|
|
372
527
|
private settleActiveConfirmation(): void {
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
for (const [confirmationId, state] of confirmations) {
|
|
376
|
-
if (state.settled) {
|
|
377
|
-
continue;
|
|
378
|
-
}
|
|
379
|
-
state.settled = true;
|
|
380
|
-
try {
|
|
381
|
-
this.agent.tools.respondToConfirmation(
|
|
382
|
-
confirmationId,
|
|
383
|
-
ToolConfirmationOutcome.Cancel,
|
|
384
|
-
);
|
|
385
|
-
} catch (error) {
|
|
386
|
-
debugLogger.error('Failed to cancel active tool confirmation:', error);
|
|
387
|
-
} finally {
|
|
388
|
-
state.cancelWaiter();
|
|
389
|
-
}
|
|
528
|
+
for (const confirmationId of [...this.activeConfirmations.keys()]) {
|
|
529
|
+
this.settleConfirmation(confirmationId);
|
|
390
530
|
}
|
|
391
531
|
}
|
|
392
|
-
|
|
393
532
|
private settleConfirmation(confirmationId: string): void {
|
|
394
533
|
const state = this.activeConfirmations.get(confirmationId);
|
|
395
|
-
if (state === undefined)
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
534
|
+
if (state === undefined) return;
|
|
398
535
|
this.activeConfirmations.delete(confirmationId);
|
|
399
|
-
if (state.settled)
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
536
|
+
if (state.settled) return;
|
|
402
537
|
state.settled = true;
|
|
403
538
|
try {
|
|
404
539
|
this.agent.tools.respondToConfirmation(
|
|
@@ -411,218 +546,141 @@ export class Session {
|
|
|
411
546
|
state.cancelWaiter();
|
|
412
547
|
}
|
|
413
548
|
}
|
|
414
|
-
|
|
415
549
|
async prompt(params: acp.PromptRequest): Promise<acp.PromptResponse> {
|
|
416
550
|
await this.cancelPendingPrompt();
|
|
417
|
-
const
|
|
418
|
-
this.pendingPrompt = pendingSend;
|
|
419
|
-
this.promptGeneration += 1;
|
|
420
|
-
const promptGeneration = this.promptGeneration;
|
|
421
|
-
|
|
422
|
-
const promptId = Math.random().toString(16).slice(2);
|
|
423
|
-
|
|
551
|
+
const eligibility = this.sessionInfo.consumeTitleEligibility(params.prompt);
|
|
424
552
|
try {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
(error instanceof Error && error.name === 'AbortError')
|
|
435
|
-
) {
|
|
436
|
-
return { stopReason: 'cancelled' };
|
|
437
|
-
}
|
|
438
|
-
throw error;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const batcher = new StreamBatcher(this.emojiFilter, (u) =>
|
|
442
|
-
this.sendUpdate(u),
|
|
553
|
+
this.recordMetadataTitle(eligibility.title);
|
|
554
|
+
} catch (error) {
|
|
555
|
+
this.logger.debug(() => `Session metadata recording failed: ${error}`);
|
|
556
|
+
}
|
|
557
|
+
try {
|
|
558
|
+
const commandResult = await tryHandleZedCommand(
|
|
559
|
+
params.prompt,
|
|
560
|
+
this.agent,
|
|
561
|
+
(update) => this.sendUpdateStrict(update),
|
|
443
562
|
);
|
|
444
|
-
|
|
445
|
-
|
|
563
|
+
if (commandResult !== null) {
|
|
564
|
+
return commandResult.response;
|
|
565
|
+
}
|
|
566
|
+
const pendingSend = new AbortController();
|
|
567
|
+
this.pendingPrompt = pendingSend;
|
|
568
|
+
this.promptGeneration += 1;
|
|
569
|
+
const promptGeneration = this.promptGeneration;
|
|
570
|
+
const promptId = randomUUID();
|
|
446
571
|
try {
|
|
447
|
-
|
|
448
|
-
|
|
572
|
+
return await this.runPromptTurn(
|
|
573
|
+
params,
|
|
449
574
|
pendingSend,
|
|
450
575
|
promptId,
|
|
451
576
|
promptGeneration,
|
|
452
|
-
batcher,
|
|
453
577
|
);
|
|
454
|
-
} catch (error) {
|
|
455
|
-
if (getErrorStatus(error) === 429) {
|
|
456
|
-
throw new acp.RequestError(
|
|
457
|
-
429,
|
|
458
|
-
'Rate limit exceeded. Try again later.',
|
|
459
|
-
);
|
|
460
|
-
}
|
|
461
|
-
if (
|
|
462
|
-
pendingSend.signal.aborted ||
|
|
463
|
-
(error instanceof Error && error.name === 'AbortError')
|
|
464
|
-
) {
|
|
465
|
-
return { stopReason: 'cancelled' };
|
|
466
|
-
}
|
|
467
|
-
throw error;
|
|
468
578
|
} finally {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
if (pendingSend.signal.aborted && terminalStopReason !== 'cancelled') {
|
|
473
|
-
return { stopReason: 'cancelled' };
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
if (terminalStopReason !== null) {
|
|
477
|
-
return { stopReason: terminalStopReason };
|
|
579
|
+
if (this.pendingPrompt === pendingSend) {
|
|
580
|
+
this.pendingPrompt = null;
|
|
581
|
+
}
|
|
478
582
|
}
|
|
479
|
-
|
|
480
|
-
return { stopReason: 'end_turn' };
|
|
481
583
|
} finally {
|
|
482
|
-
|
|
483
|
-
this.
|
|
584
|
+
try {
|
|
585
|
+
await this.emitTurnMetadata(eligibility);
|
|
586
|
+
} catch (error) {
|
|
587
|
+
this.logger.debug(() => `emitTurnMetadata ERROR: ${String(error)}`);
|
|
484
588
|
}
|
|
485
589
|
}
|
|
486
590
|
}
|
|
487
591
|
|
|
488
|
-
private async
|
|
489
|
-
|
|
592
|
+
private async runPromptTurn(
|
|
593
|
+
params: acp.PromptRequest,
|
|
490
594
|
pendingSend: AbortController,
|
|
491
595
|
promptId: string,
|
|
492
596
|
promptGeneration: number,
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
597
|
+
): Promise<acp.PromptResponse> {
|
|
598
|
+
return executePromptTurn(
|
|
599
|
+
{
|
|
600
|
+
pathResolver: this.pathResolver,
|
|
601
|
+
emojiFilterMode: (this.config.getEphemeralSetting('emojifilter') ??
|
|
602
|
+
'auto') as FilterConfiguration['mode'],
|
|
603
|
+
streamDeps: this.buildStreamDeps(promptGeneration, pendingSend),
|
|
604
|
+
},
|
|
605
|
+
params,
|
|
606
|
+
pendingSend,
|
|
497
607
|
promptId,
|
|
608
|
+
promptGeneration,
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private buildStreamDeps(
|
|
613
|
+
promptGeneration: number,
|
|
614
|
+
pendingSend: AbortController,
|
|
615
|
+
): SessionStreamDeps {
|
|
616
|
+
return {
|
|
617
|
+
agent: this.agent,
|
|
618
|
+
terminals: this.terminals,
|
|
619
|
+
sendUpdate: (update) => this.sendUpdate(update),
|
|
620
|
+
sendUsage: (usage) => this.sendUsageUpdate(usage),
|
|
621
|
+
handleConfirmation: (confirmation) =>
|
|
622
|
+
this.handleToolConfirmation(
|
|
623
|
+
confirmation,
|
|
624
|
+
promptGeneration,
|
|
625
|
+
pendingSend,
|
|
626
|
+
),
|
|
627
|
+
isPromptStale: (gen, send) => this.isPromptStale(gen, send),
|
|
498
628
|
maxTurns: this.config.getMaxSessionTurns(),
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
629
|
+
logger: this.logger,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
private async emitTurnMetadata(eligibility: {
|
|
634
|
+
readonly wonTitle: boolean;
|
|
635
|
+
readonly title: string | undefined;
|
|
636
|
+
}): Promise<void> {
|
|
637
|
+
const { updates } = this.sessionInfo.recordTurn(new Date().toISOString());
|
|
638
|
+
const updatedAt = updates[0]?.updatedAt ?? undefined;
|
|
639
|
+
if (eligibility.wonTitle && eligibility.title !== undefined) {
|
|
640
|
+
await this.sendTitleUpdate(
|
|
641
|
+
buildSessionInfoUpdate({ title: eligibility.title, updatedAt }),
|
|
642
|
+
eligibility.title,
|
|
513
643
|
);
|
|
514
|
-
|
|
515
|
-
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
for (const update of updates) {
|
|
647
|
+
if (typeof update.title === 'string') {
|
|
648
|
+
await this.sendTitleUpdate(update, update.title);
|
|
649
|
+
} else {
|
|
650
|
+
await this.sendUpdate(update);
|
|
516
651
|
}
|
|
517
652
|
}
|
|
518
|
-
return terminalStopReason;
|
|
519
653
|
}
|
|
520
654
|
|
|
521
|
-
private async
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
if (thoughtText.length > 0) {
|
|
534
|
-
batcher.append(thoughtText, true);
|
|
535
|
-
}
|
|
536
|
-
return null;
|
|
537
|
-
}
|
|
538
|
-
case 'tool-call':
|
|
539
|
-
await batcher.flush();
|
|
540
|
-
await emitToolCallStart(event.call, (u) => this.sendUpdate(u));
|
|
541
|
-
return null;
|
|
542
|
-
case 'tool-status':
|
|
543
|
-
await batcher.flush();
|
|
544
|
-
await emitToolStatus(event.update, (u) => this.sendUpdate(u));
|
|
545
|
-
return null;
|
|
546
|
-
case 'tool-result':
|
|
547
|
-
await batcher.flush();
|
|
548
|
-
await emitToolResult(event.result, (u) => this.sendUpdate(u));
|
|
549
|
-
return null;
|
|
550
|
-
case 'tool-confirmation':
|
|
551
|
-
await batcher.flush();
|
|
552
|
-
await this.handleToolConfirmation(event, promptGeneration, pendingSend);
|
|
553
|
-
return null;
|
|
554
|
-
case 'done':
|
|
555
|
-
await batcher.flush();
|
|
556
|
-
return mapDoneReasonToStopReason(event.reason);
|
|
557
|
-
case 'error':
|
|
558
|
-
await batcher.flush();
|
|
559
|
-
throw this.translateErrorEvent(event);
|
|
560
|
-
case 'idle-timeout':
|
|
561
|
-
await batcher.flush();
|
|
562
|
-
throw this.translateIdleTimeout(event);
|
|
563
|
-
case 'invalid-stream':
|
|
564
|
-
await batcher.flush();
|
|
565
|
-
throw new Error(
|
|
566
|
-
'Agent produced an invalid stream that could not be recovered.',
|
|
567
|
-
);
|
|
568
|
-
case 'hook-blocked':
|
|
569
|
-
await batcher.flush();
|
|
570
|
-
throw new Error(
|
|
571
|
-
event.info.systemMessage ?? 'Agent stopped by a hook blocker.',
|
|
572
|
-
);
|
|
573
|
-
case 'loop-detected':
|
|
574
|
-
// The agent detects a tool-call loop. Map to end_turn so the client
|
|
575
|
-
// sees a clean turn end rather than an indefinite hang.
|
|
576
|
-
await batcher.flush();
|
|
577
|
-
return 'end_turn';
|
|
578
|
-
case 'notice':
|
|
579
|
-
// Informational notices have no direct ACP SessionUpdate; surface as
|
|
580
|
-
// an agent message so the user sees them.
|
|
581
|
-
await batcher.flush();
|
|
582
|
-
await this.sendUpdate({
|
|
583
|
-
sessionUpdate: 'agent_message_chunk',
|
|
584
|
-
content: { type: 'text', text: event.message },
|
|
585
|
-
});
|
|
586
|
-
return null;
|
|
587
|
-
case 'usage': {
|
|
588
|
-
await batcher.flush();
|
|
589
|
-
await this.sendUsageUpdate(event.usage);
|
|
590
|
-
return null;
|
|
591
|
-
}
|
|
592
|
-
case 'context-warning':
|
|
593
|
-
case 'compression':
|
|
594
|
-
case 'model-info':
|
|
595
|
-
case 'retry':
|
|
596
|
-
case 'citation':
|
|
597
|
-
// These variants have no faithful ACP translation: usage is mapped
|
|
598
|
-
// separately, compression/model-info/citation are metadata, retry is
|
|
599
|
-
// an internal agent detail, and context-warning is advisory. They are
|
|
600
|
-
// intentionally consumed without surfacing to ACP.
|
|
601
|
-
return null;
|
|
602
|
-
default: {
|
|
603
|
-
// Exhaustiveness guard: any future AgentEvent variant added to the
|
|
604
|
-
// union without an explicit case above will fail to type-check here.
|
|
605
|
-
const exhaustive: never = event;
|
|
606
|
-
throw new Error(`Unhandled agent event: ${String(exhaustive)}`);
|
|
607
|
-
}
|
|
655
|
+
private async sendTitleUpdate(
|
|
656
|
+
update: acp.SessionInfoUpdate & { sessionUpdate: 'session_info_update' },
|
|
657
|
+
title: string,
|
|
658
|
+
): Promise<void> {
|
|
659
|
+
try {
|
|
660
|
+
await this.sendUpdateStrict(update);
|
|
661
|
+
} catch (error) {
|
|
662
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
663
|
+
this.logger.debug(
|
|
664
|
+
() => `sendTitleUpdate ERROR (will retry next turn): ${msg}`,
|
|
665
|
+
);
|
|
666
|
+
this.sessionInfo.markPendingTitle(title);
|
|
608
667
|
}
|
|
609
668
|
}
|
|
610
669
|
|
|
670
|
+
private recordMetadataTitle(title: string | undefined): void {
|
|
671
|
+
const recording = this.config.getSessionRecordingService();
|
|
672
|
+
if (recording?.isActive() === true)
|
|
673
|
+
recording.recordSessionMetadata(title ?? null);
|
|
674
|
+
}
|
|
611
675
|
private async sendUsageUpdate(
|
|
612
676
|
usage: Extract<AgentEvent, { type: 'usage' }>['usage'],
|
|
613
677
|
): Promise<void> {
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
await this.sendUpdate({
|
|
620
|
-
sessionUpdate: 'usage_update',
|
|
621
|
-
used: size,
|
|
622
|
-
size,
|
|
623
|
-
});
|
|
678
|
+
const update = buildUsageUpdate(
|
|
679
|
+
usage,
|
|
680
|
+
getTokenLimitForConfiguredContext(this.config.getModel(), this.config),
|
|
681
|
+
);
|
|
682
|
+
if (update !== null) await this.sendUpdate(update);
|
|
624
683
|
}
|
|
625
|
-
|
|
626
684
|
private isPromptStale(
|
|
627
685
|
promptGeneration: number,
|
|
628
686
|
pendingSend: AbortController,
|
|
@@ -633,7 +691,6 @@ export class Session {
|
|
|
633
691
|
pendingSend.signal.aborted
|
|
634
692
|
);
|
|
635
693
|
}
|
|
636
|
-
|
|
637
694
|
private async handleToolConfirmation(
|
|
638
695
|
event: Extract<AgentEvent, { type: 'tool-confirmation' }>,
|
|
639
696
|
promptGeneration: number,
|
|
@@ -647,12 +704,10 @@ export class Session {
|
|
|
647
704
|
settled: false,
|
|
648
705
|
});
|
|
649
706
|
});
|
|
650
|
-
|
|
651
707
|
if (this.isPromptStale(promptGeneration, pendingSend)) {
|
|
652
708
|
this.settleConfirmation(confirmationId);
|
|
653
709
|
return;
|
|
654
710
|
}
|
|
655
|
-
|
|
656
711
|
let result: PermissionRoundTripResult | null;
|
|
657
712
|
try {
|
|
658
713
|
result = await Promise.race([
|
|
@@ -662,6 +717,7 @@ export class Session {
|
|
|
662
717
|
event.confirmation.name,
|
|
663
718
|
event.confirmation.details,
|
|
664
719
|
this.connection,
|
|
720
|
+
this.agent.tools.get(event.confirmation.name)?.kind,
|
|
665
721
|
),
|
|
666
722
|
cancelled,
|
|
667
723
|
] as const);
|
|
@@ -669,7 +725,6 @@ export class Session {
|
|
|
669
725
|
this.settleConfirmation(confirmationId);
|
|
670
726
|
throw error;
|
|
671
727
|
}
|
|
672
|
-
|
|
673
728
|
const state = this.activeConfirmations.get(confirmationId);
|
|
674
729
|
if (
|
|
675
730
|
result === null ||
|
|
@@ -679,7 +734,6 @@ export class Session {
|
|
|
679
734
|
) {
|
|
680
735
|
return;
|
|
681
736
|
}
|
|
682
|
-
|
|
683
737
|
state.settled = true;
|
|
684
738
|
this.activeConfirmations.delete(confirmationId);
|
|
685
739
|
try {
|
|
@@ -696,43 +750,16 @@ export class Session {
|
|
|
696
750
|
);
|
|
697
751
|
}
|
|
698
752
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
(v) => v.length > 0,
|
|
703
|
-
);
|
|
704
|
-
return parts.join(parts.length > 1 ? ' ' : '');
|
|
753
|
+
private async sendUpdateStrict(update: acp.SessionUpdate): Promise<void> {
|
|
754
|
+
this.logger.debug(() => describeSessionUpdateForLog(update));
|
|
755
|
+
await this.connection.sessionUpdate({ sessionId: this.id, update });
|
|
705
756
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
event: Extract<AgentEvent, { type: 'error' }>,
|
|
709
|
-
): Error {
|
|
710
|
-
const error = new Error(event.error.message);
|
|
711
|
-
if (event.error.status !== undefined) {
|
|
712
|
-
Object.assign(error, { status: event.error.status });
|
|
713
|
-
}
|
|
714
|
-
return error;
|
|
757
|
+
sendAvailableCommands(): Promise<void> {
|
|
758
|
+
return this.sendUpdateStrict(buildAvailableCommandsUpdate());
|
|
715
759
|
}
|
|
716
|
-
|
|
717
|
-
private translateIdleTimeout(
|
|
718
|
-
event: Extract<AgentEvent, { type: 'idle-timeout' }>,
|
|
719
|
-
): Error {
|
|
720
|
-
return new Error(event.error.message);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
760
|
private async sendUpdate(update: acp.SessionUpdate): Promise<void> {
|
|
724
|
-
const params: acp.SessionNotification = { sessionId: this.id, update };
|
|
725
|
-
this.logger.debug(
|
|
726
|
-
() =>
|
|
727
|
-
`sendUpdate: ${update.sessionUpdate} ${
|
|
728
|
-
'content' in update && update.content && 'text' in update.content
|
|
729
|
-
? `(${(update.content as { text: string }).text.length} chars)`
|
|
730
|
-
: ''
|
|
731
|
-
}`,
|
|
732
|
-
);
|
|
733
761
|
try {
|
|
734
|
-
await this.
|
|
735
|
-
this.logger.debug(() => 'sendUpdate: delivered');
|
|
762
|
+
await this.sendUpdateStrict(update);
|
|
736
763
|
} catch (error) {
|
|
737
764
|
this.logger.debug(
|
|
738
765
|
() =>
|
|
@@ -740,122 +767,46 @@ export class Session {
|
|
|
740
767
|
);
|
|
741
768
|
}
|
|
742
769
|
}
|
|
743
|
-
|
|
744
770
|
debug(msg: string) {
|
|
745
|
-
if (this.config.getDebugMode())
|
|
746
|
-
|
|
771
|
+
if (this.config.getDebugMode()) debugLogger.warn(msg);
|
|
772
|
+
}
|
|
773
|
+
private sendPlanUpdate(todos: TodoUpdateEvent['todos']): Promise<void> {
|
|
774
|
+
return this.sendUpdate(buildZedPlanUpdate(todos));
|
|
775
|
+
}
|
|
776
|
+
async streamHistory(items: readonly IContent[]): Promise<void> {
|
|
777
|
+
const updates = mapHistoryToSessionUpdates(items);
|
|
778
|
+
for (const update of updates) {
|
|
779
|
+
try {
|
|
780
|
+
await this.sendUpdateStrict(update);
|
|
781
|
+
} catch (error) {
|
|
782
|
+
throw wrapReplayFailure(this.id, error);
|
|
783
|
+
}
|
|
747
784
|
}
|
|
785
|
+
this.sessionInfo.hydrateFromHistory(items);
|
|
748
786
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
status: todo.status,
|
|
754
|
-
priority: 'medium' as const,
|
|
755
|
-
}));
|
|
756
|
-
await this.sendUpdate({ sessionUpdate: 'plan', entries });
|
|
787
|
+
async replayLiveHistory(): Promise<void> {
|
|
788
|
+
await this.streamHistory(
|
|
789
|
+
await readAgentHistoryForReplay(this.agent, this.id),
|
|
790
|
+
);
|
|
757
791
|
}
|
|
758
|
-
|
|
759
792
|
async dispose(): Promise<void> {
|
|
760
793
|
try {
|
|
761
794
|
todoEvents.offTodoUpdated(this.todoListener);
|
|
795
|
+
this.stopConfigUpdates();
|
|
762
796
|
this.settleActiveConfirmation();
|
|
797
|
+
await this.terminals
|
|
798
|
+
?.settleAll()
|
|
799
|
+
.catch((error) =>
|
|
800
|
+
this.logger.debug(() => `Terminal cleanup failed: ${error}`),
|
|
801
|
+
);
|
|
763
802
|
this.pendingPrompt?.abort();
|
|
764
803
|
this.pendingPrompt = null;
|
|
765
804
|
} finally {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
const BATCH_INTERVAL_MS = 100;
|
|
776
|
-
|
|
777
|
-
class StreamBatcher {
|
|
778
|
-
private pendingChunks: Array<{ kind: 'text' | 'thought'; text: string }> = [];
|
|
779
|
-
private batchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
780
|
-
private flushChain: Promise<void> = Promise.resolve();
|
|
781
|
-
|
|
782
|
-
constructor(
|
|
783
|
-
private readonly emojiFilter: EmojiFilter,
|
|
784
|
-
private readonly sendUpdate: (update: acp.SessionUpdate) => Promise<void>,
|
|
785
|
-
) {}
|
|
786
|
-
|
|
787
|
-
append(text: string, isThought: boolean): void {
|
|
788
|
-
const filterResult = isThought
|
|
789
|
-
? this.emojiFilter.filterText(text)
|
|
790
|
-
: this.emojiFilter.filterStreamChunk(text);
|
|
791
|
-
if (filterResult.blocked) {
|
|
792
|
-
const pending = this.flushChain
|
|
793
|
-
.then(() => this.doFlush())
|
|
794
|
-
.then(() =>
|
|
795
|
-
this.sendUpdate({
|
|
796
|
-
sessionUpdate: 'agent_message_chunk',
|
|
797
|
-
content: {
|
|
798
|
-
type: 'text',
|
|
799
|
-
text: '[Error: Response blocked due to emoji detection]',
|
|
800
|
-
},
|
|
801
|
-
}),
|
|
805
|
+
await this.agent
|
|
806
|
+
.dispose()
|
|
807
|
+
.catch((e: unknown) =>
|
|
808
|
+
this.logger.debug(() => `Failed to dispose Zed session agent: ${e}`),
|
|
802
809
|
);
|
|
803
|
-
this.flushChain = pending.catch(() => undefined);
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
const filteredText =
|
|
807
|
-
typeof filterResult.filtered === 'string' ? filterResult.filtered : '';
|
|
808
|
-
if (filteredText.length === 0) {
|
|
809
|
-
return;
|
|
810
|
-
}
|
|
811
|
-
this.appendPendingChunk(isThought ? 'thought' : 'text', filteredText);
|
|
812
|
-
this.batchTimer ??= setTimeout(() => {
|
|
813
|
-
this.batchTimer = null;
|
|
814
|
-
void this.flush();
|
|
815
|
-
}, BATCH_INTERVAL_MS);
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
async flush(): Promise<void> {
|
|
819
|
-
if (this.batchTimer !== null) {
|
|
820
|
-
clearTimeout(this.batchTimer);
|
|
821
|
-
this.batchTimer = null;
|
|
822
|
-
}
|
|
823
|
-
const pending = this.flushChain
|
|
824
|
-
.then(() => this.doFlush())
|
|
825
|
-
.then(() => this.flushEmojiBuffer());
|
|
826
|
-
this.flushChain = pending.catch(() => undefined);
|
|
827
|
-
await pending;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
private async flushEmojiBuffer(): Promise<void> {
|
|
831
|
-
const remaining = this.emojiFilter.flushBuffer();
|
|
832
|
-
if (remaining.length === 0) {
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
this.appendPendingChunk('text', remaining);
|
|
836
|
-
await this.doFlush();
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
private appendPendingChunk(kind: 'text' | 'thought', text: string): void {
|
|
840
|
-
const lastChunk = this.pendingChunks.at(-1);
|
|
841
|
-
if (lastChunk?.kind === kind) {
|
|
842
|
-
lastChunk.text += text;
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
this.pendingChunks.push({ kind, text });
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
private async doFlush(): Promise<void> {
|
|
849
|
-
const chunks = this.pendingChunks;
|
|
850
|
-
this.pendingChunks = [];
|
|
851
|
-
for (const chunk of chunks) {
|
|
852
|
-
await this.sendUpdate({
|
|
853
|
-
sessionUpdate:
|
|
854
|
-
chunk.kind === 'thought'
|
|
855
|
-
? 'agent_thought_chunk'
|
|
856
|
-
: 'agent_message_chunk',
|
|
857
|
-
content: { type: 'text', text: chunk.text },
|
|
858
|
-
});
|
|
859
810
|
}
|
|
860
811
|
}
|
|
861
812
|
}
|