pi2web 0.2.1 → 0.2.8
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 +53 -6
- package/dist/activity.js +178 -0
- package/dist/agent-feed.js +133 -0
- package/dist/agent-templates.js +144 -0
- package/dist/agents.js +671 -152
- package/dist/auth.js +3 -1
- package/dist/collab/baseline.js +84 -0
- package/dist/collab/dispatcher.js +45 -10
- package/dist/collab/hub.js +1392 -384
- package/dist/collab/pi-extension.js +322 -65
- package/dist/collab/review-flow.js +197 -48
- package/dist/collab/routes.js +94 -23
- package/dist/collab/schemas.js +69 -13
- package/dist/collab/scoring-flow.js +145 -39
- package/dist/collab/similarity.js +15 -0
- package/dist/collab/store.js +467 -112
- package/dist/collab/types.js +94 -10
- package/dist/collab/validate.js +62 -25
- package/dist/drafts.js +26 -6
- package/dist/http/agent-routes.js +222 -0
- package/dist/http/login-limiter.js +53 -0
- package/dist/http/mail-settings-routes.js +53 -0
- package/dist/http/system-prompt-routes.js +52 -0
- package/dist/http/utils.js +47 -0
- package/dist/http/web-assets.js +64 -0
- package/dist/http/workspace-routes.js +58 -0
- package/dist/mail-notifier.js +63 -17
- package/dist/main.js +61 -16
- package/dist/mcp.js +24 -5
- package/dist/metadata-store.js +203 -56
- package/dist/paths.js +10 -0
- package/dist/sdk-backend.js +249 -45
- package/dist/server.js +488 -614
- package/dist/session-namer.js +66 -17
- package/dist/session-performance.js +137 -0
- package/dist/sotto-stt.js +217 -0
- package/dist/terminals.js +94 -27
- package/dist/timeline.js +185 -0
- package/dist/voice.js +153 -35
- package/dist/workspaces.js +91 -26
- package/dist/ws/agent-socket.js +256 -0
- package/dist/ws/terminal-socket.js +46 -0
- package/npm-shrinkwrap.json +3685 -0
- package/package.json +11 -1
- package/packages/protocol/openapi.yaml +24 -2
- package/packages/protocol/websocket.schema.json +58 -15
- package/web/activity-view.js +121 -0
- package/web/app.js +3647 -756
- package/web/backend-state.js +59 -27
- package/web/collab.js +684 -257
- package/web/hid-keyboard.js +173 -140
- package/web/index.html +7 -2
- package/web/styles.css +21 -2
- package/web/voice-pcm.js +36 -0
package/dist/agents.js
CHANGED
|
@@ -2,79 +2,307 @@ import { EventEmitter } from 'node:events';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { relative as pathRelative } from 'node:path';
|
|
4
4
|
import { createSdkBackend } from './sdk-backend.js';
|
|
5
|
+
import { ActivityTracker } from './activity.js';
|
|
6
|
+
import { AgentFeed } from './agent-feed.js';
|
|
7
|
+
import { buildTimeline, itemBody, pageTimeline } from './timeline.js';
|
|
8
|
+
import { sessionPerformance } from './session-performance.js';
|
|
9
|
+
/**
|
|
10
|
+
* Reduce a Pi SDK event to the fields server-side consumers and clients use, or drop it.
|
|
11
|
+
* Heavy SDK events are whitelisted field by field (Pi attaches whole messages to many of
|
|
12
|
+
* them) so new SDK fields never leak into memory or onto the wire; pi2web's own events pass through.
|
|
13
|
+
*/
|
|
14
|
+
export function slimEvent(event) {
|
|
15
|
+
const e = event;
|
|
16
|
+
switch (event.type) {
|
|
17
|
+
// Streamed text and running tool output reach viewing clients through AgentFeed, not as events.
|
|
18
|
+
case 'message_update':
|
|
19
|
+
case 'tool_execution_update':
|
|
20
|
+
return undefined;
|
|
21
|
+
case 'message_start':
|
|
22
|
+
return { type: 'message_start', role: e.message?.role ?? e.role };
|
|
23
|
+
case 'turn_start':
|
|
24
|
+
case 'turn_end':
|
|
25
|
+
return { type: event.type };
|
|
26
|
+
case 'message_end':
|
|
27
|
+
return { type: 'message_end', message: e.message };
|
|
28
|
+
case 'agent_end':
|
|
29
|
+
return { type: 'agent_end', willRetry: e.willRetry, aborted: e.aborted };
|
|
30
|
+
case 'tool_execution_start':
|
|
31
|
+
return {
|
|
32
|
+
type: event.type,
|
|
33
|
+
toolCallId: e.toolCallId,
|
|
34
|
+
toolName: e.toolName,
|
|
35
|
+
args: e.args,
|
|
36
|
+
parentToolCallId: e.parentToolCallId
|
|
37
|
+
};
|
|
38
|
+
case 'tool_execution_end':
|
|
39
|
+
return {
|
|
40
|
+
type: event.type,
|
|
41
|
+
toolCallId: e.toolCallId,
|
|
42
|
+
toolName: e.toolName,
|
|
43
|
+
parentToolCallId: e.parentToolCallId,
|
|
44
|
+
result: e.result,
|
|
45
|
+
isError: e.isError,
|
|
46
|
+
durationMs: e.durationMs
|
|
47
|
+
};
|
|
48
|
+
default:
|
|
49
|
+
return event;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
5
52
|
/** Deterministic backend used by the server and tests. The SDK adapter can implement the same contract. */
|
|
6
53
|
export class MockBackend {
|
|
54
|
+
options;
|
|
7
55
|
emitter = new EventEmitter();
|
|
8
56
|
state;
|
|
9
|
-
|
|
57
|
+
entries = [];
|
|
10
58
|
timer;
|
|
11
59
|
queue = { steering: [], followUp: [] };
|
|
12
|
-
constructor(agentId, cwd, sessionId = randomUUID()
|
|
13
|
-
|
|
60
|
+
constructor(agentId, cwd, sessionId = randomUUID(), options = {}) {
|
|
61
|
+
this.options = options;
|
|
62
|
+
this.state = { agentId, sessionId, cwd, status: 'idle' };
|
|
63
|
+
}
|
|
64
|
+
emit(e) {
|
|
65
|
+
this.emitter.emit('event', e);
|
|
66
|
+
}
|
|
67
|
+
/** Persist like Pi: append a session entry after listeners have seen the message. */
|
|
68
|
+
append(message) {
|
|
69
|
+
const id = randomUUID().slice(0, 8);
|
|
70
|
+
this.entries.push({
|
|
71
|
+
type: 'message',
|
|
72
|
+
id,
|
|
73
|
+
parentId: this.entries.at(-1)?.id ?? null,
|
|
74
|
+
timestamp: new Date().toISOString(),
|
|
75
|
+
message: { timestamp: Date.now(), ...message }
|
|
76
|
+
});
|
|
77
|
+
return id;
|
|
78
|
+
}
|
|
14
79
|
/** The body of a turn. Tests override it to keep a run streaming while they inspect queued input. */
|
|
15
|
-
runTurn() {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
this.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
80
|
+
runTurn() {
|
|
81
|
+
return new Promise(r => {
|
|
82
|
+
this.timer = setTimeout(r, 0);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async prompt(message) {
|
|
86
|
+
if (this.state.status === 'streaming')
|
|
87
|
+
throw Object.assign(new Error('Agent is busy'), { code: 'AGENT_BUSY' });
|
|
88
|
+
this.state.status = 'streaming';
|
|
89
|
+
this.emit({ type: 'agent_start', message });
|
|
90
|
+
await this.runTurn();
|
|
91
|
+
const user = { role: 'user', content: message };
|
|
92
|
+
this.emit({ type: 'message_end', message: user });
|
|
93
|
+
this.append(user);
|
|
94
|
+
const text = `Mock response: ${message}`, assistant = { role: 'assistant', content: [{ type: 'text', text }] };
|
|
95
|
+
this.emit({ type: 'message_start', message: { role: 'assistant', content: [] } });
|
|
96
|
+
this.emit({ type: 'message_update', assistantMessageEvent: { type: 'text_delta', delta: text } });
|
|
97
|
+
this.emit({ type: 'message_end', message: assistant });
|
|
98
|
+
this.append(assistant);
|
|
30
99
|
this.state.status = 'idle';
|
|
31
|
-
this.emit({ type: 'agent_end'
|
|
32
|
-
}
|
|
33
|
-
async
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
100
|
+
this.emit({ type: 'agent_end' });
|
|
101
|
+
}
|
|
102
|
+
async steer(message) {
|
|
103
|
+
const queued = this.state.status === 'streaming';
|
|
104
|
+
if (queued)
|
|
105
|
+
this.queue.steering.push(message);
|
|
106
|
+
this.emit({ type: 'steer', message });
|
|
107
|
+
if (queued)
|
|
108
|
+
this.emitQueue();
|
|
109
|
+
return queued ? 'queued' : 'handled';
|
|
110
|
+
}
|
|
111
|
+
async followUp(message) {
|
|
112
|
+
const queued = this.state.status === 'streaming';
|
|
113
|
+
if (queued)
|
|
114
|
+
this.queue.followUp.push(message);
|
|
115
|
+
this.emit({ type: 'follow_up', message });
|
|
116
|
+
if (queued)
|
|
117
|
+
this.emitQueue();
|
|
118
|
+
return queued ? 'queued' : 'handled';
|
|
119
|
+
}
|
|
120
|
+
async getQueue() {
|
|
121
|
+
return { steering: [...this.queue.steering], followUp: [...this.queue.followUp] };
|
|
122
|
+
}
|
|
123
|
+
async clearQueue() {
|
|
124
|
+
const cleared = await this.getQueue();
|
|
125
|
+
this.queue = { steering: [], followUp: [] };
|
|
126
|
+
this.emitQueue();
|
|
127
|
+
return cleared;
|
|
128
|
+
}
|
|
129
|
+
async moveQueue(from, index, expected) {
|
|
130
|
+
if (this.state.status !== 'streaming' || this.queue[from][index] !== expected)
|
|
131
|
+
throw Object.assign(new Error('Queued message has changed or already been consumed'), {
|
|
132
|
+
code: 'QUEUE_CHANGED'
|
|
133
|
+
});
|
|
134
|
+
const to = from === 'steering' ? 'followUp' : 'steering';
|
|
135
|
+
this.queue[from].splice(index, 1);
|
|
136
|
+
this.queue[to].push(expected);
|
|
137
|
+
this.emitQueue();
|
|
138
|
+
return this.getQueue();
|
|
139
|
+
}
|
|
140
|
+
emitQueue() {
|
|
141
|
+
this.emit({ type: 'queue_update', steering: [...this.queue.steering], followUp: [...this.queue.followUp] });
|
|
142
|
+
}
|
|
143
|
+
async abort() {
|
|
144
|
+
if (this.state.status === 'streaming') {
|
|
145
|
+
if (this.timer)
|
|
146
|
+
clearTimeout(this.timer);
|
|
147
|
+
this.state.status = 'idle';
|
|
148
|
+
this.emit({ type: 'agent_end', aborted: true });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async getState() {
|
|
152
|
+
return { ...this.state };
|
|
153
|
+
}
|
|
154
|
+
timeline() {
|
|
155
|
+
return {
|
|
156
|
+
sessionId: this.state.sessionId,
|
|
157
|
+
leafId: this.entries.at(-1)?.id ?? null,
|
|
158
|
+
entries: () => [...this.entries]
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
async getSystemPrompt() {
|
|
162
|
+
const template = this.options.piDefaultPrompt ? undefined : this.options.template;
|
|
163
|
+
const base = template?.mode === 'replace' ? template.systemPrompt : 'Mock Pi system prompt';
|
|
164
|
+
return {
|
|
165
|
+
systemPrompt: [base, template?.mode === 'append' ? template.systemPrompt : '', `cwd: ${this.state.cwd}`]
|
|
166
|
+
.filter(Boolean)
|
|
167
|
+
.join('\n\n'),
|
|
168
|
+
appendFiles: [],
|
|
169
|
+
contextFiles: []
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
async getCapabilities() {
|
|
173
|
+
return { model: null, models: [], thinkingLevel: 'off', thinkingLevels: ['off'], supportsThinking: false };
|
|
174
|
+
}
|
|
175
|
+
async getSession() {
|
|
176
|
+
return { ...(await this.getSessionInfo()), entries: [...this.entries], tree: [] };
|
|
177
|
+
}
|
|
178
|
+
async getSessionInfo() {
|
|
179
|
+
return {
|
|
180
|
+
sessionId: this.state.sessionId,
|
|
181
|
+
sessionName: this.state.sessionName,
|
|
182
|
+
stats: { totalMessages: this.entries.length, tokens: { total: 0 }, cost: 0 },
|
|
183
|
+
contextUsage: { tokens: null, contextWindow: 0, percent: null }
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
async compact() {
|
|
187
|
+
return { summary: 'mock' };
|
|
188
|
+
}
|
|
39
189
|
async setModel(_provider, _modelId) { }
|
|
40
190
|
async setThinkingLevel(_level) { }
|
|
41
|
-
async setSessionName(name) {
|
|
42
|
-
|
|
43
|
-
|
|
191
|
+
async setSessionName(name) {
|
|
192
|
+
this.state.sessionName = name;
|
|
193
|
+
this.emit({ type: 'session_info_changed', name });
|
|
194
|
+
}
|
|
195
|
+
/** Move the leaf back to an earlier entry, like rewinding a Pi session. */
|
|
196
|
+
async navigate(entryId) {
|
|
197
|
+
const index = this.entries.findIndex(entry => entry.id === entryId);
|
|
198
|
+
if (index >= 0)
|
|
199
|
+
this.entries = this.entries.slice(0, index + 1);
|
|
200
|
+
return { cancelled: false };
|
|
201
|
+
}
|
|
202
|
+
async fork(_entryId) {
|
|
203
|
+
return {};
|
|
204
|
+
}
|
|
44
205
|
async extensionResponse(_requestId, _value) { }
|
|
45
|
-
async listCommands() {
|
|
46
|
-
|
|
206
|
+
async listCommands() {
|
|
207
|
+
return [{ name: 'mock', description: 'Mock prompt template', source: 'prompt' }];
|
|
208
|
+
}
|
|
209
|
+
async runBash(command, excludeFromContext = false) {
|
|
210
|
+
const id = randomUUID(), output = `Mock bash: ${command}`;
|
|
211
|
+
this.emit({ type: 'bash_execution_start', id, command });
|
|
212
|
+
this.emit({ type: 'bash_execution_update', id, delta: `${output}\n` });
|
|
213
|
+
const result = { id, output, exitCode: 0, cancelled: false, truncated: false };
|
|
214
|
+
this.append({
|
|
215
|
+
role: 'bashExecution',
|
|
216
|
+
command,
|
|
217
|
+
output,
|
|
218
|
+
exitCode: 0,
|
|
219
|
+
cancelled: false,
|
|
220
|
+
truncated: false,
|
|
221
|
+
excludeFromContext
|
|
222
|
+
});
|
|
223
|
+
this.emit({ type: 'bash_execution_end', command, ...result });
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
47
226
|
abortBash() { }
|
|
48
|
-
subscribe(listener) {
|
|
49
|
-
|
|
227
|
+
subscribe(listener) {
|
|
228
|
+
this.emitter.on('event', listener);
|
|
229
|
+
return () => this.emitter.off('event', listener);
|
|
230
|
+
}
|
|
231
|
+
async dispose() {
|
|
232
|
+
await this.abort();
|
|
233
|
+
this.state.status = 'stopped';
|
|
234
|
+
this.emitter.removeAllListeners();
|
|
235
|
+
}
|
|
50
236
|
}
|
|
51
237
|
const toStringList = (value) => Array.isArray(value) ? value.filter((x) => typeof x === 'string') : [];
|
|
52
238
|
const emptyQueue = () => ({ steering: [], followUp: [] });
|
|
239
|
+
const LIFECYCLE_ONLY = new Set(['agent_loaded', 'agent_unloaded']);
|
|
53
240
|
/** A pending dialog blocks the run until a human answers it, which is a different wait than streaming. */
|
|
54
241
|
const runStatus = (entry, streaming) => entry.dialogRequests.size ? 'waiting_for_user' : streaming ? 'streaming' : 'idle';
|
|
55
242
|
export class AgentManager {
|
|
56
243
|
workspaces;
|
|
57
244
|
factory;
|
|
58
245
|
agents = new Map();
|
|
59
|
-
|
|
246
|
+
detailListeners = new Map();
|
|
60
247
|
globalListeners = new Set();
|
|
61
|
-
constructor(workspaces, factory = createSdkBackend) {
|
|
248
|
+
constructor(workspaces, factory = (id, cwd, sessionFile, profile, options) => createSdkBackend(id, cwd, sessionFile, profile, undefined, options)) {
|
|
62
249
|
this.workspaces = workspaces;
|
|
63
250
|
this.factory = factory;
|
|
64
251
|
}
|
|
65
|
-
async resolveCwd(workspaceId, relativeCwd = '.') {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
252
|
+
async resolveCwd(workspaceId, relativeCwd = '.') {
|
|
253
|
+
const ws = this.workspaces.get(workspaceId);
|
|
254
|
+
if (!ws)
|
|
255
|
+
throw Object.assign(new Error('Workspace not found'), { code: 'WORKSPACE_NOT_FOUND' });
|
|
256
|
+
const cwd = await this.workspaces.resolve(ws, relativeCwd);
|
|
257
|
+
if (!(await import('node:fs/promises')).stat(cwd).then(s => s.isDirectory()))
|
|
258
|
+
throw new Error('cwd is not a directory');
|
|
259
|
+
return cwd;
|
|
260
|
+
}
|
|
261
|
+
async create(workspaceId, relativeCwd = '.', sessionFile, profile = 'default', template) {
|
|
262
|
+
const cwd = await this.resolveCwd(workspaceId, relativeCwd);
|
|
263
|
+
const id = `agent-${randomUUID()}`, backend = await this.factory(id, cwd, sessionFile, profile, template ? { template } : {}), now = new Date().toISOString();
|
|
264
|
+
const record = {
|
|
265
|
+
...(await backend.getState()),
|
|
266
|
+
workspaceId,
|
|
267
|
+
profile,
|
|
268
|
+
...(template ? { template: { ...template } } : {}),
|
|
269
|
+
createdAt: now,
|
|
270
|
+
lastActiveAt: now
|
|
271
|
+
};
|
|
272
|
+
this.attach(id, record, backend);
|
|
273
|
+
return { ...record };
|
|
274
|
+
}
|
|
69
275
|
/** Build a disposable session so the UI sees the models actually available for this project and its extensions. */
|
|
70
|
-
async capabilityPreview(workspaceId, relativeCwd = '.') {
|
|
71
|
-
|
|
276
|
+
async capabilityPreview(workspaceId, relativeCwd = '.') {
|
|
277
|
+
const cwd = await this.resolveCwd(workspaceId, relativeCwd), backend = await this.factory(`preview-${randomUUID()}`, cwd);
|
|
278
|
+
try {
|
|
279
|
+
return await backend.getCapabilities();
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
await backend.dispose();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* The system prompt an Agent created here would start with. `template` previews a template;
|
|
287
|
+
* `piDefaultPrompt` ignores SYSTEM.md / APPEND_SYSTEM.md to show Pi's built-in prompt.
|
|
288
|
+
*/
|
|
289
|
+
async systemPromptPreview(workspaceId, relativeCwd = '.', options = {}) {
|
|
290
|
+
const cwd = await this.resolveCwd(workspaceId, relativeCwd), backend = await this.factory(`preview-${randomUUID()}`, cwd, undefined, 'default', options);
|
|
291
|
+
try {
|
|
292
|
+
return { ...(await backend.getSystemPrompt()), cwd };
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
await backend.dispose();
|
|
296
|
+
}
|
|
72
297
|
}
|
|
73
|
-
finally {
|
|
74
|
-
await backend.dispose();
|
|
75
|
-
} }
|
|
76
298
|
connect(entry, backend) {
|
|
77
299
|
entry.backend = backend;
|
|
300
|
+
entry.feed?.close();
|
|
301
|
+
const agentId = entry.record.agentId, feed = new AgentFeed(() => backend.timeline(), message => {
|
|
302
|
+
for (const listener of this.detailListeners.get(agentId) ?? [])
|
|
303
|
+
listener(message);
|
|
304
|
+
});
|
|
305
|
+
entry.feed = feed;
|
|
78
306
|
backend.subscribe(event => {
|
|
79
307
|
if (entry.backend !== backend)
|
|
80
308
|
return;
|
|
@@ -87,14 +315,8 @@ export class AgentManager {
|
|
|
87
315
|
entry.streaming = false;
|
|
88
316
|
if (type === 'queue_update')
|
|
89
317
|
entry.queue = { steering: toStringList(event.steering), followUp: toStringList(event.followUp) };
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
if (typeof event.id === 'string') {
|
|
93
|
-
if (type === 'bash_execution_start' || type === 'bash_execution_update')
|
|
94
|
-
entry.bashIds.add(event.id);
|
|
95
|
-
else if (type === 'bash_execution_end')
|
|
96
|
-
entry.bashIds.delete(event.id);
|
|
97
|
-
}
|
|
318
|
+
const activityChanged = entry.activity.apply(event);
|
|
319
|
+
// Keep dialog payloads so reconnecting clients can rebuild their controls.
|
|
98
320
|
if (typeof event.requestId === 'string') {
|
|
99
321
|
if (type === 'extension_ui_request')
|
|
100
322
|
entry.dialogRequests.set(event.requestId, event);
|
|
@@ -103,122 +325,419 @@ export class AgentManager {
|
|
|
103
325
|
}
|
|
104
326
|
record.status = runStatus(entry, entry.streaming === true);
|
|
105
327
|
if (type === 'session_info_changed')
|
|
106
|
-
record.sessionName =
|
|
107
|
-
|
|
328
|
+
record.sessionName =
|
|
329
|
+
typeof event.name === 'string' && event.name.trim() ? event.name.trim() : undefined;
|
|
330
|
+
feed.handle(event);
|
|
331
|
+
const slim = slimEvent(event);
|
|
332
|
+
if (slim)
|
|
333
|
+
this.publish(entry, slim);
|
|
334
|
+
if (activityChanged)
|
|
335
|
+
this.publishActivity(entry);
|
|
108
336
|
});
|
|
109
337
|
}
|
|
338
|
+
publishActivity(entry) {
|
|
339
|
+
this.publish(entry, { type: 'activity_changed', activity: entry.activity.snapshot() });
|
|
340
|
+
}
|
|
341
|
+
/** Loading or unloading a backend is not activity: it must not reorder the Agent list. */
|
|
110
342
|
publish(entry, event) {
|
|
111
343
|
const record = entry.record;
|
|
112
|
-
|
|
344
|
+
if (!LIFECYCLE_ONLY.has(event.type))
|
|
345
|
+
record.lastActiveAt = new Date().toISOString();
|
|
113
346
|
// Every event carries the authoritative status, so clients never have to derive it from event types.
|
|
114
|
-
const item = {
|
|
115
|
-
entry.events.push(item);
|
|
116
|
-
if (entry.events.length > 1000)
|
|
117
|
-
entry.events.shift();
|
|
118
|
-
for (const listener of this.listeners.get(record.agentId) ?? [])
|
|
119
|
-
listener(item);
|
|
347
|
+
const item = { timestamp: Date.now(), status: record.status, event };
|
|
120
348
|
for (const listener of this.globalListeners)
|
|
121
349
|
listener(record.agentId, item);
|
|
122
350
|
}
|
|
123
|
-
attach(id, record, backend) {
|
|
124
|
-
|
|
351
|
+
attach(id, record, backend) {
|
|
352
|
+
const entry = {
|
|
353
|
+
record,
|
|
354
|
+
backend,
|
|
355
|
+
writes: 0,
|
|
356
|
+
activity: new ActivityTracker(),
|
|
357
|
+
dialogRequests: new Map(),
|
|
358
|
+
queue: emptyQueue()
|
|
359
|
+
};
|
|
360
|
+
this.connect(entry, backend);
|
|
361
|
+
this.agents.set(id, entry);
|
|
362
|
+
}
|
|
363
|
+
async restore(records) {
|
|
364
|
+
for (const saved of records) {
|
|
365
|
+
try {
|
|
366
|
+
const ws = this.workspaces.get(saved.workspaceId);
|
|
367
|
+
if (!ws)
|
|
368
|
+
continue;
|
|
369
|
+
const cwd = await this.workspaces.resolve(ws, pathRelative(ws.rootPath, saved.cwd));
|
|
370
|
+
const sessionModified = saved.sessionFile
|
|
371
|
+
? await import('node:fs/promises').then(fs => fs
|
|
372
|
+
.stat(saved.sessionFile)
|
|
373
|
+
.then(value => value.mtime.toISOString())
|
|
374
|
+
.catch(() => undefined))
|
|
375
|
+
: undefined;
|
|
376
|
+
const record = {
|
|
377
|
+
...saved,
|
|
378
|
+
profile: saved.profile ?? 'default',
|
|
379
|
+
cwd,
|
|
380
|
+
status: 'unloaded',
|
|
381
|
+
lastActiveAt: sessionModified && sessionModified > saved.lastActiveAt ? sessionModified : saved.lastActiveAt
|
|
382
|
+
};
|
|
383
|
+
this.agents.set(saved.agentId, {
|
|
384
|
+
record,
|
|
385
|
+
writes: 0,
|
|
386
|
+
activity: new ActivityTracker(),
|
|
387
|
+
dialogRequests: new Map(),
|
|
388
|
+
queue: emptyQueue()
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
/* Keep server startup resilient to deleted workspaces. */
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
get(id) {
|
|
397
|
+
const x = this.agents.get(id);
|
|
398
|
+
if (!x)
|
|
399
|
+
throw Object.assign(new Error('Agent not found'), { code: 'AGENT_NOT_FOUND' });
|
|
400
|
+
return x;
|
|
401
|
+
}
|
|
402
|
+
list() {
|
|
403
|
+
return [...this.agents.values()].map(x => ({ ...x.record }));
|
|
404
|
+
}
|
|
405
|
+
isLoaded(id) {
|
|
406
|
+
return !!this.get(id).backend;
|
|
407
|
+
}
|
|
408
|
+
assertCurrent(entry, backend) {
|
|
409
|
+
if (entry.unloading ||
|
|
410
|
+
this.agents.get(entry.record.agentId) !== entry ||
|
|
411
|
+
(backend && entry.backend !== backend))
|
|
412
|
+
throw Object.assign(new Error('Agent session changed; retry the command'), { code: 'SESSION_BUSY' });
|
|
413
|
+
}
|
|
414
|
+
/** All session mutations share admission. Replacement is exclusive; ordinary writes remain concurrent. */
|
|
415
|
+
async writeSession(id, action) {
|
|
416
|
+
const entry = this.get(id);
|
|
417
|
+
this.assertCurrent(entry);
|
|
418
|
+
if (entry.templateChanging)
|
|
419
|
+
throw Object.assign(new Error('System Prompt is being changed'), { code: 'SESSION_BUSY' });
|
|
420
|
+
entry.writes++;
|
|
125
421
|
try {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const cwd = await this.workspaces.resolve(ws, pathRelative(ws.rootPath, saved.cwd));
|
|
130
|
-
const sessionModified = saved.sessionFile ? await import('node:fs/promises').then(fs => fs.stat(saved.sessionFile).then(value => value.mtime.toISOString()).catch(() => undefined)) : undefined;
|
|
131
|
-
const record = { ...saved, profile: saved.profile ?? 'default', cwd, status: 'unloaded', lastActiveAt: sessionModified && sessionModified > saved.lastActiveAt ? sessionModified : saved.lastActiveAt };
|
|
132
|
-
this.agents.set(saved.agentId, { record, events: [], nextSequence: 1, bashIds: new Set(), dialogRequests: new Map(), queue: emptyQueue() });
|
|
422
|
+
const backend = await this.ensureLoaded(id);
|
|
423
|
+
this.assertCurrent(entry, backend);
|
|
424
|
+
return await action(backend, entry);
|
|
133
425
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
426
|
+
finally {
|
|
427
|
+
entry.writes--;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async ensureLoaded(id) {
|
|
431
|
+
const entry = this.get(id);
|
|
432
|
+
this.assertCurrent(entry);
|
|
433
|
+
if (entry.backend)
|
|
434
|
+
return entry.backend;
|
|
435
|
+
if (entry.record.status === 'stopped')
|
|
436
|
+
throw Object.assign(new Error('Agent is not running'), { code: 'AGENT_NOT_RUNNING' });
|
|
437
|
+
if (entry.loading)
|
|
438
|
+
return entry.loading;
|
|
439
|
+
entry.record.status = 'starting';
|
|
440
|
+
entry.loading = (async () => {
|
|
441
|
+
try {
|
|
442
|
+
const backend = await this.factory(id, entry.record.cwd, entry.record.sessionFile, entry.record.profile, entry.record.template ? { template: entry.record.template } : {});
|
|
443
|
+
let state;
|
|
444
|
+
try {
|
|
445
|
+
state = await backend.getState();
|
|
446
|
+
this.assertCurrent(entry);
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
await backend.dispose();
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
entry.record = {
|
|
453
|
+
...entry.record,
|
|
454
|
+
...state,
|
|
455
|
+
sessionName: state.sessionName?.trim() || entry.record.sessionName,
|
|
456
|
+
status: state.status === 'stopped' ? 'idle' : state.status
|
|
457
|
+
};
|
|
458
|
+
this.connect(entry, backend);
|
|
459
|
+
// The status moved from unloaded to idle without a Pi event; tell clients.
|
|
460
|
+
this.publish(entry, { type: 'agent_loaded' });
|
|
461
|
+
return backend;
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
if (!entry.unloading)
|
|
465
|
+
entry.record.status = 'error';
|
|
466
|
+
throw error;
|
|
467
|
+
}
|
|
468
|
+
finally {
|
|
469
|
+
entry.loading = undefined;
|
|
470
|
+
}
|
|
471
|
+
})();
|
|
472
|
+
return entry.loading;
|
|
473
|
+
}
|
|
474
|
+
async command(id, kind, message = '') {
|
|
475
|
+
if (kind === 'abort') {
|
|
476
|
+
const entry = this.get(id);
|
|
477
|
+
if (!entry.backend)
|
|
478
|
+
return;
|
|
479
|
+
if (entry.activity.stopping())
|
|
480
|
+
this.publishActivity(entry);
|
|
481
|
+
return entry.backend.abort();
|
|
482
|
+
}
|
|
483
|
+
return this.writeSession(id, async (backend, entry) => {
|
|
484
|
+
const state = await backend.getState();
|
|
485
|
+
this.assertCurrent(entry, backend);
|
|
486
|
+
// Read and reserve without yielding: simultaneous idle inputs start exactly one turn.
|
|
487
|
+
const busy = entry.promptPending || entry.streaming || state.status === 'streaming';
|
|
488
|
+
const effective = busy ? (kind === 'prompt' ? 'follow-up' : kind) : 'prompt';
|
|
489
|
+
if (effective === 'prompt')
|
|
490
|
+
entry.promptPending = true;
|
|
491
|
+
try {
|
|
492
|
+
return await backend[effective === 'follow-up' ? 'followUp' : effective](message);
|
|
493
|
+
}
|
|
494
|
+
finally {
|
|
495
|
+
if (effective === 'prompt' && entry.backend === backend)
|
|
496
|
+
entry.promptPending = false;
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
}
|
|
161
500
|
/** Queued steering and follow-up messages waiting for the running turn. */
|
|
162
501
|
// Both read through to the backend and leave the cache to `queue_update`, which stays correct when a
|
|
163
502
|
// message is queued while these run.
|
|
164
|
-
async queue(id) {
|
|
503
|
+
async queue(id) {
|
|
504
|
+
const entry = this.get(id);
|
|
505
|
+
return entry.backend
|
|
506
|
+
? entry.backend.getQueue()
|
|
507
|
+
: { steering: [...entry.queue.steering], followUp: [...entry.queue.followUp] };
|
|
508
|
+
}
|
|
165
509
|
/** Drop queued messages and return them so the caller can restore them to its composer. */
|
|
166
|
-
async clearQueue(id) {
|
|
167
|
-
|
|
168
|
-
|
|
510
|
+
async clearQueue(id) {
|
|
511
|
+
const entry = this.get(id);
|
|
512
|
+
return entry.backend ? this.writeSession(id, backend => backend.clearQueue()) : emptyQueue();
|
|
513
|
+
}
|
|
514
|
+
async moveQueue(id, from, index, expected) {
|
|
515
|
+
return this.writeSession(id, backend => backend.moveQueue(from, index, expected));
|
|
516
|
+
}
|
|
169
517
|
snapshotState(id) {
|
|
170
518
|
const a = this.get(id);
|
|
171
|
-
return {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
519
|
+
return {
|
|
520
|
+
...a.record,
|
|
521
|
+
activity: {
|
|
522
|
+
...a.activity.snapshot(),
|
|
523
|
+
dialogRequests: [...a.dialogRequests.values()],
|
|
524
|
+
queue: { steering: [...a.queue.steering], followUp: [...a.queue.followUp] }
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Start viewing an Agent in detail: load it, then subscribe and take a snapshot together, with
|
|
530
|
+
* nothing in between, so every message the listener gets is newer than the snapshot.
|
|
531
|
+
*/
|
|
532
|
+
async focus(id, listener, limit) {
|
|
533
|
+
await this.ensureLoaded(id);
|
|
534
|
+
const entry = this.get(id);
|
|
535
|
+
// Snapshot first: it publishes pending changes, which it includes, before this listener exists.
|
|
536
|
+
const snapshot = {
|
|
537
|
+
...entry.feed.snapshot(limit),
|
|
538
|
+
// Dialog controls live in the transcript, which a viewer replaces when it starts viewing.
|
|
539
|
+
dialogRequests: [...entry.dialogRequests.values()]
|
|
540
|
+
};
|
|
541
|
+
if (!this.detailListeners.has(id))
|
|
542
|
+
this.detailListeners.set(id, new Set());
|
|
543
|
+
this.detailListeners.get(id).add(listener);
|
|
544
|
+
return { unsubscribe: () => this.detailListeners.get(id)?.delete(listener), snapshot };
|
|
545
|
+
}
|
|
546
|
+
toolOutput(id, toolCallId) {
|
|
547
|
+
return this.get(id).feed?.output(toolCallId);
|
|
548
|
+
}
|
|
549
|
+
/** Events of every Agent: server-side consumers and the overview stream. */
|
|
550
|
+
subscribeAll(listener) {
|
|
551
|
+
this.globalListeners.add(listener);
|
|
552
|
+
return () => this.globalListeners.delete(listener);
|
|
553
|
+
}
|
|
554
|
+
dispose(id) {
|
|
555
|
+
const entry = this.get(id);
|
|
556
|
+
if (entry.unloading)
|
|
557
|
+
return entry.unloading;
|
|
558
|
+
// Reserve Stop synchronously. In-flight loads/replacements must validate before attaching.
|
|
559
|
+
entry.unloading = Promise.resolve()
|
|
560
|
+
.then(async () => {
|
|
561
|
+
if (entry.loading)
|
|
562
|
+
await entry.loading.catch(() => { });
|
|
563
|
+
const backend = entry.backend;
|
|
564
|
+
entry.backend = undefined;
|
|
565
|
+
entry.feed?.close();
|
|
566
|
+
entry.feed = undefined;
|
|
567
|
+
try {
|
|
568
|
+
await backend?.dispose();
|
|
569
|
+
}
|
|
570
|
+
finally {
|
|
571
|
+
entry.record.status = 'unloaded';
|
|
572
|
+
entry.streaming = false;
|
|
573
|
+
entry.promptPending = false;
|
|
574
|
+
entry.dialogRequests.clear();
|
|
575
|
+
entry.activity.reset();
|
|
576
|
+
entry.queue = emptyQueue();
|
|
577
|
+
this.publish(entry, { type: 'agent_unloaded' });
|
|
578
|
+
}
|
|
579
|
+
})
|
|
580
|
+
.finally(() => {
|
|
581
|
+
entry.unloading = undefined;
|
|
582
|
+
});
|
|
583
|
+
return entry.unloading;
|
|
584
|
+
}
|
|
585
|
+
async remove(id) {
|
|
586
|
+
await this.dispose(id);
|
|
587
|
+
this.agents.delete(id);
|
|
588
|
+
this.detailListeners.delete(id);
|
|
589
|
+
}
|
|
590
|
+
async archive(id) {
|
|
591
|
+
const current = this.get(id).record;
|
|
592
|
+
if (['starting', 'streaming', 'waiting_for_user', 'stopping'].includes(current.status))
|
|
593
|
+
throw Object.assign(new Error('Active Agent cannot be archived'), { code: 'AGENT_ACTIVE' });
|
|
594
|
+
const record = { ...current, status: 'unloaded' };
|
|
595
|
+
await this.remove(id);
|
|
596
|
+
return record;
|
|
597
|
+
}
|
|
203
598
|
// The record status, not the backend's, is authoritative: it also knows about pending dialogs.
|
|
204
|
-
async state(id) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
async
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
async
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
599
|
+
async state(id) {
|
|
600
|
+
const state = await (await this.ensureLoaded(id)).getState(), record = this.get(id).record;
|
|
601
|
+
return { ...state, status: record.status, sessionName: state.sessionName?.trim() || record.sessionName };
|
|
602
|
+
}
|
|
603
|
+
/** Older history: the page before item `before` (or the newest one). Live viewers get it from focus(). */
|
|
604
|
+
async timeline(id, limit, before) {
|
|
605
|
+
const source = (await this.ensureLoaded(id)).timeline(), page = pageTimeline(buildTimeline(source.entries()), limit, before);
|
|
606
|
+
if (!page)
|
|
607
|
+
throw Object.assign(new Error('Timeline item not found'), { code: 'TIMELINE_ITEM_NOT_FOUND' });
|
|
608
|
+
return { sessionId: source.sessionId, ...page };
|
|
609
|
+
}
|
|
610
|
+
/** All timing records of the current branch; older sessions only have partial tool timings. */
|
|
611
|
+
async performance(id) {
|
|
612
|
+
const source = (await this.ensureLoaded(id)).timeline();
|
|
613
|
+
return { sessionId: source.sessionId, ...sessionPerformance(source.entries()) };
|
|
614
|
+
}
|
|
615
|
+
/** The full body of one timeline item of the given session. It never changes. */
|
|
616
|
+
async item(id, sessionId, itemId) {
|
|
617
|
+
const source = (await this.ensureLoaded(id)).timeline(), body = source.sessionId === sessionId ? itemBody(buildTimeline(source.entries()), itemId) : undefined;
|
|
618
|
+
if (body === undefined)
|
|
619
|
+
throw Object.assign(new Error('Timeline item not found'), { code: 'TIMELINE_ITEM_NOT_FOUND' });
|
|
620
|
+
return { id: itemId, body };
|
|
621
|
+
}
|
|
622
|
+
async systemPrompt(id) {
|
|
623
|
+
const info = await (await this.ensureLoaded(id)).getSystemPrompt();
|
|
624
|
+
return { ...info, template: this.get(id).record.template ?? null };
|
|
625
|
+
}
|
|
626
|
+
/** Prepare → persist → activate under the template lock. A failed save leaves the old session intact. */
|
|
627
|
+
async setTemplate(id, template, persist) {
|
|
628
|
+
const entry = this.get(id);
|
|
629
|
+
this.assertCurrent(entry);
|
|
630
|
+
if (entry.record.profile !== 'default' ||
|
|
631
|
+
entry.record.status === 'stopped' ||
|
|
632
|
+
entry.streaming ||
|
|
633
|
+
entry.writes)
|
|
634
|
+
throw Object.assign(new Error('System Prompt can only change before the first turn'), {
|
|
635
|
+
code: 'SESSION_STARTED'
|
|
636
|
+
});
|
|
637
|
+
if (entry.templateChanging)
|
|
638
|
+
throw Object.assign(new Error('System Prompt is being changed'), { code: 'SESSION_BUSY' });
|
|
639
|
+
entry.templateChanging = true;
|
|
640
|
+
try {
|
|
641
|
+
const old = await this.ensureLoaded(id);
|
|
642
|
+
this.assertCurrent(entry, old);
|
|
643
|
+
const session = await old.getSession();
|
|
644
|
+
if (session.entries.some(x => x.type === 'message'))
|
|
645
|
+
throw Object.assign(new Error('This session already has messages'), { code: 'SESSION_STARTED' });
|
|
646
|
+
const capabilities = await old.getCapabilities();
|
|
647
|
+
this.assertCurrent(entry, old);
|
|
648
|
+
const replacement = await this.factory(id, entry.record.cwd, entry.record.sessionFile, entry.record.profile, template ? { template } : {});
|
|
649
|
+
try {
|
|
650
|
+
const model = capabilities.model;
|
|
651
|
+
if (model?.provider && model?.id)
|
|
652
|
+
await replacement.setModel(model.provider, model.id);
|
|
653
|
+
if (typeof capabilities.thinkingLevel === 'string')
|
|
654
|
+
await replacement.setThinkingLevel(capabilities.thinkingLevel);
|
|
655
|
+
if (entry.record.sessionName)
|
|
656
|
+
await replacement.setSessionName(entry.record.sessionName);
|
|
657
|
+
const state = await replacement.getState();
|
|
658
|
+
this.assertCurrent(entry, old);
|
|
659
|
+
const record = { ...entry.record, ...state, template };
|
|
660
|
+
// Persistence is synchronous: no command can observe a choice that has not been committed.
|
|
661
|
+
persist(record);
|
|
662
|
+
entry.record = record;
|
|
663
|
+
this.connect(entry, replacement);
|
|
664
|
+
this.publish(entry, { type: 'agent_loaded' });
|
|
665
|
+
await old.dispose().catch(() => { });
|
|
666
|
+
return record;
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
await replacement.dispose();
|
|
670
|
+
throw error;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
finally {
|
|
674
|
+
entry.templateChanging = false;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
async capabilities(id) {
|
|
678
|
+
return (await this.ensureLoaded(id)).getCapabilities();
|
|
679
|
+
}
|
|
680
|
+
async session(id) {
|
|
681
|
+
const info = await (await this.ensureLoaded(id)).getSession(), name = typeof info.sessionName === 'string' && info.sessionName.trim()
|
|
682
|
+
? info.sessionName
|
|
683
|
+
: this.get(id).record.sessionName;
|
|
684
|
+
return { ...info, sessionName: name };
|
|
685
|
+
}
|
|
686
|
+
async sessionInfo(id) {
|
|
687
|
+
const info = await (await this.ensureLoaded(id)).getSessionInfo(), name = typeof info.sessionName === 'string' && info.sessionName.trim()
|
|
688
|
+
? info.sessionName
|
|
689
|
+
: this.get(id).record.sessionName;
|
|
690
|
+
return { ...info, sessionName: name };
|
|
691
|
+
}
|
|
692
|
+
async compact(id, instructions) {
|
|
693
|
+
return this.writeSession(id, backend => backend.compact(instructions));
|
|
694
|
+
}
|
|
695
|
+
async setModel(id, provider, modelId) {
|
|
696
|
+
return this.writeSession(id, backend => backend.setModel(provider, modelId));
|
|
697
|
+
}
|
|
698
|
+
async setThinkingLevel(id, level) {
|
|
699
|
+
return this.writeSession(id, backend => backend.setThinkingLevel(level));
|
|
700
|
+
}
|
|
701
|
+
async setSessionName(id, name) {
|
|
702
|
+
return this.writeSession(id, async (backend, entry) => {
|
|
703
|
+
await backend.setSessionName(name);
|
|
704
|
+
this.assertCurrent(entry, backend);
|
|
705
|
+
entry.record.sessionName = name.trim();
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
async navigate(id, entryId) {
|
|
709
|
+
return this.writeSession(id, async (backend, entry) => {
|
|
710
|
+
const result = await backend.navigate(entryId);
|
|
711
|
+
this.assertCurrent(entry, backend);
|
|
712
|
+
entry.feed?.sync(); // Moving the leaf back is no event; tell viewers to reload.
|
|
713
|
+
return result;
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
async fork(id, entryId) {
|
|
717
|
+
return this.writeSession(id, async (backend, source) => {
|
|
718
|
+
const result = await backend.fork(entryId);
|
|
719
|
+
this.assertCurrent(source, backend);
|
|
720
|
+
if (!result.sessionFile)
|
|
721
|
+
throw Object.assign(new Error('Session persistence is disabled'), { code: 'SESSION_NOT_PERSISTED' });
|
|
722
|
+
const ws = this.workspaces.get(source.record.workspaceId);
|
|
723
|
+
const agent = await this.create(source.record.workspaceId, pathRelative(ws.rootPath, source.record.cwd), result.sessionFile, source.record.profile, source.record.template);
|
|
724
|
+
return { agent, selectedText: result.selectedText };
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
async extensionResponse(id, requestId, value) {
|
|
728
|
+
return this.writeSession(id, backend => backend.extensionResponse(requestId, value));
|
|
729
|
+
}
|
|
730
|
+
async listCommands(id) {
|
|
731
|
+
return (await this.ensureLoaded(id)).listCommands();
|
|
732
|
+
}
|
|
220
733
|
/** Session-scoped bash. Output is streamed as `bash_execution_update` events and recorded in the Session. */
|
|
221
|
-
async runBash(id, command, excludeFromContext = false) {
|
|
222
|
-
|
|
223
|
-
|
|
734
|
+
async runBash(id, command, excludeFromContext = false) {
|
|
735
|
+
return this.writeSession(id, backend => backend.runBash(command, excludeFromContext));
|
|
736
|
+
}
|
|
737
|
+
async abortBash(id) {
|
|
738
|
+
const entry = this.get(id);
|
|
739
|
+
if (entry.loading)
|
|
740
|
+
await entry.loading;
|
|
741
|
+
entry.backend?.abortBash();
|
|
742
|
+
}
|
|
224
743
|
}
|