@jyork0828/pi-pilot 0.0.3 → 0.0.6

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