@jmfederico/pi-web 1.202606.1 → 1.202606.3

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.
Files changed (54) hide show
  1. package/README.md +4 -2
  2. package/dist/cli.js +14 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/client/assets/{CodeViewer-BcQUZop_.js → CodeViewer-iYSv2-dg.js} +1 -1
  5. package/dist/client/assets/{TerminalPanel-1o4j3QP_.js → TerminalPanel-BpFW6j27.js} +1 -1
  6. package/dist/client/assets/index-D4dbiKXF.js +2210 -0
  7. package/dist/client/index.html +1 -1
  8. package/dist/pi-web-plugins/updates/package.json +1 -1
  9. package/dist/server/app.js +15 -6
  10. package/dist/server/app.js.map +1 -1
  11. package/dist/server/machines/machinePluginProxyRoutes.js +8 -0
  12. package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
  13. package/dist/server/machines/machineRoutes.js +6 -0
  14. package/dist/server/machines/machineRoutes.js.map +1 -1
  15. package/dist/server/machines/machineService.js +97 -5
  16. package/dist/server/machines/machineService.js.map +1 -1
  17. package/dist/server/piWebPluginService.js +24 -4
  18. package/dist/server/piWebPluginService.js.map +1 -1
  19. package/dist/server/piWebStatus.js +134 -39
  20. package/dist/server/piWebStatus.js.map +1 -1
  21. package/dist/server/piWebStatusCache.js +32 -0
  22. package/dist/server/piWebStatusCache.js.map +1 -0
  23. package/dist/server/sessiond/sessionProxyRoutes.js +2 -1
  24. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  25. package/dist/server/sessiond.js +18 -7
  26. package/dist/server/sessiond.js.map +1 -1
  27. package/dist/server/sessions/oauthLoginFlowService.js +7 -0
  28. package/dist/server/sessions/oauthLoginFlowService.js.map +1 -1
  29. package/dist/server/sessions/piSessionManagerGateway.js +96 -0
  30. package/dist/server/sessions/piSessionManagerGateway.js.map +1 -0
  31. package/dist/server/sessions/piSessionService.js +129 -61
  32. package/dist/server/sessions/piSessionService.js.map +1 -1
  33. package/dist/server/sessions/sessionArchiveStore.js +16 -2
  34. package/dist/server/sessions/sessionArchiveStore.js.map +1 -1
  35. package/dist/server/sessions/sessionRoutes.js +132 -43
  36. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  37. package/dist/server/terminals/terminalRoutes.js +10 -4
  38. package/dist/server/terminals/terminalRoutes.js.map +1 -1
  39. package/dist/server/workingDirectory.js +44 -0
  40. package/dist/server/workingDirectory.js.map +1 -0
  41. package/dist/server/workspaces/fileSuggestions.js +96 -16
  42. package/dist/server/workspaces/fileSuggestions.js.map +1 -1
  43. package/dist/shared/apiTypes.d.ts +36 -2
  44. package/dist/shared/apiTypes.js +3 -1
  45. package/dist/shared/apiTypes.js.map +1 -1
  46. package/dist/shared/capabilities.js +25 -0
  47. package/dist/shared/capabilities.js.map +1 -0
  48. package/dist/shared/federatedRoutes.js +2 -0
  49. package/dist/shared/federatedRoutes.js.map +1 -1
  50. package/dist/shared/piWebStatusParsing.js +43 -0
  51. package/dist/shared/piWebStatusParsing.js.map +1 -1
  52. package/docs/plugins.md +22 -26
  53. package/package.json +5 -5
  54. package/dist/client/assets/index-CM8WX5nf.js +0 -1994
