@jyork0828/pi-pilot 0.0.3 → 0.0.6
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/dist/index.js +1935 -714
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
- package/public/assets/c-BIGW1oBm.js +1 -0
- package/public/assets/cpp-DIPi6g--.js +1 -0
- package/public/assets/csharp-DSvCPggb.js +1 -0
- package/public/assets/css-CLj8gQPS.js +1 -0
- package/public/assets/dart-bE4Kk8sk.js +1 -0
- package/public/assets/diff-D97Zzqfu.js +1 -0
- package/public/assets/dockerfile-BcOcwvcX.js +1 -0
- package/public/assets/elixir-CkH2-t6x.js +1 -0
- package/public/assets/github-dark-DHJKELXO.js +1 -0
- package/public/assets/github-light-DAi9KRSo.js +1 -0
- package/public/assets/go-C27-OAKa.js +1 -0
- package/public/assets/graphql-ChdNCCLP.js +1 -0
- package/public/assets/haskell-Df6bDoY_.js +1 -0
- package/public/assets/html-pp8916En.js +1 -0
- package/public/assets/index-CsC5-YPT.js +506 -0
- package/public/assets/index-R8FKUxOS.css +1 -0
- package/public/assets/ini-BEwlwnbL.js +1 -0
- package/public/assets/java-CylS5w8V.js +1 -0
- package/public/assets/javascript-wDzz0qaB.js +1 -0
- package/public/assets/json-Cp-IABpG.js +1 -0
- package/public/assets/jsonc-Des-eS-w.js +1 -0
- package/public/assets/jsx-g9-lgVsj.js +1 -0
- package/public/assets/kotlin-BdnUsdx6.js +1 -0
- package/public/assets/less-B1dDrJ26.js +1 -0
- package/public/assets/lua-BaeVxFsk.js +1 -0
- package/public/assets/make-CHLpvVh8.js +1 -0
- package/public/assets/markdown-Cvjx9yec.js +1 -0
- package/public/assets/mdx-Cmh6b_Ma.js +1 -0
- package/public/assets/objective-c-DXmwc3jG.js +1 -0
- package/public/assets/php-Csjmro_R.js +1 -0
- package/public/assets/python-B6aJPvgy.js +1 -0
- package/public/assets/r-Dspwwk_N.js +1 -0
- package/public/assets/ruby-CV7NnX5q.js +1 -0
- package/public/assets/rust-B1yitclQ.js +1 -0
- package/public/assets/scala-C151Ov-r.js +1 -0
- package/public/assets/scss-D5BDwBP9.js +1 -0
- package/public/assets/shellscript-Yzrsuije.js +1 -0
- package/public/assets/sql-CRqJ_cUM.js +1 -0
- package/public/assets/svelte-DR4MIrkg.js +1 -0
- package/public/assets/swift-D82vCrfD.js +1 -0
- package/public/assets/toml-vGWfd6FD.js +1 -0
- package/public/assets/tsx-COt5Ahok.js +1 -0
- package/public/assets/typescript-BPQ3VLAy.js +1 -0
- package/public/assets/vue-DMJtu8ND.js +1 -0
- package/public/assets/xml-sdJ4AIDG.js +1 -0
- package/public/assets/yaml-Buea-lGh.js +1 -0
- package/public/favicon.svg +15 -0
- package/public/index.html +12 -3
- package/public/assets/index-CXMCsN3t.js +0 -228
- package/public/assets/index-D9--qZLP.css +0 -1
package/dist/index.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { existsSync } from "fs";
|
|
5
|
-
import { readFile as
|
|
6
|
-
import { dirname as
|
|
5
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
6
|
+
import { dirname as dirname6, extname, join as join9, resolve as resolve5, sep as sep3 } from "path";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
import { serve } from "@hono/node-server";
|
|
9
|
-
import { Hono as
|
|
9
|
+
import { Hono as Hono5 } from "hono";
|
|
10
10
|
import { cors } from "hono/cors";
|
|
11
11
|
|
|
12
12
|
// src/config.ts
|
|
@@ -24,10 +24,26 @@ var config = {
|
|
|
24
24
|
corsOrigin: process.env.PI_PILOT_CORS_ORIGIN ?? "http://localhost:5173"
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
// src/http-proxy.ts
|
|
28
|
+
import { EnvHttpProxyAgent, install, setGlobalDispatcher } from "undici";
|
|
29
|
+
var configured = false;
|
|
30
|
+
function configureHttpProxy() {
|
|
31
|
+
if (configured) return;
|
|
32
|
+
configured = true;
|
|
33
|
+
setGlobalDispatcher(
|
|
34
|
+
new EnvHttpProxyAgent({
|
|
35
|
+
allowH2: false,
|
|
36
|
+
bodyTimeout: 3e5,
|
|
37
|
+
headersTimeout: 3e5
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
install?.();
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
// src/api/workspaces.ts
|
|
28
|
-
import { stat as stat2 } from "fs/promises";
|
|
44
|
+
import { readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
29
45
|
import { basename as basename2, isAbsolute as isAbsolute3, resolve as resolve3 } from "path";
|
|
30
|
-
import { Hono } from "hono";
|
|
46
|
+
import { Hono as Hono2 } from "hono";
|
|
31
47
|
|
|
32
48
|
// src/storage/resource-writer.ts
|
|
33
49
|
import {
|
|
@@ -261,9 +277,9 @@ async function updatePrompt(opts) {
|
|
|
261
277
|
await writeFile(newPath, text, "utf8");
|
|
262
278
|
try {
|
|
263
279
|
await unlink(opts.filePath);
|
|
264
|
-
} catch (
|
|
280
|
+
} catch (err2) {
|
|
265
281
|
await unlink(newPath).catch(() => void 0);
|
|
266
|
-
throw
|
|
282
|
+
throw err2;
|
|
267
283
|
}
|
|
268
284
|
return newPath;
|
|
269
285
|
}
|
|
@@ -335,17 +351,22 @@ import { dirname as dirname2, join as join3 } from "path";
|
|
|
335
351
|
import { randomUUID } from "crypto";
|
|
336
352
|
var REGISTRY_PATH = join3(config.dataDir, "workspaces.json");
|
|
337
353
|
var cache;
|
|
354
|
+
var writeChain = Promise.resolve();
|
|
355
|
+
function serializedWrite(fn) {
|
|
356
|
+
writeChain = writeChain.then(fn, fn);
|
|
357
|
+
return writeChain;
|
|
358
|
+
}
|
|
338
359
|
async function load() {
|
|
339
360
|
if (cache) return cache;
|
|
340
361
|
try {
|
|
341
362
|
const raw = await readFile2(REGISTRY_PATH, "utf8");
|
|
342
363
|
cache = JSON.parse(raw);
|
|
343
364
|
if (!Array.isArray(cache.workspaces)) cache = { workspaces: [] };
|
|
344
|
-
} catch (
|
|
345
|
-
if (
|
|
365
|
+
} catch (err2) {
|
|
366
|
+
if (err2.code === "ENOENT") {
|
|
346
367
|
cache = { workspaces: [] };
|
|
347
368
|
} else {
|
|
348
|
-
throw
|
|
369
|
+
throw err2;
|
|
349
370
|
}
|
|
350
371
|
}
|
|
351
372
|
return cache;
|
|
@@ -364,26 +385,57 @@ async function getWorkspace(id) {
|
|
|
364
385
|
return r.workspaces.find((w) => w.id === id);
|
|
365
386
|
}
|
|
366
387
|
async function addWorkspace(input) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
388
|
+
let result;
|
|
389
|
+
await serializedWrite(async () => {
|
|
390
|
+
const r = await load();
|
|
391
|
+
const existing = r.workspaces.find((w) => w.path === input.path);
|
|
392
|
+
if (existing) {
|
|
393
|
+
result = existing;
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const ws = {
|
|
397
|
+
id: randomUUID(),
|
|
398
|
+
name: input.name,
|
|
399
|
+
path: input.path,
|
|
400
|
+
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
401
|
+
};
|
|
402
|
+
r.workspaces.push(ws);
|
|
403
|
+
await save();
|
|
404
|
+
result = ws;
|
|
405
|
+
});
|
|
406
|
+
return result;
|
|
379
407
|
}
|
|
380
408
|
async function removeWorkspace(id) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
409
|
+
let removed = false;
|
|
410
|
+
await serializedWrite(async () => {
|
|
411
|
+
const r = await load();
|
|
412
|
+
const before = r.workspaces.length;
|
|
413
|
+
r.workspaces = r.workspaces.filter((w) => w.id !== id);
|
|
414
|
+
if (r.workspaces.length === before) return;
|
|
415
|
+
removed = true;
|
|
416
|
+
await save();
|
|
417
|
+
});
|
|
418
|
+
return removed;
|
|
419
|
+
}
|
|
420
|
+
async function reorderWorkspaces(ids) {
|
|
421
|
+
await serializedWrite(async () => {
|
|
422
|
+
const r = await load();
|
|
423
|
+
const byId = new Map(r.workspaces.map((w) => [w.id, w]));
|
|
424
|
+
const reordered = [];
|
|
425
|
+
const seen = /* @__PURE__ */ new Set();
|
|
426
|
+
for (const id of ids) {
|
|
427
|
+
const ws = byId.get(id);
|
|
428
|
+
if (ws && !seen.has(id)) {
|
|
429
|
+
reordered.push(ws);
|
|
430
|
+
seen.add(id);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
for (const ws of r.workspaces) {
|
|
434
|
+
if (!seen.has(ws.id)) reordered.push(ws);
|
|
435
|
+
}
|
|
436
|
+
r.workspaces = reordered;
|
|
437
|
+
await save();
|
|
438
|
+
});
|
|
387
439
|
}
|
|
388
440
|
|
|
389
441
|
// src/storage/workspace-stats.ts
|
|
@@ -408,8 +460,8 @@ async function getStats(path) {
|
|
|
408
460
|
const now = Date.now();
|
|
409
461
|
const cached = cache2.get(path);
|
|
410
462
|
if (cached && cached.expiresAt > now) return cached;
|
|
411
|
-
const
|
|
412
|
-
if (
|
|
463
|
+
const pending2 = inflight.get(path);
|
|
464
|
+
if (pending2) return pending2;
|
|
413
465
|
const probe = probeStats(path).then((stats) => {
|
|
414
466
|
const entry = {
|
|
415
467
|
...stats,
|
|
@@ -486,307 +538,1213 @@ import {
|
|
|
486
538
|
SessionManager
|
|
487
539
|
} from "@earendil-works/pi-coding-agent";
|
|
488
540
|
|
|
489
|
-
// src/extensions/
|
|
541
|
+
// src/extensions/todo/schema.ts
|
|
490
542
|
import { Type } from "typebox";
|
|
491
|
-
var
|
|
543
|
+
var EMPTY_STATE = { tasks: [], nextId: 1 };
|
|
544
|
+
var VALID_TRANSITIONS = {
|
|
545
|
+
pending: /* @__PURE__ */ new Set(["in_progress", "completed", "deleted"]),
|
|
546
|
+
in_progress: /* @__PURE__ */ new Set(["pending", "completed", "deleted"]),
|
|
547
|
+
completed: /* @__PURE__ */ new Set(["deleted"]),
|
|
548
|
+
deleted: /* @__PURE__ */ new Set()
|
|
549
|
+
};
|
|
550
|
+
function isTransitionValid(from, to) {
|
|
551
|
+
if (from === to) return true;
|
|
552
|
+
return VALID_TRANSITIONS[from].has(to);
|
|
553
|
+
}
|
|
554
|
+
var ActionEnum = Type.Union([
|
|
555
|
+
Type.Literal("create"),
|
|
556
|
+
Type.Literal("update"),
|
|
557
|
+
Type.Literal("list"),
|
|
558
|
+
Type.Literal("get"),
|
|
559
|
+
Type.Literal("delete"),
|
|
560
|
+
Type.Literal("clear")
|
|
561
|
+
]);
|
|
562
|
+
var StatusEnum = Type.Union([
|
|
492
563
|
Type.Literal("pending"),
|
|
493
564
|
Type.Literal("in_progress"),
|
|
494
|
-
Type.Literal("completed")
|
|
565
|
+
Type.Literal("completed"),
|
|
566
|
+
Type.Literal("deleted")
|
|
495
567
|
]);
|
|
496
|
-
var
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
})
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
})
|
|
568
|
+
var todoParamsSchema = Type.Object({
|
|
569
|
+
action: ActionEnum,
|
|
570
|
+
subject: Type.Optional(Type.String({ description: "Task subject line (required for create)" })),
|
|
571
|
+
description: Type.Optional(Type.String({ description: "Long-form task description" })),
|
|
572
|
+
status: Type.Optional(StatusEnum),
|
|
573
|
+
id: Type.Optional(Type.Number({ description: "Task id (required for update, get, delete)" })),
|
|
574
|
+
includeDeleted: Type.Optional(Type.Boolean({
|
|
575
|
+
description: "If true, list action returns deleted (tombstoned) tasks as well. Default: false."
|
|
576
|
+
}))
|
|
506
577
|
});
|
|
507
578
|
|
|
508
|
-
// src/extensions/
|
|
509
|
-
|
|
579
|
+
// src/extensions/todo/reducer.ts
|
|
580
|
+
function err(state, message) {
|
|
581
|
+
return { state, text: `Error: ${message}`, error: message };
|
|
582
|
+
}
|
|
583
|
+
function formatListLine(t) {
|
|
584
|
+
return `[${t.status}] #${t.id} ${t.subject}`;
|
|
585
|
+
}
|
|
586
|
+
function applyTodoAction(state, action, params) {
|
|
587
|
+
switch (action) {
|
|
588
|
+
case "create": {
|
|
589
|
+
if (!params.subject?.trim()) {
|
|
590
|
+
return err(state, "subject required for create");
|
|
591
|
+
}
|
|
592
|
+
const task = {
|
|
593
|
+
id: state.nextId,
|
|
594
|
+
subject: params.subject,
|
|
595
|
+
status: "pending"
|
|
596
|
+
};
|
|
597
|
+
if (params.description) task.description = params.description;
|
|
598
|
+
const newTasks = [...state.tasks, task];
|
|
599
|
+
return {
|
|
600
|
+
state: { tasks: newTasks, nextId: state.nextId + 1 },
|
|
601
|
+
text: `Created #${task.id}: ${task.subject} (pending)`
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
case "update": {
|
|
605
|
+
if (params.id === void 0) return err(state, "id required for update");
|
|
606
|
+
const idx = state.tasks.findIndex((t) => t.id === params.id);
|
|
607
|
+
if (idx === -1) return err(state, `#${params.id} not found`);
|
|
608
|
+
const current = state.tasks[idx];
|
|
609
|
+
const hasMutation = params.subject !== void 0 || params.description !== void 0 || params.status !== void 0;
|
|
610
|
+
if (!hasMutation) return err(state, "update requires at least one mutable field");
|
|
611
|
+
let newStatus = current.status;
|
|
612
|
+
if (params.status !== void 0) {
|
|
613
|
+
if (!isTransitionValid(current.status, params.status)) {
|
|
614
|
+
return err(state, `illegal transition ${current.status} \u2192 ${params.status}`);
|
|
615
|
+
}
|
|
616
|
+
newStatus = params.status;
|
|
617
|
+
}
|
|
618
|
+
const updated = { ...current, status: newStatus };
|
|
619
|
+
if (params.subject !== void 0) updated.subject = params.subject;
|
|
620
|
+
if (params.description !== void 0) updated.description = params.description;
|
|
621
|
+
const newTasks = [...state.tasks];
|
|
622
|
+
newTasks[idx] = updated;
|
|
623
|
+
const transition = current.status !== newStatus ? ` (${current.status} \u2192 ${newStatus})` : "";
|
|
624
|
+
return {
|
|
625
|
+
state: { tasks: newTasks, nextId: state.nextId },
|
|
626
|
+
text: `Updated #${updated.id}${transition}`
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
case "list": {
|
|
630
|
+
let view = state.tasks;
|
|
631
|
+
if (!params.includeDeleted) view = view.filter((t) => t.status !== "deleted");
|
|
632
|
+
if (params.status) view = view.filter((t) => t.status === params.status);
|
|
633
|
+
return {
|
|
634
|
+
state,
|
|
635
|
+
text: view.length === 0 ? "No tasks" : view.map(formatListLine).join("\n")
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
case "get": {
|
|
639
|
+
if (params.id === void 0) return err(state, "id required for get");
|
|
640
|
+
const task = state.tasks.find((t) => t.id === params.id);
|
|
641
|
+
if (!task) return err(state, `#${params.id} not found`);
|
|
642
|
+
const lines = [`#${task.id} [${task.status}] ${task.subject}`];
|
|
643
|
+
if (task.description) lines.push(` description: ${task.description}`);
|
|
644
|
+
return { state, text: lines.join("\n") };
|
|
645
|
+
}
|
|
646
|
+
case "delete": {
|
|
647
|
+
if (params.id === void 0) return err(state, "id required for delete");
|
|
648
|
+
const idx = state.tasks.findIndex((t) => t.id === params.id);
|
|
649
|
+
if (idx === -1) return err(state, `#${params.id} not found`);
|
|
650
|
+
const current = state.tasks[idx];
|
|
651
|
+
if (current.status === "deleted") return err(state, `#${current.id} is already deleted`);
|
|
652
|
+
const updated = { ...current, status: "deleted" };
|
|
653
|
+
const newTasks = [...state.tasks];
|
|
654
|
+
newTasks[idx] = updated;
|
|
655
|
+
return {
|
|
656
|
+
state: { tasks: newTasks, nextId: state.nextId },
|
|
657
|
+
text: `Deleted #${updated.id}: ${updated.subject}`
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
case "clear": {
|
|
661
|
+
const count = state.tasks.length;
|
|
662
|
+
return {
|
|
663
|
+
state: { tasks: [], nextId: 1 },
|
|
664
|
+
text: `Cleared ${count} tasks`
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/extensions/todo/factory.ts
|
|
671
|
+
var TOOL_NAME = "todo";
|
|
672
|
+
function replayFromBranch(ctx) {
|
|
673
|
+
let result = { tasks: [...EMPTY_STATE.tasks], nextId: EMPTY_STATE.nextId };
|
|
674
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
675
|
+
const e = entry;
|
|
676
|
+
if (e.type !== "message") continue;
|
|
677
|
+
const msg = e.message;
|
|
678
|
+
if (msg?.role !== "toolResult" || msg.toolName !== TOOL_NAME) continue;
|
|
679
|
+
if (msg.isError) continue;
|
|
680
|
+
const details = msg.details;
|
|
681
|
+
if (!details || !Array.isArray(details.tasks) || typeof details.nextId !== "number") continue;
|
|
682
|
+
if (details.error) continue;
|
|
683
|
+
result = {
|
|
684
|
+
tasks: details.tasks.map((t) => ({ ...t })),
|
|
685
|
+
nextId: details.nextId
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
return result;
|
|
689
|
+
}
|
|
690
|
+
var todoExtensionFactory = (pi) => {
|
|
691
|
+
let state = { tasks: [...EMPTY_STATE.tasks], nextId: EMPTY_STATE.nextId };
|
|
510
692
|
pi.registerTool({
|
|
511
|
-
name:
|
|
512
|
-
label: "
|
|
513
|
-
description: "
|
|
514
|
-
|
|
515
|
-
promptSnippet: "update_plan: maintain a live checklist for multi-step tasks; update statuses as you progress.",
|
|
693
|
+
name: TOOL_NAME,
|
|
694
|
+
label: "Todo",
|
|
695
|
+
description: "Manage a task list for tracking multi-step progress. Actions: create (new task), update (change status/fields), list (all tasks, optionally filtered), get (single task), delete (tombstone), clear (reset). Status: pending \u2192 in_progress \u2192 completed, plus deleted tombstone.",
|
|
696
|
+
promptSnippet: "Manage a task list to track multi-step progress.",
|
|
516
697
|
promptGuidelines: [
|
|
517
|
-
"
|
|
518
|
-
"
|
|
519
|
-
|
|
520
|
-
"Exactly one item should be in_progress at a time. Mark completed only when the work is actually done."
|
|
698
|
+
"Use `todo` for complex work with 3+ steps or when the user gives you an explicit list of tasks. Skip it for single trivial tasks and purely conversational requests.",
|
|
699
|
+
"When starting any task, mark it in_progress BEFORE beginning work. Mark it completed IMMEDIATELY when done \u2014 never batch completions. Exactly one task should be in_progress at a time.",
|
|
700
|
+
"Never mark a task completed if tests are failing, the implementation is partial, or you hit unresolved errors \u2014 keep it in_progress and address the issue first."
|
|
521
701
|
],
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
702
|
+
parameters: todoParamsSchema,
|
|
703
|
+
async execute(_toolCallId, params) {
|
|
704
|
+
const result = applyTodoAction(state, params.action, params);
|
|
705
|
+
state = result.state;
|
|
706
|
+
const details = {
|
|
707
|
+
action: params.action,
|
|
708
|
+
tasks: state.tasks,
|
|
709
|
+
nextId: state.nextId,
|
|
710
|
+
...result.error ? { error: result.error } : {}
|
|
711
|
+
};
|
|
712
|
+
return {
|
|
713
|
+
content: [{ type: "text", text: result.text }],
|
|
714
|
+
details
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
pi.registerCommand("todos", {
|
|
719
|
+
description: "Show current todo list grouped by status.",
|
|
720
|
+
handler: async () => {
|
|
721
|
+
const visible = state.tasks.filter((t) => t.status !== "deleted");
|
|
722
|
+
if (visible.length === 0) {
|
|
723
|
+
pi.sendUserMessage("Show the current todo list.");
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const pending2 = visible.filter((t) => t.status === "pending");
|
|
727
|
+
const inProgress = visible.filter((t) => t.status === "in_progress");
|
|
728
|
+
const completed = visible.filter((t) => t.status === "completed");
|
|
729
|
+
const lines = [];
|
|
730
|
+
const total = visible.length;
|
|
731
|
+
const doneCount = completed.length;
|
|
732
|
+
lines.push(`Todos (${doneCount}/${total})`);
|
|
733
|
+
if (inProgress.length > 0) {
|
|
734
|
+
lines.push("\u2500\u2500 In Progress \u2500\u2500");
|
|
735
|
+
for (const t of inProgress) lines.push(` \u25D0 #${t.id} ${t.subject}`);
|
|
736
|
+
}
|
|
737
|
+
if (pending2.length > 0) {
|
|
738
|
+
lines.push("\u2500\u2500 Pending \u2500\u2500");
|
|
739
|
+
for (const t of pending2) lines.push(` \u25CB #${t.id} ${t.subject}`);
|
|
740
|
+
}
|
|
741
|
+
if (completed.length > 0) {
|
|
742
|
+
lines.push("\u2500\u2500 Completed \u2500\u2500");
|
|
743
|
+
for (const t of completed) lines.push(` \u2713 #${t.id} ${t.subject}`);
|
|
744
|
+
}
|
|
745
|
+
pi.sendUserMessage(`Current todos:
|
|
746
|
+
${lines.join("\n")}`);
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
750
|
+
state = replayFromBranch(ctx);
|
|
531
751
|
});
|
|
532
|
-
pi.
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
752
|
+
pi.on("session_compact", async (_event, ctx) => {
|
|
753
|
+
try {
|
|
754
|
+
state = replayFromBranch(ctx);
|
|
755
|
+
} catch {
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
759
|
+
try {
|
|
760
|
+
state = replayFromBranch(ctx);
|
|
761
|
+
} catch {
|
|
538
762
|
}
|
|
539
763
|
});
|
|
764
|
+
pi.on("session_shutdown", async () => {
|
|
765
|
+
state = { tasks: [...EMPTY_STATE.tasks], nextId: EMPTY_STATE.nextId };
|
|
766
|
+
});
|
|
540
767
|
};
|
|
541
768
|
|
|
542
|
-
// src/extensions/
|
|
543
|
-
|
|
769
|
+
// src/extensions/ask_user/schema.ts
|
|
770
|
+
import { Type as Type2 } from "typebox";
|
|
771
|
+
var askUserOptionSchema = Type2.Object({
|
|
772
|
+
label: Type2.String({
|
|
773
|
+
description: "Short, distinct choice (1\u20135 words). User-facing."
|
|
774
|
+
}),
|
|
775
|
+
description: Type2.Optional(
|
|
776
|
+
Type2.String({
|
|
777
|
+
description: "Optional one-line clarification of what this choice means or implies."
|
|
778
|
+
})
|
|
779
|
+
)
|
|
780
|
+
});
|
|
781
|
+
var askUserParamsSchema = Type2.Object({
|
|
782
|
+
question: Type2.String({
|
|
783
|
+
description: "Full question, specific and ending with '?'. Avoid yes/no when options can be more concrete."
|
|
784
|
+
}),
|
|
785
|
+
header: Type2.Optional(
|
|
786
|
+
Type2.String({
|
|
787
|
+
maxLength: 12,
|
|
788
|
+
description: "Optional short chip label (\u2264 12 chars), e.g. 'Auth method'. Shown next to the question."
|
|
789
|
+
})
|
|
790
|
+
),
|
|
791
|
+
options: Type2.Array(askUserOptionSchema, {
|
|
792
|
+
minItems: 2,
|
|
793
|
+
maxItems: 4,
|
|
794
|
+
description: "2\u20134 mutually exclusive choices (unless multiSelect is true). Recommended option first; append ' (Recommended)' to its label."
|
|
795
|
+
}),
|
|
796
|
+
multiSelect: Type2.Optional(
|
|
797
|
+
Type2.Boolean({
|
|
798
|
+
description: "If true, render checkboxes and allow multiple selections. Default false."
|
|
799
|
+
})
|
|
800
|
+
),
|
|
801
|
+
allowOther: Type2.Optional(
|
|
802
|
+
Type2.Boolean({
|
|
803
|
+
description: "If true, show a freeform 'Other' input as an escape hatch. Default true."
|
|
804
|
+
})
|
|
805
|
+
),
|
|
806
|
+
timeoutSec: Type2.Optional(
|
|
807
|
+
Type2.Integer({
|
|
808
|
+
minimum: 5,
|
|
809
|
+
description: "Auto-resolve as skip after this many seconds. Only set for time-sensitive questions where a stale answer would be worse than no answer."
|
|
810
|
+
})
|
|
811
|
+
)
|
|
812
|
+
});
|
|
544
813
|
|
|
545
|
-
// src/
|
|
546
|
-
var
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
setStatus() {
|
|
568
|
-
}
|
|
569
|
-
setWidget() {
|
|
570
|
-
}
|
|
571
|
-
setTitle() {
|
|
572
|
-
}
|
|
573
|
-
setEditorText() {
|
|
574
|
-
}
|
|
575
|
-
pasteToEditor() {
|
|
576
|
-
}
|
|
577
|
-
// ============== TUI-only methods (no-op / defaults) ==============
|
|
578
|
-
onTerminalInput() {
|
|
579
|
-
return () => {
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
setWorkingMessage() {
|
|
583
|
-
}
|
|
584
|
-
setWorkingVisible() {
|
|
585
|
-
}
|
|
586
|
-
setWorkingIndicator() {
|
|
587
|
-
}
|
|
588
|
-
setHiddenThinkingLabel() {
|
|
589
|
-
}
|
|
590
|
-
setFooter() {
|
|
591
|
-
}
|
|
592
|
-
setHeader() {
|
|
593
|
-
}
|
|
594
|
-
async custom() {
|
|
595
|
-
return void 0;
|
|
596
|
-
}
|
|
597
|
-
getEditorText() {
|
|
598
|
-
return "";
|
|
599
|
-
}
|
|
600
|
-
addAutocompleteProvider() {
|
|
601
|
-
}
|
|
602
|
-
setEditorComponent() {
|
|
603
|
-
}
|
|
604
|
-
getEditorComponent() {
|
|
605
|
-
return void 0;
|
|
606
|
-
}
|
|
607
|
-
get theme() {
|
|
608
|
-
return void 0;
|
|
609
|
-
}
|
|
610
|
-
getAllThemes() {
|
|
611
|
-
return [];
|
|
612
|
-
}
|
|
613
|
-
getTheme() {
|
|
614
|
-
return void 0;
|
|
615
|
-
}
|
|
616
|
-
setTheme() {
|
|
617
|
-
return { success: false, error: "Theme switching not supported in pi-pilot" };
|
|
618
|
-
}
|
|
619
|
-
getToolsExpanded() {
|
|
620
|
-
return false;
|
|
814
|
+
// src/extensions/ask_user/registry.ts
|
|
815
|
+
var pending = /* @__PURE__ */ new Map();
|
|
816
|
+
function register(toolCallId, entry) {
|
|
817
|
+
pending.set(toolCallId, entry);
|
|
818
|
+
}
|
|
819
|
+
function unregister(toolCallId) {
|
|
820
|
+
pending.delete(toolCallId);
|
|
821
|
+
}
|
|
822
|
+
function resolveAnswer(toolCallId, answer, expectedSessionFile) {
|
|
823
|
+
const entry = pending.get(toolCallId);
|
|
824
|
+
if (!entry) return false;
|
|
825
|
+
if (entry.sessionFile !== expectedSessionFile) return false;
|
|
826
|
+
pending.delete(toolCallId);
|
|
827
|
+
entry.resolve(answer);
|
|
828
|
+
return true;
|
|
829
|
+
}
|
|
830
|
+
function cancelPendingForSession(sessionFile) {
|
|
831
|
+
for (const [id, entry] of pending) {
|
|
832
|
+
if (entry.sessionFile !== sessionFile) continue;
|
|
833
|
+
pending.delete(id);
|
|
834
|
+
entry.reject(new Error("Workspace disposed"));
|
|
621
835
|
}
|
|
622
|
-
|
|
836
|
+
}
|
|
837
|
+
function snapshotForSession(sessionFile) {
|
|
838
|
+
const out = [];
|
|
839
|
+
for (const [id, entry] of pending) {
|
|
840
|
+
if (entry.sessionFile !== sessionFile) continue;
|
|
841
|
+
out.push({ toolCallId: id, args: entry.args });
|
|
623
842
|
}
|
|
624
|
-
|
|
843
|
+
return out;
|
|
844
|
+
}
|
|
625
845
|
|
|
626
|
-
// src/
|
|
627
|
-
var
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
846
|
+
// src/extensions/ask_user/factory.ts
|
|
847
|
+
var askUserExtensionFactory = (pi) => {
|
|
848
|
+
pi.registerTool({
|
|
849
|
+
name: "ask_user",
|
|
850
|
+
label: "Ask user",
|
|
851
|
+
description: "Pause and ask the user a structured multiple-choice question. Use ONLY for genuine forks where the user's preference materially changes the work (UI layout, library choice, scope boundary). Returns the user's selection (or 'skip' if they dismissed).",
|
|
852
|
+
parameters: askUserParamsSchema,
|
|
853
|
+
promptSnippet: "ask_user: pause and ask the user a structured multiple-choice question when a decision is genuinely theirs.",
|
|
854
|
+
promptGuidelines: [
|
|
855
|
+
"Call ask_user only for genuine forks where the user's preference materially changes the work \u2014 UI layout, library choice, scope boundary. Don't ask for opinions you can defend yourself.",
|
|
856
|
+
"Each ask_user call asks ONE question. If you have several, call it multiple times in order so the user sees one at a time.",
|
|
857
|
+
"ask_user options must be distinct and mutually exclusive (unless multiSelect is true). 2\u20134 options; put the recommended option first and append ' (Recommended)' to its label.",
|
|
858
|
+
"Don't follow up ask_user with 'Should I proceed?' \u2014 the answer already authorizes the next step.",
|
|
859
|
+
"Only set ask_user's timeoutSec for time-sensitive questions where a stale answer would be worse than no answer; otherwise let the user take as long as they need."
|
|
860
|
+
],
|
|
861
|
+
execute: async (toolCallId, params, signal, _onUpdate, ctx) => {
|
|
862
|
+
const sessionFile = ctx.sessionManager.getSessionFile() ?? null;
|
|
863
|
+
const startedAt = Date.now();
|
|
864
|
+
const answer = await waitForAnswer({
|
|
865
|
+
toolCallId,
|
|
866
|
+
params,
|
|
867
|
+
sessionFile,
|
|
868
|
+
signal
|
|
869
|
+
});
|
|
870
|
+
return formatResult(params, answer, startedAt);
|
|
638
871
|
}
|
|
639
872
|
});
|
|
640
|
-
const sessionResult = await createAgentSessionFromServices({
|
|
641
|
-
services,
|
|
642
|
-
sessionManager,
|
|
643
|
-
sessionStartEvent
|
|
644
|
-
});
|
|
645
|
-
return {
|
|
646
|
-
...sessionResult,
|
|
647
|
-
services,
|
|
648
|
-
diagnostics: services.diagnostics
|
|
649
|
-
};
|
|
650
873
|
};
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
return state.runtime;
|
|
682
|
-
} finally {
|
|
683
|
-
this.pending.delete(workspaceId);
|
|
874
|
+
function waitForAnswer({
|
|
875
|
+
toolCallId,
|
|
876
|
+
params,
|
|
877
|
+
sessionFile,
|
|
878
|
+
signal
|
|
879
|
+
}) {
|
|
880
|
+
return new Promise((resolve6, reject) => {
|
|
881
|
+
let settled = false;
|
|
882
|
+
let timeoutHandle;
|
|
883
|
+
const cleanup = () => {
|
|
884
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
885
|
+
signal?.removeEventListener("abort", onAbort);
|
|
886
|
+
unregister(toolCallId);
|
|
887
|
+
};
|
|
888
|
+
const finishOk = (a) => {
|
|
889
|
+
if (settled) return;
|
|
890
|
+
settled = true;
|
|
891
|
+
cleanup();
|
|
892
|
+
resolve6(a);
|
|
893
|
+
};
|
|
894
|
+
const finishErr = (err2) => {
|
|
895
|
+
if (settled) return;
|
|
896
|
+
settled = true;
|
|
897
|
+
cleanup();
|
|
898
|
+
reject(err2);
|
|
899
|
+
};
|
|
900
|
+
const onAbort = () => finishErr(new Error("Aborted by user"));
|
|
901
|
+
if (signal?.aborted) {
|
|
902
|
+
finishErr(new Error("Aborted by user"));
|
|
903
|
+
return;
|
|
684
904
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
905
|
+
signal?.addEventListener("abort", onAbort);
|
|
906
|
+
if (typeof params.timeoutSec === "number" && params.timeoutSec > 0) {
|
|
907
|
+
const ms = params.timeoutSec * 1e3;
|
|
908
|
+
timeoutHandle = setTimeout(() => {
|
|
909
|
+
finishOk({ kind: "timeout", afterMs: ms });
|
|
910
|
+
}, ms);
|
|
911
|
+
}
|
|
912
|
+
register(toolCallId, {
|
|
913
|
+
args: params,
|
|
914
|
+
sessionFile,
|
|
915
|
+
resolve: (answer) => finishOk(answer),
|
|
916
|
+
reject: (err2) => finishErr(err2)
|
|
694
917
|
});
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
);
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
function formatResult(params, answer, startedAt) {
|
|
921
|
+
if (answer.kind === "option") {
|
|
922
|
+
const labels = answer.indices.map((i) => params.options[i]?.label).filter((l) => typeof l === "string");
|
|
923
|
+
const text2 = labels.length === 1 ? `User selected: "${labels[0]}"${descriptionSuffix(params, answer.indices[0])}.` : `User selected: ${labels.map((l) => `"${l}"`).join(", ")}.`;
|
|
924
|
+
const details2 = {
|
|
925
|
+
kind: "option",
|
|
926
|
+
indices: answer.indices,
|
|
927
|
+
labels
|
|
928
|
+
};
|
|
929
|
+
return {
|
|
930
|
+
content: [{ type: "text", text: text2 }],
|
|
931
|
+
details: details2
|
|
710
932
|
};
|
|
711
|
-
await runtime.session.bindExtensions({ uiContext: bridge, onError });
|
|
712
|
-
runtime.setRebindSession(async () => {
|
|
713
|
-
await runtime.session.bindExtensions({ uiContext: bridge, onError });
|
|
714
|
-
this.notifySessionReplaced(workspaceId);
|
|
715
|
-
});
|
|
716
|
-
return { runtime, bridge };
|
|
717
|
-
}
|
|
718
|
-
get(workspaceId) {
|
|
719
|
-
return this.states.get(workspaceId)?.runtime;
|
|
720
|
-
}
|
|
721
|
-
/**
|
|
722
|
-
* Register a WS connection as a subscriber for `workspaceId`. Safe to
|
|
723
|
-
* call before `getOrCreate`; the set is lazily created so the bridge,
|
|
724
|
-
* when later built, sees the same Set instance and any pre-existing
|
|
725
|
-
* subscribers.
|
|
726
|
-
*/
|
|
727
|
-
addSubscriber(workspaceId, ws) {
|
|
728
|
-
this.getOrCreateSubscriberSet(workspaceId).add(ws);
|
|
729
|
-
}
|
|
730
|
-
removeSubscriber(workspaceId, ws) {
|
|
731
|
-
const set = this.subscribers.get(workspaceId);
|
|
732
|
-
if (!set) return;
|
|
733
|
-
set.delete(ws);
|
|
734
|
-
if (set.size === 0) this.subscribers.delete(workspaceId);
|
|
735
|
-
}
|
|
736
|
-
/**
|
|
737
|
-
* Fan a server-initiated message out to every WS subscribed to the
|
|
738
|
-
* workspace. Used by API handlers that mutate runtime state and need
|
|
739
|
-
* to refresh derived snapshots (e.g. `context_usage` after `setModel`,
|
|
740
|
-
* which pi's event stream doesn't surface unless thinking-level also
|
|
741
|
-
* clamps).
|
|
742
|
-
*/
|
|
743
|
-
broadcast(workspaceId, msg) {
|
|
744
|
-
const set = this.subscribers.get(workspaceId);
|
|
745
|
-
if (!set || set.size === 0) return;
|
|
746
|
-
broadcastTo(set, msg);
|
|
747
933
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
listeners.add(listener);
|
|
755
|
-
return () => {
|
|
756
|
-
const current = this.rebindListeners.get(workspaceId);
|
|
757
|
-
if (!current) return;
|
|
758
|
-
current.delete(listener);
|
|
759
|
-
if (current.size === 0) {
|
|
760
|
-
this.rebindListeners.delete(workspaceId);
|
|
761
|
-
}
|
|
934
|
+
if (answer.kind === "other") {
|
|
935
|
+
const text2 = `User answered (freeform): "${answer.text}"`;
|
|
936
|
+
const details2 = { kind: "other", text: answer.text };
|
|
937
|
+
return {
|
|
938
|
+
content: [{ type: "text", text: text2 }],
|
|
939
|
+
details: details2
|
|
762
940
|
};
|
|
763
941
|
}
|
|
764
|
-
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
console.error(`[wm] rebind listener for ${workspaceId} failed:`, e);
|
|
772
|
-
}
|
|
773
|
-
}
|
|
942
|
+
if (answer.kind === "skip") {
|
|
943
|
+
const text2 = "User chose to skip this question. Use your best judgement and proceed; you may ask again later if it still matters.";
|
|
944
|
+
const details2 = { kind: "skip" };
|
|
945
|
+
return {
|
|
946
|
+
content: [{ type: "text", text: text2 }],
|
|
947
|
+
details: details2
|
|
948
|
+
};
|
|
774
949
|
}
|
|
775
|
-
|
|
950
|
+
const waited = Math.round((Date.now() - startedAt) / 1e3);
|
|
951
|
+
const text = `No answer within ${waited}s (agent-supplied timeout). Use your best judgement and proceed; you may ask again later if it still matters.`;
|
|
952
|
+
const details = {
|
|
953
|
+
kind: "timeout",
|
|
954
|
+
afterMs: answer.afterMs
|
|
955
|
+
};
|
|
956
|
+
return {
|
|
957
|
+
content: [{ type: "text", text }],
|
|
958
|
+
details
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
function descriptionSuffix(params, index) {
|
|
962
|
+
const desc = params.options[index]?.description;
|
|
963
|
+
return desc ? ` \u2014 ${desc}` : "";
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// src/extensions/artifact/schema.ts
|
|
967
|
+
import { Type as Type3 } from "typebox";
|
|
968
|
+
var TypeEnum = Type3.Union(
|
|
969
|
+
[
|
|
970
|
+
Type3.Literal("html"),
|
|
971
|
+
Type3.Literal("svg"),
|
|
972
|
+
Type3.Literal("markdown"),
|
|
973
|
+
Type3.Literal("code")
|
|
974
|
+
],
|
|
975
|
+
{
|
|
976
|
+
description: 'How to render the content: "html" (a self-contained HTML document or fragment, run in a sandboxed iframe), "svg" (SVG markup), "markdown" (rich text), or "code" (a source file shown with syntax highlighting).'
|
|
977
|
+
}
|
|
978
|
+
);
|
|
979
|
+
var createArtifactParamsSchema = Type3.Object({
|
|
980
|
+
id: Type3.Optional(
|
|
981
|
+
Type3.String({
|
|
982
|
+
description: 'Stable identifier. Omit on first creation. To REVISE an existing artifact, pass the same id you used before \u2014 that records a new version instead of a separate artifact. Use a short slug like "landing-page".'
|
|
983
|
+
})
|
|
984
|
+
),
|
|
985
|
+
type: TypeEnum,
|
|
986
|
+
title: Type3.String({
|
|
987
|
+
description: "Short human-readable title shown in the artifact panel."
|
|
988
|
+
}),
|
|
989
|
+
content: Type3.String({
|
|
990
|
+
description: "The full artifact content \u2014 the complete document, markup, or source."
|
|
991
|
+
}),
|
|
992
|
+
language: Type3.Optional(
|
|
993
|
+
Type3.String({
|
|
994
|
+
description: 'For type="code", the language id for syntax highlighting (e.g. "python", "typescript").'
|
|
995
|
+
})
|
|
996
|
+
)
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
// src/extensions/artifact/factory.ts
|
|
1000
|
+
var TOOL_NAME2 = "create_artifact";
|
|
1001
|
+
var artifactExtensionFactory = (pi) => {
|
|
1002
|
+
pi.registerTool({
|
|
1003
|
+
name: TOOL_NAME2,
|
|
1004
|
+
label: "Create artifact",
|
|
1005
|
+
description: 'Publish a substantial, self-contained piece of content as an "artifact" the user can view and iterate on in a dedicated side panel: a web page (html), an SVG diagram (svg), a document (markdown), or a source file (code). Reuse the same `id` to revise an existing artifact (records a new version).',
|
|
1006
|
+
promptSnippet: "create_artifact: render substantial, self-contained content (web page / SVG / document / code file) in a side panel the user can view and iterate on.",
|
|
1007
|
+
promptGuidelines: [
|
|
1008
|
+
"Use create_artifact for substantial, self-contained, reusable content the user will want to view, keep, or iterate on \u2014 a runnable HTML page, an SVG diagram, a full document, or a standalone code file. Do NOT use it for short snippets, command output, or your normal conversational answer; a fenced code block in your reply is better for those.",
|
|
1009
|
+
"To revise an artifact, call create_artifact again with the SAME id and the full updated content \u2014 this records a new version the user can step through. Don't spawn a near-duplicate artifact under a new id.",
|
|
1010
|
+
'Put the entire content in `content`, give it a concise `title`, and pick the `type` that matches how it should render. For type="code", set `language`.',
|
|
1011
|
+
"After creating an artifact, keep your chat reply short \u2014 the content lives in the panel, so don't paste it again in prose."
|
|
1012
|
+
],
|
|
1013
|
+
parameters: createArtifactParamsSchema,
|
|
1014
|
+
execute: async (toolCallId, params) => {
|
|
1015
|
+
const id = params.id?.trim() || toolCallId;
|
|
1016
|
+
const details = {
|
|
1017
|
+
id,
|
|
1018
|
+
type: params.type,
|
|
1019
|
+
title: params.title
|
|
1020
|
+
};
|
|
1021
|
+
const text = `Artifact "${params.title}" (${params.type}) is now shown to the user in the Artifacts panel. Its id is "${id}" \u2014 pass that same id to create_artifact to revise it.`;
|
|
1022
|
+
return {
|
|
1023
|
+
content: [{ type: "text", text }],
|
|
1024
|
+
details
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
// src/storage/builtin-extension-prefs.ts
|
|
1031
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
1032
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
1033
|
+
var PREFS_PATH = join4(config.dataDir, "builtin-extensions.json");
|
|
1034
|
+
var cache3 = { disabled: [] };
|
|
1035
|
+
async function loadBuiltinPrefs() {
|
|
1036
|
+
try {
|
|
1037
|
+
const raw = await readFile3(PREFS_PATH, "utf8");
|
|
1038
|
+
const parsed = JSON.parse(raw);
|
|
1039
|
+
cache3 = { disabled: Array.isArray(parsed.disabled) ? parsed.disabled : [] };
|
|
1040
|
+
} catch (err2) {
|
|
1041
|
+
cache3 = { disabled: [] };
|
|
1042
|
+
if (err2.code !== "ENOENT") {
|
|
1043
|
+
console.warn(`[builtin-prefs] ignoring unreadable ${PREFS_PATH}:`, err2);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
function isBuiltinDisabled(id) {
|
|
1048
|
+
if (cache3.disabled.includes(id)) return true;
|
|
1049
|
+
if (id === "todo" && cache3.disabled.includes("plan")) return true;
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
function getDisabledBuiltins() {
|
|
1053
|
+
return [...cache3.disabled];
|
|
1054
|
+
}
|
|
1055
|
+
async function setBuiltinEnabled(id, enabled) {
|
|
1056
|
+
const next = new Set(cache3.disabled);
|
|
1057
|
+
if (enabled) next.delete(id);
|
|
1058
|
+
else next.add(id);
|
|
1059
|
+
cache3 = { disabled: [...next] };
|
|
1060
|
+
await save2();
|
|
1061
|
+
}
|
|
1062
|
+
async function save2() {
|
|
1063
|
+
await mkdir3(dirname3(PREFS_PATH), { recursive: true });
|
|
1064
|
+
await writeFile3(PREFS_PATH, JSON.stringify(cache3, null, 2), "utf8");
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// src/extensions/index.ts
|
|
1068
|
+
var BUILTIN_EXTENSIONS = [
|
|
1069
|
+
{
|
|
1070
|
+
id: "todo",
|
|
1071
|
+
name: "Todo",
|
|
1072
|
+
description: "A CRUD task list for tracking multi-step work \u2014 adds the todo tool and the /todos command.",
|
|
1073
|
+
tools: ["todo"],
|
|
1074
|
+
commands: ["todos"],
|
|
1075
|
+
factory: todoExtensionFactory
|
|
1076
|
+
},
|
|
1077
|
+
{
|
|
1078
|
+
id: "ask_user",
|
|
1079
|
+
name: "Ask user",
|
|
1080
|
+
description: "Lets the agent pause and ask you a structured multiple-choice question \u2014 adds the ask_user tool.",
|
|
1081
|
+
tools: ["ask_user"],
|
|
1082
|
+
commands: [],
|
|
1083
|
+
factory: askUserExtensionFactory
|
|
1084
|
+
},
|
|
1085
|
+
{
|
|
1086
|
+
id: "artifact",
|
|
1087
|
+
name: "Artifacts",
|
|
1088
|
+
description: "Lets the agent publish substantial, self-contained content \u2014 web pages, SVG diagrams, documents, code files \u2014 as versioned artifacts rendered in a side panel. Adds the create_artifact tool.",
|
|
1089
|
+
tools: ["create_artifact"],
|
|
1090
|
+
commands: [],
|
|
1091
|
+
factory: artifactExtensionFactory
|
|
1092
|
+
}
|
|
1093
|
+
];
|
|
1094
|
+
function gate(def) {
|
|
1095
|
+
return (pi) => {
|
|
1096
|
+
if (isBuiltinDisabled(def.id)) return;
|
|
1097
|
+
return def.factory(pi);
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
var builtinExtensionFactories = BUILTIN_EXTENSIONS.map(gate);
|
|
1101
|
+
|
|
1102
|
+
// src/extensions/ask_user/cleanup.ts
|
|
1103
|
+
var CUSTOM_TYPE = "ask_user-restart-cancelled";
|
|
1104
|
+
function reconcileAfterRestart(sessionManager) {
|
|
1105
|
+
const branch = sessionManager.getBranch();
|
|
1106
|
+
if (branch.length === 0) return;
|
|
1107
|
+
const satisfied = /* @__PURE__ */ new Set();
|
|
1108
|
+
const danglingIds = [];
|
|
1109
|
+
const danglingAlreadyHandled = /* @__PURE__ */ new Set();
|
|
1110
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
1111
|
+
const entry = branch[i];
|
|
1112
|
+
if (entry.type === "custom_message") {
|
|
1113
|
+
const cm = entry;
|
|
1114
|
+
if (cm.customType === CUSTOM_TYPE) {
|
|
1115
|
+
const ids = cm.details?.ids;
|
|
1116
|
+
if (Array.isArray(ids)) {
|
|
1117
|
+
for (const id of ids) {
|
|
1118
|
+
if (typeof id === "string") danglingAlreadyHandled.add(id);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
if (entry.type !== "message") continue;
|
|
1125
|
+
const msg = entry.message;
|
|
1126
|
+
if (msg.role === "toolResult" && typeof msg.toolCallId === "string") {
|
|
1127
|
+
satisfied.add(msg.toolCallId);
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
1131
|
+
for (const block of msg.content) {
|
|
1132
|
+
if (!block || typeof block !== "object") continue;
|
|
1133
|
+
const b = block;
|
|
1134
|
+
if (b.type !== "toolCall") continue;
|
|
1135
|
+
if (b.name !== "ask_user") continue;
|
|
1136
|
+
if (typeof b.id !== "string") continue;
|
|
1137
|
+
if (satisfied.has(b.id)) continue;
|
|
1138
|
+
if (danglingAlreadyHandled.has(b.id)) continue;
|
|
1139
|
+
danglingIds.push(b.id);
|
|
1140
|
+
}
|
|
1141
|
+
break;
|
|
1142
|
+
}
|
|
1143
|
+
if (msg.role === "user") break;
|
|
1144
|
+
}
|
|
1145
|
+
if (danglingIds.length === 0) return;
|
|
1146
|
+
const idList = danglingIds.join(", ");
|
|
1147
|
+
const text = `[pi-pilot] Your previous ask_user call(s) [${idList}] were cancelled because the server restarted before the user answered. Use your best judgement and proceed; you may re-call ask_user if the decision still matters.`;
|
|
1148
|
+
sessionManager.appendCustomMessageEntry(
|
|
1149
|
+
CUSTOM_TYPE,
|
|
1150
|
+
text,
|
|
1151
|
+
true,
|
|
1152
|
+
// display in TUI too (no-op in pi-pilot, but harmless)
|
|
1153
|
+
{ ids: danglingIds }
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// src/ws/bridge.ts
|
|
1158
|
+
function translatePiEvent(ev) {
|
|
1159
|
+
switch (ev.type) {
|
|
1160
|
+
case "agent_start":
|
|
1161
|
+
return { kind: "agent_start" };
|
|
1162
|
+
case "agent_end":
|
|
1163
|
+
return { kind: "agent_end", willRetry: ev.willRetry };
|
|
1164
|
+
case "turn_start":
|
|
1165
|
+
return { kind: "turn_start" };
|
|
1166
|
+
case "turn_end":
|
|
1167
|
+
return { kind: "turn_end" };
|
|
1168
|
+
case "message_start": {
|
|
1169
|
+
const role = roleOf(ev.message);
|
|
1170
|
+
const text = role === "user" ? extractUserText(ev.message) : void 0;
|
|
1171
|
+
return { kind: "message_start", role, text };
|
|
1172
|
+
}
|
|
1173
|
+
case "message_end":
|
|
1174
|
+
return { kind: "message_end", role: roleOf(ev.message) };
|
|
1175
|
+
case "message_update": {
|
|
1176
|
+
const ame = ev.assistantMessageEvent;
|
|
1177
|
+
if (ame.type === "text_delta") {
|
|
1178
|
+
return {
|
|
1179
|
+
kind: "message_update",
|
|
1180
|
+
delta: { kind: "text", contentIndex: ame.contentIndex, text: ame.delta }
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
if (ame.type === "thinking_delta") {
|
|
1184
|
+
return {
|
|
1185
|
+
kind: "message_update",
|
|
1186
|
+
delta: { kind: "thinking", contentIndex: ame.contentIndex, text: ame.delta }
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
return { kind: "message_update", delta: { kind: "other" } };
|
|
1190
|
+
}
|
|
1191
|
+
case "tool_execution_start":
|
|
1192
|
+
return {
|
|
1193
|
+
kind: "tool_execution_start",
|
|
1194
|
+
toolCallId: ev.toolCallId,
|
|
1195
|
+
toolName: ev.toolName,
|
|
1196
|
+
args: ev.args
|
|
1197
|
+
};
|
|
1198
|
+
case "tool_execution_update":
|
|
1199
|
+
return {
|
|
1200
|
+
kind: "tool_execution_update",
|
|
1201
|
+
toolCallId: ev.toolCallId,
|
|
1202
|
+
toolName: ev.toolName,
|
|
1203
|
+
partialText: extractText(ev.partialResult)
|
|
1204
|
+
};
|
|
1205
|
+
case "tool_execution_end": {
|
|
1206
|
+
const details = shouldForwardDetails(ev.toolName) ? ev.result?.details : void 0;
|
|
1207
|
+
return {
|
|
1208
|
+
kind: "tool_execution_end",
|
|
1209
|
+
toolCallId: ev.toolCallId,
|
|
1210
|
+
toolName: ev.toolName,
|
|
1211
|
+
isError: ev.isError,
|
|
1212
|
+
text: extractText(ev.result),
|
|
1213
|
+
...details !== void 0 ? { details } : {}
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
case "queue_update":
|
|
1217
|
+
return {
|
|
1218
|
+
kind: "queue_update",
|
|
1219
|
+
steering: [...ev.steering],
|
|
1220
|
+
followUp: [...ev.followUp]
|
|
1221
|
+
};
|
|
1222
|
+
case "auto_retry_start":
|
|
1223
|
+
return {
|
|
1224
|
+
kind: "auto_retry_start",
|
|
1225
|
+
attempt: ev.attempt,
|
|
1226
|
+
maxAttempts: ev.maxAttempts,
|
|
1227
|
+
delayMs: ev.delayMs,
|
|
1228
|
+
errorMessage: ev.errorMessage
|
|
1229
|
+
};
|
|
1230
|
+
case "auto_retry_end":
|
|
1231
|
+
return {
|
|
1232
|
+
kind: "auto_retry_end",
|
|
1233
|
+
success: ev.success,
|
|
1234
|
+
attempt: ev.attempt,
|
|
1235
|
+
finalError: ev.finalError
|
|
1236
|
+
};
|
|
1237
|
+
case "compaction_start":
|
|
1238
|
+
return { kind: "compaction_start", reason: ev.reason };
|
|
1239
|
+
case "compaction_end":
|
|
1240
|
+
return {
|
|
1241
|
+
kind: "compaction_end",
|
|
1242
|
+
reason: ev.reason,
|
|
1243
|
+
aborted: ev.aborted,
|
|
1244
|
+
willRetry: ev.willRetry,
|
|
1245
|
+
errorMessage: ev.errorMessage
|
|
1246
|
+
};
|
|
1247
|
+
case "session_info_changed":
|
|
1248
|
+
return { kind: "session_info_changed", name: ev.name };
|
|
1249
|
+
case "thinking_level_changed":
|
|
1250
|
+
return { kind: "thinking_level_changed", level: ev.level };
|
|
1251
|
+
default:
|
|
1252
|
+
return void 0;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
var warnedUnknownRoles = /* @__PURE__ */ new Set();
|
|
1256
|
+
function roleOf(message) {
|
|
1257
|
+
const role = message?.role;
|
|
1258
|
+
if (role === "user" || role === "assistant" || role === "toolResult" || role === "bashExecution") {
|
|
1259
|
+
return role;
|
|
1260
|
+
}
|
|
1261
|
+
const key = typeof role === "string" ? role : `<${role === void 0 ? "missing" : typeof role}>`;
|
|
1262
|
+
if (!warnedUnknownRoles.has(key)) {
|
|
1263
|
+
warnedUnknownRoles.add(key);
|
|
1264
|
+
console.warn(
|
|
1265
|
+
`[bridge] unknown message role "${key}" \u2014 treating as assistant. Pi SDK may have added a role bridge.ts and chat.ts don't handle yet.`
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
return "assistant";
|
|
1269
|
+
}
|
|
1270
|
+
function extractUserText(message) {
|
|
1271
|
+
if (!message || typeof message !== "object") return void 0;
|
|
1272
|
+
const content = message.content;
|
|
1273
|
+
if (typeof content === "string") return content;
|
|
1274
|
+
if (!Array.isArray(content)) return void 0;
|
|
1275
|
+
const parts = [];
|
|
1276
|
+
for (const block of content) {
|
|
1277
|
+
if (block && typeof block === "object" && block.type === "text") {
|
|
1278
|
+
const text = block.text;
|
|
1279
|
+
if (typeof text === "string") parts.push(text);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
return parts.length === 0 ? void 0 : parts.join("");
|
|
1283
|
+
}
|
|
1284
|
+
function inFlightAssistantSnapshot(streamingMessage) {
|
|
1285
|
+
if (!streamingMessage || typeof streamingMessage !== "object") return void 0;
|
|
1286
|
+
const m = streamingMessage;
|
|
1287
|
+
if (m.role !== "assistant" || !Array.isArray(m.content)) return void 0;
|
|
1288
|
+
let textAccum = "";
|
|
1289
|
+
let thinkingAccum = "";
|
|
1290
|
+
for (const block of m.content) {
|
|
1291
|
+
if (!block || typeof block !== "object") continue;
|
|
1292
|
+
const b = block;
|
|
1293
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
1294
|
+
textAccum += b.text;
|
|
1295
|
+
} else if (b.type === "thinking" && typeof b.thinking === "string") {
|
|
1296
|
+
thinkingAccum += b.thinking;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
if (!textAccum && !thinkingAccum) return void 0;
|
|
1300
|
+
const events = [
|
|
1301
|
+
{ kind: "message_start", role: "assistant" }
|
|
1302
|
+
];
|
|
1303
|
+
if (thinkingAccum) {
|
|
1304
|
+
events.push({
|
|
1305
|
+
kind: "message_update",
|
|
1306
|
+
delta: { kind: "thinking", contentIndex: 0, text: thinkingAccum }
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
if (textAccum) {
|
|
1310
|
+
events.push({
|
|
1311
|
+
kind: "message_update",
|
|
1312
|
+
delta: { kind: "text", contentIndex: 0, text: textAccum }
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
return events;
|
|
1316
|
+
}
|
|
1317
|
+
function inFlightRunningToolsSnapshot(pendingToolCalls, messages) {
|
|
1318
|
+
const pending2 = new Set(pendingToolCalls);
|
|
1319
|
+
if (pending2.size === 0) return [];
|
|
1320
|
+
const infoById = /* @__PURE__ */ new Map();
|
|
1321
|
+
for (const message of messages) {
|
|
1322
|
+
if (!message || typeof message !== "object") continue;
|
|
1323
|
+
if (message.role !== "assistant") continue;
|
|
1324
|
+
const content = message.content;
|
|
1325
|
+
if (!Array.isArray(content)) continue;
|
|
1326
|
+
for (const block of content) {
|
|
1327
|
+
if (!block || typeof block !== "object") continue;
|
|
1328
|
+
const b = block;
|
|
1329
|
+
if (b.type === "toolCall" && typeof b.id === "string" && pending2.has(b.id)) {
|
|
1330
|
+
infoById.set(b.id, { name: typeof b.name === "string" ? b.name : "tool", args: b.arguments });
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
const events = [];
|
|
1335
|
+
for (const toolCallId of pending2) {
|
|
1336
|
+
const info = infoById.get(toolCallId);
|
|
1337
|
+
if (!info) continue;
|
|
1338
|
+
if (info.name === "ask_user") continue;
|
|
1339
|
+
events.push({
|
|
1340
|
+
kind: "tool_execution_start",
|
|
1341
|
+
toolCallId,
|
|
1342
|
+
toolName: info.name,
|
|
1343
|
+
args: info.args
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
return events;
|
|
1347
|
+
}
|
|
1348
|
+
function inFlightToolCallsSnapshot(sessionFile) {
|
|
1349
|
+
const pending2 = snapshotForSession(sessionFile);
|
|
1350
|
+
return pending2.map((p) => ({
|
|
1351
|
+
kind: "tool_execution_start",
|
|
1352
|
+
toolCallId: p.toolCallId,
|
|
1353
|
+
toolName: "ask_user",
|
|
1354
|
+
args: p.args
|
|
1355
|
+
}));
|
|
1356
|
+
}
|
|
1357
|
+
var DETAILS_FORWARD_WHITELIST = /* @__PURE__ */ new Set(["ask_user", "todo"]);
|
|
1358
|
+
function shouldForwardDetails(toolName) {
|
|
1359
|
+
return DETAILS_FORWARD_WHITELIST.has(toolName);
|
|
1360
|
+
}
|
|
1361
|
+
function extractText(result) {
|
|
1362
|
+
if (!result || typeof result !== "object") return void 0;
|
|
1363
|
+
const content = result.content;
|
|
1364
|
+
if (!Array.isArray(content)) return void 0;
|
|
1365
|
+
const parts = [];
|
|
1366
|
+
for (const c of content) {
|
|
1367
|
+
if (c && typeof c === "object" && c.type === "text") {
|
|
1368
|
+
const text = c.text;
|
|
1369
|
+
if (typeof text === "string") parts.push(text);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
return parts.length === 0 ? void 0 : parts.join("");
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// src/ws/extension-ui.ts
|
|
1376
|
+
var ExtensionUIBridge = class {
|
|
1377
|
+
/** Symmetric with the old bridge so workspace-manager's dispose path
|
|
1378
|
+
* can call it uniformly. There is no state to release. */
|
|
1379
|
+
dispose() {
|
|
1380
|
+
}
|
|
1381
|
+
// ============== dialog methods (resolve to default) ==============
|
|
1382
|
+
select() {
|
|
1383
|
+
return Promise.resolve(void 0);
|
|
1384
|
+
}
|
|
1385
|
+
confirm() {
|
|
1386
|
+
return Promise.resolve(false);
|
|
1387
|
+
}
|
|
1388
|
+
input() {
|
|
1389
|
+
return Promise.resolve(void 0);
|
|
1390
|
+
}
|
|
1391
|
+
editor() {
|
|
1392
|
+
return Promise.resolve(void 0);
|
|
1393
|
+
}
|
|
1394
|
+
// ============== fire-and-forget methods (no-op) ==============
|
|
1395
|
+
notify() {
|
|
1396
|
+
}
|
|
1397
|
+
setStatus() {
|
|
1398
|
+
}
|
|
1399
|
+
setWidget() {
|
|
1400
|
+
}
|
|
1401
|
+
setTitle() {
|
|
1402
|
+
}
|
|
1403
|
+
setEditorText() {
|
|
1404
|
+
}
|
|
1405
|
+
pasteToEditor() {
|
|
1406
|
+
}
|
|
1407
|
+
// ============== TUI-only methods (no-op / defaults) ==============
|
|
1408
|
+
onTerminalInput() {
|
|
1409
|
+
return () => {
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
setWorkingMessage() {
|
|
1413
|
+
}
|
|
1414
|
+
setWorkingVisible() {
|
|
1415
|
+
}
|
|
1416
|
+
setWorkingIndicator() {
|
|
1417
|
+
}
|
|
1418
|
+
setHiddenThinkingLabel() {
|
|
1419
|
+
}
|
|
1420
|
+
setFooter() {
|
|
1421
|
+
}
|
|
1422
|
+
setHeader() {
|
|
1423
|
+
}
|
|
1424
|
+
async custom() {
|
|
1425
|
+
return void 0;
|
|
1426
|
+
}
|
|
1427
|
+
getEditorText() {
|
|
1428
|
+
return "";
|
|
1429
|
+
}
|
|
1430
|
+
addAutocompleteProvider() {
|
|
1431
|
+
}
|
|
1432
|
+
setEditorComponent() {
|
|
1433
|
+
}
|
|
1434
|
+
getEditorComponent() {
|
|
1435
|
+
return void 0;
|
|
1436
|
+
}
|
|
1437
|
+
get theme() {
|
|
1438
|
+
return void 0;
|
|
1439
|
+
}
|
|
1440
|
+
getAllThemes() {
|
|
1441
|
+
return [];
|
|
1442
|
+
}
|
|
1443
|
+
getTheme() {
|
|
1444
|
+
return void 0;
|
|
1445
|
+
}
|
|
1446
|
+
setTheme() {
|
|
1447
|
+
return { success: false, error: "Theme switching not supported in pi-pilot" };
|
|
1448
|
+
}
|
|
1449
|
+
getToolsExpanded() {
|
|
1450
|
+
return false;
|
|
1451
|
+
}
|
|
1452
|
+
setToolsExpanded() {
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
// src/workspace-manager.ts
|
|
1457
|
+
var EXTENSIONS_ENABLED = process.env.PI_PILOT_ENABLE_EXTENSIONS === "1";
|
|
1458
|
+
var MAX_LIVE_RUNTIMES = 12;
|
|
1459
|
+
var createRuntime = async ({
|
|
1460
|
+
cwd,
|
|
1461
|
+
sessionManager,
|
|
1462
|
+
sessionStartEvent
|
|
1463
|
+
}) => {
|
|
1464
|
+
const services = await createAgentSessionServices({
|
|
1465
|
+
cwd,
|
|
1466
|
+
resourceLoaderOptions: {
|
|
1467
|
+
noExtensions: !EXTENSIONS_ENABLED,
|
|
1468
|
+
extensionFactories: builtinExtensionFactories
|
|
1469
|
+
}
|
|
1470
|
+
});
|
|
1471
|
+
const sessionResult = await createAgentSessionFromServices({
|
|
1472
|
+
services,
|
|
1473
|
+
sessionManager,
|
|
1474
|
+
sessionStartEvent
|
|
1475
|
+
});
|
|
1476
|
+
return {
|
|
1477
|
+
...sessionResult,
|
|
1478
|
+
services,
|
|
1479
|
+
diagnostics: services.diagnostics
|
|
1480
|
+
};
|
|
1481
|
+
};
|
|
1482
|
+
var KEY_SEP = "\0";
|
|
1483
|
+
var SessionRuntimeManager = class {
|
|
1484
|
+
/** All live runtimes, keyed by `runtimeKey`. */
|
|
1485
|
+
runtimes = /* @__PURE__ */ new Map();
|
|
1486
|
+
/** Per-build lock keyed by `runtimeKey` to serialize concurrent creations. */
|
|
1487
|
+
pending = /* @__PURE__ */ new Map();
|
|
1488
|
+
/** The runtime the hub last made primary, per workspace. Drives `get`. */
|
|
1489
|
+
activeByWorkspace = /* @__PURE__ */ new Map();
|
|
1490
|
+
/**
|
|
1491
|
+
* WS subscribers, keyed by workspaceId (not runtimeKey): a connection
|
|
1492
|
+
* viewing any session of a workspace receives that workspace's
|
|
1493
|
+
* server-initiated broadcasts (extension errors, context_usage). Owned by
|
|
1494
|
+
* the manager so it can pre-exist any runtime build (extensions may fire
|
|
1495
|
+
* `onError` from `session_start` before any client subscribed).
|
|
1496
|
+
*/
|
|
1497
|
+
subscribers = /* @__PURE__ */ new Map();
|
|
1498
|
+
touchSeq = 0;
|
|
1499
|
+
/** `runtimeKey` for a (workspace, session identity). */
|
|
1500
|
+
keyOf(workspaceId, sessionIdentity) {
|
|
1501
|
+
return `${workspaceId}${KEY_SEP}${sessionIdentity}`;
|
|
1502
|
+
}
|
|
1503
|
+
/** `runtimeKey` for a built runtime, from its session file (or sessionId). */
|
|
1504
|
+
keyForRuntime(workspaceId, runtime) {
|
|
1505
|
+
const file = runtime.session.sessionFile;
|
|
1506
|
+
return this.keyOf(workspaceId, file ? resolve2(file) : runtime.session.sessionId);
|
|
1507
|
+
}
|
|
1508
|
+
/** Public so the WS hub derives the exact same key for a returned runtime. */
|
|
1509
|
+
runtimeKeyFor(workspaceId, runtime) {
|
|
1510
|
+
return this.keyForRuntime(workspaceId, runtime);
|
|
1511
|
+
}
|
|
1512
|
+
getOrCreateSubscriberSet(workspaceId) {
|
|
1513
|
+
let set = this.subscribers.get(workspaceId);
|
|
1514
|
+
if (!set) {
|
|
1515
|
+
set = /* @__PURE__ */ new Set();
|
|
1516
|
+
this.subscribers.set(workspaceId, set);
|
|
1517
|
+
}
|
|
1518
|
+
return set;
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Build (but do not register) a runtime for `workspaceId` from a
|
|
1522
|
+
* SessionManager factory. Binds the UI bridge + onError and runs ask_user
|
|
1523
|
+
* post-restart cleanup. The bridge broadcasts to the workspace's subscriber
|
|
1524
|
+
* set, resolved lazily so it works even if the set is created later.
|
|
1525
|
+
*/
|
|
1526
|
+
async buildState(workspaceId, cwd, makeSessionManager) {
|
|
1527
|
+
const runtime = await createAgentSessionRuntime(createRuntime, {
|
|
1528
|
+
cwd,
|
|
1529
|
+
agentDir: getAgentDir(),
|
|
1530
|
+
sessionManager: makeSessionManager()
|
|
1531
|
+
});
|
|
1532
|
+
const bridge = new ExtensionUIBridge();
|
|
1533
|
+
await this.bindExtensions(workspaceId, runtime, bridge);
|
|
1534
|
+
safeReconcileAskUser(workspaceId, runtime.session.sessionManager);
|
|
1535
|
+
return {
|
|
1536
|
+
runtime,
|
|
1537
|
+
bridge,
|
|
1538
|
+
workspaceId,
|
|
1539
|
+
sessionPath: runtime.session.sessionFile ? resolve2(runtime.session.sessionFile) : null,
|
|
1540
|
+
touchedAt: ++this.touchSeq
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
/** Bind (or re-bind, after a fork) the UI context + onError on a session. */
|
|
1544
|
+
async bindExtensions(workspaceId, runtime, bridge) {
|
|
1545
|
+
const onError = (err2) => {
|
|
1546
|
+
const msg = {
|
|
1547
|
+
type: "extension_error",
|
|
1548
|
+
workspaceId,
|
|
1549
|
+
extensionPath: err2.extensionPath,
|
|
1550
|
+
event: err2.event,
|
|
1551
|
+
message: err2.error
|
|
1552
|
+
};
|
|
1553
|
+
const set = this.subscribers.get(workspaceId);
|
|
1554
|
+
if (set) broadcastTo(set, msg);
|
|
1555
|
+
console.error(
|
|
1556
|
+
`[ext-error] ${workspaceId} ${err2.extensionPath}@${err2.event}: ${err2.error}` + (err2.stack ? `
|
|
1557
|
+
${err2.stack}` : "")
|
|
1558
|
+
);
|
|
1559
|
+
};
|
|
1560
|
+
await runtime.session.bindExtensions({ uiContext: bridge, onError });
|
|
1561
|
+
}
|
|
1562
|
+
/** Register a freshly-built state under its session key, deduping against a
|
|
1563
|
+
* concurrent build of the same session. Returns the winning state. */
|
|
1564
|
+
async adopt(state) {
|
|
1565
|
+
const key = this.keyForRuntime(state.workspaceId, state.runtime);
|
|
1566
|
+
const existing = this.runtimes.get(key);
|
|
1567
|
+
if (existing) {
|
|
1568
|
+
await this.disposeState(state);
|
|
1569
|
+
return existing;
|
|
1570
|
+
}
|
|
1571
|
+
this.runtimes.set(key, state);
|
|
1572
|
+
this.evictIfOverCap(key);
|
|
1573
|
+
return state;
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Ensure a runtime exists for the target session and return it.
|
|
1577
|
+
*
|
|
1578
|
+
* - With `sessionPath`: opens that specific session (deduped by key).
|
|
1579
|
+
* - Without: returns the workspace's active/any live runtime, or builds the
|
|
1580
|
+
* "continue recent" default if none exists yet.
|
|
1581
|
+
*
|
|
1582
|
+
* Does NOT change the active pointer — the hub owns that via `setActive`.
|
|
1583
|
+
*/
|
|
1584
|
+
async getOrCreate(workspaceId, sessionPath) {
|
|
1585
|
+
const ws = await getWorkspace(workspaceId);
|
|
1586
|
+
if (!ws) throw new Error(`Workspace not found: ${workspaceId}`);
|
|
1587
|
+
if (sessionPath) {
|
|
1588
|
+
if (!isAbsolute2(sessionPath)) throw new Error("Session path must be absolute");
|
|
1589
|
+
const resolved = resolve2(sessionPath);
|
|
1590
|
+
const key = this.keyOf(workspaceId, resolved);
|
|
1591
|
+
const existing2 = this.runtimes.get(key);
|
|
1592
|
+
if (existing2) {
|
|
1593
|
+
existing2.touchedAt = ++this.touchSeq;
|
|
1594
|
+
return existing2.runtime;
|
|
1595
|
+
}
|
|
1596
|
+
const inflight4 = this.pending.get(key);
|
|
1597
|
+
if (inflight4) return (await inflight4).runtime;
|
|
1598
|
+
const p2 = this.buildState(
|
|
1599
|
+
workspaceId,
|
|
1600
|
+
ws.path,
|
|
1601
|
+
() => SessionManager.open(resolved, void 0, ws.path)
|
|
1602
|
+
).then((s) => this.adopt(s));
|
|
1603
|
+
this.pending.set(key, p2);
|
|
1604
|
+
try {
|
|
1605
|
+
return (await p2).runtime;
|
|
1606
|
+
} finally {
|
|
1607
|
+
this.pending.delete(key);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
const existing = this.get(workspaceId);
|
|
1611
|
+
if (existing) return existing;
|
|
1612
|
+
const defaultKey = this.keyOf(workspaceId, "<default>");
|
|
1613
|
+
const inflight3 = this.pending.get(defaultKey);
|
|
1614
|
+
if (inflight3) return (await inflight3).runtime;
|
|
1615
|
+
const p = this.buildState(
|
|
1616
|
+
workspaceId,
|
|
1617
|
+
ws.path,
|
|
1618
|
+
() => SessionManager.continueRecent(ws.path)
|
|
1619
|
+
).then((s) => this.adopt(s));
|
|
1620
|
+
this.pending.set(defaultKey, p);
|
|
1621
|
+
try {
|
|
1622
|
+
return (await p).runtime;
|
|
1623
|
+
} finally {
|
|
1624
|
+
this.pending.delete(defaultKey);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
/** Create a brand-new empty session + runtime for the workspace (does not
|
|
1628
|
+
* touch any existing runtime, so a streaming session keeps running). */
|
|
1629
|
+
async createSession(workspaceId) {
|
|
1630
|
+
const ws = await getWorkspace(workspaceId);
|
|
1631
|
+
if (!ws) throw new Error(`Workspace not found: ${workspaceId}`);
|
|
1632
|
+
const state = await this.buildState(
|
|
1633
|
+
workspaceId,
|
|
1634
|
+
ws.path,
|
|
1635
|
+
() => SessionManager.create(ws.path)
|
|
1636
|
+
);
|
|
1637
|
+
return (await this.adopt(state)).runtime;
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Fork an existing session at `entryId` into a new branched session and
|
|
1641
|
+
* return a runtime bound to the branch. The source session's own runtime
|
|
1642
|
+
* (if any) is untouched. Returns `{ cancelled: true }` if pi cancelled the
|
|
1643
|
+
* fork (e.g. a `session_before_switch` veto).
|
|
1644
|
+
*/
|
|
1645
|
+
async fork(workspaceId, sourceSessionPath, entryId) {
|
|
1646
|
+
const ws = await getWorkspace(workspaceId);
|
|
1647
|
+
if (!ws) throw new Error(`Workspace not found: ${workspaceId}`);
|
|
1648
|
+
if (!isAbsolute2(sourceSessionPath)) throw new Error("Session path must be absolute");
|
|
1649
|
+
const state = await this.buildState(
|
|
1650
|
+
workspaceId,
|
|
1651
|
+
ws.path,
|
|
1652
|
+
() => SessionManager.open(resolve2(sourceSessionPath), void 0, ws.path)
|
|
1653
|
+
);
|
|
1654
|
+
let result;
|
|
1655
|
+
try {
|
|
1656
|
+
result = await state.runtime.fork(entryId);
|
|
1657
|
+
} catch (err2) {
|
|
1658
|
+
await this.disposeState(state);
|
|
1659
|
+
throw err2;
|
|
1660
|
+
}
|
|
1661
|
+
if (result.cancelled) {
|
|
1662
|
+
await this.disposeState(state);
|
|
1663
|
+
return { cancelled: true };
|
|
1664
|
+
}
|
|
1665
|
+
await this.bindExtensions(workspaceId, state.runtime, state.bridge);
|
|
1666
|
+
safeReconcileAskUser(workspaceId, state.runtime.session.sessionManager);
|
|
1667
|
+
state.sessionPath = state.runtime.session.sessionFile ? resolve2(state.runtime.session.sessionFile) : null;
|
|
1668
|
+
const winner = await this.adopt(state);
|
|
1669
|
+
return { cancelled: false, runtime: winner.runtime };
|
|
1670
|
+
}
|
|
1671
|
+
/** The active session's runtime for this workspace (hub-designated), or any
|
|
1672
|
+
* live runtime for it, or undefined. Used by per-workspace REST routes. */
|
|
1673
|
+
get(workspaceId) {
|
|
1674
|
+
const activeKey = this.activeByWorkspace.get(workspaceId);
|
|
1675
|
+
if (activeKey) {
|
|
1676
|
+
const active = this.runtimes.get(activeKey);
|
|
1677
|
+
if (active) return active.runtime;
|
|
1678
|
+
}
|
|
1679
|
+
for (const state of this.runtimes.values()) {
|
|
1680
|
+
if (state.workspaceId === workspaceId) return state.runtime;
|
|
1681
|
+
}
|
|
1682
|
+
return void 0;
|
|
1683
|
+
}
|
|
1684
|
+
/** The runtime bound to a specific (workspace, session), if live. */
|
|
1685
|
+
getForSession(workspaceId, sessionPath) {
|
|
1686
|
+
return this.runtimes.get(this.keyOf(workspaceId, resolve2(sessionPath)))?.runtime;
|
|
1687
|
+
}
|
|
1688
|
+
/** Mark `runtime` as the active session for its workspace (hub on primary
|
|
1689
|
+
* bind), so per-workspace routes resolve to it. */
|
|
1690
|
+
setActive(workspaceId, runtime) {
|
|
1691
|
+
const key = this.keyForRuntime(workspaceId, runtime);
|
|
1692
|
+
this.activeByWorkspace.set(workspaceId, key);
|
|
1693
|
+
const state = this.runtimes.get(key);
|
|
1694
|
+
if (state) state.touchedAt = ++this.touchSeq;
|
|
1695
|
+
}
|
|
1696
|
+
/**
|
|
1697
|
+
* Register a WS connection as a subscriber for `workspaceId` (server-
|
|
1698
|
+
* initiated broadcasts: extension errors, context_usage). Safe to call
|
|
1699
|
+
* before any runtime build.
|
|
1700
|
+
*/
|
|
1701
|
+
addSubscriber(workspaceId, ws) {
|
|
1702
|
+
this.getOrCreateSubscriberSet(workspaceId).add(ws);
|
|
1703
|
+
}
|
|
1704
|
+
removeSubscriber(workspaceId, ws) {
|
|
1705
|
+
const set = this.subscribers.get(workspaceId);
|
|
1706
|
+
if (!set) return;
|
|
1707
|
+
set.delete(ws);
|
|
1708
|
+
if (set.size === 0) this.subscribers.delete(workspaceId);
|
|
1709
|
+
}
|
|
1710
|
+
/** Fan a server-initiated message out to every WS subscribed to the
|
|
1711
|
+
* workspace (e.g. context_usage after setModel). */
|
|
1712
|
+
broadcast(workspaceId, msg) {
|
|
1713
|
+
const set = this.subscribers.get(workspaceId);
|
|
1714
|
+
if (!set || set.size === 0) return;
|
|
1715
|
+
broadcastTo(set, msg);
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Check that `sessionPath` belongs to the given workspace's session list.
|
|
1719
|
+
* Returns an error string if validation fails, or `null` if the path is
|
|
1720
|
+
* owned by this workspace.
|
|
1721
|
+
*/
|
|
1722
|
+
async validateSessionOwnership(workspaceId, sessionPath) {
|
|
1723
|
+
if (!isAbsolute2(sessionPath)) return "session path must be absolute";
|
|
1724
|
+
const ws = await getWorkspace(workspaceId);
|
|
1725
|
+
if (!ws) return `workspace not found: ${workspaceId}`;
|
|
1726
|
+
const sessions = await SessionManager.list(ws.path);
|
|
1727
|
+
const resolved = resolve2(sessionPath);
|
|
1728
|
+
const found = sessions.some((s) => resolve2(s.path) === resolved);
|
|
1729
|
+
if (!found) return `session not found in workspace: ${sessionPath}`;
|
|
1730
|
+
return null;
|
|
1731
|
+
}
|
|
1732
|
+
async listSessions(workspaceId) {
|
|
776
1733
|
const ws = await getWorkspace(workspaceId);
|
|
777
1734
|
if (!ws) throw new Error(`Workspace not found: ${workspaceId}`);
|
|
1735
|
+
const streaming = /* @__PURE__ */ new Set();
|
|
1736
|
+
for (const state of this.runtimes.values()) {
|
|
1737
|
+
if (state.workspaceId !== workspaceId) continue;
|
|
1738
|
+
if (state.sessionPath && state.runtime.session.isStreaming) {
|
|
1739
|
+
streaming.add(state.sessionPath);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
778
1742
|
const sessions = await SessionManager.list(ws.path);
|
|
779
|
-
return sessions.slice().sort((a, b) => b.modified.getTime() - a.modified.getTime()).map(toSessionSummary);
|
|
1743
|
+
return sessions.slice().sort((a, b) => b.modified.getTime() - a.modified.getTime()).map((info) => toSessionSummary(info, streaming.has(resolve2(info.path))));
|
|
780
1744
|
}
|
|
781
1745
|
getSessionHistory(workspaceId, sessionPath) {
|
|
782
|
-
const runtime = this.
|
|
1746
|
+
const runtime = sessionPath ? this.getForSession(workspaceId, sessionPath) : this.get(workspaceId);
|
|
783
1747
|
if (!runtime) return { items: [], isStreaming: false };
|
|
784
|
-
if (sessionPath) {
|
|
785
|
-
const activeFile = runtime.session.sessionFile ? resolve2(runtime.session.sessionFile) : void 0;
|
|
786
|
-
if (activeFile !== resolve2(sessionPath)) {
|
|
787
|
-
return { items: [], isStreaming: false };
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
1748
|
const isStreaming = runtime.session.isStreaming ?? false;
|
|
791
1749
|
const branch = runtime.session.sessionManager.getBranch();
|
|
792
1750
|
const items = [];
|
|
@@ -796,8 +1754,8 @@ ${err.stack}` : "")
|
|
|
796
1754
|
const msg = entry.message;
|
|
797
1755
|
const role = msg.role;
|
|
798
1756
|
if (role === "user") {
|
|
799
|
-
const text =
|
|
800
|
-
if (text) items.push({ kind: "user", text });
|
|
1757
|
+
const text = extractUserText2(msg);
|
|
1758
|
+
if (text) items.push({ kind: "user", text, entryId: entry.id });
|
|
801
1759
|
} else if (role === "assistant") {
|
|
802
1760
|
const { text, thinking, toolCalls } = extractAssistantContent(
|
|
803
1761
|
msg
|
|
@@ -814,7 +1772,11 @@ ${err.stack}` : "")
|
|
|
814
1772
|
toolName: tr.toolName,
|
|
815
1773
|
args: argsByCallId.get(tr.toolCallId) ?? "",
|
|
816
1774
|
text: extractContentText(tr.content),
|
|
817
|
-
isError: tr.isError
|
|
1775
|
+
isError: tr.isError,
|
|
1776
|
+
// Mirror live wire whitelist (bridge.ts): only ship details
|
|
1777
|
+
// for tools whose cards need the structured shape, so the
|
|
1778
|
+
// history payload stays small for bash / edit / read.
|
|
1779
|
+
...shouldForwardDetails(tr.toolName) && tr.details !== void 0 ? { details: tr.details } : {}
|
|
818
1780
|
});
|
|
819
1781
|
} else if (role === "bashExecution") {
|
|
820
1782
|
const be = msg;
|
|
@@ -831,15 +1793,13 @@ ${err.stack}` : "")
|
|
|
831
1793
|
/**
|
|
832
1794
|
* Delete a session JSONL file belonging to this workspace.
|
|
833
1795
|
*
|
|
834
|
-
*
|
|
835
|
-
* can map them to the right status code:
|
|
1796
|
+
* HTTP-tagged errors (HttpError) map to status codes at the route layer:
|
|
836
1797
|
* - 400: sessionPath not absolute
|
|
837
1798
|
* - 404: workspace gone, or session not in this workspace's list
|
|
838
|
-
* - 409:
|
|
1799
|
+
* - 409: a live runtime is bound to it and is streaming (stop it first)
|
|
839
1800
|
*
|
|
840
|
-
*
|
|
841
|
-
*
|
|
842
|
-
* success — the goal state has been reached.
|
|
1801
|
+
* If a live but idle runtime is bound to the session, it is disposed before
|
|
1802
|
+
* the file is unlinked. Idempotent on ENOENT.
|
|
843
1803
|
*/
|
|
844
1804
|
async deleteSession(workspaceId, sessionPath) {
|
|
845
1805
|
const ws = await getWorkspace(workspaceId);
|
|
@@ -853,79 +1813,109 @@ ${err.stack}` : "")
|
|
|
853
1813
|
if (!target) {
|
|
854
1814
|
throw new HttpError(404, `Session not found: ${sessionPath}`);
|
|
855
1815
|
}
|
|
856
|
-
const
|
|
857
|
-
const
|
|
858
|
-
if (
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1816
|
+
const key = this.keyOf(workspaceId, resolved);
|
|
1817
|
+
const live = this.runtimes.get(key);
|
|
1818
|
+
if (live) {
|
|
1819
|
+
if (live.runtime.session.isStreaming) {
|
|
1820
|
+
throw new HttpError(
|
|
1821
|
+
409,
|
|
1822
|
+
"Cannot delete a streaming session \u2014 stop it first"
|
|
1823
|
+
);
|
|
1824
|
+
}
|
|
1825
|
+
await this.disposeState(live, key);
|
|
863
1826
|
}
|
|
864
1827
|
try {
|
|
865
1828
|
await unlink2(resolved);
|
|
866
|
-
} catch (
|
|
867
|
-
if (
|
|
1829
|
+
} catch (err2) {
|
|
1830
|
+
if (err2?.code === "ENOENT") {
|
|
868
1831
|
console.warn(
|
|
869
1832
|
`[wm] deleteSession: ${resolved} was already gone at unlink time`
|
|
870
1833
|
);
|
|
871
1834
|
return;
|
|
872
1835
|
}
|
|
873
|
-
throw
|
|
1836
|
+
throw err2;
|
|
874
1837
|
}
|
|
875
1838
|
}
|
|
876
|
-
|
|
877
|
-
const ws = await getWorkspace(workspaceId);
|
|
878
|
-
if (!ws) throw new Error(`Workspace not found: ${workspaceId}`);
|
|
879
|
-
if (!isAbsolute2(sessionPath)) {
|
|
880
|
-
throw new Error("Session path must be absolute");
|
|
881
|
-
}
|
|
882
|
-
const sessions = await SessionManager.list(ws.path);
|
|
883
|
-
const resolved = resolve2(sessionPath);
|
|
884
|
-
const target = sessions.find((session) => resolve2(session.path) === resolved);
|
|
885
|
-
if (!target) {
|
|
886
|
-
throw new Error(`Session not found: ${sessionPath}`);
|
|
887
|
-
}
|
|
888
|
-
const runtime = await this.getOrCreate(workspaceId);
|
|
889
|
-
const currentPath = runtime.session.sessionFile ? resolve2(runtime.session.sessionFile) : void 0;
|
|
890
|
-
if (currentPath === resolved) return false;
|
|
891
|
-
if (runtime.session.isStreaming) {
|
|
892
|
-
throw new Error("Cannot switch sessions while the agent is streaming");
|
|
893
|
-
}
|
|
894
|
-
const result = await runtime.switchSession(resolved, { cwdOverride: ws.path });
|
|
895
|
-
return !result.cancelled;
|
|
896
|
-
}
|
|
1839
|
+
/** Dispose every runtime for a workspace (e.g. when it's removed). */
|
|
897
1840
|
async dispose(workspaceId) {
|
|
898
|
-
|
|
899
|
-
if (!state) return;
|
|
900
|
-
this.states.delete(workspaceId);
|
|
901
|
-
this.rebindListeners.delete(workspaceId);
|
|
1841
|
+
this.activeByWorkspace.delete(workspaceId);
|
|
902
1842
|
this.subscribers.delete(workspaceId);
|
|
1843
|
+
const doomed = [...this.runtimes].filter(([, s]) => s.workspaceId === workspaceId);
|
|
1844
|
+
for (const [key, state] of doomed) {
|
|
1845
|
+
await this.disposeState(state, key);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
async disposeAll() {
|
|
1849
|
+
await Promise.allSettled([...this.pending.values()]);
|
|
1850
|
+
const states = [...this.runtimes.entries()];
|
|
1851
|
+
this.runtimes.clear();
|
|
1852
|
+
this.activeByWorkspace.clear();
|
|
1853
|
+
this.subscribers.clear();
|
|
1854
|
+
await Promise.all(states.map(([, state]) => this.disposeState(state)));
|
|
1855
|
+
}
|
|
1856
|
+
/** Tear down a single runtime + its bridge, releasing any ask_user Promises
|
|
1857
|
+
* bound to it first. Removes it from `runtimes` when `key` is given. */
|
|
1858
|
+
async disposeState(state, key) {
|
|
1859
|
+
if (key) this.runtimes.delete(key);
|
|
1860
|
+
if (key && this.activeByWorkspace.get(state.workspaceId) === key) {
|
|
1861
|
+
this.activeByWorkspace.delete(state.workspaceId);
|
|
1862
|
+
}
|
|
1863
|
+
cancelPendingForSession(state.runtime.session.sessionFile ?? null);
|
|
903
1864
|
try {
|
|
904
1865
|
state.bridge.dispose();
|
|
905
1866
|
} catch (e) {
|
|
906
|
-
console.error(`[wm] dispose bridge ${workspaceId} failed:`, e);
|
|
1867
|
+
console.error(`[wm] dispose bridge ${state.workspaceId} failed:`, e);
|
|
907
1868
|
}
|
|
908
1869
|
try {
|
|
909
|
-
state.runtime.
|
|
1870
|
+
await state.runtime.dispose();
|
|
910
1871
|
} catch (e) {
|
|
911
|
-
console.error(`[wm] dispose ${workspaceId} failed:`, e);
|
|
1872
|
+
console.error(`[wm] dispose ${state.workspaceId} failed:`, e);
|
|
912
1873
|
}
|
|
913
1874
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1875
|
+
/**
|
|
1876
|
+
* Keep the live-runtime count under `MAX_LIVE_RUNTIMES` by disposing the
|
|
1877
|
+
* least-recently-touched IDLE runtime (never streaming, never the freshly-
|
|
1878
|
+
* registered one, never a workspace's active pointer). Best-effort: if every
|
|
1879
|
+
* runtime is busy we simply exceed the cap until one frees up.
|
|
1880
|
+
*/
|
|
1881
|
+
evictIfOverCap(justRegistered) {
|
|
1882
|
+
while (this.runtimes.size > MAX_LIVE_RUNTIMES) {
|
|
1883
|
+
let victimKey;
|
|
1884
|
+
let victimTouched = Infinity;
|
|
1885
|
+
for (const [key, state] of this.runtimes) {
|
|
1886
|
+
if (key === justRegistered) continue;
|
|
1887
|
+
if (this.activeByWorkspace.get(state.workspaceId) === key) continue;
|
|
1888
|
+
if (state.runtime.session.isStreaming) continue;
|
|
1889
|
+
if (state.touchedAt < victimTouched) {
|
|
1890
|
+
victimTouched = state.touchedAt;
|
|
1891
|
+
victimKey = key;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
if (!victimKey) break;
|
|
1895
|
+
const victim = this.runtimes.get(victimKey);
|
|
1896
|
+
if (!victim) break;
|
|
1897
|
+
void this.disposeState(victim, victimKey);
|
|
1898
|
+
}
|
|
917
1899
|
}
|
|
918
1900
|
};
|
|
919
|
-
function
|
|
1901
|
+
function safeReconcileAskUser(workspaceId, sm) {
|
|
1902
|
+
try {
|
|
1903
|
+
reconcileAfterRestart(sm);
|
|
1904
|
+
} catch (e) {
|
|
1905
|
+
console.error(`[wm] ask_user cleanup for ${workspaceId} failed:`, e);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
function toSessionSummary(info, running) {
|
|
920
1909
|
const preview = info.firstMessage.replace(/\s+/g, " ").trim();
|
|
921
1910
|
return {
|
|
922
1911
|
path: info.path,
|
|
923
1912
|
name: info.name,
|
|
924
1913
|
updatedAt: info.modified.toISOString(),
|
|
925
|
-
preview: preview ? preview.slice(0, 160) : void 0
|
|
1914
|
+
preview: preview ? preview.slice(0, 160) : void 0,
|
|
1915
|
+
...running ? { running: true } : {}
|
|
926
1916
|
};
|
|
927
1917
|
}
|
|
928
|
-
function
|
|
1918
|
+
function extractUserText2(msg) {
|
|
929
1919
|
if (typeof msg.content === "string") return msg.content;
|
|
930
1920
|
return extractContentText(msg.content);
|
|
931
1921
|
}
|
|
@@ -958,7 +1948,7 @@ function extractContentText(content) {
|
|
|
958
1948
|
}
|
|
959
1949
|
return parts.join("");
|
|
960
1950
|
}
|
|
961
|
-
var workspaceManager = new
|
|
1951
|
+
var workspaceManager = new SessionRuntimeManager();
|
|
962
1952
|
function broadcastTo(subscribers, msg) {
|
|
963
1953
|
const wire = JSON.stringify(msg);
|
|
964
1954
|
for (const ws of subscribers) {
|
|
@@ -1041,8 +2031,8 @@ function mountConfigRoutes(app2) {
|
|
|
1041
2031
|
try {
|
|
1042
2032
|
await workspaceManager.getOrCreate(id);
|
|
1043
2033
|
return c.json(buildConfigResponse(id));
|
|
1044
|
-
} catch (
|
|
1045
|
-
const message =
|
|
2034
|
+
} catch (err2) {
|
|
2035
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
1046
2036
|
return c.json({ ok: false, error: message }, 500);
|
|
1047
2037
|
}
|
|
1048
2038
|
});
|
|
@@ -1068,8 +2058,8 @@ function mountConfigRoutes(app2) {
|
|
|
1068
2058
|
await runtime.session.setModel(model);
|
|
1069
2059
|
broadcastContextUsage(id, runtime);
|
|
1070
2060
|
return c.json(buildConfigResponse(id));
|
|
1071
|
-
} catch (
|
|
1072
|
-
const message =
|
|
2061
|
+
} catch (err2) {
|
|
2062
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
1073
2063
|
return c.json({ ok: false, error: message }, 500);
|
|
1074
2064
|
}
|
|
1075
2065
|
});
|
|
@@ -1093,8 +2083,8 @@ function mountConfigRoutes(app2) {
|
|
|
1093
2083
|
}
|
|
1094
2084
|
runtime.session.setThinkingLevel(body.level);
|
|
1095
2085
|
return c.json(buildConfigResponse(id));
|
|
1096
|
-
} catch (
|
|
1097
|
-
const message =
|
|
2086
|
+
} catch (err2) {
|
|
2087
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
1098
2088
|
return c.json({ ok: false, error: message }, 500);
|
|
1099
2089
|
}
|
|
1100
2090
|
});
|
|
@@ -1117,8 +2107,37 @@ function mountConfigRoutes(app2) {
|
|
|
1117
2107
|
}
|
|
1118
2108
|
runtime.session.setActiveToolsByName(body.tools);
|
|
1119
2109
|
return c.json(buildConfigResponse(id));
|
|
1120
|
-
} catch (
|
|
1121
|
-
const message =
|
|
2110
|
+
} catch (err2) {
|
|
2111
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
2112
|
+
return c.json({ ok: false, error: message }, 500);
|
|
2113
|
+
}
|
|
2114
|
+
});
|
|
2115
|
+
app2.put("/:id/config/session-name", async (c) => {
|
|
2116
|
+
const id = c.req.param("id");
|
|
2117
|
+
const exists2 = await requireWorkspace(c, id);
|
|
2118
|
+
if (!exists2) return c.json({ ok: false, error: "not found" }, 404);
|
|
2119
|
+
const body = await c.req.json();
|
|
2120
|
+
if (typeof body?.name !== "string") {
|
|
2121
|
+
return c.json({ ok: false, error: "name is required" }, 400);
|
|
2122
|
+
}
|
|
2123
|
+
const trimmed = body.name.trim();
|
|
2124
|
+
if (!trimmed) {
|
|
2125
|
+
return c.json({ ok: false, error: "name must not be empty" }, 400);
|
|
2126
|
+
}
|
|
2127
|
+
try {
|
|
2128
|
+
const sessionPath = c.req.query("sessionPath");
|
|
2129
|
+
if (sessionPath) {
|
|
2130
|
+
const err2 = await workspaceManager.validateSessionOwnership(id, sessionPath);
|
|
2131
|
+
if (err2) return c.json({ ok: false, error: err2 }, 404);
|
|
2132
|
+
}
|
|
2133
|
+
const runtime = await workspaceManager.getOrCreate(id, sessionPath || void 0);
|
|
2134
|
+
if (runtime.session.isStreaming) {
|
|
2135
|
+
return c.json({ ok: false, error: "cannot rename while the agent is streaming" }, 409);
|
|
2136
|
+
}
|
|
2137
|
+
runtime.session.setSessionName(trimmed);
|
|
2138
|
+
return c.json({ name: trimmed });
|
|
2139
|
+
} catch (err2) {
|
|
2140
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
1122
2141
|
return c.json({ ok: false, error: message }, 500);
|
|
1123
2142
|
}
|
|
1124
2143
|
});
|
|
@@ -1127,7 +2146,7 @@ function mountConfigRoutes(app2) {
|
|
|
1127
2146
|
// src/api/files.ts
|
|
1128
2147
|
import { execFile as execFile2 } from "child_process";
|
|
1129
2148
|
import { readdir } from "fs/promises";
|
|
1130
|
-
import { join as
|
|
2149
|
+
import { join as join5, relative, sep as sep2 } from "path";
|
|
1131
2150
|
import { promisify as promisify2 } from "util";
|
|
1132
2151
|
var exec2 = promisify2(execFile2);
|
|
1133
2152
|
var LIST_TTL_MS = 1e4;
|
|
@@ -1159,8 +2178,8 @@ async function getFileList(workspacePath) {
|
|
|
1159
2178
|
const now = Date.now();
|
|
1160
2179
|
const cached = listCache.get(workspacePath);
|
|
1161
2180
|
if (cached && cached.expiresAt > now) return cached.files;
|
|
1162
|
-
const
|
|
1163
|
-
if (
|
|
2181
|
+
const pending2 = inflight2.get(workspacePath);
|
|
2182
|
+
if (pending2) return (await pending2).files;
|
|
1164
2183
|
const probe = probeFileList(workspacePath).then((files) => {
|
|
1165
2184
|
const entry = {
|
|
1166
2185
|
files,
|
|
@@ -1216,7 +2235,7 @@ async function walkDir(root, dir, depth, out) {
|
|
|
1216
2235
|
for (const d of dirents) {
|
|
1217
2236
|
if (out.length >= MAX_FILES_TRACKED) return;
|
|
1218
2237
|
if (WALK_IGNORES.has(d.name)) continue;
|
|
1219
|
-
const abs =
|
|
2238
|
+
const abs = join5(dir, d.name);
|
|
1220
2239
|
if (d.isDirectory()) {
|
|
1221
2240
|
await walkDir(root, abs, depth + 1, out);
|
|
1222
2241
|
} else if (d.isFile()) {
|
|
@@ -1256,7 +2275,7 @@ function mountFilesRoute(app2) {
|
|
|
1256
2275
|
if (!qRaw) {
|
|
1257
2276
|
const slice = all.slice(0, limit);
|
|
1258
2277
|
entries = slice.map((relPath) => ({
|
|
1259
|
-
path:
|
|
2278
|
+
path: join5(workspacePath, relPath),
|
|
1260
2279
|
relPath
|
|
1261
2280
|
}));
|
|
1262
2281
|
truncated = all.length > limit;
|
|
@@ -1273,16 +2292,16 @@ function mountFilesRoute(app2) {
|
|
|
1273
2292
|
scored.sort((a, b) => b.score - a.score);
|
|
1274
2293
|
const top = scored.slice(0, limit);
|
|
1275
2294
|
entries = top.map((e) => ({
|
|
1276
|
-
path:
|
|
2295
|
+
path: join5(workspacePath, e.relPath),
|
|
1277
2296
|
relPath: e.relPath
|
|
1278
2297
|
}));
|
|
1279
2298
|
truncated = matchCount > limit;
|
|
1280
2299
|
}
|
|
1281
2300
|
const body = { workspacePath, entries, truncated };
|
|
1282
2301
|
return c.json(body);
|
|
1283
|
-
} catch (
|
|
1284
|
-
const message =
|
|
1285
|
-
console.error(`[api/files] search for ${id} failed:`,
|
|
2302
|
+
} catch (err2) {
|
|
2303
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
2304
|
+
console.error(`[api/files] search for ${id} failed:`, err2);
|
|
1286
2305
|
return c.json({ ok: false, error: message }, 500);
|
|
1287
2306
|
}
|
|
1288
2307
|
});
|
|
@@ -1290,7 +2309,7 @@ function mountFilesRoute(app2) {
|
|
|
1290
2309
|
|
|
1291
2310
|
// src/api/resources.ts
|
|
1292
2311
|
import { readdir as readdir2 } from "fs/promises";
|
|
1293
|
-
import { join as
|
|
2312
|
+
import { join as join6 } from "path";
|
|
1294
2313
|
import { getAgentDir as getAgentDir2 } from "@earendil-works/pi-coding-agent";
|
|
1295
2314
|
function toResourceSource(info) {
|
|
1296
2315
|
return {
|
|
@@ -1300,16 +2319,16 @@ function toResourceSource(info) {
|
|
|
1300
2319
|
};
|
|
1301
2320
|
}
|
|
1302
2321
|
async function scanExtensionDirs(workspaceCwd) {
|
|
1303
|
-
const dirs = [
|
|
2322
|
+
const dirs = [join6(getAgentDir2(), "extensions"), join6(workspaceCwd, ".pi", "extensions")];
|
|
1304
2323
|
const found = [];
|
|
1305
2324
|
for (const dir of dirs) {
|
|
1306
2325
|
try {
|
|
1307
2326
|
const entries = await readdir2(dir, { withFileTypes: true });
|
|
1308
2327
|
for (const entry of entries) {
|
|
1309
2328
|
if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".js"))) {
|
|
1310
|
-
found.push(
|
|
2329
|
+
found.push(join6(dir, entry.name));
|
|
1311
2330
|
} else if (entry.isDirectory()) {
|
|
1312
|
-
found.push(
|
|
2331
|
+
found.push(join6(dir, entry.name));
|
|
1313
2332
|
}
|
|
1314
2333
|
}
|
|
1315
2334
|
} catch {
|
|
@@ -1364,18 +2383,28 @@ async function snapshot(workspaceId, roots, workspaceCwd) {
|
|
|
1364
2383
|
shortcuts: [...e.shortcuts.keys()]
|
|
1365
2384
|
};
|
|
1366
2385
|
});
|
|
1367
|
-
const extensionErrors = extResult.errors.map((
|
|
1368
|
-
path:
|
|
1369
|
-
error:
|
|
2386
|
+
const extensionErrors = extResult.errors.map((err2) => ({
|
|
2387
|
+
path: err2.path,
|
|
2388
|
+
error: err2.error
|
|
1370
2389
|
}));
|
|
1371
2390
|
const disabledExtensions = EXTENSIONS_ENABLED ? [] : await scanExtensionDirs(workspaceCwd);
|
|
2391
|
+
const disabledBuiltins = new Set(getDisabledBuiltins());
|
|
2392
|
+
const builtinExtensions = BUILTIN_EXTENSIONS.map((d) => ({
|
|
2393
|
+
id: d.id,
|
|
2394
|
+
name: d.name,
|
|
2395
|
+
description: d.description,
|
|
2396
|
+
enabled: !disabledBuiltins.has(d.id),
|
|
2397
|
+
tools: d.tools,
|
|
2398
|
+
commands: d.commands
|
|
2399
|
+
}));
|
|
1372
2400
|
return {
|
|
1373
2401
|
skills: skillsOut,
|
|
1374
2402
|
prompts: promptsOut,
|
|
1375
2403
|
extensionsEnabled: EXTENSIONS_ENABLED,
|
|
1376
2404
|
extensions: extensionsOut,
|
|
1377
2405
|
extensionErrors,
|
|
1378
|
-
disabledExtensions
|
|
2406
|
+
disabledExtensions,
|
|
2407
|
+
builtinExtensions
|
|
1379
2408
|
};
|
|
1380
2409
|
}
|
|
1381
2410
|
async function rootsFor(workspaceId) {
|
|
@@ -1384,12 +2413,12 @@ async function rootsFor(workspaceId) {
|
|
|
1384
2413
|
const roots = resolveResourceRoots({ agentDir: getAgentDir2(), workspaceCwd: ws.path });
|
|
1385
2414
|
return { roots, workspaceCwd: ws.path };
|
|
1386
2415
|
}
|
|
1387
|
-
function respondError(c,
|
|
1388
|
-
if (
|
|
1389
|
-
return c.json({ ok: false, error:
|
|
2416
|
+
function respondError(c, err2) {
|
|
2417
|
+
if (err2 instanceof HttpError) {
|
|
2418
|
+
return c.json({ ok: false, error: err2.message }, err2.status);
|
|
1390
2419
|
}
|
|
1391
|
-
const message =
|
|
1392
|
-
console.error(`[api/resources] unexpected error:`,
|
|
2420
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
2421
|
+
console.error(`[api/resources] unexpected error:`, err2);
|
|
1393
2422
|
return c.json({ ok: false, error: message }, 500);
|
|
1394
2423
|
}
|
|
1395
2424
|
async function reload(workspaceId) {
|
|
@@ -1406,8 +2435,8 @@ function mountResourcesRoute(app2) {
|
|
|
1406
2435
|
await workspaceManager.getOrCreate(id);
|
|
1407
2436
|
const { roots, workspaceCwd } = await rootsFor(id);
|
|
1408
2437
|
return c.json(await snapshot(id, roots, workspaceCwd));
|
|
1409
|
-
} catch (
|
|
1410
|
-
return respondError(c,
|
|
2438
|
+
} catch (err2) {
|
|
2439
|
+
return respondError(c, err2);
|
|
1411
2440
|
}
|
|
1412
2441
|
});
|
|
1413
2442
|
app2.post("/:id/resources/reload", async (c) => {
|
|
@@ -1419,8 +2448,8 @@ function mountResourcesRoute(app2) {
|
|
|
1419
2448
|
await reload(id);
|
|
1420
2449
|
const { roots, workspaceCwd } = await rootsFor(id);
|
|
1421
2450
|
return c.json(await snapshot(id, roots, workspaceCwd));
|
|
1422
|
-
} catch (
|
|
1423
|
-
return respondError(c,
|
|
2451
|
+
} catch (err2) {
|
|
2452
|
+
return respondError(c, err2);
|
|
1424
2453
|
}
|
|
1425
2454
|
});
|
|
1426
2455
|
app2.get("/:id/resources/skill", async (c) => {
|
|
@@ -1445,8 +2474,8 @@ function mountResourcesRoute(app2) {
|
|
|
1445
2474
|
body: data.body
|
|
1446
2475
|
};
|
|
1447
2476
|
return c.json(body);
|
|
1448
|
-
} catch (
|
|
1449
|
-
return respondError(c,
|
|
2477
|
+
} catch (err2) {
|
|
2478
|
+
return respondError(c, err2);
|
|
1450
2479
|
}
|
|
1451
2480
|
});
|
|
1452
2481
|
app2.post("/:id/resources/skills", async (c) => {
|
|
@@ -1470,8 +2499,8 @@ function mountResourcesRoute(app2) {
|
|
|
1470
2499
|
});
|
|
1471
2500
|
await reload(id);
|
|
1472
2501
|
return c.json(await snapshot(id, roots, workspaceCwd));
|
|
1473
|
-
} catch (
|
|
1474
|
-
return respondError(c,
|
|
2502
|
+
} catch (err2) {
|
|
2503
|
+
return respondError(c, err2);
|
|
1475
2504
|
}
|
|
1476
2505
|
});
|
|
1477
2506
|
app2.put("/:id/resources/skills", async (c) => {
|
|
@@ -1495,8 +2524,8 @@ function mountResourcesRoute(app2) {
|
|
|
1495
2524
|
});
|
|
1496
2525
|
await reload(id);
|
|
1497
2526
|
return c.json(await snapshot(id, roots, workspaceCwd));
|
|
1498
|
-
} catch (
|
|
1499
|
-
return respondError(c,
|
|
2527
|
+
} catch (err2) {
|
|
2528
|
+
return respondError(c, err2);
|
|
1500
2529
|
}
|
|
1501
2530
|
});
|
|
1502
2531
|
app2.delete("/:id/resources/skills", async (c) => {
|
|
@@ -1511,8 +2540,8 @@ function mountResourcesRoute(app2) {
|
|
|
1511
2540
|
await deleteSkill(filePath, roots);
|
|
1512
2541
|
await reload(id);
|
|
1513
2542
|
return c.json(await snapshot(id, roots, workspaceCwd));
|
|
1514
|
-
} catch (
|
|
1515
|
-
return respondError(c,
|
|
2543
|
+
} catch (err2) {
|
|
2544
|
+
return respondError(c, err2);
|
|
1516
2545
|
}
|
|
1517
2546
|
});
|
|
1518
2547
|
app2.get("/:id/resources/prompt", async (c) => {
|
|
@@ -1537,8 +2566,8 @@ function mountResourcesRoute(app2) {
|
|
|
1537
2566
|
body: data.body
|
|
1538
2567
|
};
|
|
1539
2568
|
return c.json(body);
|
|
1540
|
-
} catch (
|
|
1541
|
-
return respondError(c,
|
|
2569
|
+
} catch (err2) {
|
|
2570
|
+
return respondError(c, err2);
|
|
1542
2571
|
}
|
|
1543
2572
|
});
|
|
1544
2573
|
app2.post("/:id/resources/prompts", async (c) => {
|
|
@@ -1562,8 +2591,8 @@ function mountResourcesRoute(app2) {
|
|
|
1562
2591
|
});
|
|
1563
2592
|
await reload(id);
|
|
1564
2593
|
return c.json(await snapshot(id, roots, workspaceCwd));
|
|
1565
|
-
} catch (
|
|
1566
|
-
return respondError(c,
|
|
2594
|
+
} catch (err2) {
|
|
2595
|
+
return respondError(c, err2);
|
|
1567
2596
|
}
|
|
1568
2597
|
});
|
|
1569
2598
|
app2.put("/:id/resources/prompts", async (c) => {
|
|
@@ -1587,8 +2616,8 @@ function mountResourcesRoute(app2) {
|
|
|
1587
2616
|
});
|
|
1588
2617
|
await reload(id);
|
|
1589
2618
|
return c.json(await snapshot(id, roots, workspaceCwd));
|
|
1590
|
-
} catch (
|
|
1591
|
-
return respondError(c,
|
|
2619
|
+
} catch (err2) {
|
|
2620
|
+
return respondError(c, err2);
|
|
1592
2621
|
}
|
|
1593
2622
|
});
|
|
1594
2623
|
app2.delete("/:id/resources/prompts", async (c) => {
|
|
@@ -1603,23 +2632,218 @@ function mountResourcesRoute(app2) {
|
|
|
1603
2632
|
await deletePrompt(filePath, roots);
|
|
1604
2633
|
await reload(id);
|
|
1605
2634
|
return c.json(await snapshot(id, roots, workspaceCwd));
|
|
1606
|
-
} catch (
|
|
1607
|
-
return respondError(c,
|
|
2635
|
+
} catch (err2) {
|
|
2636
|
+
return respondError(c, err2);
|
|
2637
|
+
}
|
|
2638
|
+
});
|
|
2639
|
+
app2.put("/:id/resources/builtin-extensions", async (c) => {
|
|
2640
|
+
const id = c.req.param("id");
|
|
2641
|
+
const ws = await getWorkspace(id);
|
|
2642
|
+
if (!ws) return c.json({ ok: false, error: "not found" }, 404);
|
|
2643
|
+
const body = await c.req.json().catch(() => null);
|
|
2644
|
+
if (!body || typeof body.id !== "string" || typeof body.enabled !== "boolean") {
|
|
2645
|
+
return c.json({ ok: false, error: "id and enabled are required" }, 400);
|
|
2646
|
+
}
|
|
2647
|
+
if (!BUILTIN_EXTENSIONS.some((d) => d.id === body.id)) {
|
|
2648
|
+
return c.json({ ok: false, error: `unknown builtin extension: ${body.id}` }, 400);
|
|
2649
|
+
}
|
|
2650
|
+
try {
|
|
2651
|
+
const runtime = await workspaceManager.getOrCreate(id);
|
|
2652
|
+
if (runtime.session.isStreaming) {
|
|
2653
|
+
return c.json(
|
|
2654
|
+
{ ok: false, error: "Stop the current turn before changing extensions" },
|
|
2655
|
+
409
|
|
2656
|
+
);
|
|
2657
|
+
}
|
|
2658
|
+
await setBuiltinEnabled(body.id, body.enabled);
|
|
2659
|
+
await runtime.session.reload();
|
|
2660
|
+
const { roots, workspaceCwd } = await rootsFor(id);
|
|
2661
|
+
return c.json(await snapshot(id, roots, workspaceCwd));
|
|
2662
|
+
} catch (err2) {
|
|
2663
|
+
return respondError(c, err2);
|
|
2664
|
+
}
|
|
2665
|
+
});
|
|
2666
|
+
}
|
|
2667
|
+
function isScope(value) {
|
|
2668
|
+
return value === "user" || value === "project";
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
// src/api/tree.ts
|
|
2672
|
+
import { Hono } from "hono";
|
|
2673
|
+
var treeRoute = new Hono();
|
|
2674
|
+
treeRoute.get("/", async (c) => {
|
|
2675
|
+
const id = c.req.param("id") ?? "";
|
|
2676
|
+
const existed = await getWorkspace(id);
|
|
2677
|
+
if (!existed) return c.json({ ok: false, error: "not found" }, 404);
|
|
2678
|
+
try {
|
|
2679
|
+
const runtime = await workspaceManager.getOrCreate(id);
|
|
2680
|
+
const sm = runtime.session.sessionManager;
|
|
2681
|
+
const tree = sm.getTree();
|
|
2682
|
+
const leafId = sm.getLeafId();
|
|
2683
|
+
const activeIds = /* @__PURE__ */ new Set();
|
|
2684
|
+
if (leafId) {
|
|
2685
|
+
const branch = sm.getBranch(leafId);
|
|
2686
|
+
for (const entry of branch) {
|
|
2687
|
+
activeIds.add(entry.id);
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
const nodes = [];
|
|
2691
|
+
flattenTree(tree, leafId, activeIds, nodes, 0);
|
|
2692
|
+
const body = { nodes, leafId };
|
|
2693
|
+
return c.json(body);
|
|
2694
|
+
} catch (err2) {
|
|
2695
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
2696
|
+
console.error(`[api] tree for ${id} failed:`, err2);
|
|
2697
|
+
return c.json({ ok: false, error: message }, 500);
|
|
2698
|
+
}
|
|
2699
|
+
});
|
|
2700
|
+
function flattenTree(nodes, leafId, activeIds, out, depth) {
|
|
2701
|
+
const sorted = [...nodes].sort(
|
|
2702
|
+
(a, b) => new Date(a.entry.timestamp).getTime() - new Date(b.entry.timestamp).getTime()
|
|
2703
|
+
);
|
|
2704
|
+
for (const node of sorted) {
|
|
2705
|
+
const entry = node.entry;
|
|
2706
|
+
const id = entry.id;
|
|
2707
|
+
out.push({
|
|
2708
|
+
id,
|
|
2709
|
+
parentId: entry.parentId,
|
|
2710
|
+
depth,
|
|
2711
|
+
entryType: mapEntryType(entry.type),
|
|
2712
|
+
messageRole: entry.type === "message" ? mapMessageRole(entry["message"]) : void 0,
|
|
2713
|
+
timestamp: entry.timestamp,
|
|
2714
|
+
preview: extractPreview(entry),
|
|
2715
|
+
active: activeIds.has(id),
|
|
2716
|
+
isLeaf: id === leafId,
|
|
2717
|
+
childCount: node.children.length,
|
|
2718
|
+
label: node.label
|
|
2719
|
+
});
|
|
2720
|
+
if (node.children.length > 0) {
|
|
2721
|
+
flattenTree(node.children, leafId, activeIds, out, depth + 1);
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
function mapEntryType(type) {
|
|
2726
|
+
switch (type) {
|
|
2727
|
+
case "message":
|
|
2728
|
+
return "message";
|
|
2729
|
+
case "compaction":
|
|
2730
|
+
return "compaction";
|
|
2731
|
+
case "branch_summary":
|
|
2732
|
+
return "branch_summary";
|
|
2733
|
+
case "model_change":
|
|
2734
|
+
return "model_change";
|
|
2735
|
+
case "thinking_level_change":
|
|
2736
|
+
return "thinking_level_change";
|
|
2737
|
+
case "custom":
|
|
2738
|
+
return "custom";
|
|
2739
|
+
case "custom_message":
|
|
2740
|
+
return "custom_message";
|
|
2741
|
+
case "label":
|
|
2742
|
+
return "label";
|
|
2743
|
+
case "session_info":
|
|
2744
|
+
return "session_info";
|
|
2745
|
+
default:
|
|
2746
|
+
console.warn(`[tree] unknown entry type "${type}" \u2014 mapping to "custom". pi SDK may have added a type tree.ts doesn't handle yet.`);
|
|
2747
|
+
return "custom";
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
function mapMessageRole(msg) {
|
|
2751
|
+
if (!msg || typeof msg !== "object") return void 0;
|
|
2752
|
+
const role = msg.role;
|
|
2753
|
+
switch (role) {
|
|
2754
|
+
case "user":
|
|
2755
|
+
return "user";
|
|
2756
|
+
case "assistant":
|
|
2757
|
+
return "assistant";
|
|
2758
|
+
case "toolResult":
|
|
2759
|
+
return "toolResult";
|
|
2760
|
+
case "bashExecution":
|
|
2761
|
+
return "bashExecution";
|
|
2762
|
+
case "custom":
|
|
2763
|
+
return "custom";
|
|
2764
|
+
case "branchSummary":
|
|
2765
|
+
return "branchSummary";
|
|
2766
|
+
case "compactionSummary":
|
|
2767
|
+
return "compactionSummary";
|
|
2768
|
+
default:
|
|
2769
|
+
return void 0;
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
var PREVIEW_MAX = 120;
|
|
2773
|
+
function extractPreview(entry) {
|
|
2774
|
+
switch (entry.type) {
|
|
2775
|
+
case "message":
|
|
2776
|
+
return extractMessagePreview(entry["message"]);
|
|
2777
|
+
case "compaction":
|
|
2778
|
+
return truncate(String(entry["summary"] ?? "Compaction"), PREVIEW_MAX);
|
|
2779
|
+
case "branch_summary":
|
|
2780
|
+
return truncate(String(entry["summary"] ?? "Branch summary"), PREVIEW_MAX);
|
|
2781
|
+
case "model_change":
|
|
2782
|
+
return `${entry["provider"] ?? ""}/${entry["modelId"] ?? ""}`;
|
|
2783
|
+
case "thinking_level_change":
|
|
2784
|
+
return `Thinking: ${entry["thinkingLevel"] ?? ""}`;
|
|
2785
|
+
case "session_info":
|
|
2786
|
+
return entry["name"] ? `Name: ${entry["name"]}` : "Session info";
|
|
2787
|
+
case "custom_message":
|
|
2788
|
+
return truncate(extractContentText2(entry["content"]), PREVIEW_MAX) || "Extension message";
|
|
2789
|
+
case "custom":
|
|
2790
|
+
return `Custom: ${entry["customType"] ?? ""}`;
|
|
2791
|
+
case "label":
|
|
2792
|
+
return "Label";
|
|
2793
|
+
default:
|
|
2794
|
+
return "";
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
function extractMessagePreview(msg) {
|
|
2798
|
+
if (!msg || typeof msg !== "object") return "";
|
|
2799
|
+
const m = msg;
|
|
2800
|
+
if (m.role === "bashExecution") {
|
|
2801
|
+
return truncate(`$ ${m.command ?? ""}`, PREVIEW_MAX);
|
|
2802
|
+
}
|
|
2803
|
+
return truncate(extractContentText2(m.content), PREVIEW_MAX);
|
|
2804
|
+
}
|
|
2805
|
+
function extractContentText2(content) {
|
|
2806
|
+
if (typeof content === "string") return content.replace(/\s+/g, " ").trim();
|
|
2807
|
+
if (!Array.isArray(content)) return "";
|
|
2808
|
+
const parts = [];
|
|
2809
|
+
for (const block of content) {
|
|
2810
|
+
if (!block || typeof block !== "object") continue;
|
|
2811
|
+
const b = block;
|
|
2812
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
2813
|
+
parts.push(b.text);
|
|
2814
|
+
} else if (b.type === "thinking" && typeof b.thinking === "string") {
|
|
2815
|
+
parts.push(b.thinking);
|
|
1608
2816
|
}
|
|
1609
|
-
}
|
|
2817
|
+
}
|
|
2818
|
+
return parts.join(" ").replace(/\s+/g, " ").trim();
|
|
1610
2819
|
}
|
|
1611
|
-
function
|
|
1612
|
-
|
|
2820
|
+
function truncate(text, max) {
|
|
2821
|
+
if (text.length <= max) return text;
|
|
2822
|
+
return text.slice(0, max) + "\u2026";
|
|
1613
2823
|
}
|
|
1614
2824
|
|
|
1615
2825
|
// src/api/workspaces.ts
|
|
1616
|
-
var workspacesRoute = new
|
|
2826
|
+
var workspacesRoute = new Hono2();
|
|
1617
2827
|
workspacesRoute.get("/", async (c) => {
|
|
1618
2828
|
const raw = await listWorkspaces();
|
|
1619
2829
|
const workspaces = await Promise.all(raw.map(enrichWorkspace));
|
|
1620
2830
|
const body = { workspaces };
|
|
1621
2831
|
return c.json(body);
|
|
1622
2832
|
});
|
|
2833
|
+
workspacesRoute.put("/reorder", async (c) => {
|
|
2834
|
+
const body = await c.req.json();
|
|
2835
|
+
if (!body?.ids || !Array.isArray(body.ids) || body.ids.length === 0 || !body.ids.every((id) => typeof id === "string")) {
|
|
2836
|
+
return c.json(
|
|
2837
|
+
{ ok: false, error: "ids must be a non-empty array of strings" },
|
|
2838
|
+
400
|
|
2839
|
+
);
|
|
2840
|
+
}
|
|
2841
|
+
await reorderWorkspaces(body.ids);
|
|
2842
|
+
const raw = await listWorkspaces();
|
|
2843
|
+
const workspaces = await Promise.all(raw.map(enrichWorkspace));
|
|
2844
|
+
const resBody = { workspaces };
|
|
2845
|
+
return c.json(resBody);
|
|
2846
|
+
});
|
|
1623
2847
|
workspacesRoute.get("/:id/sessions", async (c) => {
|
|
1624
2848
|
const id = c.req.param("id");
|
|
1625
2849
|
const existed = await getWorkspace(id);
|
|
@@ -1628,9 +2852,9 @@ workspacesRoute.get("/:id/sessions", async (c) => {
|
|
|
1628
2852
|
const sessions = await workspaceManager.listSessions(id);
|
|
1629
2853
|
const body = { sessions };
|
|
1630
2854
|
return c.json(body);
|
|
1631
|
-
} catch (
|
|
1632
|
-
const message =
|
|
1633
|
-
console.error(`[api] list sessions for ${id} failed:`,
|
|
2855
|
+
} catch (err2) {
|
|
2856
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
2857
|
+
console.error(`[api] list sessions for ${id} failed:`, err2);
|
|
1634
2858
|
return c.json({ ok: false, error: message }, 500);
|
|
1635
2859
|
}
|
|
1636
2860
|
});
|
|
@@ -1646,15 +2870,15 @@ workspacesRoute.delete("/:id/sessions", async (c) => {
|
|
|
1646
2870
|
await workspaceManager.deleteSession(id, sessionPath);
|
|
1647
2871
|
const body = { ok: true };
|
|
1648
2872
|
return c.json(body);
|
|
1649
|
-
} catch (
|
|
1650
|
-
if (
|
|
2873
|
+
} catch (err2) {
|
|
2874
|
+
if (err2 instanceof HttpError) {
|
|
1651
2875
|
return c.json(
|
|
1652
|
-
{ ok: false, error:
|
|
1653
|
-
|
|
2876
|
+
{ ok: false, error: err2.message },
|
|
2877
|
+
err2.status
|
|
1654
2878
|
);
|
|
1655
2879
|
}
|
|
1656
|
-
const message =
|
|
1657
|
-
console.error(`[api] delete session for ${id} failed:`,
|
|
2880
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
2881
|
+
console.error(`[api] delete session for ${id} failed:`, err2);
|
|
1658
2882
|
return c.json({ ok: false, error: message }, 500);
|
|
1659
2883
|
}
|
|
1660
2884
|
});
|
|
@@ -1671,9 +2895,31 @@ workspacesRoute.get("/:id/fork-points", async (c) => {
|
|
|
1671
2895
|
}));
|
|
1672
2896
|
const body = { points };
|
|
1673
2897
|
return c.json(body);
|
|
1674
|
-
} catch (
|
|
1675
|
-
const message =
|
|
1676
|
-
console.error(`[api] fork-points for ${id} failed:`,
|
|
2898
|
+
} catch (err2) {
|
|
2899
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
2900
|
+
console.error(`[api] fork-points for ${id} failed:`, err2);
|
|
2901
|
+
return c.json({ ok: false, error: message }, 500);
|
|
2902
|
+
}
|
|
2903
|
+
});
|
|
2904
|
+
workspacesRoute.get("/:id/export", async (c) => {
|
|
2905
|
+
const id = c.req.param("id");
|
|
2906
|
+
const existed = await getWorkspace(id);
|
|
2907
|
+
if (!existed) return c.json({ ok: false, error: "not found" }, 404);
|
|
2908
|
+
try {
|
|
2909
|
+
const sessionPath = c.req.query("sessionPath");
|
|
2910
|
+
if (sessionPath) {
|
|
2911
|
+
const err2 = await workspaceManager.validateSessionOwnership(id, sessionPath);
|
|
2912
|
+
if (err2) return c.json({ ok: false, error: err2 }, 404);
|
|
2913
|
+
}
|
|
2914
|
+
const runtime = await workspaceManager.getOrCreate(id, sessionPath || void 0);
|
|
2915
|
+
const outputPath = await runtime.session.exportToHtml();
|
|
2916
|
+
const html = await readFile4(outputPath, "utf-8");
|
|
2917
|
+
const filename = basename2(outputPath);
|
|
2918
|
+
const body = { html, filename };
|
|
2919
|
+
return c.json(body);
|
|
2920
|
+
} catch (err2) {
|
|
2921
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
2922
|
+
console.error(`[api] export for ${id} failed:`, err2);
|
|
1677
2923
|
return c.json({ ok: false, error: message }, 500);
|
|
1678
2924
|
}
|
|
1679
2925
|
});
|
|
@@ -1686,9 +2932,9 @@ workspacesRoute.get("/:id/history", async (c) => {
|
|
|
1686
2932
|
const sessionPath = c.req.query("sessionPath");
|
|
1687
2933
|
const body = workspaceManager.getSessionHistory(id, sessionPath);
|
|
1688
2934
|
return c.json(body);
|
|
1689
|
-
} catch (
|
|
1690
|
-
const message =
|
|
1691
|
-
console.error(`[api] history for ${id} failed:`,
|
|
2935
|
+
} catch (err2) {
|
|
2936
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
2937
|
+
console.error(`[api] history for ${id} failed:`, err2);
|
|
1692
2938
|
return c.json({ ok: false, error: message }, 500);
|
|
1693
2939
|
}
|
|
1694
2940
|
});
|
|
@@ -1729,13 +2975,14 @@ workspacesRoute.delete("/:id", async (c) => {
|
|
|
1729
2975
|
mountConfigRoutes(workspacesRoute);
|
|
1730
2976
|
mountResourcesRoute(workspacesRoute);
|
|
1731
2977
|
mountFilesRoute(workspacesRoute);
|
|
2978
|
+
workspacesRoute.route("/:id/tree", treeRoute);
|
|
1732
2979
|
|
|
1733
2980
|
// src/api/fs.ts
|
|
1734
2981
|
import { readdir as readdir3 } from "fs/promises";
|
|
1735
2982
|
import { homedir as homedir2 } from "os";
|
|
1736
|
-
import { dirname as
|
|
1737
|
-
import { Hono as
|
|
1738
|
-
var fsRoute = new
|
|
2983
|
+
import { dirname as dirname4, isAbsolute as isAbsolute4, join as join7, resolve as resolve4 } from "path";
|
|
2984
|
+
import { Hono as Hono3 } from "hono";
|
|
2985
|
+
var fsRoute = new Hono3();
|
|
1739
2986
|
fsRoute.get("/browse", async (c) => {
|
|
1740
2987
|
const rawPath = c.req.query("path");
|
|
1741
2988
|
const showHidden = c.req.query("showHidden") === "1";
|
|
@@ -1743,18 +2990,18 @@ fsRoute.get("/browse", async (c) => {
|
|
|
1743
2990
|
let dirents;
|
|
1744
2991
|
try {
|
|
1745
2992
|
dirents = await readdir3(target, { withFileTypes: true });
|
|
1746
|
-
} catch (
|
|
1747
|
-
const code =
|
|
2993
|
+
} catch (err2) {
|
|
2994
|
+
const code = err2.code;
|
|
1748
2995
|
const msg = code === "EACCES" ? "permission denied" : code === "ENOENT" ? "not found" : "read failed";
|
|
1749
2996
|
return c.json({ ok: false, error: msg, path: target }, 400);
|
|
1750
2997
|
}
|
|
1751
2998
|
const entries = dirents.filter((d) => d.isDirectory()).filter((d) => showHidden || !d.name.startsWith(".")).map((d) => ({
|
|
1752
2999
|
name: d.name,
|
|
1753
|
-
path:
|
|
3000
|
+
path: join7(target, d.name),
|
|
1754
3001
|
type: "dir"
|
|
1755
3002
|
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
1756
3003
|
const parent = (() => {
|
|
1757
|
-
const p =
|
|
3004
|
+
const p = dirname4(target);
|
|
1758
3005
|
return p === target ? null : p;
|
|
1759
3006
|
})();
|
|
1760
3007
|
const body = { path: target, parent, entries };
|
|
@@ -1762,13 +3009,13 @@ fsRoute.get("/browse", async (c) => {
|
|
|
1762
3009
|
});
|
|
1763
3010
|
|
|
1764
3011
|
// src/api/model-configs.ts
|
|
1765
|
-
import { readFile as
|
|
1766
|
-
import { dirname as
|
|
1767
|
-
import { Hono as
|
|
3012
|
+
import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
|
|
3013
|
+
import { dirname as dirname5, join as join8 } from "path";
|
|
3014
|
+
import { Hono as Hono4 } from "hono";
|
|
1768
3015
|
import {
|
|
1769
3016
|
getAgentDir as getAgentDir3
|
|
1770
3017
|
} from "@earendil-works/pi-coding-agent";
|
|
1771
|
-
var modelConfigsRoute = new
|
|
3018
|
+
var modelConfigsRoute = new Hono4();
|
|
1772
3019
|
var writeLock = Promise.resolve();
|
|
1773
3020
|
function withWriteLock(fn) {
|
|
1774
3021
|
const next = writeLock.then(fn, fn);
|
|
@@ -1778,23 +3025,23 @@ function withWriteLock(fn) {
|
|
|
1778
3025
|
return next;
|
|
1779
3026
|
}
|
|
1780
3027
|
function modelsPath() {
|
|
1781
|
-
return
|
|
3028
|
+
return join8(getAgentDir3(), "models.json");
|
|
1782
3029
|
}
|
|
1783
3030
|
async function readModelsJson() {
|
|
1784
3031
|
try {
|
|
1785
|
-
const raw = await
|
|
3032
|
+
const raw = await readFile5(modelsPath(), "utf-8");
|
|
1786
3033
|
return JSON.parse(raw);
|
|
1787
|
-
} catch (
|
|
1788
|
-
if (
|
|
3034
|
+
} catch (err2) {
|
|
3035
|
+
if (err2?.code === "ENOENT") {
|
|
1789
3036
|
return { providers: {} };
|
|
1790
3037
|
}
|
|
1791
|
-
throw
|
|
3038
|
+
throw err2;
|
|
1792
3039
|
}
|
|
1793
3040
|
}
|
|
1794
3041
|
async function writeModelsJson(config2) {
|
|
1795
3042
|
const p = modelsPath();
|
|
1796
|
-
await
|
|
1797
|
-
await
|
|
3043
|
+
await mkdir4(dirname5(p), { recursive: true });
|
|
3044
|
+
await writeFile4(p, JSON.stringify(config2, null, 2), "utf-8");
|
|
1798
3045
|
}
|
|
1799
3046
|
var ValidationError = class extends Error {
|
|
1800
3047
|
constructor(message, status) {
|
|
@@ -1819,8 +3066,8 @@ modelConfigsRoute.get("/", async (c) => {
|
|
|
1819
3066
|
const config2 = await readModelsJson();
|
|
1820
3067
|
const body = { config: config2 };
|
|
1821
3068
|
return c.json(body);
|
|
1822
|
-
} catch (
|
|
1823
|
-
const message =
|
|
3069
|
+
} catch (err2) {
|
|
3070
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
1824
3071
|
return c.json({ ok: false, error: message }, 500);
|
|
1825
3072
|
}
|
|
1826
3073
|
});
|
|
@@ -1837,8 +3084,8 @@ modelConfigsRoute.put("/", async (c) => {
|
|
|
1837
3084
|
refreshRegistry(workspaceId ?? void 0);
|
|
1838
3085
|
const resp = { config: body.config };
|
|
1839
3086
|
return c.json(resp);
|
|
1840
|
-
} catch (
|
|
1841
|
-
const message =
|
|
3087
|
+
} catch (err2) {
|
|
3088
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
1842
3089
|
return c.json({ ok: false, error: message }, 500);
|
|
1843
3090
|
}
|
|
1844
3091
|
});
|
|
@@ -1864,8 +3111,8 @@ modelConfigsRoute.post("/providers", async (c) => {
|
|
|
1864
3111
|
refreshRegistry(workspaceId ?? void 0);
|
|
1865
3112
|
const resp = { config: config2 };
|
|
1866
3113
|
return c.json(resp);
|
|
1867
|
-
} catch (
|
|
1868
|
-
const message =
|
|
3114
|
+
} catch (err2) {
|
|
3115
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
1869
3116
|
return c.json({ ok: false, error: message }, 500);
|
|
1870
3117
|
}
|
|
1871
3118
|
});
|
|
@@ -1888,11 +3135,11 @@ modelConfigsRoute.delete("/providers", async (c) => {
|
|
|
1888
3135
|
refreshRegistry(workspaceId ?? void 0);
|
|
1889
3136
|
const resp = { config: config2 };
|
|
1890
3137
|
return c.json(resp);
|
|
1891
|
-
} catch (
|
|
1892
|
-
if (
|
|
1893
|
-
return c.json({ ok: false, error:
|
|
3138
|
+
} catch (err2) {
|
|
3139
|
+
if (err2 instanceof ValidationError) {
|
|
3140
|
+
return c.json({ ok: false, error: err2.message }, err2.status);
|
|
1894
3141
|
}
|
|
1895
|
-
const message =
|
|
3142
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
1896
3143
|
return c.json({ ok: false, error: message }, 500);
|
|
1897
3144
|
}
|
|
1898
3145
|
});
|
|
@@ -1920,11 +3167,11 @@ modelConfigsRoute.post("/providers/:provider/models", async (c) => {
|
|
|
1920
3167
|
refreshRegistry(workspaceId ?? void 0);
|
|
1921
3168
|
const resp = { config: config2 };
|
|
1922
3169
|
return c.json(resp);
|
|
1923
|
-
} catch (
|
|
1924
|
-
if (
|
|
1925
|
-
return c.json({ ok: false, error:
|
|
3170
|
+
} catch (err2) {
|
|
3171
|
+
if (err2 instanceof ValidationError) {
|
|
3172
|
+
return c.json({ ok: false, error: err2.message }, err2.status);
|
|
1926
3173
|
}
|
|
1927
|
-
const message =
|
|
3174
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
1928
3175
|
return c.json({ ok: false, error: message }, 500);
|
|
1929
3176
|
}
|
|
1930
3177
|
});
|
|
@@ -1956,11 +3203,11 @@ modelConfigsRoute.put("/providers/:provider/models/:modelId", async (c) => {
|
|
|
1956
3203
|
refreshRegistry(workspaceId ?? void 0);
|
|
1957
3204
|
const resp = { config: config2 };
|
|
1958
3205
|
return c.json(resp);
|
|
1959
|
-
} catch (
|
|
1960
|
-
if (
|
|
1961
|
-
return c.json({ ok: false, error:
|
|
3206
|
+
} catch (err2) {
|
|
3207
|
+
if (err2 instanceof ValidationError) {
|
|
3208
|
+
return c.json({ ok: false, error: err2.message }, err2.status);
|
|
1962
3209
|
}
|
|
1963
|
-
const message =
|
|
3210
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
1964
3211
|
return c.json({ ok: false, error: message }, 500);
|
|
1965
3212
|
}
|
|
1966
3213
|
});
|
|
@@ -1985,149 +3232,18 @@ modelConfigsRoute.delete("/providers/:provider/models/:modelId", async (c) => {
|
|
|
1985
3232
|
refreshRegistry(workspaceId ?? void 0);
|
|
1986
3233
|
const resp = { config: config2 };
|
|
1987
3234
|
return c.json(resp);
|
|
1988
|
-
} catch (
|
|
1989
|
-
if (
|
|
1990
|
-
return c.json({ ok: false, error:
|
|
3235
|
+
} catch (err2) {
|
|
3236
|
+
if (err2 instanceof ValidationError) {
|
|
3237
|
+
return c.json({ ok: false, error: err2.message }, err2.status);
|
|
1991
3238
|
}
|
|
1992
|
-
const message =
|
|
3239
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
1993
3240
|
return c.json({ ok: false, error: message }, 500);
|
|
1994
3241
|
}
|
|
1995
3242
|
});
|
|
1996
3243
|
|
|
1997
3244
|
// src/ws/hub.ts
|
|
1998
3245
|
import { WebSocketServer } from "ws";
|
|
1999
|
-
|
|
2000
|
-
// src/ws/bridge.ts
|
|
2001
|
-
function translatePiEvent(ev) {
|
|
2002
|
-
switch (ev.type) {
|
|
2003
|
-
case "agent_start":
|
|
2004
|
-
return { kind: "agent_start" };
|
|
2005
|
-
case "agent_end":
|
|
2006
|
-
return { kind: "agent_end", willRetry: ev.willRetry };
|
|
2007
|
-
case "turn_start":
|
|
2008
|
-
return { kind: "turn_start" };
|
|
2009
|
-
case "turn_end":
|
|
2010
|
-
return { kind: "turn_end" };
|
|
2011
|
-
case "message_start": {
|
|
2012
|
-
const role = roleOf(ev.message);
|
|
2013
|
-
const text = role === "user" ? extractUserText2(ev.message) : void 0;
|
|
2014
|
-
return { kind: "message_start", role, text };
|
|
2015
|
-
}
|
|
2016
|
-
case "message_end":
|
|
2017
|
-
return { kind: "message_end", role: roleOf(ev.message) };
|
|
2018
|
-
case "message_update": {
|
|
2019
|
-
const ame = ev.assistantMessageEvent;
|
|
2020
|
-
if (ame.type === "text_delta") {
|
|
2021
|
-
return {
|
|
2022
|
-
kind: "message_update",
|
|
2023
|
-
delta: { kind: "text", contentIndex: ame.contentIndex, text: ame.delta }
|
|
2024
|
-
};
|
|
2025
|
-
}
|
|
2026
|
-
if (ame.type === "thinking_delta") {
|
|
2027
|
-
return {
|
|
2028
|
-
kind: "message_update",
|
|
2029
|
-
delta: { kind: "thinking", contentIndex: ame.contentIndex, text: ame.delta }
|
|
2030
|
-
};
|
|
2031
|
-
}
|
|
2032
|
-
return { kind: "message_update", delta: { kind: "other" } };
|
|
2033
|
-
}
|
|
2034
|
-
case "tool_execution_start":
|
|
2035
|
-
return {
|
|
2036
|
-
kind: "tool_execution_start",
|
|
2037
|
-
toolCallId: ev.toolCallId,
|
|
2038
|
-
toolName: ev.toolName,
|
|
2039
|
-
args: ev.args
|
|
2040
|
-
};
|
|
2041
|
-
case "tool_execution_update":
|
|
2042
|
-
return {
|
|
2043
|
-
kind: "tool_execution_update",
|
|
2044
|
-
toolCallId: ev.toolCallId,
|
|
2045
|
-
toolName: ev.toolName,
|
|
2046
|
-
partialText: extractText(ev.partialResult)
|
|
2047
|
-
};
|
|
2048
|
-
case "tool_execution_end":
|
|
2049
|
-
return {
|
|
2050
|
-
kind: "tool_execution_end",
|
|
2051
|
-
toolCallId: ev.toolCallId,
|
|
2052
|
-
toolName: ev.toolName,
|
|
2053
|
-
isError: ev.isError,
|
|
2054
|
-
text: extractText(ev.result)
|
|
2055
|
-
};
|
|
2056
|
-
case "queue_update":
|
|
2057
|
-
return {
|
|
2058
|
-
kind: "queue_update",
|
|
2059
|
-
steering: [...ev.steering],
|
|
2060
|
-
followUp: [...ev.followUp]
|
|
2061
|
-
};
|
|
2062
|
-
case "auto_retry_start":
|
|
2063
|
-
return {
|
|
2064
|
-
kind: "auto_retry_start",
|
|
2065
|
-
attempt: ev.attempt,
|
|
2066
|
-
maxAttempts: ev.maxAttempts,
|
|
2067
|
-
delayMs: ev.delayMs,
|
|
2068
|
-
errorMessage: ev.errorMessage
|
|
2069
|
-
};
|
|
2070
|
-
case "auto_retry_end":
|
|
2071
|
-
return {
|
|
2072
|
-
kind: "auto_retry_end",
|
|
2073
|
-
success: ev.success,
|
|
2074
|
-
attempt: ev.attempt,
|
|
2075
|
-
finalError: ev.finalError
|
|
2076
|
-
};
|
|
2077
|
-
case "compaction_start":
|
|
2078
|
-
return { kind: "compaction_start", reason: ev.reason };
|
|
2079
|
-
case "compaction_end":
|
|
2080
|
-
return {
|
|
2081
|
-
kind: "compaction_end",
|
|
2082
|
-
reason: ev.reason,
|
|
2083
|
-
aborted: ev.aborted,
|
|
2084
|
-
willRetry: ev.willRetry,
|
|
2085
|
-
errorMessage: ev.errorMessage
|
|
2086
|
-
};
|
|
2087
|
-
case "session_info_changed":
|
|
2088
|
-
return { kind: "session_info_changed", name: ev.name };
|
|
2089
|
-
case "thinking_level_changed":
|
|
2090
|
-
return { kind: "thinking_level_changed", level: ev.level };
|
|
2091
|
-
default:
|
|
2092
|
-
return void 0;
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
function roleOf(message) {
|
|
2096
|
-
const role = message?.role;
|
|
2097
|
-
if (role === "user" || role === "assistant" || role === "toolResult" || role === "bashExecution") {
|
|
2098
|
-
return role;
|
|
2099
|
-
}
|
|
2100
|
-
return "assistant";
|
|
2101
|
-
}
|
|
2102
|
-
function extractUserText2(message) {
|
|
2103
|
-
if (!message || typeof message !== "object") return void 0;
|
|
2104
|
-
const content = message.content;
|
|
2105
|
-
if (typeof content === "string") return content;
|
|
2106
|
-
if (!Array.isArray(content)) return void 0;
|
|
2107
|
-
const parts = [];
|
|
2108
|
-
for (const block of content) {
|
|
2109
|
-
if (block && typeof block === "object" && block.type === "text") {
|
|
2110
|
-
const text = block.text;
|
|
2111
|
-
if (typeof text === "string") parts.push(text);
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2114
|
-
return parts.length === 0 ? void 0 : parts.join("");
|
|
2115
|
-
}
|
|
2116
|
-
function extractText(result) {
|
|
2117
|
-
if (!result || typeof result !== "object") return void 0;
|
|
2118
|
-
const content = result.content;
|
|
2119
|
-
if (!Array.isArray(content)) return void 0;
|
|
2120
|
-
const parts = [];
|
|
2121
|
-
for (const c of content) {
|
|
2122
|
-
if (c && typeof c === "object" && c.type === "text") {
|
|
2123
|
-
const text = c.text;
|
|
2124
|
-
if (typeof text === "string") parts.push(text);
|
|
2125
|
-
}
|
|
2126
|
-
}
|
|
2127
|
-
return parts.length === 0 ? void 0 : parts.join("");
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
// src/ws/hub.ts
|
|
3246
|
+
var BACKGROUND_CAP = 4;
|
|
2131
3247
|
var replacementLocks = /* @__PURE__ */ new Map();
|
|
2132
3248
|
function withReplacementLock(workspaceId, fn) {
|
|
2133
3249
|
const prev = replacementLocks.get(workspaceId) ?? Promise.resolve();
|
|
@@ -2144,7 +3260,7 @@ function withReplacementLock(workspaceId, fn) {
|
|
|
2144
3260
|
function attachWsHub(httpServer) {
|
|
2145
3261
|
const wss = new WebSocketServer({ server: httpServer, path: "/ws" });
|
|
2146
3262
|
wss.on("connection", (ws) => {
|
|
2147
|
-
const state = {};
|
|
3263
|
+
const state = { background: /* @__PURE__ */ new Map() };
|
|
2148
3264
|
ws.on("message", async (raw) => {
|
|
2149
3265
|
let msg;
|
|
2150
3266
|
try {
|
|
@@ -2155,8 +3271,8 @@ function attachWsHub(httpServer) {
|
|
|
2155
3271
|
}
|
|
2156
3272
|
try {
|
|
2157
3273
|
await handle(ws, state, msg);
|
|
2158
|
-
} catch (
|
|
2159
|
-
const message =
|
|
3274
|
+
} catch (err2) {
|
|
3275
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
2160
3276
|
send(ws, { type: "error", message, command: msg.type });
|
|
2161
3277
|
}
|
|
2162
3278
|
});
|
|
@@ -2169,110 +3285,161 @@ function attachWsHub(httpServer) {
|
|
|
2169
3285
|
async function handle(ws, state, msg) {
|
|
2170
3286
|
switch (msg.type) {
|
|
2171
3287
|
case "subscribe": {
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
switched = await workspaceManager.switchSession(msg.workspaceId, msg.sessionPath);
|
|
2181
|
-
} catch (err) {
|
|
2182
|
-
switchError = err instanceof Error ? err.message : String(err);
|
|
2183
|
-
}
|
|
2184
|
-
});
|
|
2185
|
-
}
|
|
2186
|
-
if (!switched && !hadCurrentSubscription) {
|
|
2187
|
-
bindCurrentSession(ws, state, msg.workspaceId);
|
|
2188
|
-
}
|
|
2189
|
-
if (switchError) {
|
|
2190
|
-
send(ws, { type: "error", message: switchError, command: "subscribe" });
|
|
3288
|
+
let runtime;
|
|
3289
|
+
try {
|
|
3290
|
+
runtime = await workspaceManager.getOrCreate(msg.workspaceId, msg.sessionPath);
|
|
3291
|
+
} catch (err2) {
|
|
3292
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
3293
|
+
send(ws, { type: "error", message, command: "subscribe" });
|
|
3294
|
+
send(ws, { type: "ack", command: "subscribe" });
|
|
3295
|
+
return;
|
|
2191
3296
|
}
|
|
3297
|
+
promoteToPrimary(ws, state, msg.workspaceId, runtime);
|
|
2192
3298
|
send(ws, { type: "ack", command: "subscribe" });
|
|
2193
3299
|
return;
|
|
2194
3300
|
}
|
|
3301
|
+
case "unsubscribe": {
|
|
3302
|
+
for (const [key, bg] of [...state.background]) {
|
|
3303
|
+
if (bg.workspaceId === msg.workspaceId) teardownBackground(state, key, ws);
|
|
3304
|
+
}
|
|
3305
|
+
return;
|
|
3306
|
+
}
|
|
2195
3307
|
case "prompt": {
|
|
2196
|
-
const
|
|
2197
|
-
if (!
|
|
3308
|
+
const primary = state.primary;
|
|
3309
|
+
if (!primary) {
|
|
2198
3310
|
send(ws, { type: "error", message: "not subscribed", command: "prompt" });
|
|
2199
3311
|
return;
|
|
2200
3312
|
}
|
|
2201
|
-
if (replacementLocks.has(
|
|
3313
|
+
if (replacementLocks.has(primary.workspaceId)) {
|
|
2202
3314
|
send(ws, { type: "error", message: "session switching in progress", command: "prompt" });
|
|
2203
3315
|
return;
|
|
2204
3316
|
}
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
send(ws, { type: "error", message: "runtime gone", command: "prompt" });
|
|
2208
|
-
return;
|
|
2209
|
-
}
|
|
2210
|
-
void runtime.session.prompt(msg.message, {
|
|
2211
|
-
streamingBehavior: msg.streamingBehavior
|
|
2212
|
-
}).catch((err) => {
|
|
2213
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
3317
|
+
void primary.runtime.session.prompt(msg.message, { streamingBehavior: msg.streamingBehavior }).catch((err2) => {
|
|
3318
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
2214
3319
|
send(ws, { type: "error", message, command: "prompt" });
|
|
2215
3320
|
});
|
|
2216
3321
|
return;
|
|
2217
3322
|
}
|
|
2218
3323
|
case "abort": {
|
|
2219
|
-
const
|
|
2220
|
-
if (!
|
|
3324
|
+
const primary = state.primary;
|
|
3325
|
+
if (!primary) {
|
|
2221
3326
|
send(ws, { type: "error", message: "not subscribed", command: "abort" });
|
|
2222
3327
|
return;
|
|
2223
3328
|
}
|
|
2224
|
-
|
|
2225
|
-
send(ws, { type: "error", message: "session switching in progress", command: "abort" });
|
|
2226
|
-
return;
|
|
2227
|
-
}
|
|
2228
|
-
const runtime = workspaceManager.get(wsId);
|
|
2229
|
-
if (!runtime) return;
|
|
2230
|
-
await runtime.session.abort();
|
|
3329
|
+
await primary.runtime.session.abort();
|
|
2231
3330
|
return;
|
|
2232
3331
|
}
|
|
2233
3332
|
case "new_session": {
|
|
2234
|
-
const
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
send(ws, { type: "error", message: "runtime gone", command: "new_session" });
|
|
2243
|
-
return;
|
|
2244
|
-
}
|
|
2245
|
-
if (runtime.session.isStreaming) {
|
|
2246
|
-
send(ws, { type: "error", message: "cannot create session while streaming", command: "new_session" });
|
|
3333
|
+
const workspaceId = msg.workspaceId;
|
|
3334
|
+
await withReplacementLock(workspaceId, async () => {
|
|
3335
|
+
let runtime;
|
|
3336
|
+
try {
|
|
3337
|
+
runtime = await workspaceManager.createSession(workspaceId);
|
|
3338
|
+
} catch (err2) {
|
|
3339
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
3340
|
+
send(ws, { type: "error", message, command: "new_session" });
|
|
2247
3341
|
return;
|
|
2248
3342
|
}
|
|
2249
|
-
|
|
2250
|
-
if (result.cancelled) {
|
|
2251
|
-
send(ws, { type: "error", message: "new session cancelled", command: "new_session" });
|
|
2252
|
-
}
|
|
3343
|
+
promoteToPrimary(ws, state, workspaceId, runtime);
|
|
2253
3344
|
});
|
|
2254
3345
|
return;
|
|
2255
3346
|
}
|
|
2256
3347
|
case "fork": {
|
|
2257
|
-
const
|
|
2258
|
-
if (!
|
|
3348
|
+
const primary = state.primary;
|
|
3349
|
+
if (!primary) {
|
|
2259
3350
|
send(ws, { type: "error", message: "not subscribed", command: "fork" });
|
|
2260
3351
|
return;
|
|
2261
3352
|
}
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
3353
|
+
const workspaceId = primary.workspaceId;
|
|
3354
|
+
await withReplacementLock(workspaceId, async () => {
|
|
3355
|
+
const source = state.primary;
|
|
3356
|
+
if (!source) {
|
|
3357
|
+
send(ws, { type: "error", message: "not subscribed", command: "fork" });
|
|
2266
3358
|
return;
|
|
2267
3359
|
}
|
|
2268
|
-
if (
|
|
3360
|
+
if (!source.sessionPath) {
|
|
3361
|
+
send(ws, { type: "error", message: "cannot fork an unsaved session", command: "fork" });
|
|
3362
|
+
return;
|
|
3363
|
+
}
|
|
3364
|
+
if (source.runtime.session.isStreaming) {
|
|
2269
3365
|
send(ws, { type: "error", message: "cannot fork while streaming", command: "fork" });
|
|
2270
3366
|
return;
|
|
2271
3367
|
}
|
|
2272
|
-
|
|
2273
|
-
|
|
3368
|
+
let result;
|
|
3369
|
+
try {
|
|
3370
|
+
result = await workspaceManager.fork(workspaceId, source.sessionPath, msg.entryId);
|
|
3371
|
+
} catch (err2) {
|
|
3372
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
3373
|
+
send(ws, { type: "error", message, command: "fork" });
|
|
3374
|
+
return;
|
|
3375
|
+
}
|
|
3376
|
+
if (result.cancelled || !result.runtime) {
|
|
2274
3377
|
send(ws, { type: "error", message: "fork cancelled", command: "fork" });
|
|
3378
|
+
return;
|
|
3379
|
+
}
|
|
3380
|
+
promoteToPrimary(ws, state, workspaceId, result.runtime);
|
|
3381
|
+
});
|
|
3382
|
+
return;
|
|
3383
|
+
}
|
|
3384
|
+
case "answer_question": {
|
|
3385
|
+
const primary = state.primary;
|
|
3386
|
+
if (!primary) {
|
|
3387
|
+
send(ws, { type: "error", message: "not subscribed", command: "answer_question" });
|
|
3388
|
+
return;
|
|
3389
|
+
}
|
|
3390
|
+
resolveAnswer(msg.toolCallId, msg.answer, primary.runtime.session.sessionFile ?? null);
|
|
3391
|
+
return;
|
|
3392
|
+
}
|
|
3393
|
+
case "navigate_tree": {
|
|
3394
|
+
const primary = state.primary;
|
|
3395
|
+
if (!primary) {
|
|
3396
|
+
send(ws, { type: "error", message: "not subscribed", command: "navigate_tree" });
|
|
3397
|
+
return;
|
|
3398
|
+
}
|
|
3399
|
+
if (msg.workspaceId !== primary.workspaceId) {
|
|
3400
|
+
send(ws, { type: "error", message: "workspace mismatch", command: "navigate_tree" });
|
|
3401
|
+
return;
|
|
3402
|
+
}
|
|
3403
|
+
await withReplacementLock(primary.workspaceId, async () => {
|
|
3404
|
+
const current = state.primary;
|
|
3405
|
+
if (!current) {
|
|
3406
|
+
send(ws, { type: "error", message: "not subscribed", command: "navigate_tree" });
|
|
3407
|
+
return;
|
|
3408
|
+
}
|
|
3409
|
+
if (current.runtime.session.isStreaming) {
|
|
3410
|
+
send(ws, { type: "error", message: "cannot navigate tree while streaming", command: "navigate_tree" });
|
|
3411
|
+
return;
|
|
2275
3412
|
}
|
|
3413
|
+
const result = await current.runtime.session.navigateTree(msg.targetId, {
|
|
3414
|
+
summarize: msg.summarize,
|
|
3415
|
+
customInstructions: msg.customInstructions
|
|
3416
|
+
});
|
|
3417
|
+
send(ws, {
|
|
3418
|
+
type: "navigate_tree_result",
|
|
3419
|
+
workspaceId: current.workspaceId,
|
|
3420
|
+
editorText: result.editorText,
|
|
3421
|
+
cancelled: result.cancelled
|
|
3422
|
+
});
|
|
3423
|
+
});
|
|
3424
|
+
return;
|
|
3425
|
+
}
|
|
3426
|
+
case "compact": {
|
|
3427
|
+
const primary = state.primary;
|
|
3428
|
+
if (!primary) {
|
|
3429
|
+
send(ws, { type: "error", message: "not subscribed", command: "compact" });
|
|
3430
|
+
return;
|
|
3431
|
+
}
|
|
3432
|
+
if (primary.runtime.session.isStreaming) {
|
|
3433
|
+
send(ws, { type: "error", message: "cannot compact while streaming", command: "compact" });
|
|
3434
|
+
return;
|
|
3435
|
+
}
|
|
3436
|
+
if (primary.runtime.session.isCompacting) {
|
|
3437
|
+
send(ws, { type: "error", message: "compaction already in progress", command: "compact" });
|
|
3438
|
+
return;
|
|
3439
|
+
}
|
|
3440
|
+
primary.runtime.session.compact().catch((err2) => {
|
|
3441
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
3442
|
+
send(ws, { type: "error", message, command: "compact" });
|
|
2276
3443
|
});
|
|
2277
3444
|
return;
|
|
2278
3445
|
}
|
|
@@ -2283,28 +3450,26 @@ async function handle(ws, state, msg) {
|
|
|
2283
3450
|
}
|
|
2284
3451
|
}
|
|
2285
3452
|
}
|
|
2286
|
-
function
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
state.unsubscribeRebind = workspaceManager.onSessionReplaced(workspaceId, () => {
|
|
2292
|
-
if (state.workspaceId !== workspaceId) return;
|
|
2293
|
-
bindCurrentSession(ws, state, workspaceId);
|
|
2294
|
-
});
|
|
2295
|
-
}
|
|
2296
|
-
function bindCurrentSession(ws, state, workspaceId) {
|
|
2297
|
-
const runtime = workspaceManager.get(workspaceId);
|
|
2298
|
-
if (!runtime) {
|
|
2299
|
-
send(ws, { type: "error", message: "runtime gone", command: "subscribe" });
|
|
3453
|
+
function promoteToPrimary(ws, state, workspaceId, runtime) {
|
|
3454
|
+
const runtimeKey = workspaceManager.runtimeKeyFor(workspaceId, runtime);
|
|
3455
|
+
if (state.primary?.runtimeKey === runtimeKey) {
|
|
3456
|
+
workspaceManager.setActive(workspaceId, runtime);
|
|
3457
|
+
sendSubscribed(ws, workspaceId, runtime);
|
|
2300
3458
|
return;
|
|
2301
3459
|
}
|
|
2302
|
-
state.
|
|
3460
|
+
if (state.primary) demotePrimaryToBackground(ws, state);
|
|
3461
|
+
if (state.background.has(runtimeKey)) teardownBackground(state, runtimeKey, ws);
|
|
3462
|
+
bindPrimary(ws, state, workspaceId, runtime);
|
|
3463
|
+
}
|
|
3464
|
+
function bindPrimary(ws, state, workspaceId, runtime) {
|
|
3465
|
+
workspaceManager.addSubscriber(workspaceId, ws);
|
|
3466
|
+
workspaceManager.setActive(workspaceId, runtime);
|
|
2303
3467
|
const session = runtime.session;
|
|
2304
3468
|
const sessionPath = session.sessionFile ?? null;
|
|
3469
|
+
const runtimeKey = workspaceManager.runtimeKeyFor(workspaceId, runtime);
|
|
2305
3470
|
let assistantStartAt;
|
|
2306
3471
|
let assistantFirstTokenAt;
|
|
2307
|
-
|
|
3472
|
+
const unsubscribe = session.subscribe((ev) => {
|
|
2308
3473
|
const payload = translatePiEvent(ev);
|
|
2309
3474
|
if (!payload) return;
|
|
2310
3475
|
if (payload.kind === "message_start" && payload.role === "assistant") {
|
|
@@ -2313,12 +3478,7 @@ function bindCurrentSession(ws, state, workspaceId) {
|
|
|
2313
3478
|
} else if (payload.kind === "message_update" && payload.delta.kind === "text" && assistantStartAt !== void 0 && assistantFirstTokenAt === void 0) {
|
|
2314
3479
|
assistantFirstTokenAt = performance.now();
|
|
2315
3480
|
}
|
|
2316
|
-
send(ws, {
|
|
2317
|
-
type: "event",
|
|
2318
|
-
workspaceId,
|
|
2319
|
-
sessionPath,
|
|
2320
|
-
payload
|
|
2321
|
-
});
|
|
3481
|
+
send(ws, { type: "event", workspaceId, sessionPath, payload });
|
|
2322
3482
|
if (payload.kind === "message_end" && payload.role === "assistant" && assistantStartAt !== void 0) {
|
|
2323
3483
|
const now = performance.now();
|
|
2324
3484
|
const timing = {
|
|
@@ -2326,12 +3486,7 @@ function bindCurrentSession(ws, state, workspaceId) {
|
|
|
2326
3486
|
firstTokenMs: assistantFirstTokenAt !== void 0 ? Math.round(assistantFirstTokenAt - assistantStartAt) : null,
|
|
2327
3487
|
totalMs: Math.round(now - assistantStartAt)
|
|
2328
3488
|
};
|
|
2329
|
-
send(ws, {
|
|
2330
|
-
type: "event",
|
|
2331
|
-
workspaceId,
|
|
2332
|
-
sessionPath,
|
|
2333
|
-
payload: timing
|
|
2334
|
-
});
|
|
3489
|
+
send(ws, { type: "event", workspaceId, sessionPath, payload: timing });
|
|
2335
3490
|
assistantStartAt = void 0;
|
|
2336
3491
|
assistantFirstTokenAt = void 0;
|
|
2337
3492
|
}
|
|
@@ -2339,13 +3494,79 @@ function bindCurrentSession(ws, state, workspaceId) {
|
|
|
2339
3494
|
sendContextUsage(ws, runtime, workspaceId, sessionPath);
|
|
2340
3495
|
}
|
|
2341
3496
|
});
|
|
3497
|
+
state.primary = { runtimeKey, workspaceId, sessionPath, runtime, unsubscribe };
|
|
3498
|
+
sendSubscribed(ws, workspaceId, runtime);
|
|
3499
|
+
const streamingMessage = runtime.session.state.streamingMessage;
|
|
3500
|
+
const scanMessages = streamingMessage ? [...runtime.session.messages, streamingMessage] : runtime.session.messages;
|
|
3501
|
+
for (const payload of inFlightRunningToolsSnapshot(
|
|
3502
|
+
runtime.session.state.pendingToolCalls,
|
|
3503
|
+
scanMessages
|
|
3504
|
+
)) {
|
|
3505
|
+
send(ws, { type: "event", workspaceId, sessionPath, payload });
|
|
3506
|
+
}
|
|
3507
|
+
const inFlight = inFlightAssistantSnapshot(streamingMessage);
|
|
3508
|
+
if (inFlight) {
|
|
3509
|
+
for (const payload of inFlight) {
|
|
3510
|
+
send(ws, { type: "event", workspaceId, sessionPath, payload });
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
for (const payload of inFlightToolCallsSnapshot(sessionPath)) {
|
|
3514
|
+
send(ws, { type: "event", workspaceId, sessionPath, payload });
|
|
3515
|
+
}
|
|
3516
|
+
sendContextUsage(ws, runtime, workspaceId, sessionPath);
|
|
3517
|
+
}
|
|
3518
|
+
function demotePrimaryToBackground(ws, state) {
|
|
3519
|
+
const primary = state.primary;
|
|
3520
|
+
if (!primary) return;
|
|
3521
|
+
primary.unsubscribe();
|
|
3522
|
+
state.primary = void 0;
|
|
3523
|
+
const session = primary.runtime.session;
|
|
3524
|
+
const sessionPath = primary.sessionPath;
|
|
3525
|
+
const unsubscribeSession = session.subscribe((ev) => {
|
|
3526
|
+
const payload = translatePiEvent(ev);
|
|
3527
|
+
if (!payload) return;
|
|
3528
|
+
send(ws, { type: "event", workspaceId: primary.workspaceId, sessionPath, payload });
|
|
3529
|
+
if (payload.kind === "agent_end" || payload.kind === "compaction_end" || payload.kind === "session_info_changed" || payload.kind === "thinking_level_changed") {
|
|
3530
|
+
sendContextUsage(ws, primary.runtime, primary.workspaceId, sessionPath);
|
|
3531
|
+
}
|
|
3532
|
+
});
|
|
3533
|
+
state.background.set(primary.runtimeKey, {
|
|
3534
|
+
workspaceId: primary.workspaceId,
|
|
3535
|
+
sessionPath,
|
|
3536
|
+
unsubscribeSession
|
|
3537
|
+
});
|
|
3538
|
+
while (state.background.size > BACKGROUND_CAP) {
|
|
3539
|
+
const oldestKey = state.background.keys().next().value;
|
|
3540
|
+
if (oldestKey === void 0) break;
|
|
3541
|
+
const evicted = state.background.get(oldestKey);
|
|
3542
|
+
teardownBackground(state, oldestKey, ws);
|
|
3543
|
+
if (evicted) {
|
|
3544
|
+
send(ws, {
|
|
3545
|
+
type: "background_evicted",
|
|
3546
|
+
workspaceId: evicted.workspaceId,
|
|
3547
|
+
sessionPath: evicted.sessionPath
|
|
3548
|
+
});
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
function teardownBackground(state, runtimeKey, ws) {
|
|
3553
|
+
const bg = state.background.get(runtimeKey);
|
|
3554
|
+
if (!bg) return;
|
|
3555
|
+
bg.unsubscribeSession();
|
|
3556
|
+
state.background.delete(runtimeKey);
|
|
3557
|
+
if (ws) unrefWorkspaceSubscriber(state, bg.workspaceId, ws);
|
|
3558
|
+
}
|
|
3559
|
+
function sendSubscribed(ws, workspaceId, runtime) {
|
|
2342
3560
|
send(ws, {
|
|
2343
3561
|
type: "subscribed",
|
|
2344
3562
|
workspaceId,
|
|
2345
|
-
sessionPath,
|
|
2346
|
-
sessionId: session.sessionId
|
|
3563
|
+
sessionPath: runtime.session.sessionFile ?? null,
|
|
3564
|
+
sessionId: runtime.session.sessionId
|
|
2347
3565
|
});
|
|
2348
|
-
|
|
3566
|
+
}
|
|
3567
|
+
function unrefWorkspaceSubscriber(state, workspaceId, ws) {
|
|
3568
|
+
const stillUsed = state.primary?.workspaceId === workspaceId || [...state.background.values()].some((b) => b.workspaceId === workspaceId);
|
|
3569
|
+
if (!stillUsed) workspaceManager.removeSubscriber(workspaceId, ws);
|
|
2349
3570
|
}
|
|
2350
3571
|
function sendContextUsage(ws, runtime, workspaceId, sessionPath) {
|
|
2351
3572
|
const usage = runtime.session.getContextUsage();
|
|
@@ -2356,22 +3577,20 @@ function sendContextUsage(ws, runtime, workspaceId, sessionPath) {
|
|
|
2356
3577
|
contextWindow: usage.contextWindow,
|
|
2357
3578
|
percent: usage.percent
|
|
2358
3579
|
};
|
|
2359
|
-
send(ws, {
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
3580
|
+
send(ws, { type: "event", workspaceId, sessionPath, payload });
|
|
3581
|
+
}
|
|
3582
|
+
function detachPrimary(state, ws) {
|
|
3583
|
+
const primary = state.primary;
|
|
3584
|
+
if (!primary) return;
|
|
3585
|
+
primary.unsubscribe();
|
|
3586
|
+
state.primary = void 0;
|
|
3587
|
+
if (ws) unrefWorkspaceSubscriber(state, primary.workspaceId, ws);
|
|
2365
3588
|
}
|
|
2366
3589
|
function detach(state, ws) {
|
|
2367
|
-
state
|
|
2368
|
-
state.
|
|
2369
|
-
|
|
2370
|
-
state.unsubscribeRebind = void 0;
|
|
2371
|
-
if (state.workspaceId && ws) {
|
|
2372
|
-
workspaceManager.removeSubscriber(state.workspaceId, ws);
|
|
3590
|
+
detachPrimary(state, ws);
|
|
3591
|
+
for (const runtimeKey of [...state.background.keys()]) {
|
|
3592
|
+
teardownBackground(state, runtimeKey, ws);
|
|
2373
3593
|
}
|
|
2374
|
-
state.workspaceId = void 0;
|
|
2375
3594
|
}
|
|
2376
3595
|
function send(ws, msg) {
|
|
2377
3596
|
if (ws.readyState !== ws.OPEN) return;
|
|
@@ -2379,10 +3598,11 @@ function send(ws, msg) {
|
|
|
2379
3598
|
}
|
|
2380
3599
|
|
|
2381
3600
|
// src/index.ts
|
|
2382
|
-
|
|
2383
|
-
var
|
|
2384
|
-
var
|
|
2385
|
-
var
|
|
3601
|
+
configureHttpProxy();
|
|
3602
|
+
var app = new Hono5();
|
|
3603
|
+
var distDir = dirname6(fileURLToPath(import.meta.url));
|
|
3604
|
+
var webRoot = resolve5(process.env.PI_PILOT_WEB_ROOT ?? join9(distDir, "..", "public"));
|
|
3605
|
+
var webIndexPath = join9(webRoot, "index.html");
|
|
2386
3606
|
var mimeTypes = {
|
|
2387
3607
|
".css": "text/css; charset=utf-8",
|
|
2388
3608
|
".html": "text/html; charset=utf-8",
|
|
@@ -2416,11 +3636,11 @@ function safeResolveWebPath(pathname) {
|
|
|
2416
3636
|
}
|
|
2417
3637
|
async function readWebFile(path) {
|
|
2418
3638
|
try {
|
|
2419
|
-
return await
|
|
2420
|
-
} catch (
|
|
2421
|
-
const code =
|
|
3639
|
+
return await readFile6(path);
|
|
3640
|
+
} catch (err2) {
|
|
3641
|
+
const code = err2.code;
|
|
2422
3642
|
if (code === "ENOENT" || code === "EISDIR") return void 0;
|
|
2423
|
-
throw
|
|
3643
|
+
throw err2;
|
|
2424
3644
|
}
|
|
2425
3645
|
}
|
|
2426
3646
|
async function serveWeb(c) {
|
|
@@ -2429,7 +3649,7 @@ async function serveWeb(c) {
|
|
|
2429
3649
|
const assetPath = safeResolveWebPath(pathname);
|
|
2430
3650
|
if (!assetPath) return c.text("invalid asset path", 400);
|
|
2431
3651
|
const asset = await readWebFile(assetPath);
|
|
2432
|
-
const body = asset ?? await
|
|
3652
|
+
const body = asset ?? await readFile6(webIndexPath);
|
|
2433
3653
|
const filePath = asset ? assetPath : webIndexPath;
|
|
2434
3654
|
const headers = {
|
|
2435
3655
|
"Content-Type": mimeTypes[extname(filePath)] ?? "application/octet-stream",
|
|
@@ -2459,6 +3679,7 @@ if (existsSync(webIndexPath)) {
|
|
|
2459
3679
|
)
|
|
2460
3680
|
);
|
|
2461
3681
|
}
|
|
3682
|
+
await loadBuiltinPrefs();
|
|
2462
3683
|
var server = serve(
|
|
2463
3684
|
{
|
|
2464
3685
|
fetch: app.fetch,
|