@camerontaylor/paseo-client 0.8.0-fork.1
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 +44 -0
- package/dist/compat/normalize-provider-models.d.ts +11 -0
- package/dist/compat/normalize-provider-models.js +51 -0
- package/dist/connection/index.d.ts +42 -0
- package/dist/connection/index.js +111 -0
- package/dist/creation/index.d.ts +47 -0
- package/dist/creation/index.js +216 -0
- package/dist/daemon-client-relay-e2ee-transport.d.ts +8 -0
- package/dist/daemon-client-relay-e2ee-transport.js +161 -0
- package/dist/daemon-client-runtime-metrics.d.ts +39 -0
- package/dist/daemon-client-runtime-metrics.js +173 -0
- package/dist/daemon-client-transport-types.d.ts +36 -0
- package/dist/daemon-client-transport-types.js +2 -0
- package/dist/daemon-client-transport-utils.d.ts +13 -0
- package/dist/daemon-client-transport-utils.js +123 -0
- package/dist/daemon-client-transport.d.ts +5 -0
- package/dist/daemon-client-transport.js +4 -0
- package/dist/daemon-client-websocket-transport.d.ts +12 -0
- package/dist/daemon-client-websocket-transport.js +140 -0
- package/dist/daemon-client.d.ts +1245 -0
- package/dist/daemon-client.js +4783 -0
- package/dist/index.d.ts +383 -0
- package/dist/index.js +407 -0
- package/dist/provider-snapshots/index.d.ts +23 -0
- package/dist/provider-snapshots/index.js +60 -0
- package/dist/terminal-stream-router.d.ts +28 -0
- package/dist/terminal-stream-router.js +109 -0
- package/dist/terminals/index.d.ts +69 -0
- package/dist/terminals/index.js +103 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { DaemonClient } from "./daemon-client.js";
|
|
2
|
+
import { createTerminalActions, } from "./terminals/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Coding turns routinely run for minutes, so the handle waits far longer than
|
|
5
|
+
* the transport's own conservative default.
|
|
6
|
+
*/
|
|
7
|
+
const DEFAULT_WAIT_FOR_FINISH_MS = 10 * 60000;
|
|
8
|
+
export function createPaseoClient(config) {
|
|
9
|
+
const daemonClient = new DaemonClient({
|
|
10
|
+
...config,
|
|
11
|
+
clientId: config.clientId ?? createGeneratedClientId(),
|
|
12
|
+
clientType: "cli",
|
|
13
|
+
});
|
|
14
|
+
return {
|
|
15
|
+
...createPaseoApi(daemonClient),
|
|
16
|
+
connect: () => daemonClient.connect(),
|
|
17
|
+
close: () => daemonClient.close(),
|
|
18
|
+
ensureConnected: () => daemonClient.ensureConnected(),
|
|
19
|
+
getConnectionState: () => daemonClient.getConnectionState(),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function toDaemonAgentCreateOptions(options, placement) {
|
|
23
|
+
const { config: agentConfig, cwd, parent, title, prompt, ...requestOptions } = options;
|
|
24
|
+
const { provider: providerModel, options: providerOptions, ...runtimeConfig } = agentConfig;
|
|
25
|
+
const { provider, model } = parseProviderModel(providerModel);
|
|
26
|
+
return {
|
|
27
|
+
...requestOptions,
|
|
28
|
+
config: {
|
|
29
|
+
...runtimeConfig,
|
|
30
|
+
provider,
|
|
31
|
+
model,
|
|
32
|
+
cwd: placement?.cwd ?? cwd,
|
|
33
|
+
...(title !== undefined ? { title } : {}),
|
|
34
|
+
...(providerOptions !== undefined ? { providerOptions } : {}),
|
|
35
|
+
},
|
|
36
|
+
...(placement ? { workspaceId: placement.workspaceId } : {}),
|
|
37
|
+
...(parent ? { callerAgentId: resolveAgentId(parent) } : {}),
|
|
38
|
+
...(prompt !== undefined ? { initialPrompt: prompt } : {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function createPaseoApi(daemonClient) {
|
|
42
|
+
const createAgentHandle = createAgentHandleFactory(daemonClient);
|
|
43
|
+
const createAgent = async (options, placement) => {
|
|
44
|
+
const agent = await daemonClient.createAgent(toDaemonAgentCreateOptions(options, placement));
|
|
45
|
+
return createAgentHandle(agent);
|
|
46
|
+
};
|
|
47
|
+
const terminals = createTerminalActions(daemonClient, async (workspaceId) => {
|
|
48
|
+
const workspace = await createWorkspaceHandle(workspaceId).refresh();
|
|
49
|
+
if (!workspace?.workspaceDirectory) {
|
|
50
|
+
throw new Error(`Workspace ${workspaceId} is not active or has no available directory`);
|
|
51
|
+
}
|
|
52
|
+
return workspace.workspaceDirectory;
|
|
53
|
+
});
|
|
54
|
+
const createWorkspaceHandle = createWorkspaceHandleFactory(daemonClient, createAgent, terminals);
|
|
55
|
+
return {
|
|
56
|
+
terminals,
|
|
57
|
+
projects: {
|
|
58
|
+
list: (options) => daemonClient.listProjects(options),
|
|
59
|
+
subscribe: (handler) => daemonClient.on("project.update", (message) => {
|
|
60
|
+
handler(message.payload);
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
63
|
+
workspaces: {
|
|
64
|
+
list: (options) => daemonClient.fetchWorkspaces(options),
|
|
65
|
+
ref: (workspace) => createWorkspaceHandle(workspace),
|
|
66
|
+
open: (input, requestId) => openWorkspace(daemonClient, createWorkspaceHandle, input, requestId),
|
|
67
|
+
create: async ({ requestId, agent, ...options }) => {
|
|
68
|
+
const result = await daemonClient.createWorkspace({ ...options, ...(agent ? { agent: toDaemonAgentCreateOptions(agent) } : {}) }, requestId);
|
|
69
|
+
if (result.error || !result.workspace) {
|
|
70
|
+
throw new Error(result.error ?? "The daemon did not create a workspace");
|
|
71
|
+
}
|
|
72
|
+
return createWorkspaceHandle(result.workspace);
|
|
73
|
+
},
|
|
74
|
+
archive: (workspace, requestId) => daemonClient.archiveWorkspace(resolveWorkspaceId(workspace), requestId),
|
|
75
|
+
subscribe: (handler) => daemonClient.on("workspace_update", (message) => {
|
|
76
|
+
handler(message.payload);
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
agents: {
|
|
80
|
+
list: (options) => daemonClient.fetchAgents(options),
|
|
81
|
+
ref: (agent) => createAgentHandle(agent),
|
|
82
|
+
create: (options) => createAgent(options),
|
|
83
|
+
subscribe: (handler) => daemonClient.on("agent_update", (message) => {
|
|
84
|
+
handler(message.payload);
|
|
85
|
+
}),
|
|
86
|
+
},
|
|
87
|
+
providers: {
|
|
88
|
+
listModels: (provider, options) => daemonClient.listProviderModels(provider, options),
|
|
89
|
+
listModes: (provider, options) => daemonClient.listProviderModes(provider, options),
|
|
90
|
+
listFeatures: ({ provider: providerModel, ...draftConfig }, options) => {
|
|
91
|
+
const { provider, model } = parseProviderModel(providerModel);
|
|
92
|
+
return daemonClient.listProviderFeatures({ ...draftConfig, provider, model }, options);
|
|
93
|
+
},
|
|
94
|
+
listAvailable: (options) => daemonClient.listAvailableProviders(options),
|
|
95
|
+
snapshot: (options) => daemonClient.getProvidersSnapshot(options),
|
|
96
|
+
waitForReady: (options) => waitForProvidersReady(daemonClient, options),
|
|
97
|
+
refresh: (options) => daemonClient.refreshProvidersSnapshot(options),
|
|
98
|
+
diagnostic: (provider, options) => daemonClient.getProviderDiagnostic(provider, options),
|
|
99
|
+
listUsage: (options) => listProviderUsage(daemonClient, options),
|
|
100
|
+
subscribe: (handler) => daemonClient.on("providers_snapshot_update", (message) => {
|
|
101
|
+
handler(message.payload);
|
|
102
|
+
}),
|
|
103
|
+
},
|
|
104
|
+
config: {
|
|
105
|
+
get: (requestId) => daemonClient.getDaemonConfig(requestId),
|
|
106
|
+
patch: (patch, requestId) => daemonClient.patchDaemonConfig(patch, requestId),
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function createWorkspaceHandleFactory(daemonClient, createAgent, terminals) {
|
|
111
|
+
return (workspace) => {
|
|
112
|
+
const id = typeof workspace === "string" ? workspace : workspace.id;
|
|
113
|
+
let current = typeof workspace === "string" ? null : workspace;
|
|
114
|
+
const refresh = async (options) => {
|
|
115
|
+
let cursor;
|
|
116
|
+
let requestId = options?.requestId;
|
|
117
|
+
do {
|
|
118
|
+
const result = await daemonClient.fetchWorkspaces({
|
|
119
|
+
requestId,
|
|
120
|
+
page: { limit: 200, ...(cursor ? { cursor } : {}) },
|
|
121
|
+
});
|
|
122
|
+
const match = result.entries.find((entry) => entry.id === id);
|
|
123
|
+
if (match) {
|
|
124
|
+
current = match;
|
|
125
|
+
return current;
|
|
126
|
+
}
|
|
127
|
+
cursor = result.pageInfo.nextCursor ?? undefined;
|
|
128
|
+
requestId = undefined;
|
|
129
|
+
} while (cursor);
|
|
130
|
+
current = null;
|
|
131
|
+
return current;
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
id,
|
|
135
|
+
get projectId() {
|
|
136
|
+
return current?.projectId ?? null;
|
|
137
|
+
},
|
|
138
|
+
get directory() {
|
|
139
|
+
return current?.workspaceDirectory ?? null;
|
|
140
|
+
},
|
|
141
|
+
get name() {
|
|
142
|
+
return current?.name ?? null;
|
|
143
|
+
},
|
|
144
|
+
get status() {
|
|
145
|
+
return current?.status ?? null;
|
|
146
|
+
},
|
|
147
|
+
agents: {
|
|
148
|
+
create: async (options) => {
|
|
149
|
+
const snapshot = current ?? (await refresh());
|
|
150
|
+
if (!snapshot?.workspaceDirectory) {
|
|
151
|
+
throw new Error(`Workspace ${id} has no available directory`);
|
|
152
|
+
}
|
|
153
|
+
return createAgent({ ...options, cwd: snapshot.workspaceDirectory }, { workspaceId: id, cwd: snapshot.workspaceDirectory });
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
terminals: {
|
|
157
|
+
create: (options) => terminals.create({ ...options, workspaceId: id }),
|
|
158
|
+
list: (options) => terminals.list({ ...options, workspaceId: id }),
|
|
159
|
+
},
|
|
160
|
+
current: () => current,
|
|
161
|
+
refresh,
|
|
162
|
+
setTitle: (title, requestId) => daemonClient.setWorkspaceTitle(id, title, requestId),
|
|
163
|
+
archive: async (requestId) => {
|
|
164
|
+
const result = await daemonClient.archiveWorkspace(id, requestId);
|
|
165
|
+
if (current) {
|
|
166
|
+
current = { ...current, archivingAt: result.archivedAt };
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
},
|
|
170
|
+
subscribe: (handler) => daemonClient.on("workspace_update", (message) => {
|
|
171
|
+
const update = message.payload;
|
|
172
|
+
if (update.kind === "upsert" && update.workspace.id === id) {
|
|
173
|
+
current = update.workspace;
|
|
174
|
+
handler(update);
|
|
175
|
+
}
|
|
176
|
+
if (update.kind === "remove" && update.id === id) {
|
|
177
|
+
current = null;
|
|
178
|
+
handler(update);
|
|
179
|
+
}
|
|
180
|
+
}),
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function createAgentHandleFactory(daemonClient) {
|
|
185
|
+
return (agent) => {
|
|
186
|
+
const id = typeof agent === "string" ? agent : agent.id;
|
|
187
|
+
let current = typeof agent === "string" ? null : agent;
|
|
188
|
+
const handle = {
|
|
189
|
+
id,
|
|
190
|
+
timeline: {
|
|
191
|
+
append: (item) => daemonClient.appendAgentTimelineItem(id, item),
|
|
192
|
+
refetch: async (options) => {
|
|
193
|
+
const result = await daemonClient.fetchAgentTimeline(id, options);
|
|
194
|
+
if (result.agent) {
|
|
195
|
+
current = result.agent;
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
198
|
+
},
|
|
199
|
+
subscribe: (handler) => daemonClient.subscribeAgentTimeline(id, (message) => {
|
|
200
|
+
handler(message.type === "agent_stream"
|
|
201
|
+
? message.payload
|
|
202
|
+
: {
|
|
203
|
+
agentId: message.payload.agentId,
|
|
204
|
+
event: { type: "replacement", epoch: message.payload.epoch },
|
|
205
|
+
});
|
|
206
|
+
}),
|
|
207
|
+
},
|
|
208
|
+
get workspaceId() {
|
|
209
|
+
return current?.workspaceId ?? null;
|
|
210
|
+
},
|
|
211
|
+
get cwd() {
|
|
212
|
+
return current?.cwd ?? null;
|
|
213
|
+
},
|
|
214
|
+
get status() {
|
|
215
|
+
return current?.status ?? null;
|
|
216
|
+
},
|
|
217
|
+
get capabilities() {
|
|
218
|
+
return current?.capabilities ?? null;
|
|
219
|
+
},
|
|
220
|
+
get availableModes() {
|
|
221
|
+
return current?.availableModes ?? null;
|
|
222
|
+
},
|
|
223
|
+
get pendingPermissions() {
|
|
224
|
+
return current?.pendingPermissions ?? null;
|
|
225
|
+
},
|
|
226
|
+
get activeTurn() {
|
|
227
|
+
return current?.activeTurn ?? null;
|
|
228
|
+
},
|
|
229
|
+
get lastUsage() {
|
|
230
|
+
return current?.lastUsage ?? null;
|
|
231
|
+
},
|
|
232
|
+
get lastError() {
|
|
233
|
+
return current?.lastError ?? null;
|
|
234
|
+
},
|
|
235
|
+
get features() {
|
|
236
|
+
return current?.features ?? null;
|
|
237
|
+
},
|
|
238
|
+
get runtimeInfo() {
|
|
239
|
+
return current?.runtimeInfo ?? null;
|
|
240
|
+
},
|
|
241
|
+
get archivedAt() {
|
|
242
|
+
return current?.archivedAt ?? null;
|
|
243
|
+
},
|
|
244
|
+
current: () => current,
|
|
245
|
+
refresh: async (requestId) => {
|
|
246
|
+
const result = await daemonClient.fetchAgent({ agentId: id, requestId });
|
|
247
|
+
current = result?.agent ?? null;
|
|
248
|
+
return result;
|
|
249
|
+
},
|
|
250
|
+
send: async (text, options) => {
|
|
251
|
+
await daemonClient.sendAgentMessage(id, text, options);
|
|
252
|
+
},
|
|
253
|
+
respondToPermission: async ({ requestId, response }) => {
|
|
254
|
+
await daemonClient.respondToPermission(id, requestId, response);
|
|
255
|
+
},
|
|
256
|
+
run: async (text, options) => {
|
|
257
|
+
const { timeoutMs, ...sendOptions } = options ?? {};
|
|
258
|
+
await daemonClient.sendAgentMessage(id, text, sendOptions);
|
|
259
|
+
const result = await daemonClient.waitForFinish(id, timeoutMs ?? DEFAULT_WAIT_FOR_FINISH_MS);
|
|
260
|
+
if (result.final) {
|
|
261
|
+
current = result.final;
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
},
|
|
265
|
+
waitForFinish: async (timeoutMs) => {
|
|
266
|
+
const result = await daemonClient.waitForFinish(id, timeoutMs ?? DEFAULT_WAIT_FOR_FINISH_MS);
|
|
267
|
+
if (result.final) {
|
|
268
|
+
current = result.final;
|
|
269
|
+
}
|
|
270
|
+
return result;
|
|
271
|
+
},
|
|
272
|
+
commands: (options) => daemonClient.listCommands({ agentId: id, ...options }),
|
|
273
|
+
archive: async () => {
|
|
274
|
+
const result = await daemonClient.archiveAgent(id);
|
|
275
|
+
if (current) {
|
|
276
|
+
current = { ...current, archivedAt: result.archivedAt };
|
|
277
|
+
}
|
|
278
|
+
return result;
|
|
279
|
+
},
|
|
280
|
+
detach: async () => {
|
|
281
|
+
await daemonClient.detachAgent(id);
|
|
282
|
+
},
|
|
283
|
+
subscribe: (handler) => daemonClient.on("agent_update", (message) => {
|
|
284
|
+
const update = message.payload;
|
|
285
|
+
if (update.kind === "upsert" && update.agent.id === id) {
|
|
286
|
+
current = update.agent;
|
|
287
|
+
handler(update);
|
|
288
|
+
}
|
|
289
|
+
if (update.kind === "remove" && update.agentId === id) {
|
|
290
|
+
current = null;
|
|
291
|
+
handler(update);
|
|
292
|
+
}
|
|
293
|
+
}),
|
|
294
|
+
};
|
|
295
|
+
return handle;
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
async function openWorkspace(daemonClient, createWorkspaceHandle, input, requestId) {
|
|
299
|
+
const options = typeof input === "string" ? { cwd: input, requestId } : input;
|
|
300
|
+
const result = await daemonClient.openProject(options.cwd, options.requestId);
|
|
301
|
+
if (result.error || !result.workspace) {
|
|
302
|
+
throw new Error(result.error ?? `The daemon did not open a workspace for ${options.cwd}`);
|
|
303
|
+
}
|
|
304
|
+
return createWorkspaceHandle(result.workspace);
|
|
305
|
+
}
|
|
306
|
+
function resolveWorkspaceId(workspace) {
|
|
307
|
+
return typeof workspace === "string" ? workspace : workspace.id;
|
|
308
|
+
}
|
|
309
|
+
function resolveAgentId(agent) {
|
|
310
|
+
return typeof agent === "string" ? agent : agent.id;
|
|
311
|
+
}
|
|
312
|
+
function parseProviderModel(selection) {
|
|
313
|
+
const separator = selection.indexOf("/");
|
|
314
|
+
if (separator <= 0 || separator === selection.length - 1) {
|
|
315
|
+
throw new Error('Expected config.provider in "provider/model" format');
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
provider: selection.slice(0, separator),
|
|
319
|
+
model: selection.slice(separator + 1),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function listProviderUsage(daemonClient, options) {
|
|
323
|
+
// COMPAT(providerUsageList): added in v0.1.98, remove after 2027-02-28 once daemon floor >= v0.1.98.
|
|
324
|
+
if (daemonClient.getLastServerInfoMessage()?.features?.providerUsageList !== true) {
|
|
325
|
+
return Promise.reject(new Error("Update the host to list provider usage."));
|
|
326
|
+
}
|
|
327
|
+
return daemonClient.listProviderUsage(options);
|
|
328
|
+
}
|
|
329
|
+
function waitForProvidersReady(daemonClient, options = {}) {
|
|
330
|
+
// COMPAT(providersSnapshotCwd): added in v0.3.2, remove gate after 2027-02-10.
|
|
331
|
+
if (daemonClient.getLastServerInfoMessage()?.features?.providersSnapshotCwd !== true) {
|
|
332
|
+
return Promise.reject(new Error("Update the host to wait for provider discovery."));
|
|
333
|
+
}
|
|
334
|
+
const { timeoutMs = 60000, ...snapshotOptions } = options;
|
|
335
|
+
return new Promise((resolve, reject) => {
|
|
336
|
+
let settled = false;
|
|
337
|
+
let requestId = null;
|
|
338
|
+
let snapshotCwd;
|
|
339
|
+
const pendingUpdates = new Map();
|
|
340
|
+
let latestEntries = [];
|
|
341
|
+
const cleanup = () => {
|
|
342
|
+
clearTimeout(timeout);
|
|
343
|
+
unsubscribe();
|
|
344
|
+
};
|
|
345
|
+
const finish = (snapshot) => {
|
|
346
|
+
if (settled)
|
|
347
|
+
return;
|
|
348
|
+
settled = true;
|
|
349
|
+
cleanup();
|
|
350
|
+
resolve(snapshot);
|
|
351
|
+
};
|
|
352
|
+
const fail = (error) => {
|
|
353
|
+
if (settled)
|
|
354
|
+
return;
|
|
355
|
+
settled = true;
|
|
356
|
+
cleanup();
|
|
357
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
358
|
+
};
|
|
359
|
+
const updateMatches = (update) => update.cwd === snapshotCwd;
|
|
360
|
+
const unsubscribe = daemonClient.on("providers_snapshot_update", (message) => {
|
|
361
|
+
const update = message.payload;
|
|
362
|
+
if (!requestId) {
|
|
363
|
+
pendingUpdates.set(update.cwd, update);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (!updateMatches(update))
|
|
367
|
+
return;
|
|
368
|
+
latestEntries = update.entries;
|
|
369
|
+
if (update.entries.some((entry) => entry.status === "loading"))
|
|
370
|
+
return;
|
|
371
|
+
finish({ ...update, requestId });
|
|
372
|
+
});
|
|
373
|
+
const timeout = setTimeout(() => {
|
|
374
|
+
const loading = latestEntries
|
|
375
|
+
.filter((entry) => entry.status === "loading")
|
|
376
|
+
.map((entry) => entry.provider)
|
|
377
|
+
.join(", ");
|
|
378
|
+
fail(new Error(loading
|
|
379
|
+
? `Timed out waiting for providers: ${loading}`
|
|
380
|
+
: "Timed out waiting for provider discovery"));
|
|
381
|
+
}, timeoutMs);
|
|
382
|
+
void daemonClient
|
|
383
|
+
.getProvidersSnapshot(snapshotOptions)
|
|
384
|
+
.then((snapshot) => {
|
|
385
|
+
requestId = snapshot.requestId;
|
|
386
|
+
snapshotCwd = snapshot.cwd;
|
|
387
|
+
latestEntries = snapshot.entries;
|
|
388
|
+
if (!snapshot.entries.some((entry) => entry.status === "loading")) {
|
|
389
|
+
finish(snapshot);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const pendingUpdate = pendingUpdates.get(snapshotCwd);
|
|
393
|
+
if (pendingUpdate && !pendingUpdate.entries.some((entry) => entry.status === "loading")) {
|
|
394
|
+
finish({ ...pendingUpdate, requestId });
|
|
395
|
+
}
|
|
396
|
+
return undefined;
|
|
397
|
+
})
|
|
398
|
+
.catch(fail);
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
function createGeneratedClientId() {
|
|
402
|
+
const randomId = typeof globalThis.crypto?.randomUUID === "function"
|
|
403
|
+
? globalThis.crypto.randomUUID()
|
|
404
|
+
: Math.random().toString(36).slice(2);
|
|
405
|
+
return `paseo-sdk-${randomId}`;
|
|
406
|
+
}
|
|
407
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { GetProvidersSnapshotResponseMessage, SessionOutboundMessage } from "@camerontaylor/paseo-protocol/messages";
|
|
2
|
+
type Update = Extract<SessionOutboundMessage, {
|
|
3
|
+
type: "providers_snapshot_update";
|
|
4
|
+
}>;
|
|
5
|
+
type Snapshot = GetProvidersSnapshotResponseMessage["payload"];
|
|
6
|
+
/** Resolve announcements only while observed, coalescing changes during a fetch. */
|
|
7
|
+
export declare class ProviderSnapshotUpdates {
|
|
8
|
+
private readonly host;
|
|
9
|
+
private pending;
|
|
10
|
+
private paused;
|
|
11
|
+
constructor(host: {
|
|
12
|
+
fetch(cwd: string | undefined): Promise<Snapshot>;
|
|
13
|
+
emit(update: Update): void;
|
|
14
|
+
failed(error: unknown): void;
|
|
15
|
+
});
|
|
16
|
+
clear(): void;
|
|
17
|
+
pause(): void;
|
|
18
|
+
resume(): void;
|
|
19
|
+
receive(update: Update): void;
|
|
20
|
+
private resolve;
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Resolve announcements only while observed, coalescing changes during a fetch. */
|
|
2
|
+
export class ProviderSnapshotUpdates {
|
|
3
|
+
constructor(host) {
|
|
4
|
+
this.host = host;
|
|
5
|
+
this.pending = new Map();
|
|
6
|
+
this.paused = false;
|
|
7
|
+
}
|
|
8
|
+
clear() {
|
|
9
|
+
this.pending.clear();
|
|
10
|
+
}
|
|
11
|
+
pause() {
|
|
12
|
+
this.paused = true;
|
|
13
|
+
// Invalidate old responses while retaining the announcements they would resolve.
|
|
14
|
+
for (const [cwd, pending] of this.pending) {
|
|
15
|
+
this.pending.set(cwd, { latest: pending.latest, running: false });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
resume() {
|
|
19
|
+
this.paused = false;
|
|
20
|
+
for (const pending of this.pending.values())
|
|
21
|
+
this.receive(pending.latest);
|
|
22
|
+
}
|
|
23
|
+
receive(update) {
|
|
24
|
+
const cwd = update.payload.cwd;
|
|
25
|
+
const pending = this.pending.get(cwd) ?? { latest: update, running: false };
|
|
26
|
+
pending.latest = update;
|
|
27
|
+
this.pending.set(cwd, pending);
|
|
28
|
+
if (this.paused || pending.running)
|
|
29
|
+
return;
|
|
30
|
+
pending.running = true;
|
|
31
|
+
void this.resolve(cwd, pending);
|
|
32
|
+
}
|
|
33
|
+
async resolve(cwd, pending) {
|
|
34
|
+
try {
|
|
35
|
+
while (this.pending.get(cwd) === pending) {
|
|
36
|
+
const announced = pending.latest;
|
|
37
|
+
const snapshot = await this.host.fetch(cwd);
|
|
38
|
+
if (this.pending.get(cwd) !== pending)
|
|
39
|
+
return;
|
|
40
|
+
// A change arriving while the body was in flight needs the current body.
|
|
41
|
+
// If that response already covers it, no second fetch is necessary.
|
|
42
|
+
if (pending.latest !== announced &&
|
|
43
|
+
pending.latest.payload.snapshotHash !== snapshot.snapshotHash)
|
|
44
|
+
continue;
|
|
45
|
+
this.pending.delete(cwd);
|
|
46
|
+
this.host.emit({ type: "providers_snapshot_update", payload: snapshot });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (this.pending.get(cwd) === pending)
|
|
52
|
+
this.host.failed(error);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
if (this.pending.get(cwd) === pending)
|
|
56
|
+
this.pending.delete(cwd);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type TerminalStreamFrame } from "@camerontaylor/paseo-protocol/binary-frames/index";
|
|
2
|
+
import type { TerminalInput, TerminalState } from "@camerontaylor/paseo-protocol/messages";
|
|
3
|
+
export type TerminalStreamEvent = {
|
|
4
|
+
terminalId: string;
|
|
5
|
+
type: "output";
|
|
6
|
+
data: Uint8Array;
|
|
7
|
+
} | {
|
|
8
|
+
terminalId: string;
|
|
9
|
+
type: "snapshot";
|
|
10
|
+
state: TerminalState;
|
|
11
|
+
} | {
|
|
12
|
+
terminalId: string;
|
|
13
|
+
type: "restore";
|
|
14
|
+
data: Uint8Array;
|
|
15
|
+
};
|
|
16
|
+
export declare class TerminalStreamRouter {
|
|
17
|
+
private readonly terminalSlots;
|
|
18
|
+
private readonly slotTerminals;
|
|
19
|
+
private readonly listeners;
|
|
20
|
+
onEvent(handler: (event: TerminalStreamEvent) => void): () => void;
|
|
21
|
+
setSlot(terminalId: string, slot: number): void;
|
|
22
|
+
removeTerminal(terminalId: string): void;
|
|
23
|
+
clearSlots(): void;
|
|
24
|
+
encodeInput(terminalId: string, message: TerminalInput["message"]): Uint8Array | null;
|
|
25
|
+
handleFrame(frame: TerminalStreamFrame): void;
|
|
26
|
+
private emit;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=terminal-stream-router.d.ts.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { decodeTerminalSnapshotPayload, encodeTerminalResizePayload, encodeTerminalStreamFrame, TerminalStreamOpcode, } from "@camerontaylor/paseo-protocol/binary-frames/index";
|
|
2
|
+
export class TerminalStreamRouter {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.terminalSlots = new Map();
|
|
5
|
+
this.slotTerminals = new Map();
|
|
6
|
+
this.listeners = new Set();
|
|
7
|
+
}
|
|
8
|
+
onEvent(handler) {
|
|
9
|
+
this.listeners.add(handler);
|
|
10
|
+
return () => {
|
|
11
|
+
this.listeners.delete(handler);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
setSlot(terminalId, slot) {
|
|
15
|
+
const existingTerminalId = this.slotTerminals.get(slot);
|
|
16
|
+
if (existingTerminalId && existingTerminalId !== terminalId) {
|
|
17
|
+
this.terminalSlots.delete(existingTerminalId);
|
|
18
|
+
}
|
|
19
|
+
const existingSlot = this.terminalSlots.get(terminalId);
|
|
20
|
+
if (typeof existingSlot === "number" && existingSlot !== slot) {
|
|
21
|
+
this.slotTerminals.delete(existingSlot);
|
|
22
|
+
}
|
|
23
|
+
this.terminalSlots.set(terminalId, slot);
|
|
24
|
+
this.slotTerminals.set(slot, terminalId);
|
|
25
|
+
}
|
|
26
|
+
removeTerminal(terminalId) {
|
|
27
|
+
const slot = this.terminalSlots.get(terminalId);
|
|
28
|
+
if (typeof slot !== "number") {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.terminalSlots.delete(terminalId);
|
|
32
|
+
if (this.slotTerminals.get(slot) === terminalId) {
|
|
33
|
+
this.slotTerminals.delete(slot);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
clearSlots() {
|
|
37
|
+
this.terminalSlots.clear();
|
|
38
|
+
this.slotTerminals.clear();
|
|
39
|
+
}
|
|
40
|
+
encodeInput(terminalId, message) {
|
|
41
|
+
const slot = this.terminalSlots.get(terminalId);
|
|
42
|
+
if (typeof slot !== "number") {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
if (message.type === "input") {
|
|
46
|
+
return encodeTerminalStreamFrame({
|
|
47
|
+
opcode: TerminalStreamOpcode.Input,
|
|
48
|
+
slot,
|
|
49
|
+
payload: message.data,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (message.type === "resize") {
|
|
53
|
+
return encodeTerminalStreamFrame({
|
|
54
|
+
opcode: TerminalStreamOpcode.Resize,
|
|
55
|
+
slot,
|
|
56
|
+
payload: encodeTerminalResizePayload({
|
|
57
|
+
rows: message.rows,
|
|
58
|
+
cols: message.cols,
|
|
59
|
+
...(message.intent ? { intent: message.intent } : {}),
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
handleFrame(frame) {
|
|
66
|
+
const terminalId = this.slotTerminals.get(frame.slot);
|
|
67
|
+
if (!terminalId) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (frame.opcode === TerminalStreamOpcode.Output) {
|
|
71
|
+
this.emit({
|
|
72
|
+
terminalId,
|
|
73
|
+
type: "output",
|
|
74
|
+
data: frame.payload,
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (frame.opcode === TerminalStreamOpcode.Restore) {
|
|
79
|
+
this.emit({
|
|
80
|
+
terminalId,
|
|
81
|
+
type: "restore",
|
|
82
|
+
data: frame.payload,
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (frame.opcode === TerminalStreamOpcode.Snapshot) {
|
|
87
|
+
const state = decodeTerminalSnapshotPayload(frame.payload);
|
|
88
|
+
if (!state) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.emit({
|
|
92
|
+
terminalId,
|
|
93
|
+
type: "snapshot",
|
|
94
|
+
state,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
emit(event) {
|
|
99
|
+
for (const listener of this.listeners) {
|
|
100
|
+
try {
|
|
101
|
+
listener(event);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// no-op
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=terminal-stream-router.js.map
|