@@ -0,0 +1,96 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
+ import { getAgentDir, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
5
+ import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
6
+ export const PI_SESSION_DIR_ENV = "PI_CODING_AGENT_SESSION_DIR";
7
+ export class SessionDirResolver {
8
+ constructor(options = {}) {
9
+ this.agentDir = options.agentDir ?? getAgentDir();
10
+ this.env = options.env ?? process.env;
11
+ }
12
+ defaultSessionsRoot() {
13
+ return defaultPiSessionsRoot(this.agentDir);
14
+ }
15
+ resolve(cwd) {
16
+ const envSessionDir = this.env[PI_SESSION_DIR_ENV];
17
+ if (envSessionDir !== undefined && envSessionDir !== "") {
18
+ return { source: "env", sessionDir: resolveConfiguredPath(envSessionDir, cwd), usesConfiguredSessionDir: true };
19
+ }
20
+ const settingsSessionDir = SettingsManager.create(cwd, this.agentDir).getSessionDir();
21
+ if (settingsSessionDir !== undefined && settingsSessionDir !== "") {
22
+ return { source: "settings", sessionDir: resolveConfiguredPath(settingsSessionDir, cwd), usesConfiguredSessionDir: true };
23
+ }
24
+ return { source: "pi-default", sessionDir: defaultPiSessionDir(cwd, this.agentDir), usesConfiguredSessionDir: false };
25
+ }
26
+ }
27
+ export function createPiSessionManagerGateway(options = {}) {
28
+ return new SettingsAwarePiSessionManagerGateway(new SessionDirResolver(options));
29
+ }
30
+ class SettingsAwarePiSessionManagerGateway {
31
+ constructor(resolver) {
32
+ this.resolver = resolver;
33
+ }
34
+ async list(cwd) {
35
+ const resolution = this.resolver.resolve(cwd);
36
+ return filterSessionsForCwd(await listSessionsInDir(resolution.sessionDir), cwd);
37
+ }
38
+ create(cwd) {
39
+ const resolution = this.resolver.resolve(cwd);
40
+ return SessionManager.create(cwd, resolution.sessionDir);
41
+ }
42
+ listAll() {
43
+ return listSessionsInDefaultPiStore(this.resolver.defaultSessionsRoot());
44
+ }
45
+ open(path) {
46
+ return SessionManager.open(path, dirname(path));
47
+ }
48
+ }
49
+ export async function listSessionsInDir(sessionDir) {
50
+ // listAll(sessionDir) lists without the SDK's internal cwd filter, which would
51
+ // otherwise compare against this process's cwd and drop other projects' sessions.
52
+ // Cwd filtering is applied explicitly by filterSessionsForCwd where needed.
53
+ // Session file headers are written by external tools (Pi CLI, SDK consumers),
54
+ // so their cwd is canonicalized here before it enters pi-web.
55
+ const sessions = await SessionManager.listAll(sessionDir);
56
+ return sessions.map((session) => ({ ...session, cwd: canonicalizeStoredCwd(session.cwd) }));
57
+ }
58
+ export async function listSessionsInDefaultPiStore(storeRoot = defaultPiSessionsRoot()) {
59
+ let entries;
60
+ try {
61
+ entries = await readdir(storeRoot, { withFileTypes: true });
62
+ }
63
+ catch {
64
+ return [];
65
+ }
66
+ const sessionDirs = entries.filter((entry) => entry.isDirectory()).map((entry) => join(storeRoot, entry.name));
67
+ const sessions = (await Promise.all(sessionDirs.map((dir) => listSessionsInDir(dir)))).flat();
68
+ return sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
69
+ }
70
+ export function filterSessionsForCwd(sessions, cwd) {
71
+ // Sessions with an empty cwd (old session files) are excluded: resolve("") would
72
+ // resolve to this process's cwd and produce false matches.
73
+ return sessions.filter((session) => session.cwd !== "" && cwdPathsEqual(session.cwd, cwd));
74
+ }
75
+ export function defaultPiSessionsRoot(agentDir = getAgentDir()) {
76
+ return join(agentDir, "sessions");
77
+ }
78
+ export function defaultPiSessionDir(cwd, agentDir = getAgentDir()) {
79
+ return sessionDirInDefaultPiStore(defaultPiSessionsRoot(agentDir), cwd);
80
+ }
81
+ export function sessionDirInDefaultPiStore(storeRoot, cwd) {
82
+ const safePath = `--${cwd.replace(/^[/\\]/u, "").replace(/[/\\:]/gu, "-")}--`;
83
+ return join(storeRoot, safePath);
84
+ }
85
+ export function resolveConfiguredPath(path, cwd) {
86
+ const expanded = expandTildePath(path);
87
+ return isAbsolute(expanded) ? expanded : resolve(cwd, expanded);
88
+ }
89
+ function expandTildePath(path) {
90
+ if (path === "~")
91
+ return homedir();
92
+ if (path.startsWith("~/"))
93
+ return join(homedir(), path.slice(2));
94
+ return path;
95
+ }
96
+ //# sourceMappingURL=piSessionManagerGateway.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"piSessionManagerGateway.js","sourceRoot":"","sources":["../../../src/server/sessions/piSessionManagerGateway.ts"],"names":[],"mappings":"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"}
@@ -7,12 +7,23 @@ import { SessionArchiveStore } from "./sessionArchiveStore.js";
7
7
  import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree } from "./sessionArchiveTree.js";
