@jyork0828/pi-pilot 0.0.5 → 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 +1208 -463
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/public/assets/index-CsC5-YPT.js +506 -0
- package/public/assets/index-R8FKUxOS.css +1 -0
- package/public/index.html +2 -2
- package/public/assets/index-CBa7EReb.js +0 -411
- package/public/assets/index-DeSNeuE1.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
|
|
5
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
6
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
|
|
@@ -41,9 +41,9 @@ function configureHttpProxy() {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// src/api/workspaces.ts
|
|
44
|
-
import { stat as stat2 } from "fs/promises";
|
|
44
|
+
import { readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
45
45
|
import { basename as basename2, isAbsolute as isAbsolute3, resolve as resolve3 } from "path";
|
|
46
|
-
import { Hono } from "hono";
|
|
46
|
+
import { Hono as Hono2 } from "hono";
|
|
47
47
|
|
|
48
48
|
// src/storage/resource-writer.ts
|
|
49
49
|
import {
|
|
@@ -277,9 +277,9 @@ async function updatePrompt(opts) {
|
|
|
277
277
|
await writeFile(newPath, text, "utf8");
|
|
278
278
|
try {
|
|
279
279
|
await unlink(opts.filePath);
|
|
280
|
-
} catch (
|
|
280
|
+
} catch (err2) {
|
|
281
281
|
await unlink(newPath).catch(() => void 0);
|
|
282
|
-
throw
|
|
282
|
+
throw err2;
|
|
283
283
|
}
|
|
284
284
|
return newPath;
|
|
285
285
|
}
|
|
@@ -351,17 +351,22 @@ import { dirname as dirname2, join as join3 } from "path";
|
|
|
351
351
|
import { randomUUID } from "crypto";
|
|
352
352
|
var REGISTRY_PATH = join3(config.dataDir, "workspaces.json");
|
|
353
353
|
var cache;
|
|
354
|
+
var writeChain = Promise.resolve();
|
|
355
|
+
function serializedWrite(fn) {
|
|
356
|
+
writeChain = writeChain.then(fn, fn);
|
|
357
|
+
return writeChain;
|
|
358
|
+
}
|
|
354
359
|
async function load() {
|
|
355
360
|
if (cache) return cache;
|
|
356
361
|
try {
|
|
357
362
|
const raw = await readFile2(REGISTRY_PATH, "utf8");
|
|
358
363
|
cache = JSON.parse(raw);
|
|
359
364
|
if (!Array.isArray(cache.workspaces)) cache = { workspaces: [] };
|
|
360
|
-
} catch (
|
|
361
|
-
if (
|
|
365
|
+
} catch (err2) {
|
|
366
|
+
if (err2.code === "ENOENT") {
|
|
362
367
|
cache = { workspaces: [] };
|
|
363
368
|
} else {
|
|
364
|
-
throw
|
|
369
|
+
throw err2;
|
|
365
370
|
}
|
|
366
371
|
}
|
|
367
372
|
return cache;
|
|
@@ -380,26 +385,57 @@ async function getWorkspace(id) {
|
|
|
380
385
|
return r.workspaces.find((w) => w.id === id);
|
|
381
386
|
}
|
|
382
387
|
async function addWorkspace(input) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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;
|
|
395
407
|
}
|
|
396
408
|
async function removeWorkspace(id) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
+
});
|
|
403
439
|
}
|
|
404
440
|
|
|
405
441
|
// src/storage/workspace-stats.ts
|
|
@@ -502,57 +538,232 @@ import {
|
|
|
502
538
|
SessionManager
|
|
503
539
|
} from "@earendil-works/pi-coding-agent";
|
|
504
540
|
|
|
505
|
-
// src/extensions/
|
|
541
|
+
// src/extensions/todo/schema.ts
|
|
506
542
|
import { Type } from "typebox";
|
|
507
|
-
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([
|
|
508
563
|
Type.Literal("pending"),
|
|
509
564
|
Type.Literal("in_progress"),
|
|
510
|
-
Type.Literal("completed")
|
|
565
|
+
Type.Literal("completed"),
|
|
566
|
+
Type.Literal("deleted")
|
|
511
567
|
]);
|
|
512
|
-
var
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
})
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
})
|
|
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
|
+
}))
|
|
522
577
|
});
|
|
523
578
|
|
|
524
|
-
// src/extensions/
|
|
525
|
-
|
|
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 };
|
|
526
692
|
pi.registerTool({
|
|
527
|
-
name:
|
|
528
|
-
label: "
|
|
529
|
-
description: "
|
|
530
|
-
|
|
531
|
-
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.",
|
|
532
697
|
promptGuidelines: [
|
|
533
|
-
"
|
|
534
|
-
"
|
|
535
|
-
|
|
536
|
-
"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."
|
|
537
701
|
],
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
+
}
|
|
547
717
|
});
|
|
548
|
-
pi.registerCommand("
|
|
549
|
-
description: "
|
|
550
|
-
handler: async (
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
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")}`);
|
|
554
747
|
}
|
|
555
748
|
});
|
|
749
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
750
|
+
state = replayFromBranch(ctx);
|
|
751
|
+
});
|
|
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 {
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
pi.on("session_shutdown", async () => {
|
|
765
|
+
state = { tasks: [...EMPTY_STATE.tasks], nextId: EMPTY_STATE.nextId };
|
|
766
|
+
});
|
|
556
767
|
};
|
|
557
768
|
|
|
558
769
|
// src/extensions/ask_user/schema.ts
|
|
@@ -616,13 +827,6 @@ function resolveAnswer(toolCallId, answer, expectedSessionFile) {
|
|
|
616
827
|
entry.resolve(answer);
|
|
617
828
|
return true;
|
|
618
829
|
}
|
|
619
|
-
function cancelPendingExcept(keepSessionFile) {
|
|
620
|
-
for (const [id, entry] of pending) {
|
|
621
|
-
if (entry.sessionFile === keepSessionFile) continue;
|
|
622
|
-
pending.delete(id);
|
|
623
|
-
entry.reject(new Error("Session replaced before answer arrived"));
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
830
|
function cancelPendingForSession(sessionFile) {
|
|
627
831
|
for (const [id, entry] of pending) {
|
|
628
832
|
if (entry.sessionFile !== sessionFile) continue;
|
|
@@ -687,11 +891,11 @@ function waitForAnswer({
|
|
|
687
891
|
cleanup();
|
|
688
892
|
resolve6(a);
|
|
689
893
|
};
|
|
690
|
-
const finishErr = (
|
|
894
|
+
const finishErr = (err2) => {
|
|
691
895
|
if (settled) return;
|
|
692
896
|
settled = true;
|
|
693
897
|
cleanup();
|
|
694
|
-
reject(
|
|
898
|
+
reject(err2);
|
|
695
899
|
};
|
|
696
900
|
const onAbort = () => finishErr(new Error("Aborted by user"));
|
|
697
901
|
if (signal?.aborted) {
|
|
@@ -709,7 +913,7 @@ function waitForAnswer({
|
|
|
709
913
|
args: params,
|
|
710
914
|
sessionFile,
|
|
711
915
|
resolve: (answer) => finishOk(answer),
|
|
712
|
-
reject: (
|
|
916
|
+
reject: (err2) => finishErr(err2)
|
|
713
917
|
});
|
|
714
918
|
});
|
|
715
919
|
}
|
|
@@ -759,6 +963,70 @@ function descriptionSuffix(params, index) {
|
|
|
759
963
|
return desc ? ` \u2014 ${desc}` : "";
|
|
760
964
|
}
|
|
761
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
|
+
|
|
762
1030
|
// src/storage/builtin-extension-prefs.ts
|
|
763
1031
|
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
764
1032
|
import { dirname as dirname3, join as join4 } from "path";
|
|
@@ -769,15 +1037,17 @@ async function loadBuiltinPrefs() {
|
|
|
769
1037
|
const raw = await readFile3(PREFS_PATH, "utf8");
|
|
770
1038
|
const parsed = JSON.parse(raw);
|
|
771
1039
|
cache3 = { disabled: Array.isArray(parsed.disabled) ? parsed.disabled : [] };
|
|
772
|
-
} catch (
|
|
1040
|
+
} catch (err2) {
|
|
773
1041
|
cache3 = { disabled: [] };
|
|
774
|
-
if (
|
|
775
|
-
console.warn(`[builtin-prefs] ignoring unreadable ${PREFS_PATH}:`,
|
|
1042
|
+
if (err2.code !== "ENOENT") {
|
|
1043
|
+
console.warn(`[builtin-prefs] ignoring unreadable ${PREFS_PATH}:`, err2);
|
|
776
1044
|
}
|
|
777
1045
|
}
|
|
778
1046
|
}
|
|
779
1047
|
function isBuiltinDisabled(id) {
|
|
780
|
-
|
|
1048
|
+
if (cache3.disabled.includes(id)) return true;
|
|
1049
|
+
if (id === "todo" && cache3.disabled.includes("plan")) return true;
|
|
1050
|
+
return false;
|
|
781
1051
|
}
|
|
782
1052
|
function getDisabledBuiltins() {
|
|
783
1053
|
return [...cache3.disabled];
|
|
@@ -797,12 +1067,12 @@ async function save2() {
|
|
|
797
1067
|
// src/extensions/index.ts
|
|
798
1068
|
var BUILTIN_EXTENSIONS = [
|
|
799
1069
|
{
|
|
800
|
-
id: "
|
|
801
|
-
name: "
|
|
802
|
-
description: "A
|
|
803
|
-
tools: ["
|
|
804
|
-
commands: ["
|
|
805
|
-
factory:
|
|
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
|
|
806
1076
|
},
|
|
807
1077
|
{
|
|
808
1078
|
id: "ask_user",
|
|
@@ -811,6 +1081,14 @@ var BUILTIN_EXTENSIONS = [
|
|
|
811
1081
|
tools: ["ask_user"],
|
|
812
1082
|
commands: [],
|
|
813
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
|
|
814
1092
|
}
|
|
815
1093
|
];
|
|
816
1094
|
function gate(def) {
|
|
@@ -1036,6 +1314,37 @@ function inFlightAssistantSnapshot(streamingMessage) {
|
|
|
1036
1314
|
}
|
|
1037
1315
|
return events;
|
|
1038
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
|
+
}
|
|
1039
1348
|
function inFlightToolCallsSnapshot(sessionFile) {
|
|
1040
1349
|
const pending2 = snapshotForSession(sessionFile);
|
|
1041
1350
|
return pending2.map((p) => ({
|
|
@@ -1045,7 +1354,7 @@ function inFlightToolCallsSnapshot(sessionFile) {
|
|
|
1045
1354
|
args: p.args
|
|
1046
1355
|
}));
|
|
1047
1356
|
}
|
|
1048
|
-
var DETAILS_FORWARD_WHITELIST = /* @__PURE__ */ new Set(["ask_user"]);
|
|
1357
|
+
var DETAILS_FORWARD_WHITELIST = /* @__PURE__ */ new Set(["ask_user", "todo"]);
|
|
1049
1358
|
function shouldForwardDetails(toolName) {
|
|
1050
1359
|
return DETAILS_FORWARD_WHITELIST.has(toolName);
|
|
1051
1360
|
}
|
|
@@ -1146,6 +1455,7 @@ var ExtensionUIBridge = class {
|
|
|
1146
1455
|
|
|
1147
1456
|
// src/workspace-manager.ts
|
|
1148
1457
|
var EXTENSIONS_ENABLED = process.env.PI_PILOT_ENABLE_EXTENSIONS === "1";
|
|
1458
|
+
var MAX_LIVE_RUNTIMES = 12;
|
|
1149
1459
|
var createRuntime = async ({
|
|
1150
1460
|
cwd,
|
|
1151
1461
|
sessionManager,
|
|
@@ -1169,18 +1479,36 @@ var createRuntime = async ({
|
|
|
1169
1479
|
diagnostics: services.diagnostics
|
|
1170
1480
|
};
|
|
1171
1481
|
};
|
|
1172
|
-
var
|
|
1173
|
-
|
|
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();
|
|
1174
1490
|
/**
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
1177
|
-
*
|
|
1178
|
-
*
|
|
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).
|
|
1179
1496
|
*/
|
|
1180
1497
|
subscribers = /* @__PURE__ */ new Map();
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
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
|
+
}
|
|
1184
1512
|
getOrCreateSubscriberSet(workspaceId) {
|
|
1185
1513
|
let set = this.subscribers.get(workspaceId);
|
|
1186
1514
|
if (!set) {
|
|
@@ -1189,64 +1517,186 @@ var WorkspaceManager = class {
|
|
|
1189
1517
|
}
|
|
1190
1518
|
return set;
|
|
1191
1519
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
try {
|
|
1200
|
-
const state = await p;
|
|
1201
|
-
this.states.set(workspaceId, state);
|
|
1202
|
-
return state.runtime;
|
|
1203
|
-
} finally {
|
|
1204
|
-
this.pending.delete(workspaceId);
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
async build(workspaceId) {
|
|
1208
|
-
const ws = await getWorkspace(workspaceId);
|
|
1209
|
-
if (!ws) throw new Error(`Workspace not found: ${workspaceId}`);
|
|
1210
|
-
const sessionManager = SessionManager.continueRecent(ws.path);
|
|
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) {
|
|
1211
1527
|
const runtime = await createAgentSessionRuntime(createRuntime, {
|
|
1212
|
-
cwd
|
|
1528
|
+
cwd,
|
|
1213
1529
|
agentDir: getAgentDir(),
|
|
1214
|
-
sessionManager
|
|
1530
|
+
sessionManager: makeSessionManager()
|
|
1215
1531
|
});
|
|
1216
|
-
const subscribers = this.getOrCreateSubscriberSet(workspaceId);
|
|
1217
1532
|
const bridge = new ExtensionUIBridge();
|
|
1218
|
-
|
|
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) => {
|
|
1219
1546
|
const msg = {
|
|
1220
1547
|
type: "extension_error",
|
|
1221
1548
|
workspaceId,
|
|
1222
|
-
extensionPath:
|
|
1223
|
-
event:
|
|
1224
|
-
message:
|
|
1549
|
+
extensionPath: err2.extensionPath,
|
|
1550
|
+
event: err2.event,
|
|
1551
|
+
message: err2.error
|
|
1225
1552
|
};
|
|
1226
|
-
|
|
1553
|
+
const set = this.subscribers.get(workspaceId);
|
|
1554
|
+
if (set) broadcastTo(set, msg);
|
|
1227
1555
|
console.error(
|
|
1228
|
-
`[ext-error] ${workspaceId} ${
|
|
1229
|
-
${
|
|
1556
|
+
`[ext-error] ${workspaceId} ${err2.extensionPath}@${err2.event}: ${err2.error}` + (err2.stack ? `
|
|
1557
|
+
${err2.stack}` : "")
|
|
1230
1558
|
);
|
|
1231
1559
|
};
|
|
1232
1560
|
await runtime.session.bindExtensions({ uiContext: bridge, onError });
|
|
1233
|
-
safeReconcileAskUser(workspaceId, runtime.session.sessionManager);
|
|
1234
|
-
runtime.setRebindSession(async () => {
|
|
1235
|
-
await runtime.session.bindExtensions({ uiContext: bridge, onError });
|
|
1236
|
-
cancelPendingExcept(runtime.session.sessionFile ?? null);
|
|
1237
|
-
safeReconcileAskUser(workspaceId, runtime.session.sessionManager);
|
|
1238
|
-
this.notifySessionReplaced(workspaceId);
|
|
1239
|
-
});
|
|
1240
|
-
return { runtime, bridge };
|
|
1241
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. */
|
|
1242
1673
|
get(workspaceId) {
|
|
1243
|
-
|
|
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;
|
|
1244
1695
|
}
|
|
1245
1696
|
/**
|
|
1246
|
-
* Register a WS connection as a subscriber for `workspaceId
|
|
1247
|
-
*
|
|
1248
|
-
*
|
|
1249
|
-
* subscribers.
|
|
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.
|
|
1250
1700
|
*/
|
|
1251
1701
|
addSubscriber(workspaceId, ws) {
|
|
1252
1702
|
this.getOrCreateSubscriberSet(workspaceId).add(ws);
|
|
@@ -1257,60 +1707,44 @@ ${err.stack}` : "")
|
|
|
1257
1707
|
set.delete(ws);
|
|
1258
1708
|
if (set.size === 0) this.subscribers.delete(workspaceId);
|
|
1259
1709
|
}
|
|
1260
|
-
/**
|
|
1261
|
-
*
|
|
1262
|
-
* workspace. Used by API handlers that mutate runtime state and need
|
|
1263
|
-
* to refresh derived snapshots (e.g. `context_usage` after `setModel`,
|
|
1264
|
-
* which pi's event stream doesn't surface unless thinking-level also
|
|
1265
|
-
* clamps).
|
|
1266
|
-
*/
|
|
1710
|
+
/** Fan a server-initiated message out to every WS subscribed to the
|
|
1711
|
+
* workspace (e.g. context_usage after setModel). */
|
|
1267
1712
|
broadcast(workspaceId, msg) {
|
|
1268
1713
|
const set = this.subscribers.get(workspaceId);
|
|
1269
1714
|
if (!set || set.size === 0) return;
|
|
1270
1715
|
broadcastTo(set, msg);
|
|
1271
1716
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
};
|
|
1287
|
-
}
|
|
1288
|
-
notifySessionReplaced(workspaceId) {
|
|
1289
|
-
const listeners = this.rebindListeners.get(workspaceId);
|
|
1290
|
-
if (!listeners) return;
|
|
1291
|
-
for (const listener of [...listeners]) {
|
|
1292
|
-
try {
|
|
1293
|
-
listener();
|
|
1294
|
-
} catch (e) {
|
|
1295
|
-
console.error(`[wm] rebind listener for ${workspaceId} failed:`, e);
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
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;
|
|
1298
1731
|
}
|
|
1299
1732
|
async listSessions(workspaceId) {
|
|
1300
1733
|
const ws = await getWorkspace(workspaceId);
|
|
1301
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
|
+
}
|
|
1302
1742
|
const sessions = await SessionManager.list(ws.path);
|
|
1303
|
-
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))));
|
|
1304
1744
|
}
|
|
1305
1745
|
getSessionHistory(workspaceId, sessionPath) {
|
|
1306
|
-
const runtime = this.
|
|
1746
|
+
const runtime = sessionPath ? this.getForSession(workspaceId, sessionPath) : this.get(workspaceId);
|
|
1307
1747
|
if (!runtime) return { items: [], isStreaming: false };
|
|
1308
|
-
if (sessionPath) {
|
|
1309
|
-
const activeFile = runtime.session.sessionFile ? resolve2(runtime.session.sessionFile) : void 0;
|
|
1310
|
-
if (activeFile !== resolve2(sessionPath)) {
|
|
1311
|
-
return { items: [], isStreaming: false };
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
1748
|
const isStreaming = runtime.session.isStreaming ?? false;
|
|
1315
1749
|
const branch = runtime.session.sessionManager.getBranch();
|
|
1316
1750
|
const items = [];
|
|
@@ -1321,7 +1755,7 @@ ${err.stack}` : "")
|
|
|
1321
1755
|
const role = msg.role;
|
|
1322
1756
|
if (role === "user") {
|
|
1323
1757
|
const text = extractUserText2(msg);
|
|
1324
|
-
if (text) items.push({ kind: "user", text });
|
|
1758
|
+
if (text) items.push({ kind: "user", text, entryId: entry.id });
|
|
1325
1759
|
} else if (role === "assistant") {
|
|
1326
1760
|
const { text, thinking, toolCalls } = extractAssistantContent(
|
|
1327
1761
|
msg
|
|
@@ -1359,15 +1793,13 @@ ${err.stack}` : "")
|
|
|
1359
1793
|
/**
|
|
1360
1794
|
* Delete a session JSONL file belonging to this workspace.
|
|
1361
1795
|
*
|
|
1362
|
-
*
|
|
1363
|
-
* can map them to the right status code:
|
|
1796
|
+
* HTTP-tagged errors (HttpError) map to status codes at the route layer:
|
|
1364
1797
|
* - 400: sessionPath not absolute
|
|
1365
1798
|
* - 404: workspace gone, or session not in this workspace's list
|
|
1366
|
-
* - 409:
|
|
1799
|
+
* - 409: a live runtime is bound to it and is streaming (stop it first)
|
|
1367
1800
|
*
|
|
1368
|
-
*
|
|
1369
|
-
*
|
|
1370
|
-
* 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.
|
|
1371
1803
|
*/
|
|
1372
1804
|
async deleteSession(workspaceId, sessionPath) {
|
|
1373
1805
|
const ws = await getWorkspace(workspaceId);
|
|
@@ -1381,69 +1813,89 @@ ${err.stack}` : "")
|
|
|
1381
1813
|
if (!target) {
|
|
1382
1814
|
throw new HttpError(404, `Session not found: ${sessionPath}`);
|
|
1383
1815
|
}
|
|
1384
|
-
const
|
|
1385
|
-
const
|
|
1386
|
-
if (
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
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);
|
|
1391
1826
|
}
|
|
1392
1827
|
try {
|
|
1393
1828
|
await unlink2(resolved);
|
|
1394
|
-
} catch (
|
|
1395
|
-
if (
|
|
1829
|
+
} catch (err2) {
|
|
1830
|
+
if (err2?.code === "ENOENT") {
|
|
1396
1831
|
console.warn(
|
|
1397
1832
|
`[wm] deleteSession: ${resolved} was already gone at unlink time`
|
|
1398
1833
|
);
|
|
1399
1834
|
return;
|
|
1400
1835
|
}
|
|
1401
|
-
throw
|
|
1836
|
+
throw err2;
|
|
1402
1837
|
}
|
|
1403
1838
|
}
|
|
1404
|
-
|
|
1405
|
-
const ws = await getWorkspace(workspaceId);
|
|
1406
|
-
if (!ws) throw new Error(`Workspace not found: ${workspaceId}`);
|
|
1407
|
-
if (!isAbsolute2(sessionPath)) {
|
|
1408
|
-
throw new Error("Session path must be absolute");
|
|
1409
|
-
}
|
|
1410
|
-
const sessions = await SessionManager.list(ws.path);
|
|
1411
|
-
const resolved = resolve2(sessionPath);
|
|
1412
|
-
const target = sessions.find((session) => resolve2(session.path) === resolved);
|
|
1413
|
-
if (!target) {
|
|
1414
|
-
throw new Error(`Session not found: ${sessionPath}`);
|
|
1415
|
-
}
|
|
1416
|
-
const runtime = await this.getOrCreate(workspaceId);
|
|
1417
|
-
const currentPath = runtime.session.sessionFile ? resolve2(runtime.session.sessionFile) : void 0;
|
|
1418
|
-
if (currentPath === resolved) return false;
|
|
1419
|
-
if (runtime.session.isStreaming) {
|
|
1420
|
-
throw new Error("Cannot switch sessions while the agent is streaming");
|
|
1421
|
-
}
|
|
1422
|
-
const result = await runtime.switchSession(resolved, { cwdOverride: ws.path });
|
|
1423
|
-
return !result.cancelled;
|
|
1424
|
-
}
|
|
1839
|
+
/** Dispose every runtime for a workspace (e.g. when it's removed). */
|
|
1425
1840
|
async dispose(workspaceId) {
|
|
1426
|
-
|
|
1427
|
-
if (!state) return;
|
|
1428
|
-
this.states.delete(workspaceId);
|
|
1429
|
-
this.rebindListeners.delete(workspaceId);
|
|
1841
|
+
this.activeByWorkspace.delete(workspaceId);
|
|
1430
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
|
+
}
|
|
1431
1863
|
cancelPendingForSession(state.runtime.session.sessionFile ?? null);
|
|
1432
1864
|
try {
|
|
1433
1865
|
state.bridge.dispose();
|
|
1434
1866
|
} catch (e) {
|
|
1435
|
-
console.error(`[wm] dispose bridge ${workspaceId} failed:`, e);
|
|
1867
|
+
console.error(`[wm] dispose bridge ${state.workspaceId} failed:`, e);
|
|
1436
1868
|
}
|
|
1437
1869
|
try {
|
|
1438
|
-
state.runtime.
|
|
1870
|
+
await state.runtime.dispose();
|
|
1439
1871
|
} catch (e) {
|
|
1440
|
-
console.error(`[wm] dispose ${workspaceId} failed:`, e);
|
|
1872
|
+
console.error(`[wm] dispose ${state.workspaceId} failed:`, e);
|
|
1441
1873
|
}
|
|
1442
1874
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
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
|
+
}
|
|
1447
1899
|
}
|
|
1448
1900
|
};
|
|
1449
1901
|
function safeReconcileAskUser(workspaceId, sm) {
|
|
@@ -1453,13 +1905,14 @@ function safeReconcileAskUser(workspaceId, sm) {
|
|
|
1453
1905
|
console.error(`[wm] ask_user cleanup for ${workspaceId} failed:`, e);
|
|
1454
1906
|
}
|
|
1455
1907
|
}
|
|
1456
|
-
function toSessionSummary(info) {
|
|
1908
|
+
function toSessionSummary(info, running) {
|
|
1457
1909
|
const preview = info.firstMessage.replace(/\s+/g, " ").trim();
|
|
1458
1910
|
return {
|
|
1459
1911
|
path: info.path,
|
|
1460
1912
|
name: info.name,
|
|
1461
1913
|
updatedAt: info.modified.toISOString(),
|
|
1462
|
-
preview: preview ? preview.slice(0, 160) : void 0
|
|
1914
|
+
preview: preview ? preview.slice(0, 160) : void 0,
|
|
1915
|
+
...running ? { running: true } : {}
|
|
1463
1916
|
};
|
|
1464
1917
|
}
|
|
1465
1918
|
function extractUserText2(msg) {
|
|
@@ -1495,7 +1948,7 @@ function extractContentText(content) {
|
|
|
1495
1948
|
}
|
|
1496
1949
|
return parts.join("");
|
|
1497
1950
|
}
|
|
1498
|
-
var workspaceManager = new
|
|
1951
|
+
var workspaceManager = new SessionRuntimeManager();
|
|
1499
1952
|
function broadcastTo(subscribers, msg) {
|
|
1500
1953
|
const wire = JSON.stringify(msg);
|
|
1501
1954
|
for (const ws of subscribers) {
|
|
@@ -1578,8 +2031,8 @@ function mountConfigRoutes(app2) {
|
|
|
1578
2031
|
try {
|
|
1579
2032
|
await workspaceManager.getOrCreate(id);
|
|
1580
2033
|
return c.json(buildConfigResponse(id));
|
|
1581
|
-
} catch (
|
|
1582
|
-
const message =
|
|
2034
|
+
} catch (err2) {
|
|
2035
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
1583
2036
|
return c.json({ ok: false, error: message }, 500);
|
|
1584
2037
|
}
|
|
1585
2038
|
});
|
|
@@ -1605,8 +2058,8 @@ function mountConfigRoutes(app2) {
|
|
|
1605
2058
|
await runtime.session.setModel(model);
|
|
1606
2059
|
broadcastContextUsage(id, runtime);
|
|
1607
2060
|
return c.json(buildConfigResponse(id));
|
|
1608
|
-
} catch (
|
|
1609
|
-
const message =
|
|
2061
|
+
} catch (err2) {
|
|
2062
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
1610
2063
|
return c.json({ ok: false, error: message }, 500);
|
|
1611
2064
|
}
|
|
1612
2065
|
});
|
|
@@ -1630,8 +2083,8 @@ function mountConfigRoutes(app2) {
|
|
|
1630
2083
|
}
|
|
1631
2084
|
runtime.session.setThinkingLevel(body.level);
|
|
1632
2085
|
return c.json(buildConfigResponse(id));
|
|
1633
|
-
} catch (
|
|
1634
|
-
const message =
|
|
2086
|
+
} catch (err2) {
|
|
2087
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
1635
2088
|
return c.json({ ok: false, error: message }, 500);
|
|
1636
2089
|
}
|
|
1637
2090
|
});
|
|
@@ -1654,8 +2107,37 @@ function mountConfigRoutes(app2) {
|
|
|
1654
2107
|
}
|
|
1655
2108
|
runtime.session.setActiveToolsByName(body.tools);
|
|
1656
2109
|
return c.json(buildConfigResponse(id));
|
|
1657
|
-
} catch (
|
|
1658
|
-
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);
|
|
1659
2141
|
return c.json({ ok: false, error: message }, 500);
|
|
1660
2142
|
}
|
|
1661
2143
|
});
|
|
@@ -1817,9 +2299,9 @@ function mountFilesRoute(app2) {
|
|
|
1817
2299
|
}
|
|
1818
2300
|
const body = { workspacePath, entries, truncated };
|
|
1819
2301
|
return c.json(body);
|
|
1820
|
-
} catch (
|
|
1821
|
-
const message =
|
|
1822
|
-
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);
|
|
1823
2305
|
return c.json({ ok: false, error: message }, 500);
|
|
1824
2306
|
}
|
|
1825
2307
|
});
|
|
@@ -1901,9 +2383,9 @@ async function snapshot(workspaceId, roots, workspaceCwd) {
|
|
|
1901
2383
|
shortcuts: [...e.shortcuts.keys()]
|
|
1902
2384
|
};
|
|
1903
2385
|
});
|
|
1904
|
-
const extensionErrors = extResult.errors.map((
|
|
1905
|
-
path:
|
|
1906
|
-
error:
|
|
2386
|
+
const extensionErrors = extResult.errors.map((err2) => ({
|
|
2387
|
+
path: err2.path,
|
|
2388
|
+
error: err2.error
|
|
1907
2389
|
}));
|
|
1908
2390
|
const disabledExtensions = EXTENSIONS_ENABLED ? [] : await scanExtensionDirs(workspaceCwd);
|
|
1909
2391
|
const disabledBuiltins = new Set(getDisabledBuiltins());
|
|
@@ -1931,12 +2413,12 @@ async function rootsFor(workspaceId) {
|
|
|
1931
2413
|
const roots = resolveResourceRoots({ agentDir: getAgentDir2(), workspaceCwd: ws.path });
|
|
1932
2414
|
return { roots, workspaceCwd: ws.path };
|
|
1933
2415
|
}
|
|
1934
|
-
function respondError(c,
|
|
1935
|
-
if (
|
|
1936
|
-
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);
|
|
1937
2419
|
}
|
|
1938
|
-
const message =
|
|
1939
|
-
console.error(`[api/resources] unexpected error:`,
|
|
2420
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
2421
|
+
console.error(`[api/resources] unexpected error:`, err2);
|
|
1940
2422
|
return c.json({ ok: false, error: message }, 500);
|
|
1941
2423
|
}
|
|
1942
2424
|
async function reload(workspaceId) {
|
|
@@ -1953,8 +2435,8 @@ function mountResourcesRoute(app2) {
|
|
|
1953
2435
|
await workspaceManager.getOrCreate(id);
|
|
1954
2436
|
const { roots, workspaceCwd } = await rootsFor(id);
|
|
1955
2437
|
return c.json(await snapshot(id, roots, workspaceCwd));
|
|
1956
|
-
} catch (
|
|
1957
|
-
return respondError(c,
|
|
2438
|
+
} catch (err2) {
|
|
2439
|
+
return respondError(c, err2);
|
|
1958
2440
|
}
|
|
1959
2441
|
});
|
|
1960
2442
|
app2.post("/:id/resources/reload", async (c) => {
|
|
@@ -1966,8 +2448,8 @@ function mountResourcesRoute(app2) {
|
|
|
1966
2448
|
await reload(id);
|
|
1967
2449
|
const { roots, workspaceCwd } = await rootsFor(id);
|
|
1968
2450
|
return c.json(await snapshot(id, roots, workspaceCwd));
|
|
1969
|
-
} catch (
|
|
1970
|
-
return respondError(c,
|
|
2451
|
+
} catch (err2) {
|
|
2452
|
+
return respondError(c, err2);
|
|
1971
2453
|
}
|
|
1972
2454
|
});
|
|
1973
2455
|
app2.get("/:id/resources/skill", async (c) => {
|
|
@@ -1992,8 +2474,8 @@ function mountResourcesRoute(app2) {
|
|
|
1992
2474
|
body: data.body
|
|
1993
2475
|
};
|
|
1994
2476
|
return c.json(body);
|
|
1995
|
-
} catch (
|
|
1996
|
-
return respondError(c,
|
|
2477
|
+
} catch (err2) {
|
|
2478
|
+
return respondError(c, err2);
|
|
1997
2479
|
}
|
|
1998
2480
|
});
|
|
1999
2481
|
app2.post("/:id/resources/skills", async (c) => {
|
|
@@ -2017,8 +2499,8 @@ function mountResourcesRoute(app2) {
|
|
|
2017
2499
|
});
|
|
2018
2500
|
await reload(id);
|
|
2019
2501
|
return c.json(await snapshot(id, roots, workspaceCwd));
|
|
2020
|
-
} catch (
|
|
2021
|
-
return respondError(c,
|
|
2502
|
+
} catch (err2) {
|
|
2503
|
+
return respondError(c, err2);
|
|
2022
2504
|
}
|
|
2023
2505
|
});
|
|
2024
2506
|
app2.put("/:id/resources/skills", async (c) => {
|
|
@@ -2042,8 +2524,8 @@ function mountResourcesRoute(app2) {
|
|
|
2042
2524
|
});
|
|
2043
2525
|
await reload(id);
|
|
2044
2526
|
return c.json(await snapshot(id, roots, workspaceCwd));
|
|
2045
|
-
} catch (
|
|
2046
|
-
return respondError(c,
|
|
2527
|
+
} catch (err2) {
|
|
2528
|
+
return respondError(c, err2);
|
|
2047
2529
|
}
|
|
2048
2530
|
});
|
|
2049
2531
|
app2.delete("/:id/resources/skills", async (c) => {
|
|
@@ -2058,8 +2540,8 @@ function mountResourcesRoute(app2) {
|
|
|
2058
2540
|
await deleteSkill(filePath, roots);
|
|
2059
2541
|
await reload(id);
|
|
2060
2542
|
return c.json(await snapshot(id, roots, workspaceCwd));
|
|
2061
|
-
} catch (
|
|
2062
|
-
return respondError(c,
|
|
2543
|
+
} catch (err2) {
|
|
2544
|
+
return respondError(c, err2);
|
|
2063
2545
|
}
|
|
2064
2546
|
});
|
|
2065
2547
|
app2.get("/:id/resources/prompt", async (c) => {
|
|
@@ -2084,8 +2566,8 @@ function mountResourcesRoute(app2) {
|
|
|
2084
2566
|
body: data.body
|
|
2085
2567
|
};
|
|
2086
2568
|
return c.json(body);
|
|
2087
|
-
} catch (
|
|
2088
|
-
return respondError(c,
|
|
2569
|
+
} catch (err2) {
|
|
2570
|
+
return respondError(c, err2);
|
|
2089
2571
|
}
|
|
2090
2572
|
});
|
|
2091
2573
|
app2.post("/:id/resources/prompts", async (c) => {
|
|
@@ -2109,8 +2591,8 @@ function mountResourcesRoute(app2) {
|
|
|
2109
2591
|
});
|
|
2110
2592
|
await reload(id);
|
|
2111
2593
|
return c.json(await snapshot(id, roots, workspaceCwd));
|
|
2112
|
-
} catch (
|
|
2113
|
-
return respondError(c,
|
|
2594
|
+
} catch (err2) {
|
|
2595
|
+
return respondError(c, err2);
|
|
2114
2596
|
}
|
|
2115
2597
|
});
|
|
2116
2598
|
app2.put("/:id/resources/prompts", async (c) => {
|
|
@@ -2134,8 +2616,8 @@ function mountResourcesRoute(app2) {
|
|
|
2134
2616
|
});
|
|
2135
2617
|
await reload(id);
|
|
2136
2618
|
return c.json(await snapshot(id, roots, workspaceCwd));
|
|
2137
|
-
} catch (
|
|
2138
|
-
return respondError(c,
|
|
2619
|
+
} catch (err2) {
|
|
2620
|
+
return respondError(c, err2);
|
|
2139
2621
|
}
|
|
2140
2622
|
});
|
|
2141
2623
|
app2.delete("/:id/resources/prompts", async (c) => {
|
|
@@ -2150,8 +2632,8 @@ function mountResourcesRoute(app2) {
|
|
|
2150
2632
|
await deletePrompt(filePath, roots);
|
|
2151
2633
|
await reload(id);
|
|
2152
2634
|
return c.json(await snapshot(id, roots, workspaceCwd));
|
|
2153
|
-
} catch (
|
|
2154
|
-
return respondError(c,
|
|
2635
|
+
} catch (err2) {
|
|
2636
|
+
return respondError(c, err2);
|
|
2155
2637
|
}
|
|
2156
2638
|
});
|
|
2157
2639
|
app2.put("/:id/resources/builtin-extensions", async (c) => {
|
|
@@ -2177,8 +2659,8 @@ function mountResourcesRoute(app2) {
|
|
|
2177
2659
|
await runtime.session.reload();
|
|
2178
2660
|
const { roots, workspaceCwd } = await rootsFor(id);
|
|
2179
2661
|
return c.json(await snapshot(id, roots, workspaceCwd));
|
|
2180
|
-
} catch (
|
|
2181
|
-
return respondError(c,
|
|
2662
|
+
} catch (err2) {
|
|
2663
|
+
return respondError(c, err2);
|
|
2182
2664
|
}
|
|
2183
2665
|
});
|
|
2184
2666
|
}
|
|
@@ -2186,14 +2668,182 @@ function isScope(value) {
|
|
|
2186
2668
|
return value === "user" || value === "project";
|
|
2187
2669
|
}
|
|
2188
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);
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
return parts.join(" ").replace(/\s+/g, " ").trim();
|
|
2819
|
+
}
|
|
2820
|
+
function truncate(text, max) {
|
|
2821
|
+
if (text.length <= max) return text;
|
|
2822
|
+
return text.slice(0, max) + "\u2026";
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2189
2825
|
// src/api/workspaces.ts
|
|
2190
|
-
var workspacesRoute = new
|
|
2826
|
+
var workspacesRoute = new Hono2();
|
|
2191
2827
|
workspacesRoute.get("/", async (c) => {
|
|
2192
2828
|
const raw = await listWorkspaces();
|
|
2193
2829
|
const workspaces = await Promise.all(raw.map(enrichWorkspace));
|
|
2194
2830
|
const body = { workspaces };
|
|
2195
2831
|
return c.json(body);
|
|
2196
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
|
+
});
|
|
2197
2847
|
workspacesRoute.get("/:id/sessions", async (c) => {
|
|
2198
2848
|
const id = c.req.param("id");
|
|
2199
2849
|
const existed = await getWorkspace(id);
|
|
@@ -2202,9 +2852,9 @@ workspacesRoute.get("/:id/sessions", async (c) => {
|
|
|
2202
2852
|
const sessions = await workspaceManager.listSessions(id);
|
|
2203
2853
|
const body = { sessions };
|
|
2204
2854
|
return c.json(body);
|
|
2205
|
-
} catch (
|
|
2206
|
-
const message =
|
|
2207
|
-
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);
|
|
2208
2858
|
return c.json({ ok: false, error: message }, 500);
|
|
2209
2859
|
}
|
|
2210
2860
|
});
|
|
@@ -2220,15 +2870,15 @@ workspacesRoute.delete("/:id/sessions", async (c) => {
|
|
|
2220
2870
|
await workspaceManager.deleteSession(id, sessionPath);
|
|
2221
2871
|
const body = { ok: true };
|
|
2222
2872
|
return c.json(body);
|
|
2223
|
-
} catch (
|
|
2224
|
-
if (
|
|
2873
|
+
} catch (err2) {
|
|
2874
|
+
if (err2 instanceof HttpError) {
|
|
2225
2875
|
return c.json(
|
|
2226
|
-
{ ok: false, error:
|
|
2227
|
-
|
|
2876
|
+
{ ok: false, error: err2.message },
|
|
2877
|
+
err2.status
|
|
2228
2878
|
);
|
|
2229
2879
|
}
|
|
2230
|
-
const message =
|
|
2231
|
-
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);
|
|
2232
2882
|
return c.json({ ok: false, error: message }, 500);
|
|
2233
2883
|
}
|
|
2234
2884
|
});
|
|
@@ -2245,9 +2895,31 @@ workspacesRoute.get("/:id/fork-points", async (c) => {
|
|
|
2245
2895
|
}));
|
|
2246
2896
|
const body = { points };
|
|
2247
2897
|
return c.json(body);
|
|
2248
|
-
} catch (
|
|
2249
|
-
const message =
|
|
2250
|
-
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);
|
|
2251
2923
|
return c.json({ ok: false, error: message }, 500);
|
|
2252
2924
|
}
|
|
2253
2925
|
});
|
|
@@ -2260,9 +2932,9 @@ workspacesRoute.get("/:id/history", async (c) => {
|
|
|
2260
2932
|
const sessionPath = c.req.query("sessionPath");
|
|
2261
2933
|
const body = workspaceManager.getSessionHistory(id, sessionPath);
|
|
2262
2934
|
return c.json(body);
|
|
2263
|
-
} catch (
|
|
2264
|
-
const message =
|
|
2265
|
-
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);
|
|
2266
2938
|
return c.json({ ok: false, error: message }, 500);
|
|
2267
2939
|
}
|
|
2268
2940
|
});
|
|
@@ -2303,13 +2975,14 @@ workspacesRoute.delete("/:id", async (c) => {
|
|
|
2303
2975
|
mountConfigRoutes(workspacesRoute);
|
|
2304
2976
|
mountResourcesRoute(workspacesRoute);
|
|
2305
2977
|
mountFilesRoute(workspacesRoute);
|
|
2978
|
+
workspacesRoute.route("/:id/tree", treeRoute);
|
|
2306
2979
|
|
|
2307
2980
|
// src/api/fs.ts
|
|
2308
2981
|
import { readdir as readdir3 } from "fs/promises";
|
|
2309
2982
|
import { homedir as homedir2 } from "os";
|
|
2310
2983
|
import { dirname as dirname4, isAbsolute as isAbsolute4, join as join7, resolve as resolve4 } from "path";
|
|
2311
|
-
import { Hono as
|
|
2312
|
-
var fsRoute = new
|
|
2984
|
+
import { Hono as Hono3 } from "hono";
|
|
2985
|
+
var fsRoute = new Hono3();
|
|
2313
2986
|
fsRoute.get("/browse", async (c) => {
|
|
2314
2987
|
const rawPath = c.req.query("path");
|
|
2315
2988
|
const showHidden = c.req.query("showHidden") === "1";
|
|
@@ -2317,8 +2990,8 @@ fsRoute.get("/browse", async (c) => {
|
|
|
2317
2990
|
let dirents;
|
|
2318
2991
|
try {
|
|
2319
2992
|
dirents = await readdir3(target, { withFileTypes: true });
|
|
2320
|
-
} catch (
|
|
2321
|
-
const code =
|
|
2993
|
+
} catch (err2) {
|
|
2994
|
+
const code = err2.code;
|
|
2322
2995
|
const msg = code === "EACCES" ? "permission denied" : code === "ENOENT" ? "not found" : "read failed";
|
|
2323
2996
|
return c.json({ ok: false, error: msg, path: target }, 400);
|
|
2324
2997
|
}
|
|
@@ -2336,13 +3009,13 @@ fsRoute.get("/browse", async (c) => {
|
|
|
2336
3009
|
});
|
|
2337
3010
|
|
|
2338
3011
|
// src/api/model-configs.ts
|
|
2339
|
-
import { readFile as
|
|
3012
|
+
import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
|
|
2340
3013
|
import { dirname as dirname5, join as join8 } from "path";
|
|
2341
|
-
import { Hono as
|
|
3014
|
+
import { Hono as Hono4 } from "hono";
|
|
2342
3015
|
import {
|
|
2343
3016
|
getAgentDir as getAgentDir3
|
|
2344
3017
|
} from "@earendil-works/pi-coding-agent";
|
|
2345
|
-
var modelConfigsRoute = new
|
|
3018
|
+
var modelConfigsRoute = new Hono4();
|
|
2346
3019
|
var writeLock = Promise.resolve();
|
|
2347
3020
|
function withWriteLock(fn) {
|
|
2348
3021
|
const next = writeLock.then(fn, fn);
|
|
@@ -2356,13 +3029,13 @@ function modelsPath() {
|
|
|
2356
3029
|
}
|
|
2357
3030
|
async function readModelsJson() {
|
|
2358
3031
|
try {
|
|
2359
|
-
const raw = await
|
|
3032
|
+
const raw = await readFile5(modelsPath(), "utf-8");
|
|
2360
3033
|
return JSON.parse(raw);
|
|
2361
|
-
} catch (
|
|
2362
|
-
if (
|
|
3034
|
+
} catch (err2) {
|
|
3035
|
+
if (err2?.code === "ENOENT") {
|
|
2363
3036
|
return { providers: {} };
|
|
2364
3037
|
}
|
|
2365
|
-
throw
|
|
3038
|
+
throw err2;
|
|
2366
3039
|
}
|
|
2367
3040
|
}
|
|
2368
3041
|
async function writeModelsJson(config2) {
|
|
@@ -2393,8 +3066,8 @@ modelConfigsRoute.get("/", async (c) => {
|
|
|
2393
3066
|
const config2 = await readModelsJson();
|
|
2394
3067
|
const body = { config: config2 };
|
|
2395
3068
|
return c.json(body);
|
|
2396
|
-
} catch (
|
|
2397
|
-
const message =
|
|
3069
|
+
} catch (err2) {
|
|
3070
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
2398
3071
|
return c.json({ ok: false, error: message }, 500);
|
|
2399
3072
|
}
|
|
2400
3073
|
});
|
|
@@ -2411,8 +3084,8 @@ modelConfigsRoute.put("/", async (c) => {
|
|
|
2411
3084
|
refreshRegistry(workspaceId ?? void 0);
|
|
2412
3085
|
const resp = { config: body.config };
|
|
2413
3086
|
return c.json(resp);
|
|
2414
|
-
} catch (
|
|
2415
|
-
const message =
|
|
3087
|
+
} catch (err2) {
|
|
3088
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
2416
3089
|
return c.json({ ok: false, error: message }, 500);
|
|
2417
3090
|
}
|
|
2418
3091
|
});
|
|
@@ -2438,8 +3111,8 @@ modelConfigsRoute.post("/providers", async (c) => {
|
|
|
2438
3111
|
refreshRegistry(workspaceId ?? void 0);
|
|
2439
3112
|
const resp = { config: config2 };
|
|
2440
3113
|
return c.json(resp);
|
|
2441
|
-
} catch (
|
|
2442
|
-
const message =
|
|
3114
|
+
} catch (err2) {
|
|
3115
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
2443
3116
|
return c.json({ ok: false, error: message }, 500);
|
|
2444
3117
|
}
|
|
2445
3118
|
});
|
|
@@ -2462,11 +3135,11 @@ modelConfigsRoute.delete("/providers", async (c) => {
|
|
|
2462
3135
|
refreshRegistry(workspaceId ?? void 0);
|
|
2463
3136
|
const resp = { config: config2 };
|
|
2464
3137
|
return c.json(resp);
|
|
2465
|
-
} catch (
|
|
2466
|
-
if (
|
|
2467
|
-
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);
|
|
2468
3141
|
}
|
|
2469
|
-
const message =
|
|
3142
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
2470
3143
|
return c.json({ ok: false, error: message }, 500);
|
|
2471
3144
|
}
|
|
2472
3145
|
});
|
|
@@ -2494,11 +3167,11 @@ modelConfigsRoute.post("/providers/:provider/models", async (c) => {
|
|
|
2494
3167
|
refreshRegistry(workspaceId ?? void 0);
|
|
2495
3168
|
const resp = { config: config2 };
|
|
2496
3169
|
return c.json(resp);
|
|
2497
|
-
} catch (
|
|
2498
|
-
if (
|
|
2499
|
-
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);
|
|
2500
3173
|
}
|
|
2501
|
-
const message =
|
|
3174
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
2502
3175
|
return c.json({ ok: false, error: message }, 500);
|
|
2503
3176
|
}
|
|
2504
3177
|
});
|
|
@@ -2530,11 +3203,11 @@ modelConfigsRoute.put("/providers/:provider/models/:modelId", async (c) => {
|
|
|
2530
3203
|
refreshRegistry(workspaceId ?? void 0);
|
|
2531
3204
|
const resp = { config: config2 };
|
|
2532
3205
|
return c.json(resp);
|
|
2533
|
-
} catch (
|
|
2534
|
-
if (
|
|
2535
|
-
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);
|
|
2536
3209
|
}
|
|
2537
|
-
const message =
|
|
3210
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
2538
3211
|
return c.json({ ok: false, error: message }, 500);
|
|
2539
3212
|
}
|
|
2540
3213
|
});
|
|
@@ -2559,17 +3232,18 @@ modelConfigsRoute.delete("/providers/:provider/models/:modelId", async (c) => {
|
|
|
2559
3232
|
refreshRegistry(workspaceId ?? void 0);
|
|
2560
3233
|
const resp = { config: config2 };
|
|
2561
3234
|
return c.json(resp);
|
|
2562
|
-
} catch (
|
|
2563
|
-
if (
|
|
2564
|
-
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);
|
|
2565
3238
|
}
|
|
2566
|
-
const message =
|
|
3239
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
2567
3240
|
return c.json({ ok: false, error: message }, 500);
|
|
2568
3241
|
}
|
|
2569
3242
|
});
|
|
2570
3243
|
|
|
2571
3244
|
// src/ws/hub.ts
|
|
2572
3245
|
import { WebSocketServer } from "ws";
|
|
3246
|
+
var BACKGROUND_CAP = 4;
|
|
2573
3247
|
var replacementLocks = /* @__PURE__ */ new Map();
|
|
2574
3248
|
function withReplacementLock(workspaceId, fn) {
|
|
2575
3249
|
const prev = replacementLocks.get(workspaceId) ?? Promise.resolve();
|
|
@@ -2586,7 +3260,7 @@ function withReplacementLock(workspaceId, fn) {
|
|
|
2586
3260
|
function attachWsHub(httpServer) {
|
|
2587
3261
|
const wss = new WebSocketServer({ server: httpServer, path: "/ws" });
|
|
2588
3262
|
wss.on("connection", (ws) => {
|
|
2589
|
-
const state = {};
|
|
3263
|
+
const state = { background: /* @__PURE__ */ new Map() };
|
|
2590
3264
|
ws.on("message", async (raw) => {
|
|
2591
3265
|
let msg;
|
|
2592
3266
|
try {
|
|
@@ -2597,8 +3271,8 @@ function attachWsHub(httpServer) {
|
|
|
2597
3271
|
}
|
|
2598
3272
|
try {
|
|
2599
3273
|
await handle(ws, state, msg);
|
|
2600
|
-
} catch (
|
|
2601
|
-
const message =
|
|
3274
|
+
} catch (err2) {
|
|
3275
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
2602
3276
|
send(ws, { type: "error", message, command: msg.type });
|
|
2603
3277
|
}
|
|
2604
3278
|
});
|
|
@@ -2611,124 +3285,162 @@ function attachWsHub(httpServer) {
|
|
|
2611
3285
|
async function handle(ws, state, msg) {
|
|
2612
3286
|
switch (msg.type) {
|
|
2613
3287
|
case "subscribe": {
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
switched = await workspaceManager.switchSession(msg.workspaceId, msg.sessionPath);
|
|
2623
|
-
} catch (err) {
|
|
2624
|
-
switchError = err instanceof Error ? err.message : String(err);
|
|
2625
|
-
}
|
|
2626
|
-
});
|
|
2627
|
-
}
|
|
2628
|
-
if (!switched && !hadCurrentSubscription) {
|
|
2629
|
-
bindCurrentSession(ws, state, msg.workspaceId);
|
|
2630
|
-
}
|
|
2631
|
-
if (switchError) {
|
|
2632
|
-
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;
|
|
2633
3296
|
}
|
|
3297
|
+
promoteToPrimary(ws, state, msg.workspaceId, runtime);
|
|
2634
3298
|
send(ws, { type: "ack", command: "subscribe" });
|
|
2635
3299
|
return;
|
|
2636
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
|
+
}
|
|
2637
3307
|
case "prompt": {
|
|
2638
|
-
const
|
|
2639
|
-
if (!
|
|
3308
|
+
const primary = state.primary;
|
|
3309
|
+
if (!primary) {
|
|
2640
3310
|
send(ws, { type: "error", message: "not subscribed", command: "prompt" });
|
|
2641
3311
|
return;
|
|
2642
3312
|
}
|
|
2643
|
-
if (replacementLocks.has(
|
|
3313
|
+
if (replacementLocks.has(primary.workspaceId)) {
|
|
2644
3314
|
send(ws, { type: "error", message: "session switching in progress", command: "prompt" });
|
|
2645
3315
|
return;
|
|
2646
3316
|
}
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
send(ws, { type: "error", message: "runtime gone", command: "prompt" });
|
|
2650
|
-
return;
|
|
2651
|
-
}
|
|
2652
|
-
void runtime.session.prompt(msg.message, {
|
|
2653
|
-
streamingBehavior: msg.streamingBehavior
|
|
2654
|
-
}).catch((err) => {
|
|
2655
|
-
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);
|
|
2656
3319
|
send(ws, { type: "error", message, command: "prompt" });
|
|
2657
3320
|
});
|
|
2658
3321
|
return;
|
|
2659
3322
|
}
|
|
2660
3323
|
case "abort": {
|
|
2661
|
-
const
|
|
2662
|
-
if (!
|
|
3324
|
+
const primary = state.primary;
|
|
3325
|
+
if (!primary) {
|
|
2663
3326
|
send(ws, { type: "error", message: "not subscribed", command: "abort" });
|
|
2664
3327
|
return;
|
|
2665
3328
|
}
|
|
2666
|
-
|
|
2667
|
-
send(ws, { type: "error", message: "session switching in progress", command: "abort" });
|
|
2668
|
-
return;
|
|
2669
|
-
}
|
|
2670
|
-
const runtime = workspaceManager.get(wsId);
|
|
2671
|
-
if (!runtime) return;
|
|
2672
|
-
await runtime.session.abort();
|
|
3329
|
+
await primary.runtime.session.abort();
|
|
2673
3330
|
return;
|
|
2674
3331
|
}
|
|
2675
3332
|
case "new_session": {
|
|
2676
|
-
const
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
send(ws, { type: "error", message: "runtime gone", command: "new_session" });
|
|
2685
|
-
return;
|
|
2686
|
-
}
|
|
2687
|
-
if (runtime.session.isStreaming) {
|
|
2688
|
-
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" });
|
|
2689
3341
|
return;
|
|
2690
3342
|
}
|
|
2691
|
-
|
|
2692
|
-
if (result.cancelled) {
|
|
2693
|
-
send(ws, { type: "error", message: "new session cancelled", command: "new_session" });
|
|
2694
|
-
}
|
|
3343
|
+
promoteToPrimary(ws, state, workspaceId, runtime);
|
|
2695
3344
|
});
|
|
2696
3345
|
return;
|
|
2697
3346
|
}
|
|
2698
3347
|
case "fork": {
|
|
2699
|
-
const
|
|
2700
|
-
if (!
|
|
3348
|
+
const primary = state.primary;
|
|
3349
|
+
if (!primary) {
|
|
2701
3350
|
send(ws, { type: "error", message: "not subscribed", command: "fork" });
|
|
2702
3351
|
return;
|
|
2703
3352
|
}
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
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" });
|
|
2708
3358
|
return;
|
|
2709
3359
|
}
|
|
2710
|
-
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) {
|
|
2711
3365
|
send(ws, { type: "error", message: "cannot fork while streaming", command: "fork" });
|
|
2712
3366
|
return;
|
|
2713
3367
|
}
|
|
2714
|
-
|
|
2715
|
-
|
|
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) {
|
|
2716
3377
|
send(ws, { type: "error", message: "fork cancelled", command: "fork" });
|
|
3378
|
+
return;
|
|
2717
3379
|
}
|
|
3380
|
+
promoteToPrimary(ws, state, workspaceId, result.runtime);
|
|
2718
3381
|
});
|
|
2719
3382
|
return;
|
|
2720
3383
|
}
|
|
2721
3384
|
case "answer_question": {
|
|
2722
|
-
const
|
|
2723
|
-
if (!
|
|
3385
|
+
const primary = state.primary;
|
|
3386
|
+
if (!primary) {
|
|
2724
3387
|
send(ws, { type: "error", message: "not subscribed", command: "answer_question" });
|
|
2725
3388
|
return;
|
|
2726
3389
|
}
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
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;
|
|
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" });
|
|
3443
|
+
});
|
|
2732
3444
|
return;
|
|
2733
3445
|
}
|
|
2734
3446
|
default: {
|
|
@@ -2738,28 +3450,26 @@ async function handle(ws, state, msg) {
|
|
|
2738
3450
|
}
|
|
2739
3451
|
}
|
|
2740
3452
|
}
|
|
2741
|
-
function
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
state.unsubscribeRebind = workspaceManager.onSessionReplaced(workspaceId, () => {
|
|
2747
|
-
if (state.workspaceId !== workspaceId) return;
|
|
2748
|
-
bindCurrentSession(ws, state, workspaceId);
|
|
2749
|
-
});
|
|
2750
|
-
}
|
|
2751
|
-
function bindCurrentSession(ws, state, workspaceId) {
|
|
2752
|
-
const runtime = workspaceManager.get(workspaceId);
|
|
2753
|
-
if (!runtime) {
|
|
2754
|
-
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);
|
|
2755
3458
|
return;
|
|
2756
3459
|
}
|
|
2757
|
-
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);
|
|
2758
3467
|
const session = runtime.session;
|
|
2759
3468
|
const sessionPath = session.sessionFile ?? null;
|
|
3469
|
+
const runtimeKey = workspaceManager.runtimeKeyFor(workspaceId, runtime);
|
|
2760
3470
|
let assistantStartAt;
|
|
2761
3471
|
let assistantFirstTokenAt;
|
|
2762
|
-
|
|
3472
|
+
const unsubscribe = session.subscribe((ev) => {
|
|
2763
3473
|
const payload = translatePiEvent(ev);
|
|
2764
3474
|
if (!payload) return;
|
|
2765
3475
|
if (payload.kind === "message_start" && payload.role === "assistant") {
|
|
@@ -2768,12 +3478,7 @@ function bindCurrentSession(ws, state, workspaceId) {
|
|
|
2768
3478
|
} else if (payload.kind === "message_update" && payload.delta.kind === "text" && assistantStartAt !== void 0 && assistantFirstTokenAt === void 0) {
|
|
2769
3479
|
assistantFirstTokenAt = performance.now();
|
|
2770
3480
|
}
|
|
2771
|
-
send(ws, {
|
|
2772
|
-
type: "event",
|
|
2773
|
-
workspaceId,
|
|
2774
|
-
sessionPath,
|
|
2775
|
-
payload
|
|
2776
|
-
});
|
|
3481
|
+
send(ws, { type: "event", workspaceId, sessionPath, payload });
|
|
2777
3482
|
if (payload.kind === "message_end" && payload.role === "assistant" && assistantStartAt !== void 0) {
|
|
2778
3483
|
const now = performance.now();
|
|
2779
3484
|
const timing = {
|
|
@@ -2781,12 +3486,7 @@ function bindCurrentSession(ws, state, workspaceId) {
|
|
|
2781
3486
|
firstTokenMs: assistantFirstTokenAt !== void 0 ? Math.round(assistantFirstTokenAt - assistantStartAt) : null,
|
|
2782
3487
|
totalMs: Math.round(now - assistantStartAt)
|
|
2783
3488
|
};
|
|
2784
|
-
send(ws, {
|
|
2785
|
-
type: "event",
|
|
2786
|
-
workspaceId,
|
|
2787
|
-
sessionPath,
|
|
2788
|
-
payload: timing
|
|
2789
|
-
});
|
|
3489
|
+
send(ws, { type: "event", workspaceId, sessionPath, payload: timing });
|
|
2790
3490
|
assistantStartAt = void 0;
|
|
2791
3491
|
assistantFirstTokenAt = void 0;
|
|
2792
3492
|
}
|
|
@@ -2794,33 +3494,80 @@ function bindCurrentSession(ws, state, workspaceId) {
|
|
|
2794
3494
|
sendContextUsage(ws, runtime, workspaceId, sessionPath);
|
|
2795
3495
|
}
|
|
2796
3496
|
});
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
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);
|
|
2804
3508
|
if (inFlight) {
|
|
2805
3509
|
for (const payload of inFlight) {
|
|
2806
|
-
send(ws, {
|
|
2807
|
-
type: "event",
|
|
2808
|
-
workspaceId,
|
|
2809
|
-
sessionPath,
|
|
2810
|
-
payload
|
|
2811
|
-
});
|
|
3510
|
+
send(ws, { type: "event", workspaceId, sessionPath, payload });
|
|
2812
3511
|
}
|
|
2813
3512
|
}
|
|
2814
3513
|
for (const payload of inFlightToolCallsSnapshot(sessionPath)) {
|
|
2815
|
-
send(ws, {
|
|
2816
|
-
type: "event",
|
|
2817
|
-
workspaceId,
|
|
2818
|
-
sessionPath,
|
|
2819
|
-
payload
|
|
2820
|
-
});
|
|
3514
|
+
send(ws, { type: "event", workspaceId, sessionPath, payload });
|
|
2821
3515
|
}
|
|
2822
3516
|
sendContextUsage(ws, runtime, workspaceId, sessionPath);
|
|
2823
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) {
|
|
3560
|
+
send(ws, {
|
|
3561
|
+
type: "subscribed",
|
|
3562
|
+
workspaceId,
|
|
3563
|
+
sessionPath: runtime.session.sessionFile ?? null,
|
|
3564
|
+
sessionId: runtime.session.sessionId
|
|
3565
|
+
});
|
|
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);
|
|
3570
|
+
}
|
|
2824
3571
|
function sendContextUsage(ws, runtime, workspaceId, sessionPath) {
|
|
2825
3572
|
const usage = runtime.session.getContextUsage();
|
|
2826
3573
|
if (!usage) return;
|
|
@@ -2830,22 +3577,20 @@ function sendContextUsage(ws, runtime, workspaceId, sessionPath) {
|
|
|
2830
3577
|
contextWindow: usage.contextWindow,
|
|
2831
3578
|
percent: usage.percent
|
|
2832
3579
|
};
|
|
2833
|
-
send(ws, {
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
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);
|
|
2839
3588
|
}
|
|
2840
3589
|
function detach(state, ws) {
|
|
2841
|
-
state
|
|
2842
|
-
state.
|
|
2843
|
-
|
|
2844
|
-
state.unsubscribeRebind = void 0;
|
|
2845
|
-
if (state.workspaceId && ws) {
|
|
2846
|
-
workspaceManager.removeSubscriber(state.workspaceId, ws);
|
|
3590
|
+
detachPrimary(state, ws);
|
|
3591
|
+
for (const runtimeKey of [...state.background.keys()]) {
|
|
3592
|
+
teardownBackground(state, runtimeKey, ws);
|
|
2847
3593
|
}
|
|
2848
|
-
state.workspaceId = void 0;
|
|
2849
3594
|
}
|
|
2850
3595
|
function send(ws, msg) {
|
|
2851
3596
|
if (ws.readyState !== ws.OPEN) return;
|
|
@@ -2854,7 +3599,7 @@ function send(ws, msg) {
|
|
|
2854
3599
|
|
|
2855
3600
|
// src/index.ts
|
|
2856
3601
|
configureHttpProxy();
|
|
2857
|
-
var app = new
|
|
3602
|
+
var app = new Hono5();
|
|
2858
3603
|
var distDir = dirname6(fileURLToPath(import.meta.url));
|
|
2859
3604
|
var webRoot = resolve5(process.env.PI_PILOT_WEB_ROOT ?? join9(distDir, "..", "public"));
|
|
2860
3605
|
var webIndexPath = join9(webRoot, "index.html");
|
|
@@ -2891,11 +3636,11 @@ function safeResolveWebPath(pathname) {
|
|
|
2891
3636
|
}
|
|
2892
3637
|
async function readWebFile(path) {
|
|
2893
3638
|
try {
|
|
2894
|
-
return await
|
|
2895
|
-
} catch (
|
|
2896
|
-
const code =
|
|
3639
|
+
return await readFile6(path);
|
|
3640
|
+
} catch (err2) {
|
|
3641
|
+
const code = err2.code;
|
|
2897
3642
|
if (code === "ENOENT" || code === "EISDIR") return void 0;
|
|
2898
|
-
throw
|
|
3643
|
+
throw err2;
|
|
2899
3644
|
}
|
|
2900
3645
|
}
|
|
2901
3646
|
async function serveWeb(c) {
|
|
@@ -2904,7 +3649,7 @@ async function serveWeb(c) {
|
|
|
2904
3649
|
const assetPath = safeResolveWebPath(pathname);
|
|
2905
3650
|
if (!assetPath) return c.text("invalid asset path", 400);
|
|
2906
3651
|
const asset = await readWebFile(assetPath);
|
|
2907
|
-
const body = asset ?? await
|
|
3652
|
+
const body = asset ?? await readFile6(webIndexPath);
|
|
2908
3653
|
const filePath = asset ? assetPath : webIndexPath;
|
|
2909
3654
|
const headers = {
|
|
2910
3655
|
"Content-Type": mimeTypes[extname(filePath)] ?? "application/octet-stream",
|