ds-harness-remote 0.4.26 → 0.5.0
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/README.md +6 -6
- package/dist/acp/adapter.d.ts +19 -0
- package/dist/acp/adapter.d.ts.map +1 -0
- package/dist/acp/adapter.js +14 -0
- package/dist/acp/adapter.js.map +1 -0
- package/dist/acp/adapters/antigravity/executor.d.ts +20 -0
- package/dist/acp/adapters/antigravity/executor.d.ts.map +1 -0
- package/dist/acp/adapters/antigravity/executor.js +99 -0
- package/dist/acp/adapters/antigravity/executor.js.map +1 -0
- package/dist/acp/adapters/antigravity/image-store.d.ts +9 -0
- package/dist/acp/adapters/antigravity/image-store.d.ts.map +1 -0
- package/dist/acp/adapters/antigravity/image-store.js +116 -0
- package/dist/acp/adapters/antigravity/image-store.js.map +1 -0
- package/dist/acp/adapters/antigravity/index.d.ts +5 -0
- package/dist/acp/adapters/antigravity/index.d.ts.map +1 -0
- package/dist/acp/adapters/antigravity/index.js +5 -0
- package/dist/acp/adapters/antigravity/index.js.map +1 -0
- package/dist/acp/adapters/antigravity/models.d.ts +35 -0
- package/dist/acp/adapters/antigravity/models.d.ts.map +1 -0
- package/dist/acp/adapters/antigravity/models.js +59 -0
- package/dist/acp/adapters/antigravity/models.js.map +1 -0
- package/dist/acp/adapters/antigravity/service.d.ts +35 -0
- package/dist/acp/adapters/antigravity/service.d.ts.map +1 -0
- package/dist/acp/adapters/antigravity/service.js +67 -0
- package/dist/acp/adapters/antigravity/service.js.map +1 -0
- package/dist/acp/adapters/antigravity/transcript-loader.d.ts +21 -0
- package/dist/acp/adapters/antigravity/transcript-loader.d.ts.map +1 -0
- package/dist/acp/adapters/antigravity/transcript-loader.js +356 -0
- package/dist/acp/adapters/antigravity/transcript-loader.js.map +1 -0
- package/dist/acp/adapters/antigravity/transcript-watcher.d.ts +28 -0
- package/dist/acp/adapters/antigravity/transcript-watcher.d.ts.map +1 -0
- package/dist/acp/adapters/antigravity/transcript-watcher.js +157 -0
- package/dist/acp/adapters/antigravity/transcript-watcher.js.map +1 -0
- package/dist/acp/adapters/antigravity/types.d.ts +56 -0
- package/dist/acp/adapters/antigravity/types.d.ts.map +1 -0
- package/dist/acp/adapters/antigravity/types.js +2 -0
- package/dist/acp/adapters/antigravity/types.js.map +1 -0
- package/dist/acp/adapters/antigravity-process.d.ts +69 -0
- package/dist/acp/adapters/antigravity-process.d.ts.map +1 -0
- package/dist/acp/adapters/antigravity-process.js +566 -0
- package/dist/acp/adapters/antigravity-process.js.map +1 -0
- package/dist/acp/adapters/antigravity.d.ts +8 -0
- package/dist/acp/adapters/antigravity.d.ts.map +1 -0
- package/dist/acp/adapters/antigravity.js +9 -0
- package/dist/acp/adapters/antigravity.js.map +1 -0
- package/dist/acp/adapters/cursor-process.d.ts +75 -0
- package/dist/acp/adapters/cursor-process.d.ts.map +1 -0
- package/dist/acp/adapters/cursor-process.js +273 -0
- package/dist/acp/adapters/cursor-process.js.map +1 -0
- package/dist/acp/adapters/cursor.d.ts +8 -0
- package/dist/acp/adapters/cursor.d.ts.map +1 -0
- package/dist/acp/adapters/cursor.js +8 -0
- package/dist/acp/adapters/cursor.js.map +1 -0
- package/dist/acp/gateway.d.ts +108 -0
- package/dist/acp/gateway.d.ts.map +1 -0
- package/dist/acp/gateway.js +1040 -0
- package/dist/acp/gateway.js.map +1 -0
- package/dist/acp/image-content.d.ts +19 -0
- package/dist/acp/image-content.d.ts.map +1 -0
- package/dist/acp/image-content.js +76 -0
- package/dist/acp/image-content.js.map +1 -0
- package/dist/acp/method-policy.d.ts +239 -0
- package/dist/acp/method-policy.d.ts.map +1 -0
- package/dist/acp/method-policy.js +102 -0
- package/dist/acp/method-policy.js.map +1 -0
- package/dist/acp/peer-bridge.d.ts +59 -0
- package/dist/acp/peer-bridge.d.ts.map +1 -0
- package/dist/acp/peer-bridge.js +367 -0
- package/dist/acp/peer-bridge.js.map +1 -0
- package/dist/acp/virtual-harness.d.ts +181 -0
- package/dist/acp/virtual-harness.d.ts.map +1 -0
- package/dist/acp/virtual-harness.js +1556 -0
- package/dist/acp/virtual-harness.js.map +1 -0
- package/dist/acp/workspace-tools.d.ts +13 -0
- package/dist/acp/workspace-tools.d.ts.map +1 -0
- package/dist/acp/workspace-tools.js +46 -0
- package/dist/acp/workspace-tools.js.map +1 -0
- package/dist/acp.d.ts.map +1 -1
- package/dist/acp.js +1 -0
- package/dist/acp.js.map +1 -1
- package/dist/api-proxy-switch.d.ts +2 -0
- package/dist/api-proxy-switch.d.ts.map +1 -1
- package/dist/api-proxy-switch.js +9 -0
- package/dist/api-proxy-switch.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -3
- package/dist/cli.js.map +1 -1
- package/dist/client-runtime.d.ts +25 -1
- package/dist/client-runtime.d.ts.map +1 -1
- package/dist/client-runtime.js +222 -8
- package/dist/client-runtime.js.map +1 -1
- package/dist/client.github.js +327 -190
- package/dist/client.js +327 -190
- package/dist/client.js.map +1 -1
- package/dist/codex/domain.d.ts +4 -1
- package/dist/codex/domain.d.ts.map +1 -1
- package/dist/codex/domain.js +54 -4
- package/dist/codex/domain.js.map +1 -1
- package/dist/codex/virtual-harness.d.ts.map +1 -1
- package/dist/codex/virtual-harness.js +3 -0
- package/dist/codex/virtual-harness.js.map +1 -1
- package/dist/codex-workspace-bridge.d.ts +7 -105
- package/dist/codex-workspace-bridge.d.ts.map +1 -1
- package/dist/codex-workspace-bridge.js +10 -498
- package/dist/codex-workspace-bridge.js.map +1 -1
- package/dist/config.d.ts +28 -10
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +49 -5
- package/dist/config.js.map +1 -1
- package/dist/connection-controller.d.ts.map +1 -1
- package/dist/connection-controller.js +7 -4
- package/dist/connection-controller.js.map +1 -1
- package/dist/control-runtime.d.ts +1 -0
- package/dist/control-runtime.d.ts.map +1 -1
- package/dist/control-runtime.js +65 -10
- package/dist/control-runtime.js.map +1 -1
- package/dist/harness-version.d.ts.map +1 -1
- package/dist/harness-version.js +16 -3
- package/dist/harness-version.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13197 -8204
- package/dist/index.js.map +4 -4
- package/dist/loopback-host.d.ts +2 -2
- package/dist/loopback-host.d.ts.map +1 -1
- package/dist/loopback-host.js +41 -16
- package/dist/loopback-host.js.map +1 -1
- package/dist/remote-typert-gateway.d.ts.map +1 -1
- package/dist/remote-typert-gateway.js +7 -4
- package/dist/remote-typert-gateway.js.map +1 -1
- package/dist/rpc-router.d.ts +6 -5
- package/dist/rpc-router.d.ts.map +1 -1
- package/dist/rpc-router.js +49 -18
- package/dist/rpc-router.js.map +1 -1
- package/dist/server-api.d.ts +2 -0
- package/dist/server-api.d.ts.map +1 -1
- package/dist/server-api.js +5 -0
- package/dist/server-api.js.map +1 -1
- package/dist/server-connection.d.ts.map +1 -1
- package/dist/server-connection.js +15 -0
- package/dist/server-connection.js.map +1 -1
- package/dist/service.d.ts +30 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +114 -12
- package/dist/service.js.map +1 -1
- package/dist/session-format-compat.d.ts +1 -0
- package/dist/session-format-compat.d.ts.map +1 -1
- package/dist/session-format-compat.js +14 -0
- package/dist/session-format-compat.js.map +1 -1
- package/dist/tui-command.js +2 -0
- package/dist/tui-command.js.map +1 -1
- package/dist/typert-gateway-switch.d.ts +2 -0
- package/dist/typert-gateway-switch.d.ts.map +1 -1
- package/dist/typert-gateway-switch.js +16 -0
- package/dist/typert-gateway-switch.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist/workspace-bridge.d.ts +117 -0
- package/dist/workspace-bridge.d.ts.map +1 -0
- package/dist/workspace-bridge.js +512 -0
- package/dist/workspace-bridge.js.map +1 -0
- package/dsh-plugin.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1556 @@
|
|
|
1
|
+
import { ACP_TOOL_CALL_ENDPOINTS, ACP_TOOL_STREAM_ENDPOINTS } from '@dsh-remote/protocol';
|
|
2
|
+
import { parseAcpImage, acpImageContent, acpImageLimits } from './image-content.js';
|
|
3
|
+
import { AgentAcpClient } from '@dsh-remote/client-core';
|
|
4
|
+
const CURSOR_SESSION_PREFIX = 'cursor:';
|
|
5
|
+
const ACP_SESSION_PREFIX = 'acp:';
|
|
6
|
+
const CURSOR_WORKSPACE_PREFIX = 'cursor:cwd:';
|
|
7
|
+
const AGY_WORKSPACE_PREFIX = 'antigravity:cwd:';
|
|
8
|
+
const CURSOR_PROVIDER = 'cursor';
|
|
9
|
+
const CURSOR_MODEL = 'cursor';
|
|
10
|
+
/** Build a backend-scoped virtual Workspace id for an absolute Host cwd. */
|
|
11
|
+
export function acpCwdWorkspaceId(path, backend) {
|
|
12
|
+
if (backend !== 'cursor' && backend !== 'antigravity')
|
|
13
|
+
throw new Error('An explicit ACP workspace backend is required.');
|
|
14
|
+
return `${backend === 'antigravity' ? AGY_WORKSPACE_PREFIX : CURSOR_WORKSPACE_PREFIX}${encodeURIComponent(path)}`;
|
|
15
|
+
}
|
|
16
|
+
export function createAcpWorkspaceView(path, backend, title) {
|
|
17
|
+
const now = new Date().toISOString();
|
|
18
|
+
const label = title?.trim() || path.split(/[\\/]/).filter(Boolean).at(-1) || path;
|
|
19
|
+
return {
|
|
20
|
+
workspaceId: acpCwdWorkspaceId(path, backend),
|
|
21
|
+
path,
|
|
22
|
+
title: label,
|
|
23
|
+
sessionIds: [],
|
|
24
|
+
sessionCount: 0,
|
|
25
|
+
createdAt: now,
|
|
26
|
+
updatedAt: now,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Discover ACP virtual workspaces visible on the Host. */
|
|
30
|
+
export async function discoverAcpVirtualWorkspaces(client, backend, signal) {
|
|
31
|
+
if (client?.listWorkspaces !== undefined) {
|
|
32
|
+
try {
|
|
33
|
+
const items = await client.listWorkspaces(backend, signal);
|
|
34
|
+
if (Array.isArray(items) && items.length > 0) {
|
|
35
|
+
return items
|
|
36
|
+
.filter(item => typeof item?.path === 'string' && item.path.trim() !== '')
|
|
37
|
+
.map(item => {
|
|
38
|
+
const view = createAcpWorkspaceView(item.path, backend, item.title);
|
|
39
|
+
if (typeof item.sessionCount === 'number')
|
|
40
|
+
view.sessionCount = item.sessionCount;
|
|
41
|
+
return view;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Discovery is optional; an unavailable Host returns no workspaces.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Browser-safe, in-memory Harness projection for the independent Agent ACP domain. Projects
|
|
53
|
+
* cwd workspaces and ACP sessions onto the native DSH Workspace / Session /
|
|
54
|
+
* Composer surface without writing DSH SessionStore.
|
|
55
|
+
*/
|
|
56
|
+
export class AcpVirtualHarness {
|
|
57
|
+
client;
|
|
58
|
+
host;
|
|
59
|
+
backend;
|
|
60
|
+
api;
|
|
61
|
+
workspaceById = new Map();
|
|
62
|
+
sessions = new Map();
|
|
63
|
+
workspaceStreams = new Set();
|
|
64
|
+
controlStreams = new Set();
|
|
65
|
+
eventStreams = new Map();
|
|
66
|
+
rcMuxStreams = new Set();
|
|
67
|
+
rcHostStreams = new Set();
|
|
68
|
+
follows = new Set();
|
|
69
|
+
pendingApprovals = new Map();
|
|
70
|
+
selectedWorkspaceId;
|
|
71
|
+
selectedModels = new Map();
|
|
72
|
+
lastProjectionSeq = 0;
|
|
73
|
+
imageAttachments = new Map();
|
|
74
|
+
closed = false;
|
|
75
|
+
constructor(client, host, backend = 'cursor') {
|
|
76
|
+
this.client = client;
|
|
77
|
+
this.host = host;
|
|
78
|
+
this.backend = backend;
|
|
79
|
+
this.api = this.createApiProxy();
|
|
80
|
+
}
|
|
81
|
+
static remote(core, host, backend = 'cursor', capabilities = []) {
|
|
82
|
+
return new AcpVirtualHarness(new AgentAcpClient(core, capabilities), host, backend);
|
|
83
|
+
}
|
|
84
|
+
async workspaces() {
|
|
85
|
+
return [...this.workspaceById.values()];
|
|
86
|
+
}
|
|
87
|
+
backendLabel() {
|
|
88
|
+
return this.backend === 'antigravity' ? 'Antigravity' : 'Cursor';
|
|
89
|
+
}
|
|
90
|
+
async selectWorkspace(workspaceId) {
|
|
91
|
+
const workspace = this.workspaceById.get(workspaceId) ?? recreateWorkspaceFromId(workspaceId, this.backend);
|
|
92
|
+
if (workspace === undefined)
|
|
93
|
+
throw new Error(`The selected ${this.backendLabel()} workspace is no longer available.`);
|
|
94
|
+
this.workspaceById.set(workspace.workspaceId, workspace);
|
|
95
|
+
this.selectedWorkspaceId = workspace.workspaceId;
|
|
96
|
+
await this.discoverAndAttachSessions(workspace);
|
|
97
|
+
if (workspace.sessionIds.length === 0) {
|
|
98
|
+
await this.ensureInitialSession(workspace);
|
|
99
|
+
}
|
|
100
|
+
this.publishWorkspaceBaseline();
|
|
101
|
+
return this.workspaceById.get(workspace.workspaceId) ?? workspace;
|
|
102
|
+
}
|
|
103
|
+
async selectOrCreateWorkspace(path) {
|
|
104
|
+
const trimmed = path.trim();
|
|
105
|
+
if (trimmed.length === 0)
|
|
106
|
+
throw new Error(`A ${this.backendLabel()} working directory is required.`);
|
|
107
|
+
const existing = [...this.workspaceById.values()].find(item => item.path === trimmed);
|
|
108
|
+
const workspace = existing ?? createAcpWorkspaceView(trimmed, this.backend);
|
|
109
|
+
this.workspaceById.set(workspace.workspaceId, workspace);
|
|
110
|
+
this.selectedWorkspaceId = workspace.workspaceId;
|
|
111
|
+
await this.discoverAndAttachSessions(workspace);
|
|
112
|
+
if (workspace.sessionIds.length === 0) {
|
|
113
|
+
await this.ensureInitialSession(workspace);
|
|
114
|
+
}
|
|
115
|
+
this.publishWorkspaceBaseline();
|
|
116
|
+
return this.workspaceById.get(workspace.workspaceId) ?? workspace;
|
|
117
|
+
}
|
|
118
|
+
async preferredSessionId() {
|
|
119
|
+
const selected = this.selectedWorkspaceId === undefined
|
|
120
|
+
? undefined
|
|
121
|
+
: this.workspaceById.get(this.selectedWorkspaceId);
|
|
122
|
+
const sessionIds = selected?.sessionIds ?? [];
|
|
123
|
+
let bestSessionId;
|
|
124
|
+
let bestUpdatedAt = -1;
|
|
125
|
+
for (const sessionId of sessionIds) {
|
|
126
|
+
const session = this.sessions.get(sessionId);
|
|
127
|
+
if (session !== undefined && !session.running && !session.blank && session.updatedAt > bestUpdatedAt) {
|
|
128
|
+
bestUpdatedAt = session.updatedAt;
|
|
129
|
+
bestSessionId = sessionId;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (bestSessionId !== undefined)
|
|
133
|
+
return bestSessionId;
|
|
134
|
+
for (const sessionId of sessionIds) {
|
|
135
|
+
const session = this.sessions.get(sessionId);
|
|
136
|
+
if (session !== undefined && !session.running)
|
|
137
|
+
return sessionId;
|
|
138
|
+
}
|
|
139
|
+
return sessionIds[0] ?? sessionIds.at(-1);
|
|
140
|
+
}
|
|
141
|
+
async invoke(request) {
|
|
142
|
+
const result = await this.dispatch(`${request.namespace}/${request.method}`, { args: request.args }, request.signal ?? new AbortController().signal);
|
|
143
|
+
if (result.ok)
|
|
144
|
+
return result.value;
|
|
145
|
+
throw Object.assign(new Error(result.error.message), {
|
|
146
|
+
isDSHRemoteError: true,
|
|
147
|
+
code: result.error.code,
|
|
148
|
+
details: result.error.details,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
async dispatch(endpoint, payload, signal) {
|
|
152
|
+
try {
|
|
153
|
+
const args = carrierArgs(payload);
|
|
154
|
+
if (ACP_TOOL_CALL_ENDPOINTS.some(item => item === endpoint)) {
|
|
155
|
+
if (!this.client.workspaceToolCall)
|
|
156
|
+
return business(failure('method-not-found', 'The Host does not support ACP workspace tools.'));
|
|
157
|
+
const sessionId = requiredString(args.workspaceFileScopeId ?? args.agentId ?? args.sessionId, 'sessionId');
|
|
158
|
+
return business(await this.client.workspaceToolCall(nativeAcpId(sessionId, this.backend), this.backend, endpoint, args, signal));
|
|
159
|
+
}
|
|
160
|
+
switch (endpoint) {
|
|
161
|
+
case '$events/result': return business(await this.answerRemoteEvent(args, signal));
|
|
162
|
+
case 'workspace/list': return business(success({
|
|
163
|
+
items: this.visibleWorkspaces().map(nativeWorkspace),
|
|
164
|
+
archivedSessionIds: [],
|
|
165
|
+
pinnedSessionIds: [],
|
|
166
|
+
}));
|
|
167
|
+
case 'workspace/create': return business(await this.createWorkspace(requestArg(args)));
|
|
168
|
+
case 'workspace/rename': return business(await this.renameWorkspace(requestArg(args)));
|
|
169
|
+
case 'workspace/delete': return business(failure('workspace-read-only', `${this.backendLabel()} virtual Workspaces cannot be deleted from Desktop yet.`));
|
|
170
|
+
case 'workspace/insertBefore': return business({
|
|
171
|
+
workspaceIds: this.visibleWorkspaces().map(item => item.workspaceId),
|
|
172
|
+
});
|
|
173
|
+
case 'workspace/insertSessionBefore': return business(await this.workspaceForSession(requestArg(args)));
|
|
174
|
+
case 'workspace/archiveSession': return business(await this.archiveSession(requestArg(args)));
|
|
175
|
+
case 'session/list': return business(success({ items: await this.handleSessionList() }));
|
|
176
|
+
case 'session/search': return business(success({ items: [], hasMore: false }));
|
|
177
|
+
case 'session/create': return business(await this.createSession(requestArg(args), signal));
|
|
178
|
+
case 'session/fork': return business(failure('bad-request', `${this.backendLabel()} Remote does not support session fork yet.`));
|
|
179
|
+
case 'session/history': return business(await this.sessionHistory(requestArg(args)));
|
|
180
|
+
case 'session/page': return business(await this.sessionPage(requestArg(args)));
|
|
181
|
+
case 'session/prompt': return business(await this.prompt(requestArg(args), signal));
|
|
182
|
+
case 'session/cancel': return business(await this.cancel(requestArg(args), signal));
|
|
183
|
+
case 'session/rename': return business(await this.renameSession(requestArg(args)));
|
|
184
|
+
case 'session/updateQueue': return business(failure('queue-item-not-found', `${this.backendLabel()} does not expose a DSH inbox queue.`));
|
|
185
|
+
case 'session/attachment': return business(await this.attachment(requestArg(args)));
|
|
186
|
+
case 'session/modelCatalog': {
|
|
187
|
+
const sessionId = await this.preferredSessionId();
|
|
188
|
+
if (this.backend !== 'antigravity' || !sessionId)
|
|
189
|
+
return business(success(modelCatalog(this.backend)));
|
|
190
|
+
const models = await this.hostSessionModels(sessionId);
|
|
191
|
+
return business(success({ default: models.current, routableProviders: models.routable ? [models.current.provider] : [], groups: models.groups, failures: models.failures }));
|
|
192
|
+
}
|
|
193
|
+
case 'session/models': {
|
|
194
|
+
const rawId = extractSessionId(requestArg(args));
|
|
195
|
+
nativeAcpId(rawId, this.backend);
|
|
196
|
+
if (this.backend === 'antigravity')
|
|
197
|
+
return business(success(await this.hostSessionModels(rawId)));
|
|
198
|
+
const catalog = modelCatalog(this.backend);
|
|
199
|
+
return business(success({
|
|
200
|
+
current: this.modelSelection(rawId),
|
|
201
|
+
routable: true,
|
|
202
|
+
groups: catalog.groups,
|
|
203
|
+
failures: [],
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
case 'session/selectModel': return business(await this.selectModel(requestArg(args)));
|
|
207
|
+
case 'session/canOpenWorkspacePath': return business(this.selectedWorkspaceId !== undefined);
|
|
208
|
+
case 'session/openWorkspacePath': return business(failure('bad-request', `Opening Host paths is unavailable in ${this.backendLabel()} mode.`));
|
|
209
|
+
case 'host/describe': return business(success(this.describeHost()));
|
|
210
|
+
case 'host/listDirectory':
|
|
211
|
+
case 'directoryPicker/list': return business(await this.listDirectory(requestArg(args), signal));
|
|
212
|
+
case 'skills/list': return business(success({ items: [] }));
|
|
213
|
+
case 'commands/list': return business([]);
|
|
214
|
+
case 'commands/execute': return business(undefined);
|
|
215
|
+
default:
|
|
216
|
+
return fail('method-not-found', `${this.backendLabel()} virtual Harness does not implement ${endpoint}.`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
return failFrom(error);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async open(endpoint, payload, signal) {
|
|
224
|
+
const args = carrierArgs(payload);
|
|
225
|
+
if (ACP_TOOL_STREAM_ENDPOINTS.some(item => item === endpoint)) {
|
|
226
|
+
const sessionId = requiredString(args.workspaceFileScopeId ?? args.agentId ?? args.sessionId, 'sessionId');
|
|
227
|
+
const queue = new AsyncValueQueue(signal);
|
|
228
|
+
const stream = await this.client.openStream(nativeAcpId(sessionId, this.backend), frame => {
|
|
229
|
+
if (frame.frame.method === 'dsh/workspaceTool')
|
|
230
|
+
queue.push(record(frame.frame.params).value);
|
|
231
|
+
}, () => queue.close(), signal, { backend: this.backend, endpoint: endpoint, args });
|
|
232
|
+
return queue.iterate(() => { void stream.close().catch(() => undefined); });
|
|
233
|
+
}
|
|
234
|
+
if (endpoint === 'workspace/follow')
|
|
235
|
+
return this.workspaceFollow(signal);
|
|
236
|
+
if (endpoint === 'session/control')
|
|
237
|
+
return this.sessionControl(signal);
|
|
238
|
+
if (endpoint === 'session/follow')
|
|
239
|
+
return this.sessionFollow(requestArg(args), signal);
|
|
240
|
+
if (endpoint === '$events')
|
|
241
|
+
return this.remoteEvents(signal);
|
|
242
|
+
throw Object.assign(new Error(`${this.backendLabel()} virtual Harness does not implement stream ${endpoint}.`), {
|
|
243
|
+
isDSHRemoteError: true,
|
|
244
|
+
code: 'method-not-found',
|
|
245
|
+
details: {},
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
async close() {
|
|
249
|
+
if (this.closed)
|
|
250
|
+
return;
|
|
251
|
+
this.closed = true;
|
|
252
|
+
for (const stream of this.workspaceStreams)
|
|
253
|
+
stream.close();
|
|
254
|
+
for (const stream of this.controlStreams)
|
|
255
|
+
stream.close();
|
|
256
|
+
for (const stream of this.eventStreams.values())
|
|
257
|
+
stream.close();
|
|
258
|
+
for (const stream of this.rcMuxStreams)
|
|
259
|
+
stream.close();
|
|
260
|
+
for (const stream of this.rcHostStreams)
|
|
261
|
+
stream.close();
|
|
262
|
+
this.workspaceStreams.clear();
|
|
263
|
+
this.controlStreams.clear();
|
|
264
|
+
this.eventStreams.clear();
|
|
265
|
+
this.rcMuxStreams.clear();
|
|
266
|
+
this.rcHostStreams.clear();
|
|
267
|
+
const follows = [...this.follows];
|
|
268
|
+
this.follows.clear();
|
|
269
|
+
for (const follow of follows) {
|
|
270
|
+
follow.queue.close();
|
|
271
|
+
await follow.close?.().catch(() => undefined);
|
|
272
|
+
}
|
|
273
|
+
this.pendingApprovals.clear();
|
|
274
|
+
this.imageAttachments.clear();
|
|
275
|
+
}
|
|
276
|
+
visibleWorkspaces() {
|
|
277
|
+
if (this.selectedWorkspaceId === undefined)
|
|
278
|
+
return [...this.workspaceById.values()];
|
|
279
|
+
const selected = this.workspaceById.get(this.selectedWorkspaceId);
|
|
280
|
+
if (selected === undefined)
|
|
281
|
+
return [...this.workspaceById.values()];
|
|
282
|
+
return [
|
|
283
|
+
selected,
|
|
284
|
+
...[...this.workspaceById.values()].filter(item => item.workspaceId !== selected.workspaceId),
|
|
285
|
+
];
|
|
286
|
+
}
|
|
287
|
+
async createWorkspace(request) {
|
|
288
|
+
const path = string(request.path)?.trim();
|
|
289
|
+
if (path === undefined || path.length === 0)
|
|
290
|
+
return failure('bad-request', 'A Cursor working directory is required.');
|
|
291
|
+
const workspace = await this.selectOrCreateWorkspace(path);
|
|
292
|
+
this.publishWorkspaceBaseline();
|
|
293
|
+
return success({ workspace: nativeWorkspace(workspace), created: true });
|
|
294
|
+
}
|
|
295
|
+
async renameWorkspace(request) {
|
|
296
|
+
const workspaceId = requiredString(request.workspaceId, 'workspaceId');
|
|
297
|
+
const title = string(request.title)?.trim();
|
|
298
|
+
const workspace = this.workspaceById.get(workspaceId);
|
|
299
|
+
if (workspace === undefined)
|
|
300
|
+
return failure('workspace-not-found', 'The Cursor virtual Workspace was not found.');
|
|
301
|
+
if (title === undefined || title.length === 0)
|
|
302
|
+
return failure('bad-request', 'A Workspace name is required.');
|
|
303
|
+
const next = { ...workspace, title, updatedAt: new Date().toISOString() };
|
|
304
|
+
this.workspaceById.set(workspaceId, next);
|
|
305
|
+
this.publishWorkspaceBaseline();
|
|
306
|
+
return success({ workspace: nativeWorkspace(next) });
|
|
307
|
+
}
|
|
308
|
+
async workspaceForSession(request) {
|
|
309
|
+
const sessionId = string(request.sessionId);
|
|
310
|
+
const workspace = [...this.workspaceById.values()].find(item => sessionId !== undefined && item.sessionIds.includes(sessionId));
|
|
311
|
+
return workspace === undefined
|
|
312
|
+
? failure('workspace-not-found', 'The Cursor virtual Workspace was not found.')
|
|
313
|
+
: success({ workspace: nativeWorkspace(workspace) });
|
|
314
|
+
}
|
|
315
|
+
async archiveSession(request) {
|
|
316
|
+
const sessionId = extractSessionId(request);
|
|
317
|
+
this.sessions.delete(sessionId);
|
|
318
|
+
for (const [workspaceId, workspace] of this.workspaceById) {
|
|
319
|
+
if (!workspace.sessionIds.includes(sessionId))
|
|
320
|
+
continue;
|
|
321
|
+
const sessionIds = workspace.sessionIds.filter(id => id !== sessionId);
|
|
322
|
+
this.workspaceById.set(workspaceId, {
|
|
323
|
+
...workspace,
|
|
324
|
+
sessionIds,
|
|
325
|
+
sessionCount: sessionIds.length,
|
|
326
|
+
updatedAt: new Date().toISOString(),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
this.emitRemoteEvent('api-session/removed', [sessionId]);
|
|
330
|
+
this.publishWorkspaceBaseline();
|
|
331
|
+
return success({ archivedSessionIds: [sessionId] });
|
|
332
|
+
}
|
|
333
|
+
async createSession(request, signal) {
|
|
334
|
+
const workspaceId = string(request.workspaceId) ?? this.selectedWorkspaceId;
|
|
335
|
+
const workspace = workspaceId === undefined ? undefined : this.workspaceById.get(workspaceId);
|
|
336
|
+
const cwd = string(request.cwd) ?? workspace?.path;
|
|
337
|
+
if (cwd === undefined)
|
|
338
|
+
return failure('workspace-not-found', 'The Cursor virtual Workspace was not found.');
|
|
339
|
+
const requestedSessionId = string(request.sessionId);
|
|
340
|
+
const reusable = requestedSessionId === undefined ? undefined : this.sessions.get(requestedSessionId);
|
|
341
|
+
if (reusable !== undefined && reusable.cwd === cwd && reusable.blank && !reusable.running) {
|
|
342
|
+
return success({ sessionId: reusable.sessionId });
|
|
343
|
+
}
|
|
344
|
+
const created = await this.client.createSession(cwd, 'agent', this.backend, signal);
|
|
345
|
+
if (this.backend === 'antigravity' && this.sessions.has(`${ACP_SESSION_PREFIX}${created.sessionId}`)) {
|
|
346
|
+
return failure('session-already-exists', 'The backend returned an existing conversation for a new Session.');
|
|
347
|
+
}
|
|
348
|
+
const session = this.registerSession(created.sessionId, cwd, workspace?.title);
|
|
349
|
+
this.attachSessionToWorkspace(cwd, session.sessionId);
|
|
350
|
+
this.publishWorkspaceBaseline();
|
|
351
|
+
const seq = this.nextProjectionSeq();
|
|
352
|
+
this.emitRemoteEvent('api-session/added', [this.sessionSummary(session, seq)]);
|
|
353
|
+
this.publishProjection(session.sessionId, 'title', session.title ?? this.backendLabel(), seq);
|
|
354
|
+
return success({ sessionId: session.sessionId });
|
|
355
|
+
}
|
|
356
|
+
ensureSessionRegistered(sessionId) {
|
|
357
|
+
let session = this.sessions.get(sessionId);
|
|
358
|
+
if (session !== undefined)
|
|
359
|
+
return session;
|
|
360
|
+
if (this.backend === 'antigravity' && sessionId.startsWith(ACP_SESSION_PREFIX)) {
|
|
361
|
+
const acpSessionId = sessionId.slice(ACP_SESSION_PREFIX.length);
|
|
362
|
+
const workspace = this.selectedWorkspaceId !== undefined ? this.workspaceById.get(this.selectedWorkspaceId) : undefined;
|
|
363
|
+
const cwd = workspace?.path ?? '/';
|
|
364
|
+
session = this.registerSession(acpSessionId, cwd, 'Antigravity', Date.now(), Date.now(), false);
|
|
365
|
+
if (workspace && !workspace.sessionIds.includes(sessionId)) {
|
|
366
|
+
workspace.sessionIds.unshift(sessionId);
|
|
367
|
+
workspace.sessionCount = workspace.sessionIds.length;
|
|
368
|
+
this.publishWorkspaceBaseline();
|
|
369
|
+
}
|
|
370
|
+
return session;
|
|
371
|
+
}
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
async prompt(request, signal) {
|
|
375
|
+
const sessionId = extractSessionId(request);
|
|
376
|
+
const session = this.ensureSessionRegistered(sessionId) ?? this.sessions.get(sessionId);
|
|
377
|
+
if (session === undefined)
|
|
378
|
+
return failure('session-not-found', 'The Session was not found.');
|
|
379
|
+
await this.hydrateSession(session);
|
|
380
|
+
const text = extractPromptText(array(request.content));
|
|
381
|
+
let images;
|
|
382
|
+
try {
|
|
383
|
+
images = array(request.content).filter(part => record(part).type === 'image').map(parseAcpImage);
|
|
384
|
+
if (images.length > 4 || (images.length > 0 && this.backend !== 'antigravity') || array(request.content).some(part => !['text', 'image'].includes(String(record(part).type))))
|
|
385
|
+
throw new Error('Unsupported ACP content.');
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
return failure('attachment-error', 'Invalid or unsupported image attachment.');
|
|
389
|
+
}
|
|
390
|
+
if (text === undefined && images.length === 0)
|
|
391
|
+
return failure('attachment-error', 'A prompt or image is required.');
|
|
392
|
+
const imageBlocks = images.map(image => acpImageContent(image, `agy-image:${crypto.randomUUID()}.${image.mimeType === 'image/jpeg' ? 'jpg' : image.mimeType.slice(6)}`));
|
|
393
|
+
this.cacheImageAttachments(sessionId, imageBlocks);
|
|
394
|
+
const requestId = typeof request.requestId === 'string' && request.requestId.length > 0
|
|
395
|
+
? request.requestId
|
|
396
|
+
: undefined;
|
|
397
|
+
await this.ensureFollow(session);
|
|
398
|
+
const follow = [...this.follows].find(f => f.sessionId === sessionId);
|
|
399
|
+
if (follow !== undefined) {
|
|
400
|
+
follow.turn += 1;
|
|
401
|
+
follow.stepOpen = true;
|
|
402
|
+
this.pushEvent(follow, 'turn/start', { turn: follow.turn });
|
|
403
|
+
this.pushEvent(follow, 'step/start', { turn: follow.turn, step: 1 });
|
|
404
|
+
this.pushEvent(follow, 'user/message', {
|
|
405
|
+
id: `user:${Date.now()}`,
|
|
406
|
+
role: 'user',
|
|
407
|
+
content: [...(text ? [{ type: 'text', text }] : []), ...imageBlocks],
|
|
408
|
+
source: requestId ? { kind: 'user', rpcId: requestId } : { kind: 'user' },
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
session.blank = false;
|
|
412
|
+
session.running = true;
|
|
413
|
+
session.updatedAt = Date.now();
|
|
414
|
+
this.emitRemoteEvent('api-session/status', [sessionId, true]);
|
|
415
|
+
await this.client.prompt(session.acpSessionId, text ?? '', signal, images, this.backend);
|
|
416
|
+
return success({ accepted: true });
|
|
417
|
+
}
|
|
418
|
+
async cancel(request, signal) {
|
|
419
|
+
const sessionId = extractSessionId(request);
|
|
420
|
+
const session = this.ensureSessionRegistered(sessionId) ?? this.sessions.get(sessionId);
|
|
421
|
+
if (session === undefined)
|
|
422
|
+
return failure('session-not-found', 'The Session was not found.');
|
|
423
|
+
await this.client.cancel(session.acpSessionId, signal);
|
|
424
|
+
session.running = false;
|
|
425
|
+
this.emitRemoteEvent('api-session/status', [sessionId, false]);
|
|
426
|
+
return success({ accepted: true });
|
|
427
|
+
}
|
|
428
|
+
async renameSession(request) {
|
|
429
|
+
const sessionId = extractSessionId(request);
|
|
430
|
+
const title = string(request.title)?.trim();
|
|
431
|
+
const session = this.ensureSessionRegistered(sessionId) ?? this.sessions.get(sessionId);
|
|
432
|
+
if (session === undefined)
|
|
433
|
+
return failure('session-not-found', 'The Session was not found.');
|
|
434
|
+
if (title === undefined)
|
|
435
|
+
return failure('bad-request', 'A Session title is required.');
|
|
436
|
+
session.title = title;
|
|
437
|
+
session.titleOverridden = true;
|
|
438
|
+
session.updatedAt = Date.now();
|
|
439
|
+
this.publishProjection(sessionId, 'title', title);
|
|
440
|
+
return success({ sessionId });
|
|
441
|
+
}
|
|
442
|
+
cacheImageAttachments(sessionId, content) {
|
|
443
|
+
for (const value of content) {
|
|
444
|
+
const block = record(value);
|
|
445
|
+
const attachment = record(block.attachment);
|
|
446
|
+
if (block.type === 'image' && typeof attachment.attachmentId === 'string' && typeof block.data === 'string') {
|
|
447
|
+
this.imageAttachments.set(attachment.attachmentId, { sessionId, attachment, data: block.data });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async attachment(request) {
|
|
452
|
+
const sessionId = extractSessionId(request);
|
|
453
|
+
const attachmentId = string(request.attachmentId);
|
|
454
|
+
const session = this.ensureSessionRegistered(sessionId);
|
|
455
|
+
if (this.backend !== 'antigravity' || session === undefined || !attachmentId?.startsWith('agy-image:'))
|
|
456
|
+
return failure('attachment-error', 'The image is not referenced by this AGY Session.');
|
|
457
|
+
await this.hydrateSession(session);
|
|
458
|
+
const cached = this.imageAttachments.get(attachmentId);
|
|
459
|
+
return cached?.sessionId === sessionId ? success({ attachment: cached.attachment, data: cached.data }) : failure('attachment-error', 'The AGY image is unavailable or expired.');
|
|
460
|
+
}
|
|
461
|
+
async hydrateSession(session) {
|
|
462
|
+
if (this.backend !== 'antigravity' || session.blank || session.events.length > 0)
|
|
463
|
+
return;
|
|
464
|
+
let events = [];
|
|
465
|
+
if (this.client.loadSessionHistory !== undefined) {
|
|
466
|
+
try {
|
|
467
|
+
events = await this.client.loadSessionHistory(session.sessionId, this.backend);
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
// Keep the projection empty until remote history is available.
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (events.length > 0) {
|
|
474
|
+
session.events = events;
|
|
475
|
+
session.blank = false;
|
|
476
|
+
session.updatedAt = Date.now();
|
|
477
|
+
for (const event of session.events)
|
|
478
|
+
this.cacheImageAttachments(session.sessionId, array(record(event.event.data).content ?? record(record(event.event.data).message).content));
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
async sessionHistory(request) {
|
|
482
|
+
const sessionId = extractSessionId(request);
|
|
483
|
+
const session = this.ensureSessionRegistered(sessionId) ?? this.sessions.get(sessionId);
|
|
484
|
+
if (session === undefined)
|
|
485
|
+
return failure('session-not-found', 'The Session was not found.');
|
|
486
|
+
await this.hydrateSession(session);
|
|
487
|
+
const page = this.historyPage(session, {
|
|
488
|
+
beforeSeq: optionalInteger(request.beforeSeq),
|
|
489
|
+
limit: optionalPositiveInteger(request.maxMessages) ?? 50,
|
|
490
|
+
});
|
|
491
|
+
return success({
|
|
492
|
+
...page,
|
|
493
|
+
events: page.records.map(entry => ({ event: entry.event })),
|
|
494
|
+
projections: {
|
|
495
|
+
asOfSeq: page.cursor,
|
|
496
|
+
values: {
|
|
497
|
+
title: session.title ?? (this.backend === 'antigravity' ? 'Antigravity' : 'Cursor'),
|
|
498
|
+
sessionListMetadata: { blank: session.blank && session.events.length === 0, lastPromptAt: null },
|
|
499
|
+
modelSelection: this.modelSelectionProjection(sessionId),
|
|
500
|
+
imageLimits: acpImageLimits(this.backend),
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
async sessionPage(request) {
|
|
506
|
+
const sessionId = extractSessionId(request);
|
|
507
|
+
const session = this.ensureSessionRegistered(sessionId) ?? this.sessions.get(sessionId);
|
|
508
|
+
if (session === undefined)
|
|
509
|
+
return failure('session-not-found', 'The Session was not found.');
|
|
510
|
+
await this.hydrateSession(session);
|
|
511
|
+
const page = this.historyPage(session, {
|
|
512
|
+
beforeSeq: optionalInteger(request.beforeSeq),
|
|
513
|
+
throughSeq: optionalInteger(request.throughSeq),
|
|
514
|
+
limit: optionalPositiveInteger(request.limit) ?? optionalPositiveInteger(request.maxMessages) ?? 50,
|
|
515
|
+
});
|
|
516
|
+
return success({ records: page.records, hasMore: page.hasMore });
|
|
517
|
+
}
|
|
518
|
+
historyPage(session, options = {}) {
|
|
519
|
+
const lastSessionSeq = session.events.at(-1)?.event.seq ?? -1;
|
|
520
|
+
const throughSeq = Math.min(options.throughSeq ?? lastSessionSeq, lastSessionSeq);
|
|
521
|
+
const endSeq = Math.min(throughSeq, options.beforeSeq === undefined ? throughSeq : options.beforeSeq - 1);
|
|
522
|
+
const window = endSeq < 0 ? [] : session.events.filter(entry => entry.event.seq <= endSeq);
|
|
523
|
+
const limit = options.limit ?? 50;
|
|
524
|
+
const records = window.slice(Math.max(0, window.length - limit));
|
|
525
|
+
const cursor = records.at(-1)?.event.seq ?? -1;
|
|
526
|
+
return {
|
|
527
|
+
header: {
|
|
528
|
+
version: 3,
|
|
529
|
+
id: session.sessionId,
|
|
530
|
+
createdAt: session.createdAt,
|
|
531
|
+
cwd: session.cwd,
|
|
532
|
+
isSeeded: false,
|
|
533
|
+
},
|
|
534
|
+
cursor,
|
|
535
|
+
nextTurn: Math.max(0, ...session.events.map(entry => isRecord(entry.event.data) && typeof entry.event.data.turn === 'number' ? entry.event.data.turn : 0)) + 1,
|
|
536
|
+
records,
|
|
537
|
+
hasMore: window.length > records.length,
|
|
538
|
+
...(session.running ? { activeTurnId: 'cursor-live' } : {}),
|
|
539
|
+
assistantStream: { revision: 0 },
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
describeHost() {
|
|
543
|
+
const workspace = this.selectedWorkspaceId === undefined
|
|
544
|
+
? undefined
|
|
545
|
+
: this.workspaceById.get(this.selectedWorkspaceId);
|
|
546
|
+
const name = this.backendLabel();
|
|
547
|
+
return {
|
|
548
|
+
version: `${name} Remote`,
|
|
549
|
+
cwd: workspace?.path ?? '',
|
|
550
|
+
home: workspace?.path ?? '',
|
|
551
|
+
provider: name,
|
|
552
|
+
model: modelCatalog(this.backend).default.model,
|
|
553
|
+
attachedSessions: this.sessions.size,
|
|
554
|
+
canOpenPath: workspace !== undefined,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
async listDirectory(request, signal) {
|
|
558
|
+
const workspace = this.selectedWorkspaceId === undefined
|
|
559
|
+
? undefined
|
|
560
|
+
: this.workspaceById.get(this.selectedWorkspaceId);
|
|
561
|
+
if (workspace === undefined)
|
|
562
|
+
return failure('workspace-not-found', `The ${this.backendLabel()} virtual Workspace was not found.`);
|
|
563
|
+
const path = string(request.path) ?? workspace.path;
|
|
564
|
+
return success(await this.client.listDirectory(path, signal));
|
|
565
|
+
}
|
|
566
|
+
async answerRemoteEvent(args, signal) {
|
|
567
|
+
const eventId = string(args.eventId);
|
|
568
|
+
const outcome = record(args.outcome);
|
|
569
|
+
if (eventId === undefined)
|
|
570
|
+
throw new Error(`The ${this.backendLabel()} approval result is missing its event id.`);
|
|
571
|
+
const pending = this.pendingApprovals.get(eventId);
|
|
572
|
+
if (pending === undefined)
|
|
573
|
+
return undefined;
|
|
574
|
+
this.pendingApprovals.delete(eventId);
|
|
575
|
+
const decision = outcome.kind === 'result' && outcome.value === 'allowed-once'
|
|
576
|
+
? 'allow-once'
|
|
577
|
+
: outcome.kind === 'result' && outcome.value === 'cancelled'
|
|
578
|
+
? 'cancel'
|
|
579
|
+
: 'reject-once';
|
|
580
|
+
await this.client.respond(pending.requestHandle, decision, undefined, signal);
|
|
581
|
+
return undefined;
|
|
582
|
+
}
|
|
583
|
+
async workspaceFollow(signal) {
|
|
584
|
+
const queue = new AsyncValueQueue(signal);
|
|
585
|
+
this.workspaceStreams.add(queue);
|
|
586
|
+
queue.push({
|
|
587
|
+
type: 'baseline',
|
|
588
|
+
value: {
|
|
589
|
+
items: this.visibleWorkspaces().map(nativeWorkspace),
|
|
590
|
+
archivedSessionIds: [],
|
|
591
|
+
pinnedSessionIds: [],
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
return queue.iterate(() => this.workspaceStreams.delete(queue));
|
|
595
|
+
}
|
|
596
|
+
async sessionControl(signal) {
|
|
597
|
+
const queue = new AsyncValueQueue(signal);
|
|
598
|
+
this.controlStreams.add(queue);
|
|
599
|
+
// Session Controller treats the first control frame as the opening
|
|
600
|
+
// snapshot. Without it the Client-side RemoteSnapshotStream never moves
|
|
601
|
+
// to `ready`, so a workspace selected through the Remote modal remains
|
|
602
|
+
// stuck on the empty shell after the page reload.
|
|
603
|
+
queue.push({ type: 'baseline', value: { projections: {} } });
|
|
604
|
+
return queue.iterate(() => this.controlStreams.delete(queue));
|
|
605
|
+
}
|
|
606
|
+
async remoteEvents(signal) {
|
|
607
|
+
const id = `cursor-events:${Date.now()}:${Math.random()}`;
|
|
608
|
+
const queue = new AsyncValueQueue(signal);
|
|
609
|
+
this.eventStreams.set(id, queue);
|
|
610
|
+
const workspace = this.selectedWorkspaceId === undefined
|
|
611
|
+
? undefined
|
|
612
|
+
: this.workspaceById.get(this.selectedWorkspaceId);
|
|
613
|
+
queue.push({
|
|
614
|
+
type: 'ready',
|
|
615
|
+
clientId: id,
|
|
616
|
+
host: { home: workspace?.path ?? '/' },
|
|
617
|
+
});
|
|
618
|
+
return queue.iterate(() => this.eventStreams.delete(id));
|
|
619
|
+
}
|
|
620
|
+
async sessionFollow(request, signal) {
|
|
621
|
+
const sessionId = extractSessionId(request);
|
|
622
|
+
const session = this.ensureSessionRegistered(sessionId) ?? this.sessions.get(sessionId);
|
|
623
|
+
if (session === undefined)
|
|
624
|
+
throw new Error('The Session was not found.');
|
|
625
|
+
await this.hydrateSession(session);
|
|
626
|
+
const history = this.historyPage(session, {
|
|
627
|
+
limit: optionalPositiveInteger(request.maxMessages) ?? 50,
|
|
628
|
+
});
|
|
629
|
+
const queue = new AsyncValueQueue(signal);
|
|
630
|
+
const follow = {
|
|
631
|
+
sessionId,
|
|
632
|
+
acpSessionId: session.acpSessionId,
|
|
633
|
+
queue,
|
|
634
|
+
nextSeq: session.events.length,
|
|
635
|
+
turn: Math.max(0, ...session.events.map(entry => (isRecord(entry.event.data) && typeof entry.event.data.turn === 'number' ? entry.event.data.turn : 0))),
|
|
636
|
+
stepOpen: session.running,
|
|
637
|
+
streamActive: false,
|
|
638
|
+
assistantStreamRevision: 0,
|
|
639
|
+
accumulatedText: '',
|
|
640
|
+
activeToolCalls: new Set(),
|
|
641
|
+
};
|
|
642
|
+
this.follows.add(follow);
|
|
643
|
+
queue.push({
|
|
644
|
+
type: 'snapshot',
|
|
645
|
+
header: history.header,
|
|
646
|
+
cursor: history.cursor,
|
|
647
|
+
records: history.records,
|
|
648
|
+
hasMore: history.hasMore,
|
|
649
|
+
projections: {
|
|
650
|
+
asOfSeq: history.cursor,
|
|
651
|
+
values: {
|
|
652
|
+
title: session.title ?? (this.backend === 'antigravity' ? 'Antigravity' : 'Cursor'),
|
|
653
|
+
sessionListMetadata: { blank: session.blank && session.events.length === 0, lastPromptAt: null },
|
|
654
|
+
modelSelection: this.modelSelectionProjection(sessionId),
|
|
655
|
+
imageLimits: acpImageLimits(this.backend),
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
assistantStream: { revision: 0 },
|
|
659
|
+
});
|
|
660
|
+
try {
|
|
661
|
+
const stream = await this.client.openStream(session.acpSessionId, frame => this.acceptAcpFrame(follow, frame), () => {
|
|
662
|
+
session.running = false;
|
|
663
|
+
this.emitRemoteEvent('api-session/status', [sessionId, false]);
|
|
664
|
+
this.closeFollowAfterRemoteStreamClosed(follow);
|
|
665
|
+
}, signal);
|
|
666
|
+
follow.close = () => stream.close();
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
this.follows.delete(follow);
|
|
670
|
+
queue.close();
|
|
671
|
+
throw error;
|
|
672
|
+
}
|
|
673
|
+
return queue.iterate(() => {
|
|
674
|
+
this.follows.delete(follow);
|
|
675
|
+
void follow.close?.().catch(() => undefined);
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
async ensureFollow(session) {
|
|
679
|
+
if ([...this.follows].some(follow => follow.sessionId === session.sessionId))
|
|
680
|
+
return;
|
|
681
|
+
// Native UI opens session/follow; prompts before follow still work via Host stream open on demand.
|
|
682
|
+
const controller = new AbortController();
|
|
683
|
+
const queue = new AsyncValueQueue(controller.signal);
|
|
684
|
+
const follow = {
|
|
685
|
+
sessionId: session.sessionId,
|
|
686
|
+
acpSessionId: session.acpSessionId,
|
|
687
|
+
queue,
|
|
688
|
+
nextSeq: session.events.length === 0 ? 0 : Math.max(...session.events.map(entry => entry.event.seq)) + 1,
|
|
689
|
+
turn: Math.max(0, ...session.events.map(entry => (isRecord(entry.event.data) && typeof entry.event.data.turn === 'number' ? entry.event.data.turn : 0))),
|
|
690
|
+
stepOpen: false,
|
|
691
|
+
streamActive: false,
|
|
692
|
+
assistantStreamRevision: 0,
|
|
693
|
+
accumulatedText: '',
|
|
694
|
+
activeToolCalls: new Set(),
|
|
695
|
+
};
|
|
696
|
+
this.follows.add(follow);
|
|
697
|
+
const stream = await this.client.openStream(session.acpSessionId, frame => this.acceptAcpFrame(follow, frame), () => this.closeFollowAfterRemoteStreamClosed(follow));
|
|
698
|
+
follow.close = async () => {
|
|
699
|
+
controller.abort();
|
|
700
|
+
await stream.close();
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
acceptAcpFrame(follow, frame) {
|
|
704
|
+
const method = frame.frame.method;
|
|
705
|
+
const params = record(frame.frame.params);
|
|
706
|
+
if (method === 'session/update') {
|
|
707
|
+
this.acceptSessionUpdate(follow, params);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (method === 'session/request_permission' || method === 'cursor/ask_question' || method === 'cursor/create_plan') {
|
|
711
|
+
this.acceptApproval(follow, params, method);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
acceptSessionUpdate(follow, params) {
|
|
715
|
+
const update = isRecord(params.update) ? params.update : params;
|
|
716
|
+
const kind = string(update.sessionUpdate) ?? string(update.type);
|
|
717
|
+
const session = this.sessions.get(follow.sessionId);
|
|
718
|
+
if (kind === 'model_selection' && isRecord(update.selected)
|
|
719
|
+
&& update.selected.provider === this.backend && typeof update.selected.model === 'string') {
|
|
720
|
+
this.selectedModels.set(follow.sessionId, update.selected);
|
|
721
|
+
this.publishProjection(follow.sessionId, 'modelSelection', this.modelSelectionProjection(follow.sessionId), this.nextProjectionSeq());
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (kind === 'agent_message_chunk' || kind === 'agent_message') {
|
|
725
|
+
const text = extractText(update);
|
|
726
|
+
if (text === undefined || text.length === 0)
|
|
727
|
+
return;
|
|
728
|
+
this.appendAssistantDelta(follow, text);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (kind === 'tool_call' || kind === 'tool_call_update') {
|
|
732
|
+
const toolName = string(update.title) ?? string(update.toolName) ?? string(update.name) ?? 'tool';
|
|
733
|
+
const status = string(update.status);
|
|
734
|
+
const callId = string(update.toolCallId) ?? string(update.callId) ?? toolName;
|
|
735
|
+
const terminal = status === 'completed' || status === 'failed';
|
|
736
|
+
if (kind === 'tool_call_update' && !terminal)
|
|
737
|
+
return;
|
|
738
|
+
if (kind === 'tool_call' && follow.activeToolCalls.has(callId))
|
|
739
|
+
return;
|
|
740
|
+
this.pushEvent(follow, 'tool/call', {
|
|
741
|
+
turn: follow.turn,
|
|
742
|
+
step: 1,
|
|
743
|
+
callId,
|
|
744
|
+
name: toolName,
|
|
745
|
+
arguments: typeof update.parameters === 'object' && update.parameters !== null
|
|
746
|
+
? JSON.stringify(update.parameters)
|
|
747
|
+
: typeof update.rawInput === 'object' && update.rawInput !== null
|
|
748
|
+
? JSON.stringify(update.rawInput)
|
|
749
|
+
: '{}',
|
|
750
|
+
toolCallId: callId,
|
|
751
|
+
toolName,
|
|
752
|
+
status: status === 'completed' ? 'finished' : status === 'failed' ? 'failed' : 'running',
|
|
753
|
+
});
|
|
754
|
+
if (terminal)
|
|
755
|
+
follow.activeToolCalls.delete(callId);
|
|
756
|
+
else
|
|
757
|
+
follow.activeToolCalls.add(callId);
|
|
758
|
+
if (session !== undefined)
|
|
759
|
+
session.updatedAt = Date.now();
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (kind === 'agent_thought_chunk') {
|
|
763
|
+
const text = extractText(update);
|
|
764
|
+
if (text === undefined || text.length === 0)
|
|
765
|
+
return;
|
|
766
|
+
this.appendReasoningDelta(follow, text);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (kind === 'prompt_completed' || kind === 'prompt_failed') {
|
|
770
|
+
const hasLiveContent = follow.stepOpen || follow.streamActive;
|
|
771
|
+
if (!hasLiveContent && Array.isArray(update.catchUp)) {
|
|
772
|
+
for (const item of update.catchUp) {
|
|
773
|
+
if (!isRecord(item) || typeof item.method !== 'string')
|
|
774
|
+
continue;
|
|
775
|
+
const params = isRecord(item.params) ? item.params : {};
|
|
776
|
+
if (item.method === 'session/update')
|
|
777
|
+
this.acceptSessionUpdate(follow, params);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
if (session !== undefined) {
|
|
781
|
+
session.running = false;
|
|
782
|
+
session.updatedAt = Date.now();
|
|
783
|
+
this.emitRemoteEvent('api-session/status', [follow.sessionId, false]);
|
|
784
|
+
if (kind === 'prompt_completed')
|
|
785
|
+
void this.refreshSessionTitle(session);
|
|
786
|
+
}
|
|
787
|
+
this.closeFollowAfterRemoteStreamClosed(follow);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
acceptApproval(follow, params, method) {
|
|
791
|
+
const requestHandle = string(params.requestHandle);
|
|
792
|
+
if (requestHandle === undefined)
|
|
793
|
+
return;
|
|
794
|
+
this.pendingApprovals.set(requestHandle, { requestHandle, sessionId: follow.sessionId });
|
|
795
|
+
const toolName = method === 'cursor/create_plan'
|
|
796
|
+
? (string(params.name) ?? 'plan')
|
|
797
|
+
: method === 'cursor/ask_question'
|
|
798
|
+
? (string(params.title) ?? 'question')
|
|
799
|
+
: (string(params.toolName) ?? 'permission');
|
|
800
|
+
const reason = method === 'cursor/create_plan'
|
|
801
|
+
? string(params.overview) ?? string(params.plan)
|
|
802
|
+
: method === 'cursor/ask_question'
|
|
803
|
+
? summarizeQuestions(params)
|
|
804
|
+
: string(params.reason);
|
|
805
|
+
this.emitApproval({
|
|
806
|
+
eventId: requestHandle,
|
|
807
|
+
agentId: follow.sessionId,
|
|
808
|
+
request: {
|
|
809
|
+
toolName,
|
|
810
|
+
...(reason === undefined ? {} : { reason }),
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
pushAssistantChunk(follow, chunk) {
|
|
815
|
+
let attempt = follow.assistantAttempt;
|
|
816
|
+
if (attempt === undefined) {
|
|
817
|
+
attempt = {
|
|
818
|
+
attemptId: `cursor-attempt:${follow.turn}:${follow.nextSeq}`,
|
|
819
|
+
startedAfterSeq: follow.nextSeq - 1,
|
|
820
|
+
nextIndex: 0,
|
|
821
|
+
stream: [],
|
|
822
|
+
};
|
|
823
|
+
follow.assistantAttempt = attempt;
|
|
824
|
+
follow.assistantStreamRevision += 1;
|
|
825
|
+
follow.queue.push({
|
|
826
|
+
type: 'assistant-stream',
|
|
827
|
+
frame: {
|
|
828
|
+
type: 'start',
|
|
829
|
+
attemptId: attempt.attemptId,
|
|
830
|
+
revision: follow.assistantStreamRevision,
|
|
831
|
+
startedAfterSeq: attempt.startedAfterSeq,
|
|
832
|
+
turn: follow.turn,
|
|
833
|
+
step: 1,
|
|
834
|
+
},
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
const time = Date.now();
|
|
838
|
+
const index = attempt.nextIndex++;
|
|
839
|
+
// Durable streams use AssistantStreamRecord, unlike the timed chunks in
|
|
840
|
+
// the live feed. Without this discriminator the native reader treats a
|
|
841
|
+
// raw chunk as a packed run and throws while settling the conversation.
|
|
842
|
+
attempt.stream.push({ type: 'chunk', time, chunk });
|
|
843
|
+
follow.assistantStreamRevision += 1;
|
|
844
|
+
follow.queue.push({
|
|
845
|
+
type: 'assistant-stream',
|
|
846
|
+
frame: {
|
|
847
|
+
type: 'chunk',
|
|
848
|
+
attemptId: attempt.attemptId,
|
|
849
|
+
revision: follow.assistantStreamRevision,
|
|
850
|
+
index,
|
|
851
|
+
time,
|
|
852
|
+
chunk,
|
|
853
|
+
},
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
endAssistantAttempt(follow, outcome) {
|
|
857
|
+
const attempt = follow.assistantAttempt;
|
|
858
|
+
if (attempt === undefined)
|
|
859
|
+
return;
|
|
860
|
+
follow.assistantAttempt = undefined;
|
|
861
|
+
follow.assistantStreamRevision += 1;
|
|
862
|
+
follow.queue.push({
|
|
863
|
+
type: 'assistant-stream',
|
|
864
|
+
frame: {
|
|
865
|
+
type: 'end',
|
|
866
|
+
attemptId: attempt.attemptId,
|
|
867
|
+
revision: follow.assistantStreamRevision,
|
|
868
|
+
index: attempt.nextIndex,
|
|
869
|
+
outcome,
|
|
870
|
+
},
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
appendAssistantDelta(follow, delta) {
|
|
874
|
+
if (!follow.stepOpen) {
|
|
875
|
+
follow.turn += 1;
|
|
876
|
+
follow.stepOpen = true;
|
|
877
|
+
this.pushEvent(follow, 'turn/start', { turn: follow.turn });
|
|
878
|
+
this.pushEvent(follow, 'step/start', { turn: follow.turn, step: 1 });
|
|
879
|
+
}
|
|
880
|
+
if (follow.blockIndex === undefined) {
|
|
881
|
+
follow.blockIndex = (follow.reasoningBlockIndex ?? -1) + 1;
|
|
882
|
+
follow.streamActive = true;
|
|
883
|
+
this.pushAssistantChunk(follow, { type: 'block-start', index: follow.blockIndex, blockType: 'text' });
|
|
884
|
+
}
|
|
885
|
+
follow.accumulatedText += delta;
|
|
886
|
+
this.pushAssistantChunk(follow, { type: 'text-delta', index: follow.blockIndex, text: delta });
|
|
887
|
+
}
|
|
888
|
+
appendReasoningDelta(follow, delta) {
|
|
889
|
+
if (!follow.stepOpen) {
|
|
890
|
+
follow.turn += 1;
|
|
891
|
+
follow.stepOpen = true;
|
|
892
|
+
this.pushEvent(follow, 'turn/start', { turn: follow.turn });
|
|
893
|
+
this.pushEvent(follow, 'step/start', { turn: follow.turn, step: 1 });
|
|
894
|
+
}
|
|
895
|
+
if (follow.reasoningBlockIndex === undefined) {
|
|
896
|
+
follow.reasoningBlockIndex = (follow.blockIndex ?? -1) + 1;
|
|
897
|
+
follow.streamActive = true;
|
|
898
|
+
this.pushAssistantChunk(follow, { type: 'block-start', index: follow.reasoningBlockIndex, blockType: 'reasoning' });
|
|
899
|
+
}
|
|
900
|
+
this.pushAssistantChunk(follow, { type: 'reasoning-delta', index: follow.reasoningBlockIndex, text: delta });
|
|
901
|
+
}
|
|
902
|
+
closeFollowAfterRemoteStreamClosed(follow) {
|
|
903
|
+
if (follow.streamActive) {
|
|
904
|
+
if (follow.reasoningBlockIndex !== undefined) {
|
|
905
|
+
this.pushAssistantChunk(follow, {
|
|
906
|
+
type: 'block-end',
|
|
907
|
+
index: follow.reasoningBlockIndex,
|
|
908
|
+
block: { type: 'reasoning', text: '' },
|
|
909
|
+
});
|
|
910
|
+
follow.reasoningBlockIndex = undefined;
|
|
911
|
+
}
|
|
912
|
+
if (follow.blockIndex !== undefined) {
|
|
913
|
+
const fullText = follow.accumulatedText;
|
|
914
|
+
this.pushAssistantChunk(follow, {
|
|
915
|
+
type: 'block-end',
|
|
916
|
+
index: follow.blockIndex,
|
|
917
|
+
block: { type: 'text', text: fullText },
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
this.pushAssistantChunk(follow, {
|
|
921
|
+
type: 'finish',
|
|
922
|
+
reason: { kind: 'stop' },
|
|
923
|
+
});
|
|
924
|
+
const fullText = follow.accumulatedText;
|
|
925
|
+
const selection = this.modelSelection(follow.sessionId);
|
|
926
|
+
const stream = follow.assistantAttempt?.stream ?? [];
|
|
927
|
+
const assistantMessageSeq = this.pushEvent(follow, 'assistant/message', {
|
|
928
|
+
turn: follow.turn,
|
|
929
|
+
step: 1,
|
|
930
|
+
message: {
|
|
931
|
+
id: `${follow.sessionId}:${follow.turn}`,
|
|
932
|
+
role: 'assistant',
|
|
933
|
+
content: [{ type: 'text', text: fullText }],
|
|
934
|
+
source: { kind: 'model', provider: selection.provider, model: selection.model },
|
|
935
|
+
},
|
|
936
|
+
stream,
|
|
937
|
+
});
|
|
938
|
+
this.endAssistantAttempt(follow, {
|
|
939
|
+
kind: 'committed',
|
|
940
|
+
eventType: 'assistant/message',
|
|
941
|
+
seq: assistantMessageSeq,
|
|
942
|
+
});
|
|
943
|
+
this.pushEvent(follow, 'step/end', { turn: follow.turn, step: 1 });
|
|
944
|
+
this.pushEvent(follow, 'turn/end', { turn: follow.turn, reason: { kind: 'completed' } });
|
|
945
|
+
}
|
|
946
|
+
else if (follow.stepOpen) {
|
|
947
|
+
this.pushEvent(follow, 'step/end', { turn: follow.turn, step: 1 });
|
|
948
|
+
this.pushEvent(follow, 'turn/end', { turn: follow.turn, reason: { kind: 'completed' } });
|
|
949
|
+
}
|
|
950
|
+
follow.streamActive = false;
|
|
951
|
+
follow.stepOpen = false;
|
|
952
|
+
follow.blockIndex = undefined;
|
|
953
|
+
follow.reasoningBlockIndex = undefined;
|
|
954
|
+
follow.accumulatedText = '';
|
|
955
|
+
}
|
|
956
|
+
pushEvent(follow, type, data) {
|
|
957
|
+
const session = this.sessions.get(follow.sessionId);
|
|
958
|
+
const seq = session !== undefined ? session.events.length : follow.nextSeq++;
|
|
959
|
+
const event = {
|
|
960
|
+
type,
|
|
961
|
+
seq,
|
|
962
|
+
time: Date.now(),
|
|
963
|
+
data,
|
|
964
|
+
...(isSurfaceEvent(type) ? { surfaceOp: 'append' } : {}),
|
|
965
|
+
};
|
|
966
|
+
if (session !== undefined) {
|
|
967
|
+
session.events.push({ type: 'event', event });
|
|
968
|
+
}
|
|
969
|
+
for (const f of this.follows) {
|
|
970
|
+
if (f.sessionId === follow.sessionId) {
|
|
971
|
+
f.queue.push({ type: 'event', event });
|
|
972
|
+
f.nextSeq = seq + 1;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
this.broadcastRcMux({
|
|
976
|
+
type: 'session/event',
|
|
977
|
+
sessionId: follow.sessionId,
|
|
978
|
+
event,
|
|
979
|
+
});
|
|
980
|
+
return seq;
|
|
981
|
+
}
|
|
982
|
+
registerSession(acpSessionId, cwd, title, createdAt = Date.now(), updatedAt = Date.now(), blank = true) {
|
|
983
|
+
const session = {
|
|
984
|
+
sessionId: `${this.backend === 'antigravity' ? ACP_SESSION_PREFIX : CURSOR_SESSION_PREFIX}${acpSessionId}`,
|
|
985
|
+
acpSessionId,
|
|
986
|
+
cwd,
|
|
987
|
+
...(title === undefined ? {} : { title }),
|
|
988
|
+
blank,
|
|
989
|
+
running: false,
|
|
990
|
+
createdAt,
|
|
991
|
+
updatedAt,
|
|
992
|
+
events: [],
|
|
993
|
+
};
|
|
994
|
+
this.sessions.set(session.sessionId, session);
|
|
995
|
+
return session;
|
|
996
|
+
}
|
|
997
|
+
applyDiscoveredTitle(session, title) {
|
|
998
|
+
const next = title.trim();
|
|
999
|
+
if (this.closed || session.titleOverridden || !next || next === 'Untitled Session' || next === session.title)
|
|
1000
|
+
return;
|
|
1001
|
+
session.title = next;
|
|
1002
|
+
this.publishProjection(session.sessionId, 'title', next);
|
|
1003
|
+
}
|
|
1004
|
+
async refreshSessionTitle(session) {
|
|
1005
|
+
if (this.backend !== 'antigravity' || this.closed || session.titleOverridden || this.client.listSessions === undefined)
|
|
1006
|
+
return;
|
|
1007
|
+
try {
|
|
1008
|
+
const items = await this.client.listSessions(session.cwd, this.backend, 30);
|
|
1009
|
+
const current = items.find(item => item.conversationId === session.acpSessionId);
|
|
1010
|
+
if (current !== undefined && this.sessions.get(session.sessionId) === session)
|
|
1011
|
+
this.applyDiscoveredTitle(session, current.title);
|
|
1012
|
+
}
|
|
1013
|
+
catch {
|
|
1014
|
+
// Title discovery must not delay or fail an already completed turn.
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
async discoverAndAttachSessions(workspace) {
|
|
1018
|
+
if (this.backend !== 'antigravity')
|
|
1019
|
+
return;
|
|
1020
|
+
try {
|
|
1021
|
+
let discovered = [];
|
|
1022
|
+
if (this.client.listSessions !== undefined) {
|
|
1023
|
+
try {
|
|
1024
|
+
discovered = await this.client.listSessions(workspace.path, this.backend, 30);
|
|
1025
|
+
}
|
|
1026
|
+
catch {
|
|
1027
|
+
// Keep the projection empty until remote history is available.
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
for (const item of discovered) {
|
|
1031
|
+
const sessionId = `${ACP_SESSION_PREFIX}${item.conversationId}`;
|
|
1032
|
+
if (!this.sessions.has(sessionId)) {
|
|
1033
|
+
this.registerSession(item.conversationId, workspace.path, item.title, item.createdAt, item.updatedAt, false);
|
|
1034
|
+
}
|
|
1035
|
+
const existing = this.sessions.get(sessionId);
|
|
1036
|
+
if (existing !== undefined)
|
|
1037
|
+
this.applyDiscoveredTitle(existing, item.title);
|
|
1038
|
+
if (!workspace.sessionIds.includes(sessionId)) {
|
|
1039
|
+
workspace.sessionIds.push(sessionId);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
workspace.sessionCount = workspace.sessionIds.length;
|
|
1043
|
+
workspace.updatedAt = new Date().toISOString();
|
|
1044
|
+
}
|
|
1045
|
+
catch {
|
|
1046
|
+
// 容错降级
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
attachSessionToWorkspace(cwd, sessionId) {
|
|
1050
|
+
const workspace = [...this.workspaceById.values()].find(item => item.path === cwd)
|
|
1051
|
+
?? createAcpWorkspaceView(cwd, this.backend);
|
|
1052
|
+
const sessionIds = [sessionId, ...workspace.sessionIds.filter(id => id !== sessionId)];
|
|
1053
|
+
const next = {
|
|
1054
|
+
...workspace,
|
|
1055
|
+
sessionIds,
|
|
1056
|
+
sessionCount: sessionIds.length,
|
|
1057
|
+
updatedAt: new Date().toISOString(),
|
|
1058
|
+
};
|
|
1059
|
+
this.workspaceById.set(next.workspaceId, next);
|
|
1060
|
+
if (this.selectedWorkspaceId === undefined)
|
|
1061
|
+
this.selectedWorkspaceId = next.workspaceId;
|
|
1062
|
+
}
|
|
1063
|
+
async ensureInitialSession(workspace) {
|
|
1064
|
+
const cwd = workspace.path;
|
|
1065
|
+
const created = await this.client.createSession(cwd, 'agent', this.backend);
|
|
1066
|
+
const sessionId = created.sessionId;
|
|
1067
|
+
const title = this.backend === 'antigravity' ? 'Antigravity' : workspace.title;
|
|
1068
|
+
const session = this.registerSession(sessionId, cwd, title);
|
|
1069
|
+
this.attachSessionToWorkspace(cwd, session.sessionId);
|
|
1070
|
+
this.emitRemoteEvent('api-session/added', [this.sessionSummary(session, 0)]);
|
|
1071
|
+
this.publishWorkspaceBaseline();
|
|
1072
|
+
return session;
|
|
1073
|
+
}
|
|
1074
|
+
async handleSessionList() {
|
|
1075
|
+
for (const workspace of this.workspaceById.values()) {
|
|
1076
|
+
await this.discoverAndAttachSessions(workspace);
|
|
1077
|
+
if (workspace.sessionIds.length === 0) {
|
|
1078
|
+
await this.ensureInitialSession(workspace);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return this.sessionSummaries();
|
|
1082
|
+
}
|
|
1083
|
+
sessionSummaries() {
|
|
1084
|
+
const selected = this.selectedWorkspaceId === undefined
|
|
1085
|
+
? undefined
|
|
1086
|
+
: this.workspaceById.get(this.selectedWorkspaceId);
|
|
1087
|
+
const allowed = new Set(selected?.sessionIds ?? [...this.sessions.keys()]);
|
|
1088
|
+
return [...this.sessions.values()]
|
|
1089
|
+
.filter(session => allowed.has(session.sessionId))
|
|
1090
|
+
.map(session => this.sessionSummary(session, 0));
|
|
1091
|
+
}
|
|
1092
|
+
sessionSummary(session, asOfSeq) {
|
|
1093
|
+
return {
|
|
1094
|
+
sessionId: session.sessionId,
|
|
1095
|
+
agentAvailable: true,
|
|
1096
|
+
running: session.running,
|
|
1097
|
+
blank: session.blank,
|
|
1098
|
+
cwd: session.cwd,
|
|
1099
|
+
updatedAt: session.updatedAt,
|
|
1100
|
+
projections: {
|
|
1101
|
+
kind: 'sequenced',
|
|
1102
|
+
asOfSeq,
|
|
1103
|
+
values: {
|
|
1104
|
+
title: session.title ?? (this.backend === 'antigravity' ? 'Antigravity' : 'Cursor'),
|
|
1105
|
+
sessionListMetadata: { blank: session.blank, lastPromptAt: null },
|
|
1106
|
+
modelSelection: this.modelSelectionProjection(session.sessionId),
|
|
1107
|
+
imageLimits: acpImageLimits(this.backend),
|
|
1108
|
+
},
|
|
1109
|
+
},
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
modelSelection(sessionId) {
|
|
1113
|
+
return this.selectedModels.get(sessionId) ?? modelCatalog(this.backend).default;
|
|
1114
|
+
}
|
|
1115
|
+
modelSelectionProjection(sessionId) {
|
|
1116
|
+
const selection = this.modelSelection(sessionId);
|
|
1117
|
+
return { lastUsed: selection, next: selection };
|
|
1118
|
+
}
|
|
1119
|
+
async hostSessionModels(sessionId) {
|
|
1120
|
+
if (this.backend !== 'antigravity' || !this.client.sessionModels)
|
|
1121
|
+
return { current: { provider: this.backend, model: 'host-settings' }, routable: true, groups: [], failures: [] };
|
|
1122
|
+
const models = await this.client.sessionModels(nativeAcpId(sessionId, this.backend), 'antigravity');
|
|
1123
|
+
this.selectedModels.set(sessionId, models.current);
|
|
1124
|
+
return models;
|
|
1125
|
+
}
|
|
1126
|
+
async selectModel(request) {
|
|
1127
|
+
const sessionId = extractSessionId(request);
|
|
1128
|
+
nativeAcpId(sessionId, this.backend);
|
|
1129
|
+
const provider = requiredString(request.provider, 'provider');
|
|
1130
|
+
const model = requiredString(request.model, 'model');
|
|
1131
|
+
const reasoningEffort = string(request.reasoningEffort);
|
|
1132
|
+
const catalog = this.backend === 'antigravity' ? await this.hostSessionModels(sessionId) : modelCatalog(this.backend);
|
|
1133
|
+
const group = catalog.groups.find(g => g.id === provider);
|
|
1134
|
+
const targetModel = group?.models.find(m => m.id === model);
|
|
1135
|
+
if (group === undefined || targetModel === undefined) {
|
|
1136
|
+
return failure('model-unavailable', `The selected ${this.backend} model is unavailable on this Host.`);
|
|
1137
|
+
}
|
|
1138
|
+
const supportedEfforts = targetModel.reasoning?.efforts.map(effort => effort.id) ?? [];
|
|
1139
|
+
if (reasoningEffort !== undefined && !supportedEfforts.includes(reasoningEffort)) {
|
|
1140
|
+
return failure('model-unavailable', `The selected reasoning effort is unavailable for this ${this.backend} model.`);
|
|
1141
|
+
}
|
|
1142
|
+
const selected = {
|
|
1143
|
+
provider,
|
|
1144
|
+
model,
|
|
1145
|
+
...(reasoningEffort === undefined ? {} : { reasoningEffort }),
|
|
1146
|
+
};
|
|
1147
|
+
if (this.backend === 'antigravity') {
|
|
1148
|
+
if (!this.client.selectModel)
|
|
1149
|
+
return failure('model-unavailable', 'The Host does not support ACP model selection.');
|
|
1150
|
+
const confirmed = await this.client.selectModel(nativeAcpId(sessionId, this.backend), 'antigravity', selected);
|
|
1151
|
+
this.selectedModels.set(sessionId, confirmed);
|
|
1152
|
+
}
|
|
1153
|
+
else
|
|
1154
|
+
return failure('model-unavailable', 'Cursor model selection follows Host settings.');
|
|
1155
|
+
const seq = this.nextProjectionSeq();
|
|
1156
|
+
this.publishProjection(sessionId, 'modelSelection', this.modelSelectionProjection(sessionId), seq);
|
|
1157
|
+
return success({ selected: this.modelSelection(sessionId) });
|
|
1158
|
+
}
|
|
1159
|
+
publishWorkspaceBaseline() {
|
|
1160
|
+
for (const queue of this.workspaceStreams) {
|
|
1161
|
+
for (const workspace of this.visibleWorkspaces()) {
|
|
1162
|
+
queue.push({ type: 'upsert', workspace: nativeWorkspace(workspace) });
|
|
1163
|
+
}
|
|
1164
|
+
queue.push({ type: 'archived', archivedSessionIds: [] });
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
publishProjection(sessionId, key, value, seq = this.nextProjectionSeq()) {
|
|
1168
|
+
for (const queue of this.controlStreams) {
|
|
1169
|
+
queue.push({ type: 'projection', sessionId, key, value, asOfSeq: seq, seq });
|
|
1170
|
+
}
|
|
1171
|
+
this.broadcastRcMux({ type: 'session/projection', sessionId, key, value, seq });
|
|
1172
|
+
}
|
|
1173
|
+
emitRemoteEvent(event, args) {
|
|
1174
|
+
for (const queue of this.eventStreams.values())
|
|
1175
|
+
queue.push({ type: 'emit', event, args });
|
|
1176
|
+
const sessionId = typeof args[0] === 'string' ? args[0] : undefined;
|
|
1177
|
+
if (event === 'api-session/status' && sessionId !== undefined && typeof args[1] === 'boolean') {
|
|
1178
|
+
this.broadcastRcHost({ type: 'host/session-status', sessionId, running: args[1] });
|
|
1179
|
+
}
|
|
1180
|
+
else if (event === 'api-session/removed' && sessionId !== undefined) {
|
|
1181
|
+
this.broadcastRcHost({ type: 'host/session-removed', sessionId });
|
|
1182
|
+
}
|
|
1183
|
+
else if (event === 'api-session/added' && isRecord(args[0])) {
|
|
1184
|
+
const summary = args[0];
|
|
1185
|
+
this.broadcastRcHost({
|
|
1186
|
+
type: 'host/session-added',
|
|
1187
|
+
sessionId: summary.sessionId,
|
|
1188
|
+
blank: summary.blank === true,
|
|
1189
|
+
...(typeof summary.cwd === 'string' ? { cwd: summary.cwd } : {}),
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
emitApproval(input) {
|
|
1194
|
+
for (const queue of this.eventStreams.values())
|
|
1195
|
+
queue.push({
|
|
1196
|
+
type: 'waterfall',
|
|
1197
|
+
event: 'approval/request',
|
|
1198
|
+
eventId: input.eventId,
|
|
1199
|
+
agentId: input.agentId,
|
|
1200
|
+
request: input.request,
|
|
1201
|
+
});
|
|
1202
|
+
this.broadcastRcMux({
|
|
1203
|
+
type: 'approval/requested',
|
|
1204
|
+
sessionId: input.agentId,
|
|
1205
|
+
approvalId: input.eventId,
|
|
1206
|
+
toolName: string(input.request.toolName) ?? this.backendLabel(),
|
|
1207
|
+
...(typeof input.request.reason === 'string' ? { reason: input.request.reason } : {}),
|
|
1208
|
+
}, input.eventId);
|
|
1209
|
+
}
|
|
1210
|
+
nextProjectionSeq() {
|
|
1211
|
+
this.lastProjectionSeq += 1;
|
|
1212
|
+
return this.lastProjectionSeq;
|
|
1213
|
+
}
|
|
1214
|
+
createApiProxy() {
|
|
1215
|
+
const call = (endpoint) => async (request, signal) => {
|
|
1216
|
+
const payload = endpoint === 'session.prompt' && isRecord(request.payload)
|
|
1217
|
+
? { ...request.payload, requestId: String(request.rpcId) }
|
|
1218
|
+
: request.payload;
|
|
1219
|
+
const result = await this.dispatch(rcEndpoint(endpoint), { args: { request: payload } }, signal ?? new AbortController().signal);
|
|
1220
|
+
return {
|
|
1221
|
+
rpcId: request.rpcId,
|
|
1222
|
+
result: (result.ok
|
|
1223
|
+
? success(result.value)
|
|
1224
|
+
: failure(result.error.code, result.error.message, result.error.details)),
|
|
1225
|
+
};
|
|
1226
|
+
};
|
|
1227
|
+
return {
|
|
1228
|
+
sessions: {
|
|
1229
|
+
list: call('session.list'),
|
|
1230
|
+
search: call('session.search'),
|
|
1231
|
+
create: call('session.create'),
|
|
1232
|
+
history: call('session.history'),
|
|
1233
|
+
models: call('session.models'),
|
|
1234
|
+
selectModel: call('session.selectModel'),
|
|
1235
|
+
rename: call('session.rename'),
|
|
1236
|
+
fork: call('session.fork'),
|
|
1237
|
+
prompt: call('session.prompt'),
|
|
1238
|
+
attachment: call('session.attachment'),
|
|
1239
|
+
updateQueue: call('session.updateQueue'),
|
|
1240
|
+
cancel: call('session.cancel'),
|
|
1241
|
+
},
|
|
1242
|
+
workspace: {
|
|
1243
|
+
list: call('workspace.list'),
|
|
1244
|
+
create: call('workspace.create'),
|
|
1245
|
+
rename: call('workspace.rename'),
|
|
1246
|
+
delete: call('workspace.delete'),
|
|
1247
|
+
insertBefore: call('workspace.insertBefore'),
|
|
1248
|
+
insertSessionBefore: call('workspace.insertSessionBefore'),
|
|
1249
|
+
archiveSession: call('workspace.archiveSession'),
|
|
1250
|
+
},
|
|
1251
|
+
subagents: {},
|
|
1252
|
+
host: {
|
|
1253
|
+
describe: call('host.describe'),
|
|
1254
|
+
listDirectory: call('host.listDirectory'),
|
|
1255
|
+
},
|
|
1256
|
+
skills: { list: call('skills.list') },
|
|
1257
|
+
agentPresets: {},
|
|
1258
|
+
goals: {},
|
|
1259
|
+
settings: {},
|
|
1260
|
+
credentials: {},
|
|
1261
|
+
llm: {},
|
|
1262
|
+
events: {
|
|
1263
|
+
mux: ((request, signal) => this.rcMux(request, signal)),
|
|
1264
|
+
host: ((request, signal) => this.rcHost(request, signal)),
|
|
1265
|
+
},
|
|
1266
|
+
downloads: {},
|
|
1267
|
+
respond: async (message) => {
|
|
1268
|
+
const pending = this.pendingApprovals.get(String(message.rpcId))
|
|
1269
|
+
?? [...this.pendingApprovals.values()].find(item => item.requestHandle === String(message.rpcId));
|
|
1270
|
+
if (pending === undefined)
|
|
1271
|
+
return { accepted: false, reason: 'not-pending' };
|
|
1272
|
+
const result = message.result;
|
|
1273
|
+
const outcome = result.ok ? result.value : undefined;
|
|
1274
|
+
const decision = outcome === 'allowed-once' ? 'allow-once' : outcome === 'cancelled' ? 'cancel' : 'reject-once';
|
|
1275
|
+
await this.client.respond(pending.requestHandle, decision);
|
|
1276
|
+
this.pendingApprovals.delete(pending.requestHandle);
|
|
1277
|
+
return { accepted: true };
|
|
1278
|
+
},
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
async *rcMux(request, signal) {
|
|
1282
|
+
const queue = new AsyncValueQueue(signal);
|
|
1283
|
+
this.rcMuxStreams.add(queue);
|
|
1284
|
+
for (const session of this.sessions.values()) {
|
|
1285
|
+
queue.push({
|
|
1286
|
+
rpcId: `${String(request.rpcId)}:${session.sessionId}:subscribed`,
|
|
1287
|
+
payload: { type: 'session/subscribed', sessionId: session.sessionId, lastSeq: -1 },
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
try {
|
|
1291
|
+
yield* queue;
|
|
1292
|
+
}
|
|
1293
|
+
finally {
|
|
1294
|
+
this.rcMuxStreams.delete(queue);
|
|
1295
|
+
queue.close();
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
async *rcHost(_request, signal) {
|
|
1299
|
+
const queue = new AsyncValueQueue(signal);
|
|
1300
|
+
this.rcHostStreams.add(queue);
|
|
1301
|
+
try {
|
|
1302
|
+
yield* queue;
|
|
1303
|
+
}
|
|
1304
|
+
finally {
|
|
1305
|
+
this.rcHostStreams.delete(queue);
|
|
1306
|
+
queue.close();
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
broadcastRcMux(payload, rpcId = `cursor-mux:${Date.now()}:${Math.random()}`) {
|
|
1310
|
+
for (const queue of this.rcMuxStreams)
|
|
1311
|
+
queue.push({ rpcId, payload });
|
|
1312
|
+
}
|
|
1313
|
+
broadcastRcHost(payload) {
|
|
1314
|
+
const frame = { rpcId: `cursor-host:${Date.now()}:${Math.random()}`, payload };
|
|
1315
|
+
for (const queue of this.rcHostStreams)
|
|
1316
|
+
queue.push(frame);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
function recreateWorkspaceFromId(workspaceId, backend) {
|
|
1320
|
+
const prefix = backend === 'antigravity' ? AGY_WORKSPACE_PREFIX : CURSOR_WORKSPACE_PREFIX;
|
|
1321
|
+
if (!workspaceId.startsWith(prefix))
|
|
1322
|
+
return undefined;
|
|
1323
|
+
try {
|
|
1324
|
+
const path = decodeURIComponent(workspaceId.slice(prefix.length));
|
|
1325
|
+
if (path.length > 0)
|
|
1326
|
+
return createAcpWorkspaceView(path, backend);
|
|
1327
|
+
}
|
|
1328
|
+
catch {
|
|
1329
|
+
return undefined;
|
|
1330
|
+
}
|
|
1331
|
+
return undefined;
|
|
1332
|
+
}
|
|
1333
|
+
function nativeWorkspace(view) {
|
|
1334
|
+
return {
|
|
1335
|
+
workspaceId: view.workspaceId,
|
|
1336
|
+
path: view.path,
|
|
1337
|
+
title: view.title,
|
|
1338
|
+
sessionIds: view.sessionIds,
|
|
1339
|
+
createdAt: view.createdAt,
|
|
1340
|
+
updatedAt: view.updatedAt,
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
const CURSOR_MODELS = [
|
|
1344
|
+
{ id: 'auto', name: 'Auto' },
|
|
1345
|
+
{ id: 'claude-3.7-sonnet', name: 'Claude 3.7 Sonnet' },
|
|
1346
|
+
{ id: 'claude-3.5-sonnet', name: 'Claude 3.5 Sonnet' },
|
|
1347
|
+
{ id: 'gpt-4o', name: 'GPT-4o' },
|
|
1348
|
+
{ id: 'o3-mini', name: 'o3-mini' },
|
|
1349
|
+
{ id: 'deepseek-r1', name: 'DeepSeek R1' },
|
|
1350
|
+
];
|
|
1351
|
+
export function modelCatalog(backend = 'cursor') {
|
|
1352
|
+
const provider = backend === 'antigravity' ? 'antigravity' : CURSOR_PROVIDER;
|
|
1353
|
+
const name = backend === 'antigravity' ? 'Antigravity' : 'Cursor';
|
|
1354
|
+
const models = backend === 'antigravity' ? [] : CURSOR_MODELS;
|
|
1355
|
+
const defaultModel = models[0]?.id ?? 'host-settings';
|
|
1356
|
+
return {
|
|
1357
|
+
default: { provider, model: defaultModel },
|
|
1358
|
+
routableProviders: [provider],
|
|
1359
|
+
groups: [{
|
|
1360
|
+
id: provider,
|
|
1361
|
+
name,
|
|
1362
|
+
models,
|
|
1363
|
+
}],
|
|
1364
|
+
failures: [],
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
function extractPromptText(content) {
|
|
1368
|
+
const parts = content.flatMap(part => {
|
|
1369
|
+
const item = record(part);
|
|
1370
|
+
if (item.type === 'text' && typeof item.text === 'string')
|
|
1371
|
+
return [item.text];
|
|
1372
|
+
return [];
|
|
1373
|
+
});
|
|
1374
|
+
const text = parts.join('');
|
|
1375
|
+
return text.length === 0 ? undefined : text;
|
|
1376
|
+
}
|
|
1377
|
+
function extractText(update) {
|
|
1378
|
+
const content = update.content;
|
|
1379
|
+
if (typeof content === 'string')
|
|
1380
|
+
return content;
|
|
1381
|
+
if (isRecord(content) && typeof content.text === 'string')
|
|
1382
|
+
return content.text;
|
|
1383
|
+
if (Array.isArray(content)) {
|
|
1384
|
+
const parts = content
|
|
1385
|
+
.map(part => (isRecord(part) && typeof part.text === 'string' ? part.text : undefined))
|
|
1386
|
+
.filter((part) => part !== undefined);
|
|
1387
|
+
return parts.length === 0 ? undefined : parts.join('');
|
|
1388
|
+
}
|
|
1389
|
+
return typeof update.text === 'string' ? update.text : undefined;
|
|
1390
|
+
}
|
|
1391
|
+
function summarizeQuestions(params) {
|
|
1392
|
+
if (!Array.isArray(params.questions))
|
|
1393
|
+
return undefined;
|
|
1394
|
+
const lines = params.questions
|
|
1395
|
+
.map(question => (isRecord(question) ? string(question.prompt) : undefined))
|
|
1396
|
+
.filter((line) => line !== undefined);
|
|
1397
|
+
return lines.length === 0 ? undefined : lines.join('\n');
|
|
1398
|
+
}
|
|
1399
|
+
function sessionIdFromAddress(address) {
|
|
1400
|
+
if (typeof address.sessionId === 'string' && address.sessionId.length > 0)
|
|
1401
|
+
return address.sessionId;
|
|
1402
|
+
if (typeof address.childSessionId === 'string' && address.childSessionId.length > 0)
|
|
1403
|
+
return address.childSessionId;
|
|
1404
|
+
if (address.kind === 'session' && typeof address.sessionId === 'string' && address.sessionId.length > 0) {
|
|
1405
|
+
return address.sessionId;
|
|
1406
|
+
}
|
|
1407
|
+
throw new Error('The sessionId is required.');
|
|
1408
|
+
}
|
|
1409
|
+
function extractSessionId(request) {
|
|
1410
|
+
if (typeof request.sessionId === 'string' && request.sessionId.length > 0)
|
|
1411
|
+
return request.sessionId;
|
|
1412
|
+
if (isRecord(request.address))
|
|
1413
|
+
return sessionIdFromAddress(request.address);
|
|
1414
|
+
throw new Error('The sessionId is required.');
|
|
1415
|
+
}
|
|
1416
|
+
function nativeAcpId(sessionId, backend) {
|
|
1417
|
+
const prefix = backend === 'antigravity' ? ACP_SESSION_PREFIX : CURSOR_SESSION_PREFIX;
|
|
1418
|
+
if (!sessionId.startsWith(prefix) || sessionId.length === prefix.length) {
|
|
1419
|
+
throw new Error('The selected Session does not belong to the virtual harness.');
|
|
1420
|
+
}
|
|
1421
|
+
return sessionId.slice(prefix.length);
|
|
1422
|
+
}
|
|
1423
|
+
function isSurfaceEvent(type) {
|
|
1424
|
+
return type === 'user/message' || type === 'assistant/message' || type === 'tool/result';
|
|
1425
|
+
}
|
|
1426
|
+
function carrierArgs(payload) {
|
|
1427
|
+
return record(record(payload).args);
|
|
1428
|
+
}
|
|
1429
|
+
function requestArg(args) {
|
|
1430
|
+
return record(args.request ?? args._request ?? args);
|
|
1431
|
+
}
|
|
1432
|
+
function rcEndpoint(endpoint) {
|
|
1433
|
+
if (endpoint === 'workspace.list')
|
|
1434
|
+
return 'workspace/list';
|
|
1435
|
+
return endpoint.replace('.', '/');
|
|
1436
|
+
}
|
|
1437
|
+
function success(value) {
|
|
1438
|
+
return { ok: true, value };
|
|
1439
|
+
}
|
|
1440
|
+
function failure(code, message, details = {}) {
|
|
1441
|
+
return { ok: false, error: { code, message, details } };
|
|
1442
|
+
}
|
|
1443
|
+
function business(value) {
|
|
1444
|
+
if (isRecord(value) && value.ok === false && isRecord(value.error)) {
|
|
1445
|
+
return { ok: false, error: {
|
|
1446
|
+
code: string(value.error.code) ?? 'internal',
|
|
1447
|
+
message: string(value.error.message) ?? 'The virtual Harness request failed.',
|
|
1448
|
+
details: isRecord(value.error.details) ? value.error.details : {},
|
|
1449
|
+
} };
|
|
1450
|
+
}
|
|
1451
|
+
if (isRecord(value) && value.ok === true)
|
|
1452
|
+
return { ok: true, value: value.value };
|
|
1453
|
+
return { ok: true, value };
|
|
1454
|
+
}
|
|
1455
|
+
function fail(code, message, details = {}) {
|
|
1456
|
+
return { ok: false, error: { code, message, details } };
|
|
1457
|
+
}
|
|
1458
|
+
function failFrom(error) {
|
|
1459
|
+
const source = error instanceof Error ? error : new Error(String(error));
|
|
1460
|
+
const code = 'code' in source && typeof source.code === 'string' ? source.code : 'internal';
|
|
1461
|
+
return fail(code, source.message);
|
|
1462
|
+
}
|
|
1463
|
+
function record(value) {
|
|
1464
|
+
return isRecord(value) ? value : {};
|
|
1465
|
+
}
|
|
1466
|
+
function isRecord(value) {
|
|
1467
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1468
|
+
}
|
|
1469
|
+
function array(value) {
|
|
1470
|
+
return Array.isArray(value) ? value : [];
|
|
1471
|
+
}
|
|
1472
|
+
function string(value) {
|
|
1473
|
+
return typeof value === 'string' ? value : undefined;
|
|
1474
|
+
}
|
|
1475
|
+
function integer(value) {
|
|
1476
|
+
return typeof value === 'number' && Number.isSafeInteger(value) ? value : undefined;
|
|
1477
|
+
}
|
|
1478
|
+
function optionalInteger(value) {
|
|
1479
|
+
if (value === undefined)
|
|
1480
|
+
return undefined;
|
|
1481
|
+
const parsed = integer(value);
|
|
1482
|
+
if (parsed === undefined)
|
|
1483
|
+
throw new Error('The History cursor is invalid.');
|
|
1484
|
+
return parsed;
|
|
1485
|
+
}
|
|
1486
|
+
function optionalPositiveInteger(value) {
|
|
1487
|
+
const parsed = optionalInteger(value);
|
|
1488
|
+
if (parsed !== undefined && parsed <= 0)
|
|
1489
|
+
throw new Error('The History page size is invalid.');
|
|
1490
|
+
return parsed;
|
|
1491
|
+
}
|
|
1492
|
+
function requiredString(value, field) {
|
|
1493
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
1494
|
+
throw new Error(`The ${field} is required.`);
|
|
1495
|
+
return value;
|
|
1496
|
+
}
|
|
1497
|
+
class AsyncValueQueue {
|
|
1498
|
+
signal;
|
|
1499
|
+
values = [];
|
|
1500
|
+
waiters = [];
|
|
1501
|
+
closed = false;
|
|
1502
|
+
onAbort;
|
|
1503
|
+
constructor(signal) {
|
|
1504
|
+
this.signal = signal;
|
|
1505
|
+
this.onAbort = () => this.close();
|
|
1506
|
+
signal.addEventListener('abort', this.onAbort, { once: true });
|
|
1507
|
+
if (signal.aborted)
|
|
1508
|
+
this.close();
|
|
1509
|
+
}
|
|
1510
|
+
push(value) {
|
|
1511
|
+
if (this.closed)
|
|
1512
|
+
return;
|
|
1513
|
+
const waiter = this.waiters.shift();
|
|
1514
|
+
if (waiter === undefined)
|
|
1515
|
+
this.values.push(value);
|
|
1516
|
+
else
|
|
1517
|
+
waiter({ done: false, value });
|
|
1518
|
+
}
|
|
1519
|
+
close() {
|
|
1520
|
+
if (this.closed)
|
|
1521
|
+
return;
|
|
1522
|
+
this.closed = true;
|
|
1523
|
+
this.signal.removeEventListener('abort', this.onAbort);
|
|
1524
|
+
for (const waiter of this.waiters.splice(0))
|
|
1525
|
+
waiter({ done: true, value: undefined });
|
|
1526
|
+
}
|
|
1527
|
+
iterate(dispose) {
|
|
1528
|
+
const queue = this;
|
|
1529
|
+
return {
|
|
1530
|
+
async *[Symbol.asyncIterator]() {
|
|
1531
|
+
try {
|
|
1532
|
+
yield* queue;
|
|
1533
|
+
}
|
|
1534
|
+
finally {
|
|
1535
|
+
dispose();
|
|
1536
|
+
queue.close();
|
|
1537
|
+
}
|
|
1538
|
+
},
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
async *[Symbol.asyncIterator]() {
|
|
1542
|
+
while (true) {
|
|
1543
|
+
if (this.values.length > 0) {
|
|
1544
|
+
yield this.values.shift();
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
if (this.closed)
|
|
1548
|
+
return;
|
|
1549
|
+
const next = await new Promise(resolve => this.waiters.push(resolve));
|
|
1550
|
+
if (next.done)
|
|
1551
|
+
return;
|
|
1552
|
+
yield next.value;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
//# sourceMappingURL=virtual-harness.js.map
|