8
8
  import { fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
9
9
  import { computeEditPreview } from "./editPreview.js";
10
+ import { createPiSessionManagerGateway } from "./piSessionManagerGateway.js";
11
+ import { cwdPathsEqual } from "../workingDirectory.js";
10
12
  function noop() {
11
13
  // Intentionally empty default unsubscribe callback.
12
14
  }
13
15
  function authLossWarningKey(sessionId, provider, modelId) {
14
16
  return `${sessionId}:${provider}/${modelId}`;
15
17
  }
18
+ function sessionIdFromLookup(ref) {
19
+ return typeof ref === "string" ? ref : ref.id;
20
+ }
21
+ function isPiSessionRef(ref) {
22
+ return typeof ref !== "string";
23
+ }
24
+ function lookupMatchesActiveSession(ref, active) {
25
+ return !isPiSessionRef(ref) || cwdPathsEqual(active.runtime.cwd, ref.cwd);
26
+ }
16
27
  function requirePromptText(value) {
17
28
  if (typeof value !== "string")
18
29
  throw new Error("Prompt text is required");
@@ -72,7 +83,7 @@ export class PiSessionService {
72
83
  this.authLossWarnings = new Set();
73
84
  this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
74
85
  this.agentDir = deps.agentDir ?? getAgentDir();
75
- this.sessionManager = deps.sessionManager ?? SessionManager;
86
+ this.sessionManager = deps.sessionManager ?? createPiSessionManagerGateway({ agentDir: this.agentDir });
76
87
  this.modelRegistry = deps.modelRegistry ?? ModelRegistry.create(AuthStorage.create());
77
88
  this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.modelRegistry.authStorage, this.modelRegistry);
78
89
  this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime;
@@ -135,24 +146,24 @@ export class PiSessionService {
135
146
  firstMessage: "",
136
147
  };
137
148
  }
138
- async messages(sessionId, page) {
139
- const session = await this.getOrOpen(sessionId);
149
+ async messages(ref, page) {
150
+ const session = await this.getOrOpen(ref);
140
151
  return pageMessagesAtSafeBoundary(historyMessages(session), page);
141
152
  }
142
- async status(sessionId) {
143
- return this.statusFromSession(await this.getOrOpen(sessionId));
153
+ async status(ref) {
154
+ return this.statusFromSession(await this.getOrOpen(ref));
144
155
  }
145
- async availableModels(sessionId) {
146
- const session = await this.getOrOpen(sessionId);
156
+ async availableModels(ref) {
157
+ const session = await this.getOrOpen(ref);
147
158
  session.modelRegistry.refresh();
148
159
  const models = session.scopedModels.length > 0
149
160
  ? session.scopedModels.map((scoped) => scoped.model)
150
161
  : session.modelRegistry.getAvailable();
151
162
  return models.map(modelToClientModel);
152
163
  }
153
- async setModel(sessionId, provider, modelId) {
154
- await this.assertWritable(sessionId);
155
- const session = await this.getOrOpen(sessionId);
164
+ async setModel(ref, provider, modelId) {
165
+ await this.assertWritable(ref);
166
+ const session = await this.getOrOpen(ref);
156
167
  session.modelRegistry.refresh();
157
168
  const candidates = session.scopedModels.length > 0
158
169
  ? session.scopedModels.map((scoped) => scoped.model)
@@ -166,9 +177,9 @@ export class PiSessionService {
166
177
  this.publishStatus(session);
167
178
  return this.statusFromSession(session);
168
179
  }
169
- async cycleModel(sessionId, direction) {
170
- await this.assertWritable(sessionId);
171
- const session = await this.getOrOpen(sessionId);
180
+ async cycleModel(ref, direction) {
181
+ await this.assertWritable(ref);
182
+ const session = await this.getOrOpen(ref);
172
183
  const result = await session.cycleModel(direction);
173
184
  if (result === undefined)
174
185
  throw new Error(session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available");
@@ -176,21 +187,21 @@ export class PiSessionService {
176
187
  this.publishStatus(session);
177
188
  return this.statusFromSession(session);
178
189
  }
179
- async availableThinkingLevels(sessionId) {
180
- const session = await this.getOrOpen(sessionId);
190
+ async availableThinkingLevels(ref) {
191
+ const session = await this.getOrOpen(ref);
181
192
  return session.getAvailableThinkingLevels();
182
193
  }
183
- async setThinkingLevel(sessionId, level) {
184
- await this.assertWritable(sessionId);
185
- const session = await this.getOrOpen(sessionId);
194
+ async setThinkingLevel(ref, level) {
195
+ await this.assertWritable(ref);
196
+ const session = await this.getOrOpen(ref);
186
197
  session.setThinkingLevel(level);
187
198
  this.publishActivity(session, `thinking: ${session.thinkingLevel}`, "idle");
188
199
  this.publishStatus(session);
189
200
  return this.statusFromSession(session);
190
201
  }
191
- async cycleThinkingLevel(sessionId) {
192
- await this.assertWritable(sessionId);
193
- const session = await this.getOrOpen(sessionId);
202
+ async cycleThinkingLevel(ref) {
203
+ await this.assertWritable(ref);
204
+ const session = await this.getOrOpen(ref);
194
205
  const level = session.cycleThinkingLevel();
195
206
  if (level === undefined)
196
207
  throw new Error("Current model does not support thinking");
@@ -198,8 +209,8 @@ export class PiSessionService {
198
209
  this.publishStatus(session);
199
210
  return this.statusFromSession(session);
200
211
  }
201
- async commands(sessionId) {
202
- const session = await this.getOrOpen(sessionId);
212
+ async commands(ref) {
213
+ const session = await this.getOrOpen(ref);
203
214
  const commands = [...BUILTIN_COMMANDS];
204
215
  for (const command of session.extensionRunner.getRegisteredCommands()) {
205
216
  commands.push({ name: command.invocationName, ...(command.description === undefined ? {} : { description: command.description }), source: "extension" });
@@ -212,11 +223,11 @@ export class PiSessionService {
212
223
  }
213
224
  return commands.sort((a, b) => a.name.localeCompare(b.name));
214
225
  }
215
- async prompt(sessionId, text, streamingBehavior) {
226
+ async prompt(ref, text, streamingBehavior) {
216
227
  const promptText = requirePromptText(text);
217
228
  const requestedBehavior = parsePromptStreamingBehavior(streamingBehavior);
218
- await this.assertWritable(sessionId);
219
- const session = await this.getOrOpen(sessionId);
229
+ await this.assertWritable(ref);
230
+ const session = await this.getOrOpen(ref);
220
231
  this.maybeGenerateSessionName(session, promptText);
221
232
  const isQueued = session.isStreaming || session.isCompacting;
222
233
  const behavior = isQueued ? requestedBehavior ?? "followUp" : undefined;
@@ -250,9 +261,9 @@ export class PiSessionService {
250
261
  this.publishActivity(session, "message queued during compaction", "active");
251
262
  this.publishStatus(session);
252
263
  }
253
- async shell(sessionId, text) {
254
- await this.assertWritable(sessionId);
255
- const active = await this.getActive(sessionId);
264
+ async shell(ref, text) {
265
+ await this.assertWritable(ref);
266
+ const active = await this.getActive(ref);
256
267
  const { session } = active.runtime;
257
268
  const isExcluded = text.startsWith("!!");
258
269
  const command = (isExcluded ? text.slice(2) : text.slice(1)).trim();
@@ -285,24 +296,26 @@ export class PiSessionService {
285
296
  this.publishStatus(session);
286
297
  });
287
298
  }
288
- async runCommand(sessionId, text) {
289
- await this.assertWritable(sessionId);
290
- return this.commandService.run(sessionId, text);
299
+ async runCommand(ref, text) {
300
+ await this.assertWritable(ref);
301
+ const active = await this.getActive(ref);
302
+ return this.commandService.run(active.runtime.session.sessionId, text);
291
303
  }
292
- async respondToCommand(sessionId, requestId, value) {
293
- await this.assertWritable(sessionId);
294
- return this.commandService.respond(sessionId, requestId, value);
304
+ async respondToCommand(ref, requestId, value) {
305
+ await this.assertWritable(ref);
306
+ const active = await this.getActive(ref);
307
+ return this.commandService.respond(active.runtime.session.sessionId, requestId, value);
295
308
  }
296
- async archive(sessionId) {
297
- const session = await this.getOrOpen(sessionId);
309
+ async archive(ref) {
310
+ const session = await this.getOrOpen(ref);
298
311
  if (this.hasActiveWork(session))
299
312
  throw new Error("Stop current session activity before archiving");
300
313
  const archiveInput = await this.archiveInputForSession(session);
301
314
  await this.closeActive(session.sessionId);
302
315
  await this.archiveStore.archive(archiveInput);
303
316
  }
304
- async archiveTree(sessionId) {
305
- const session = await this.getOrOpen(sessionId);
317
+ async archiveTree(ref) {
318
+ const session = await this.getOrOpen(ref);
306
319
  const catalog = await this.workspaceArchiveCandidates(session.sessionManager.getCwd());
307
320
  const root = findArchiveCandidateByIdOrPrefix(catalog, session.sessionId) ?? archiveCandidateFromActiveSession(session, false);
308
321
  const plan = planSessionArchiveTree(root, catalog);
@@ -321,29 +334,47 @@ export class PiSessionService {
321
334
  skippedAlreadyArchivedCount: plan.skippedAlreadyArchivedCount,
322
335
  };
323
336
  }
324
- async restore(sessionId) {
325
- await this.closeActive(sessionId);
326
- await this.archiveStore.restore(sessionId);
327
- }
328
- async detachParent(sessionId) {
329
- const session = await this.getOrOpen(sessionId);
337
+ async restore(ref) {
338
+ const archived = await this.getArchived(ref);
339
+ if (archived === undefined)
340
+ throw new Error("Session not found");
341
+ await this.closeActive(archived.sessionId);
342
+ await this.archiveStore.restore(archived.sessionId);
343
+ }
344
+ async deleteArchived(ref) {
345
+ const record = await this.getArchived(ref);
346
+ if (record === undefined)
347
+ throw new Error("Archived session not found");
348
+ if (this.archiveStore.deleteArchived === undefined)
349
+ throw new Error("Archive store does not support deletion");
350
+ await this.closeActive(record.sessionId);
351
+ if (record.archivePath === undefined)
352
+ await this.ensureArchivedRecordMoved(record);
353
+ await this.archiveStore.deleteArchived(record.sessionId);
354
+ }
355
+ async detachParent(ref) {
356
+ const session = await this.getOrOpen(ref);
330
357
  const sessionFile = session.sessionFile;
331
358
  if (sessionFile === undefined || sessionFile === "")
332
359
  throw new Error("Session is not persisted");
333
360
  await clearParentSession(sessionFile);
334
361
  }
335
- async abort(sessionId) {
336
- const active = this.active.get(sessionId);
337
- if (!active)
362
+ async abort(ref) {
363
+ const active = this.activeForLookup(ref);
364
+ if (active === undefined)
338
365
  return;
366
+ const sessionId = active.runtime.session.sessionId;
339
367
  this.clearCompactionPromptQueue(sessionId);
340
368
  clearSessionQueue(active.runtime.session);
341
369
  await active.runtime.session.abort();
342
370
  this.publishActivity(active.runtime.session, "stopped", "idle");
343
371
  this.publishStatus(active.runtime.session);
344
372
  }
345
- stop(sessionId) {
346
- void this.closeActive(sessionId).catch(() => {
373
+ stop(ref) {
374
+ const active = this.activeForLookup(ref);
375
+ if (active === undefined)
376
+ return;
377
+ void this.closeActive(active.runtime.session.sessionId).catch(() => {
347
378
  // Best-effort shutdown; callers that need errors await closeActive directly.
348
379
  });
349
380
  }
@@ -366,6 +397,12 @@ export class PiSessionService {
366
397
  return record;
367
398
  }
368
399
  }
400
+ async ensureArchivedRecordMoved(record) {
401
+ const session = (await this.sessionManager.list(record.cwd)).find((candidate) => candidate.id === record.sessionId);
402
+ if (session === undefined)
403
+ return record;
404
+ return this.archiveStore.archive(archiveInputFromListEntry(session));
405
+ }
369
406
  async archiveInputForSession(session) {
370
407
  const cwd = session.sessionManager.getCwd();
371
408
  const sessionFile = session.sessionFile;
@@ -444,37 +481,68 @@ export class PiSessionService {
444
481
  await active.runtime.dispose();
445
482
  }
446
483
  }
447
- async assertWritable(sessionId) {
448
- if (await this.archiveStore.isArchived(sessionId))
484
+ async assertWritable(ref) {
485
+ if (await this.getArchived(ref) !== undefined)
449
486
  throw new Error("Archived sessions are read-only. Restore the session to continue.");
450
487
  }
451
- async getOrOpen(sessionId) {
452
- return (await this.getActive(sessionId)).runtime.session;
488
+ async getOrOpen(ref) {
489
+ return (await this.getActive(ref)).runtime.session;
453
490
  }
454
- async getActive(sessionId) {
455
- const active = this.active.get(sessionId);
456
- if (active)
491
+ async getActive(ref) {
492
+ const active = this.activeForLookup(ref);
493
+ if (active !== undefined)
457
494
  return active;
458
- const archived = await this.archiveStore.get(sessionId);
495
+ const archived = await this.getArchived(ref);
459
496
  if (archived?.archivePath !== undefined)
460
497
  return this.create(this.sessionManager.open(archived.archivePath), archived.cwd);
461
- const match = (await this.sessionManager.listAll()).find((s) => s.id === sessionId || s.id.startsWith(sessionId));
498
+ const match = isPiSessionRef(ref)
499
+ ? (await this.sessionManager.list(ref.cwd)).find((s) => s.id === ref.id || s.id.startsWith(ref.id))
500
+ : (await this.sessionManager.listAll?.() ?? []).find((s) => s.id === ref || s.id.startsWith(ref));
462
501
  if (!match)
463
502
  throw new Error("Session not found");
464
503
  return this.create(this.sessionManager.open(match.path), match.cwd);
465
504
  }
505
+ async getArchived(ref) {
506
+ const archived = await this.archiveStore.get(sessionIdFromLookup(ref));
507
+ if (archived === undefined)
508
+ return undefined;
509
+ if (isPiSessionRef(ref) && archived.cwd !== ref.cwd)
510
+ return undefined;
511
+ return archived;
512
+ }
513
+ activeForLookup(ref) {
514
+ const sessionId = sessionIdFromLookup(ref);
515
+ const exact = this.active.get(sessionId);
516
+ if (exact !== undefined && lookupMatchesActiveSession(ref, exact))
517
+ return exact;
518
+ for (const [candidateId, active] of this.active.entries()) {
519
+ if (candidateId.startsWith(sessionId) && lookupMatchesActiveSession(ref, active))
520
+ return active;
521
+ }
522
+ return undefined;
523
+ }
466
524
  async create(sessionManager, cwd) {
467
525
  const runtime = await this.createAgentRuntime(this.createRuntime, { cwd, agentDir: this.agentDir, sessionManager });
526
+ await this.bindSessionExtensions(runtime.session);
468
527
  const active = { runtime, unsubscribe: noop };
469
528
  this.bindRuntime(active);
470
- runtime.setRebindSession(() => {
529
+ runtime.setRebindSession(async (session) => {
530
+ await this.bindSessionExtensions(session);
471
531
  this.bindRuntime(active);
472
- return Promise.resolve();
473
532
  });
474
533
  this.active.set(runtime.session.sessionId, active);
475
534
  this.publishStatus(runtime.session);
476
535
  return active;
477
536
  }
537
+ async bindSessionExtensions(session) {
538
+ await session.bindExtensions({
539
+ onError: (error) => {
540
+ const message = `${error.extensionPath}: ${error.error}`;
541
+ this.publishActivity(session, "extension error", "error", message);
542
+ this.events.publish(session.sessionId, { type: "session.error", message });
543
+ },
544
+ });
545
+ }
478
546
  bindRuntime(active) {
479
547
  active.unsubscribe();
480
548
  const { session } = active.runtime;