@jyork0828/pi-pilot 0.0.2 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +641 -165
- package/dist/index.js.map +1 -1
- package/package.json +14 -11
- package/public/assets/c-BIGW1oBm.js +1 -0
- package/public/assets/cpp-DIPi6g--.js +1 -0
- package/public/assets/csharp-DSvCPggb.js +1 -0
- package/public/assets/css-CLj8gQPS.js +1 -0
- package/public/assets/dart-bE4Kk8sk.js +1 -0
- package/public/assets/diff-D97Zzqfu.js +1 -0
- package/public/assets/dockerfile-BcOcwvcX.js +1 -0
- package/public/assets/elixir-CkH2-t6x.js +1 -0
- package/public/assets/github-dark-DHJKELXO.js +1 -0
- package/public/assets/github-light-DAi9KRSo.js +1 -0
- package/public/assets/go-C27-OAKa.js +1 -0
- package/public/assets/graphql-ChdNCCLP.js +1 -0
- package/public/assets/haskell-Df6bDoY_.js +1 -0
- package/public/assets/html-pp8916En.js +1 -0
- package/public/assets/index-CBa7EReb.js +411 -0
- package/public/assets/index-DeSNeuE1.css +1 -0
- package/public/assets/ini-BEwlwnbL.js +1 -0
- package/public/assets/java-CylS5w8V.js +1 -0
- package/public/assets/javascript-wDzz0qaB.js +1 -0
- package/public/assets/json-Cp-IABpG.js +1 -0
- package/public/assets/jsonc-Des-eS-w.js +1 -0
- package/public/assets/jsx-g9-lgVsj.js +1 -0
- package/public/assets/kotlin-BdnUsdx6.js +1 -0
- package/public/assets/less-B1dDrJ26.js +1 -0
- package/public/assets/lua-BaeVxFsk.js +1 -0
- package/public/assets/make-CHLpvVh8.js +1 -0
- package/public/assets/markdown-Cvjx9yec.js +1 -0
- package/public/assets/mdx-Cmh6b_Ma.js +1 -0
- package/public/assets/objective-c-DXmwc3jG.js +1 -0
- package/public/assets/php-Csjmro_R.js +1 -0
- package/public/assets/python-B6aJPvgy.js +1 -0
- package/public/assets/r-Dspwwk_N.js +1 -0
- package/public/assets/ruby-CV7NnX5q.js +1 -0
- package/public/assets/rust-B1yitclQ.js +1 -0
- package/public/assets/scala-C151Ov-r.js +1 -0
- package/public/assets/scss-D5BDwBP9.js +1 -0
- package/public/assets/shellscript-Yzrsuije.js +1 -0
- package/public/assets/sql-CRqJ_cUM.js +1 -0
- package/public/assets/svelte-DR4MIrkg.js +1 -0
- package/public/assets/swift-D82vCrfD.js +1 -0
- package/public/assets/toml-vGWfd6FD.js +1 -0
- package/public/assets/tsx-COt5Ahok.js +1 -0
- package/public/assets/typescript-BPQ3VLAy.js +1 -0
- package/public/assets/vue-DMJtu8ND.js +1 -0
- package/public/assets/xml-sdJ4AIDG.js +1 -0
- package/public/assets/yaml-Buea-lGh.js +1 -0
- package/public/favicon.svg +15 -0
- package/public/index.html +12 -3
- package/public/assets/index-Cpw6tfat.css +0 -1
- package/public/assets/index-Dw_Ppxym.js +0 -223
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { existsSync } from "fs";
|
|
5
|
-
import { readFile as
|
|
6
|
-
import { dirname as
|
|
5
|
+
import { readFile as readFile5 } 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
9
|
import { Hono as Hono4 } from "hono";
|
|
@@ -24,6 +24,22 @@ 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
44
|
import { stat as stat2 } from "fs/promises";
|
|
29
45
|
import { basename as basename2, isAbsolute as isAbsolute3, resolve as resolve3 } from "path";
|
|
@@ -408,8 +424,8 @@ async function getStats(path) {
|
|
|
408
424
|
const now = Date.now();
|
|
409
425
|
const cached = cache2.get(path);
|
|
410
426
|
if (cached && cached.expiresAt > now) return cached;
|
|
411
|
-
const
|
|
412
|
-
if (
|
|
427
|
+
const pending2 = inflight.get(path);
|
|
428
|
+
if (pending2) return pending2;
|
|
413
429
|
const probe = probeStats(path).then((stats) => {
|
|
414
430
|
const entry = {
|
|
415
431
|
...stats,
|
|
@@ -539,8 +555,513 @@ var planExtensionFactory = (pi) => {
|
|
|
539
555
|
});
|
|
540
556
|
};
|
|
541
557
|
|
|
558
|
+
// src/extensions/ask_user/schema.ts
|
|
559
|
+
import { Type as Type2 } from "typebox";
|
|
560
|
+
var askUserOptionSchema = Type2.Object({
|
|
561
|
+
label: Type2.String({
|
|
562
|
+
description: "Short, distinct choice (1\u20135 words). User-facing."
|
|
563
|
+
}),
|
|
564
|
+
description: Type2.Optional(
|
|
565
|
+
Type2.String({
|
|
566
|
+
description: "Optional one-line clarification of what this choice means or implies."
|
|
567
|
+
})
|
|
568
|
+
)
|
|
569
|
+
});
|
|
570
|
+
var askUserParamsSchema = Type2.Object({
|
|
571
|
+
question: Type2.String({
|
|
572
|
+
description: "Full question, specific and ending with '?'. Avoid yes/no when options can be more concrete."
|
|
573
|
+
}),
|
|
574
|
+
header: Type2.Optional(
|
|
575
|
+
Type2.String({
|
|
576
|
+
maxLength: 12,
|
|
577
|
+
description: "Optional short chip label (\u2264 12 chars), e.g. 'Auth method'. Shown next to the question."
|
|
578
|
+
})
|
|
579
|
+
),
|
|
580
|
+
options: Type2.Array(askUserOptionSchema, {
|
|
581
|
+
minItems: 2,
|
|
582
|
+
maxItems: 4,
|
|
583
|
+
description: "2\u20134 mutually exclusive choices (unless multiSelect is true). Recommended option first; append ' (Recommended)' to its label."
|
|
584
|
+
}),
|
|
585
|
+
multiSelect: Type2.Optional(
|
|
586
|
+
Type2.Boolean({
|
|
587
|
+
description: "If true, render checkboxes and allow multiple selections. Default false."
|
|
588
|
+
})
|
|
589
|
+
),
|
|
590
|
+
allowOther: Type2.Optional(
|
|
591
|
+
Type2.Boolean({
|
|
592
|
+
description: "If true, show a freeform 'Other' input as an escape hatch. Default true."
|
|
593
|
+
})
|
|
594
|
+
),
|
|
595
|
+
timeoutSec: Type2.Optional(
|
|
596
|
+
Type2.Integer({
|
|
597
|
+
minimum: 5,
|
|
598
|
+
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."
|
|
599
|
+
})
|
|
600
|
+
)
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// src/extensions/ask_user/registry.ts
|
|
604
|
+
var pending = /* @__PURE__ */ new Map();
|
|
605
|
+
function register(toolCallId, entry) {
|
|
606
|
+
pending.set(toolCallId, entry);
|
|
607
|
+
}
|
|
608
|
+
function unregister(toolCallId) {
|
|
609
|
+
pending.delete(toolCallId);
|
|
610
|
+
}
|
|
611
|
+
function resolveAnswer(toolCallId, answer, expectedSessionFile) {
|
|
612
|
+
const entry = pending.get(toolCallId);
|
|
613
|
+
if (!entry) return false;
|
|
614
|
+
if (entry.sessionFile !== expectedSessionFile) return false;
|
|
615
|
+
pending.delete(toolCallId);
|
|
616
|
+
entry.resolve(answer);
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
function cancelPendingExcept(keepSessionFile) {
|
|
620
|
+
for (const [id, entry] of pending) {
|
|
621
|
+
if (entry.sessionFile === keepSessionFile) continue;
|
|
622
|
+
pending.delete(id);
|
|
623
|
+
entry.reject(new Error("Session replaced before answer arrived"));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function cancelPendingForSession(sessionFile) {
|
|
627
|
+
for (const [id, entry] of pending) {
|
|
628
|
+
if (entry.sessionFile !== sessionFile) continue;
|
|
629
|
+
pending.delete(id);
|
|
630
|
+
entry.reject(new Error("Workspace disposed"));
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
function snapshotForSession(sessionFile) {
|
|
634
|
+
const out = [];
|
|
635
|
+
for (const [id, entry] of pending) {
|
|
636
|
+
if (entry.sessionFile !== sessionFile) continue;
|
|
637
|
+
out.push({ toolCallId: id, args: entry.args });
|
|
638
|
+
}
|
|
639
|
+
return out;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/extensions/ask_user/factory.ts
|
|
643
|
+
var askUserExtensionFactory = (pi) => {
|
|
644
|
+
pi.registerTool({
|
|
645
|
+
name: "ask_user",
|
|
646
|
+
label: "Ask user",
|
|
647
|
+
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).",
|
|
648
|
+
parameters: askUserParamsSchema,
|
|
649
|
+
promptSnippet: "ask_user: pause and ask the user a structured multiple-choice question when a decision is genuinely theirs.",
|
|
650
|
+
promptGuidelines: [
|
|
651
|
+
"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.",
|
|
652
|
+
"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.",
|
|
653
|
+
"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.",
|
|
654
|
+
"Don't follow up ask_user with 'Should I proceed?' \u2014 the answer already authorizes the next step.",
|
|
655
|
+
"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."
|
|
656
|
+
],
|
|
657
|
+
execute: async (toolCallId, params, signal, _onUpdate, ctx) => {
|
|
658
|
+
const sessionFile = ctx.sessionManager.getSessionFile() ?? null;
|
|
659
|
+
const startedAt = Date.now();
|
|
660
|
+
const answer = await waitForAnswer({
|
|
661
|
+
toolCallId,
|
|
662
|
+
params,
|
|
663
|
+
sessionFile,
|
|
664
|
+
signal
|
|
665
|
+
});
|
|
666
|
+
return formatResult(params, answer, startedAt);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
};
|
|
670
|
+
function waitForAnswer({
|
|
671
|
+
toolCallId,
|
|
672
|
+
params,
|
|
673
|
+
sessionFile,
|
|
674
|
+
signal
|
|
675
|
+
}) {
|
|
676
|
+
return new Promise((resolve6, reject) => {
|
|
677
|
+
let settled = false;
|
|
678
|
+
let timeoutHandle;
|
|
679
|
+
const cleanup = () => {
|
|
680
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
681
|
+
signal?.removeEventListener("abort", onAbort);
|
|
682
|
+
unregister(toolCallId);
|
|
683
|
+
};
|
|
684
|
+
const finishOk = (a) => {
|
|
685
|
+
if (settled) return;
|
|
686
|
+
settled = true;
|
|
687
|
+
cleanup();
|
|
688
|
+
resolve6(a);
|
|
689
|
+
};
|
|
690
|
+
const finishErr = (err) => {
|
|
691
|
+
if (settled) return;
|
|
692
|
+
settled = true;
|
|
693
|
+
cleanup();
|
|
694
|
+
reject(err);
|
|
695
|
+
};
|
|
696
|
+
const onAbort = () => finishErr(new Error("Aborted by user"));
|
|
697
|
+
if (signal?.aborted) {
|
|
698
|
+
finishErr(new Error("Aborted by user"));
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
signal?.addEventListener("abort", onAbort);
|
|
702
|
+
if (typeof params.timeoutSec === "number" && params.timeoutSec > 0) {
|
|
703
|
+
const ms = params.timeoutSec * 1e3;
|
|
704
|
+
timeoutHandle = setTimeout(() => {
|
|
705
|
+
finishOk({ kind: "timeout", afterMs: ms });
|
|
706
|
+
}, ms);
|
|
707
|
+
}
|
|
708
|
+
register(toolCallId, {
|
|
709
|
+
args: params,
|
|
710
|
+
sessionFile,
|
|
711
|
+
resolve: (answer) => finishOk(answer),
|
|
712
|
+
reject: (err) => finishErr(err)
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
function formatResult(params, answer, startedAt) {
|
|
717
|
+
if (answer.kind === "option") {
|
|
718
|
+
const labels = answer.indices.map((i) => params.options[i]?.label).filter((l) => typeof l === "string");
|
|
719
|
+
const text2 = labels.length === 1 ? `User selected: "${labels[0]}"${descriptionSuffix(params, answer.indices[0])}.` : `User selected: ${labels.map((l) => `"${l}"`).join(", ")}.`;
|
|
720
|
+
const details2 = {
|
|
721
|
+
kind: "option",
|
|
722
|
+
indices: answer.indices,
|
|
723
|
+
labels
|
|
724
|
+
};
|
|
725
|
+
return {
|
|
726
|
+
content: [{ type: "text", text: text2 }],
|
|
727
|
+
details: details2
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
if (answer.kind === "other") {
|
|
731
|
+
const text2 = `User answered (freeform): "${answer.text}"`;
|
|
732
|
+
const details2 = { kind: "other", text: answer.text };
|
|
733
|
+
return {
|
|
734
|
+
content: [{ type: "text", text: text2 }],
|
|
735
|
+
details: details2
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
if (answer.kind === "skip") {
|
|
739
|
+
const text2 = "User chose to skip this question. Use your best judgement and proceed; you may ask again later if it still matters.";
|
|
740
|
+
const details2 = { kind: "skip" };
|
|
741
|
+
return {
|
|
742
|
+
content: [{ type: "text", text: text2 }],
|
|
743
|
+
details: details2
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
const waited = Math.round((Date.now() - startedAt) / 1e3);
|
|
747
|
+
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.`;
|
|
748
|
+
const details = {
|
|
749
|
+
kind: "timeout",
|
|
750
|
+
afterMs: answer.afterMs
|
|
751
|
+
};
|
|
752
|
+
return {
|
|
753
|
+
content: [{ type: "text", text }],
|
|
754
|
+
details
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
function descriptionSuffix(params, index) {
|
|
758
|
+
const desc = params.options[index]?.description;
|
|
759
|
+
return desc ? ` \u2014 ${desc}` : "";
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// src/storage/builtin-extension-prefs.ts
|
|
763
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
764
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
765
|
+
var PREFS_PATH = join4(config.dataDir, "builtin-extensions.json");
|
|
766
|
+
var cache3 = { disabled: [] };
|
|
767
|
+
async function loadBuiltinPrefs() {
|
|
768
|
+
try {
|
|
769
|
+
const raw = await readFile3(PREFS_PATH, "utf8");
|
|
770
|
+
const parsed = JSON.parse(raw);
|
|
771
|
+
cache3 = { disabled: Array.isArray(parsed.disabled) ? parsed.disabled : [] };
|
|
772
|
+
} catch (err) {
|
|
773
|
+
cache3 = { disabled: [] };
|
|
774
|
+
if (err.code !== "ENOENT") {
|
|
775
|
+
console.warn(`[builtin-prefs] ignoring unreadable ${PREFS_PATH}:`, err);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function isBuiltinDisabled(id) {
|
|
780
|
+
return cache3.disabled.includes(id);
|
|
781
|
+
}
|
|
782
|
+
function getDisabledBuiltins() {
|
|
783
|
+
return [...cache3.disabled];
|
|
784
|
+
}
|
|
785
|
+
async function setBuiltinEnabled(id, enabled) {
|
|
786
|
+
const next = new Set(cache3.disabled);
|
|
787
|
+
if (enabled) next.delete(id);
|
|
788
|
+
else next.add(id);
|
|
789
|
+
cache3 = { disabled: [...next] };
|
|
790
|
+
await save2();
|
|
791
|
+
}
|
|
792
|
+
async function save2() {
|
|
793
|
+
await mkdir3(dirname3(PREFS_PATH), { recursive: true });
|
|
794
|
+
await writeFile3(PREFS_PATH, JSON.stringify(cache3, null, 2), "utf8");
|
|
795
|
+
}
|
|
796
|
+
|
|
542
797
|
// src/extensions/index.ts
|
|
543
|
-
var
|
|
798
|
+
var BUILTIN_EXTENSIONS = [
|
|
799
|
+
{
|
|
800
|
+
id: "plan",
|
|
801
|
+
name: "Plan",
|
|
802
|
+
description: "A live task checklist for multi-step work \u2014 adds the update_plan tool and the /plan command.",
|
|
803
|
+
tools: ["update_plan"],
|
|
804
|
+
commands: ["plan"],
|
|
805
|
+
factory: planExtensionFactory
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
id: "ask_user",
|
|
809
|
+
name: "Ask user",
|
|
810
|
+
description: "Lets the agent pause and ask you a structured multiple-choice question \u2014 adds the ask_user tool.",
|
|
811
|
+
tools: ["ask_user"],
|
|
812
|
+
commands: [],
|
|
813
|
+
factory: askUserExtensionFactory
|
|
814
|
+
}
|
|
815
|
+
];
|
|
816
|
+
function gate(def) {
|
|
817
|
+
return (pi) => {
|
|
818
|
+
if (isBuiltinDisabled(def.id)) return;
|
|
819
|
+
return def.factory(pi);
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
var builtinExtensionFactories = BUILTIN_EXTENSIONS.map(gate);
|
|
823
|
+
|
|
824
|
+
// src/extensions/ask_user/cleanup.ts
|
|
825
|
+
var CUSTOM_TYPE = "ask_user-restart-cancelled";
|
|
826
|
+
function reconcileAfterRestart(sessionManager) {
|
|
827
|
+
const branch = sessionManager.getBranch();
|
|
828
|
+
if (branch.length === 0) return;
|
|
829
|
+
const satisfied = /* @__PURE__ */ new Set();
|
|
830
|
+
const danglingIds = [];
|
|
831
|
+
const danglingAlreadyHandled = /* @__PURE__ */ new Set();
|
|
832
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
833
|
+
const entry = branch[i];
|
|
834
|
+
if (entry.type === "custom_message") {
|
|
835
|
+
const cm = entry;
|
|
836
|
+
if (cm.customType === CUSTOM_TYPE) {
|
|
837
|
+
const ids = cm.details?.ids;
|
|
838
|
+
if (Array.isArray(ids)) {
|
|
839
|
+
for (const id of ids) {
|
|
840
|
+
if (typeof id === "string") danglingAlreadyHandled.add(id);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
if (entry.type !== "message") continue;
|
|
847
|
+
const msg = entry.message;
|
|
848
|
+
if (msg.role === "toolResult" && typeof msg.toolCallId === "string") {
|
|
849
|
+
satisfied.add(msg.toolCallId);
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
853
|
+
for (const block of msg.content) {
|
|
854
|
+
if (!block || typeof block !== "object") continue;
|
|
855
|
+
const b = block;
|
|
856
|
+
if (b.type !== "toolCall") continue;
|
|
857
|
+
if (b.name !== "ask_user") continue;
|
|
858
|
+
if (typeof b.id !== "string") continue;
|
|
859
|
+
if (satisfied.has(b.id)) continue;
|
|
860
|
+
if (danglingAlreadyHandled.has(b.id)) continue;
|
|
861
|
+
danglingIds.push(b.id);
|
|
862
|
+
}
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
if (msg.role === "user") break;
|
|
866
|
+
}
|
|
867
|
+
if (danglingIds.length === 0) return;
|
|
868
|
+
const idList = danglingIds.join(", ");
|
|
869
|
+
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.`;
|
|
870
|
+
sessionManager.appendCustomMessageEntry(
|
|
871
|
+
CUSTOM_TYPE,
|
|
872
|
+
text,
|
|
873
|
+
true,
|
|
874
|
+
// display in TUI too (no-op in pi-pilot, but harmless)
|
|
875
|
+
{ ids: danglingIds }
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// src/ws/bridge.ts
|
|
880
|
+
function translatePiEvent(ev) {
|
|
881
|
+
switch (ev.type) {
|
|
882
|
+
case "agent_start":
|
|
883
|
+
return { kind: "agent_start" };
|
|
884
|
+
case "agent_end":
|
|
885
|
+
return { kind: "agent_end", willRetry: ev.willRetry };
|
|
886
|
+
case "turn_start":
|
|
887
|
+
return { kind: "turn_start" };
|
|
888
|
+
case "turn_end":
|
|
889
|
+
return { kind: "turn_end" };
|
|
890
|
+
case "message_start": {
|
|
891
|
+
const role = roleOf(ev.message);
|
|
892
|
+
const text = role === "user" ? extractUserText(ev.message) : void 0;
|
|
893
|
+
return { kind: "message_start", role, text };
|
|
894
|
+
}
|
|
895
|
+
case "message_end":
|
|
896
|
+
return { kind: "message_end", role: roleOf(ev.message) };
|
|
897
|
+
case "message_update": {
|
|
898
|
+
const ame = ev.assistantMessageEvent;
|
|
899
|
+
if (ame.type === "text_delta") {
|
|
900
|
+
return {
|
|
901
|
+
kind: "message_update",
|
|
902
|
+
delta: { kind: "text", contentIndex: ame.contentIndex, text: ame.delta }
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
if (ame.type === "thinking_delta") {
|
|
906
|
+
return {
|
|
907
|
+
kind: "message_update",
|
|
908
|
+
delta: { kind: "thinking", contentIndex: ame.contentIndex, text: ame.delta }
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
return { kind: "message_update", delta: { kind: "other" } };
|
|
912
|
+
}
|
|
913
|
+
case "tool_execution_start":
|
|
914
|
+
return {
|
|
915
|
+
kind: "tool_execution_start",
|
|
916
|
+
toolCallId: ev.toolCallId,
|
|
917
|
+
toolName: ev.toolName,
|
|
918
|
+
args: ev.args
|
|
919
|
+
};
|
|
920
|
+
case "tool_execution_update":
|
|
921
|
+
return {
|
|
922
|
+
kind: "tool_execution_update",
|
|
923
|
+
toolCallId: ev.toolCallId,
|
|
924
|
+
toolName: ev.toolName,
|
|
925
|
+
partialText: extractText(ev.partialResult)
|
|
926
|
+
};
|
|
927
|
+
case "tool_execution_end": {
|
|
928
|
+
const details = shouldForwardDetails(ev.toolName) ? ev.result?.details : void 0;
|
|
929
|
+
return {
|
|
930
|
+
kind: "tool_execution_end",
|
|
931
|
+
toolCallId: ev.toolCallId,
|
|
932
|
+
toolName: ev.toolName,
|
|
933
|
+
isError: ev.isError,
|
|
934
|
+
text: extractText(ev.result),
|
|
935
|
+
...details !== void 0 ? { details } : {}
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
case "queue_update":
|
|
939
|
+
return {
|
|
940
|
+
kind: "queue_update",
|
|
941
|
+
steering: [...ev.steering],
|
|
942
|
+
followUp: [...ev.followUp]
|
|
943
|
+
};
|
|
944
|
+
case "auto_retry_start":
|
|
945
|
+
return {
|
|
946
|
+
kind: "auto_retry_start",
|
|
947
|
+
attempt: ev.attempt,
|
|
948
|
+
maxAttempts: ev.maxAttempts,
|
|
949
|
+
delayMs: ev.delayMs,
|
|
950
|
+
errorMessage: ev.errorMessage
|
|
951
|
+
};
|
|
952
|
+
case "auto_retry_end":
|
|
953
|
+
return {
|
|
954
|
+
kind: "auto_retry_end",
|
|
955
|
+
success: ev.success,
|
|
956
|
+
attempt: ev.attempt,
|
|
957
|
+
finalError: ev.finalError
|
|
958
|
+
};
|
|
959
|
+
case "compaction_start":
|
|
960
|
+
return { kind: "compaction_start", reason: ev.reason };
|
|
961
|
+
case "compaction_end":
|
|
962
|
+
return {
|
|
963
|
+
kind: "compaction_end",
|
|
964
|
+
reason: ev.reason,
|
|
965
|
+
aborted: ev.aborted,
|
|
966
|
+
willRetry: ev.willRetry,
|
|
967
|
+
errorMessage: ev.errorMessage
|
|
968
|
+
};
|
|
969
|
+
case "session_info_changed":
|
|
970
|
+
return { kind: "session_info_changed", name: ev.name };
|
|
971
|
+
case "thinking_level_changed":
|
|
972
|
+
return { kind: "thinking_level_changed", level: ev.level };
|
|
973
|
+
default:
|
|
974
|
+
return void 0;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
var warnedUnknownRoles = /* @__PURE__ */ new Set();
|
|
978
|
+
function roleOf(message) {
|
|
979
|
+
const role = message?.role;
|
|
980
|
+
if (role === "user" || role === "assistant" || role === "toolResult" || role === "bashExecution") {
|
|
981
|
+
return role;
|
|
982
|
+
}
|
|
983
|
+
const key = typeof role === "string" ? role : `<${role === void 0 ? "missing" : typeof role}>`;
|
|
984
|
+
if (!warnedUnknownRoles.has(key)) {
|
|
985
|
+
warnedUnknownRoles.add(key);
|
|
986
|
+
console.warn(
|
|
987
|
+
`[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.`
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
return "assistant";
|
|
991
|
+
}
|
|
992
|
+
function extractUserText(message) {
|
|
993
|
+
if (!message || typeof message !== "object") return void 0;
|
|
994
|
+
const content = message.content;
|
|
995
|
+
if (typeof content === "string") return content;
|
|
996
|
+
if (!Array.isArray(content)) return void 0;
|
|
997
|
+
const parts = [];
|
|
998
|
+
for (const block of content) {
|
|
999
|
+
if (block && typeof block === "object" && block.type === "text") {
|
|
1000
|
+
const text = block.text;
|
|
1001
|
+
if (typeof text === "string") parts.push(text);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return parts.length === 0 ? void 0 : parts.join("");
|
|
1005
|
+
}
|
|
1006
|
+
function inFlightAssistantSnapshot(streamingMessage) {
|
|
1007
|
+
if (!streamingMessage || typeof streamingMessage !== "object") return void 0;
|
|
1008
|
+
const m = streamingMessage;
|
|
1009
|
+
if (m.role !== "assistant" || !Array.isArray(m.content)) return void 0;
|
|
1010
|
+
let textAccum = "";
|
|
1011
|
+
let thinkingAccum = "";
|
|
1012
|
+
for (const block of m.content) {
|
|
1013
|
+
if (!block || typeof block !== "object") continue;
|
|
1014
|
+
const b = block;
|
|
1015
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
1016
|
+
textAccum += b.text;
|
|
1017
|
+
} else if (b.type === "thinking" && typeof b.thinking === "string") {
|
|
1018
|
+
thinkingAccum += b.thinking;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
if (!textAccum && !thinkingAccum) return void 0;
|
|
1022
|
+
const events = [
|
|
1023
|
+
{ kind: "message_start", role: "assistant" }
|
|
1024
|
+
];
|
|
1025
|
+
if (thinkingAccum) {
|
|
1026
|
+
events.push({
|
|
1027
|
+
kind: "message_update",
|
|
1028
|
+
delta: { kind: "thinking", contentIndex: 0, text: thinkingAccum }
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
if (textAccum) {
|
|
1032
|
+
events.push({
|
|
1033
|
+
kind: "message_update",
|
|
1034
|
+
delta: { kind: "text", contentIndex: 0, text: textAccum }
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
return events;
|
|
1038
|
+
}
|
|
1039
|
+
function inFlightToolCallsSnapshot(sessionFile) {
|
|
1040
|
+
const pending2 = snapshotForSession(sessionFile);
|
|
1041
|
+
return pending2.map((p) => ({
|
|
1042
|
+
kind: "tool_execution_start",
|
|
1043
|
+
toolCallId: p.toolCallId,
|
|
1044
|
+
toolName: "ask_user",
|
|
1045
|
+
args: p.args
|
|
1046
|
+
}));
|
|
1047
|
+
}
|
|
1048
|
+
var DETAILS_FORWARD_WHITELIST = /* @__PURE__ */ new Set(["ask_user"]);
|
|
1049
|
+
function shouldForwardDetails(toolName) {
|
|
1050
|
+
return DETAILS_FORWARD_WHITELIST.has(toolName);
|
|
1051
|
+
}
|
|
1052
|
+
function extractText(result) {
|
|
1053
|
+
if (!result || typeof result !== "object") return void 0;
|
|
1054
|
+
const content = result.content;
|
|
1055
|
+
if (!Array.isArray(content)) return void 0;
|
|
1056
|
+
const parts = [];
|
|
1057
|
+
for (const c of content) {
|
|
1058
|
+
if (c && typeof c === "object" && c.type === "text") {
|
|
1059
|
+
const text = c.text;
|
|
1060
|
+
if (typeof text === "string") parts.push(text);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
return parts.length === 0 ? void 0 : parts.join("");
|
|
1064
|
+
}
|
|
544
1065
|
|
|
545
1066
|
// src/ws/extension-ui.ts
|
|
546
1067
|
var ExtensionUIBridge = class {
|
|
@@ -709,8 +1230,11 @@ ${err.stack}` : "")
|
|
|
709
1230
|
);
|
|
710
1231
|
};
|
|
711
1232
|
await runtime.session.bindExtensions({ uiContext: bridge, onError });
|
|
1233
|
+
safeReconcileAskUser(workspaceId, runtime.session.sessionManager);
|
|
712
1234
|
runtime.setRebindSession(async () => {
|
|
713
1235
|
await runtime.session.bindExtensions({ uiContext: bridge, onError });
|
|
1236
|
+
cancelPendingExcept(runtime.session.sessionFile ?? null);
|
|
1237
|
+
safeReconcileAskUser(workspaceId, runtime.session.sessionManager);
|
|
714
1238
|
this.notifySessionReplaced(workspaceId);
|
|
715
1239
|
});
|
|
716
1240
|
return { runtime, bridge };
|
|
@@ -796,7 +1320,7 @@ ${err.stack}` : "")
|
|
|
796
1320
|
const msg = entry.message;
|
|
797
1321
|
const role = msg.role;
|
|
798
1322
|
if (role === "user") {
|
|
799
|
-
const text =
|
|
1323
|
+
const text = extractUserText2(msg);
|
|
800
1324
|
if (text) items.push({ kind: "user", text });
|
|
801
1325
|
} else if (role === "assistant") {
|
|
802
1326
|
const { text, thinking, toolCalls } = extractAssistantContent(
|
|
@@ -814,7 +1338,11 @@ ${err.stack}` : "")
|
|
|
814
1338
|
toolName: tr.toolName,
|
|
815
1339
|
args: argsByCallId.get(tr.toolCallId) ?? "",
|
|
816
1340
|
text: extractContentText(tr.content),
|
|
817
|
-
isError: tr.isError
|
|
1341
|
+
isError: tr.isError,
|
|
1342
|
+
// Mirror live wire whitelist (bridge.ts): only ship details
|
|
1343
|
+
// for tools whose cards need the structured shape, so the
|
|
1344
|
+
// history payload stays small for bash / edit / read.
|
|
1345
|
+
...shouldForwardDetails(tr.toolName) && tr.details !== void 0 ? { details: tr.details } : {}
|
|
818
1346
|
});
|
|
819
1347
|
} else if (role === "bashExecution") {
|
|
820
1348
|
const be = msg;
|
|
@@ -900,6 +1428,7 @@ ${err.stack}` : "")
|
|
|
900
1428
|
this.states.delete(workspaceId);
|
|
901
1429
|
this.rebindListeners.delete(workspaceId);
|
|
902
1430
|
this.subscribers.delete(workspaceId);
|
|
1431
|
+
cancelPendingForSession(state.runtime.session.sessionFile ?? null);
|
|
903
1432
|
try {
|
|
904
1433
|
state.bridge.dispose();
|
|
905
1434
|
} catch (e) {
|
|
@@ -912,10 +1441,18 @@ ${err.stack}` : "")
|
|
|
912
1441
|
}
|
|
913
1442
|
}
|
|
914
1443
|
async disposeAll() {
|
|
1444
|
+
await Promise.allSettled([...this.pending.values()]);
|
|
915
1445
|
const ids = [...this.states.keys()];
|
|
916
1446
|
await Promise.all(ids.map((id) => this.dispose(id)));
|
|
917
1447
|
}
|
|
918
1448
|
};
|
|
1449
|
+
function safeReconcileAskUser(workspaceId, sm) {
|
|
1450
|
+
try {
|
|
1451
|
+
reconcileAfterRestart(sm);
|
|
1452
|
+
} catch (e) {
|
|
1453
|
+
console.error(`[wm] ask_user cleanup for ${workspaceId} failed:`, e);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
919
1456
|
function toSessionSummary(info) {
|
|
920
1457
|
const preview = info.firstMessage.replace(/\s+/g, " ").trim();
|
|
921
1458
|
return {
|
|
@@ -925,7 +1462,7 @@ function toSessionSummary(info) {
|
|
|
925
1462
|
preview: preview ? preview.slice(0, 160) : void 0
|
|
926
1463
|
};
|
|
927
1464
|
}
|
|
928
|
-
function
|
|
1465
|
+
function extractUserText2(msg) {
|
|
929
1466
|
if (typeof msg.content === "string") return msg.content;
|
|
930
1467
|
return extractContentText(msg.content);
|
|
931
1468
|
}
|
|
@@ -1127,7 +1664,7 @@ function mountConfigRoutes(app2) {
|
|
|
1127
1664
|
// src/api/files.ts
|
|
1128
1665
|
import { execFile as execFile2 } from "child_process";
|
|
1129
1666
|
import { readdir } from "fs/promises";
|
|
1130
|
-
import { join as
|
|
1667
|
+
import { join as join5, relative, sep as sep2 } from "path";
|
|
1131
1668
|
import { promisify as promisify2 } from "util";
|
|
1132
1669
|
var exec2 = promisify2(execFile2);
|
|
1133
1670
|
var LIST_TTL_MS = 1e4;
|
|
@@ -1159,8 +1696,8 @@ async function getFileList(workspacePath) {
|
|
|
1159
1696
|
const now = Date.now();
|
|
1160
1697
|
const cached = listCache.get(workspacePath);
|
|
1161
1698
|
if (cached && cached.expiresAt > now) return cached.files;
|
|
1162
|
-
const
|
|
1163
|
-
if (
|
|
1699
|
+
const pending2 = inflight2.get(workspacePath);
|
|
1700
|
+
if (pending2) return (await pending2).files;
|
|
1164
1701
|
const probe = probeFileList(workspacePath).then((files) => {
|
|
1165
1702
|
const entry = {
|
|
1166
1703
|
files,
|
|
@@ -1216,7 +1753,7 @@ async function walkDir(root, dir, depth, out) {
|
|
|
1216
1753
|
for (const d of dirents) {
|
|
1217
1754
|
if (out.length >= MAX_FILES_TRACKED) return;
|
|
1218
1755
|
if (WALK_IGNORES.has(d.name)) continue;
|
|
1219
|
-
const abs =
|
|
1756
|
+
const abs = join5(dir, d.name);
|
|
1220
1757
|
if (d.isDirectory()) {
|
|
1221
1758
|
await walkDir(root, abs, depth + 1, out);
|
|
1222
1759
|
} else if (d.isFile()) {
|
|
@@ -1256,7 +1793,7 @@ function mountFilesRoute(app2) {
|
|
|
1256
1793
|
if (!qRaw) {
|
|
1257
1794
|
const slice = all.slice(0, limit);
|
|
1258
1795
|
entries = slice.map((relPath) => ({
|
|
1259
|
-
path:
|
|
1796
|
+
path: join5(workspacePath, relPath),
|
|
1260
1797
|
relPath
|
|
1261
1798
|
}));
|
|
1262
1799
|
truncated = all.length > limit;
|
|
@@ -1273,7 +1810,7 @@ function mountFilesRoute(app2) {
|
|
|
1273
1810
|
scored.sort((a, b) => b.score - a.score);
|
|
1274
1811
|
const top = scored.slice(0, limit);
|
|
1275
1812
|
entries = top.map((e) => ({
|
|
1276
|
-
path:
|
|
1813
|
+
path: join5(workspacePath, e.relPath),
|
|
1277
1814
|
relPath: e.relPath
|
|
1278
1815
|
}));
|
|
1279
1816
|
truncated = matchCount > limit;
|
|
@@ -1290,7 +1827,7 @@ function mountFilesRoute(app2) {
|
|
|
1290
1827
|
|
|
1291
1828
|
// src/api/resources.ts
|
|
1292
1829
|
import { readdir as readdir2 } from "fs/promises";
|
|
1293
|
-
import { join as
|
|
1830
|
+
import { join as join6 } from "path";
|
|
1294
1831
|
import { getAgentDir as getAgentDir2 } from "@earendil-works/pi-coding-agent";
|
|
1295
1832
|
function toResourceSource(info) {
|
|
1296
1833
|
return {
|
|
@@ -1300,16 +1837,16 @@ function toResourceSource(info) {
|
|
|
1300
1837
|
};
|
|
1301
1838
|
}
|
|
1302
1839
|
async function scanExtensionDirs(workspaceCwd) {
|
|
1303
|
-
const dirs = [
|
|
1840
|
+
const dirs = [join6(getAgentDir2(), "extensions"), join6(workspaceCwd, ".pi", "extensions")];
|
|
1304
1841
|
const found = [];
|
|
1305
1842
|
for (const dir of dirs) {
|
|
1306
1843
|
try {
|
|
1307
1844
|
const entries = await readdir2(dir, { withFileTypes: true });
|
|
1308
1845
|
for (const entry of entries) {
|
|
1309
1846
|
if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".js"))) {
|
|
1310
|
-
found.push(
|
|
1847
|
+
found.push(join6(dir, entry.name));
|
|
1311
1848
|
} else if (entry.isDirectory()) {
|
|
1312
|
-
found.push(
|
|
1849
|
+
found.push(join6(dir, entry.name));
|
|
1313
1850
|
}
|
|
1314
1851
|
}
|
|
1315
1852
|
} catch {
|
|
@@ -1369,13 +1906,23 @@ async function snapshot(workspaceId, roots, workspaceCwd) {
|
|
|
1369
1906
|
error: err.error
|
|
1370
1907
|
}));
|
|
1371
1908
|
const disabledExtensions = EXTENSIONS_ENABLED ? [] : await scanExtensionDirs(workspaceCwd);
|
|
1909
|
+
const disabledBuiltins = new Set(getDisabledBuiltins());
|
|
1910
|
+
const builtinExtensions = BUILTIN_EXTENSIONS.map((d) => ({
|
|
1911
|
+
id: d.id,
|
|
1912
|
+
name: d.name,
|
|
1913
|
+
description: d.description,
|
|
1914
|
+
enabled: !disabledBuiltins.has(d.id),
|
|
1915
|
+
tools: d.tools,
|
|
1916
|
+
commands: d.commands
|
|
1917
|
+
}));
|
|
1372
1918
|
return {
|
|
1373
1919
|
skills: skillsOut,
|
|
1374
1920
|
prompts: promptsOut,
|
|
1375
1921
|
extensionsEnabled: EXTENSIONS_ENABLED,
|
|
1376
1922
|
extensions: extensionsOut,
|
|
1377
1923
|
extensionErrors,
|
|
1378
|
-
disabledExtensions
|
|
1924
|
+
disabledExtensions,
|
|
1925
|
+
builtinExtensions
|
|
1379
1926
|
};
|
|
1380
1927
|
}
|
|
1381
1928
|
async function rootsFor(workspaceId) {
|
|
@@ -1607,6 +2154,33 @@ function mountResourcesRoute(app2) {
|
|
|
1607
2154
|
return respondError(c, err);
|
|
1608
2155
|
}
|
|
1609
2156
|
});
|
|
2157
|
+
app2.put("/:id/resources/builtin-extensions", async (c) => {
|
|
2158
|
+
const id = c.req.param("id");
|
|
2159
|
+
const ws = await getWorkspace(id);
|
|
2160
|
+
if (!ws) return c.json({ ok: false, error: "not found" }, 404);
|
|
2161
|
+
const body = await c.req.json().catch(() => null);
|
|
2162
|
+
if (!body || typeof body.id !== "string" || typeof body.enabled !== "boolean") {
|
|
2163
|
+
return c.json({ ok: false, error: "id and enabled are required" }, 400);
|
|
2164
|
+
}
|
|
2165
|
+
if (!BUILTIN_EXTENSIONS.some((d) => d.id === body.id)) {
|
|
2166
|
+
return c.json({ ok: false, error: `unknown builtin extension: ${body.id}` }, 400);
|
|
2167
|
+
}
|
|
2168
|
+
try {
|
|
2169
|
+
const runtime = await workspaceManager.getOrCreate(id);
|
|
2170
|
+
if (runtime.session.isStreaming) {
|
|
2171
|
+
return c.json(
|
|
2172
|
+
{ ok: false, error: "Stop the current turn before changing extensions" },
|
|
2173
|
+
409
|
|
2174
|
+
);
|
|
2175
|
+
}
|
|
2176
|
+
await setBuiltinEnabled(body.id, body.enabled);
|
|
2177
|
+
await runtime.session.reload();
|
|
2178
|
+
const { roots, workspaceCwd } = await rootsFor(id);
|
|
2179
|
+
return c.json(await snapshot(id, roots, workspaceCwd));
|
|
2180
|
+
} catch (err) {
|
|
2181
|
+
return respondError(c, err);
|
|
2182
|
+
}
|
|
2183
|
+
});
|
|
1610
2184
|
}
|
|
1611
2185
|
function isScope(value) {
|
|
1612
2186
|
return value === "user" || value === "project";
|
|
@@ -1733,7 +2307,7 @@ mountFilesRoute(workspacesRoute);
|
|
|
1733
2307
|
// src/api/fs.ts
|
|
1734
2308
|
import { readdir as readdir3 } from "fs/promises";
|
|
1735
2309
|
import { homedir as homedir2 } from "os";
|
|
1736
|
-
import { dirname as
|
|
2310
|
+
import { dirname as dirname4, isAbsolute as isAbsolute4, join as join7, resolve as resolve4 } from "path";
|
|
1737
2311
|
import { Hono as Hono2 } from "hono";
|
|
1738
2312
|
var fsRoute = new Hono2();
|
|
1739
2313
|
fsRoute.get("/browse", async (c) => {
|
|
@@ -1750,11 +2324,11 @@ fsRoute.get("/browse", async (c) => {
|
|
|
1750
2324
|
}
|
|
1751
2325
|
const entries = dirents.filter((d) => d.isDirectory()).filter((d) => showHidden || !d.name.startsWith(".")).map((d) => ({
|
|
1752
2326
|
name: d.name,
|
|
1753
|
-
path:
|
|
2327
|
+
path: join7(target, d.name),
|
|
1754
2328
|
type: "dir"
|
|
1755
2329
|
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
1756
2330
|
const parent = (() => {
|
|
1757
|
-
const p =
|
|
2331
|
+
const p = dirname4(target);
|
|
1758
2332
|
return p === target ? null : p;
|
|
1759
2333
|
})();
|
|
1760
2334
|
const body = { path: target, parent, entries };
|
|
@@ -1762,8 +2336,8 @@ fsRoute.get("/browse", async (c) => {
|
|
|
1762
2336
|
});
|
|
1763
2337
|
|
|
1764
2338
|
// src/api/model-configs.ts
|
|
1765
|
-
import { readFile as
|
|
1766
|
-
import { dirname as
|
|
2339
|
+
import { readFile as readFile4, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
|
|
2340
|
+
import { dirname as dirname5, join as join8 } from "path";
|
|
1767
2341
|
import { Hono as Hono3 } from "hono";
|
|
1768
2342
|
import {
|
|
1769
2343
|
getAgentDir as getAgentDir3
|
|
@@ -1778,11 +2352,11 @@ function withWriteLock(fn) {
|
|
|
1778
2352
|
return next;
|
|
1779
2353
|
}
|
|
1780
2354
|
function modelsPath() {
|
|
1781
|
-
return
|
|
2355
|
+
return join8(getAgentDir3(), "models.json");
|
|
1782
2356
|
}
|
|
1783
2357
|
async function readModelsJson() {
|
|
1784
2358
|
try {
|
|
1785
|
-
const raw = await
|
|
2359
|
+
const raw = await readFile4(modelsPath(), "utf-8");
|
|
1786
2360
|
return JSON.parse(raw);
|
|
1787
2361
|
} catch (err) {
|
|
1788
2362
|
if (err?.code === "ENOENT") {
|
|
@@ -1793,8 +2367,8 @@ async function readModelsJson() {
|
|
|
1793
2367
|
}
|
|
1794
2368
|
async function writeModelsJson(config2) {
|
|
1795
2369
|
const p = modelsPath();
|
|
1796
|
-
await
|
|
1797
|
-
await
|
|
2370
|
+
await mkdir4(dirname5(p), { recursive: true });
|
|
2371
|
+
await writeFile4(p, JSON.stringify(config2, null, 2), "utf-8");
|
|
1798
2372
|
}
|
|
1799
2373
|
var ValidationError = class extends Error {
|
|
1800
2374
|
constructor(message, status) {
|
|
@@ -1996,138 +2570,6 @@ modelConfigsRoute.delete("/providers/:provider/models/:modelId", async (c) => {
|
|
|
1996
2570
|
|
|
1997
2571
|
// src/ws/hub.ts
|
|
1998
2572
|
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
|
|
2131
2573
|
var replacementLocks = /* @__PURE__ */ new Map();
|
|
2132
2574
|
function withReplacementLock(workspaceId, fn) {
|
|
2133
2575
|
const prev = replacementLocks.get(workspaceId) ?? Promise.resolve();
|
|
@@ -2276,6 +2718,19 @@ async function handle(ws, state, msg) {
|
|
|
2276
2718
|
});
|
|
2277
2719
|
return;
|
|
2278
2720
|
}
|
|
2721
|
+
case "answer_question": {
|
|
2722
|
+
const wsId = state.workspaceId;
|
|
2723
|
+
if (!wsId) {
|
|
2724
|
+
send(ws, { type: "error", message: "not subscribed", command: "answer_question" });
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
if (replacementLocks.has(wsId)) return;
|
|
2728
|
+
const runtime = workspaceManager.get(wsId);
|
|
2729
|
+
if (!runtime) return;
|
|
2730
|
+
const activeFile = runtime.session.sessionFile ?? null;
|
|
2731
|
+
resolveAnswer(msg.toolCallId, msg.answer, activeFile);
|
|
2732
|
+
return;
|
|
2733
|
+
}
|
|
2279
2734
|
default: {
|
|
2280
2735
|
const _ = msg;
|
|
2281
2736
|
void _;
|
|
@@ -2345,6 +2800,25 @@ function bindCurrentSession(ws, state, workspaceId) {
|
|
|
2345
2800
|
sessionPath,
|
|
2346
2801
|
sessionId: session.sessionId
|
|
2347
2802
|
});
|
|
2803
|
+
const inFlight = inFlightAssistantSnapshot(runtime.session.state.streamingMessage);
|
|
2804
|
+
if (inFlight) {
|
|
2805
|
+
for (const payload of inFlight) {
|
|
2806
|
+
send(ws, {
|
|
2807
|
+
type: "event",
|
|
2808
|
+
workspaceId,
|
|
2809
|
+
sessionPath,
|
|
2810
|
+
payload
|
|
2811
|
+
});
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
for (const payload of inFlightToolCallsSnapshot(sessionPath)) {
|
|
2815
|
+
send(ws, {
|
|
2816
|
+
type: "event",
|
|
2817
|
+
workspaceId,
|
|
2818
|
+
sessionPath,
|
|
2819
|
+
payload
|
|
2820
|
+
});
|
|
2821
|
+
}
|
|
2348
2822
|
sendContextUsage(ws, runtime, workspaceId, sessionPath);
|
|
2349
2823
|
}
|
|
2350
2824
|
function sendContextUsage(ws, runtime, workspaceId, sessionPath) {
|
|
@@ -2379,10 +2853,11 @@ function send(ws, msg) {
|
|
|
2379
2853
|
}
|
|
2380
2854
|
|
|
2381
2855
|
// src/index.ts
|
|
2856
|
+
configureHttpProxy();
|
|
2382
2857
|
var app = new Hono4();
|
|
2383
|
-
var distDir =
|
|
2384
|
-
var webRoot = resolve5(process.env.PI_PILOT_WEB_ROOT ??
|
|
2385
|
-
var webIndexPath =
|
|
2858
|
+
var distDir = dirname6(fileURLToPath(import.meta.url));
|
|
2859
|
+
var webRoot = resolve5(process.env.PI_PILOT_WEB_ROOT ?? join9(distDir, "..", "public"));
|
|
2860
|
+
var webIndexPath = join9(webRoot, "index.html");
|
|
2386
2861
|
var mimeTypes = {
|
|
2387
2862
|
".css": "text/css; charset=utf-8",
|
|
2388
2863
|
".html": "text/html; charset=utf-8",
|
|
@@ -2416,7 +2891,7 @@ function safeResolveWebPath(pathname) {
|
|
|
2416
2891
|
}
|
|
2417
2892
|
async function readWebFile(path) {
|
|
2418
2893
|
try {
|
|
2419
|
-
return await
|
|
2894
|
+
return await readFile5(path);
|
|
2420
2895
|
} catch (err) {
|
|
2421
2896
|
const code = err.code;
|
|
2422
2897
|
if (code === "ENOENT" || code === "EISDIR") return void 0;
|
|
@@ -2429,7 +2904,7 @@ async function serveWeb(c) {
|
|
|
2429
2904
|
const assetPath = safeResolveWebPath(pathname);
|
|
2430
2905
|
if (!assetPath) return c.text("invalid asset path", 400);
|
|
2431
2906
|
const asset = await readWebFile(assetPath);
|
|
2432
|
-
const body = asset ?? await
|
|
2907
|
+
const body = asset ?? await readFile5(webIndexPath);
|
|
2433
2908
|
const filePath = asset ? assetPath : webIndexPath;
|
|
2434
2909
|
const headers = {
|
|
2435
2910
|
"Content-Type": mimeTypes[extname(filePath)] ?? "application/octet-stream",
|
|
@@ -2459,6 +2934,7 @@ if (existsSync(webIndexPath)) {
|
|
|
2459
2934
|
)
|
|
2460
2935
|
);
|
|
2461
2936
|
}
|
|
2937
|
+
await loadBuiltinPrefs();
|
|
2462
2938
|
var server = serve(
|
|
2463
2939
|
{
|
|
2464
2940
|
fetch: app.fetch,
|